yslootahrobotics/src/store/useOrderStore.ts

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())
);
};