feat(saleor): Phase 1 - GraphQL Client Setup
- 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
This commit is contained in:
466
mautic-abandoned-cart.md
Normal file
466
mautic-abandoned-cart.md
Normal file
@@ -0,0 +1,466 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user