feat(saleor): Phase 3 - Cart Migration
- Create Saleor checkout store (Zustand + persist) - Update CartDrawer to use Saleor checkout - Update Header to use Saleor checkout store - Update ProductDetail with Add to Cart functionality - Add checkout initialization on app load - Handle checkout line add/update/delete operations - Add error handling and loading states
This commit is contained in:
281
src/stores/saleorCheckoutStore.ts
Normal file
281
src/stores/saleorCheckoutStore.ts
Normal file
@@ -0,0 +1,281 @@
|
||||
"use client";
|
||||
|
||||
import { create } from "zustand";
|
||||
import { persist } from "zustand/middleware";
|
||||
import { saleorClient } from "@/lib/saleor/client";
|
||||
import {
|
||||
CHECKOUT_CREATE,
|
||||
CHECKOUT_LINES_ADD,
|
||||
CHECKOUT_LINES_UPDATE,
|
||||
CHECKOUT_LINES_DELETE,
|
||||
CHECKOUT_EMAIL_UPDATE,
|
||||
} from "@/lib/saleor/mutations/Checkout";
|
||||
import { GET_CHECKOUT } from "@/lib/saleor/queries/Checkout";
|
||||
import type { Checkout, CheckoutLine } from "@/types/saleor";
|
||||
|
||||
const CHANNEL = process.env.NEXT_PUBLIC_SALEOR_CHANNEL || "default-channel";
|
||||
|
||||
interface SaleorCheckoutStore {
|
||||
checkout: Checkout | null;
|
||||
checkoutToken: string | null;
|
||||
isOpen: boolean;
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
|
||||
// Actions
|
||||
initCheckout: () => Promise<void>;
|
||||
addLine: (variantId: string, quantity: number) => Promise<void>;
|
||||
updateLine: (lineId: string, quantity: number) => Promise<void>;
|
||||
removeLine: (lineId: string) => Promise<void>;
|
||||
setEmail: (email: string) => Promise<void>;
|
||||
refreshCheckout: () => Promise<void>;
|
||||
toggleCart: () => void;
|
||||
openCart: () => void;
|
||||
closeCart: () => void;
|
||||
clearError: () => void;
|
||||
|
||||
// Getters
|
||||
getLineCount: () => number;
|
||||
getTotal: () => number;
|
||||
getLines: () => CheckoutLine[];
|
||||
}
|
||||
|
||||
export const useSaleorCheckoutStore = create<SaleorCheckoutStore>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
checkout: null,
|
||||
checkoutToken: null,
|
||||
isOpen: false,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
|
||||
initCheckout: async () => {
|
||||
const { checkoutToken } = get();
|
||||
|
||||
if (checkoutToken) {
|
||||
// Try to fetch existing checkout
|
||||
try {
|
||||
const { data } = await saleorClient.query({
|
||||
query: GET_CHECKOUT,
|
||||
variables: { token: checkoutToken },
|
||||
});
|
||||
|
||||
if (data?.checkout) {
|
||||
set({ checkout: data.checkout });
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
// Checkout not found or expired, create new one
|
||||
}
|
||||
}
|
||||
|
||||
// Create new checkout
|
||||
try {
|
||||
const { data } = await saleorClient.mutate({
|
||||
mutation: CHECKOUT_CREATE,
|
||||
variables: {
|
||||
input: {
|
||||
channel: CHANNEL,
|
||||
lines: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (data?.checkoutCreate?.checkout) {
|
||||
set({
|
||||
checkout: data.checkoutCreate.checkout,
|
||||
checkoutToken: data.checkoutCreate.checkout.token,
|
||||
});
|
||||
}
|
||||
} catch (e: any) {
|
||||
set({ error: e.message });
|
||||
}
|
||||
},
|
||||
|
||||
addLine: async (variantId: string, quantity: number) => {
|
||||
set({ isLoading: true, error: null });
|
||||
|
||||
try {
|
||||
let { checkout, checkoutToken } = get();
|
||||
|
||||
// Initialize checkout if needed
|
||||
if (!checkout) {
|
||||
await get().initCheckout();
|
||||
checkout = get().checkout;
|
||||
checkoutToken = get().checkoutToken;
|
||||
}
|
||||
|
||||
if (!checkout) {
|
||||
throw new Error("Failed to initialize checkout");
|
||||
}
|
||||
|
||||
const { data } = await saleorClient.mutate({
|
||||
mutation: CHECKOUT_LINES_ADD,
|
||||
variables: {
|
||||
checkoutId: checkout.id,
|
||||
lines: [{ variantId, quantity }],
|
||||
},
|
||||
});
|
||||
|
||||
if (data?.checkoutLinesAdd?.checkout) {
|
||||
set({
|
||||
checkout: data.checkoutLinesAdd.checkout,
|
||||
isOpen: true,
|
||||
isLoading: false,
|
||||
});
|
||||
} else if (data?.checkoutLinesAdd?.errors?.length > 0) {
|
||||
throw new Error(data.checkoutLinesAdd.errors[0].message);
|
||||
}
|
||||
} catch (e: any) {
|
||||
set({ error: e.message, isLoading: false });
|
||||
}
|
||||
},
|
||||
|
||||
updateLine: async (lineId: string, quantity: number) => {
|
||||
set({ isLoading: true, error: null });
|
||||
|
||||
try {
|
||||
const { checkout } = get();
|
||||
|
||||
if (!checkout) {
|
||||
throw new Error("No active checkout");
|
||||
}
|
||||
|
||||
if (quantity <= 0) {
|
||||
// Remove line if quantity is 0 or less
|
||||
await get().removeLine(lineId);
|
||||
return;
|
||||
}
|
||||
|
||||
const { data } = await saleorClient.mutate({
|
||||
mutation: CHECKOUT_LINES_UPDATE,
|
||||
variables: {
|
||||
checkoutId: checkout.id,
|
||||
lines: [{ lineId, quantity }],
|
||||
},
|
||||
});
|
||||
|
||||
if (data?.checkoutLinesUpdate?.checkout) {
|
||||
set({
|
||||
checkout: data.checkoutLinesUpdate.checkout,
|
||||
isLoading: false,
|
||||
});
|
||||
} else if (data?.checkoutLinesUpdate?.errors?.length > 0) {
|
||||
throw new Error(data.checkoutLinesUpdate.errors[0].message);
|
||||
}
|
||||
} catch (e: any) {
|
||||
set({ error: e.message, isLoading: false });
|
||||
}
|
||||
},
|
||||
|
||||
removeLine: async (lineId: string) => {
|
||||
set({ isLoading: true, error: null });
|
||||
|
||||
try {
|
||||
const { checkout } = get();
|
||||
|
||||
if (!checkout) {
|
||||
throw new Error("No active checkout");
|
||||
}
|
||||
|
||||
const { data } = await saleorClient.mutate({
|
||||
mutation: CHECKOUT_LINES_DELETE,
|
||||
variables: {
|
||||
checkoutId: checkout.id,
|
||||
lineIds: [lineId],
|
||||
},
|
||||
});
|
||||
|
||||
if (data?.checkoutLinesDelete?.checkout) {
|
||||
set({
|
||||
checkout: data.checkoutLinesDelete.checkout,
|
||||
isLoading: false,
|
||||
});
|
||||
} else if (data?.checkoutLinesDelete?.errors?.length > 0) {
|
||||
throw new Error(data.checkoutLinesDelete.errors[0].message);
|
||||
}
|
||||
} catch (e: any) {
|
||||
set({ error: e.message, isLoading: false });
|
||||
}
|
||||
},
|
||||
|
||||
setEmail: async (email: string) => {
|
||||
set({ isLoading: true, error: null });
|
||||
|
||||
try {
|
||||
const { checkout } = get();
|
||||
|
||||
if (!checkout) {
|
||||
throw new Error("No active checkout");
|
||||
}
|
||||
|
||||
const { data } = await saleorClient.mutate({
|
||||
mutation: CHECKOUT_EMAIL_UPDATE,
|
||||
variables: {
|
||||
checkoutId: checkout.id,
|
||||
email,
|
||||
},
|
||||
});
|
||||
|
||||
if (data?.checkoutEmailUpdate?.checkout) {
|
||||
set({
|
||||
checkout: data.checkoutEmailUpdate.checkout,
|
||||
isLoading: false,
|
||||
});
|
||||
} else if (data?.checkoutEmailUpdate?.errors?.length > 0) {
|
||||
throw new Error(data.checkoutEmailUpdate.errors[0].message);
|
||||
}
|
||||
} catch (e: any) {
|
||||
set({ error: e.message, isLoading: false });
|
||||
}
|
||||
},
|
||||
|
||||
refreshCheckout: async () => {
|
||||
const { checkoutToken } = get();
|
||||
|
||||
if (!checkoutToken) return;
|
||||
|
||||
try {
|
||||
const { data } = await saleorClient.query({
|
||||
query: GET_CHECKOUT,
|
||||
variables: { token: checkoutToken },
|
||||
});
|
||||
|
||||
if (data?.checkout) {
|
||||
set({ checkout: data.checkout });
|
||||
}
|
||||
} catch (e) {
|
||||
// Checkout might be expired
|
||||
set({ checkout: null, checkoutToken: null });
|
||||
}
|
||||
},
|
||||
|
||||
toggleCart: () => set((state) => ({ isOpen: !state.isOpen })),
|
||||
openCart: () => set({ isOpen: true }),
|
||||
closeCart: () => set({ isOpen: false }),
|
||||
clearError: () => set({ error: null }),
|
||||
|
||||
getLineCount: () => {
|
||||
const { checkout } = get();
|
||||
if (!checkout?.lines) return 0;
|
||||
return checkout.lines.reduce((count, line) => count + line.quantity, 0);
|
||||
},
|
||||
|
||||
getTotal: () => {
|
||||
const { checkout } = get();
|
||||
return checkout?.totalPrice?.gross?.amount || 0;
|
||||
},
|
||||
|
||||
getLines: () => {
|
||||
const { checkout } = get();
|
||||
return checkout?.lines || [];
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: "manoonoils-saleor-checkout",
|
||||
partialize: (state) => ({
|
||||
checkoutToken: state.checkoutToken,
|
||||
}),
|
||||
}
|
||||
)
|
||||
);
|
||||
Reference in New Issue
Block a user