Some checks failed
Build and Deploy / build (push) Has been cancelled
- Fix checkoutLinesDelete mutation: use 'id' param and 'linesIds' instead of 'lineIds' - Fix viewport metadata warning: move to separate viewport export in layout.tsx - Add sizes prop to checkout Image with fill - Fix CartDrawer init checkout useEffect to prevent re-render loops - Various product detail improvements
322 lines
9.0 KiB
TypeScript
322 lines
9.0 KiB
TypeScript
"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";
|
|
|
|
// GraphQL Response Types
|
|
interface CheckoutCreateResponse {
|
|
checkoutCreate?: {
|
|
checkout?: Checkout;
|
|
errors?: Array<{ message: string }>;
|
|
};
|
|
}
|
|
|
|
interface CheckoutLinesAddResponse {
|
|
checkoutLinesAdd?: {
|
|
checkout?: Checkout;
|
|
errors?: Array<{ message: string }>;
|
|
};
|
|
}
|
|
|
|
interface CheckoutLinesUpdateResponse {
|
|
checkoutLinesUpdate?: {
|
|
checkout?: Checkout;
|
|
errors?: Array<{ message: string }>;
|
|
};
|
|
}
|
|
|
|
interface CheckoutLinesDeleteResponse {
|
|
checkoutLinesDelete?: {
|
|
checkout?: Checkout;
|
|
errors?: Array<{ message: string }>;
|
|
};
|
|
}
|
|
|
|
interface CheckoutEmailUpdateResponse {
|
|
checkoutEmailUpdate?: {
|
|
checkout?: Checkout;
|
|
errors?: Array<{ message: string }>;
|
|
};
|
|
}
|
|
|
|
interface GetCheckoutResponse {
|
|
checkout?: Checkout;
|
|
}
|
|
|
|
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<GetCheckoutResponse>({
|
|
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<CheckoutCreateResponse>({
|
|
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<CheckoutLinesAddResponse>({
|
|
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 && 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<CheckoutLinesUpdateResponse>({
|
|
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 && 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<CheckoutLinesDeleteResponse>({
|
|
mutation: CHECKOUT_LINES_DELETE,
|
|
variables: {
|
|
id: checkout.id,
|
|
linesIds: [lineId],
|
|
},
|
|
});
|
|
|
|
if (data?.checkoutLinesDelete?.checkout) {
|
|
set({
|
|
checkout: data.checkoutLinesDelete.checkout,
|
|
isLoading: false,
|
|
});
|
|
} else if (data?.checkoutLinesDelete?.errors && 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<CheckoutEmailUpdateResponse>({
|
|
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 && 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<GetCheckoutResponse>({
|
|
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,
|
|
}),
|
|
}
|
|
)
|
|
);
|