- Add Apollo Client for Saleor GraphQL API - Create GraphQL fragments (Product, Variant, Checkout) - Create GraphQL queries (Products, Checkout) - Create GraphQL mutations (Checkout operations) - Add TypeScript types for Saleor entities - Add product helper functions - Install @apollo/client and graphql dependencies Part of WordPress/WooCommerce → Saleor migration
467 lines
12 KiB
Markdown
467 lines
12 KiB
Markdown
# Mautic Abandoned Cart Recovery Setup
|
|
|
|
## Overview
|
|
Use your existing Mautic instance for abandoned cart recovery instead of paying for Klaviyo.
|
|
|
|
**Mautic URL:** https://mautic.nodecrew.me
|
|
|
|
## How It Works
|
|
|
|
```
|
|
1. User adds item to cart
|
|
↓
|
|
2. Storefront sends event to Mautic (via API or tracking pixel)
|
|
↓
|
|
3. Mautic creates/updates contact with cart data
|
|
↓
|
|
4. Campaign waits 1 hour
|
|
↓
|
|
5. If no purchase → Send abandoned cart email
|
|
↓
|
|
6. User clicks email → Cart restored → Convert!
|
|
```
|
|
|
|
## Step 1: Set Up Mautic Tracking
|
|
|
|
### Option A: Mautic Tracking Pixel (JavaScript)
|
|
|
|
Add to your Next.js storefront:
|
|
|
|
```typescript
|
|
// lib/mautic.ts
|
|
export function trackAddToCart(product: any, quantity: number) {
|
|
if (typeof window !== 'undefined' && (window as any).mt) {
|
|
(window as any).mt('send', 'pageview', {
|
|
page_title: `Added to Cart: ${product.name}`,
|
|
page_url: window.location.href,
|
|
product_name: product.name,
|
|
product_sku: product.variants[0]?.sku,
|
|
product_price: product.variants[0]?.channelListings[0]?.price?.amount,
|
|
quantity: quantity,
|
|
event: 'add_to_cart'
|
|
});
|
|
}
|
|
}
|
|
|
|
export function trackCheckoutStarted(checkout: any) {
|
|
if (typeof window !== 'undefined' && (window as any).mt) {
|
|
(window as any).mt('send', 'pageview', {
|
|
page_title: 'Checkout Started',
|
|
page_url: window.location.href,
|
|
checkout_value: checkout.totalPrice?.amount,
|
|
checkout_id: checkout.id,
|
|
event: 'checkout_started'
|
|
});
|
|
}
|
|
}
|
|
|
|
export function trackOrderCompleted(order: any) {
|
|
if (typeof window !== 'undefined' && (window as any).mt) {
|
|
(window as any).mt('send', 'pageview', {
|
|
page_title: 'Order Completed',
|
|
page_url: window.location.href,
|
|
order_total: order.total.gross.amount,
|
|
order_id: order.id,
|
|
event: 'purchase_completed'
|
|
});
|
|
}
|
|
}
|
|
```
|
|
|
|
```typescript
|
|
// pages/_app.tsx or layout.tsx
|
|
import Script from 'next/script';
|
|
|
|
export default function RootLayout({ children }) {
|
|
return (
|
|
<html>
|
|
<head>
|
|
{/* Mautic Tracking */}
|
|
<Script
|
|
id="mautic-tracking"
|
|
strategy="afterInteractive"
|
|
dangerouslySetInnerHTML={{
|
|
__html: `
|
|
(function(w,d,t,u,n,a,m){
|
|
w['MauticTrackingObject']=n;
|
|
w[n]=w[n]||function(){(w[n].q=w[n].q||[]).push(arguments)},a=d.createElement(t),
|
|
m=d.getElementsByTagName(t)[0];a.async=1;a.src=u;m.parentNode.insertBefore(a,m)
|
|
})(window,document,'script','https://mautic.nodecrew.me/mtc.js','mt');
|
|
|
|
mt('send', 'pageview');
|
|
`
|
|
}}
|
|
/>
|
|
</head>
|
|
<body>{children}</body>
|
|
</html>
|
|
);
|
|
}
|
|
```
|
|
|
|
### Option B: Direct Mautic API Integration
|
|
|
|
More reliable for e-commerce events:
|
|
|
|
```typescript
|
|
// lib/mautic-api.ts
|
|
const MAUTIC_URL = 'https://mautic.nodecrew.me';
|
|
const MAUTIC_USERNAME = process.env.MAUTIC_API_USER;
|
|
const MAUTIC_PASSWORD = process.env.MAUTIC_API_PASS;
|
|
|
|
export async function createOrUpdateContact(email: string, data: any) {
|
|
const response = await fetch(`${MAUTIC_URL}/api/contacts/new`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Authorization': `Basic ${Buffer.from(`${MAUTIC_USERNAME}:${MAUTIC_PASSWORD}`).toString('base64')}`,
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
email: email,
|
|
firstname: data.firstName,
|
|
lastname: data.lastName,
|
|
phone: data.phone,
|
|
// Custom fields for cart
|
|
cart_items: JSON.stringify(data.cartItems),
|
|
cart_value: data.cartValue,
|
|
cart_abandoned: true,
|
|
cart_abandoned_at: new Date().toISOString(),
|
|
last_product_added: data.lastProductName,
|
|
}),
|
|
});
|
|
|
|
return response.json();
|
|
}
|
|
|
|
export async function trackCartAbandoned(email: string, checkout: any) {
|
|
return createOrUpdateContact(email, {
|
|
cartItems: checkout.lines.map((line: any) => ({
|
|
name: line.variant.name,
|
|
quantity: line.quantity,
|
|
price: line.totalPrice.gross.amount,
|
|
})),
|
|
cartValue: checkout.totalPrice.gross.amount,
|
|
lastProductName: checkout.lines[0]?.variant.name,
|
|
});
|
|
}
|
|
|
|
export async function markCartRecovered(email: string) {
|
|
const response = await fetch(`${MAUTIC_URL}/api/contacts/edit`, {
|
|
method: 'PATCH',
|
|
headers: {
|
|
'Authorization': `Basic ${Buffer.from(`${MAUTIC_USERNAME}:${MAUTIC_PASSWORD}`).toString('base64')}`,
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
email: email,
|
|
cart_abandoned: false,
|
|
cart_recovered: true,
|
|
cart_recovered_at: new Date().toISOString(),
|
|
}),
|
|
});
|
|
|
|
return response.json();
|
|
}
|
|
```
|
|
|
|
## Step 2: Create Custom Fields in Mautic
|
|
|
|
1. Go to https://mautic.nodecrew.me
|
|
2. Settings → Custom Fields
|
|
3. Create these fields:
|
|
|
|
| Field Label | Alias | Data Type | Default Value |
|
|
|-------------|-------|-----------|---------------|
|
|
| Cart Items | cart_items | Text | |
|
|
| Cart Value | cart_value | Number | 0 |
|
|
| Cart Abandoned | cart_abandoned | Boolean | false |
|
|
| Cart Abandoned At | cart_abandoned_at | Date/Time | |
|
|
| Last Product Added | last_product_added | Text | |
|
|
| Cart Recovered | cart_recovered | Boolean | false |
|
|
|
|
## Step 3: Create Segments
|
|
|
|
### Segment 1: Abandoned Cart (1 hour)
|
|
|
|
1. Segments → New
|
|
2. Name: "Abandoned Cart - 1 Hour"
|
|
3. Filters:
|
|
- Cart Abandoned = true
|
|
- Cart Abandoned At > 1 hour ago
|
|
- Cart Recovered = false
|
|
- Email = not empty
|
|
|
|
### Segment 2: Abandoned Cart (24 hours)
|
|
|
|
1. Segments → New
|
|
2. Name: "Abandoned Cart - 24 Hours"
|
|
3. Filters:
|
|
- Cart Abandoned = true
|
|
- Cart Abandoned At > 1 day ago
|
|
- Cart Recovered = false
|
|
- Email = not empty
|
|
|
|
## Step 4: Create Email Templates
|
|
|
|
### Email 1: First Reminder (1 hour)
|
|
|
|
**Subject:** Zaboravili ste nešto u korpi / You left something in your cart
|
|
|
|
```html
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
</head>
|
|
<body>
|
|
<!-- Serbian Version -->
|
|
<h2>Zdravo {contactfield=firstname},</h2>
|
|
<p>Primijetili smo da ste ostavili artikle u korpi:</p>
|
|
|
|
<!-- Cart Items (you'd dynamically insert these) -->
|
|
<div style="border: 1px solid #ddd; padding: 15px; margin: 15px 0;">
|
|
<p><strong>Poslednji proizvod:</strong> {contactfield=last_product_added}</p>
|
|
<p><strong>Vrednost korpe:</strong> {contactfield=cart_value} USD</p>
|
|
</div>
|
|
|
|
<a href="https://dev.manoonoils.com/cart-recovery?email={contactfield=email}"
|
|
style="background: #007bff; color: white; padding: 12px 24px; text-decoration: none; display: inline-block;">
|
|
Završite kupovinu
|
|
</a>
|
|
|
|
<hr>
|
|
|
|
<!-- English Version -->
|
|
<h2>Hello {contactfield=firstname},</h2>
|
|
<p>We noticed you left items in your cart:</p>
|
|
|
|
<div style="border: 1px solid #ddd; padding: 15px; margin: 15px 0;">
|
|
<p><strong>Last product:</strong> {contactfield=last_product_added}</p>
|
|
<p><strong>Cart value:</strong> {contactfield=cart_value} USD</p>
|
|
</div>
|
|
|
|
<a href="https://dev.manoonoils.com/cart-recovery?email={contactfield=email}"
|
|
style="background: #007bff; color: white; padding: 12px 24px; text-decoration: none; display: inline-block;">
|
|
Complete Purchase
|
|
</a>
|
|
</body>
|
|
</html>
|
|
```
|
|
|
|
### Email 2: Second Reminder (24 hours) - With Discount
|
|
|
|
**Subject:** Još uvijek čekamo! / Still waiting for you! (10% off)
|
|
|
|
```html
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<body>
|
|
<h2>Hej {contactfield=firstname},</h2>
|
|
<p>Vaša korpa još uvijek čeka! Dajemo vam <strong>10% popusta</strong> da završite kupovinu:</p>
|
|
|
|
<p>Koristite kod: <strong>COMEBACK10</strong></p>
|
|
|
|
<a href="https://dev.manoonoils.com/cart-recovery?email={contactfield=email}&coupon=COMEBACK10"
|
|
style="background: #28a745; color: white; padding: 12px 24px; text-decoration: none; display: inline-block;">
|
|
Završite kupovinu sa 10% popusta
|
|
</a>
|
|
|
|
<hr>
|
|
|
|
<h2>Hey {contactfield=firstname},</h2>
|
|
<p>Your cart is still waiting! Here's <strong>10% off</strong> to complete your purchase:</p>
|
|
|
|
<p>Use code: <strong>COMEBACK10</strong></p>
|
|
|
|
<a href="https://dev.manoonoils.com/cart-recovery?email={contactfield=email}&coupon=COMEBACK10"
|
|
style="background: #28a745; color: white; padding: 12px 24px; text-decoration: none; display: inline-block;">
|
|
Complete Purchase with 10% Off
|
|
</a>
|
|
</body>
|
|
</html>
|
|
```
|
|
|
|
## Step 5: Create Campaign
|
|
|
|
### Campaign Workflow
|
|
|
|
1. **Campaigns → New**
|
|
2. Name: "Abandoned Cart Recovery"
|
|
3. Description: "Recover abandoned carts with 2-email sequence"
|
|
|
|
**Campaign Canvas:**
|
|
|
|
```
|
|
[Contact enters campaign]
|
|
↓
|
|
[Decision: Cart Abandoned?]
|
|
↓ Yes
|
|
[Wait: 1 hour]
|
|
↓
|
|
[Send Email: First Reminder]
|
|
↓
|
|
[Wait: 23 hours]
|
|
↓
|
|
[Decision: Cart Recovered?]
|
|
↓ No
|
|
[Send Email: Second Reminder + 10% off]
|
|
↓
|
|
[Wait: 3 days]
|
|
↓
|
|
[Decision: Cart Recovered?]
|
|
↓ No
|
|
[Remove from campaign]
|
|
```
|
|
|
|
### Campaign Settings
|
|
|
|
**Entry Conditions:**
|
|
- Contact added to segment "Abandoned Cart - 1 Hour"
|
|
|
|
**Exit Conditions:**
|
|
- Cart Recovered = true
|
|
- Order Completed event triggered
|
|
|
|
## Step 6: Cart Recovery Page
|
|
|
|
Create a recovery page in Next.js:
|
|
|
|
```typescript
|
|
// pages/cart-recovery.tsx
|
|
import { useEffect } from 'react';
|
|
import { useRouter } from 'next/router';
|
|
import { saleorClient } from '@/lib/saleor/client';
|
|
import { gql } from '@apollo/client';
|
|
import { markCartRecovered } from '@/lib/mautic-api';
|
|
|
|
const GET_CHECKOUT_BY_EMAIL = gql`
|
|
query GetCheckoutByEmail($email: String!) {
|
|
checkouts(first: 1, filter: {customer: $email}) {
|
|
edges {
|
|
node {
|
|
id
|
|
token
|
|
lines {
|
|
id
|
|
quantity
|
|
variant {
|
|
id
|
|
name
|
|
product {
|
|
name
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`;
|
|
|
|
export default function CartRecoveryPage() {
|
|
const router = useRouter();
|
|
const { email, coupon } = router.query;
|
|
|
|
useEffect(() => {
|
|
if (email) {
|
|
// Mark cart as recovered in Mautic
|
|
markCartRecovered(email as string);
|
|
|
|
// Redirect to checkout with recovered cart
|
|
// You'll need to implement checkout restoration logic
|
|
router.push(`/checkout?email=${email}${coupon ? `&coupon=${coupon}` : ''}`);
|
|
}
|
|
}, [email, coupon, router]);
|
|
|
|
return (
|
|
<div style={{ textAlign: 'center', padding: '50px' }}>
|
|
<p>Restoring your cart...</p>
|
|
</div>
|
|
);
|
|
}
|
|
```
|
|
|
|
## Step 7: Storefront Integration
|
|
|
|
Add tracking to your add-to-cart and checkout flows:
|
|
|
|
```typescript
|
|
// components/AddToCartButton.tsx
|
|
import { trackAddToCart } from '@/lib/mautic';
|
|
import { trackCartAbandoned } from '@/lib/mautic-api';
|
|
|
|
export function AddToCartButton({ product, variant, quantity }) {
|
|
const handleAddToCart = async () => {
|
|
// Add to Saleor cart
|
|
await addToCart(variant.id, quantity);
|
|
|
|
// Track in Mautic
|
|
trackAddToCart(product, quantity);
|
|
};
|
|
|
|
return <button onClick={handleAddToCart}>Add to Cart</button>;
|
|
}
|
|
```
|
|
|
|
```typescript
|
|
// components/CheckoutForm.tsx
|
|
import { trackCheckoutStarted } from '@/lib/mautic';
|
|
|
|
export function CheckoutForm({ checkout, email }) {
|
|
useEffect(() => {
|
|
if (checkout && email) {
|
|
trackCheckoutStarted(checkout);
|
|
}
|
|
}, [checkout, email]);
|
|
|
|
// Track abandonment when user leaves
|
|
useEffect(() => {
|
|
const handleBeforeUnload = () => {
|
|
if (!orderCompleted && checkout) {
|
|
trackCartAbandoned(email, checkout);
|
|
}
|
|
};
|
|
|
|
window.addEventListener('beforeunload', handleBeforeUnload);
|
|
return () => window.removeEventListener('beforeunload', handleBeforeUnload);
|
|
}, [checkout, email, orderCompleted]);
|
|
|
|
return <form>...</form>;
|
|
}
|
|
```
|
|
|
|
## Testing
|
|
|
|
1. **Add item to cart** on storefront
|
|
2. **Enter email** at checkout
|
|
3. **Close browser** (don't complete purchase)
|
|
4. **Wait 1 hour**
|
|
5. **Check Mautic** → Contact should have cart_abandoned = true
|
|
6. **Check email** → Should receive first reminder
|
|
|
|
## Monitoring
|
|
|
|
Track campaign performance in Mautic:
|
|
- **Emails Sent**
|
|
- **Open Rate**
|
|
- **Click Rate**
|
|
- **Conversion Rate** (cart recovered)
|
|
- **Revenue Generated**
|
|
|
|
## Cost Comparison
|
|
|
|
| Solution | Monthly Cost | Setup Time |
|
|
|----------|-------------|------------|
|
|
| **Mautic** (existing) | FREE | 2-3 days |
|
|
| Klaviyo | $20-50+ | 1 day |
|
|
| Custom Build | FREE | 2-4 weeks |
|
|
|
|
## Summary
|
|
|
|
✅ **Mautic CAN do abandoned cart recovery**
|
|
✅ **Use your existing instance = FREE**
|
|
⚠️ **Requires custom integration work**
|
|
⚠️ **Email templates need manual setup**
|
|
|
|
**Recommendation:** Since you already pay for Mautic hosting, use it for abandoned cart instead of paying for Klaviyo. The setup is moderate complexity but saves $20-50/month.
|