forked from hazem/yslootahrobotics
162 lines
4.4 KiB
TypeScript
162 lines
4.4 KiB
TypeScript
import { createStore } from 'zustand/vanilla';
|
|
import { useSyncExternalStore } from 'react';
|
|
|
|
export type CheckoutStep = 'config' | 'shipping' | 'payment' | 'review' | 'confirmed';
|
|
|
|
export interface ShippingInfo {
|
|
name: string;
|
|
email: string;
|
|
phone: string;
|
|
address: string;
|
|
city: string;
|
|
country: string;
|
|
postalCode: string;
|
|
}
|
|
|
|
export interface PaymentInfo {
|
|
paymentIntentId: string;
|
|
clientSecret: string;
|
|
status: 'idle' | 'processing' | 'succeeded' | 'failed';
|
|
errorMessage: string;
|
|
}
|
|
|
|
export interface PriceLineItem {
|
|
label: string;
|
|
price: number;
|
|
}
|
|
|
|
export interface OrderState {
|
|
step: CheckoutStep;
|
|
shipping: ShippingInfo;
|
|
payment: PaymentInfo;
|
|
orderId: string;
|
|
orderTotal: number;
|
|
personaSummary: string;
|
|
colorSummary: string;
|
|
priceItems: PriceLineItem[];
|
|
}
|
|
|
|
export interface OrderActions {
|
|
setStep: (step: CheckoutStep) => void;
|
|
setShipping: (shipping: ShippingInfo) => void;
|
|
setPayment: (payment: Partial<PaymentInfo>) => void;
|
|
setOrderTotal: (total: number) => void;
|
|
setConfigSummary: (persona: string, color: string, priceItems?: PriceLineItem[]) => void;
|
|
createPaymentIntent: () => Promise<string | null>;
|
|
placeOrder: () => void;
|
|
resetOrder: () => void;
|
|
}
|
|
|
|
export type OrderStore = OrderState & OrderActions;
|
|
|
|
const emptyShipping: ShippingInfo = {
|
|
name: '',
|
|
email: '',
|
|
phone: '',
|
|
address: '',
|
|
city: '',
|
|
country: '',
|
|
postalCode: '',
|
|
};
|
|
|
|
const emptyPayment: PaymentInfo = {
|
|
paymentIntentId: '',
|
|
clientSecret: '',
|
|
status: 'idle',
|
|
errorMessage: '',
|
|
};
|
|
|
|
const defaultState: OrderState = {
|
|
step: 'config',
|
|
shipping: emptyShipping,
|
|
payment: emptyPayment,
|
|
orderId: '',
|
|
orderTotal: 0,
|
|
personaSummary: '',
|
|
colorSummary: '',
|
|
priceItems: [],
|
|
};
|
|
|
|
function generateOrderId(): string {
|
|
const timestamp = Date.now().toString(36).toUpperCase();
|
|
const random = Math.random().toString(36).substring(2, 6).toUpperCase();
|
|
return `LR-G1-${timestamp}${random}`;
|
|
}
|
|
|
|
export const orderStore = createStore<OrderStore>((set) => ({
|
|
...defaultState,
|
|
|
|
setStep: (step: CheckoutStep) => set({ step }),
|
|
|
|
setShipping: (shipping: ShippingInfo) => set({ shipping }),
|
|
|
|
setPayment: (payment: Partial<PaymentInfo>) => set((state) => ({
|
|
payment: { ...state.payment, ...payment },
|
|
})),
|
|
|
|
setOrderTotal: (total: number) => set({ orderTotal: total }),
|
|
|
|
setConfigSummary: (persona: string, color: string, priceItems: PriceLineItem[] = []) => set({
|
|
personaSummary: persona,
|
|
colorSummary: color,
|
|
priceItems,
|
|
}),
|
|
|
|
createPaymentIntent: async (): Promise<string | null> => {
|
|
const { orderTotal, personaSummary, colorSummary, shipping, priceItems } = orderStore.getState();
|
|
try {
|
|
const res: Response = await fetch('/api/create-payment-intent/', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
amount: orderTotal,
|
|
currency: 'aed',
|
|
receiptEmail: shipping.email || undefined,
|
|
metadata: {
|
|
persona: personaSummary,
|
|
color: colorSummary,
|
|
priceItems: JSON.stringify(priceItems),
|
|
customerName: shipping.name,
|
|
customerEmail: shipping.email,
|
|
customerPhone: shipping.phone,
|
|
customerAddress: shipping.address,
|
|
customerCity: shipping.city,
|
|
customerCountry: shipping.country,
|
|
customerPostalCode: shipping.postalCode,
|
|
},
|
|
}),
|
|
});
|
|
const data: { clientSecret: string; paymentIntentId: string; error?: string } = await res.json();
|
|
if (!res.ok) throw new Error(data.error || 'Failed to create payment intent');
|
|
set({
|
|
payment: {
|
|
paymentIntentId: data.paymentIntentId,
|
|
clientSecret: data.clientSecret,
|
|
status: 'idle',
|
|
errorMessage: '',
|
|
},
|
|
});
|
|
return data.clientSecret;
|
|
} catch (err) {
|
|
const msg = err instanceof Error ? err.message : 'Payment initialization failed';
|
|
set((s) => ({ payment: { ...s.payment, status: 'failed', errorMessage: msg } }));
|
|
return null;
|
|
}
|
|
},
|
|
|
|
placeOrder: () => set({
|
|
orderId: generateOrderId(),
|
|
step: 'confirmed',
|
|
}),
|
|
|
|
resetOrder: () => set({ ...defaultState }),
|
|
}));
|
|
|
|
export const useOrderStore = <T>(selector: (state: OrderStore) => T): T => {
|
|
return useSyncExternalStore(
|
|
orderStore.subscribe,
|
|
() => selector(orderStore.getState()),
|
|
() => selector(orderStore.getState())
|
|
);
|
|
};
|