import type { Cart, Order, CartProduct } from "~/types"; import { trackEvent } from "~/utils/trackEvent"; const CART_STORAGE_KEY = "shop:cart"; const MAX_RETRY_ATTEMPTS = 3; const RETRY_DELAYS = [1000, 2000, 4000]; // Exponential backoff: 1s, 2s, 4s // Module-level promise for deduplication (not reactive, just a lock) let initPromise: Promise | null = null; export function useCart() { const shopApi = useShopApi(); // Use useState for proper Nuxt SSR/client state management const isInitialized = useState("cart:initialized", () => false); const isInitializing = useState("cart:initializing", () => false); const hasError = useState("cart:hasError", () => false); const retryCount = useState("cart:retryCount", () => 0); const cart = useState("cart", () => ({ uuid: "", products: [], productsCount: 0, total: 0, subtotal: 0, VAT: 0, delivery: 0, deliveryMethod: null, emailAddress: "", invoiceAddress: "", deliveryAddress: "", invoiceAddressStructured: null, deliveryAddressStructured: null, acceptedTermsAndConditionsAt: false })); function calculateCount(products: CartProduct[] | null) { return products ? products.reduce((sum, { count }) => sum + count, 0) : 0; } function overwrite(order: Order) { cart.value.products = (order.cart as CartProduct[] | null) ?? []; cart.value.productsCount = calculateCount(cart.value.products); cart.value.total = order.total ?? 0; cart.value.subtotal = order.subtotal ?? 0; cart.value.VAT = order.VAT ?? 0; cart.value.delivery = order.delivery?.price ?? 0; cart.value.deliveryMethod = order.delivery?.id ?? null; cart.value.uuid = order.uuid ?? ""; cart.value.emailAddress = order.email ?? ""; cart.value.invoiceAddress = order.invoiceAddress ?? ""; cart.value.deliveryAddress = order.deliveryAddress ?? ""; cart.value.invoiceAddressStructured = order.invoiceAddressStructured ?? null; cart.value.deliveryAddressStructured = order.deliveryAddressStructured ?? null; cart.value.acceptedTermsAndConditionsAt = Boolean(order.acceptedTermsAndConditionsAt); // Persist to localStorage (client-only) if (import.meta.client) { localStorage.setItem(CART_STORAGE_KEY, order.uuid); } } /** * Sleep utility for retry delays */ function sleep(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); } /** * Reset cart to empty state */ function resetCart() { cart.value = { uuid: "", products: [], productsCount: 0, total: 0, subtotal: 0, VAT: 0, delivery: 0, deliveryMethod: null, emailAddress: "", invoiceAddress: "", deliveryAddress: "", invoiceAddressStructured: null, deliveryAddressStructured: null, acceptedTermsAndConditionsAt: false }; } /** * Attempt to fetch cart with retry logic */ async function fetchWithRetry(attempt = 0): Promise { const storedUuid = localStorage.getItem(CART_STORAGE_KEY); try { let order: Order | null = null; if (storedUuid) { try { order = await shopApi.getOrder(storedUuid); // If order is paid, we need a fresh cart if (order?.paymentAuthorised) { order = null; } } catch { // Order not found or error - will create new one order = null; } } // Create new order if we don't have a valid one if (!order) { order = await shopApi.createOrder(); } return order; } catch (error) { // Track retry attempt if (attempt > 0) { trackEvent("cart-fetch-retry", { attempt, maxAttempts: MAX_RETRY_ATTEMPTS }); } // Check if we should retry if (attempt < MAX_RETRY_ATTEMPTS - 1) { const delay = RETRY_DELAYS[attempt] || RETRY_DELAYS[RETRY_DELAYS.length - 1]; console.warn(`Cart fetch attempt ${attempt + 1} failed, retrying in ${delay}ms...`); await sleep(delay); return fetchWithRetry(attempt + 1); } // All retries exhausted trackEvent("cart-fetch-failed", { attempts: attempt + 1 }); throw error; } } async function fetch() { // Only run on client if (!import.meta.client) return; hasError.value = false; retryCount.value = 0; try { const order = await fetchWithRetry(); if (order) { overwrite(order); } } catch (error) { console.error("Error fetching/creating cart after retries:", error); localStorage.removeItem(CART_STORAGE_KEY); hasError.value = true; resetCart(); } } /** * Retry cart initialization after an error */ async function retry() { if (!import.meta.client) return; hasError.value = false; isInitialized.value = false; retryCount.value += 1; trackEvent("cart-manual-retry", { retryCount: retryCount.value }); await initialize(); } /** * Initialize cart - ensures cart is fetched exactly once. * Safe to call from multiple components; subsequent calls return the same promise. */ async function initialize() { // Only run on client if (!import.meta.client) return; // Already initialized if (isInitialized.value) return; // Already initializing - wait for existing promise if (isInitializing.value && initPromise) { return initPromise; } // Start initialization isInitializing.value = true; initPromise = fetch().finally(() => { isInitialized.value = true; isInitializing.value = false; }); return initPromise; } /** * Wait for cart to be ready. Use this in components that need the cart. */ async function ensureReady() { if (!import.meta.client) return; if (!isInitialized.value) { await initialize(); } } async function addProduct(productId: number, count = 1) { // Ensure cart is initialized if (!cart.value.uuid) { await fetch(); } // Verify we have a valid uuid before making the API call if (!cart.value.uuid) { throw new Error("Failed to initialize cart - no order UUID available"); } const order = await shopApi.addProductToCart(cart.value.uuid, productId, count); overwrite(order); } async function removeProduct(productId: number, count = 1) { const order = await shopApi.removeProductFromCart(cart.value.uuid, productId, count); overwrite(order); } async function update(data: Partial) { const order = await shopApi.updateOrder(cart.value.uuid, data); overwrite(order); } async function checkout() { return await shopApi.checkoutOrder(cart.value.uuid); } // Expose individual computed refs for easier access in components const uuid = computed(() => cart.value.uuid); const products = computed(() => cart.value.products); const productsCount = computed(() => cart.value.productsCount); const total = computed(() => cart.value.total); const subtotal = computed(() => cart.value.subtotal); const VAT = computed(() => cart.value.VAT); const delivery = computed(() => cart.value.delivery); const deliveryMethod = computed(() => cart.value.deliveryMethod); const emailAddress = computed(() => cart.value.emailAddress); const invoiceAddress = computed(() => cart.value.invoiceAddress); const deliveryAddress = computed(() => cart.value.deliveryAddress); const invoiceAddressStructured = computed(() => cart.value.invoiceAddressStructured); const deliveryAddressStructured = computed(() => cart.value.deliveryAddressStructured); const acceptedTermsAndConditionsAt = computed(() => cart.value.acceptedTermsAndConditionsAt); return { // State isInitialized, isInitializing, hasError, retryCount, // Individual computed refs for easy template access uuid, products, productsCount, total, subtotal, VAT, delivery, deliveryMethod, emailAddress, invoiceAddress, deliveryAddress, invoiceAddressStructured, deliveryAddressStructured, acceptedTermsAndConditionsAt, // Methods initialize, ensureReady, fetch, retry, overwrite, addProduct, removeProduct, update, checkout }; }