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:
Unchained
2026-03-21 12:36:21 +02:00
parent db1914d69b
commit 7b94537670
27 changed files with 7879 additions and 3 deletions

466
mautic-abandoned-cart.md Normal file
View 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.