@shoppexio/storefront-react 0.1.5 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/account-route-messages.ts","../src/cart.tsx","../src/parse.ts","../src/product-badges.ts","../src/video-embed.ts","../src/storefront-data.ts","../src/checkout-fields.ts","../src/cart-quote-bounds.ts","../src/checkout-error-state.ts","../src/sanitize-html.ts","../src/catalog.ts","../src/product-detail.ts","../src/cart-view.ts","../src/contact.ts","../src/menus.ts","../src/reviews.ts","../src/shop-pages.ts","../src/storefront-context.tsx"],"sourcesContent":["export const ACCOUNT_ROUTE_MESSAGE_KEYS = [\n 'account.checkingSession',\n 'common.notLoaded',\n 'common.genericError',\n 'common.retry',\n] as const;\n\nexport type AccountRouteMessageKey = (typeof ACCOUNT_ROUTE_MESSAGE_KEYS)[number];\ntype AccountRouteMessageTuple = readonly [string, string, string, string];\n\n/**\n * A synchronous projection of the four existing catalog keys the lazy route\n * itself needs. The tuple order follows ACCOUNT_ROUTE_MESSAGE_KEYS.\n *\n * Importing the complete catalog here would put all account translations back\n * in the main bundle. The catalog-parity test guards every value against drift.\n */\nconst ACCOUNT_ROUTE_MESSAGES = {\n ar: ['جارٍ التحقق من جلستك', 'تعذّر تحميل هذا', 'تعذّر تحميل هذا. يُرجى المحاولة بعد قليل.', 'أعد المحاولة'],\n de: ['Deine Sitzung wird geprüft', 'Das wurde nicht geladen', 'Das konnte nicht geladen werden. Bitte versuche es gleich noch einmal.', 'Erneut versuchen'],\n en: ['Checking your session', 'This did not load', 'This could not be loaded. Please try again in a moment.', 'Try again'],\n es: ['Comprobando tu sesión', 'Esto no se cargó', 'No se pudo cargar. Inténtalo de nuevo en un momento.', 'Inténtalo de nuevo'],\n fi: ['Tarkistetaan istuntoasi', 'Tämä ei latautunut', 'Tätä ei voitu ladata. Yritä hetken kuluttua uudelleen.', 'Yritä uudelleen'],\n fr: ['Vérification de votre session', 'Le chargement a échoué', 'Le chargement a échoué. Réessayez dans un instant.', 'Réessayer'],\n hi: ['आपका सत्र जाँचा जा रहा है', 'यह लोड नहीं हुआ', 'यह लोड नहीं हो सका। कृपया थोड़ी देर बाद फिर कोशिश करें।', 'फिर कोशिश करें'],\n it: ['Verifica della tua sessione', 'Caricamento non riuscito', 'Non è stato possibile caricare questo contenuto. Riprova tra poco.', 'Riprova'],\n lt: ['Tikriname jūsų seansą', 'Šis turinys neįsikėlė', 'Nepavyko to įkelti. Po akimirkos bandykite dar kartą.', 'Bandykite dar kartą'],\n nl: ['Je sessie wordt gecontroleerd', 'Dit is niet geladen', 'Dit kon niet worden geladen. Probeer het zo meteen opnieuw.', 'Probeer opnieuw'],\n pl: ['Sprawdzamy Twoją sesję', 'To się nie wczytało', 'Nie udało się tego wczytać. Spróbuj ponownie za chwilę.', 'Spróbuj ponownie'],\n pt: ['A verificar a tua sessão', 'Isto não carregou', 'Não foi possível carregar isto. Tenta de novo daqui a pouco.', 'Tentar de novo'],\n ru: ['Проверяем вашу сессию', 'Это не загрузилось', 'Не удалось загрузить это. Повторите попытку через минуту.', 'Повторить'],\n tr: ['Oturumunuz kontrol ediliyor', 'Bu yüklenemedi', 'Bu yüklenemedi. Lütfen birazdan tekrar deneyin.', 'Tekrar deneyin'],\n uk: ['Перевіряємо вашу сесію', 'Це не завантажилося', 'Не вдалося завантажити. Спробуйте ще раз за хвилину.', 'Спробувати ще раз'],\n zh: ['正在检查登录状态', '此内容未加载', '无法加载此内容,请稍后重试。', '重试'],\n} as const satisfies Readonly<Record<string, AccountRouteMessageTuple>>;\n\ntype AccountRouteLocale = keyof typeof ACCOUNT_ROUTE_MESSAGES;\n\nfunction resolveAccountRouteLocale(lang: string | null | undefined): AccountRouteLocale {\n const base = (lang ?? '').trim().toLowerCase().split(/[-_]/)[0] ?? '';\n return base in ACCOUNT_ROUTE_MESSAGES ? base as AccountRouteLocale : 'en';\n}\n\nexport function accountRouteMessage(\n key: AccountRouteMessageKey,\n lang?: string | null,\n): string {\n const activeLang = lang === undefined && typeof document !== 'undefined'\n ? document.documentElement.getAttribute('lang')\n : lang;\n const messageIndex = ACCOUNT_ROUTE_MESSAGE_KEYS.indexOf(key);\n return ACCOUNT_ROUTE_MESSAGES[resolveAccountRouteLocale(activeLang)][messageIndex];\n}\n","/**\n * Cart state, backed entirely by the Storefront SDK.\n *\n * The SDK owns the line list, the coupon and the checkout call; this context\n * owns only what React needs on top of it — the current quote, the drawer's\n * open state, and a single in-flight indicator so buttons can disable\n * themselves. Nothing here computes a price the backend has not confirmed.\n *\n * It also owns the one decision the SDK cannot make for it: whether the hand-off\n * has to stop first to collect a product's required details. See `checkout` and\n * `checkout-fields.ts`.\n *\n * What it does NOT own is which FIELD a cart code is submitted under. Only the\n * server can say whether a string is a merchant coupon or an affiliate referral,\n * and its answer is saved next to the code by `applyCoupon` — so the hand-off\n * reads both back out of SDK storage instead of tracking either in React. See\n * `resolveSubmittedCode`.\n *\n * The Easy-lane clean-minimal storefront presents the cart as a right-hand\n * drawer opened from the header (`sx-cart-drawer`) with `/cart` as a full-page\n * fallback, so this theme has no cart-placement setting to carry.\n *\n * Adding a line does NOT open the drawer. The Easy-lane clean-minimal shell\n * omits `data-sx-cart-open-on-add`, which is the runtime's opt-in for that\n * behaviour — the header count is the whole acknowledgement a buyer gets, and\n * a panel that covers the catalog after every add would be this theme\n * inventing an interruption the reference storefront does not have.\n */\n\nimport {\n createContext,\n type ReactNode,\n useCallback,\n useContext,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from 'react';\nimport {\n buildCustomFieldPayload,\n checkoutFieldWriteRepriced,\n checkoutNeedsDetails as needsCheckoutDetails,\n missingRequiredCheckoutValues,\n resolveCheckoutFields,\n type CheckoutFields,\n type CheckoutFieldValues,\n} from './checkout-fields';\nimport {\n parseCartQuoteLines,\n resolveCartBoundsHeals,\n resolveUnsellableLineIds,\n} from './cart-quote-bounds';\nimport {\n QUOTE_UNAVAILABLE_CHECKOUT_ERROR,\n clearRecoveredQuoteCheckoutError,\n} from './checkout-error-state';\nimport { formatMoney } from './catalog';\nimport { calculateLinePricing, resolvePayWhatYouWantMinimum } from './product-detail';\nimport {\n getProducts,\n isSelectionSoldOut,\n resolveQuantityBounds,\n type ProductAddon,\n type ProductBundleItem,\n type StorefrontPriceVariant,\n type StorefrontProduct,\n} from './storefront-data';\nimport type {\n SdkCartAddOptions,\n SdkCartCodeSource,\n SdkCartItem,\n SdkCartQuote,\n ShoppexSdk,\n} from './sdk';\nimport { getBuyerCurrency, getShoppex } from './shoppex';\n\ntype CartContextValue = {\n lines: SdkCartItem[];\n count: number;\n quote: SdkCartQuote | null;\n /** The server could not price the cart. Written by the refresh, only. */\n quoteError: string | null;\n /**\n * A cart WRITE failed — an add the SDK refused or dropped, a bundle that had\n * to be rolled back. Held apart from `quoteError` on purpose: a refresh must\n * never be able to clear it. See `mutate`.\n */\n mutationError: string | null;\n /**\n * The last code the buyer applied was refused by the quote.\n *\n * Written only by `applyCoupon` and cleared only by the buyer's next cart\n * action (see `mutate`). It cannot be derived from the quote any more,\n * because a refused code is REMOVED from storage the moment it is refused —\n * the very next quote therefore carries no code and would erase the message\n * that explains why.\n */\n couponRejected: boolean;\n /**\n * True when the code the CURRENT quote was priced with is the ambient\n * affiliate referral rather than one the buyer applied.\n *\n * The promo field decides between its entry row and its \"applied\" chip from\n * `quote.coupon_applied`, which was a complete answer while the only code\n * that could ever reach a quote was one the buyer typed. It is not one any\n * more: an accepted ambient referral sets that same flag, and the chip would\n * then congratulate the buyer on a code they never entered, print it blank\n * (the field is empty, because nothing was applied), and offer a Remove\n * button wired to cart-coupon storage the referral does not live in — so it\n * would reappear on the next quote. Kept next to the quote it describes and\n * written by the same statements, so the two can never disagree.\n */\n quotedAmbientAffiliate: boolean;\n /**\n * The shop's checkout maximum, stated once, while the CURRENT quote says this\n * cart is over it. Null while it is within the limit or while no quote has\n * settled.\n *\n * PAYMENT PATH. The summary prints it beside the total — an over-limit cart\n * is a cart the checkout refuses, and the buyer cannot act on a limit nobody\n * names — and `checkout` refuses the hand-off on the same fact, exactly as it\n * refuses an unsellable line. See `resolveCartMaximumWarning`.\n */\n cartMaxWarning: string | null;\n /**\n * The settled quote contains a PHYSICAL line, so its total excludes shipping,\n * which the hosted checkout prices from the buyer's address. False while no\n * quote has settled. The summary says so under the total.\n */\n requiresShipping: boolean;\n checkoutError: string | null;\n /**\n * True when the LAST hand-off was refused because the referral on this cart\n * is the buyer's own (`affiliate_code_not_applicable`).\n *\n * It needs its own flag rather than being read out of `checkoutError`,\n * because it is the one refusal the buyer cannot resolve by re-reading the\n * message: the code is ambient — nobody typed it, no field shows it, and no\n * control removes it — so without an explicit exit the cart refuses every\n * retry forever. The surface renders `clearBlockedReferral` next to the\n * message; the flag is what tells it to.\n */\n selfReferralBlocked: boolean;\n /**\n * The exit. Drops the referral attribution the SDK is carrying, re-quotes\n * without it, and lets the buyer check out again. See the implementation.\n */\n clearBlockedReferral: () => Promise<void>;\n busy: boolean;\n couponInput: string;\n setCouponInput: (value: string) => void;\n drawerOpen: boolean;\n setDrawerOpen: (open: boolean) => void;\n add: (\n product: StorefrontProduct,\n variant: StorefrontPriceVariant | null,\n quantity: number,\n options?: {\n /**\n * Buyer-chosen unit price for a pay-what-you-want product. Part of the\n * cart line identity — the server validates it against the minimum.\n */\n payWhatYouWantPrice?: number;\n /** Extras the buyer ticked in the buy box. Also part of line identity. */\n addons?: ProductAddon[];\n },\n ) => Promise<void>;\n /**\n * Adds EVERY member of a bundle offer, as one operation.\n *\n * Bundle members arrive as a compact summary rather than a full product, so\n * they cannot go through `add` — their quantity bounds are resolved from the\n * member's own catalog product instead, and a member that cannot be resolved\n * is rejected rather than added. The set is all-or-nothing: see the\n * implementation for why a half-added bundle is worse than none of it.\n */\n addBundle: (items: readonly ProductBundleItem[]) => Promise<void>;\n update: (lineId: string, quantity: number) => Promise<void>;\n remove: (lineId: string) => Promise<void>;\n clear: () => Promise<void>;\n applyCoupon: () => Promise<void>;\n removeCoupon: () => Promise<void>;\n /**\n * The per-product details this cart's products require, resolved from the\n * injected catalog. `/checkout` renders the form from it.\n */\n checkoutFields: CheckoutFields;\n /**\n * True when the hand-off must go through `/checkout` to collect those details\n * first. Read it on CLICK rather than from a render closure after a cart\n * write — `readCheckoutNeedsDetails` exists for exactly that.\n */\n checkoutNeedsDetails: boolean;\n /**\n * Hands over to Hosted Checkout. `customFieldValues` writes the buyer's\n * answers onto the cart lines first and is what `/checkout` submits; every\n * other surface hands off with no values, and may only do so when\n * `checkoutNeedsDetails` is false.\n */\n checkout: (customFieldValues?: CheckoutFieldValues) => Promise<void>;\n};\n\n/**\n * Whether the checkout CTA has to stop for details, read from the SDK's cart\n * RIGHT NOW rather than from React state.\n *\n * PAYMENT PATH. Buy Now adds a line and then hands off in the same click. The\n * `checkoutNeedsDetails` on the context describes the cart as it was when the\n * component last rendered, which is the cart BEFORE that add — a product whose\n * required fields the add has just brought into the cart would be handed\n * straight to a checkout that cannot be created.\n */\nexport function readCheckoutNeedsDetails(): boolean {\n return needsCheckoutDetails(resolveCheckoutFields(getShoppex().getCart()));\n}\n\nconst CartContext = createContext<CartContextValue | null>(null);\n\n/**\n * The backend's refusal when the referral on the order belongs to the buyer\n * placing it — an affiliate opening their own storefront on their own `?ref=`\n * link is the ordinary way to reach it.\n *\n * The value is the FULL i18n key, because that is literally what crosses the\n * wire: `from-cart` sends `error_code: CHECKOUT_ERROR_CODES[reason]`, whose\n * values are the `errors.checkout.*` keys, and the SDK client copies\n * `error_code` to `code` verbatim. The short reason name never leaves the\n * server. Compared in full rather than by suffix: this is an exact identifier,\n * and a suffix match would also accept the same leaf under another namespace.\n */\nconst SELF_REFERRAL_REFUSAL_CODE = 'errors.checkout.affiliate_code_not_applicable';\nconst SELF_REFERRAL_MESSAGE = 'This referral code cannot be used on your own order.';\n\n/**\n * The backend's refusals of a SUBMITTED quote proof, with the copy each gets.\n *\n * PAYMENT PATH. `/from-cart` no longer treats a proof it cannot honour as \"no\n * proof sent\" — doing so silently removed the ceiling that stops an invoice\n * being priced above the total the buyer approved. It names the reason, and all\n * of them are resolved the same way: re-quote, so the buyer approves a current\n * total and the retry carries a proof describing the cart as it now is.\n *\n * `price_increased_since_quote` is that ceiling DOING ITS JOB rather than a\n * proof this theme got wrong: the invoice would price above the total the buyer\n * approved, and a promotion expiring between the quote and the click is the\n * ordinary way to reach it. Left out of this map it fell through to the\n * backend's prose with no re-quote, so the stale proof and the stale total both\n * stayed put — every retry submitted the identical ceiling, was refused\n * identically, and the number on screen never moved. A dead end with no\n * buyer-reachable exit, which is the same failure class as the ambient-referral\n * lock-out. Its copy names the PRICE CHANGE rather than a failure, because that\n * is the thing the buyer has to understand before reading the new total.\n *\n * Matched on the CODE — the full `errors.checkout.*` key the wire carries —\n * never on the message, which is localized prose.\n */\n/**\n * Checkout refusals the buyer can only read, not fix from the cart, with copy\n * that names the reason instead of the backend's generic prose. Matched on the\n * `errors.checkout.*` code, like the maps above.\n */\nconst CHECKOUT_REFUSAL_MESSAGES: Record<string, string> = {\n 'errors.checkout.shop_does_not_ship': 'This shop does not ship physical products yet.',\n};\n\nconst QUOTE_PROOF_REFUSAL_MESSAGES: Record<string, string> = {\n 'errors.checkout.quote_token_invalid':\n 'We could not verify the price you approved. The cart has been re-priced — review the total and check out again.',\n 'errors.checkout.quote_token_expired':\n 'The price you approved has expired. Review the updated total, then check out again.',\n 'errors.checkout.quote_token_stale':\n 'Your cart changed after the price was approved. Review the updated total, then check out again.',\n 'errors.checkout.price_increased_since_quote':\n 'The price of your cart went up after you approved the total. Review the updated total, then check out again.',\n};\n\n/**\n * The shop's checkout MAXIMUM, read off a settled quote and turned into the one\n * sentence every cart surface prints and the hand-off refuses with.\n *\n * PAYMENT PATH. `cart_max_exceeded` is advisory in the sense that the quote\n * still prices and returns the cart — but it is the server stating that\n * `/from-cart` WILL refuse this cart, and the limit itself rides along on\n * `cart_maximum_checkout`. This theme read neither: the checkout button stayed\n * live because a quote existed, the click came back as\n * `cart_maximum_checkout_exceeded`, and the buyer was never told what the limit\n * was or that they were over it.\n *\n * Null means \"within the limit\", and only because the producer OMITS the flag\n * in exactly that case. It is not a silence being read as permission: the flag\n * never authorises a checkout, it only ever stops one, so its absence leaves\n * every other guard doing its own job.\n *\n * The amount is printed through the same formatter the totals use, so the limit\n * and the total it sits beside are spelled the same way. Wording matched to the\n * Easy lane's `cart.maxExceeded`, because both lanes are stating one contract.\n * A flag set without a readable amount still BLOCKS — the sentence simply\n * cannot name a number rather than inventing one.\n */\nfunction resolveCartMaximumWarning(quote: SdkCartQuote): string | null {\n if (quote.cart_max_exceeded !== true) return null;\n const maximum = Number(quote.cart_maximum_checkout);\n return Number.isFinite(maximum) && maximum > 0\n ? `This cart is over the ${formatMoney(maximum, quote.currency)} checkout limit. `\n + 'Remove some items to continue.'\n : 'This cart is over this shop’s checkout limit. Remove some items to continue.';\n}\n\nfunction errorMessage(error: unknown, fallback: string): string {\n return error instanceof Error && error.message.trim() ? error.message : fallback;\n}\n\n/**\n * What kind of code the quote accepted, or null when it accepted none.\n *\n * PAYMENT PATH. The buyer types one code into one field, and only the SERVER can\n * say whether that string is one of the merchant's coupons or an affiliate's\n * referral — the theme has neither list. The quote answers with\n * `coupon_source`, and that answer decides which field checkout submits the code\n * under: a referral sent as `coupon` is a payout the affiliate never receives.\n *\n * Both signals have to hold. `coupon_applied` without a recognised source is a\n * shape this theme cannot act on: there is no field to submit an unclassified\n * code under, so it counts as not accepted rather than being guessed into one.\n * Mirrors `packages/storefront-commerce/src/sx/cart-store.ts`.\n */\nfunction readAcceptedCouponSource(quote: SdkCartQuote): SdkCartCodeSource | null {\n if (quote.coupon_applied !== true) return null;\n return quote.coupon_source === 'COUPON' || quote.coupon_source === 'AFFILIATE'\n ? quote.coupon_source\n : null;\n}\n\n/**\n * The code fields the checkout hand-off submits, read out of the SDK's OWN\n * storage. Null when storage holds a code this theme must not submit under\n * either field — see below.\n *\n * PAYMENT PATH, and the reason it reads storage rather than React state.\n *\n * The SDK keeps a cart code and its classification as ONE record and reads them\n * back as one: `setCartCoupon(code, source)` writes `{ code, source }`, and\n * `getStoredCartCode` returns null unless the code is non-empty AND the source\n * is one it recognises (`packages/sdk/src/modules/cart.ts`). So a stored code\n * always arrives with the answer to \"which field\", and it arrives NOW, at the\n * click — no round trip, nothing to wait for, and nothing that can be half\n * settled.\n *\n * It used to be decided from a `couponSource` state written by the quote, and\n * every gap between a stored code and a settled quote was a money bug. A cart\n * write publishes its lines before its quote resolves, and the checkout button\n * is live throughout: in that window the source was null while a code sat in\n * storage, so the hand-off sent an explicitly EMPTY `coupon`. That is not \"no\n * opinion\" — an empty string SUPPRESSES the SDK's own read of storage\n * (`resolveCheckoutCodes`) — so a returning buyer's stored `SAVE10` was dropped\n * from the invoice and they were charged full price. First paint and a second\n * tab applying a code opened the same window.\n *\n * A code stored WITHOUT a source cannot happen through the SDK's reader, which\n * refuses to hand one back. It is still answered rather than guessed, because\n * both guesses cost someone money: submitted as a coupon, the affiliate who\n * brought the sale is never credited; suppressed, the buyer loses their\n * discount. The hand-off stops and says so instead.\n */\n/**\n * One spelling for an ambient referral, so two reads of the SAME storage cannot\n * look different.\n *\n * The SDK lowercases the code on its way into the checkout request\n * (`normalizeAffiliateCode`), and storage holds whatever `?ref=` carried. The\n * comparison below decides whether a hand-off may proceed, so it compares the\n * codes as the REQUEST would spell them rather than as the URL did — otherwise\n * `?ref=QA-Partner` and `?ref=qa-partner` would read as a change of attribution\n * and stop a checkout nothing is wrong with.\n */\nfunction normalizeAmbientAffiliate(value: string | null | undefined): string {\n return value?.trim().toLowerCase() ?? '';\n}\n\nfunction resolveSubmittedCode(\n sdk: ShoppexSdk,\n): { coupon: string; affiliateCode?: string } | null {\n const code = sdk.getCartCoupon()?.trim() ?? '';\n // '' rather than undefined, in every branch: see `checkout`.\n if (!code) return { coupon: '' };\n const source = sdk.getCartCouponSource();\n if (source === 'COUPON') return { coupon: code };\n if (source === 'AFFILIATE') return { coupon: '', affiliateCode: code };\n return null;\n}\n\n/** Everything the SDK folds into a line's `line_id`, and nothing else. */\ntype CartLineIdentityInput = Pick<\n SdkCartItem,\n | 'product_id'\n | 'variant_id'\n | 'price_variant_id'\n | 'addons'\n | 'custom_fields'\n | 'price_data'\n | 'pay_what_you_want_price'\n>;\n\n/**\n * The SDK's cart-line identity, rebuilt field for field.\n *\n * `line_id` is a hash over exactly these seven fields, normalized exactly this\n * way — the SDK's `computeCartLineId` is not on the storefront global, so the\n * theme rebuilds the payload the hash is taken over and compares THAT. Comparing\n * the payload rather than a hash of it is the stricter of the two, which is the\n * right direction for a check that has to reject.\n *\n * Every field is load-bearing, and the price snapshot most of all: to the SDK\n * the same product at a new price is a NEW line, not more units of the old one.\n * A matcher that leaves one out reads back a line the write never touched, and\n * a dropped write borrows that line's units and passes.\n */\nfunction cartLineIdentity(input: CartLineIdentityInput): string {\n const addons = [...(input.addons ?? [])]\n // Addon quantity counts as much as the id: two of an extra is not one.\n .map((addon) => ({ id: addon.id, quantity: addon.quantity ?? 1 }))\n .sort((a, b) => a.id.localeCompare(b.id));\n const customFields = Object.fromEntries(\n Object.entries(input.custom_fields ?? {})\n .filter(([, value]) => typeof value === 'string')\n .sort(([a], [b]) => a.localeCompare(b)),\n );\n const unitPrice = input.price_data?.unit_price;\n const namedPrice = input.pay_what_you_want_price;\n // Absent and non-finite both normalize to null, exactly as the SDK does —\n // otherwise `undefined` (dropped by JSON.stringify) and an explicit null would\n // read as two identities for one line.\n return JSON.stringify({\n product_id: input.product_id,\n variant_id: input.variant_id,\n price_variant_id: input.price_variant_id ?? null,\n addons,\n custom_fields: customFields,\n unit_price: typeof unitPrice === 'number' && Number.isFinite(unitPrice) ? unitPrice : null,\n pay_what_you_want_price:\n typeof namedPrice === 'number' && Number.isFinite(namedPrice) ? namedPrice : null,\n });\n}\n\n/** One bundle member matched to the catalog product it actually refers to. */\ntype ResolvedBundleMember = {\n product: StorefrontProduct;\n variant: StorefrontPriceVariant | null;\n bounds: { min: number; max: number | null };\n};\n\n/**\n * One bundle member, resolved against the catalog it came from.\n *\n * A bundle member arrives as a compact summary — id, title, unit price — with\n * no bounds on it, but it IS an ordinary catalog product: the wire payload\n * carries every product, listed or not, so the member's real minimum, maximum\n * and stock are in there to be read. They have to be read, because the add\n * re-snapshots the line and the SDK adopts THIS write's bounds: an add that\n * sent none STRIPPED the cap off a line the buy box had capped, and the SDK's\n * read clamp then had nothing to hold the buyer to — the stepper carried that\n * line straight past the merchant's per-order maximum.\n *\n * The product itself is returned as well as its bounds, because two more\n * decisions need it: whether the merchant has PAUSED it, and whether it is sold\n * at a price the buyer names.\n *\n * Null when the member is not in the payload, or names a variant its product\n * does not have. A member that cannot be resolved is not written at all: a line\n * without real bounds is precisely the failure this exists to prevent.\n */\nfunction resolveBundleMember(item: ProductBundleItem): ResolvedBundleMember | null {\n // Read on demand rather than once at mount: the payload is parsed strictly\n // and may throw, and the storefront's own boot is where that error belongs.\n const product = getProducts().find((candidate) => candidate.id === item.id);\n if (product === undefined) return null;\n // 'default' is the no-price-variant marker, the same one the buy box sends.\n if (item.variantId === 'default') {\n return { product, variant: null, bounds: resolveQuantityBounds(product, null) };\n }\n const variant = product.priceVariants.find((candidate) => candidate.id === item.variantId);\n if (variant === undefined) return null;\n return { product, variant, bounds: resolveQuantityBounds(product, variant) };\n}\n\n/**\n * The quantity the SDK will actually store for a line it has just written.\n *\n * Mirrors the SDK's own clamp: floor to whole units, raise to the minimum, then\n * cap at the maximum (max wins on the degenerate min > max), never below one.\n * The bounds are the ones passed on THIS write — the theme sends\n * `replace_display_snapshot`, so the SDK adopts them before it clamps.\n */\nfunction clampToLineBounds(quantity: number, minimum: number, maximum: number | null): number {\n let clamped = Math.floor(quantity);\n if (clamped < minimum) clamped = Math.floor(minimum);\n if (maximum !== null && clamped > maximum) clamped = Math.floor(maximum);\n return Math.max(1, clamped);\n}\n\n/**\n * Whether an add landed, decided on TWO signals that have to hold together.\n *\n * PAYMENT PATH. Counting units by rebuilt line identity is not enough on its\n * own, because the SDK does not merge by identity — it merges by the STORED\n * `line_id`, and that value can be a supplied or legacy one that the identity\n * hash no longer reproduces (`packages/sdk/src/modules/cart.ts`: a stored\n * `line_id` is kept verbatim, and `addToCart` looks the line up by it). A cart\n * persisted on this domain before the merchant switched to this template can\n * therefore hold a line that is field-identical to the one being written and\n * that the SDK will NOT merge into. Its units are not this write's units, and a\n * check that counted them let a dropped write pass.\n *\n * So the add succeeded when EITHER:\n *\n * (a) the write changed the SDK's stored cart at all — the serialized cart is\n * not what it was one statement earlier. Any real add moves something: a\n * new line, more units, or a replaced display snapshot; or\n * (b) it changed nothing AND a field-matching line already sits exactly at the\n * ceiling this write's bounds impose. That is the one legitimate no-op —\n * the SDK clamps the add away — and only a line the SDK would actually\n * merge into can produce it, because a line it merges into is the only one\n * whose bounds this write has just replaced.\n *\n * Without a maximum there is no ceiling and (b) can never apply: every\n * successful add must then have changed the cart.\n *\n * Residual corner, stated rather than papered over: a field-identical line\n * under a FOREIGN legacy `line_id`, sitting at exactly the ceiling this write\n * sends, at the same moment the write is silently dropped, is still\n * indistinguishable from the legitimate no-op — both leave the cart untouched\n * with a matching line at the ceiling. Nothing the theme can read tells those\n * two apart; the real fix is the SDK follow-up in the README, where a refused\n * write should surface instead of being swallowed.\n */\nfunction unwrittenAdd(\n serializedBefore: string,\n identity: string,\n minimum: number,\n maximum: number | null,\n message: string,\n): string | null {\n const lines = getShoppex().getCart();\n if (JSON.stringify(lines) !== serializedBefore) return null;\n if (maximum === null) return message;\n const ceiling = clampToLineBounds(maximum, minimum, maximum);\n return lines.some((line) => cartLineIdentity(line) === identity && line.quantity === ceiling)\n ? null\n : message;\n}\n\n/**\n * Writes the buyer's per-product answers onto every cart line that asked for\n * them, and answers whether the cart is now safe to check out.\n *\n * PAYMENT PATH. Returns null on success, or the message to fail the hand-off\n * with — never a silent partial write.\n *\n * Two SDK behaviours shape this:\n *\n * - `custom_fields` is part of a line's IDENTITY. Writing it makes the SDK\n * recompute `line_id` and MERGE the line into any existing line that now\n * matches, so a line id captured before the loop can be gone by the time the\n * loop reaches it. That is not a failure — a line can only vanish INTO one\n * already carrying these exact answers — so it is skipped rather than\n * written, which is also what stops `updateCartItem` from throwing\n * \"Item not found in cart\" mid-way through.\n * - `updateCartItem` can drop a write in silence, exactly as `addToCart` can:\n * the SDK's storage wrapper catches a quota or blocked-storage error and only\n * logs it. So the cart is READ BACK afterwards and every required value is\n * re-validated against what actually landed. Without that, a buyer who filled\n * the form in would be sent to a checkout the backend refuses outright.\n *\n * A merge is harmless to the ANSWERS and never harmless to the PRICE: it sums\n * two lines' quantities, and quantities are hashed into the quote proof this\n * hand-off is about to present. So the cart is compared across the write and a\n * merge stops the hand-off — see `checkoutFieldWriteRepriced`.\n */\nfunction writeCheckoutFieldsToCart(values: CheckoutFieldValues): string | null {\n const sdk = getShoppex();\n const fields = resolveCheckoutFields(sdk.getCart());\n if (!fields.resolved) {\n return 'Some products in your cart are no longer available. Please review your cart.';\n }\n if (fields.groups.length === 0) return null;\n\n const payloads = new Map(fields.groups.map((group) => [\n group.productId,\n buildCustomFieldPayload(group.fields, values[group.productId]),\n ]));\n // The cart as it stood BEFORE any answer landed, and the list this pass walks.\n // One read for both, so the comparison below cannot be about a different cart\n // than the one that was written.\n const before = sdk.getCart();\n for (const line of before) {\n const payload = payloads.get(line.product_id);\n if (payload === undefined) continue;\n // Merged away by an earlier write in this pass — see above.\n if (!sdk.getCart().some((entry) => entry.line_id === line.line_id)) continue;\n sdk.updateCartItem(line.line_id, { custom_fields: payload });\n }\n\n const after = sdk.getCart();\n const missing = missingRequiredCheckoutValues(fields.groups, after);\n if (missing.length > 0) {\n return `Your details could not be saved to the cart: ${missing.join(' ')}`;\n }\n // A merge changed the quantities the current quote — and the proof it minted —\n // were taken for. See `checkoutFieldWriteRepriced`.\n return checkoutFieldWriteRepriced(before, after)\n ? 'Two lines of the same item were combined once your details were saved. '\n + 'Review the updated total, then check out again.'\n : null;\n}\n\n/** A line's stored id and units, as they stood before one write. */\ntype CartLineSnapshot = { lineId: string; quantity: number };\n\n/**\n * How to take one member add back out again: restore `quantity` on the line it\n * merged into, or remove the line outright when the add created it.\n */\ntype BundleMemberUndo = { lineId: string; quantity: number | null };\n\nfunction snapshotLines(lines: readonly SdkCartItem[]): CartLineSnapshot[] {\n return lines.map((line) => ({ lineId: line.line_id, quantity: line.quantity }));\n}\n\n/**\n * What one member add did to the cart, read as the difference across it.\n *\n * Matched on the SDK's stored `line_id` rather than on rebuilt identity for the\n * same reason the read-back cannot rely on identity alone: the id is what the\n * SDK merges by, so it is the only thing that names the line this write\n * actually touched. Null when the add moved nothing — the at-ceiling no-op —\n * which is exactly right: there is nothing to undo.\n */\nfunction diffBundleMemberAdd(\n before: readonly CartLineSnapshot[],\n after: readonly SdkCartItem[],\n): BundleMemberUndo | null {\n const created = after.find((line) => !before.some((entry) => entry.lineId === line.line_id));\n if (created !== undefined) return { lineId: created.line_id, quantity: null };\n for (const line of after) {\n const previous = before.find((entry) => entry.lineId === line.line_id);\n if (previous !== undefined && line.quantity > previous.quantity) {\n return { lineId: line.line_id, quantity: previous.quantity };\n }\n }\n return null;\n}\n\nexport function CartProvider({ children }: { children: ReactNode }) {\n const [lines, setLines] = useState<SdkCartItem[]>([]);\n const [count, setCount] = useState(0);\n const [quote, setQuote] = useState<SdkCartQuote | null>(null);\n const [quoteError, setQuoteError] = useState<string | null>(null);\n const [mutationError, setMutationError] = useState<string | null>(null);\n const [couponRejected, setCouponRejected] = useState(false);\n const [quotedAmbientAffiliate, setQuotedAmbientAffiliate] = useState(false);\n const [cartMaxWarning, setCartMaxWarning] = useState<string | null>(null);\n const [checkoutError, setCheckoutError] = useState<string | null>(null);\n const [selfReferralBlocked, setSelfReferralBlocked] = useState(false);\n /**\n * The ambient referral the CURRENT quote was priced with: `''` when it was\n * priced without one, and null when there is no settled quote at all.\n *\n * PAYMENT PATH, and the reason it is a ref rather than only the boolean above.\n * The quote reads the referral at REFRESH time; `sdk.checkout()` reads the\n * same storage again at CLICK time and submits whatever it finds\n * (`normalizeAffiliateCode` in the SDK). Between those two reads a second tab\n * on the same domain can arrive with a different `?ref=`, or clear the\n * attribution — and the invoice then carries a discount the cart never showed,\n * or drops one it did. So the code the quote used has to survive until the\n * click, next to the quote it belongs to, and be compared against storage\n * before the hand-off. A ref, because the comparison must read what is true\n * NOW: `checkout` is also called from Buy Now after an await, whose closure was\n * captured a render earlier.\n */\n const quotedAffiliate = useRef<string | null>(null);\n /**\n * The stored `line_id`s the CURRENT quote refused (`sellable: false`).\n *\n * PAYMENT PATH. The server's own statement that this cart cannot be ordered as\n * it stands — the option is unorderable, or the cart's lines of one option\n * together ask for more units than its ceiling, which `/from-cart` refuses on\n * the SUM. A settled total is not permission to order units the shelf does not\n * have, so the hand-off stops on it rather than spending the buyer's click on\n * a rejection that arrives from the checkout naming no line.\n *\n * A ref rather than state for the same reason `quotedAffiliate` is one:\n * `checkout` reads it at CLICK time and its closure was captured a render\n * earlier — a Buy Now that adds and hands off in the same click would\n * otherwise consult the verdict for the cart as it stood before the add.\n */\n const unsellableLines = useRef<string[]>([]);\n /**\n * The checkout-maximum verdict of the CURRENT quote, as the sentence the\n * buyer is shown, or null while the cart is within the limit.\n *\n * PAYMENT PATH, and a ref for the same reason `unsellableLines` is one:\n * `checkout` reads it at CLICK time out of a closure captured a render\n * earlier, and Buy Now adds a line and hands off in the same click.\n */\n const cartMaximumWarning = useRef<string | null>(null);\n const [busyCount, setBusyCount] = useState(0);\n const [couponInput, setCouponInput] = useState('');\n const [drawerOpen, setDrawerOpen] = useState(false);\n /**\n * True while the field holds a code the buyer typed but has not applied yet.\n * Every cart mutation refreshes, and the refresh writes the STORED coupon\n * back into the field — so changing a quantity after typing a code silently\n * erased it, and a buyer who did not notice paid full price.\n */\n const couponPendingEdit = useRef(false);\n const refreshVersion = useRef(0);\n const refreshTimer = useRef<number | null>(null);\n const refreshResolvers = useRef<Array<() => void>>([]);\n\n /**\n * Records which referral priced the quote now on screen, in ONE statement.\n *\n * The ref the checkout hand-off checks and the flag the coupon row renders are\n * two views of the same fact, and a site that wrote only one of them would let\n * them disagree — a cart claiming a referral it did not quote with, or a\n * hand-off comparing against a code no quote used.\n */\n const rememberQuotedAffiliate = useCallback((code: string | null) => {\n quotedAffiliate.current = code;\n setQuotedAmbientAffiliate(code !== null && code !== '');\n }, []);\n\n /**\n * Records the checkout-maximum verdict of the quote now on screen, in ONE\n * statement — for the same reason `rememberQuotedAffiliate` exists. The ref\n * the hand-off refuses on and the sentence the summary prints are two views\n * of one fact, and a site that wrote only one of them would let a cart be\n * blocked with nothing on screen saying why, or warned about a limit the\n * click no longer stops on.\n */\n const rememberCartMaximum = useCallback((warning: string | null) => {\n cartMaximumWarning.current = warning;\n setCartMaxWarning(warning);\n }, []);\n\n /**\n * Settles ONE accepted quote over the cart it priced.\n *\n * PAYMENT PATH, and the reason it is a function rather than a passage inside\n * `refreshNow`. A quote reaches this cart down TWO paths — the refresh, and\n * `applyCoupon` settling the candidate quote that accepted the buyer's code —\n * and they carry identical payloads. The apply path used to settle\n * `result.data` on its own, which meant the cart kept the PREVIOUS verdict:\n * a cart that had just become unsellable stayed checkout-enabled until the\n * backend rejected the click, and one that had been replenished stayed\n * blocked by a verdict that no longer held. Two copies of this sequence is\n * what produced that, so there is one.\n *\n * The whole sequence, in order:\n *\n * - THE CART BOUNDS CONTRACT (`CartQuoteLine`, read by\n * `cart-quote-bounds.ts`). Cart lines hold an ADD-TIME SNAPSHOT of\n * their bounds, so a line goes stale the moment stock moves or the merchant\n * edits the limits, and a line restored from an older cart carries none at\n * all. The stepper is driven off those stored values, so a buyer could raise\n * a line past a ceiling that had shrunk under them and only find out at\n * checkout. Every settled quote restates the bounds from the CURRENT row and\n * shelf, through the same resolution `/from-cart` refuses with — so this\n * heals them, REPLACING rather than merging: an absent `max_quantity` means\n * the ceiling was removed, and a merge would keep a stale one forever.\n * - the refused lines the hand-off reads at click time, from the HEALED cart.\n * - the shop's checkout maximum, which the same quote states.\n *\n * A quote that says nothing about lines leaves both the stored bounds and the\n * previous verdict alone. It is not an all-clear.\n *\n * The answer says what the CALLER still owes:\n * - `'settled'` — the quote is on screen and nothing further is needed;\n * - `'clamped'` — a heal moved a quantity, so this quote priced units the cart\n * no longer holds. Nothing is settled, and the caller must re-quote;\n * - `'superseded'` — a newer refresh owns the cart. Nothing was written and\n * nothing is owed;\n * - `'stale-cart'` — the cart moved under the round trip. Nothing was written,\n * and what the caller owes depends on who can answer for the cart that now\n * exists — see `pricedCart`.\n */\n const settleQuotedCart = useCallback((\n data: SdkCartQuote,\n /** The ambient referral THIS quote was priced with. See `refreshNow`. */\n quotedAmbient: string,\n /** The refresh generation this quote belongs to. */\n version: number,\n /**\n * The serialized cart this quote was TAKEN FOR, pinned by the caller in the\n * same synchronous step it asked for the quote.\n *\n * PAYMENT PATH. A quote is a round trip, and the cart lives in localStorage,\n * which every tab on the domain shares — so a second tab can add, remove or\n * re-quantify a line while this one waits, and `refreshVersion` cannot see\n * it. The generation guard only retires a quote once ANOTHER refresh has\n * started; a cross-tab write schedules that refresh through a debounce, and\n * `applyCoupon` bumps the generation itself and then settles under the new\n * one, so both leave a window in which a response for the OLD cart passes\n * the guard by construction.\n *\n * Nothing below may run on that response. The heals write bounds resolved\n * for the old cart — they can clamp a line the buyer just changed in the\n * other tab — and the totals, the refused-line verdict and the\n * checkout-maximum state all describe a cart that no longer exists, yet\n * stay on screen and arm the hand-off until some later quote replaces them.\n * So the pin is checked FIRST, before a single heal is written.\n */\n pricedCart: string,\n ): 'settled' | 'clamped' | 'superseded' | 'stale-cart' => {\n const sdk = getShoppex();\n if (JSON.stringify(sdk.getCart()) !== pricedCart) return 'stale-cart';\n /**\n * Both refusals are answered BEFORE the first heal is written.\n *\n * PAYMENT PATH. A response that may not settle may not write either. The\n * cart pin above catches a cart that moved under the round trip; this\n * catches one a NEWER refresh already owns, and the two are the same\n * question asked of different clocks. Asked after the heals, a superseded\n * response still wrote bounds through the SDK on its way to being told it\n * was superseded — harmless in effect, because bounds do not reprice and\n * the newer refresh restates them, but it is a write from an answer that\n * had already lost.\n */\n if (version !== refreshVersion.current) return 'superseded';\n const quotedLines = parseCartQuoteLines(data.lines);\n let unsellable: string[] | null = null;\n let healClamped = false;\n if (quotedLines.length > 0) {\n const beforeHeal = sdk.getCart();\n for (const heal of resolveCartBoundsHeals(beforeHeal, quotedLines)) {\n sdk.updateCartItem(heal.lineId, {\n min_quantity: heal.minQuantity,\n max_quantity: heal.maxQuantity,\n });\n }\n // Re-read: a heal clamps through the SDK, so the verdict and the lines on\n // screen must both come from the HEALED cart.\n const healedCart = sdk.getCart();\n healClamped = healedCart.length !== beforeHeal.length\n || healedCart.some((line, index) => line.quantity !== beforeHeal[index]?.quantity);\n unsellable = resolveUnsellableLineIds(healedCart, quotedLines);\n }\n /**\n * A heal that CLAMPED a quantity has changed the cart this quote priced.\n *\n * PAYMENT PATH. The total in hand was computed for the pre-heal units, and\n * the proof the SDK is holding is bound to that cart — so neither may\n * settle over the post-heal cart. The lines are re-published (the buyer has\n * to see what the cart now holds), the checkout stays disarmed, and the\n * caller is told to ask for ONE follow-up quote. Bounded there, because the\n * second pass finds the bounds already in agreement and heals nothing.\n *\n * Bounds themselves do not reprice anything — the proof is bound to a hash\n * of the cart's products, options and QUANTITIES — so a heal that moved no\n * quantity needs no re-quote and keeps the ceiling the buyer approved.\n */\n if (healClamped) {\n setLines(sdk.getCart());\n setCount(sdk.getCartItemCount());\n setQuote(null);\n rememberQuotedAffiliate(null);\n // The cart this warning described is gone with the clamped units; the\n // follow-up quote states the maximum for the cart that now exists.\n rememberCartMaximum(null);\n unsellableLines.current = unsellable ?? [];\n return 'clamped';\n }\n setQuote(data);\n setQuoteError(null);\n setCheckoutError(clearRecoveredQuoteCheckoutError);\n rememberQuotedAffiliate(quotedAmbient);\n rememberCartMaximum(resolveCartMaximumWarning(data));\n if (unsellable !== null) unsellableLines.current = unsellable;\n // The heals above may have rewritten stored bounds without moving a\n // quantity; the stepper reads them off the published lines.\n if (quotedLines.length > 0) setLines(sdk.getCart());\n return 'settled';\n }, [rememberCartMaximum, rememberQuotedAffiliate]);\n\n /**\n * Re-reads the SDK's cart and re-quotes it.\n *\n * It owns `quoteError` and retires only the checkout refusal caused by the\n * missing quote once pricing settles. `mutationError` and every unrelated\n * checkout error stay out of its reach: a refresh is not a buyer telling the\n * theme anything, and a quote that answers after a failed write must not wipe\n * the only notice that the write failed. A cart write error is cleared by the\n * NEXT user-initiated mutation (see `mutate`), never by a refresh that happens\n * to run after it.\n */\n const refreshNow = useCallback(async (retriesLeft = 1) => {\n const sdk = getShoppex();\n const version = ++refreshVersion.current;\n const nextLines = sdk.getCart();\n const nextCount = sdk.getCartItemCount();\n /**\n * The cart this refresh is about to ask about, pinned for the settlement.\n *\n * PAYMENT PATH. Taken here rather than at the quote call because only\n * synchronous state writes separate the two, so nothing can move the cart in\n * between — and taken at all because `refreshVersion` does not close this\n * window on its own: a cross-tab write reaches this tab as a `storage` event\n * that schedules a DEBOUNCED refresh, so for the length of that debounce no\n * newer generation exists and a response for the pre-write cart still passes\n * the guard. See `settleQuotedCart`'s `pricedCart`.\n */\n const pricedCart = JSON.stringify(nextLines);\n const coupon = sdk.getCartCoupon()?.trim() ?? '';\n /**\n * The referral the buyer arrived with, recorded — never SUBMITTED — here.\n *\n * PAYMENT PATH. `?ref=` is captured into SDK storage at boot, and the SDK\n * carries it on its OWN `affiliate_code` field in both directions: it sends\n * it with every quote (`getAmbientAffiliateCodeForQuote`) and again with the\n * checkout request. That is what keeps the discount the cart displays equal\n * to the one the invoice charges, and it needs nothing from this theme.\n *\n * What this theme must NOT do is pass it a second time as the quote's\n * `coupon`. Doing so bound the signed quote proof to a requested COUPON as\n * well as a requested affiliate, while the hand-off submits only the\n * affiliate — so `/from-cart` answered `quote_token_stale`, the cart\n * re-quoted, and the identical mismatch came straight back. A buyer whose\n * ambient code no longer resolves (deactivated, expired, mistyped) had no\n * control to clear it and could not check out at all.\n *\n * So it is read for ONE purpose: to remember which referral priced the quote\n * now on screen, so the hand-off can refuse if storage has moved since. It\n * never reaches `couponInput` — the field is the buyer's — and a refusal of\n * it is not a verdict the buyer needs: the quote simply comes back without\n * the discount, no message is raised, and the code is left in storage\n * untouched, because checkout may still accept it under conditions this\n * quote does not describe.\n *\n * Blank while the buyer has a code APPLIED: the quote then prices that code,\n * the hand-off is not in the ambient lane, and the ambient attribution rides\n * along on the invoice exactly as it always has.\n */\n const ambientAffiliate = coupon ? '' : (sdk.getAffiliateCode()?.trim() ?? '');\n\n if (version === refreshVersion.current) {\n setLines(nextLines);\n setCount(nextCount);\n // Never overwrite what the buyer is still typing — and never write the\n // ambient referral here: the field is the buyer's, and a code appearing\n // in it that they did not type reads as one they can edit or remove.\n if (!couponPendingEdit.current) setCouponInput(coupon);\n }\n\n if (nextLines.length === 0) {\n if (version === refreshVersion.current) {\n setQuote(null);\n setQuoteError(null);\n setCheckoutError(clearRecoveredQuoteCheckoutError);\n rememberQuotedAffiliate(null);\n rememberCartMaximum(null);\n unsellableLines.current = [];\n }\n return;\n }\n\n try {\n // ONLY the code the buyer stored. The ambient referral rides the SDK's own\n // `affiliate_code` field — see `ambientAffiliate` above.\n const result = await sdk.quoteCart(coupon || undefined, getBuyerCurrency());\n if (version !== refreshVersion.current) return;\n if (!result.success || !result.data) {\n setQuote(null);\n setQuoteError(result.message?.trim() || 'Cart pricing is unavailable.');\n rememberQuotedAffiliate(null);\n rememberCartMaximum(null);\n unsellableLines.current = [];\n return;\n }\n // Bounds healing, the refused-line verdict and the checkout maximum, all\n // through the ONE implementation the apply path also runs. See\n // `settleQuotedCart`.\n //\n // A `'stale-cart'` answer is DROPPED rather than re-quoted, and that is\n // the whole of what this path owes. Every writer that can move the cart\n // under a refresh schedules its own re-quote — a cross-tab write through\n // the `storage` listener, a same-tab write outside `mutate` through\n // `shoppex:cart-changed`, and `mutate` itself through `scheduleRefresh` —\n // so an answer for the cart that DOES exist is already on its way, and a\n // second one raced against it would only be redundant work.\n if (settleQuotedCart(result.data, ambientAffiliate, version, pricedCart) === 'clamped'\n && retriesLeft > 0) {\n await refreshNow(retriesLeft - 1);\n }\n } catch (error) {\n if (version !== refreshVersion.current) return;\n setQuote(null);\n setQuoteError(errorMessage(error, 'Cart pricing is unavailable.'));\n rememberQuotedAffiliate(null);\n rememberCartMaximum(null);\n unsellableLines.current = [];\n }\n }, [rememberCartMaximum, rememberQuotedAffiliate, settleQuotedCart]);\n\n // Cart mutations arrive in bursts (a stepper held down, a bundle added row by\n // row). Coalescing them into one quote keeps the displayed total from\n // flickering through intermediate states.\n const scheduleRefresh = useCallback(() => new Promise<void>((resolve) => {\n refreshResolvers.current.push(resolve);\n if (refreshTimer.current !== null) {\n window.clearTimeout(refreshTimer.current);\n }\n refreshTimer.current = window.setTimeout(() => {\n refreshTimer.current = null;\n const resolvers = refreshResolvers.current.splice(0);\n void refreshNow().finally(() => {\n resolvers.forEach((finish) => finish());\n });\n }, 60);\n }), [refreshNow]);\n\n useEffect(() => {\n void refreshNow();\n return () => {\n if (refreshTimer.current !== null) window.clearTimeout(refreshTimer.current);\n refreshResolvers.current.splice(0).forEach((resolve) => resolve());\n };\n }, [refreshNow]);\n\n /**\n * Cart writes this context did not make.\n *\n * The cart lives in localStorage, which is shared across every tab on the\n * domain: a buyer with the shop open twice who checks out — or just adds a\n * line — in one tab left the other showing a cart that no longer exists, and\n * its checkout button would then hand off a stale one. `storage` is the\n * browser's own cross-tab signal; `shoppex:cart-changed` is the SDK's, for a\n * write inside THIS tab that did not go through `mutate` (the SDK's own\n * navigation clears the cart after a successful hand-off). Both are the pair\n * the Easy lane's cart store listens to (`connectSxCartStore`).\n *\n * A re-quote follows the re-read for the same reason it does there: the lines\n * changed, so the totals on screen are no longer the quote's.\n */\n useEffect(() => {\n const onExternalChange = () => {\n void scheduleRefresh();\n };\n window.addEventListener('storage', onExternalChange);\n window.addEventListener('shoppex:cart-changed', onExternalChange);\n return () => {\n window.removeEventListener('storage', onExternalChange);\n window.removeEventListener('shoppex:cart-changed', onExternalChange);\n };\n }, [scheduleRefresh]);\n\n /**\n * Runs one SDK cart write, checks it landed, then re-quotes.\n *\n * PAYMENT PATH. A dropped write has TWO shapes and both have to reject:\n *\n * - the SDK throws (a rejected quantity, a line id it cannot find), and\n * - the SDK returns normally having stored nothing. Its localStorage wrapper\n * catches a quota or blocked-storage failure and only logs it, so the write\n * call itself is silent. `verifyWrite` reads the cart back out of the SDK\n * and reports that silence as the failure it is.\n *\n * The rejection is the contract, not an oversight: a caller that CHAINS on the\n * write — Buy Now adds a line and then hands off to checkout — has to be able\n * to stop. A resolved promise after a failed write told Buy Now the line\n * existed, and the buyer was sent to an invoice built from whatever the cart\n * held before, which is the wrong lines and the wrong money.\n *\n * The message is still recorded for the cart surfaces, so the failure is both\n * visible and unignorable. There is no retry here — a write the SDK dropped is\n * reported, never quietly attempted again behind the buyer's back.\n *\n * The failure is recorded in `mutationError`, which only a mutation writes:\n * it is cleared here, at the start of the NEXT buyer-initiated write, and\n * nowhere else. A refresh cannot clear it — see `refreshNow`.\n */\n const mutate = useCallback(async (\n mutation: () => void,\n /**\n * Reads the SDK's cart back and returns the message to fail with when the\n * write is not in it. Called once, immediately after the write, with the\n * serialized cart as it stood one statement BEFORE the write.\n */\n verifyWrite?: (serializedBefore: string) => string | null,\n ) => {\n setBusyCount((value) => value + 1);\n setCheckoutError(null);\n setMutationError(null);\n // A refused code's message survives every refresh (it has to — the code is\n // gone from storage by then), so THIS is what retires it: the next thing the\n // buyer does with their cart.\n setCouponRejected(false);\n try {\n // Taken here rather than at the call site so nothing can slip between the\n // snapshot and the write it is compared against.\n const serializedBefore = verifyWrite ? JSON.stringify(getShoppex().getCart()) : '';\n mutation();\n const unwritten = verifyWrite?.(serializedBefore) ?? null;\n // Refreshed either way: a write that vanished still has to leave every\n // cart surface showing the cart that DOES exist.\n await scheduleRefresh();\n if (unwritten) throw new Error(unwritten);\n } catch (error) {\n setMutationError(errorMessage(error, 'The cart could not be updated.'));\n throw error;\n } finally {\n setBusyCount((value) => Math.max(0, value - 1));\n }\n }, [scheduleRefresh]);\n\n const add = useCallback(async (\n product: StorefrontProduct,\n variant: StorefrontPriceVariant | null,\n quantity: number,\n options?: { payWhatYouWantPrice?: number; addons?: ProductAddon[] },\n ) => {\n if (product.onHold) {\n // PAYMENT PATH. Every surface that offers this product already disables\n // its controls, so this is the single stop behind all of them: a paused\n // product is one the backend refuses to sell, and a line the buyer cannot\n // check out is worse than a button that never worked.\n throw new Error(`${product.name} is not on sale yet.`);\n }\n if (isSelectionSoldOut(product, variant)) {\n // PAYMENT PATH, same stop one step further: the SELECTED option can be out\n // of stock while its product still has units in a sibling. The backend\n // refuses that line at invoice creation, so accepting it here would only\n // move the dead end to checkout — and the buyer would have paid attention\n // to a cart badge that meant nothing.\n throw new Error(\n `${variant === null ? product.name : `${product.name} (${variant.label})`} is out of stock.`,\n );\n }\n const { min: minimum, max: maximum } = resolveQuantityBounds(product, variant);\n const addons = withRequiredAddons(product, options?.addons ?? []);\n /**\n * The buyer's named price for a pay-what-you-want product.\n *\n * PAYMENT PATH. Only the buy box has a field for it; a catalog card, a\n * bundle-group dialog and a related-product tile all add without one. They\n * used to send NO named price at all, and that was wrong twice over: the\n * backend charges such a line at the minimum anyway, so the amount was\n * decided in silence — and `pay_what_you_want_price` is part of the SDK's\n * line identity, so a card add and a buy-box add of the SAME product at the\n * SAME price landed on two different cart lines. The buyer saw one product\n * twice and neither row counted the other's units.\n *\n * So an add without an amount states the floor explicitly: the option's\n * UNDISCOUNTED unit price, which is the number the backend validates\n * against (`resolvePayWhatYouWantUnitPrice`) and the same floor the buy box\n * pre-fills. Same product, same price, one line.\n */\n const payWhatYouWantPrice = options?.payWhatYouWantPrice\n ?? (product.payWhatYouWant ? resolvePayWhatYouWantMinimum(product, variant) : undefined);\n // The price the cart line remembers. Without it the cart can only show a\n // line total of zero until the server quote lands, so it is captured here —\n // on the one path every add goes through — rather than in each caller.\n // For pay-what-you-want the buyer's amount replaces the product price and\n // the extras are added on top exactly once, the same split the backend\n // charges.\n const addonsUnitPrice = addons.reduce((sum, addon) => sum + addon.price, 0);\n const unitPriceSnapshot = payWhatYouWantPrice !== undefined\n ? payWhatYouWantPrice + addonsUnitPrice\n : calculateLinePricing(product, variant, addons, quantity).effectiveUnitPrice;\n const variantId = variant?.id ?? 'default';\n const addOptions: SdkCartAddOptions = {\n ...(variant ? { price_variant_id: variant.id } : {}),\n price_data: { unit_price: unitPriceSnapshot },\n ...(payWhatYouWantPrice !== undefined\n ? { pay_what_you_want_price: payWhatYouWantPrice }\n : {}),\n // Addon ids are part of the line's identity — the SDK hashes them into\n // `line_id`, so a line added with extras never merges with a plain one.\n ...(addons.length > 0\n ? {\n addons: addons.map((addon) => ({ id: addon.id })),\n addon_labels: addons.map((addon) => addon.title),\n }\n : {}),\n title: product.name,\n variant_title: variant?.label,\n image_url: variant?.imageUrl ?? product.imageUrl ?? undefined,\n max_quantity: maximum ?? undefined,\n min_quantity: minimum > 1 ? minimum : undefined,\n replace_display_snapshot: true,\n };\n // The identity is read off the SAME object the write sends, so the two can\n // never describe different lines.\n const identity = cartLineIdentity({ product_id: product.id, variant_id: variantId, ...addOptions });\n await mutate(\n () => getShoppex().addToCart(product.id, variantId, quantity, addOptions),\n (serializedBefore) => unwrittenAdd(\n serializedBefore,\n identity,\n minimum,\n maximum,\n 'This product could not be added to your cart.',\n ),\n );\n }, [mutate]);\n\n const addBundleMember = useCallback(async (item: ProductBundleItem) => {\n const member = resolveBundleMember(item);\n if (member === null || member.product.onHold || isSelectionSoldOut(member.product, member.variant)) {\n // Fail fast, for three reasons that end the same way. A member whose real\n // bounds cannot be read must not be written as a line the merchant's\n // per-order limits no longer apply to; a member the merchant has PAUSED\n // must not be sold at all; a member whose selected option is OUT OF STOCK\n // is a line the backend refuses at invoice creation. A bundle missing any\n // of them can never unlock its discount anyway. `addBundle` turns any of\n // them into the one error the buyer sees, after it has taken the earlier\n // members back out.\n throw new Error(`${item.title} is not available and was not added to your cart.`);\n }\n const { bounds } = member;\n // A pay-what-you-want member has no named price in the offer, and the\n // backend charges the undiscounted minimum for a line without one. Stating\n // that floor explicitly is what puts this add on the SAME cart line the\n // buyer's own add of that product produces — `pay_what_you_want_price` is\n // part of the line identity, so a silent one splits the line in two. See\n // `add`.\n const payWhatYouWantPrice = member.product.payWhatYouWant\n ? resolvePayWhatYouWantMinimum(member.product, member.variant)\n : undefined;\n // PAYMENT PATH: a bundle member carries its required add-ons like any other\n // line (see `withRequiredAddons`), and their price is part of its snapshot.\n const requiredAddons = withRequiredAddons(member.product, []);\n const addonsUnitPrice = requiredAddons.reduce((sum, addon) => sum + addon.price, 0);\n const addOptions: SdkCartAddOptions = {\n ...(item.variantId !== 'default' ? { price_variant_id: item.variantId } : {}),\n // The offer states each member's unit price; a line without that\n // snapshot shows no money in the cart until the quote answers. A named\n // price overrides it, because that is the amount the backend charges.\n ...(payWhatYouWantPrice !== undefined\n ? { price_data: { unit_price: payWhatYouWantPrice + addonsUnitPrice }, pay_what_you_want_price: payWhatYouWantPrice }\n : item.unitPrice > 0 ? { price_data: { unit_price: item.unitPrice + addonsUnitPrice } } : {}),\n ...(requiredAddons.length > 0\n ? {\n addons: requiredAddons.map((addon) => ({ id: addon.id })),\n addon_labels: requiredAddons.map((addon) => addon.title),\n }\n : {}),\n title: item.title,\n ...(item.variantTitle ? { variant_title: item.variantTitle } : {}),\n ...(item.imageUrl ? { image_url: item.imageUrl } : {}),\n // The member's OWN bounds, sent for the same reason the buy box sends\n // them: this write is a full re-snapshot, so whatever it leaves out is\n // CLEARED off the line it merges into.\n max_quantity: bounds.max ?? undefined,\n min_quantity: bounds.min > 1 ? bounds.min : undefined,\n replace_display_snapshot: true,\n };\n // Same exact identity as the buy box uses, read off the same object that is\n // written — a bundle member is a cart line like any other, and matching it\n // on anything less would let an unrelated line answer for this write.\n const identity = cartLineIdentity({\n product_id: item.id,\n variant_id: item.variantId,\n ...addOptions,\n });\n // Checked by exactly the rule the buy box's add is checked by, and only\n // because the bounds above are the member's real ones: the write has to have\n // moved the cart, or this line has to be sitting at the ceiling those bounds\n // impose. See `unwrittenAdd`.\n await mutate(\n () => getShoppex().addToCart(item.id, item.variantId, 1, addOptions),\n (serializedBefore) => unwrittenAdd(\n serializedBefore,\n identity,\n bounds.min,\n bounds.max,\n 'This item could not be added to your cart.',\n ),\n );\n }, [mutate]);\n\n /**\n * Adds a whole bundle offer, all of it or none of it.\n *\n * PAYMENT PATH. The members were added independently and in parallel, and\n * both halves of that were wrong. A member that failed after its siblings\n * landed left the buyer holding PART of a bundle — full price for products\n * they only wanted as a set, since the discount the offer promised is applied\n * by the backend only for the complete set. And because every sibling write\n * refreshed the cart, a sibling that succeeded after the failure wiped the\n * failure off the screen, so the buyer was not even told.\n *\n * So the members go in one at a time, and the first failure ends the\n * operation: everything THIS click already put in comes back out, in reverse\n * order, and the buyer is told once that the bundle could not be added. The\n * undo is derived from the cart itself around each write (`diffBundleMemberAdd`)\n * rather than assumed, so a member that merged into a line the buyer already\n * had gives that line its units back instead of the line being removed with\n * them.\n *\n * Undo is best effort by necessity — it runs on a cart that has just proven it\n * can refuse a write. A failed undo cannot be reported as a second error\n * without contradicting the first, so it is not: the error stands, the refresh\n * that follows puts every surface back on the cart that DOES exist, and the\n * buyer reads it there.\n */\n const addBundle = useCallback(async (items: readonly ProductBundleItem[]) => {\n const undo: BundleMemberUndo[] = [];\n // Held for the whole sequence, so the offer's buttons stay disabled across\n // the members instead of flickering back to enabled between them.\n setBusyCount((value) => value + 1);\n // This click is the buyer-initiated action that clears the last one's\n // failure. Stated here as well as in `mutate`, because a bundle whose first\n // member cannot be resolved never reaches a write.\n setCheckoutError(null);\n setMutationError(null);\n try {\n for (const item of items) {\n const before = snapshotLines(getShoppex().getCart());\n try {\n await addBundleMember(item);\n } catch (error) {\n // The failing member is diffed too: whatever partial effect it did\n // leave belongs to this operation and comes back out with the rest.\n const partial = diffBundleMemberAdd(before, getShoppex().getCart());\n if (partial !== null) undo.push(partial);\n const message = 'This bundle could not be added to your cart.';\n setMutationError(message);\n for (const entry of [...undo].reverse()) {\n try {\n if (entry.quantity === null) getShoppex().removeFromCart(entry.lineId);\n else getShoppex().updateCartItem(entry.lineId, { quantity: entry.quantity });\n } catch {\n // Best effort, and deliberately silent — see above.\n }\n }\n // Runs AFTER the message is set, and must leave it standing: this is\n // the successful refresh that used to erase the failure.\n await scheduleRefresh();\n throw error instanceof Error ? error : new Error(message);\n }\n const applied = diffBundleMemberAdd(before, getShoppex().getCart());\n if (applied !== null) undo.push(applied);\n }\n } finally {\n setBusyCount((value) => Math.max(0, value - 1));\n }\n }, [addBundleMember, scheduleRefresh]);\n\n const update = useCallback(\n async (lineId: string, quantity: number) =>\n mutate(() => getShoppex().updateCartItem(lineId, { quantity })),\n [mutate],\n );\n const remove = useCallback(\n async (lineId: string) => mutate(() => getShoppex().removeFromCart(lineId)),\n [mutate],\n );\n const clear = useCallback(async () => mutate(() => getShoppex().clearCart()), [mutate]);\n\n const editCouponInput = useCallback((value: string) => {\n couponPendingEdit.current = true;\n setCouponInput(value);\n }, []);\n\n /**\n * Applies the code in the field — after the SERVER has said what it is.\n *\n * PAYMENT PATH, and the order of operations is the whole point.\n *\n * The code is quoted FIRST, as a candidate, and nothing is stored until that\n * quote comes back. Only then is it saved, together with the classification\n * the quote gave it (`coupon_source`): a merchant coupon and an affiliate\n * referral are typed into the same box and paid out completely differently,\n * and the SDK defaults an unclassified code to `'COUPON'` — so a referral\n * stored without its source is submitted as a coupon at checkout and the\n * affiliate is never credited for the sale they brought.\n *\n * A code the quote REFUSED is removed from storage and from the field. It\n * used to be kept: flagged as rejected for display, and then submitted at\n * checkout anyway, because checkout reads the stored code. The message that\n * explains the refusal is deliberately the only thing that survives — see\n * `couponRejected`.\n *\n * Both writes are conditional on storage not having moved under them. See the\n * stale check below.\n */\n const applyCoupon = useCallback(async () => {\n const coupon = couponInput.trim();\n // Submitted: the field may track the cart again.\n couponPendingEdit.current = false;\n if (!coupon) {\n await mutate(() => getShoppex().clearCartCoupon());\n return;\n }\n\n setBusyCount((value) => value + 1);\n setCheckoutError(null);\n setMutationError(null);\n setCouponRejected(false);\n try {\n const sdk = getShoppex();\n /**\n * What cart storage held when this apply started.\n *\n * PAYMENT PATH. The quote below is a round trip, and the code lives in\n * localStorage, which every tab on the domain shares. A second tab can\n * apply — or clear — a code while this one waits, and the answer that then\n * comes back was taken against a cart that no longer exists: its verdict,\n * its totals and its storage write all describe the code this tab sent,\n * not the code the buyer now has.\n *\n * Both of the writes below used to run regardless. A refusal of THIS\n * tab's code called `clearCartCoupon` and deleted the OTHER tab's accepted\n * one, so a buyer who had just applied a working code silently lost it and\n * paid full price; an acceptance overwrote it with the older code. So the\n * response is compared against storage before anything acts on it, and a\n * stale one is dropped in silence — the cross-tab write already scheduled\n * its own refresh (see the `storage` listener), which is what puts this\n * tab back on the cart that DOES exist.\n */\n const storedBefore = sdk.getCartCoupon();\n /**\n * What the CART held when this apply started — the second thing this round\n * trip has to pin, and for the same reason as the first.\n *\n * PAYMENT PATH. The code is not the only shared thing a second tab can\n * move while this quote is in flight: the cart is in the same\n * localStorage, and a line added, removed or re-quantified over there\n * makes the response describe a cart that no longer exists. Its totals,\n * its refused-line verdict and its checkout-maximum state are all for the\n * old cart, and its `lines` heal stored bounds that can clamp the very\n * line the buyer just changed in the other tab. `refreshVersion` cannot\n * catch it here: this path BUMPS the generation on acceptance (to retire a\n * refresh that quoted without the code) and then settles under the new\n * one, so the superseded check passes by construction.\n *\n * And unlike the code above, a stale answer here may NOT simply be\n * dropped. The cross-tab write does schedule its own refresh — but that\n * refresh quotes the STORED code, and this apply deliberately stores\n * nothing until the server has accepted: so it would price the cart\n * without the buyer's code and end their click in silence, spinner off,\n * code still in the box, no discount and no statement that it was ever\n * considered. A refusal the buyer can read is a different thing from an\n * apply that evaporated. So the answer is refused and the SAME question is\n * asked again, against the cart that now exists.\n *\n * Acceptance itself is cart-dependent — a minimum-order coupon resolves on\n * the cart it was quoted with — so the pin is checked BEFORE the verdict\n * is read, not just before the totals are settled.\n */\n let pricedCart = JSON.stringify(sdk.getCart());\n // The candidate quote. Nothing is persisted yet, so a refused code never\n // reaches storage in the first place.\n let result = await sdk.quoteCart(coupon, getBuyerCurrency());\n if (sdk.getCartCoupon() !== storedBefore) return;\n if (JSON.stringify(sdk.getCart()) !== pricedCart) {\n // ONE follow-up quote, bounded exactly as the clamp retry is: this is\n // the buyer's click, so it is worth re-asking, and it is one click, so\n // it is not worth chasing a cart another tab is still rewriting.\n pricedCart = JSON.stringify(sdk.getCart());\n result = await sdk.quoteCart(coupon, getBuyerCurrency());\n if (sdk.getCartCoupon() !== storedBefore) return;\n if (JSON.stringify(sdk.getCart()) !== pricedCart) {\n /**\n * Two round trips, both outrun. Nothing is settled and nothing is\n * stored — the code was never approved for any cart that still\n * exists — and the cart is handed back to the refresh path so what is\n * on screen is the cart that DOES exist, priced.\n *\n * The typed code is marked pending again so that refresh does not\n * erase it: the field is the buyer's, and this click ended without a\n * verdict, so it must end where it began — their code in the box, the\n * Apply control live, and no total claiming a discount nobody priced.\n */\n couponPendingEdit.current = true;\n await refreshNow();\n return;\n }\n }\n if (!result.success || !result.data) {\n // The server did not answer. That is a pricing failure, not a verdict on\n // the code: storing it would save something unapproved, and reporting it\n // as refused would blame the buyer for an outage.\n setQuote(null);\n setQuoteError(result.message?.trim() || 'Cart pricing is unavailable.');\n rememberQuotedAffiliate(null);\n rememberCartMaximum(null);\n return;\n }\n const source = readAcceptedCouponSource(result.data);\n if (source === null) {\n sdk.clearCartCoupon();\n setCouponInput('');\n setCouponRejected(true);\n // Re-quote without the code, so the totals stop reflecting a candidate\n // that was refused.\n await refreshNow();\n return;\n }\n sdk.setCartCoupon(coupon, source);\n // Any refresh already in flight quoted the cart WITHOUT this code, so its\n // answer is stale the moment the code is stored. Retiring it here stops it\n // landing afterwards and putting the pre-discount totals back on screen.\n refreshVersion.current += 1;\n /**\n * This quote IS the cart's quote now — it was taken with exactly the code\n * that has just been stored, so re-fetching it would be a second round\n * trip for the same answer. It therefore settles through the SAME\n * sequence a refresh's quote does.\n *\n * PAYMENT PATH. It used to be assigned straight onto the quote state,\n * which left every verdict the payload carries unread: the stored bounds\n * kept a ceiling the shelf no longer had, the refused-line list still\n * described the PREVIOUS quote's cart, and the checkout maximum was never\n * looked at at all. A cart that had just become unsellable stayed\n * checkout-enabled until the backend rejected the click, and one that had\n * been replenished stayed blocked by a verdict that no longer held.\n *\n * `''` for the referral, buyer-applied by definition: this is the one path\n * a typed code takes, so the quote on screen was priced with NO ambient\n * referral.\n *\n * `pricedCart` is the pin this path already checked. It cannot fail here —\n * only `readAcceptedCouponSource`, `setCartCoupon` and the generation bump\n * stand between the check and this call, none of which touch the cart, and\n * none of which yield — but it is passed rather than skipped because the\n * settlement, not its callers, is what must never write over a cart its\n * quote did not describe.\n */\n const settlement = settleQuotedCart(result.data, '', refreshVersion.current, pricedCart);\n // The field shows what was actually STORED — the SDK normalizes a code on\n // the way in (trimmed, uppercased), and that normalized string is the one\n // checkout submits. A chip showing the buyer's typing instead would name a\n // code slightly different from the one on the invoice.\n setCouponInput(sdk.getCartCoupon() ?? coupon);\n // A heal clamped a quantity, so this quote priced units the cart no\n // longer holds. One follow-up quote, exactly as the refresh path bounds\n // it — the second pass finds the bounds already in agreement.\n if (settlement === 'clamped') await refreshNow();\n } catch (error) {\n setQuote(null);\n setQuoteError(errorMessage(error, 'Cart pricing is unavailable.'));\n rememberQuotedAffiliate(null);\n rememberCartMaximum(null);\n } finally {\n setBusyCount((value) => Math.max(0, value - 1));\n }\n }, [couponInput, mutate, refreshNow, rememberCartMaximum, rememberQuotedAffiliate, settleQuotedCart]);\n\n const removeCoupon = useCallback(async () => {\n couponPendingEdit.current = false;\n await mutate(() => getShoppex().clearCartCoupon());\n }, [mutate]);\n\n /**\n * The way OUT of a self-referral refusal.\n *\n * PAYMENT PATH. `affiliate_code_not_applicable` is the only refusal the buyer\n * has no control for. The code is ambient: captured from `?ref=` at boot into\n * storage the SDK owns, never typed, never shown, and re-read on every quote\n * and every hand-off — so the cart re-submits it, and the backend refuses it,\n * for as long as it sits there. Without this, an affiliate who opens their own\n * shop through their own link cannot buy from it at all.\n *\n * The attribution is dropped through the SDK's own `clearAffiliateCode()`\n * rather than by deleting its storage key: the key, its shape and its TTL are\n * the SDK's, and a theme writing them directly is a second implementation of\n * something that already exists.\n *\n * Both lanes it can arrive on are cleared, because the refusal names the\n * `affiliate_code` field and both fill it:\n * - ambient — the stored `?ref=` code, cleared above.\n * - applied — a referral the buyer typed, which the quote classified as\n * `AFFILIATE` and which the hand-off submits under `affiliateCode`. A\n * merchant COUPON is deliberately left alone; it is not what was refused.\n *\n * Then re-quote: `refreshNow` re-reads storage that no longer has a referral,\n * prices the cart without one, and records `''` as the code this quote used —\n * which is exactly what the next hand-off pins onto the request (the SDK's\n * `''` tri-state, \"explicitly none\"). So the retry submits no referral at all,\n * and the total the buyer approves is the total they are charged.\n */\n const clearBlockedReferral = useCallback(async () => {\n const sdk = getShoppex();\n sdk.clearAffiliateCode();\n if (sdk.getCartCouponSource() === 'AFFILIATE') {\n sdk.clearCartCoupon();\n couponPendingEdit.current = false;\n setCouponInput('');\n }\n setSelfReferralBlocked(false);\n setCheckoutError(null);\n setCouponRejected(false);\n await refreshNow();\n }, [refreshNow]);\n\n /**\n * Hands the buyer over to Hosted Checkout.\n *\n * PAYMENT PATH. The theme never takes money and never decides a price: the\n * SDK posts the cart, the backend creates the invoice and returns the hosted\n * checkout URL, and the SDK navigates to it (and only then clears the cart).\n * Everything this function owns is what the cart submits, and the failure\n * surface.\n *\n * - The buyer's per-product details are written onto the CART LINES first,\n * because that is where the invoice reads them from (`custom_fields` per\n * line) — the checkout call has no field for them. A required value that\n * did not land aborts the hand-off rather than creating a request the\n * backend refuses with `custom_fields_validation_failed`.\n * - The code AND the field it is submitted under are read out of the SDK's\n * own storage, at click time. See `resolveSubmittedCode`.\n * - With no code in storage, an explicitly EMPTY coupon is sent. Omitting it\n * would not mean \"none\": the SDK then falls back to reading cart storage\n * itself, and this hand-off has already read it.\n * - There must be a SETTLED quote. An estimate is this theme's arithmetic,\n * not a price, and the invoice would be built from a different one.\n * - The AMBIENT referral the quote used must still be the one storage holds,\n * or the hand-off stops and re-quotes instead of pricing an invoice the\n * cart never showed. See the comparison below — and the pin under it, which\n * is what makes the comparison describe the request that is actually sent.\n * - The referral is ALWAYS named on the request, including as an empty string\n * when the quote was priced without one. Omitted, the SDK reads its own\n * referral storage instead; empty, it submits none. See the pin below.\n * - The currency is the one this page was priced in. Omitted, the SDK falls\n * back to reading `?currency=` off the URL, which a client-side navigation\n * has already dropped — the buyer would be charged in another currency than\n * the one on screen.\n * - A resolved result that is not an actual redirect is treated as a\n * FAILURE. Reporting success without navigating would leave the buyer on a\n * silent page believing an order exists.\n */\n const checkout = useCallback(async (customFieldValues?: CheckoutFieldValues) => {\n setBusyCount((value) => value + 1);\n setCheckoutError(null);\n // A new attempt is not the refused one: the exit control must not outlive\n // the message it belongs to.\n setSelfReferralBlocked(false);\n try {\n const sdk = getShoppex();\n /**\n * No settled quote, no hand-off.\n *\n * PAYMENT PATH. Without one the summary prints an ESTIMATE — the sum of\n * the stored price snapshots, with no coupon, no deal and no referral\n * applied (`buildCartTotals`) — and the invoice is priced by the server\n * from scratch. A pricing outage therefore let a buyer approve a number\n * this theme made up and be charged a different one, with the outage\n * notice sitting right above a live button.\n *\n * `quotedAffiliate.current` IS that fact: `rememberQuotedAffiliate` sets\n * it to null on an empty cart, a refused quote and a thrown one, and to a\n * string only when a quote answered for the lines on screen. The checkout\n * controls are disabled on the same condition, and this is the guard for\n * the paths that are not a button — Buy Now, and the details form.\n */\n if (quotedAffiliate.current === null) {\n setCheckoutError(QUOTE_UNAVAILABLE_CHECKOUT_ERROR);\n return;\n }\n /**\n * A line the quote REFUSED, no hand-off.\n *\n * PAYMENT PATH. `sellable: false` is the server's own statement that this\n * cart cannot be ordered as it stands, and it is the same cart the signed\n * quote proof describes — a settled total is not permission to order units\n * the shelf does not have. Handing off anyway spends the buyer's click on\n * a rejection that arrives from the checkout naming no line at all.\n *\n * NOT a re-quote loop: the cart has to CHANGE for this to clear, so no\n * re-quote is asked for here — the answer would be the same one. The\n * message stands until the buyer reduces or removes the line.\n */\n if (unsellableLines.current.length > 0) {\n setCheckoutError(\n 'Some items in your cart are not available in the quantity requested. '\n + 'Reduce or remove them, then check out again.',\n );\n return;\n }\n /**\n * A cart over the shop's checkout MAXIMUM, no hand-off.\n *\n * PAYMENT PATH, and stopped exactly the way the refused line above is.\n * The quote has already told this cart that `/from-cart` will refuse it\n * (`cart_max_exceeded`), so handing off anyway spends the buyer's click\n * on a `cart_maximum_checkout_exceeded` rejection that names no amount\n * and offers no way forward. The message names the limit instead, and it\n * is the same sentence the summary is printing next to the total.\n *\n * NOT a re-quote loop: the cart has to CHANGE for this to clear, so no\n * re-quote is asked for — the answer would be the same one. The message\n * stands until the buyer removes something.\n */\n if (cartMaximumWarning.current !== null) {\n setCheckoutError(cartMaximumWarning.current);\n return;\n }\n /**\n * The answers go onto the lines BEFORE anything is priced or submitted,\n * and a write that did not leave the cart submittable stops here.\n *\n * PAYMENT PATH. Two shapes of failure, one exit. A write the SDK dropped\n * leaves a required value off a line, which `/from-cart` refuses outright\n * as `custom_fields_validation_failed`. A write that MERGED two lines of\n * one product left every answer in place but summed their quantities,\n * which stales the signed quote proof this hand-off would have carried —\n * `/from-cart` refuses that as `quote_token_stale`, and the buyer pays a\n * round trip for a cart change they did not make.\n *\n * Both re-quote before returning: the cart on screen may have gained\n * answers, lost a line, or both, and every surface has to re-read what\n * actually exists so the total the buyer approves next is the one this\n * cart would be charged.\n */\n if (customFieldValues !== undefined) {\n const failure = writeCheckoutFieldsToCart(customFieldValues);\n if (failure !== null) {\n setCheckoutError(failure);\n await scheduleRefresh();\n return;\n }\n }\n const submitted = resolveSubmittedCode(sdk);\n if (submitted === null) {\n setCheckoutError(\n 'Your discount code could not be verified. Remove it, or apply it again, to continue.',\n );\n return;\n }\n /**\n * The quote on screen and the invoice must be priced with the SAME\n * referral.\n *\n * PAYMENT PATH. When nothing is applied, the hand-off submits no\n * affiliate code of its own and the SDK reads the ambient one out of its\n * storage at THIS moment (`normalizeAffiliateCode`), while the total the\n * buyer is looking at was priced with whatever that storage held when the\n * quote ran. Storage is shared across every tab on the domain: a second\n * tab arriving on `?ref=` — or one that cleared the attribution — moves\n * the invoice off the displayed total in either direction, and the buyer\n * approves a number the order does not use.\n *\n * So the two reads are compared, and a divergence stops the hand-off\n * rather than pricing it: the cart re-quotes with the referral that is\n * actually stored, and the buyer is asked to look at the new total. This\n * is the same compare-and-set the coupon path runs before it writes\n * (`applyCoupon`), applied to the code nobody typed.\n *\n * Only in the ambient lane. A code the buyer APPLIED is submitted\n * explicitly — as `coupon` or as `affiliateCode`, from storage this\n * hand-off already read — so the SDK never falls back for it and the\n * quote was priced with exactly that code.\n */\n const quotedAmbient = quotedAffiliate.current;\n const isAmbientLane = submitted.coupon === '' && submitted.affiliateCode === undefined;\n // ONE read of the ambient referral for the whole hand-off. The check\n // below and the code pinned onto the request must be the same read, or\n // the check answers about a value the request does not carry.\n const ambientNow = normalizeAmbientAffiliate(sdk.getAffiliateCode());\n if (isAmbientLane && ambientNow !== normalizeAmbientAffiliate(quotedAmbient)) {\n setCheckoutError(\n 'The referral discount on this cart changed. Review the updated total, then check out again.',\n );\n await scheduleRefresh();\n return;\n }\n /**\n * The referral the request CARRIES, decided here rather than by the SDK.\n *\n * PAYMENT PATH, and the half of the problem the comparison above cannot\n * solve. Omit `affiliateCode` and `sdk.checkout()` reads the ambient one\n * out of shared storage itself, a second time, on its way into the\n * request (`normalizeAffiliateCode`). The check above is therefore a\n * check on a value nobody submitted: a write landing between the two\n * reads still ships a code the quote never saw. Naming it closes that —\n * an explicitly named code is the only thing the SDK does not re-read\n * around.\n *\n * Which code, per lane, is exactly what is true today:\n * - ambient lane — the referral the CURRENT quote was priced with. Equal\n * to `ambientNow` by the check that just passed; taken from the quote\n * because that is the total the buyer approved.\n * - merchant-coupon lane — the quote deliberately ignored the referral\n * (one code per quote, and an applied coupon outranks it), while the\n * invoice has always carried the attribution anyway. So the ambient\n * read is pinned as-is: same attribution, one read instead of two.\n * - applied-referral lane — `resolveSubmittedCode` already named it.\n *\n * With NO referral in play the empty string is submitted rather than the\n * field being omitted, because the SDK's `affiliateCode` is a tri-state:\n * `''` means \"explicitly none\" and suppresses the storage read, while an\n * omitted field sends the SDK back to storage. Omitting it therefore left\n * one window open — a `?ref=` captured in another tab between this click\n * and the request still rode along on a total priced without it. Pinning\n * \"none\" closes it.\n */\n const pinnedAffiliateCode = submitted.affiliateCode\n ?? (isAmbientLane ? normalizeAmbientAffiliate(quotedAmbient) : ambientNow);\n const result = await sdk.checkout({\n ...submitted,\n affiliateCode: pinnedAffiliateCode,\n currency: getBuyerCurrency(),\n });\n if (!result.success) {\n /**\n * The refusal that would otherwise be a dead end. Every other failure\n * describes something the buyer can act on from the cart they can see;\n * this one names a code nothing on screen shows, so it gets its own\n * copy and an explicit exit rather than the backend's prose plus a\n * retry button that refuses identically.\n */\n if (result.code === SELF_REFERRAL_REFUSAL_CODE) {\n setSelfReferralBlocked(true);\n setCheckoutError(SELF_REFERRAL_MESSAGE);\n return;\n }\n const quoteProofMessage = result.code\n ? QUOTE_PROOF_REFUSAL_MESSAGES[result.code]\n : undefined;\n if (quoteProofMessage) {\n // The proof this hand-off carried was refused. Re-quote, so the next\n // attempt carries one for THIS cart and the buyer approves the total\n // that attempt would actually be charged.\n setCheckoutError(quoteProofMessage);\n await scheduleRefresh();\n return;\n }\n const refusalMessage = result.code ? CHECKOUT_REFUSAL_MESSAGES[result.code] : undefined;\n setCheckoutError(refusalMessage ?? (result.message?.trim() || 'Checkout could not be started.'));\n return;\n }\n if (!result.redirectUrl) {\n setCheckoutError('Checkout could not be started.');\n }\n } catch (error) {\n setCheckoutError(errorMessage(error, 'Checkout could not be started.'));\n } finally {\n setBusyCount((value) => Math.max(0, value - 1));\n }\n }, [scheduleRefresh]);\n\n // Recomputed from the lines rather than stored: the questions a cart raises\n // are a function of what is in it, and a second copy of that could disagree\n // with the cart it describes.\n const checkoutFields = useMemo(() => resolveCheckoutFields(lines), [lines]);\n\n const value = useMemo<CartContextValue>(() => ({\n lines,\n count,\n quote,\n quoteError,\n mutationError,\n couponRejected,\n quotedAmbientAffiliate,\n cartMaxWarning,\n requiresShipping: quote?.requires_shipping === true,\n checkoutError,\n selfReferralBlocked,\n clearBlockedReferral,\n checkoutFields,\n checkoutNeedsDetails: needsCheckoutDetails(checkoutFields),\n busy: busyCount > 0,\n couponInput,\n setCouponInput: editCouponInput,\n drawerOpen,\n setDrawerOpen,\n add,\n addBundle,\n update,\n remove,\n clear,\n applyCoupon,\n removeCoupon,\n checkout,\n }), [\n add,\n addBundle,\n applyCoupon,\n busyCount,\n cartMaxWarning,\n checkout,\n checkoutError,\n checkoutFields,\n clear,\n clearBlockedReferral,\n count,\n couponInput,\n couponRejected,\n drawerOpen,\n editCouponInput,\n lines,\n mutationError,\n quote,\n quoteError,\n quotedAmbientAffiliate,\n remove,\n removeCoupon,\n selfReferralBlocked,\n update,\n ]);\n\n return <CartContext.Provider value={value}>{children}</CartContext.Provider>;\n}\n\nexport function useCart(): CartContextValue {\n const context = useContext(CartContext);\n if (!context) throw new Error('useCart must be used inside CartProvider.');\n return context;\n}\n\n/**\n * Starts a cart mutation from a control that has nothing to chain onto it.\n *\n * Every mutation now REJECTS when the write fails, which is what lets Buy Now\n * stop before checkout. A stepper, a remove button or a bundle offer has no next\n * step to stop: its failure is already rendered from `mutationError` on the\n * drawer and `/cart`, the surfaces those controls live on. This states that at\n * the call site rather than leaving the rejection unhandled — the error is\n * displayed, not swallowed.\n */\n/**\n * The add-ons a cart line carries: the caller's choice plus every add-on the\n * merchant marked required, once each.\n *\n * PAYMENT PATH. The buy box preselects and locks required add-ons, but a card,\n * a bundle dialog or a related tile adds without choosing any. Without this the\n * line, its quote and the invoice omit a surcharge the merchant made mandatory.\n */\nexport function withRequiredAddons(product: StorefrontProduct, chosen: readonly ProductAddon[]): ProductAddon[] {\n const ids = new Set(chosen.map((addon) => addon.id));\n return [...chosen, ...product.addons.filter((addon) => addon.required && !ids.has(addon.id))];\n}\n\nexport function startCartMutation(mutation: Promise<void>): void {\n void mutation.catch(() => undefined);\n}\n","/**\n * Strict parsing helpers shared by the content contract (`src/config`) and the\n * storefront data contract (`src/commerce`).\n *\n * The rule both contracts follow: an ABSENT value may fall back to a documented\n * default, a PRESENT but malformed one throws. Silently repairing bad input\n * would hide a broken content draft or a changed wire contract behind a\n * storefront that merely looks slightly wrong.\n */\n\nexport function isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nexport function requireUnknownRecord(value: unknown, context: string): Record<string, unknown> {\n if (!isRecord(value)) {\n throw new Error(`Invalid Shoppex ${context}: expected an object.`);\n }\n return value;\n}\n\nexport function requireRecord(\n record: Record<string, unknown>,\n key: string,\n): Record<string, unknown> {\n return requireUnknownRecord(record[key], key);\n}\n\nexport function requireArray(record: Record<string, unknown>, key: string): unknown[] {\n const value = record[key];\n if (!Array.isArray(value)) {\n throw new Error(`Invalid Shoppex config: ${key} must be an array.`);\n }\n return value;\n}\n\nexport function requireString(\n record: Record<string, unknown>,\n key: string,\n context: string,\n): string {\n const value = record[key];\n if (typeof value !== 'string' || value.trim().length === 0) {\n throw new Error(`Invalid Shoppex ${context}: ${key} must be a non-empty string.`);\n }\n return value;\n}\n\nexport function requireStringFromKeys(\n record: Record<string, unknown>,\n keys: string[],\n context: string,\n): string {\n for (const key of keys) {\n const value = optionalString(record[key]);\n if (value) return value;\n }\n throw new Error(`Invalid Shoppex ${context}: ${keys.join(' or ')} must be a non-empty string.`);\n}\n\nexport function requireNumber(\n record: Record<string, unknown>,\n key: string,\n context: string,\n): number {\n const value = record[key];\n if (typeof value !== 'number' || !Number.isFinite(value)) {\n throw new Error(`Invalid Shoppex ${context}: ${key} must be a finite number.`);\n }\n return value;\n}\n\n/** A non-empty trimmed string, or null for absent/blank/non-string input. */\nexport function optionalString(value: unknown): string | null {\n return typeof value === 'string' && value.trim().length > 0 ? value.trim() : null;\n}\n\n/** The first usable string among several candidate keys. */\nexport function firstOptionalString(\n record: Record<string, unknown>,\n keys: string[],\n): string | null {\n for (const key of keys) {\n const value = optionalString(record[key]);\n if (value) return value;\n }\n return null;\n}\n\n/**\n * A visible string that may be absent from an older content draft. Absent or\n * blank renders the shipped wording; a present non-string is still an error.\n */\nexport function optionalStringWithDefault(\n value: unknown,\n fallback: string,\n context: string,\n): string {\n if (value === undefined || value === null || value === '') return fallback;\n if (typeof value !== 'string') {\n throw new Error(`Invalid Shoppex ${context}: expected a string.`);\n }\n return value.trim() || fallback;\n}\n\n/**\n * A boolean the serializer ALWAYS emits, so its absence is a broken payload.\n *\n * Defaulting one of these is worse than throwing: a merchant switch that never\n * arrives silently reads as whatever the theme guessed, and the storefront then\n * offers something the backend refuses. `0`/`1` are accepted because the wire\n * carries some flags that way; anything else is an error.\n */\nexport function requireBoolean(value: unknown, context: string): boolean {\n if (typeof value === 'boolean') return value;\n if (value === 0 || value === 1) return value === 1;\n throw new Error(`Invalid Shoppex ${context}: expected a boolean.`);\n}\n\nexport function optionalBoolean(value: unknown, fallback: boolean, context: string): boolean {\n if (value === undefined || value === null) return fallback;\n if (typeof value === 'boolean') return value;\n if (value === 0 || value === 1) return value === 1;\n throw new Error(`Invalid Shoppex ${context}: expected a boolean.`);\n}\n\nexport function parseFiniteNumber(value: unknown, context: string): number {\n const parsed = typeof value === 'number' ? value : Number(value);\n if (!Number.isFinite(parsed)) {\n throw new Error(`Invalid Shoppex ${context}: expected a finite number.`);\n }\n return parsed;\n}\n\n/**\n * One of a fixed option set. Absent falls back to the theme default; anything\n * else throws, because a typo in an enum is a broken content draft, not a\n * missing field — and the wrong branch of a layout switch is worse than a\n * visible error.\n */\nexport function parseEnum<T extends string>(\n value: unknown,\n allowed: readonly T[],\n fallback: T,\n context: string,\n): T {\n if (value === undefined || value === null || value === '') return fallback;\n if (typeof value !== 'string' || !(allowed as readonly string[]).includes(value)) {\n throw new Error(`Invalid Shoppex ${context}: expected one of ${allowed.join(', ')}.`);\n }\n return value as T;\n}\n","/**\n * Merchant-authored product badges (\"New!\", \"Limited\", \"Best value\").\n *\n * Purely decorative merchandising the merchant attaches in the dashboard. They\n * never affect price, stock or fulfilment, and they never speak for the\n * storefront: a badge cannot say a product is available, on sale, or in stock —\n * those remain the theme's own signals, derived from real fields.\n *\n * Ported rather than imported, for the same reason `video-embed.ts` is: this\n * theme builds standalone from `templates/code-storefronts/`, in an offline\n * sandbox, with no workspace dependency on `@shoppex/builder-contracts`. The\n * canonical contract is `packages/builder-contracts/src/product-badges.ts` —\n * keep the wire field names, the caps and the contrast rule in step with it.\n *\n * Everything below reads UNTRUSTED JSON: the payload arrives over the network\n * in `window.__SHOPPEX_INITIAL__`. Entries that do not parse are dropped rather\n * than repaired — a badge with no usable label has no meaning, and inventing\n * one would be a fabricated value on a merchandising surface.\n */\n\n/** Hard cap per product, matching the contract. Card corners are finite. */\nconst MAX_PRODUCT_BADGES = 4;\n\n/** Hard cap per label, matching the contract. Longer labels break card layouts. */\nconst MAX_PRODUCT_BADGE_LABEL_LENGTH = 24;\n\n/**\n * The icon catalog the dashboard offers. A name outside this list is NOT an\n * error — the catalog is append-only upstream, so a storefront artifact built\n * before an icon was added must degrade to a text-only badge rather than throw\n * on a name it has never heard of.\n */\nexport const PRODUCT_BADGE_ICON_NAMES = [\n 'sparkles',\n 'flame',\n 'star',\n 'zap',\n 'tag',\n 'percent',\n 'trending-up',\n 'megaphone',\n 'party-popper',\n 'crown',\n 'gem',\n 'trophy',\n 'award',\n 'medal',\n 'badge-check',\n 'thumbs-up',\n 'heart',\n 'shield',\n 'shield-check',\n 'lock',\n 'handshake',\n 'rocket',\n 'truck',\n 'clock',\n 'timer',\n 'infinity',\n 'refresh-ccw',\n 'download',\n 'package',\n 'send',\n 'headphones',\n 'message-circle',\n 'life-buoy',\n 'users',\n 'gamepad-2',\n 'monitor',\n 'smartphone',\n 'key',\n 'file-text',\n 'book-open',\n 'music',\n 'video',\n 'image',\n 'palette',\n 'code',\n 'wrench',\n 'wallet',\n 'credit-card',\n 'globe',\n 'gift',\n 'bot',\n] as const;\n\nexport type ProductBadgeIconName = (typeof PRODUCT_BADGE_ICON_NAMES)[number];\n\n/** One badge, in this theme's camelCase model. */\nexport interface ProductBadge {\n label: string;\n /** null for a text-only badge, and for any icon name this build cannot draw. */\n icon: ProductBadgeIconName | null;\n /** `#rrggbb` lowercase, or null when the theme owns the colour. */\n backgroundColor: string | null;\n /**\n * The readable foreground for `backgroundColor`, decided by WCAG contrast.\n * null exactly when `backgroundColor` is null — the theme then owns both.\n */\n foregroundColor: string | null;\n showOnProductCard: boolean;\n showOnProductPage: boolean;\n}\n\nconst HEX_SHORT = /^#[0-9a-f]{3}$/i;\nconst HEX_LONG = /^#[0-9a-f]{6}$/i;\n\n/**\n * A merchant colour as lowercase `#rrggbb`, or null when it is absent or\n * unparseable. Strict on purpose: no named colours, no `rgb()`, no gradients.\n * The contrast rule below can only reason about one opaque colour.\n */\nfunction normalizeBadgeColor(value: unknown): string | null {\n if (typeof value !== 'string') return null;\n const trimmed = value.trim();\n if (trimmed.length === 0) return null;\n const candidate = trimmed.startsWith('#') ? trimmed : `#${trimmed}`;\n if (HEX_LONG.test(candidate)) return candidate.toLowerCase();\n if (HEX_SHORT.test(candidate)) {\n const [, r, g, b] = candidate.toLowerCase();\n return `#${r}${r}${g}${g}${b}${b}`;\n }\n return null;\n}\n\nfunction channelLuminance(value: number): number {\n const channel = value / 255;\n return channel <= 0.04045 ? channel / 12.92 : ((channel + 0.055) / 1.055) ** 2.4;\n}\n\n/** WCAG relative luminance of a normalized `#rrggbb` colour. */\nfunction relativeLuminance(hex: string): number {\n const r = Number.parseInt(hex.slice(1, 3), 16);\n const g = Number.parseInt(hex.slice(3, 5), 16);\n const b = Number.parseInt(hex.slice(5, 7), 16);\n return 0.2126 * channelLuminance(r) + 0.7152 * channelLuminance(g) + 0.0722 * channelLuminance(b);\n}\n\n/** Near-black, used when the merchant's colour is light enough to need dark text. */\nconst BADGE_DARK_FOREGROUND = '#0b0b0f';\n/** Pure white, used when the merchant's colour is dark enough to need light text. */\nconst BADGE_LIGHT_FOREGROUND = '#ffffff';\n\n/**\n * The readable foreground for a badge background.\n *\n * The merchant picks the fill from a colour well with no contrast check of its\n * own, so the label colour cannot also be theirs: a mid-yellow badge with white\n * text is unreadable and a navy badge with black text is worse. Both candidates\n * are scored by WCAG contrast ratio against the chosen fill and the higher one\n * wins, which is the same decision the other lane makes for the same badge.\n *\n * Returns null when there is no explicit background — the theme then owns both\n * colours, and substituting one here would override its palette.\n */\nexport function resolveBadgeForeground(backgroundColor: string | null): string | null {\n if (backgroundColor === null) return null;\n const background = relativeLuminance(backgroundColor);\n const contrastWithWhite = 1.05 / (background + 0.05);\n const contrastWithBlack =\n (background + 0.05) / (relativeLuminance(BADGE_DARK_FOREGROUND) + 0.05);\n return contrastWithWhite >= contrastWithBlack ? BADGE_LIGHT_FOREGROUND : BADGE_DARK_FOREGROUND;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\n/**\n * `show_on_*` decides whether a surface renders a badge at all, so an\n * unreadable value must not silently hide the merchant's badge everywhere. The\n * wire carries booleans; the tolerated 1/0/\"true\"/\"false\" spellings are the\n * same ones the contract accepts, and anything else keeps the badge visible\n * (which is the dashboard's own default for a newly created badge).\n */\nfunction readVisibility(value: unknown): boolean {\n if (typeof value === 'boolean') return value;\n if (value === 1 || value === '1' || value === 'true') return true;\n if (value === 0 || value === '0' || value === 'false') return false;\n return true;\n}\n\nfunction readField(entry: Record<string, unknown>, snake: string, camel: string): unknown {\n return entry[snake] !== undefined ? entry[snake] : entry[camel];\n}\n\n/**\n * The `badges` array off the product wire. Missing, malformed or empty all\n * produce `[]` — a product without badges is the overwhelmingly common case and\n * is not an error.\n */\nexport function parseProductBadges(value: unknown): ProductBadge[] {\n if (!Array.isArray(value)) return [];\n\n const badges: ProductBadge[] = [];\n for (const raw of value) {\n if (badges.length >= MAX_PRODUCT_BADGES) break;\n if (!isRecord(raw)) continue;\n\n const rawLabel = raw.label;\n if (typeof rawLabel !== 'string') continue;\n const label = rawLabel.trim().slice(0, MAX_PRODUCT_BADGE_LABEL_LENGTH);\n if (label.length === 0) continue;\n\n const rawIcon = raw.icon;\n const icon =\n typeof rawIcon === 'string'\n && (PRODUCT_BADGE_ICON_NAMES as readonly string[]).includes(rawIcon)\n ? (rawIcon as ProductBadgeIconName)\n : null;\n\n const backgroundColor = normalizeBadgeColor(\n readField(raw, 'background_color', 'backgroundColor'),\n );\n\n badges.push({\n label,\n icon,\n backgroundColor,\n foregroundColor: resolveBadgeForeground(backgroundColor),\n showOnProductCard: readVisibility(readField(raw, 'show_on_product_card', 'showOnProductCard')),\n showOnProductPage: readVisibility(readField(raw, 'show_on_product_page', 'showOnProductPage')),\n });\n }\n return badges;\n}\n\n/** Badges the catalog card shows, in the merchant's order. */\nexport function badgesForCard(badges: readonly ProductBadge[]): ProductBadge[] {\n return badges.filter((badge) => badge.showOnProductCard);\n}\n\n/** Badges the product page shows, in the merchant's order. */\nexport function badgesForProductPage(badges: readonly ProductBadge[]): ProductBadge[] {\n return badges.filter((badge) => badge.showOnProductPage);\n}\n","/**\n * The merchant's product video, turned into an embeddable `src` — and gated.\n *\n * `products.video_link` is a raw string the merchant typed: a YouTube watch URL,\n * a bare video id, a Streamable short link, a Vimeo page. The Easy lane resolves\n * it through `resolveVideoEmbedSrc`\n * (`packages/builder-runtime/src/video-embed.ts`, called by the storefront\n * render context) and then renders the iframe ONLY when the resolved host is on\n * the gallery block's allowlist (`blocks/gallery.ts`, ADR-0045).\n *\n * Both halves are ported here, in that order, because they answer different\n * questions. The parser decides which provider a merchant meant; the allowlist\n * decides whether the storefront is willing to frame it at all. A host that is\n * not on the list renders NOTHING — never the raw string in an iframe, which is\n * what turns a merchant-editable text column into an arbitrary embed.\n *\n * Ported rather than imported: the theme builds standalone from\n * `templates/code-storefronts/`, with no workspace dependency on the renderer.\n * Keep the allowlist and the provider rules in step with the two files above.\n */\n\n/**\n * Exactly the hosts `blocks/gallery.ts` frames, and no others. `https://` is\n * required by the same check — an `http://` embed on an https storefront is a\n * mixed-content frame the browser blocks anyway.\n */\nconst ALLOWED_VIDEO_EMBED_HOSTS = new Set([\n 'www.youtube-nocookie.com',\n 'www.youtube.com',\n 'player.vimeo.com',\n 'streamable.com',\n]);\n\n/** Mirrors `isAllowedVideoEmbedSrc` in the Easy lane's gallery block. */\nfunction isAllowedVideoEmbedSrc(src: string): boolean {\n if (!src.startsWith('https://')) return false;\n const host = src.slice('https://'.length).split('/')[0]!.split('?')[0]!.split('#')[0]!;\n return ALLOWED_VIDEO_EMBED_HOSTS.has(host);\n}\n\nconst YOUTUBE_VIDEO_ID = /^[\\w-]{11}$/;\nconst STREAMABLE_CODE = /^[a-zA-Z0-9]+$/;\nconst VIMEO_ID = /^\\d+$/;\nconst VIMEO_HASH = /^[a-zA-Z0-9]+$/;\n\nfunction toUrl(input: string): URL | null {\n try {\n return input.startsWith('http://') || input.startsWith('https://')\n ? new URL(input)\n : new URL(`https://${input}`);\n } catch {\n return null;\n }\n}\n\n/** YouTube accepts a bare 11-character id, so it is the fallback provider. */\nfunction parseYouTubeVideoId(input: string): string | null {\n const trimmed = input.trim();\n if (!trimmed) return null;\n if (YOUTUBE_VIDEO_ID.test(trimmed)) return trimmed;\n\n const url = toUrl(trimmed);\n if (!url) return null;\n const host = url.hostname.replace(/^www\\./, '');\n\n if (host === 'youtu.be') {\n const candidate = url.pathname.split('/').filter(Boolean)[0] ?? '';\n return YOUTUBE_VIDEO_ID.test(candidate) ? candidate : null;\n }\n if (host !== 'youtube.com' && host !== 'm.youtube.com' && host !== 'music.youtube.com') {\n return null;\n }\n const watchId = url.searchParams.get('v');\n if (watchId && YOUTUBE_VIDEO_ID.test(watchId)) return watchId;\n for (const pattern of [/^\\/embed\\/([\\w-]{11})/, /^\\/shorts\\/([\\w-]{11})/, /^\\/live\\/([\\w-]{11})/]) {\n const match = url.pathname.match(pattern);\n if (match?.[1] && YOUTUBE_VIDEO_ID.test(match[1])) return match[1];\n }\n return null;\n}\n\n/** streamable.com/<code> and streamable.com/e/<code>. */\nfunction parseStreamableCode(url: URL): string | null {\n const segments = url.pathname.split('/').filter(Boolean);\n const code = segments[0] === 'e' ? segments[1] : segments[0];\n return code && STREAMABLE_CODE.test(code) ? code : null;\n}\n\n/**\n * The numeric id, plus the privacy hash an UNLISTED video needs to play. The\n * `?h=` query wins over the path segment, matching Vimeo's own player URLs.\n */\nfunction parseVimeo(url: URL): { id: string; hash?: string } | null {\n const segments = url.pathname.split('/').filter(Boolean);\n for (let index = segments.length - 1; index >= 0; index -= 1) {\n const segment = segments[index]!;\n if (!VIMEO_ID.test(segment)) continue;\n const queryHash = url.searchParams.get('h')?.trim();\n const pathHash = segments[index + 1];\n const hash = queryHash && VIMEO_HASH.test(queryHash)\n ? queryHash\n : pathHash && VIMEO_HASH.test(pathHash)\n ? pathHash\n : undefined;\n return hash ? { id: segment, hash } : { id: segment };\n }\n return null;\n}\n\n/**\n * The iframe `src` for a merchant's `video_link`, or null when there is nothing\n * this storefront is willing to frame.\n *\n * Privacy-enhanced YouTube (`youtube-nocookie.com`) because that is what the\n * render context asks for (`resolveVideoEmbedSrc(..., { privacyEnhanced: true })`)\n * — a code storefront must not set more cookies on a buyer than the Easy lane\n * for the same merchant's video.\n */\nexport function resolveProductVideoEmbedSrc(value: unknown): string | null {\n if (typeof value !== 'string') return null;\n const trimmed = value.trim();\n if (!trimmed) return null;\n\n const url = toUrl(trimmed);\n let src: string | null = null;\n if (url) {\n const host = url.hostname.replace(/^www\\./, '');\n if (host === 'streamable.com') {\n const code = parseStreamableCode(url);\n src = code ? `https://streamable.com/e/${code}` : null;\n } else if (host === 'vimeo.com' || host === 'player.vimeo.com') {\n const parsed = parseVimeo(url);\n src = parsed\n ? parsed.hash\n ? `https://player.vimeo.com/video/${parsed.id}?h=${parsed.hash}`\n : `https://player.vimeo.com/video/${parsed.id}`\n : null;\n }\n }\n if (src === null) {\n const youtubeId = parseYouTubeVideoId(trimmed);\n src = youtubeId ? `https://www.youtube-nocookie.com/embed/${youtubeId}` : null;\n }\n\n // The second gate. It is redundant for everything the parser above produces,\n // and that is the point: if a provider is ever added to the parser without\n // being added to the allowlist, the storefront frames nothing rather than an\n // origin nobody vetted.\n return src !== null && isAllowedVideoEmbedSrc(src) ? src : null;\n}\n","/**\n * The `window.__SHOPPEX_INITIAL__` contract: catalog, store switches and addon\n * slots, injected by the Shoppex storefront (or preview) worker immediately\n * before `</head>` per\n * `packages/builder-contracts/src/storefront-initial-data-html.ts`.\n *\n * This is PLATFORM data, not merchant content — the theme never authors it and\n * must never invent it. Merchant-authored content lives in\n * `src/config/site-config.ts` and is read from the same object under\n * `store.builder_settings.theme.content`.\n */\n\nimport {\n firstOptionalString,\n isRecord,\n optionalBoolean,\n optionalString,\n parseFiniteNumber,\n requireBoolean,\n requireString,\n requireStringFromKeys,\n requireUnknownRecord,\n} from './parse';\nimport { parseProductBadges, type ProductBadge } from './product-badges';\nimport { resolveProductVideoEmbedSrc } from './video-embed';\nimport type { StorefrontAddon } from './sdk';\n\nexport interface StorefrontPriceVariant {\n id: string;\n label: string;\n /** What this option states about its term; `inherit` defers to the product. */\n license: LicenseStatement;\n /** Display unit price with the product percent discount already applied. */\n price: number;\n /**\n * Undiscounted display-currency unit price — the input for line-level\n * pricing, where the percent is applied to the whole line like the quote\n * does, and the floor the server holds pay-what-you-want amounts to.\n */\n baseUnitPrice: number;\n stock: number | null;\n quantityMin: number | null;\n quantityMax: number | null;\n imageUrl: string | null;\n isFeatured: boolean;\n}\n\nexport interface VolumeDiscountRule {\n quantity: number;\n type: 'PERCENTAGE' | 'FIXED';\n value: number;\n}\n\nexport type ProductBundleItem = {\n /** Cart id (uniqid) — what addToCart expects. */\n id: string;\n title: string;\n variantId: string;\n unitPrice: number;\n imageUrl: string | null;\n variantTitle: string | null;\n};\n\n/**\n * A \"buy these together\" offer the merchant configured. The discount is applied\n * by the backend when the cart is quoted — the theme only advertises it, it\n * never computes the charged price itself.\n */\nexport type ProductBundle = {\n id: string;\n title: string;\n discountType: 'PERCENTAGE' | 'FIXED';\n discountAmount: number;\n items: ProductBundleItem[];\n};\n\nexport type ProductContentTab = {\n title: string;\n html: string;\n};\n\n/**\n * A paid extra the buyer may add to the line. The price is a per-unit surcharge\n * on top of the resolved variant price, and `required` options are locked on —\n * the buy box shows them checked and refuses to clear them, because the backend\n * charges them either way.\n */\nexport type ProductAddon = {\n id: string;\n title: string;\n price: number;\n required: boolean;\n};\n\nexport type ProductFaq = {\n question: string;\n answer: string;\n};\n\n/**\n * One ACTIVE \"buy N, get M\" deal on this product.\n *\n * The server filters the list to the currently live window before it emits it,\n * so the storefront advertises what `CartQuoteService` would actually apply —\n * it never computes the discount itself. The discounted units sit WITHIN the\n * required quantity (they are not extra items on top), and\n * `maxApplicationsPerCart` caps how often the reward repeats in one cart; both\n * are `applyAutomaticProductDeals` semantics and the wording depends on them.\n *\n * `endsAt` is what makes a row time-limited. Once it passes the backend stops\n * applying the deal, so the WHOLE row has to disappear — not just its\n * countdown — which is why the panel drops expired rows rather than freezing\n * them at 00:00:00.\n */\nexport type ProductAutomaticDeal = {\n id: string;\n /** Merchant title. Empty when they named the rule nothing. */\n title: string;\n requiredQuantity: number;\n discountedQuantity: number;\n /** FREE gives the units away; PERCENTAGE takes `discountValue` percent off. */\n discountType: 'FREE' | 'PERCENTAGE';\n /** Percent off the discounted units. 0 for a FREE deal. */\n discountValue: number;\n /** How often the reward repeats per cart; null when uncapped. */\n maxApplicationsPerCart: number | null;\n /** ISO timestamp the deal stops applying, or null when it does not expire. */\n endsAt: string | null;\n};\n\n/**\n * One detail the merchant requires from the buyer for this product — a Steam\n * id, a server name, a licence holder.\n *\n * PAYMENT PATH. These are not decoration: the backend validates them when the\n * invoice is built (`custom_fields_validation_failed`), so a product with a\n * REQUIRED field cannot be sold at all until its value reaches the cart line.\n * The values ride on the cart line's `custom_fields`, which is also part of the\n * SDK's line identity — see `checkout-fields.ts`.\n *\n * The shape mirrors the SDK's own `StorefrontCustomField`\n * (`packages/sdk/src/utils/storefront-custom-fields.ts`), because that is what\n * the Easy lane validates against and the two lanes must agree about which\n * value a merchant's definition accepts.\n */\nexport type ProductCustomField = {\n name: string;\n /** 'text' | 'textarea' | 'checkbox' | … — anything else renders as text. */\n type: string;\n required: boolean;\n defaultValue: string;\n placeholder: string;\n /** Merchant-authored validation pattern. Absent when they set none. */\n regex: string | null;\n};\n\n/** A category a product belongs to. The id is what the filter chips match on. */\nexport type ProductCategoryRef = {\n id: string;\n title: string;\n};\n\n/**\n * Product kinds the catalog renders differently. `SUBSCRIPTION` earns a badge,\n * `INFO_CARD` is not buyable at all, `SERIALS` reads its stock straight off\n * the product row instead of aggregating variants, and `PHYSICAL` ships to an\n * address the hosted checkout collects, so it is never \"instant\".\n */\nexport type ProductType = 'PRODUCT' | 'SUBSCRIPTION' | 'INFO_CARD' | 'SERIALS' | 'PHYSICAL';\n\nconst PRODUCT_TYPES: readonly ProductType[] = ['PRODUCT', 'SUBSCRIPTION', 'INFO_CARD', 'SERIALS', 'PHYSICAL'];\n\n/**\n * What a product promises about delivery.\n *\n * - `shipped`: a PHYSICAL product. Its digital `delivery_time` is null, which\n * would otherwise read as instant, so the type decides first.\n * - `timed`: the merchant set a delivery time.\n * - `instant`: digital with no delivery time.\n *\n * Parity with the hosted checkout's `apps/checkout/lib/delivery-promise.ts`.\n */\nexport type DeliveryPromise =\n | { kind: 'shipped' }\n | { kind: 'timed'; count: number; unit: DeliveryTimeUnit }\n | { kind: 'instant' };\n\nexport function resolveDeliveryPromise(\n product: Pick<StorefrontProduct, 'productType' | 'deliveryTimeCount' | 'deliveryTimeUnit'>,\n): DeliveryPromise {\n if (product.productType === 'PHYSICAL') return { kind: 'shipped' };\n if (product.deliveryTimeCount !== null) {\n return { kind: 'timed', count: product.deliveryTimeCount, unit: product.deliveryTimeUnit };\n }\n return { kind: 'instant' };\n}\n\n/**\n * How often a subscription product is charged, and how long it is free first.\n *\n * PARITY CONTRACT. The Easy lane derives the same three facts from the same\n * flat wire fields in\n * `packages/storefront-render-core/src/subscription-billing.ts`, and the\n * wordings below are that module's wordings. A change on either side without\n * the other makes the two lanes describe one product differently, which is the\n * one thing this template may not do.\n */\nexport type SubscriptionIntervalUnit = 'DAY' | 'WEEK' | 'MONTH' | 'YEAR';\n\nconst SUBSCRIPTION_INTERVAL_UNITS: readonly SubscriptionIntervalUnit[] = ['DAY', 'WEEK', 'MONTH', 'YEAR'];\n\nexport interface ProductSubscription {\n /**\n * The billing interval, or null when the merchant configured none. The\n * product contract allows a SUBSCRIPTION product without an interval, so the\n * buy box then prints no cadence rather than inventing one.\n */\n intervalUnit: SubscriptionIntervalUnit | null;\n /** Intervals per charge, at least 1. */\n intervalCount: number;\n /** Free-trial length in days; 0 when the merchant set none. */\n trialDays: number;\n}\n\nconst SUBSCRIPTION_INTERVAL_NOUNS: Record<SubscriptionIntervalUnit, string> = {\n DAY: 'day',\n WEEK: 'week',\n MONTH: 'month',\n YEAR: 'year',\n};\n\n/** The cadence as it reads next to a price: \"per month\", \"every 3 months\". */\nexport function formatSubscriptionCadence(subscription: ProductSubscription): string {\n const unit = subscription.intervalUnit;\n if (unit === null) return '';\n const noun = SUBSCRIPTION_INTERVAL_NOUNS[unit];\n return subscription.intervalCount === 1\n ? `per ${noun}`\n : `every ${subscription.intervalCount} ${noun}s`;\n}\n\n/** \"7-day free trial\" — the plan charges nothing for those days. */\nexport function formatSubscriptionTrial(subscription: ProductSubscription): string {\n const days = subscription.trialDays;\n return days > 0 ? `${days}-day free trial` : '';\n}\n\n/**\n * Recurring billing off the FLAT catalog fields the wire carries\n * (`recurring_interval`, `recurring_interval_count`, `trial_period`). Only a\n * product sold as a subscription gets one: a plain product can keep a stale\n * interval from an earlier edit, and it must never advertise a renewal.\n */\nfunction parseSubscription(\n product: Record<string, unknown>,\n productType: ProductType,\n): ProductSubscription | null {\n if (productType !== 'SUBSCRIPTION') return null;\n const rawUnit = typeof product.recurring_interval === 'string'\n ? product.recurring_interval.toUpperCase()\n : '';\n const unit = SUBSCRIPTION_INTERVAL_UNITS.find((entry) => entry === rawUnit) ?? null;\n const count = normalizePositiveDays(product.recurring_interval_count) ?? 1;\n return {\n intervalUnit: unit,\n intervalCount: count,\n trialDays: normalizePositiveDays(product.trial_period) ?? 0,\n };\n}\n\nexport interface StorefrontProduct {\n /** uniqid — the identifier the cart/SDK use for this product. */\n id: string;\n /** Drives the catalog badge, the buy controls and the stock aggregation. */\n productType: ProductType;\n /**\n * The product's real database id. Distinct from `id` (uniqid); shop feedback\n * references products by this id, so review matching must use it.\n */\n productId: string;\n name: string;\n price: number;\n currency: string;\n description: string | null;\n slug: string | null;\n imageUrl: string | null;\n /**\n * The product's own image attachments, in the merchant's order — the product\n * page gallery. Empty when the product has none; the gallery then falls back\n * to `imageUrl`, the same resolution the Easy lane's gallery producer applies.\n * The wire carries no per-image alt text, so the alt is the product title.\n */\n imageUrls: string[];\n stock: number | null;\n /**\n * Merchant-set \"Low Stock Warning\" threshold (`quantity_warning` on the\n * wire), null when disabled. NOT `stock_delimiter` — that field is the text\n * delimiter splitting the serial-key blob, never a number.\n */\n lowStockThreshold: number | null;\n /** Per-product stock-counter override; null inherits the shop-wide setting. */\n hideStockCounter: boolean | null;\n /**\n * Quantity-tier discounts, sorted ascending by quantity. The backend applies\n * them at charge time, so the displayed line total must apply them too or\n * the buyer sees a higher number than they pay.\n */\n volumeDiscounts: VolumeDiscountRule[];\n /**\n * Buyers may choose their own unit price at or above the resolved\n * product/variant price. Without an amount input the checkout silently\n * charges the minimum, so the buy box must render one when this is set.\n */\n payWhatYouWant: boolean;\n /** Normalized product percent discount (0 when none, and always 0 for PWYW). */\n discountPercent: number;\n /**\n * Undiscounted starting unit price (min variant, or the base for\n * variant-less products) — the line-pricing input for the no-variant case.\n */\n baseUnitPrice: number;\n /**\n * The product ROW's own undiscounted price, before variants are considered.\n * Distinct from `baseUnitPrice`, which already falls back to the cheapest\n * variant: a variant-led product carries 0 here, and that zero is what marks\n * its catalog price as a range (\"$7.00 - $50.00\") rather than a single\n * amount. Collapsing the two would price every variant product from its\n * minimum and silently drop the range.\n */\n basePrice: number;\n priceVariants: StorefrontPriceVariant[];\n quantityMin: number;\n quantityMax: number | null;\n /** Units sold, for social proof. 0 when the shop has no sales for it. */\n soldCount: number;\n /** Merchant flagged this product as a bestseller. */\n bestseller: boolean;\n /**\n * Merchant-authored decorative badges, in their order, at most four. Empty\n * on almost every product. Purely merchandising — see `product-badges.ts`:\n * they never speak for availability, price or fulfilment, so they are read\n * alongside the theme's own signals rather than instead of them.\n */\n badges: ProductBadge[];\n /** Merchant-assigned listing order; lower sorts first. */\n sortPriority: number;\n /** ISO timestamp the product was created, or null when the payload omits it. */\n createdAt: string | null;\n /** Categories this product belongs to. Empty when uncategorised. */\n categories: ProductCategoryRef[];\n /** Bundle offers this product takes part in. Empty when it is in none. */\n bundles: ProductBundle[];\n /**\n * Active quantity deals on this product, in the merchant's order. Empty when\n * the product has none or every rule is malformed — see `ProductAutomaticDeal`.\n */\n automaticDeals: ProductAutomaticDeal[];\n /**\n * The merchant's product video as an embeddable, allowlisted `src`. null when\n * they set none OR when the host is not one the storefront frames — see\n * `video-embed.ts`.\n */\n videoEmbedSrc: string | null;\n /** Paid extras offered alongside this product. Empty when it has none. */\n addons: ProductAddon[];\n /**\n * Details the merchant collects from the buyer for this product. Empty when\n * they configured none. A REQUIRED entry makes the product unsellable until\n * its value is on the cart line — see `ProductCustomField`.\n */\n customFields: ProductCustomField[];\n /**\n * The merchant PAUSED this product (\"Coming soon\"). It is not a supply state\n * and has nothing to do with stock or `orderable`: the backend refuses to\n * sell it at any inventory level, so the storefront must not offer it. Same\n * precedence the document lane applies (`isProductOutOfStock` checks it\n * before it looks at stock at all).\n */\n onHold: boolean;\n /**\n * Original price when a discount applies (price is the discounted one).\n * null when there is no discount — never a fabricated \"was\" price.\n */\n compareAtPrice: number | null;\n /**\n * Average star rating (1-5) derived from REAL feedback only. null when the\n * product has zero feedback — the UI then renders no stars rather than a\n * fake score.\n */\n rating: number | null;\n /** Number of feedback entries the rating is based on (0 when none). */\n ratingCount: number;\n /**\n * The feedback tally as the payload states it: how many buyers were\n * positive, neutral or negative. null exactly when `rating` is null.\n */\n ratingTally: { positive: number; neutral: number; negative: number } | null;\n /** Bulleted selling points shown in the buy box. */\n highlights: string[];\n /**\n * Merchant-defined key/value pairs, set per product in the dashboard under\n * Integrations -> Metadata. Always present ({} when the merchant set none)\n * and every value is a string, so `product.metadata.status` is safe to read\n * without a guard.\n *\n * This is your extension point: values you need that Shoppex has no field for\n * — a release status, an accent colour, a grouping tag. The merchant edits\n * them in the dashboard, so changing one does NOT require rebuilding this\n * storefront. Nothing here is rendered for you; read the keys your shop uses\n * and render them yourself.\n *\n * Public: everything in here is readable by anyone who opens the store.\n */\n metadata: Record<string, string>;\n /** Extra merchant-authored content tabs ({title, html}). */\n descriptionTabs: ProductContentTab[];\n /** Per-product FAQ entries for the details accordion. */\n faqs: ProductFaq[];\n /** What this product states about its licence term. Resolve with resolveLicenseTerm. */\n license: LicenseStatement;\n /** Whether licensing is on at all; a stale term must not be advertised when it is off. */\n licensingEnabled: boolean;\n /** Estimated delivery time as a bare count (null when unset/instant). */\n deliveryTimeCount: number | null;\n /** Unit `deliveryTimeCount` is counted in: 'minutes' | 'hours' | 'days'. */\n deliveryTimeUnit: DeliveryTimeUnit;\n /**\n * Recurring billing, present only on a product the merchant sells as a\n * subscription. null on every other product.\n */\n subscription: ProductSubscription | null;\n}\n\nexport interface StorefrontSettings {\n cartEnabled: boolean;\n hideOutOfStock: boolean;\n hideStockCounter: boolean;\n /**\n * Buyer-facing switches the MERCHANT owns, not the theme.\n *\n * A storefront that moves to the Advanced lane must not quietly re-enable\n * what its owner turned off, or offer a contact form the backend will refuse.\n * The document lane has honoured all three from the start.\n */\n searchEnabled: boolean;\n contactFormEnabled: boolean;\n requireInvoiceForContact: boolean;\n /**\n * The merchant centres bundle cards. A shop-level presentation switch, not a\n * theme setting: the Easy lane reads `ctx.shop.center_group_titles` and\n * re-lays the GROUP card out — product cards are untouched.\n */\n centerGroupTitles: boolean;\n /**\n * The merchant does not want the product's own picture used as its gallery.\n * The Easy lane's gallery producer empties the image list outright when this\n * is on, so the product page shows its placeholder frame instead.\n */\n descriptionSkipDefaultImage: boolean;\n /**\n * Whether pictures INSIDE a product description are shown. Off strips every\n * `<img>` from the description and from each content tab, the same rewrite\n * `stripDescriptionImages` performs on the document lane. It does not touch\n * the gallery, which is `descriptionSkipDefaultImage`'s job.\n */\n descriptionImage: boolean;\n /**\n * The merchant let buyers re-sort the catalog. Off hides the control on every\n * listing surface and pins the order to `defaultSort` — the same AND the Easy\n * lane applies (`enrichPageAllProductsBlockSettings`: the block setting and\n * `shop.sort_enabled` both have to be on).\n */\n sortEnabled: boolean;\n /**\n * The order the merchant wants their catalog listed in, normalized to the\n * four keys every lane sorts by. It is the INITIAL sort of a listing and the\n * only sort when `sortEnabled` is false.\n */\n defaultSort: StorefrontSortKey;\n /**\n * Every currency this shop sells in, the shop's own default first, uppercased\n * and de-duplicated. One entry (or none) means the shop has nothing to switch\n * between and the header renders no control.\n */\n availableCurrencies: string[];\n /** The currency the merchant configured — the one served without a choice. */\n defaultCurrency: string;\n /**\n * The currency THIS page was priced in. It is the buyer's choice, their\n * country's match, or the shop default, resolved at the edge before the\n * catalog was fetched — so it is the only currency the amounts on screen are\n * actually in.\n */\n activeCurrency: string;\n /**\n * Payment gateway codes the shop has connected. The cart turns them into the\n * brand marks under its checkout button — merchant fact, so an empty list\n * renders no row rather than a reassuring default.\n */\n availableGateways: string[];\n}\n\n/**\n * The shop itself: the identity and the aggregate metrics the storefront\n * chrome renders. Merchant-owned facts, never theme content — a theme that\n * authored its own store name or sales count would be publishing a claim the\n * merchant never made.\n */\nexport interface StorefrontShop {\n /** The shop's public display name. */\n name: string;\n /** Branding/domain logo, falling back to the profile picture for logo surfaces. */\n logoUrl: string | null;\n /** Shop profile picture, falling back to the logo only when no avatar exists. */\n avatarUrl: string | null;\n /** Merchant-configured social profiles, in the platform's presentation order. */\n socialLinks: SocialLink[];\n /**\n * The merchant's published terms of service, as authored HTML — null when\n * the shop has no terms or turned them off. `/terms` renders it through\n * `sanitizeMerchantHtml`, never raw.\n */\n termsHtml: string | null;\n /** True when the merchant published terms and the `/terms` route has content. */\n hasPublishedTerms: boolean;\n /**\n * The shop's reward program, or null when it is off / has no perks. Merchant\n * fact: the headline and the perk labels are the merchant's wording, and a\n * theme that supplied its own would advertise a program nobody configured.\n */\n rewardProgram: RewardProgramSummary | null;\n /**\n * The shop's wholesale reseller program, or null when the shop runs none.\n * Merchant fact: only a shop that actually accepts resellers may advertise\n * one, so the footer's \"Become a reseller\" link is gated on this and not on\n * theme content.\n */\n resellerProgram: ResellerProgramSummary | null;\n stats: StorefrontShopStats;\n}\n\n/**\n * The reseller program off the wire, gated the way the Easy lane's footer link\n * gates it (`blocks/reseller-program-link.ts`): a summary is usable only when\n * it names an enrolment mode the portal can serve. Both modes render the SAME\n * link — the portal decides between one-click enrolment and the application\n * form.\n */\nexport type ResellerProgramSummary = {\n enrollmentMode: 'OPEN' | 'APPLICATION';\n};\n\n/**\n * The reward-program teaser's data, gated exactly as the Easy lane gates it\n * (`blocks/reward-program-teaser.ts` + `mapRewardProgramSummary`): the program\n * must be enabled, it must carry a headline, and it must have at least one\n * usable perk. Anything short of that is not a program to advertise, so the\n * whole summary is null and every teaser renders nothing.\n */\nexport type RewardProgramSummary = {\n headline: string;\n items: RewardProgramItem[];\n};\n\n/**\n * One perk. `triggerType` and `actionType` are required by the platform's own\n * mapper — a row missing either is a half-configured rule, not a perk — even\n * though only the label is rendered.\n */\nexport type RewardProgramItem = {\n triggerType: string;\n actionType: string;\n label: string;\n};\n\nexport type SocialNetwork =\n | 'discord'\n | 'telegram'\n | 'youtube'\n | 'twitter'\n | 'instagram'\n | 'tiktok'\n | 'facebook'\n | 'reddit'\n | 'email';\n\nexport type SocialLink = {\n network: SocialNetwork;\n href: string;\n};\n\n/**\n * Aggregate shop metrics for the store-stats strip.\n *\n * Counts are raw numbers, not display strings: formatting belongs to the\n * component. `averageRating` and `reviewCount` are independent on purpose —\n * a shop can carry a rating from transferred feedback without a countable\n * review, and rounding that to \"no reviews\" would drop a real number.\n */\nexport interface StorefrontShopStats {\n productsSold: number;\n customers: number;\n reviewCount: number;\n /** 1-5, or null when the shop has no rating at all. */\n averageRating: number | null;\n /** Products the storefront lists — the theme counts them, the API does not. */\n productCount: number;\n}\n\n/**\n * A merchant-curated bundle of products. The wire payload references its\n * members by uniqid; the members themselves are ordinary catalog products, so\n * they arrive in `products` and are resolved from there.\n */\nexport interface StorefrontGroup {\n id: string;\n title: string;\n description: string | null;\n imageUrl: string | null;\n /** Category the merchant filed the bundle under, if any. */\n categoryId: string | null;\n /** Member product uniqids, in the merchant's order. */\n productIds: string[];\n /** Member count as the API counts it — may exceed the resolvable members. */\n productsCount: number;\n sortPriority: number;\n}\n\nexport interface StorefrontCategory {\n id: string;\n slug: string;\n title: string;\n /** The merchant's category picture. Null leaves the filter chip text-only. */\n imageUrl: string | null;\n productsCount: number;\n groupsCount: number;\n}\n\n/**\n * One entry of the storefront's listing order.\n *\n * `items` is a pure display-order reference list: it decides WHICH products and\n * groups the storefront lists and in what order, and the referenced records are\n * delivered exactly once in `products` / `groups`. A storefront that listed\n * `products` directly would show unlisted rows (bundle members, hidden\n * products) the merchant never put on the shelf.\n */\nexport interface StorefrontListingItem {\n type: 'product' | 'group';\n id: string;\n sortPriority: number;\n}\n\ninterface ShoppexInitialData {\n store?: Record<string, unknown> & {\n builder_settings?: {\n theme?: {\n content?: unknown;\n };\n };\n };\n products?: unknown[];\n groups?: unknown[];\n items?: unknown[];\n categories?: unknown[];\n addons?: {\n items?: StorefrontAddon[];\n };\n}\n\ndeclare global {\n interface Window {\n __SHOPPEX_INITIAL__?: ShoppexInitialData;\n }\n}\n\n/**\n * Whether the page carries an injected payload AT ALL.\n *\n * Not a validity check — an empty object passes. It separates \"nothing injected\n * this document\" from \"something was injected and is wrong\", which are two\n * different faults with two different fixes. See `boot` in `App.tsx`.\n */\nexport function hasInjectedStorefrontData(): boolean {\n return window.__SHOPPEX_INITIAL__ !== undefined;\n}\n\n/** Merchant-authored content, or undefined when the store has none yet. */\nexport function getAuthoredContent(): unknown {\n return window.__SHOPPEX_INITIAL__?.store?.builder_settings?.theme?.content;\n}\n\nexport function getProducts(): StorefrontProduct[] {\n const initialData = window.__SHOPPEX_INITIAL__;\n if (initialData === undefined || initialData.products === undefined) {\n return [];\n }\n if (!Array.isArray(initialData.products)) {\n throw new Error('Invalid Shoppex initial data: products must be an array.');\n }\n\n return initialData.products.map(parseProduct);\n}\n\nexport function getProductGroups(): StorefrontGroup[] {\n const groups = window.__SHOPPEX_INITIAL__?.groups;\n if (groups === undefined) return [];\n if (!Array.isArray(groups)) {\n throw new Error('Invalid Shoppex initial data: groups must be an array.');\n }\n return groups\n .map((value, index) => {\n const context = `group at index ${index}`;\n const group = requireUnknownRecord(value, context);\n const productIds = Array.isArray(group.product_uniqids)\n ? group.product_uniqids.filter((entry): entry is string => typeof entry === 'string' && entry.length > 0)\n : [];\n return {\n id: requireString(group, 'uniqid', context),\n title: requireString(group, 'title', context),\n description: optionalString(group.description),\n imageUrl: firstOptionalString(group, ['cdn_image_url', 'image_url']),\n categoryId: optionalString(group.category_id),\n productIds,\n productsCount: normalizeOptionalNonNegativeNumber(group.products_count) ?? productIds.length,\n sortPriority: normalizeOptionalNonNegativeNumber(group.sort_priority) ?? 0,\n };\n });\n}\n\nexport function getStorefrontCategories(): StorefrontCategory[] {\n const categories = window.__SHOPPEX_INITIAL__?.categories;\n if (categories === undefined) return [];\n if (!Array.isArray(categories)) {\n throw new Error('Invalid Shoppex initial data: categories must be an array.');\n }\n return categories.map((value, index) => {\n const context = `category at index ${index}`;\n const category = requireUnknownRecord(value, context);\n const id = requireString(category, 'uniqid', context);\n return {\n id,\n slug: optionalString(category.slug) ?? id,\n title: requireString(category, 'title', context),\n // Same pair of keys the group records use — `transformStorefrontCategory`\n // emits the CDN URL, and the older spelling is what a cached payload has.\n imageUrl: firstOptionalString(category, ['cdn_image_url', 'image_url']),\n productsCount: normalizeOptionalNonNegativeNumber(category.products_count) ?? 0,\n groupsCount: normalizeOptionalNonNegativeNumber(category.groups_count) ?? 0,\n };\n });\n}\n\nexport function getStorefrontListing(): StorefrontListingItem[] {\n const items = window.__SHOPPEX_INITIAL__?.items;\n if (items === undefined) return [];\n if (!Array.isArray(items)) {\n throw new Error('Invalid Shoppex initial data: items must be an array.');\n }\n return items.map((value, index) => {\n const context = `listing item at index ${index}`;\n const item = requireUnknownRecord(value, context);\n return {\n type: item.item_type === 'GROUP' ? ('group' as const) : ('product' as const),\n id: requireString(item, 'uniqid', context),\n sortPriority: normalizeOptionalNonNegativeNumber(item.sort_priority) ?? index,\n };\n });\n}\n\n/** Presentation order of the social icon row — the platform's, not the theme's. */\nconst SOCIAL_NETWORKS: readonly SocialNetwork[] = [\n 'discord',\n 'telegram',\n 'youtube',\n 'twitter',\n 'instagram',\n 'tiktok',\n 'facebook',\n 'reddit',\n 'email',\n];\n\nexport function getStorefrontShop(productCount: number): StorefrontShop {\n const store = window.__SHOPPEX_INITIAL__?.store;\n if (!store) {\n throw new Error('Invalid Shoppex initial data: store is required.');\n }\n const social = isRecord(store.social) ? store.social : {};\n const feedback = isRecord(store.feedback) ? store.feedback : {};\n // Not the integer helper: a 4.8 average floored to 4 would understate every\n // shop's rating by up to a full star.\n const ratingRaw = Number(store.average_score ?? store.rating);\n const rating = Number.isFinite(ratingRaw) && ratingRaw > 0 ? ratingRaw : null;\n // `tos_enabled` is the merchant's switch, `terms_of_service` the document.\n // A shop that stored terms and then turned them off has not published them,\n // so both have to agree before the storefront links to or renders the page.\n const termsHtml = store.tos_enabled === true ? optionalString(store.terms_of_service) : null;\n\n return {\n name: requireString(store, 'name', 'store'),\n logoUrl: firstOptionalString(store, ['logo', 'avatar']),\n avatarUrl: firstOptionalString(store, ['avatar', 'logo']),\n socialLinks: SOCIAL_NETWORKS.flatMap((network) => {\n const value = optionalString(social[network]);\n if (!value) return [];\n // The merchant types a mail address, not a URL; every other network is\n // stored as a full link.\n const href = network === 'email' && !value.includes(':') ? `mailto:${value}` : value;\n return [{ network, href }];\n }),\n termsHtml,\n hasPublishedTerms: termsHtml !== null,\n rewardProgram: parseRewardProgram(store.reward_program_summary),\n resellerProgram: parseResellerProgram(store.reseller_program_summary),\n stats: {\n productsSold: normalizeOptionalNonNegativeNumber(store.products_sold_count) ?? 0,\n customers: normalizeOptionalNonNegativeNumber(store.customers_count ?? store.buyers_count) ?? 0,\n reviewCount: normalizeOptionalNonNegativeNumber(feedback.total) ?? 0,\n averageRating: rating,\n productCount,\n },\n };\n}\n\n/**\n * The shop's reward program off the wire, gated the way the platform's own\n * mapper gates it (`mapRewardProgramSummary` in\n * `packages/storefront-render-core/src/map-shop-render-settings.ts`): a\n * disabled program, a program with no headline, and a program whose every perk\n * is half-configured all collapse to null. Null is the shape the teasers read\n * as \"render nothing\" — never an empty card with a heading and no perks.\n */\nfunction parseRewardProgram(value: unknown): RewardProgramSummary | null {\n if (!isRecord(value) || value.enabled !== true) return null;\n const headline = optionalString(value.headline);\n if (!headline || !Array.isArray(value.items)) return null;\n const items: RewardProgramItem[] = [];\n for (const entry of value.items) {\n if (!isRecord(entry)) continue;\n const triggerType = firstOptionalString(entry, ['trigger_type', 'triggerType']);\n const actionType = firstOptionalString(entry, ['action_type', 'actionType']);\n const label = optionalString(entry.label);\n if (!triggerType || !actionType || !label) continue;\n items.push({ triggerType, actionType, label });\n }\n return items.length > 0 ? { headline, items } : null;\n}\n\nconst RESELLER_ENROLLMENT_MODES = ['OPEN', 'APPLICATION'] as const;\n\n/**\n * The shop's reseller program off the wire. The field is public shop payload\n * that older payloads simply do not carry, so it is read defensively: absent,\n * null, and an enrolment mode this template does not know all collapse to null,\n * which the footer reads as \"render nothing\".\n */\nfunction parseResellerProgram(value: unknown): ResellerProgramSummary | null {\n if (!isRecord(value)) return null;\n const mode = optionalString(value.enrollment_mode);\n return mode !== null && (RESELLER_ENROLLMENT_MODES as readonly string[]).includes(mode)\n ? { enrollmentMode: mode as ResellerProgramSummary['enrollmentMode'] }\n : null;\n}\n\nconst CURRENCY_CODE = /^[A-Z]{3}$/;\n\n/**\n * The shop's currency list, cleaned to real ISO codes.\n *\n * `available_currencies` is merchant data and arrives as whatever the column\n * holds; anything that is not a three-letter code is dropped rather than\n * offered as a currency nothing can be priced in. The active and default\n * currencies lead the list because a buyer must always be able to switch back\n * to what they are looking at.\n */\nfunction parseCurrencyList(value: unknown, leading: string[]): string[] {\n const entries = Array.isArray(value) ? value : [];\n const codes = [...leading, ...entries]\n .map((entry) => (typeof entry === 'string' ? entry.trim().toUpperCase() : ''))\n .filter((entry) => CURRENCY_CODE.test(entry));\n return [...new Set(codes)];\n}\n\n/** The four orders every Shoppex lane sorts a catalog listing by. */\nexport const STOREFRONT_SORT_KEYS = ['featured', 'newest', 'price-asc', 'price-desc'] as const;\n\nexport type StorefrontSortKey = (typeof STOREFRONT_SORT_KEYS)[number];\n\n/**\n * `store.default_sort` off the wire, normalized to one of the four keys.\n *\n * Field-for-field port of `normalizeStorefrontSortKey`\n * (`packages/storefront-render-core/src/normalize-storefront-sort-key.ts`),\n * because the column does NOT hold those keys: the response contract emits the\n * dashboard's enum (`DEFAULT`, `NEWEST`, `PRICE`, `PRICE_DESC`, `BESTSELLER`,\n * `NAME`, …). Reading it literally would leave every shop on 'featured' and the\n * merchant's chosen order would never reach either lane. An order this theme\n * cannot express — name, stock, type — degrades to the merchant's own shelf\n * order rather than to a different one.\n */\nexport function parseStorefrontSortKey(value: unknown): StorefrontSortKey {\n const raw = optionalString(value);\n if (!raw) return 'featured';\n if ((STOREFRONT_SORT_KEYS as readonly string[]).includes(raw)) return raw as StorefrontSortKey;\n switch (raw.toUpperCase()) {\n case 'NEWEST':\n return 'newest';\n case 'PRICE':\n case 'PRICE_ASC':\n return 'price-asc';\n case 'PRICE_DESC':\n return 'price-desc';\n default:\n return 'featured';\n }\n}\n\nexport function getStorefrontSettings(): StorefrontSettings {\n const store = window.__SHOPPEX_INITIAL__?.store;\n if (!store) {\n throw new Error('Invalid Shoppex initial data: store is required.');\n }\n const activeCurrency = (optionalString(store.currency) ?? '').toUpperCase();\n const defaultCurrency = (optionalString(store.default_currency) ?? activeCurrency).toUpperCase();\n return {\n availableCurrencies: parseCurrencyList(store.available_currencies, [\n activeCurrency,\n defaultCurrency,\n ]),\n defaultCurrency,\n activeCurrency,\n availableGateways: (Array.isArray(store.available_gateways) ? store.available_gateways : [])\n .filter((entry): entry is string => typeof entry === 'string' && entry.trim() !== ''),\n cartEnabled: optionalBoolean(store.cart_enabled, true, 'store.cart_enabled'),\n hideOutOfStock: optionalBoolean(store.hide_out_of_stock, false, 'store.hide_out_of_stock'),\n hideStockCounter: optionalBoolean(store.hide_stock_counter, false, 'store.hide_stock_counter'),\n searchEnabled: optionalBoolean(store.search_enabled, true, 'store.search_enabled'),\n sortEnabled: optionalBoolean(store.sort_enabled, true, 'store.sort_enabled'),\n // The three presentation switches, at the defaults their columns carry.\n centerGroupTitles: optionalBoolean(\n store.center_group_titles,\n false,\n 'store.center_group_titles',\n ),\n descriptionSkipDefaultImage: optionalBoolean(\n store.description_skip_default_image,\n false,\n 'store.description_skip_default_image',\n ),\n descriptionImage: optionalBoolean(store.description_image, true, 'store.description_image'),\n defaultSort: parseStorefrontSortKey(store.default_sort),\n // Both contact switches are MERCHANT settings the serializer always emits.\n // Defaulting them meant a shop that had turned the form off still showed\n // one, and a shop that required an invoice number never asked for it — the\n // visitor only found out from the backend's refusal. A payload without them\n // is broken, so it fails here instead.\n contactFormEnabled: requireBoolean(store.contact_form_enabled, 'store.contact_form_enabled'),\n requireInvoiceForContact: requireBoolean(\n store.require_invoice_for_contact,\n 'store.require_invoice_for_contact',\n ),\n };\n}\n\nexport function getStorefrontAddons(): StorefrontAddon[] {\n const addons = window.__SHOPPEX_INITIAL__?.addons;\n if (addons === undefined) return [];\n if (!isRecord(addons) || !Array.isArray(addons.items)) {\n throw new Error('Invalid Shoppex initial data: addons.items must be an array.');\n }\n return [...addons.items].sort((a, b) => a.sort_order - b.sort_order);\n}\n\n/**\n * The stock a buyer can actually reach, in ONE number: null is unlimited.\n *\n * Variants are the trap. A variant product's own `stock` column is not the\n * inventory anyone buys — the variants hold it — so a product with two\n * unbounded variants and a `stock` of 0 is not sold out. Serial products are\n * the exception: their stock IS the key pool on the product row.\n */\nexport function resolveDisplayStock(product: StorefrontProduct): number | null {\n if (product.productType === 'SERIALS' || product.priceVariants.length === 0) {\n return product.stock;\n }\n // One unbounded option makes the whole product unbounded; otherwise the\n // options add up, because a buyer may take any of them.\n if (product.priceVariants.some((variant) => variant.stock === null)) return null;\n return product.priceVariants.reduce((total, variant) => total + Math.max(variant.stock ?? 0, 0), 0);\n}\n\nexport function isProductSoldOut(product: StorefrontProduct): boolean {\n return resolveDisplayStock(product) === 0;\n}\n\n/**\n * The stock behind ONE selected option — null is unlimited.\n *\n * A variant without its own counter inherits the product's, and the whole\n * product falls back to the product row when EVERY variant reports zero while\n * the product itself has units — that combination is a catalog that never\n * populated per-variant stock, not a sold-out product.\n */\nexport function resolveVariantStock(\n product: StorefrontProduct,\n variant: StorefrontPriceVariant,\n): number | null {\n const useProductStock = product.priceVariants.length > 0\n && (product.stock ?? 0) !== 0\n && product.priceVariants.every((entry) => (entry.stock ?? 0) === 0);\n if (useProductStock || variant.stock === null) return product.stock;\n return variant.stock;\n}\n\n/**\n * The one sold-out predicate every add path passes.\n *\n * PAYMENT PATH. A product-level read is not enough: an option of an otherwise\n * stocked product can be sold out on its own, and the backend refuses that line\n * at invoice creation (`errors.checkout.insufficient_stock`). Adding it anyway\n * only moves the dead end to checkout.\n */\nexport function isSelectionSoldOut(\n product: StorefrontProduct,\n variant: StorefrontPriceVariant | null,\n): boolean {\n if (variant === null) {\n if (isProductSoldOut(product)) return true;\n // A variant product's orderable range is a per-OPTION question — each option carries its own\n // minimum and its own counter — so only a variant-less product may be judged on the product row.\n if (product.priceVariants.length > 0) return false;\n } else if (resolveVariantStock(product, variant) === 0) {\n return true;\n }\n // PAYMENT PATH, the second way a selection can be unsellable: an EMPTY effective range.\n // `quantityMin: 2` against one remaining unit is {min 2, max 1} — no quantity the buyer is\n // allowed to order can be served. Reading `stock === 0` alone called that option in stock, so the\n // add reached the SDK and only died at invoice creation (`errors.checkout.insufficient_stock`),\n // after the buyer had been told it landed. Pay-what-you-want inherits this: every add path,\n // named price or not, passes through here.\n const { min, max } = resolveQuantityBounds(product, variant);\n return max !== null && min > max;\n}\n\n/**\n * The stock counter resolves per product: a product's own hide_stock_counter\n * wins, null inherits the shop-wide setting — same rule the document lane's\n * readHideStockCounterForProduct applies.\n */\nexport function resolveHideStockCounter(\n product: StorefrontProduct,\n settings: StorefrontSettings,\n): boolean {\n return product.hideStockCounter ?? settings.hideStockCounter;\n}\n\n/**\n * How many units the buyer may put on ONE line, for the selected option.\n *\n * A variant overrides the product's own bounds, and the stock caps whatever the\n * merchant configured: offering a stepper that runs past the inventory produces\n * a cart the quote then rejects. `max: null` is unlimited, never zero.\n */\nexport function resolveQuantityBounds(\n product: StorefrontProduct,\n variant: StorefrontPriceVariant | null,\n): { min: number; max: number | null } {\n const min = variant?.quantityMin ?? product.quantityMin;\n const configuredMax = variant?.quantityMax ?? product.quantityMax;\n // The stock a buyer can actually REACH for this selection, not the raw column: an option without\n // its own counter inherits the product's, and the all-zero legacy shape reads the product row.\n // The sold-out gate resolves the same numbers, so the stepper and the gate can never disagree —\n // a bare `variant.stock` capped the legacy shape at 0 and made every option look unsellable.\n const stockMax = variant === null ? resolveDisplayStock(product) : resolveVariantStock(product, variant);\n const max = configuredMax === null\n ? stockMax\n : stockMax === null\n ? configuredMax\n : Math.min(configuredMax, stockMax);\n return { min, max };\n}\n\nfunction parseProduct(value: unknown, index: number): StorefrontProduct {\n const context = `product at index ${index}`;\n const product = requireUnknownRecord(value, context);\n const id = requireString(product, 'uniqid', context);\n // The real db id — falls back to uniqid if the payload omits it.\n const productId = optionalString(product.id) ?? id;\n // price_discount is a PERCENT (0–100) off the wire prices, which arrive\n // undiscounted — the document lane applies it at display time and the\n // backend applies it again at charge time, so a storefront that skips it\n // shows a higher number than the buyer pays. EXCEPT for pay-what-you-want:\n // the server validates the buyer's amount against the UNDISCOUNTED minimum\n // and deliberately disables the product discount there, so discounting a\n // PWYW price would offer a default the checkout then rejects as too low.\n const discountPercent = product.pay_what_you_want === true\n ? 0\n : normalizeDiscountPercent(product.price_discount);\n const applyDiscount = (amount: number): number =>\n discountPercent > 0 ? roundCurrency(amount * (1 - discountPercent / 100)) : amount;\n const rawVariants = parsePriceVariants(product.price_variants, context);\n // Per-unit rounding is display-only (a quantity of 1 rounds identically at\n // line level); quantity math must go through calculateDisplayLineTotal on\n // baseUnitPrice, or fractional cents disagree with the quote.\n const priceVariants = rawVariants.map((variant) => ({\n ...variant,\n baseUnitPrice: variant.price,\n price: applyDiscount(variant.price),\n }));\n // Variant prices are authoritative whenever variants exist — the catalog\n // payload routinely carries price 0 on the product row of a variant product,\n // so the base price is only the fallback for variant-less products. Same\n // rule as the document lane's resolveProductStartingPrice.\n const basePrice = parseFiniteNumber(product.price_display ?? product.price, `${context}.price`);\n const undiscountedPrice = rawVariants.length\n ? Math.min(...rawVariants.map((variant) => variant.price))\n : basePrice;\n const price = applyDiscount(undiscountedPrice);\n const quantityMin = normalizeMinimum(product.quantity_min, 1, `${context}.quantity_min`);\n const rating = deriveProductRating(product.feedback);\n const productType = parseProductType(product.type);\n\n return {\n id,\n productId,\n productType,\n name: requireString(product, 'title', context),\n price,\n currency: requireString(product, 'currency', context),\n description: optionalString(product.description),\n slug: optionalString(product.slug),\n imageUrl: resolveProductImage(product),\n imageUrls: parseProductImageUrls(product.images),\n stock: applyOrderable(\n normalizeStock(product.stock, `${context}.stock`),\n product.orderable,\n product.supplier_backed,\n ),\n lowStockThreshold: normalizeLowStockThreshold(\n normalizeOptionalNonNegativeNumber(product.quantity_warning),\n ),\n hideStockCounter:\n typeof product.hide_stock_counter === 'boolean' ? product.hide_stock_counter : null,\n volumeDiscounts: parseVolumeDiscounts(product.volume_discounts),\n payWhatYouWant: product.pay_what_you_want === true,\n discountPercent,\n baseUnitPrice: undiscountedPrice,\n basePrice,\n priceVariants,\n quantityMin,\n quantityMax: normalizeMaximum(product.quantity_max, quantityMin, `${context}.quantity_max`),\n soldCount: normalizeOptionalNonNegativeNumber(product.sold_count) ?? 0,\n bestseller: product.bestseller === true,\n badges: parseProductBadges(product.badges),\n sortPriority: normalizeOptionalNonNegativeNumber(product.sort_priority) ?? 0,\n createdAt: optionalString(product.created_at),\n categories: parseCategories(product.categories),\n bundles: parseBundles(product.bundles),\n automaticDeals: parseAutomaticDeals(product.automatic_deals),\n videoEmbedSrc: resolveProductVideoEmbedSrc(product.video_link),\n addons: parseAddons(product.addons),\n customFields: parseCustomFields(product.custom_fields, context),\n // Strict on purpose: the serializer always emits `on_hold`, so an absent\n // flag is the only tolerated shape (false) and a present non-boolean is a\n // changed contract, not a paused product. Guessing here sells a product the\n // merchant paused.\n onHold: optionalBoolean(product.on_hold, false, `${context}.on_hold`),\n compareAtPrice: price < undiscountedPrice ? undiscountedPrice : null,\n rating: rating?.average ?? null,\n ratingCount: rating?.count ?? 0,\n ratingTally: rating?.tally ?? null,\n highlights: parseHighlights(product.product_highlights),\n metadata: parseMetadata(product.metadata),\n descriptionTabs: parseContentTabs(product.description_tabs),\n faqs: parseFaqs(product.faqs),\n license: parseProductLicense(product),\n licensingEnabled: product.licensing_enabled === true || product.licensing_enabled === 1,\n deliveryTimeCount: normalizePositiveDays(product.delivery_time),\n deliveryTimeUnit: normalizeDeliveryTimeUnit(product.delivery_time_unit),\n subscription: parseSubscription(product, productType),\n };\n}\n\n\n/* -------------------------------------------------------------------------\n * LICENCE TERM\n *\n * Ported verbatim from templates/code-storefronts/nova. The two code-lane\n * templates are independent merchant starting points with no shared package,\n * and this one carried the same defect nova did: it read the product row only,\n * ignored the selected option, dropped lifetime, and never checked\n * licensing_enabled. Keeping the resolution identical is what stops the two\n * templates from stating different terms for the same product.\n * ---------------------------------------------------------------------- */\n\nexport type LicenseTerm =\n | { kind: 'lifetime' }\n | { kind: 'period'; days: number };\n\n/**\n * What ONE row — a product or one of its options — states about its own term.\n *\n * `inherit` is an option saying the product answers for it; only an option may say it, because the\n * product is the root of the term. `unresolved` is a row stating a term nobody can read: an unknown\n * mode, a PERIOD carrying no days, a legacy `'90d'` that was never validated.\n *\n * These three are separate states on purpose. They used to share one nullable column — `NULL` meant\n * \"inherit\" on an option and \"nothing\" on a product — and on 2026-08-18 the unnamed state was\n * rendered as \"Lifetime\": 5,333 licences were sold as perpetual and issued with a 30-day expiry.\n * Neither `inherit` nor `unresolved` may become a term here.\n */\nexport type LicenseStatement =\n | LicenseTerm\n | { kind: 'inherit' }\n | { kind: 'unresolved' };\n\nfunction readLicensePeriodDays(value: unknown): number | null {\n if (typeof value !== 'number' && typeof value !== 'string') return null;\n const days = Number(value);\n return Number.isInteger(days) && days > 0 ? days : null;\n}\n\nfunction readLicenseMode(value: unknown): string | null {\n return typeof value === 'string' && value.trim() !== '' ? value.trim().toUpperCase() : null;\n}\n\n/** `INHERIT` is answered by the caller: only an option may state it. */\nfunction licenseStatementFromMode(mode: string, record: Record<string, unknown>): LicenseStatement {\n if (mode === 'LIFETIME') return { kind: 'lifetime' };\n if (mode !== 'PERIOD') return { kind: 'unresolved' };\n const days = readLicensePeriodDays(record.license_period_days);\n return days === null ? { kind: 'unresolved' } : { kind: 'period', days };\n}\n\n/** What the product row states. A product is the root of the term, so `INHERIT` reads as nothing. */\nfunction parseProductLicense(record: Record<string, unknown>): LicenseStatement {\n const mode = readLicenseMode(record.license_mode);\n if (mode !== null) return licenseStatementFromMode(mode, record);\n const legacy = record.license_period;\n if (typeof legacy === 'string' && legacy.trim().toUpperCase() === 'LIFETIME') return { kind: 'lifetime' };\n if (legacy === 0 || (typeof legacy === 'string' && legacy.trim() === '0')) return { kind: 'lifetime' };\n const days = readLicensePeriodDays(legacy);\n return days === null ? { kind: 'unresolved' } : { kind: 'period', days };\n}\n\n/** What one option states. Legacy `NULL` meant \"inherit from the product\" — now it is named. */\nfunction parseVariantLicense(record: Record<string, unknown>): LicenseStatement {\n const mode = readLicenseMode(record.license_mode);\n if (mode === 'INHERIT') return { kind: 'inherit' };\n if (mode !== null) return licenseStatementFromMode(mode, record);\n const legacy = record.license_period;\n if (legacy === null || legacy === undefined) return { kind: 'inherit' };\n if (legacy === 0 || (typeof legacy === 'string' && legacy.trim() === '0')) return { kind: 'lifetime' };\n const days = readLicensePeriodDays(legacy);\n return days === null ? { kind: 'unresolved' } : { kind: 'period', days };\n}\n\n/**\n * The term the buyer is being sold for the option they have selected, or `null` when nothing about\n * it is resolvable — licensing off, a mode nobody can read, a PERIOD with no days, an option\n * inheriting from a product that states nothing itself.\n *\n * `null` means the page renders NO licence row. It never means \"Lifetime\".\n */\nexport function resolveLicenseTerm(\n product: StorefrontProduct,\n variant: StorefrontPriceVariant | null,\n): LicenseTerm | null {\n if (!product.licensingEnabled) return null;\n const stated = variant ? variant.license : ({ kind: 'inherit' } as const);\n if (stated.kind === 'lifetime' || stated.kind === 'period') return stated;\n if (stated.kind === 'unresolved') return null;\n const inherited = product.license;\n return inherited.kind === 'lifetime' || inherited.kind === 'period' ? inherited : null;\n}\n\n/** \"Lifetime\" / \"30 days\" — the same wording the Easy lane prints for the same product. */\nexport function formatLicenseTerm(term: LicenseTerm): string {\n return term.kind === 'lifetime' ? 'Lifetime' : `${term.days} ${term.days === 1 ? 'day' : 'days'}`;\n}\n\n\nfunction parsePriceVariants(value: unknown, productContext: string): StorefrontPriceVariant[] {\n if (value === null || value === undefined) return [];\n const source = Array.isArray(value)\n ? value\n : isRecord(value) && Array.isArray(value.price_variants)\n ? value.price_variants\n : null;\n if (!source) {\n throw new Error(`Invalid Shoppex ${productContext}.price_variants: expected an array.`);\n }\n return source.map((entry, index) => {\n const context = `${productContext}.price_variants.${index}`;\n const variant = requireUnknownRecord(entry, context);\n const quantityMin = normalizeOptionalMinimum(variant.quantity_min, context);\n // price_display is the buyer-currency amount; price is the shop-currency\n // base. The document lane prefers price_display too. Both fields start\n // undiscounted here; parseProduct applies the percent to `price` only.\n const unitPrice = parseFiniteNumber(\n variant.price_display ?? variant.price,\n `${context}.price`,\n );\n return {\n id: requireString(variant, 'id', context),\n label: requireStringFromKeys(variant, ['label', 'title'], context),\n license: parseVariantLicense(variant),\n price: unitPrice,\n baseUnitPrice: unitPrice,\n stock: applyOrderable(\n normalizeStock(variant.stock, `${context}.stock`),\n variant.orderable,\n variant.supplier_backed,\n ),\n quantityMin,\n quantityMax: normalizeMaximum(variant.quantity_max, quantityMin ?? 1, `${context}.quantity_max`),\n imageUrl: optionalString(variant.image_url),\n isFeatured: variant.is_featured === true || variant.isFeatured === true,\n };\n });\n}\n\n/**\n * Categories off a product. The API sends objects ({title, uniqid, …}); a\n * category without both an id and a title is dropped rather than rendered as\n * an unmatchable filter chip.\n */\nfunction parseCategories(value: unknown): ProductCategoryRef[] {\n if (!Array.isArray(value)) return [];\n const categories: ProductCategoryRef[] = [];\n for (const entry of value) {\n if (!isRecord(entry)) continue;\n const id = optionalString(entry.uniqid ?? entry.id);\n const title = optionalString(entry.title);\n if (!id || !title || categories.some((category) => category.id === id)) continue;\n categories.push({ id, title });\n }\n return categories;\n}\n\n/**\n * The product kind. An unknown kind is NOT an error — the platform may ship a\n * new one before this template knows about it — but it must not silently pick\n * up `INFO_CARD` behaviour, so it degrades to the plain buyable product.\n */\nfunction parseProductType(value: unknown): ProductType {\n const normalized = typeof value === 'string' ? value.trim().toUpperCase() : '';\n return (PRODUCT_TYPES as readonly string[]).includes(normalized)\n ? (normalized as ProductType)\n : 'PRODUCT';\n}\n\n/**\n * Bundle offers off a product. A bundle needs at least two resolvable items to\n * mean anything, so partially-resolved ones (a member product was deleted) are\n * dropped rather than advertised as a deal the buyer cannot complete.\n */\nfunction parseBundles(value: unknown): ProductBundle[] {\n if (!Array.isArray(value)) return [];\n const bundles: ProductBundle[] = [];\n for (const entry of value) {\n if (!isRecord(entry)) continue;\n const rawItems = Array.isArray(entry.products) ? entry.products : [];\n const items: ProductBundleItem[] = [];\n for (const rawItem of rawItems) {\n if (!isRecord(rawItem)) continue;\n const id = typeof rawItem.uniqid === 'string' ? rawItem.uniqid.trim() : '';\n const title = typeof rawItem.title === 'string' ? rawItem.title.trim() : '';\n if (!id || !title) continue;\n const unitPrice = Number(rawItem.unit_price);\n items.push({\n id,\n title,\n variantId: typeof rawItem.default_variant_id === 'string' && rawItem.default_variant_id\n ? rawItem.default_variant_id\n : 'default',\n unitPrice: Number.isFinite(unitPrice) && unitPrice >= 0 ? unitPrice : 0,\n imageUrl: optionalString(rawItem.image_url),\n variantTitle: optionalString(rawItem.variant_title),\n });\n }\n if (items.length < 2) continue;\n\n const amount = Number(entry.discount_amount);\n bundles.push({\n id: typeof entry.uniqid === 'string' && entry.uniqid ? entry.uniqid : String(entry.id ?? ''),\n title: typeof entry.title === 'string' && entry.title.trim()\n ? entry.title.trim()\n : 'Bundle offer',\n discountType: entry.discount_type === 'FIXED' ? 'FIXED' : 'PERCENTAGE',\n discountAmount: Number.isFinite(amount) && amount > 0 ? amount : 0,\n items,\n });\n }\n return bundles;\n}\n\n/**\n * Paid extras off a product. An addon without an id cannot be selected or sent\n * to the cart, and one without a title cannot be labelled, so both are required\n * — a half-resolved row would render as an unnamed surcharge.\n */\nfunction parseAddons(value: unknown): ProductAddon[] {\n if (!Array.isArray(value)) return [];\n const addons: ProductAddon[] = [];\n for (const entry of value) {\n if (!isRecord(entry)) continue;\n const id = optionalString(entry.id);\n const title = optionalString(entry.title ?? entry.name);\n if (!id || !title || addons.some((addon) => addon.id === id)) continue;\n const price = Number(entry.price);\n addons.push({\n id,\n title,\n price: Number.isFinite(price) && price > 0 ? price : 0,\n required: entry.required === true,\n });\n }\n return addons;\n}\n\n/**\n * The product's gallery image URLs. Entries arrive as attachment records\n * (`{ id, uniqid, cloudflare_image_id, url }`); one without a resolved `url`\n * has no picture to show and is dropped rather than rendered as a blank frame.\n */\nfunction parseProductImageUrls(value: unknown): string[] {\n if (!Array.isArray(value)) return [];\n const urls: string[] = [];\n for (const entry of value) {\n const url = typeof entry === 'string' ? optionalString(entry) : isRecord(entry) ? optionalString(entry.url) : null;\n if (url && !urls.includes(url)) urls.push(url);\n }\n return urls;\n}\n\n/** Bulleted highlights — strings only, empties dropped. Missing → []. */\n/**\n * Metadata rides the wire as a flat string->string object. Non-string values\n * are dropped rather than coerced so a consumer can rely on the declared type.\n */\nfunction parseMetadata(value: unknown): Record<string, string> {\n if (!isRecord(value)) return {};\n const result: Record<string, string> = {};\n for (const [key, entry] of Object.entries(value)) {\n if (typeof entry === 'string') result[key] = entry;\n }\n return result;\n}\n\nfunction parseHighlights(value: unknown): string[] {\n if (!Array.isArray(value)) return [];\n return value\n .map((entry) =>\n typeof entry === 'string'\n ? entry.trim()\n : isRecord(entry)\n ? optionalString(entry.text ?? entry.title ?? entry.label)\n : null,\n )\n .filter((entry): entry is string => Boolean(entry));\n}\n\n/** Merchant content tabs ({title, content|html}). Missing/malformed → []. */\nfunction parseContentTabs(value: unknown): ProductContentTab[] {\n if (!Array.isArray(value)) return [];\n const tabs: ProductContentTab[] = [];\n for (const entry of value) {\n if (!isRecord(entry)) continue;\n const title = optionalString(entry.title);\n const html = optionalString(entry.content ?? entry.html ?? entry.contentHtml);\n if (title && html) tabs.push({ title, html });\n }\n return tabs;\n}\n\n/**\n * The buyer details this product requires, off the wire.\n *\n * PAYMENT PATH. `custom_fields` arrives as a JSON STRING on the catalog\n * contract (`stringifyJsonField` in the backend's response transformer), not as\n * an array, and it may wrap the list in `{ custom_fields: [...] }`. Every one of\n * those shapes is what the SDK's `normalizeStorefrontCustomFields` accepts, and\n * this is a field-for-field port of it — the Easy lane validates a merchant's\n * definitions with that function, so a definition the two lanes read\n * differently is a product one lane can sell and the other cannot.\n *\n * Ported rather than imported because the theme consumes the SDK as a UMD\n * global and these helpers are module exports of `@shoppex/sdk`, not members of\n * `window.shoppex`.\n *\n * `hidden` fields are dropped (the buyer never fills those in) and so is any\n * entry without a name, which could not be keyed onto a cart line anyway.\n */\nfunction parseCustomFields(value: unknown, context: string): ProductCustomField[] {\n const source = readCustomFieldList(value, context);\n const fields: ProductCustomField[] = [];\n for (const entry of source) {\n if (!isRecord(entry)) continue;\n const name = optionalString(entry.name);\n if (!name) continue;\n const rawType = typeof entry.type === 'string' ? entry.type.trim().toLowerCase() : '';\n const type = rawType || 'text';\n if (type === 'hidden') continue;\n // The definition's own pre-filled value, under any of the three keys the\n // dashboard has written it as.\n const defaultValue = firstOptionalString(entry, ['default_value', 'default', 'value']);\n fields.push({\n name,\n type,\n // The dashboard has stored this flag as a boolean, a number and a string\n // over time; the SDK accepts all three, and reading only one of them\n // would turn a required field into an optional one — which is exactly the\n // failure that lets a product reach checkout without its values.\n required: entry.required === true\n || entry.required === 1\n || entry.required === '1'\n || entry.required === 'true',\n defaultValue: defaultValue ?? '',\n placeholder: typeof entry.placeholder === 'string' ? entry.placeholder : '',\n regex: optionalString(entry.regex),\n });\n }\n return fields;\n}\n\n/** The three shapes `custom_fields` arrives in: a JSON string, a wrapper record, or a bare array. */\nfunction readCustomFieldList(value: unknown, context: string): unknown[] {\n if (Array.isArray(value)) return value;\n if (typeof value === 'string') {\n const text = value.trim();\n if (!text) return [];\n try {\n return readCustomFieldList(JSON.parse(text) as unknown, context);\n } catch {\n // Merchant-authored JSON that no longer parses. It cannot be repaired\n // into field definitions, and inventing none would sell a product whose\n // required details were never asked for — so the payload is rejected.\n throw new Error(`Invalid Shoppex ${context}.custom_fields: expected valid JSON.`);\n }\n }\n if (isRecord(value)) {\n const fields = value.custom_fields ?? value.customFields;\n return Array.isArray(fields) ? fields : [];\n }\n return [];\n}\n\n/**\n * Active quantity deals off a product, from the catalog's `automatic_deals`.\n *\n * The two quantities and the discount are what the wording states, so a rule\n * missing any of them is dropped rather than rendered as an offer with a blank\n * in it — exactly the rows the Easy lane's `dealRow` returns '' for. A rule the\n * theme cannot phrase is a rule the buyer cannot act on, and the backend\n * applies it either way, so dropping it understates rather than misleads.\n */\nfunction parseAutomaticDeals(value: unknown): ProductAutomaticDeal[] {\n if (!Array.isArray(value)) return [];\n const deals: ProductAutomaticDeal[] = [];\n for (const entry of value) {\n if (!isRecord(entry)) continue;\n const requiredQuantity = Number(entry.required_quantity);\n const discountedQuantity = Number(entry.discounted_quantity);\n if (\n !Number.isInteger(requiredQuantity) || requiredQuantity < 1\n || !Number.isInteger(discountedQuantity) || discountedQuantity < 1\n ) {\n continue;\n }\n const isFree = optionalString(entry.discount_type)?.toUpperCase() === 'FREE';\n const discountValue = Number(entry.discount_value);\n // A percentage rule with no percent off is not an offer.\n if (!isFree && !(Number.isFinite(discountValue) && discountValue > 0)) continue;\n const cap = Number(entry.max_applications_per_cart);\n deals.push({\n id: optionalString(entry.uniqid) ?? `deal-${deals.length}`,\n title: optionalString(entry.title) ?? '',\n requiredQuantity,\n discountedQuantity,\n discountType: isFree ? 'FREE' : 'PERCENTAGE',\n discountValue: isFree ? 0 : discountValue,\n maxApplicationsPerCart: Number.isInteger(cap) && cap > 0 ? cap : null,\n endsAt: optionalString(entry.ends_at),\n });\n }\n return deals;\n}\n\n/** Per-product FAQ ({question, answer}). Missing/malformed → []. */\nfunction parseFaqs(value: unknown): ProductFaq[] {\n if (!Array.isArray(value)) return [];\n const faqs: ProductFaq[] = [];\n for (const entry of value) {\n if (!isRecord(entry)) continue;\n const question = optionalString(entry.question);\n const answer = optionalString(entry.answer);\n if (question && answer) faqs.push({ question, answer });\n }\n return faqs;\n}\n\n/**\n * volume_discounts off the wire: either a bare array or `{ volume_discounts:\n * [...] }`, entries keyed `quantity`/`min_quantity` + `value`/`discount` +\n * `type`. Same tolerant shape the document lane's parseVolumeDiscountRules\n * accepts; malformed entries are dropped rather than guessed at.\n */\nfunction parseVolumeDiscounts(value: unknown): VolumeDiscountRule[] {\n const source = Array.isArray(value)\n ? value\n : isRecord(value) && Array.isArray(value.volume_discounts)\n ? value.volume_discounts\n : [];\n const rules: VolumeDiscountRule[] = [];\n for (const entry of source) {\n if (!isRecord(entry)) continue;\n const quantity = Number(entry.quantity ?? entry.min_quantity);\n const amount = Number(entry.value ?? entry.discount ?? entry.amount);\n const typeRaw = String(entry.type ?? entry.discount_type ?? '').toUpperCase();\n const type = typeRaw.includes('PERCENT')\n ? 'PERCENTAGE'\n : typeRaw.includes('FIX') || typeRaw.includes('AMOUNT')\n ? 'FIXED'\n : null;\n if (\n !Number.isFinite(quantity) || quantity < 1\n || !Number.isFinite(amount) || amount <= 0\n || !type\n ) {\n continue;\n }\n rules.push({ quantity: Math.floor(quantity), type, value: amount });\n }\n return rules.sort((a, b) => a.quantity - b.quantity);\n}\n\n/**\n * Exact integer cents for an amount that is itself a valid money value.\n * toPrecision(15) trims the ~1e-16-relative binary noise (10.03 x 50% floats\n * as 5.014999...) so the half-up round agrees with the quote's Decimal\n * ROUND_HALF_UP; legitimate cents survive up to 13-digit totals.\n */\nfunction toCents(value: number): number {\n return Math.round(Number((value * 100).toPrecision(15)));\n}\n\n/** Half-up rounding to cents. Every displayed money amount rounds through here. */\nexport function roundCurrency(value: number): number {\n return toCents(value) / 100;\n}\n\n/**\n * Line total the way the quote computes it: undiscounted subtotal, percent\n * discount applied at LINE level (not per rounded unit — $9.99 at 15% × 10 is\n * $84.92, not 10 × $8.49), then the volume tier. Intermediate values stay\n * unrounded like the quote's Decimal chain; only the final amount rounds —\n * rounding between the two percent stages shifts the cent ($76.43 vs the\n * quoted $76.42 for 15% + 10% on a $99.90 line). Every displayed quantity\n * price must come from here or it disagrees with checkout.\n */\nexport function calculateDisplayLineTotal(\n baseUnitPrice: number,\n quantity: number,\n discountPercent: number,\n rules: VolumeDiscountRule[],\n): number {\n // Integer minor units: the base price is a money value, so its cents are\n // exact and the quantity multiplication stays exact. Percent stages run\n // unrounded on cents; the line rounds once in applyVolumeDiscountCents.\n const subtotalCents = toCents(baseUnitPrice) * quantity;\n const afterPercentCents = discountPercent > 0\n ? subtotalCents * (1 - discountPercent / 100)\n : subtotalCents;\n return applyVolumeDiscountCents(afterPercentCents, quantity, rules) / 100;\n}\n\n/**\n * Cent subtotal with the highest matching volume tier applied — mirrors the\n * document lane's calculateDiscountedLinePricing so the shown total matches\n * what checkout charges. This is the single rounding point of the line: the\n * subtotal arrives unrounded from the percent stage and leaves as whole cents.\n */\nfunction applyVolumeDiscountCents(\n subtotalCents: number,\n quantity: number,\n rules: VolumeDiscountRule[],\n): number {\n let matched: VolumeDiscountRule | null = null;\n for (const rule of rules) {\n if (quantity >= rule.quantity) matched = rule;\n }\n if (!matched) return Math.max(0, Math.round(subtotalCents));\n const discountCents = matched.type === 'PERCENTAGE'\n ? (subtotalCents * Math.min(Math.max(matched.value, 0), 100)) / 100\n : toCents(matched.value);\n return Math.max(0, Math.round(subtotalCents - Math.min(discountCents, subtotalCents)));\n}\n\n/**\n * Honest star rating from Shoppex feedback tallies (positive/neutral/negative).\n * Returns null when there is no feedback at all, so the UI shows no stars\n * rather than a fabricated score. Weights: positive=5, neutral=3, negative=1.\n */\nfunction deriveProductRating(\n value: unknown,\n): { average: number; count: number; tally: { positive: number; neutral: number; negative: number } } | null {\n if (!isRecord(value)) return null;\n const total = Number(value.total);\n if (!Number.isFinite(total) || total <= 0) return null;\n const positive = Number(value.positive) || 0;\n const neutral = Number(value.neutral) || 0;\n const negative = Number(value.negative) || 0;\n const counted = positive + neutral + negative;\n if (counted <= 0) return null;\n const average = (positive * 5 + neutral * 3 + negative * 1) / counted;\n return { average: Math.round(average * 10) / 10, count: counted, tally: { positive, neutral, negative } };\n}\n\nfunction resolveProductImage(product: Record<string, unknown>): string | null {\n const direct = firstOptionalString(product, [\n 'image_url',\n 'imageUrl',\n 'cdn_image_url',\n 'detail_image_url',\n 'thumbnail_url',\n 'thumbnailUrl',\n ]);\n if (direct) return direct;\n if (!Array.isArray(product.images)) return null;\n for (const image of product.images) {\n if (!isRecord(image)) continue;\n const url = optionalString(image.url);\n if (url) return url;\n }\n return null;\n}\n\nfunction normalizeStock(value: unknown, context: string): number | null {\n if (value === null || value === undefined || value === '') return null;\n const stock = parseFiniteNumber(value, context);\n return stock < 0 ? null : Math.floor(stock);\n}\n\n/**\n * Dynamic Delivery: `orderable` is the backend's buyability predicate (local\n * stock covers one unit OR a supplier source fulfils the line) and is what the\n * order-time gates apply — a supplier-backed row holds NO local stock by\n * design, so gating on `stock` alone renders it \"sold out\" and the purchase\n * can never happen. Folded into the stock value the same way the document\n * lane's applyOrderable does: an orderable row without bounding local units\n * becomes unlimited (null); everything downstream then behaves as it already\n * does for unlimited stock.\n */\nfunction applyOrderable(\n stock: number | null,\n orderable: unknown,\n supplierBacked: unknown,\n): number | null {\n if (orderable !== true) return stock;\n // Supplier-backed: local units do not bound the line, at any stock value.\n if (supplierBacked === true) return null;\n return stock === 0 ? null : stock;\n}\n\nfunction normalizeOptionalNonNegativeNumber(value: unknown): number | null {\n if (value === null || value === undefined || value === '') return null;\n const parsed = Number(value);\n return Number.isFinite(parsed) && parsed >= 0 ? Math.floor(parsed) : null;\n}\n\nfunction normalizeMinimum(value: unknown, fallback: number, context: string): number {\n if (value === null || value === undefined || value === '') return fallback;\n const minimum = Math.floor(parseFiniteNumber(value, context));\n if (minimum < 1) {\n throw new Error(`Invalid Shoppex ${context}: expected a positive integer.`);\n }\n return minimum;\n}\n\nfunction normalizeOptionalMinimum(value: unknown, context: string): number | null {\n if (value === null || value === undefined || value === '') return null;\n return normalizeMinimum(value, 1, `${context}.quantity_min`);\n}\n\nfunction normalizeMaximum(value: unknown, minimum: number, context: string): number | null {\n if (value === null || value === undefined || value === '') return null;\n const maximum = Math.floor(parseFiniteNumber(value, context));\n if (maximum < 0) return null;\n if (maximum < minimum) {\n throw new Error(`Invalid Shoppex ${context}: maximum cannot be below minimum.`);\n }\n return maximum;\n}\n\n/** quantity_warning uses 0 for \"disabled\" — map that to null, not a live threshold. */\nfunction normalizeLowStockThreshold(value: number | null): number | null {\n return value !== null && value > 0 ? value : null;\n}\n\n/**\n * price_discount off the wire, clamped to a usable percent. The API contract\n * pins it to an integer 0–100; anything outside that or non-numeric means \"no\n * discount\" rather than a guessed one.\n */\nfunction normalizeDiscountPercent(value: unknown): number {\n if (value === null || value === undefined || value === '') return 0;\n const percent = Number(value);\n if (!Number.isFinite(percent) || percent <= 0) return 0;\n return Math.min(percent, 100);\n}\n\n/** Positive-integer days, or null when unset/zero/invalid. */\nfunction normalizePositiveDays(value: unknown): number | null {\n if (value === null || value === undefined || value === '') return null;\n const parsed = Number(value);\n return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : null;\n}\n\n/**\n * Delivery units, mirroring the `products.delivery_time_unit` CHECK constraint.\n * The column is NOT NULL DEFAULT 'days', so an absent value only comes from a\n * payload minted before the unit existed — and days is what it meant.\n */\nexport const DELIVERY_TIME_UNITS = ['minutes', 'hours', 'days'] as const;\nexport type DeliveryTimeUnit = (typeof DELIVERY_TIME_UNITS)[number];\n\nfunction normalizeDeliveryTimeUnit(value: unknown): DeliveryTimeUnit {\n return DELIVERY_TIME_UNITS.includes(value as DeliveryTimeUnit)\n ? (value as DeliveryTimeUnit)\n : 'days';\n}\n\n/** \"1 hour\" / \"3 days\" — the count with its unit, singular when the count is one. */\nexport function formatDeliveryTime(count: number, unit: DeliveryTimeUnit): string {\n return `${count} ${count === 1 ? unit.slice(0, -1) : unit}`;\n}\n","/**\n * The per-product details a buyer has to supply before an invoice can exist.\n *\n * PAYMENT PATH, and the reason this module exists at all: a merchant who marks\n * one product custom field REQUIRED cannot sell that product until its value is\n * on the cart line. `apps/backend/src/services/storefront/invoices/from-cart.ts`\n * validates every line's `custom_fields` and rejects the whole request with\n * `custom_fields_validation_failed` — no invoice is created, so the buyer gets a\n * dead checkout button and the merchant gets no order.\n *\n * WHERE THE VALUES GO. Not into the checkout call: the SDK's `checkout()` has no\n * custom-field option. `mapCartItemsForApi` (`packages/sdk/src/modules/checkout.ts`)\n * reads `custom_fields` off each CART LINE, so the values are written onto the\n * lines with `updateCartItem(lineId, { custom_fields })` BEFORE the hand-off.\n * That is exactly what the Easy lane does — see\n * `packages/storefront-commerce/src/checkout-custom-fields.ts`, whose\n * `writeCustomFieldsToCart` runs on the same contract.\n *\n * WHERE THE DEFINITIONS COME FROM. The Easy lane fetches each cart product\n * (`getProduct(uniqid)`) because it has no catalog in the page. This lane does:\n * `window.__SHOPPEX_INITIAL__` carries every product, listed or not, so the\n * definitions are read straight out of it. There is no request to fail — but\n * there is still a line whose product is not in the payload, and that case\n * FAILS CLOSED for the same reason the Easy lane's failed fetch does: an\n * unresolvable product may carry required fields, and treating \"unknown\" as\n * \"none\" is how a checkout submits without them.\n *\n * VALUES ARE PER PRODUCT, not per cart line. Two lines of the same product (a\n * different option, a different extra) describe the same thing being bought\n * twice, and the merchant asked their question once — so both lines receive the\n * same answers, which is also what keeps them from splitting into two identities\n * the buyer never chose. It differs from the Easy lane, which dedupes by field\n * NAME across the whole cart: two products that both ask \"Steam ID\" are two\n * questions here, and one there.\n */\n\nimport type { SdkCartItem } from './sdk';\nimport { getProducts, type ProductCustomField } from './storefront-data';\n\n/** One product's questions, and the product they belong to. */\nexport interface CheckoutFieldGroup {\n productId: string;\n /** The product's own title — the section heading the buyer reads. */\n productName: string;\n fields: ProductCustomField[];\n}\n\n/**\n * What the cart needs before it can be checked out.\n *\n * `resolved: false` is the fail-closed state: at least one line names a product\n * the payload does not carry, so nothing can be said about what it requires.\n */\nexport type CheckoutFields =\n | { resolved: true; groups: CheckoutFieldGroup[]; hasRequired: boolean }\n | { resolved: false; unresolvedProductIds: string[] };\n\n/** Answers, keyed product id → field name → value. */\nexport type CheckoutFieldValues = Record<string, Record<string, string>>;\n\n/** An empty cart asks nothing. */\nconst NO_FIELDS: CheckoutFields = { resolved: true, groups: [], hasRequired: false };\n\n/**\n * The questions the current cart raises, grouped by product and in cart order.\n *\n * Products with no custom fields produce no group — they are resolved, they\n * simply ask nothing.\n */\nexport function resolveCheckoutFields(lines: readonly SdkCartItem[]): CheckoutFields {\n if (lines.length === 0) return NO_FIELDS;\n // Read on demand rather than once at module load: the payload is parsed\n // strictly and may throw, and the storefront's own boot is where that belongs.\n const products = getProducts();\n\n const groups: CheckoutFieldGroup[] = [];\n const unresolvedProductIds: string[] = [];\n const seen = new Set<string>();\n for (const line of lines) {\n const productId = line.product_id;\n if (seen.has(productId)) continue;\n seen.add(productId);\n const product = products.find((candidate) => candidate.id === productId);\n if (product === undefined) {\n unresolvedProductIds.push(productId);\n continue;\n }\n if (product.customFields.length === 0) continue;\n groups.push({ productId, productName: product.name, fields: product.customFields });\n }\n\n if (unresolvedProductIds.length > 0) return { resolved: false, unresolvedProductIds };\n return {\n resolved: true,\n groups,\n hasRequired: groups.some((group) => group.fields.some((field) => field.required)),\n };\n}\n\n/**\n * Whether the checkout CTA has to route through the details form instead of\n * handing off straight away.\n *\n * True for the fail-closed state as well: a cart whose products cannot be\n * resolved must not be handed to `checkout()` on the assumption that it asks\n * nothing. The form is where that is said out loud.\n *\n * Optional-only fields do NOT gate the hand-off — nothing about the invoice\n * depends on them, and stopping a buyer for a question they may leave blank\n * would be this theme inventing a step. They are still collected whenever the\n * form does open, so a cart that has to stop for one product's required field\n * gathers every other answer in the same pass.\n */\nexport function checkoutNeedsDetails(fields: CheckoutFields): boolean {\n return !fields.resolved || fields.hasRequired;\n}\n\n/**\n * The form's starting values: whatever the cart lines already carry, else the\n * merchant's own default for the field.\n *\n * Pre-filling from the line is what makes the form survive a buyer going back to\n * the cart and returning — the answers are stored on the line, not in React.\n */\nexport function buildInitialCheckoutFieldValues(\n groups: readonly CheckoutFieldGroup[],\n lines: readonly SdkCartItem[],\n): CheckoutFieldValues {\n const values: CheckoutFieldValues = {};\n for (const group of groups) {\n const stored = lines.find((line) => line.product_id === group.productId)?.custom_fields ?? {};\n const entries: Record<string, string> = {};\n for (const field of group.fields) {\n const value = stored[field.name];\n entries[field.name] = typeof value === 'string' && value !== '' ? value : field.defaultValue;\n }\n values[group.productId] = entries;\n }\n return values;\n}\n\n/**\n * Whether a checkbox answer counts as ticked.\n *\n * The stored value is a string, and the four spellings below are the ones the\n * SDK's `isStorefrontCheckboxCustomFieldValueChecked` accepts — a checkbox the\n * two lanes read differently is a required tick one lane honours and the other\n * silently drops.\n */\nexport function isCheckboxValueChecked(value: string | undefined): boolean {\n const normalized = value?.trim().toLowerCase() ?? '';\n return normalized === 'true' || normalized === '1' || normalized === 'yes' || normalized === 'on';\n}\n\n/**\n * The buyer-facing problem with one answer, or null when it is acceptable.\n *\n * A port of the SDK's `validateStorefrontCustomFieldValue`, down to the wording:\n * the Easy lane refuses the same values with the same sentences, and the backend\n * refuses them again when the invoice is built. A malformed merchant regex\n * validates NOTHING rather than blocking every buyer on a pattern that cannot\n * compile — the merchant's mistake must not become the buyer's dead end, and the\n * backend still holds the line.\n */\nexport function validateCheckoutFieldValue(field: ProductCustomField, value: string): string | null {\n if (field.type === 'checkbox') {\n return field.required && !isCheckboxValueChecked(value) ? `${field.name} is required.` : null;\n }\n\n const normalized = value.trim();\n if (field.required && !normalized) return `${field.name} is required.`;\n if (field.regex && normalized) {\n let pattern: RegExp;\n try {\n pattern = new RegExp(field.regex);\n } catch {\n return null;\n }\n if (!pattern.test(normalized)) return `${field.name} has an invalid format.`;\n }\n return null;\n}\n\n/**\n * Every problem the form has to show, keyed `<productId>.<fieldName>` so two\n * products asking the same question keep their own message.\n */\nexport function validateCheckoutFields(\n groups: readonly CheckoutFieldGroup[],\n values: CheckoutFieldValues,\n): Record<string, string> {\n const errors: Record<string, string> = {};\n for (const group of groups) {\n for (const field of group.fields) {\n const message = validateCheckoutFieldValue(\n field,\n values[group.productId]?.[field.name] ?? '',\n );\n if (message) errors[checkoutFieldKey(group.productId, field.name)] = message;\n }\n }\n return errors;\n}\n\nexport function checkoutFieldKey(productId: string, fieldName: string): string {\n return `${productId}.${fieldName}`;\n}\n\n/**\n * One product's answers in the shape a cart line stores.\n *\n * A port of the SDK's `buildStorefrontCustomFieldPayload`: an unticked checkbox\n * and an empty answer are OMITTED rather than stored blank, which is what keeps\n * an optional field the buyer skipped from becoming an empty string the backend\n * has to interpret. A ticked checkbox is always the literal `'true'`.\n */\nexport function buildCustomFieldPayload(\n fields: readonly ProductCustomField[],\n values: Record<string, string> | undefined,\n): Record<string, string> {\n const payload: Record<string, string> = {};\n for (const field of fields) {\n const raw = values?.[field.name] ?? field.defaultValue;\n if (field.type === 'checkbox') {\n if (isCheckboxValueChecked(raw)) payload[field.name] = 'true';\n continue;\n }\n const normalized = raw.trim();\n if (normalized) payload[field.name] = normalized;\n }\n return payload;\n}\n\n/**\n * The required answers that are NOT on the cart, read back out of the SDK.\n *\n * PAYMENT PATH. The write-back is `updateCartItem`, which the SDK can drop the\n * same way it drops an add — its storage wrapper catches a quota or blocked\n * write and only logs it. A hand-off that trusted the write would create no\n * invoice at all (the backend rejects the cart) after telling the buyer their\n * details were taken. So the cart is read back and every required field is\n * checked against the value that actually landed, with the same validator the\n * form used.\n *\n * Returns the buyer-facing messages, empty when the cart is ready to submit.\n */\nexport function missingRequiredCheckoutValues(\n groups: readonly CheckoutFieldGroup[],\n lines: readonly SdkCartItem[],\n): string[] {\n const messages: string[] = [];\n for (const group of groups) {\n const required = group.fields.filter((field) => field.required);\n if (required.length === 0) continue;\n for (const line of lines) {\n if (line.product_id !== group.productId) continue;\n for (const field of required) {\n const message = validateCheckoutFieldValue(field, line.custom_fields?.[field.name] ?? '');\n if (message && !messages.includes(message)) messages.push(message);\n }\n }\n }\n return messages;\n}\n\n/**\n * Whether a details write changed what the cart PRICES.\n *\n * PAYMENT PATH, and the one consequence of the identity merge that is not\n * harmless. Two lines of the same product and option can exist while their\n * `custom_fields` differ — the only way this theme's `add` produces one is a\n * buyer who answered once, came back from a checkout that did not complete, and\n * added the product again from the PDP or through Buy Now, because `add` sends\n * no answers at all. Giving both the SAME answers collapses them into one line\n * whose quantity is the SUM, and the quote proof the SDK is holding is bound to\n * a hash of the cart's products, options and QUANTITIES (`custom_fields` and\n * `delivery_instructions` are deliberately NOT hashed — see\n * `apps/backend/src/services/storefront/cart/cartQuoteToken.ts`). So the answers\n * alone never stale a proof, and a merge always does.\n *\n * Handing off anyway is refused by `/from-cart` as `quote_token_stale`. The\n * hand-off already recovers from that refusal by re-quoting, so nothing is\n * mispriced — but the buyer spends a click and a server round trip on a message\n * about a cart change they did not make. Answering it here instead re-quotes\n * straight away and names what actually happened.\n *\n * Compared on the priced shape rather than on `line_id`s, because every id in\n * the cart changes when the answers are written — that is the identity rehash\n * doing its job, not a repricing. A merge is the only thing this write can do\n * to that shape, and it always removes a line, so the two multisets can never\n * coincide.\n */\nexport function checkoutFieldWriteRepriced(\n before: readonly SdkCartItem[],\n after: readonly SdkCartItem[],\n): boolean {\n const shape = (lines: readonly SdkCartItem[]) => lines\n .map((line) => `${line.product_id} ${line.variant_id} ${line.quantity}`)\n .sort()\n .join('');\n return shape(before) !== shape(after);\n}\n","/**\n * Reading THE CART BOUNDS CONTRACT off a settled quote.\n *\n * PAYMENT PATH. Its own module, free of React and of the SDK singleton, so the\n * rules can be read and exercised on their own: the cart provider hands them the\n * quote's `lines` and the cart it holds, and they answer what has to change.\n *\n * The producer is the backend's `CartQuoteService` (`CartQuoteLine`), and its\n * field rules — which this module exists to honour exactly — are:\n *\n * `variant_id` the RESOLVED option id, or `null` for a product with no\n * variants, which the cart stores under the `'default'` marker.\n * The match has to bridge the two spellings.\n * `min_quantity` OMITTED when it is 1.\n * `max_quantity` OMITTED when nothing bounds the line. `-1` is NEVER emitted;\n * omission IS the unbounded sentinel, and a missing key must\n * never be read as 0 — that says \"sold out\". SHARED when\n * several cart lines name one product+option: they draw from\n * one shelf and the checkout sums them, so the value is that\n * line's SHARE of the option's ceiling.\n * `sellable` false = this line cannot be ordered as the cart stands:\n * either no quantity of the option can be ordered at all, or\n * the cart's lines of this option together ask for more units\n * than the option's ceiling, which the checkout refuses on the\n * SUM. The line is still priced and still counted — it is one\n * the buyer must reduce or remove. It answers for the OPTION\n * and for the whole cart's demand on it, so every line of one\n * option carries the same value.\n *\n * CONSUMER DUTY, from the same contract, and the reason this module exists in\n * the Advanced lane at all: cart lines hold an ADD-TIME SNAPSHOT of their\n * bounds, so a line goes stale the moment stock moves or the merchant edits the\n * limits — and the stepper is driven off those stored values. Heal them by\n * REPLACING, never merging: an absent `max_quantity` means the ceiling was\n * REMOVED, and a merge would keep a stale one forever. And refuse the checkout\n * hand-off while any line is unsellable, because that cart is exactly the one\n * `/from-cart` rejects.\n */\n\nimport type { SdkCartItem, SdkCartQuoteLine } from './sdk';\n\nexport type CartQuoteLineBounds = {\n productId: string;\n /** `'default'` for the contract's `null`, so it compares against a cart line. */\n variantId: string;\n /** The quantity this row was quoted AT, after per-line summing. */\n quantity: number;\n /** `null` = unstated, which is the SDK's own default of 1. */\n minQuantity: number | null;\n /** `null` = unbounded. Never a negative sentinel. */\n maxQuantity: number | null;\n sellable: boolean;\n};\n\n/** One healing write: the line to patch, and the bounds to REPLACE it with. */\nexport type CartBoundsHeal = {\n lineId: string;\n /**\n * `null` DELETES the stored bound — the contract's \"no bound\" — in the SDK's\n * own tri-state (absent = leave alone, null = clear, number = set). It is not\n * `undefined`, because `undefined` is exactly what the SDK reads as \"this\n * update did not mention the field\", so a removed ceiling would stick to the\n * line forever.\n */\n minQuantity: number | null;\n maxQuantity: number | null;\n};\n\n/**\n * The wire omits these rather than nulling them, so anything non-numeric is\n * \"unstated\". A negative value is refused outright: `-1` is not emitted by this\n * contract, and treating one as a ceiling would pin the line to a single unit.\n */\nfunction readOptionalCount(value: unknown): number | null {\n if (typeof value !== 'number' || !Number.isFinite(value) || value < 0) return null;\n return Math.floor(value);\n}\n\n/** Parse `lines` off a settled quote. An absent or malformed list yields none. */\nexport function parseCartQuoteLines(raw: SdkCartQuoteLine[] | undefined): CartQuoteLineBounds[] {\n if (!Array.isArray(raw)) return [];\n const parsed: CartQuoteLineBounds[] = [];\n for (const entry of raw) {\n const row = (entry && typeof entry === 'object' ? entry : {}) as Record<string, unknown>;\n const productId = typeof row.product_id === 'string' ? row.product_id : '';\n if (!productId) continue;\n parsed.push({\n productId,\n variantId: typeof row.variant_id === 'string' && row.variant_id ? row.variant_id : 'default',\n quantity: typeof row.quantity === 'number' && Number.isFinite(row.quantity) ? row.quantity : 0,\n minQuantity: readOptionalCount(row.min_quantity),\n maxQuantity: readOptionalCount(row.max_quantity),\n // Fail closed on a malformed row: only an explicit `true` is sellable. A\n // row this runtime could not read must not re-enable a refused line.\n sellable: row.sellable === true,\n });\n }\n return parsed;\n}\n\n/**\n * The OPTION this quoted row answers for.\n *\n * Deliberately quantity-blind, because `sellable` is: the producer answers it\n * for the OPTION and for the whole cart's demand on it, so every line of one\n * option carries the same verdict. A refusal does not stop being true because\n * the buyer changed how many they wanted, and dropping it on a quantity\n * mismatch would arm a checkout the server refuses.\n */\nfunction matchesOption(line: SdkCartItem, quoted: CartQuoteLineBounds): boolean {\n const variantId = line.variant_id ? line.variant_id : 'default';\n return line.product_id === quoted.productId && variantId === quoted.variantId;\n}\n\n/**\n * The exact LINE this quoted row priced — option AND the quantity it was quoted\n * at, which the producer states explicitly.\n *\n * PAYMENT PATH. Bounds are resolved against the requested quantity, so a quote\n * that answered for 2 units is not an answer about the same line now holding 9.\n * Matching on the option alone would heal a since-changed line from a quote that\n * never saw it. A mismatch skips the heal; the next settled quote answers for\n * what the cart actually holds.\n */\nfunction matchesQuotedLine(line: SdkCartItem, quoted: CartQuoteLineBounds): boolean {\n if (!matchesOption(line, quoted)) return false;\n const quantity = Number.isFinite(line.quantity) ? Math.floor(line.quantity) : null;\n return quantity !== null && quantity === quoted.quantity;\n}\n\n/**\n * The writes needed to bring stored line bounds back in line with the quote.\n *\n * Only lines whose stored bounds actually DIFFER are returned, which is what\n * keeps this from looping: writing the same values back would emit another\n * cart-changed, re-quote, and arrive here again. Cart lines the quote did not\n * mention are left untouched — the quote answers for the cart it was given, and\n * a line added after it was sent has no answer in it yet.\n */\nexport function resolveCartBoundsHeals(\n lines: readonly SdkCartItem[],\n quoted: readonly CartQuoteLineBounds[],\n): CartBoundsHeal[] {\n const heals: CartBoundsHeal[] = [];\n for (const line of lines) {\n const lineId = typeof line.line_id === 'string' ? line.line_id : '';\n if (!lineId) continue;\n const match = quoted.find((entry) => matchesQuotedLine(line, entry));\n if (!match) continue;\n // The SDK's own default is 1 and it is stored as \"unset\", so a minimum of 1\n // is expressed by clearing the field rather than by writing the number.\n const nextMin = match.minQuantity !== null && match.minQuantity > 1 ? match.minQuantity : null;\n const nextMax = match.maxQuantity;\n const storedMin = readOptionalCount(line.min_quantity);\n const storedMax = readOptionalCount(line.max_quantity);\n const currentMin = storedMin !== null && storedMin > 1 ? storedMin : null;\n if (currentMin === nextMin && storedMax === nextMax) continue;\n heals.push({ lineId, minQuantity: nextMin, maxQuantity: nextMax });\n }\n return heals;\n}\n\n/** The stored `line_id`s the quote says cannot be ordered right now. */\nexport function resolveUnsellableLineIds(\n lines: readonly SdkCartItem[],\n quoted: readonly CartQuoteLineBounds[],\n): string[] {\n const ids: string[] = [];\n for (const line of lines) {\n const lineId = typeof line.line_id === 'string' ? line.line_id : '';\n if (!lineId) continue;\n const match = quoted.find((entry) => matchesOption(line, entry));\n // No answer is not a refusal: a line the quote never mentioned is left alone\n // rather than being flagged on this runtime's guess.\n if (match && !match.sellable) ids.push(lineId);\n }\n return ids;\n}\n","export const QUOTE_UNAVAILABLE_CHECKOUT_ERROR =\n 'Cart pricing is unavailable, so this order cannot be priced yet. Try again in a moment.';\n\n/** Retires only the checkout refusal that a newly settled quote resolved. */\nexport function clearRecoveredQuoteCheckoutError(error: string | null): string | null {\n return error === QUOTE_UNAVAILABLE_CHECKOUT_ERROR ? null : error;\n}\n","/**\n * Merchant-authored HTML, made safe to inject.\n *\n * The storefront payload carries product descriptions and content tabs exactly\n * as the merchant stored them; nothing sanitizes them on the way out. The Easy\n * lane sanitizes at RENDER time (`sanitizeStorefrontRichTextHtml`), so a code\n * storefront that skipped this step would make the same shop's content safe on\n * one lane and executable on the other.\n *\n * \"The merchant wrote it, on their own storefront\" is not a safety argument:\n * imported shops carry HTML nobody at the shop authored, and dashboard access is\n * not always the owner's. The allowlist mirrors the Easy lane's — anything\n * outside it is DROPPED rather than escaped, so a stray `onerror` disappears\n * instead of showing up as text.\n */\n\nimport DOMPurify from 'dompurify';\n\nconst HEX_COLOR = /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;\nconst TEXT_ALIGN = /^(left|right|center|justify)$/;\n/**\n * Images resolve against the shop's own CDN, the same rule the Easy lane\n * applies. An arbitrary remote `src` is a beacon: it reports every buyer who\n * opens the page to whoever chose the URL, and it survives sanitization of the\n * event handlers.\n */\nconst CDN_IMAGE_SRC = /^https:\\/\\/imagedelivery\\.net\\/[^/]+\\/[^/]+\\/[^/?#]+(?:\\?.*)?$/i;\n\n/** Only colour and alignment survive; everything else in `style` is dropped. */\nfunction filterStyle(style: string): string {\n const allowed: string[] = [];\n for (const declaration of style.split(';')) {\n const [rawProperty, rawValue] = declaration.split(':');\n if (!rawProperty || !rawValue) continue;\n const property = rawProperty.trim().toLowerCase();\n const value = rawValue.trim();\n\n if ((property === 'color' || property === 'background-color') && HEX_COLOR.test(value)) {\n allowed.push(`${property}: ${value}`);\n continue;\n }\n if (property === 'text-align' && TEXT_ALIGN.test(value.toLowerCase())) {\n allowed.push(`${property}: ${value.toLowerCase()}`);\n }\n }\n return allowed.join('; ');\n}\n\nconst ALLOWED_TAGS = [\n 'h1', 'h2', 'h3', 'p', 'br', 'hr',\n 'ul', 'ol', 'li',\n 'details', 'summary',\n 'strong', 'em', 'u', 's', 'del',\n 'code', 'pre', 'blockquote',\n 'a', 'mark', 'span', 'img',\n 'table', 'thead', 'tbody', 'tr', 'th', 'td',\n];\n\nconst ALLOWED_ATTR = [\n 'href', 'target', 'rel', 'class', 'style',\n 'data-color', 'data-type',\n 'colspan', 'rowspan',\n 'src', 'alt', 'title', 'width', 'height', 'loading',\n 'open',\n];\n\nexport function sanitizeMerchantHtml(value: unknown): string {\n if (typeof value !== 'string' || !value) return '';\n\n const hook = (\n node: unknown,\n data: { attrName?: string; attrValue?: string; keepAttr?: boolean },\n ) => {\n if (data.attrName === 'data-color') {\n if (typeof data.attrValue !== 'string' || !HEX_COLOR.test(data.attrValue)) {\n data.keepAttr = false;\n }\n return;\n }\n if (data.attrName === 'src') {\n const isImage = typeof node === 'object'\n && node !== null\n && 'tagName' in node\n && String((node as { tagName?: unknown }).tagName).toUpperCase() === 'IMG';\n if (isImage && (typeof data.attrValue !== 'string' || !CDN_IMAGE_SRC.test(data.attrValue))) {\n data.keepAttr = false;\n }\n return;\n }\n if (data.attrName !== 'style' || typeof data.attrValue !== 'string') return;\n\n const filtered = filterStyle(data.attrValue);\n if (!filtered) {\n data.keepAttr = false;\n return;\n }\n data.attrValue = filtered;\n };\n\n DOMPurify.addHook('uponSanitizeAttribute', hook);\n try {\n return DOMPurify.sanitize(value, { ALLOWED_TAGS, ALLOWED_ATTR });\n } finally {\n DOMPurify.removeHook('uponSanitizeAttribute', hook);\n }\n}\n\n/**\n * Elements that end a line of prose. Their boundary contributes a SPACE when\n * the markup is flattened, so \"<p>High speed</p><p>24/7 support</p>\" reads\n * \"High speed 24/7 support\" and not \"High speed24/7 support\" — the exact\n * artefact a bare `textContent` produced on catalog cards and in the search\n * haystack, where it also glued two words into one unsearchable token.\n */\nconst BLOCK_BOUNDARY_SELECTOR = 'address,article,aside,blockquote,br,dd,div,dl,dt,'\n + 'figcaption,figure,footer,h1,h2,h3,h4,h5,h6,header,hr,li,main,nav,ol,p,pre,'\n + 'section,table,tbody,td,tfoot,th,thead,tr,ul';\n\n/**\n * Merchant HTML reduced to plain text, safely.\n *\n * Product descriptions arrive as rich text and several surfaces render them as\n * TEXT — a catalog card's two-line summary, the search haystack. Passing the\n * raw string there printed the merchant's own `<p>` tags at the buyer, and\n * passing `textContent` of the parsed markup ran the block boundaries together.\n *\n * So the value is sanitized first (same allowlist as everything else — the\n * string is never parsed as live markup before DOMPurify has seen it) and then\n * flattened with block boundaries counting as whitespace. The result is\n * collapsed to single spaces because CSS line-clamping cannot do it for text.\n */\nexport function flattenMerchantHtmlToText(value: unknown): string {\n const sanitized = sanitizeMerchantHtml(value);\n if (!sanitized) return '';\n const container = document.createElement('div');\n container.innerHTML = sanitized;\n for (const element of container.querySelectorAll(BLOCK_BOUNDARY_SELECTOR)) {\n element.insertAdjacentText('beforebegin', ' ');\n element.insertAdjacentText('afterend', ' ');\n }\n return (container.textContent ?? '').replace(/\\s+/g, ' ').trim();\n}\n\n/**\n * True when the sanitized markup carries no renderable content.\n *\n * A description of `<p></p>` is not a description: the Easy lane's\n * `hasDescription` gate tests the SOURCE text, so a merchant who cleared their\n * editor without clearing its wrapper tags gets the empty state rather than a\n * blank panel.\n */\n/**\n * Drops every picture from merchant HTML.\n *\n * The merchant's \"show images in descriptions\" switch, applied at the same\n * place the document lane applies it (`stripDescriptionImages`): the shop asked\n * for text, so the description and every content tab arrive without their\n * `<img>` elements. Run BEFORE the sanitizer's output is injected, on the\n * sanitized string, so a tag the allowlist would have dropped anyway cannot\n * survive by looking like an image to this pass.\n */\nexport function stripMerchantHtmlImages(html: string): string {\n return html.replace(/<img\\b[^>]*>/gi, '').trim();\n}\n\nexport function isBlankMerchantHtml(html: string): boolean {\n if (!html) return true;\n const container = document.createElement('div');\n container.innerHTML = html;\n return (container.textContent ?? '').trim().length === 0\n && container.querySelector('img, hr, table') === null;\n}\n","/**\n * Catalog view models: the step between the injected wire payload and the\n * cards.\n *\n * Everything a card shows — the price label, the badge, the stock chip, the\n * buy control it gets — is decided here once, so the card components stay\n * markup. Nothing in this module invents a value: a product without a rating\n * has no rating chip, a product without a description has no description line.\n */\n\nimport { flattenMerchantHtmlToText } from './sanitize-html';\nimport { badgesForCard, type ProductBadge } from './product-badges';\nimport {\n getProductGroups,\n getProducts,\n getStorefrontCategories,\n getStorefrontListing,\n isProductSoldOut,\n isSelectionSoldOut,\n resolveDisplayStock,\n resolveHideStockCounter,\n STOREFRONT_SORT_KEYS,\n type StorefrontCategory,\n type StorefrontGroup,\n type StorefrontProduct,\n type StorefrontSettings,\n type StorefrontSortKey,\n} from './storefront-data';\n\n/**\n * Money as the storefront shows it: `$49`, `$49.50`, `€9.99`, `¥1,200`.\n *\n * A whole amount drops its zero cents (`$49`, not `$49.00`); any other amount\n * keeps the currency's own number of decimals, so `$49.50` never turns into\n * `$49.5` and a three-decimal currency keeps all three. \"Whole\" is judged on\n * the amount rounded to those decimals, so float noise like `48.99999999`\n * still reads as `$49`.\n */\nexport function formatMoney(amount: number, currency: string): string {\n const code = currency && currency !== 'AUTO' ? currency : 'USD';\n let format: Intl.NumberFormat;\n try {\n format = new Intl.NumberFormat('en-US', { style: 'currency', currency: code });\n } catch {\n // An unknown ISO code is merchant data, not a bug worth blanking a price\n // over — show the amount with the code in front of it.\n const fixed = amount.toFixed(2);\n return `${code} ${fixed.endsWith('.00') ? fixed.slice(0, -3) : fixed}`;\n }\n const decimals = format.resolvedOptions().maximumFractionDigits ?? 2;\n if (decimals === 0 || !Number.isInteger(Number(amount.toFixed(decimals)))) return format.format(amount);\n return new Intl.NumberFormat('en-US', {\n style: 'currency',\n currency: code,\n minimumFractionDigits: 0,\n maximumFractionDigits: 0,\n }).format(Math.round(amount));\n}\n\n/** Compact counts for the store-stats strip: 12450 → `12k`, 8200 → `8.2k`. */\nexport function formatCompactCount(value: number): string {\n if (value >= 1_000_000) return `${(value / 1_000_000).toFixed(1)}M`;\n if (value >= 1_000) return `${(value / 1_000).toFixed(value >= 10_000 ? 0 : 1)}k`;\n return value.toLocaleString('en-US');\n}\n\nexport type StockTone = 'in_stock' | 'low_stock' | 'sold_out';\n\nexport type StockChip = {\n tone: StockTone;\n label: string;\n};\n\n/**\n * The units at or below which stock reads as LOW when the merchant configured\n * no threshold of their own. Ten is what every Easy-lane stock surface hardcodes\n * (`resolveCatalogStockPresentation`, `resolveStockTone`), so a merchant who\n * never set \"Low Stock Warning\" sees the same wording on both lanes.\n */\nexport const DEFAULT_LOW_STOCK_THRESHOLD = 10;\n\n/**\n * The stock chip on a catalog card. A merchant who hides the stock counter\n * gets the generic label — but a hidden count must never read as unlimited, so\n * only a genuinely unbounded product loses the number for free.\n *\n * The low-stock point is the merchant's own `quantity_warning` when they set\n * one. It is a per-product number precisely because \"running low\" means five\n * for a licence pool and five hundred for a key blob; ignoring it, as this card\n * did, called a 400-unit product low and a 12-unit one plentiful.\n */\nfunction resolveStockChip(\n stock: number | null,\n hideCounter: boolean,\n lowStockThreshold: number | null,\n): StockChip {\n if (stock === 0) return { tone: 'sold_out', label: 'Out of Stock' };\n if (hideCounter || stock === null) return { tone: 'in_stock', label: 'In Stock' };\n const threshold = lowStockThreshold ?? DEFAULT_LOW_STOCK_THRESHOLD;\n return { tone: stock <= threshold ? 'low_stock' : 'in_stock', label: `${stock} In Stock` };\n}\n\n/**\n * The card badge. `Options` and `Sub` are the clean-minimal wordings — the\n * card is 217px wide at four columns, and \"Subscription\" does not fit.\n */\nfunction resolveBadgeLabel(product: StorefrontProduct): string | null {\n if (product.productType === 'INFO_CARD') return 'Info';\n if (product.productType === 'SUBSCRIPTION') return 'Sub';\n if (product.priceVariants.length > 1) return 'Options';\n return null;\n}\n\n/**\n * The catalog price. A variant-led product (no price on the product row, two\n * or more differently-priced options) advertises its RANGE; everything else\n * shows its single starting price.\n */\nfunction resolvePriceLabel(product: StorefrontProduct): string {\n const variantPrices = product.priceVariants.map((variant) => variant.price);\n const positive = variantPrices.filter((price) => price > 0);\n const candidates = positive.length > 0 ? positive : variantPrices;\n if (product.basePrice <= 0 && candidates.length >= 2) {\n const min = Math.min(...candidates);\n const max = Math.max(...candidates);\n if (max > min) {\n return `${formatMoney(min, product.currency)} - ${formatMoney(max, product.currency)}`;\n }\n }\n return formatMoney(product.price, product.currency);\n}\n\nexport interface ProductCardModel {\n kind: 'product';\n key: string;\n product: StorefrontProduct;\n href: string;\n priceLabel: string;\n badgeLabel: string | null;\n /**\n * The merchant's own badges for this surface, already filtered by\n * `show_on_product_card`. Separate from `badgeLabel`, which is the theme's\n * product-TYPE chip: one is authored, the other is derived, and they occupy\n * different corners of the media so neither can hide the other.\n */\n badges: ProductBadge[];\n /** Up to three merchant bullets; the description line renders only when empty. */\n highlights: string[];\n description: string | null;\n rating: number | null;\n /**\n * Absent for info cards, sold-out products and paused ones — each has its own\n * chip or its own action row instead.\n */\n stockChip: StockChip | null;\n soldOut: boolean;\n /**\n * The merchant PAUSED this product. Not a stock state: it outranks stock\n * entirely, because the backend refuses to sell it however many units exist.\n */\n onHold: boolean;\n categoryIds: string[];\n sortPriority: number;\n createdAt: string | null;\n price: number;\n}\n\nexport interface GroupCardModel {\n kind: 'group';\n key: string;\n group: StorefrontGroup;\n priceLabel: string;\n /** Resolvable members, in the merchant's order. */\n members: StorefrontProduct[];\n memberCountLabel: string;\n categoryIds: string[];\n sortPriority: number;\n price: number;\n}\n\nexport type CatalogCardModel = ProductCardModel | GroupCardModel;\n\n/**\n * A card is sold out when nothing on it can be ordered: no stock at all, or no\n * option whose effective quantity range is non-empty (`quantity_min` 2 against\n * one unit left). PAYMENT PATH: the card's quick add goes through `add()`,\n * which refuses exactly these selections, so an active button here would be a\n * control that silently does nothing.\n */\nexport function isCardSoldOut(product: StorefrontProduct): boolean {\n if (isProductSoldOut(product)) return true;\n return product.priceVariants.length > 0\n ? product.priceVariants.every((variant) => isSelectionSoldOut(product, variant))\n : isSelectionSoldOut(product, null);\n}\n\nfunction buildProductCard(\n product: StorefrontProduct,\n settings: StorefrontSettings,\n sortPriority: number,\n): ProductCardModel {\n const soldOut = isCardSoldOut(product);\n const isInfoCard = product.productType === 'INFO_CARD';\n const stock = resolveDisplayStock(product);\n const hideCounter = resolveHideStockCounter(product, settings);\n return {\n kind: 'product',\n key: product.id,\n product,\n href: `/product/${product.slug ?? product.id}`,\n priceLabel: resolvePriceLabel(product),\n badgeLabel: resolveBadgeLabel(product),\n badges: badgesForCard(product.badges),\n highlights: product.highlights.slice(0, 3),\n // Flattened, not raw: the description is rich text and the card renders it\n // as TEXT, so the merchant's own markup used to print as literal tags. See\n // `flattenMerchantHtmlToText` for why plain `textContent` is not enough.\n description: product.highlights.length > 0\n ? null\n : flattenMerchantHtmlToText(product.description) || null,\n rating: product.rating,\n // A paused product's stock is beside the point — announcing \"999 IN STOCK\"\n // over a card that cannot be bought states the opposite of what the action\n // row says. Checked before stock for the same reason the document lane's\n // `isProductOutOfStock` checks it first.\n stockChip: isInfoCard || soldOut || product.onHold\n ? null\n : resolveStockChip(stock, hideCounter, product.lowStockThreshold),\n soldOut,\n onHold: product.onHold,\n categoryIds: product.categories.map((category) => category.id),\n sortPriority,\n createdAt: product.createdAt,\n price: product.price,\n };\n}\n\n/**\n * A bundle's price span across its members. The bundle discount is applied by\n * the backend at quote time, so the card advertises the member prices and\n * never a total it computed itself.\n */\nfunction buildGroupCard(\n group: StorefrontGroup,\n byId: Map<string, StorefrontProduct>,\n sortPriority: number,\n): GroupCardModel {\n const members = group.productIds\n .map((id) => byId.get(id))\n .filter((product): product is StorefrontProduct => product !== undefined);\n const prices = members.map((product) => product.price);\n const currency = members[0]?.currency ?? 'USD';\n const min = prices.length > 0 ? Math.min(...prices) : 0;\n const max = prices.length > 0 ? Math.max(...prices) : 0;\n const memberCount = Math.max(group.productsCount, members.length);\n return {\n kind: 'group',\n key: group.id,\n group,\n priceLabel: prices.length === 0\n ? ''\n : min === max\n ? formatMoney(min, currency)\n : `${formatMoney(min, currency)} - ${formatMoney(max, currency)}`,\n members,\n memberCountLabel: `${memberCount} ${memberCount === 1 ? 'product' : 'products'}`,\n categoryIds: [\n ...new Set([\n ...(group.categoryId ? [group.categoryId] : []),\n ...members.flatMap((product) => product.categories.map((category) => category.id)),\n ]),\n ],\n sortPriority,\n price: min,\n };\n}\n\nexport interface StorefrontCatalog {\n cards: CatalogCardModel[];\n categories: StorefrontCategory[];\n /**\n * Every product the payload carries, including the ones the shelf does not\n * list. A bundle member has a real product page even though it has no card,\n * so product-page lookups read this and not `cards`.\n */\n products: StorefrontProduct[];\n /** Products the storefront lists — the store-stats \"products\" cell counts these. */\n listedProductCount: number;\n}\n\n/**\n * The product a `/product/:slug` route points at.\n *\n * The id is accepted alongside the slug because a product without a slug is\n * linked by uniqid, and a bookmarked uniqid must keep working after the\n * merchant adds one.\n */\nexport function findStorefrontProduct(\n catalog: StorefrontCatalog,\n slugOrId: string,\n): StorefrontProduct | null {\n return catalog.products.find((product) => product.slug === slugOrId)\n ?? catalog.products.find((product) => product.id === slugOrId)\n ?? null;\n}\n\n/**\n * The storefront's catalog, in the merchant's shelf order.\n *\n * `items` decides what is on the shelf. Reading `products` instead would put\n * every bundle member on the home page as a standalone card — they are real\n * catalog products, they are simply not listed on their own.\n */\nexport function buildStorefrontCatalog(settings: StorefrontSettings): StorefrontCatalog {\n const products = getProducts();\n const groups = getProductGroups();\n const listing = getStorefrontListing();\n const productsById = new Map(products.map((product) => [product.id, product]));\n const groupsById = new Map(groups.map((group) => [group.id, group]));\n\n const cards = listing\n .map((item, index): CatalogCardModel | null => {\n if (item.type === 'group') {\n const group = groupsById.get(item.id);\n return group ? buildGroupCard(group, productsById, item.sortPriority ?? index) : null;\n }\n const product = productsById.get(item.id);\n if (!product) return null;\n if (settings.hideOutOfStock && isProductSoldOut(product)) return null;\n return buildProductCard(product, settings, item.sortPriority ?? index);\n })\n .filter((card): card is CatalogCardModel => card !== null);\n\n return {\n cards,\n categories: getStorefrontCategories(),\n products,\n listedProductCount: products.length,\n };\n}\n\nexport const CATALOG_SORTS = STOREFRONT_SORT_KEYS;\n\nexport type CatalogSort = StorefrontSortKey;\n\nexport const CATALOG_SORT_LABELS: Record<CatalogSort, string> = {\n featured: 'Featured',\n newest: 'Newest',\n 'price-asc': 'Price: Low to High',\n 'price-desc': 'Price: High to Low',\n};\n\n/**\n * Sorting never mutates the shelf order: `featured` IS that order, and the\n * others sort a copy of it so switching back is exact.\n */\nexport function sortCatalogCards(cards: CatalogCardModel[], sort: CatalogSort): CatalogCardModel[] {\n if (sort === 'featured') return cards;\n const sorted = [...cards];\n if (sort === 'price-asc') return sorted.sort((a, b) => a.price - b.price);\n if (sort === 'price-desc') return sorted.sort((a, b) => b.price - a.price);\n // Newest: a product without a creation date keeps its shelf position rather\n // than jumping to either end of the list.\n return sorted.sort((a, b) => {\n const left = a.kind === 'product' && a.createdAt ? Date.parse(a.createdAt) : Number.NaN;\n const right = b.kind === 'product' && b.createdAt ? Date.parse(b.createdAt) : Number.NaN;\n if (Number.isNaN(left) && Number.isNaN(right)) return a.sortPriority - b.sortPriority;\n if (Number.isNaN(left)) return 1;\n if (Number.isNaN(right)) return -1;\n return right - left;\n });\n}\n\n/**\n * The category a `?category=` value names, as the CARD models key on it.\n *\n * A shared or bookmarked listing URL may carry the merchant's slug, the\n * category title, or the uniqid — the Easy lane accepts all of them\n * (`resolveAllProductsCategorySelection`) and answers with the uniqid the cards\n * are filtered by. Mirrored here so one storefront's filtered link keeps working\n * after a lane switch. An unknown value selects nothing rather than emptying the\n * grid against a category that does not exist.\n */\nexport function resolveCategorySelection(\n value: string | null,\n categories: readonly StorefrontCategory[],\n): string | null {\n const normalized = value?.trim().toLowerCase();\n if (!normalized) return null;\n const match = categories.find((category) =>\n category.id.toLowerCase() === normalized\n || category.slug.toLowerCase() === normalized\n || category.title.toLowerCase() === normalized);\n return match?.id ?? null;\n}\n\nexport function filterCatalogCards(\n cards: CatalogCardModel[],\n categoryId: string | null,\n): CatalogCardModel[] {\n if (!categoryId) return cards;\n return cards.filter((card) => card.categoryIds.includes(categoryId));\n}\n\n/** Products per page in the full listing. Bundles do not count against it. */\nexport const CATALOG_PAGE_SIZE = 24;\n\nexport interface CatalogPage {\n cards: CatalogCardModel[];\n /** The page actually shown — an out-of-range request lands on the last one. */\n page: number;\n totalPages: number;\n}\n\n/**\n * One page of the full listing.\n *\n * Bundles all sit on page one and never count against the page size: a shop\n * with three bundles and twenty-four products is one page, not two, because a\n * bundle is an entry point into the catalog rather than an item in it. The\n * input must already be filtered and sorted — paging re-orders nothing.\n */\nexport function paginateCatalogCards(cards: CatalogCardModel[], page: number): CatalogPage {\n const products = cards.filter((card) => card.kind === 'product');\n const firstPage: CatalogCardModel[] = [];\n let productsOnFirstPage = 0;\n for (const card of cards) {\n if (card.kind === 'group') {\n firstPage.push(card);\n continue;\n }\n if (productsOnFirstPage < CATALOG_PAGE_SIZE) {\n firstPage.push(card);\n productsOnFirstPage += 1;\n }\n }\n\n const remaining = products.length - productsOnFirstPage;\n const totalPages = remaining > 0 ? 1 + Math.ceil(remaining / CATALOG_PAGE_SIZE) : 1;\n const resolved = Math.min(Math.max(page, 1), totalPages);\n if (resolved === 1) return { cards: firstPage, page: 1, totalPages };\n\n const offset = productsOnFirstPage + (resolved - 2) * CATALOG_PAGE_SIZE;\n return {\n cards: products.slice(offset, offset + CATALOG_PAGE_SIZE),\n page: resolved,\n totalPages,\n };\n}\n","/**\n * Product-page view models: the step between one parsed product and the\n * gallery, buy box, details tabs and related strip.\n *\n * Same division of labour as `catalog.ts` — every decision a product page makes\n * about WHAT to show is made here, so the components stay markup. The two files\n * deliberately differ on stock wording: a catalog card shouts a short chip\n * (\"999 IN STOCK\"), the buy box states the option's availability in a sentence\n * (\"999 in stock\", \"Only 3 left\"). Same source, two audiences.\n */\n\nimport {\n calculateDisplayLineTotal,\n isSelectionSoldOut,\n resolveHideStockCounter,\n resolveQuantityBounds,\n resolveVariantStock,\n roundCurrency,\n type ProductAddon,\n type StorefrontPriceVariant,\n type StorefrontProduct,\n type StorefrontSettings,\n} from './storefront-data';\nimport {\n DEFAULT_LOW_STOCK_THRESHOLD,\n type CatalogCardModel,\n type ProductCardModel,\n type StockTone,\n} from './catalog';\n\n/** Maximum related products the strip advertises, matching the Easy lane. */\nconst RELATED_PRODUCT_LIMIT = 3;\n\n/** The five star slots under the product title; `true` is a filled star. */\nexport type RatingStars = readonly [boolean, boolean, boolean, boolean, boolean];\n\nexport interface BuyBoxVariantModel {\n variant: StorefrontPriceVariant;\n /** Unit price with the product discount applied — what one unit costs today. */\n price: number;\n /** Undiscounted unit price, shown struck through. null when nothing is off. */\n compareAtPrice: number | null;\n stockTone: StockTone;\n stockLabel: string;\n soldOut: boolean;\n}\n\nexport interface ProductRatingModel {\n stars: RatingStars;\n /** Reviews the tally counted — the label pluralises off this. */\n count: number;\n /** 1-5, rounded to one decimal. */\n average: number;\n /**\n * How the tally splits. The bootstrap payload carries these three buckets and\n * no review bodies, so this is the finest grain the code lane can honestly\n * report.\n */\n positive: number;\n neutral: number;\n negative: number;\n}\n\nexport interface ProductDetailModel {\n product: StorefrontProduct;\n /** Gallery images, primary first. Empty when the product has no art at all. */\n images: string[];\n /** Rating derived from real feedback; null when the product has none. */\n rating: ProductRatingModel | null;\n variants: BuyBoxVariantModel[];\n addons: ProductAddon[];\n soldOut: boolean;\n /** Cards for the \"You might also like\" strip, in the merchant's shelf order. */\n related: ProductCardModel[];\n}\n\n/**\n * The buy box's stock line for one option.\n *\n * A merchant who hides the stock counter still gets an availability statement —\n * hiding the NUMBER is not hiding whether the thing can be bought. Only a\n * genuinely unbounded option loses the number for free.\n */\nfunction resolveBuyBoxStock(\n stock: number | null,\n hideCounter: boolean,\n /** The product's own \"Low Stock Warning\"; null falls back to the shared default. */\n lowStockThreshold: number | null,\n): { tone: StockTone; label: string; soldOut: boolean } {\n if (stock === 0) return { tone: 'sold_out', label: 'Out of Stock', soldOut: true };\n if (stock === null || hideCounter) return { tone: 'in_stock', label: 'Available', soldOut: false };\n const threshold = lowStockThreshold ?? DEFAULT_LOW_STOCK_THRESHOLD;\n if (stock <= threshold) return { tone: 'low_stock', label: `Only ${stock} left`, soldOut: false };\n return { tone: 'in_stock', label: `${stock} in stock`, soldOut: false };\n}\n\nfunction buildVariants(\n product: StorefrontProduct,\n settings: StorefrontSettings,\n): BuyBoxVariantModel[] {\n const hideCounter = resolveHideStockCounter(product, settings);\n\n return product.priceVariants.map((variant) => {\n // Same predicate the cart's sold-out guard uses, so the button the buyer sees and the line the\n // cart accepts can never disagree — including the option whose counter is POSITIVE but whose\n // minimum order quantity exceeds it. That option is unsellable, so it is labelled and disabled\n // as sold out rather than advertising a number the buyer is not allowed to order.\n const stock = resolveVariantStock(product, variant);\n const { tone, label, soldOut } = isSelectionSoldOut(product, variant)\n ? { tone: 'sold_out' as StockTone, label: 'Out of Stock', soldOut: true }\n : resolveBuyBoxStock(stock, hideCounter, product.lowStockThreshold);\n return {\n variant,\n price: variant.price,\n compareAtPrice: variant.price < variant.baseUnitPrice ? variant.baseUnitPrice : null,\n stockTone: tone,\n stockLabel: label,\n soldOut,\n };\n });\n}\n\n/**\n * The star row and the tally behind it.\n *\n * Weighted the same way `deriveProductRating` weighs the catalog card, because\n * a product cannot carry one score on the shelf and another on its own page.\n */\nfunction buildRating(product: StorefrontProduct): ProductRatingModel | null {\n if (product.rating === null || product.ratingTally === null || product.ratingCount === 0) return null;\n const filled = Math.round(product.rating);\n // The split is the payload's own tally, never reconstructed from the average:\n // an average of 4.7 fits several splits, and printing one of them would state\n // how many buyers were unhappy without anyone having said so.\n return {\n stars: [1, 2, 3, 4, 5].map((slot) => slot <= filled) as unknown as RatingStars,\n count: product.ratingCount,\n average: product.rating,\n positive: product.ratingTally.positive,\n neutral: product.ratingTally.neutral,\n negative: product.ratingTally.negative,\n };\n}\n\n/**\n * Other products worth looking at.\n *\n * Taken from the SHELF, not from the raw product list: a bundle member is a\n * real product the merchant never put on display, and recommending it here\n * would route buyers around the bundle they were meant to see. Bundles\n * themselves are excluded — the strip is a row of product cards.\n */\nfunction buildRelated(cards: CatalogCardModel[], currentId: string): ProductCardModel[] {\n return cards\n .filter((card): card is ProductCardModel => card.kind === 'product' && card.key !== currentId)\n .slice(0, RELATED_PRODUCT_LIMIT);\n}\n\nexport function buildProductDetail(\n product: StorefrontProduct,\n settings: StorefrontSettings,\n cards: CatalogCardModel[],\n): ProductDetailModel {\n // `images` is the merchant's own gallery; a product that only ever had a\n // single primary picture still deserves a gallery frame, so the primary is\n // the fallback rather than an empty stage.\n //\n // Unless the merchant asked for no product picture at all: their\n // \"skip default image\" switch empties the list outright, exactly as the Easy\n // lane's gallery producer does, and the page renders its placeholder frame.\n const images = settings.descriptionSkipDefaultImage\n ? []\n : product.imageUrls.length > 0\n ? product.imageUrls\n : product.imageUrl\n ? [product.imageUrl]\n : [];\n\n return {\n product,\n images,\n rating: buildRating(product),\n variants: buildVariants(product, settings),\n addons: product.addons,\n // A variant-less product can be unsellable for the same reason an option can — see\n // `isSelectionSoldOut`, which folds the empty-range case into the product-level read.\n soldOut: isSelectionSoldOut(product, null),\n related: buildRelated(cards, product.id),\n };\n}\n\nexport interface LinePricing {\n /** What the cart will charge for the whole line. */\n total: number;\n /** The same line before any discount. Equal to `total` when none applies. */\n originalSubtotal: number;\n /** Per-unit price after discounts, for the \"… each\" line. */\n effectiveUnitPrice: number;\n discountAmount: number;\n hasDiscount: boolean;\n}\n\n/**\n * What the buy box headline shows.\n *\n * The unit price folds the selected variant AND the selected addons before the\n * discount stages run, exactly as the platform's product-form controller does —\n * a storefront that discounted the variant and then added the extras would\n * advertise a total the checkout does not honour.\n */\nexport function calculateLinePricing(\n product: StorefrontProduct,\n variant: StorefrontPriceVariant | null,\n addons: ProductAddon[],\n quantity: number,\n /**\n * The buyer's pay-what-you-want amount: PER UNIT, and for the product alone.\n * The extras are added on top of it here exactly once, which is the same\n * split `pay_what_you_want_price` + `price_data.unit_price` the cart sends\n * and the backend charges.\n */\n payWhatYouWantPrice?: number,\n): LinePricing {\n const addonsTotal = addons.reduce((sum, addon) => sum + addon.price, 0);\n if (payWhatYouWantPrice !== undefined) {\n // A named price is the price. The percent discount is already 0 on a PWYW\n // product (`parseProduct`), and the volume tiers are suppressed here for the\n // same reason: the backend validates the buyer's amount against the\n // UNDISCOUNTED minimum, so a storefront that discounted it would advertise a\n // total the checkout refuses.\n const effectiveUnitPrice = payWhatYouWantPrice + addonsTotal;\n const total = roundCurrency(effectiveUnitPrice * quantity);\n return {\n total,\n originalSubtotal: total,\n effectiveUnitPrice,\n discountAmount: 0,\n hasDiscount: false,\n };\n }\n const unitPrice = (variant?.baseUnitPrice ?? product.baseUnitPrice) + addonsTotal;\n const total = calculateDisplayLineTotal(\n unitPrice,\n quantity,\n product.discountPercent,\n product.volumeDiscounts,\n );\n const originalSubtotal = calculateDisplayLineTotal(unitPrice, quantity, 0, []);\n return {\n total,\n originalSubtotal,\n effectiveUnitPrice: quantity > 0 ? total / quantity : total,\n discountAmount: Math.max(0, originalSubtotal - total),\n hasDiscount: total < originalSubtotal,\n };\n}\n\n/**\n * The floor a pay-what-you-want buyer may name: PER UNIT, extras excluded.\n *\n * It is the selected option's UNDISCOUNTED price — the same number the backend\n * validates the buyer's amount against (`resolvePayWhatYouWantUnitPrice`) and\n * the same one the document lane offers as the default. Reading a discounted\n * price here would pre-fill an amount the checkout then refuses as too low.\n */\nexport function resolvePayWhatYouWantMinimum(\n product: StorefrontProduct,\n variant: StorefrontPriceVariant | null,\n): number {\n return variant?.baseUnitPrice ?? product.baseUnitPrice;\n}\n\n/**\n * The backend's ceiling for a named price (`MAX_PAY_WHAT_YOU_WANT_PRICE`).\n * An amount above it fails when the invoice is built, so the buy box refuses it\n * first instead of handing the buyer to a checkout that cannot be created.\n */\nexport const MAX_PAY_WHAT_YOU_WANT_PRICE = 1_000_000_000;\n\n/** Clamps a buyer-entered quantity into the bounds the line actually allows. */\nexport function clampQuantity(\n product: StorefrontProduct,\n variant: StorefrontPriceVariant | null,\n quantity: number,\n): number {\n const { min, max } = resolveQuantityBounds(product, variant);\n if (!Number.isFinite(quantity)) return min;\n const floored = Math.floor(quantity);\n if (floored < min) return min;\n return max !== null && floored > max ? max : floored;\n}\n","/**\n * View models for the cart drawer and the `/cart` page.\n *\n * The cart lives in the SDK as a flat list of lines plus a server quote. This\n * module turns those two into the rows a buyer reads, and it is the only place\n * that decides what a cart line SAYS.\n *\n * PRICING RULE, and it is the whole reason this file exists: a line total is\n * the stored price snapshot times the quantity, and nothing else. The totals\n * block is the SERVER's quote, verbatim — subtotal, discounts and total all\n * from the SAME quote, on both the drawer and `/cart`.\n *\n * The two are never mixed. The quote's subtotal is pre-discount while the line\n * snapshots already carry the product and volume discounts, so printing the\n * client's sum above the quote's discount rows produces a summary whose\n * arithmetic does not work (Subtotal 9, Discount −1, Total 9) — which is\n * exactly what the drawer used to show. `CartView.subtotal` is therefore the\n * client's own number for ONE purpose: standing in for the whole block while no\n * quote exists, where every row is the client's and the block still adds up.\n *\n * When a line has no snapshot its total is zero and the whole cart is flagged\n * as an estimate rather than presenting a confident wrong number.\n *\n * Cents are settled by the SHARED `roundCurrency`, the same half-up-on-the-\n * decimal-text rule the buy box and the line-pricing maths use. This file used\n * to carry a naive `Math.round(v * 100) / 100` of its own, which is a different\n * function: binary noise puts 10.03 × 50% at 5.014999…, which that version\n * floors to 5.01 while the quote's ROUND_HALF_UP gives 5.02. One rounding rule,\n * or two surfaces disagree by a cent about the same cart.\n */\n\nimport type { SdkCartItem, SdkCartQuote } from './sdk';\nimport { roundCurrency } from './storefront-data';\n\nexport interface CartViewLine {\n lineId: string;\n title: string;\n /** The option, and any extras the buyer ticked, as one caption. Blank hides it. */\n subtitle: string;\n imageUrl: string | null;\n quantity: number;\n lineTotal: number;\n /** False at the line's floor — the stepper removes the line instead. */\n canDecrease: boolean;\n /** False at the stock or per-order ceiling recorded when the line was added. */\n canIncrease: boolean;\n /** The quantity the stepper drops to; below it the line is removed. */\n minimumQuantity: number;\n}\n\nexport interface CartView {\n lines: CartViewLine[];\n /** Sum of the line totals — the client's own number, not the quote's. */\n subtotal: number;\n /**\n * True when a line carries no price snapshot, so `subtotal` understates the\n * cart. The summary says \"estimated\" instead of pretending otherwise.\n */\n subtotalIsEstimate: boolean;\n isEmpty: boolean;\n}\n\nfunction positiveInteger(value: unknown, fallback: number): number {\n return typeof value === 'number' && Number.isFinite(value)\n ? Math.max(1, Math.trunc(value))\n : fallback;\n}\n\nfunction snapshotUnitPrice(line: SdkCartItem): number | null {\n const price = line.price_data?.unit_price;\n return typeof price === 'number' && Number.isFinite(price) ? Math.max(price, 0) : null;\n}\n\nfunction lineSubtitle(line: SdkCartItem): string {\n const parts: string[] = [];\n const variantTitle = line.variant_title?.trim();\n if (variantTitle) parts.push(variantTitle);\n const addons = (line.addon_labels ?? []).map((label) => label.trim()).filter(Boolean);\n if (addons.length > 0) parts.push(addons.join(', '));\n return parts.join(' · ');\n}\n\nexport function buildCartView(lines: SdkCartItem[]): CartView {\n const rows = lines.map((line): CartViewLine => {\n const quantity = positiveInteger(line.quantity, 1);\n const unitPrice = snapshotUnitPrice(line);\n const minimumQuantity = positiveInteger(line.min_quantity, 1);\n const maximumQuantity = typeof line.max_quantity === 'number'\n && Number.isFinite(line.max_quantity)\n ? Math.max(1, Math.trunc(line.max_quantity))\n : Number.POSITIVE_INFINITY;\n return {\n lineId: line.line_id,\n // A line without a display snapshot still has to name itself; the\n // product id is the only truthful stand-in the cart holds.\n title: line.title?.trim() || line.product_id,\n subtitle: lineSubtitle(line),\n imageUrl: line.image_url?.trim() || null,\n quantity,\n lineTotal: roundCurrency((unitPrice ?? 0) * quantity),\n canDecrease: quantity > minimumQuantity,\n canIncrease: quantity < maximumQuantity,\n minimumQuantity,\n };\n });\n\n return {\n lines: rows,\n subtotal: roundCurrency(rows.reduce((sum, row) => sum + row.lineTotal, 0)),\n subtotalIsEstimate: lines.length > 0 && lines.some((line) => snapshotUnitPrice(line) === null),\n isEmpty: rows.length === 0,\n };\n}\n\nexport interface CartTotals {\n /**\n * The subtotal to print. From the quote when one exists (pre-discount, the\n * same basis its discount rows are stated against), otherwise the client's\n * own sum.\n */\n subtotal: number;\n /** Itemised discounts, exactly as the quote named them. */\n discounts: Array<{ label: string; amount: string }>;\n /**\n * Aggregate discount, shown only when the quote reports a reduction it did\n * not itemise. Never rendered alongside `discounts`.\n */\n aggregateDiscount: number | null;\n total: number;\n currency: string;\n /**\n * True while the printed numbers are not a server quote — a quote in flight,\n * a quote that failed, or a line without a price snapshot.\n */\n isEstimate: boolean;\n}\n\nfunction parseAmount(value: string): number {\n const amount = Number(value);\n return Number.isFinite(amount) ? amount : 0;\n}\n\nexport function buildCartTotals(\n view: CartView,\n quote: SdkCartQuote | null,\n fallbackCurrency: string,\n): CartTotals {\n if (!quote) {\n return {\n subtotal: view.subtotal,\n discounts: [],\n aggregateDiscount: null,\n total: view.subtotal,\n currency: fallbackCurrency,\n isEstimate: true,\n };\n }\n\n const discount = parseAmount(quote.discount);\n const itemised = quote.applied_discounts ?? [];\n return {\n subtotal: parseAmount(quote.subtotal),\n discounts: itemised.map((entry) => ({ label: entry.label, amount: entry.amount })),\n // The quote states a reduction it did not break down: show it as one row\n // rather than letting the arithmetic of the block silently not add up.\n aggregateDiscount: itemised.length === 0 && discount > 0 ? discount : null,\n total: parseAmount(quote.total),\n currency: quote.currency || fallbackCurrency,\n isEstimate: view.subtotalIsEstimate,\n };\n}\n","/**\n * The contact form's submit path and the community channels beside it.\n *\n * The Storefront SDK has no contact method, so this posts the storefront\n * ticket endpoint directly — the same request, body and validation the\n * document lane's `sx-contact-form` runs through `submitStorefrontContactTicket`\n * (`packages/sdk/src/utils/storefront-contact.ts`). Keeping the payload shape\n * identical is what makes a message sent from a code storefront indistinguishable\n * from one sent from the Builder.\n */\n\nimport type { SocialLink } from './storefront-data';\n\n/** The message body cap the platform applies before the ticket is stored. */\nconst MESSAGE_MAX_LENGTH = 2000;\n\n/** A subject shorter than this is not a subject; the API titles it generically. */\nconst MIN_SUBJECT_LENGTH = 2;\nconst MAX_TITLE_LENGTH = 30;\nconst DEFAULT_TICKET_TITLE = 'Contact Request';\n\nexport interface ContactFormValues {\n name: string;\n email: string;\n subject: string;\n message: string;\n invoiceId: string;\n}\n\nexport interface ContactSubmissionContext {\n contactEnabled: boolean;\n requireInvoice: boolean;\n}\n\n/**\n * Client-side validation, mirroring `validateStorefrontContactSubmission`.\n *\n * It is a courtesy, not a gate: the backend enforces the same rules, and a\n * form that only *looked* validated would let a disabled shop's visitor watch\n * a request fail with no explanation.\n */\nexport function validateContactSubmission(\n values: ContactFormValues,\n context: ContactSubmissionContext,\n): string | null {\n if (!context.contactEnabled) return 'The contact form is disabled for this store.';\n // The name is marked required in the markup, and the form suppresses native\n // validation so it can own its error surface — which left the asterisk as\n // decoration and let a nameless message through. The Easy lane's form lets\n // the browser enforce the same `required`, so this is where the two agree.\n if (!values.name.trim() || !values.email.trim() || !values.message.trim()) {\n return 'Name, email and message are required.';\n }\n if (context.requireInvoice && !values.invoiceId.trim()) {\n return 'An order or invoice ID is required.';\n }\n // Checked on the message as it will be SENT, name line included: cutting it\n // down to the API limit would deliver a ticket the buyer never wrote while\n // the form reports success.\n if (values.subject.trim().length > MAX_TITLE_LENGTH) {\n return `Keep the subject under ${MAX_TITLE_LENGTH} characters.`;\n }\n if (buildTicketMessage(values).length > MESSAGE_MAX_LENGTH) {\n return `Your message is too long. Keep it under ${MESSAGE_MAX_LENGTH.toLocaleString('en-US')} characters.`;\n }\n return null;\n}\n\nfunction requiredGlobal(name: 'shopSlug' | 'apiBaseUrl'): string {\n const value = window[name];\n if (typeof value !== 'string' || !value.trim()) {\n throw new Error(\n `Shoppex bootstrap global \"${name}\" is missing. This storefront must be served by the Shoppex storefront (or preview) worker.`,\n );\n }\n return value.trim().replace(/\\/+$/, '');\n}\n\n/** The buyer's name rides along inside the body — the ticket has no name field. */\nfunction buildTicketMessage(values: ContactFormValues): string {\n const name = values.name.trim();\n const body = values.message.trim();\n return `${name ? `Name: ${name}\\n\\n` : ''}${body}`;\n}\n\nexport async function submitContactTicket(\n values: ContactFormValues,\n context: ContactSubmissionContext,\n): Promise<void> {\n const error = validateContactSubmission(values, context);\n if (error) throw new Error(error);\n\n const shopSlug = requiredGlobal('shopSlug');\n const apiBaseUrl = requiredGlobal('apiBaseUrl');\n const subject = values.subject.trim();\n const invoiceId = values.invoiceId.trim();\n\n const response = await fetch(\n `${apiBaseUrl}/v1/storefront/shops/name/${encodeURIComponent(shopSlug)}/tickets`,\n {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', Accept: 'application/json' },\n body: JSON.stringify({\n email: values.email.trim().toLowerCase(),\n title: subject.length >= MIN_SUBJECT_LENGTH\n ? subject\n : DEFAULT_TICKET_TITLE,\n message: buildTicketMessage(values),\n ...(invoiceId ? { invoice_id: invoiceId } : {}),\n }),\n },\n );\n\n const payload = (await response.json().catch(() => null)) as\n | { message?: string; error?: string; data?: { uniqid?: string } }\n | null;\n\n if (!response.ok) {\n throw new Error(payload?.message || payload?.error || 'Failed to send your message.');\n }\n if (!payload?.data?.uniqid) {\n throw new Error('Your message was accepted, but the store did not confirm a ticket.');\n }\n}\n\nexport interface SupportChannel {\n label: string;\n href: string;\n /** Mail links open the visitor's client rather than a new tab. */\n isMail: boolean;\n}\n\n/**\n * The channels a buyer can reach the shop on before they buy.\n *\n * Derived from the shop's own social profiles, not from theme content: those\n * handles belong to the merchant and are edited once in the dashboard. The\n * three networks and their order are the platform's\n * (`withShopContactFallbacks` in the document renderer), so the two lanes list\n * the same links in the same order.\n */\nexport function resolveSupportChannels(links: readonly SocialLink[]): SupportChannel[] {\n const byNetwork = new Map(links.map((link) => [link.network, link.href]));\n return (['discord', 'telegram', 'email'] as const).flatMap((network) => {\n const href = byNetwork.get(network);\n if (!href) return [];\n return [{\n label: network === 'email' ? 'Email' : network === 'discord' ? 'Discord' : 'Telegram',\n href,\n isMail: network === 'email',\n }];\n });\n}\n","/**\n * The merchant's navigation menus, and where they sit against theme content.\n *\n * A shop configures menus once in the dashboard and expects every theme to use\n * them. The Easy lane resolves a slot in this order\n * (`build-storefront-navigation.ts` + `resolveDocumentNavigationLinks`):\n *\n * 1. a link list the merchant authored IN THE THEME wins — including an\n * empty one, which means \"no links here\", not \"fall back\";\n * 2. otherwise the merchant's menu for that slot;\n * 3. otherwise the theme's own static links.\n *\n * Mirrored exactly. `hasAuthoredLinks` is what separates (1) from (3): the\n * template's shipped `nav.links` are step 3, not an override, so a shop with a\n * header menu and no theme edit gets its menu — which is the whole gap this\n * closes.\n *\n * The menus are fetched once per page load and shared by both slots. While the\n * request is in flight the theme's own links render: that is the same list step\n * 3 would settle on, so the header is never empty and never shows a spinner\n * where its navigation belongs. A shop WITH a menu therefore swaps its links in\n * once, on first resolve.\n */\n\nimport { useEffect, useMemo, useState } from 'react';\nimport { getShoppex } from './shoppex';\nimport type { SdkMenu, SdkMenuItem } from './sdk';\n\nexport type MenuSlot = 'header' | 'footer';\n\n/** One navigation link as a theme authors it: a label and where it goes. */\nexport type NavLink = {\n label: string;\n href: string;\n};\n\n/**\n * One menu item as a nav link, matching `menuItemToNavLink`: a PAGE item\n * resolves to `/page/<slug>` in the same tab, a link item keeps its url and its\n * stored target, and an item with neither destination is dropped rather than\n * rendered as a dead label.\n */\nfunction toNavLink(item: SdkMenuItem): (NavLink & { target: string | null }) | null {\n const label = typeof item.title === 'string' ? item.title.trim() : '';\n if (!label) return null;\n const pageSlug = typeof item.page_slug === 'string' ? item.page_slug.trim() : '';\n if (pageSlug) return { label, href: `/page/${pageSlug}`, target: null };\n const url = typeof item.url === 'string' ? item.url.trim() : '';\n if (!url) return null;\n return { label, href: url, target: item.target ?? null };\n}\n\nexport type MenuNavLink = NavLink & { target: string | null };\n\nexport function resolveMenuLinksForSlot(menus: readonly SdkMenu[], slot: MenuSlot): MenuNavLink[] {\n const menu = menus.find((entry) => entry.slot === slot);\n if (!menu || !Array.isArray(menu.items)) return [];\n return menu.items\n .flatMap((item) => {\n const link = toNavLink(item);\n return link ? [{ link, sortOrder: item.sort_order ?? 0 }] : [];\n })\n .sort((a, b) => a.sortOrder - b.sortOrder)\n .map((entry) => entry.link);\n}\n\n/**\n * The shop's menus, requested at most once per page load.\n *\n * Shared rather than per-hook because the header and the footer both need them\n * and the SDK's own cache is per-call — two components mounting in the same\n * frame would otherwise fire two requests for one answer. A failed request\n * resolves to no menus: the theme's own links are then the answer, which is the\n * same state a shop that configured none is in.\n */\nlet menusPromise: Promise<SdkMenu[]> | null = null;\n\nfunction loadMerchantMenus(): Promise<SdkMenu[]> {\n menusPromise ??= getShoppex()\n .getMenus()\n .then((result) => (result.success && Array.isArray(result.data) ? result.data : []))\n .catch(() => []);\n return menusPromise;\n}\n\nexport interface NavigationLinks {\n links: readonly MenuNavLink[];\n /**\n * True when the rendered list is the THEME's content, which is what decides\n * whether each entry carries a `data-sx-config` path — a merchant's menu item\n * is edited in the dashboard's navigation screen, not in the design\n * inspector, and pointing the inspector at it would offer an edit that goes\n * nowhere.\n */\n fromTheme: boolean;\n}\n\n/**\n * The links one slot should render, with the precedence above already applied.\n *\n * `themeLinks` is the template's own list. It is returned unchanged when the\n * merchant authored it (step 1) and while the menus are still loading; a\n * non-empty menu replaces it once resolved (step 2); an empty menu leaves it in\n * place (step 3).\n */\nexport function useNavigationLinks(\n slot: MenuSlot,\n themeLinks: readonly NavLink[],\n /** True when the merchant edited this slot's links in the theme content. */\n authored: boolean,\n): NavigationLinks {\n const themeNavLinks = useMemo<MenuNavLink[]>(\n () => themeLinks.map((link) => ({ ...link, target: null })),\n [themeLinks],\n );\n const [menuLinks, setMenuLinks] = useState<MenuNavLink[] | null>(null);\n\n useEffect(() => {\n // An authored override wins outright, so the menus are not even consulted —\n // the same reason `buildSlotLinks` returns before it reads `payload.menus`.\n if (authored) return undefined;\n let active = true;\n void loadMerchantMenus().then((menus) => {\n if (active) setMenuLinks(resolveMenuLinksForSlot(menus, slot));\n });\n return () => {\n active = false;\n };\n }, [authored, slot]);\n\n if (!authored && menuLinks !== null && menuLinks.length > 0) {\n return { links: menuLinks, fromTheme: false };\n }\n return { links: themeNavLinks, fromTheme: true };\n}\n","/**\n * Shop feedback for the `/reviews` page.\n *\n * Shoppex feedback is SHOP-level, not per product, and it is not part of the\n * injected catalog payload — the bootstrap object carries only per-product\n * tallies. The bodies come from a runtime call, so this is the one surface in\n * the theme that renders a list it has to wait for.\n *\n * Nothing here invents a review. A shop with no feedback resolves to an empty\n * list and the page says so; a failed request resolves to an error state and\n * the page says that instead. Neither is padded with sample data.\n */\n\nimport { useCallback, useEffect, useRef, useState } from 'react';\nimport type { SdkShopReview, SdkShopReviewsSummary } from './sdk';\nimport { getShoppex } from './shoppex';\n\nexport interface ShopReview {\n id: string;\n /** Filled-star count, the rating clamped to 0-5 and rounded. */\n starCount: number;\n rating: number;\n comment: string;\n author: string;\n /** Localized short date, e.g. \"Jan 12, 2026\". Blank when the API sent none. */\n dateLabel: string;\n /** Sort key; 0 when the entry carries no usable timestamp. */\n timestamp: number;\n /**\n * The reviewed product's CURRENT title, or null when the review names none.\n * The public feed withholds product ids, so the title is the only link to a\n * product there is; see `reviewsForProduct`.\n */\n productTitle: string | null;\n /** True when the review is tied to a real completed purchase. */\n verified: boolean;\n /** Left by the shop's review automation, not written by the buyer. */\n automated: boolean;\n}\n\nexport type ShopReviewsState =\n | { status: 'loading' }\n | { status: 'error' }\n | { status: 'ready'; reviews: ShopReview[]; average: number | null };\n\n/**\n * How many cards the page shows. The reference storefront pages its list at\n * twelve; a shop with hundreds of reviews should not ship all of them into one\n * screenshot-tall grid.\n */\nexport const SHOP_REVIEW_PAGE_SIZE = 12;\n\nexport interface ShopReviewLabels {\n anonymousAuthor: string;\n emptyComment: string;\n}\n\n/**\n * Accepts the string and number forms the feedback API uses for `created_at`:\n * an ISO date, an epoch in seconds, or an epoch in milliseconds. An\n * unparseable value sorts last and renders no date rather than \"Invalid Date\".\n */\nfunction toTimestamp(value: unknown): number {\n if (typeof value === 'number' && Number.isFinite(value)) {\n return value < 1_000_000_000_000 ? value * 1000 : value;\n }\n if (typeof value !== 'string' || value.trim() === '') return 0;\n const numeric = Number(value);\n if (Number.isFinite(numeric)) {\n return numeric < 1_000_000_000_000 ? numeric * 1000 : numeric;\n }\n const parsed = Date.parse(value);\n return Number.isNaN(parsed) ? 0 : parsed;\n}\n\nfunction formatDateLabel(timestamp: number): string {\n if (!timestamp) return '';\n return new Date(timestamp).toLocaleDateString('en-US', {\n day: 'numeric',\n month: 'short',\n year: 'numeric',\n });\n}\n\nexport function toStarCount(rating: number): number {\n if (!Number.isFinite(rating)) return 0;\n return Math.max(0, Math.min(5, Math.round(rating)));\n}\n\n/**\n * Newest first, and only entries that carry a real rating: a zero-star row is\n * an unrated record, not a one-star verdict, and averaging it in would drag\n * every shop's score down.\n */\nexport function toShopReviews(\n rows: readonly SdkShopReview[],\n labels: ShopReviewLabels,\n): ShopReview[] {\n return rows\n .filter((row) => typeof row.rating === 'number' && Number.isFinite(row.rating) && row.rating > 0)\n .map((row, index) => {\n const timestamp = toTimestamp(row.created_at);\n const comment = (row.comment ?? row.message ?? '').trim();\n const author = (row.author ?? row.customer_name ?? '').trim();\n return {\n id: row.id || `review:${index}`,\n rating: row.rating,\n starCount: toStarCount(row.rating),\n comment: comment.length > 0 ? comment : labels.emptyComment,\n author: author.length > 0 ? author : labels.anonymousAuthor,\n dateLabel: formatDateLabel(timestamp),\n timestamp,\n productTitle: row.product_title?.trim() || null,\n verified: row.verified === true,\n automated: row.is_automated === true,\n };\n })\n .sort((a, b) => b.timestamp - a.timestamp);\n}\n\n/**\n * The written reviews that name this product, newest first.\n *\n * Matched on the product's title because the public feed carries no product\n * ids. That makes it a sample, not a count: the feed leaves out appealed and\n * filtered feedback, and a renamed product loses its older reviews. Surfaces\n * show these as \"latest reviews\" beside the payload's own tally and never\n * derive a number from them.\n */\nexport function reviewsForProduct(reviews: readonly ShopReview[], productTitle: string): ShopReview[] {\n const title = productTitle.trim();\n if (!title) return [];\n return reviews.filter((review) => review.productTitle === title);\n}\n\n/** One page of the shop's reviews, newest first. */\nexport interface ShopReviewsPageResult {\n reviews: ShopReview[];\n /** Pass to the next call; null on the last page. */\n nextCursor: string | null;\n /** Counts over the whole list (or the product's reviews), null when the API sent none. */\n summary: SdkShopReviewsSummary | null;\n}\n\n/**\n * One page of reviews from the feed, optionally for one product (its catalog\n * `id`). Throws when the request fails.\n *\n * A product page must never show another product's reviews, so a filtered\n * request also requires the API to echo the filter back: an API that predates\n * `product` ignores it and would answer with the whole shop's list.\n */\nexport async function loadShopReviewsPage(options: {\n cursor?: string | null;\n product?: string | null;\n labels: ShopReviewLabels;\n}): Promise<ShopReviewsPageResult> {\n const product = options.product?.trim() || null;\n const result = await getShoppex().getShopReviewsPage(options.cursor ?? null, product ? { product } : undefined);\n if (!result.success || !result.data) {\n throw new Error(result.message?.trim() || 'Reviews could not be loaded.');\n }\n if (product !== null && result.data.product !== product) {\n throw new Error('This store does not filter reviews by product yet.');\n }\n const { pagination } = result.data;\n return {\n reviews: toShopReviews(result.data.feedback, options.labels),\n nextCursor: pagination.has_more ? pagination.next_cursor : null,\n summary: result.data.summary ?? null,\n };\n}\n\n/**\n * The newest WRITTEN reviews, at most `limit`, for a sample such as a home\n * section or a product's review tab. Pages through the feed only until it has\n * enough. Automated entries and ratings without words are skipped, since a\n * sample of quotes needs quotes. Resolves null when a page request fails.\n */\nexport async function loadLatestWrittenReviews(\n limit: number,\n labels: ShopReviewLabels,\n options: { product?: string | null } = {},\n): Promise<ShopReview[] | null> {\n const written: ShopReview[] = [];\n const seen = new Set<string>();\n let cursor: string | null = null;\n try {\n while (written.length < limit) {\n const page = await loadShopReviewsPage({ cursor, product: options.product, labels: { anonymousAuthor: labels.anonymousAuthor, emptyComment: '' } });\n written.push(...page.reviews.filter((review) => !review.automated && review.comment !== ''));\n if (!page.nextCursor || seen.has(page.nextCursor)) break;\n seen.add(page.nextCursor);\n cursor = page.nextCursor;\n }\n } catch {\n return null;\n }\n return written.slice(0, limit).map((review) => ({ ...review, comment: review.comment || labels.emptyComment }));\n}\n\nexport type PagedShopReviewsState =\n | { status: 'loading' }\n | { status: 'error' }\n | {\n status: 'ready';\n reviews: ShopReview[];\n summary: SdkShopReviewsSummary | null;\n hasMore: boolean;\n /** True while the next page is on its way. */\n loadingMore: boolean;\n /** Set when the NEXT page failed; the pages already shown stay. */\n moreFailed: boolean;\n };\n\n/**\n * The shop's reviews, one page at a time: the first page on mount (or when\n * `product` changes), the next only when `loadMore` is called. The summary is\n * the API's, so the score and breakdown describe the whole list, not the pages\n * loaded so far.\n */\nexport function usePagedShopReviews(options: {\n product?: string | null;\n labels: ShopReviewLabels;\n}): PagedShopReviewsState & { loadMore: () => void } {\n const product = options.product?.trim() || null;\n const { anonymousAuthor, emptyComment } = options.labels;\n const [state, setState] = useState<PagedShopReviewsState>({ status: 'loading' });\n const [cursor, setCursor] = useState<string | null>(null);\n const generation = useRef(0);\n\n useEffect(() => {\n const current = (generation.current += 1);\n setState({ status: 'loading' });\n setCursor(null);\n loadShopReviewsPage({ product, labels: { anonymousAuthor, emptyComment } })\n .then((page) => {\n if (generation.current !== current) return;\n setCursor(page.nextCursor);\n setState({\n status: 'ready',\n reviews: page.reviews,\n summary: page.summary,\n hasMore: page.nextCursor !== null,\n loadingMore: false,\n moreFailed: false,\n });\n })\n .catch(() => {\n if (generation.current === current) setState({ status: 'error' });\n });\n }, [product, anonymousAuthor, emptyComment]);\n\n const loadMore = useCallback(() => {\n if (state.status !== 'ready' || state.loadingMore || cursor === null) return;\n const current = generation.current;\n setState({ ...state, loadingMore: true, moreFailed: false });\n loadShopReviewsPage({ cursor, product, labels: { anonymousAuthor, emptyComment } })\n .then((page) => {\n if (generation.current !== current) return;\n setCursor(page.nextCursor);\n setState((previous) => previous.status !== 'ready' ? previous : {\n ...previous,\n reviews: [...previous.reviews, ...page.reviews],\n hasMore: page.nextCursor !== null,\n loadingMore: false,\n });\n })\n .catch(() => {\n if (generation.current !== current) return;\n setState((previous) => previous.status !== 'ready' ? previous : { ...previous, loadingMore: false, moreFailed: true });\n });\n }, [state, cursor, product, anonymousAuthor, emptyComment]);\n\n return { ...state, loadMore };\n}\n\nexport function averageRating(reviews: readonly ShopReview[]): number | null {\n if (reviews.length === 0) return null;\n return reviews.reduce((total, review) => total + review.rating, 0) / reviews.length;\n}\n\nexport function useShopReviews(labels: ShopReviewLabels): ShopReviewsState {\n const [state, setState] = useState<ShopReviewsState>({ status: 'loading' });\n const { anonymousAuthor, emptyComment } = labels;\n\n useEffect(() => {\n let active = true;\n getShoppex()\n .getShopReviews()\n .then((result) => {\n if (!active) return;\n if (!result.success || !result.data) {\n setState({ status: 'error' });\n return;\n }\n const reviews = toShopReviews(result.data, { anonymousAuthor, emptyComment });\n setState({ status: 'ready', reviews, average: averageRating(reviews) });\n })\n .catch(() => {\n if (active) setState({ status: 'error' });\n });\n return () => {\n active = false;\n };\n }, [anonymousAuthor, emptyComment]);\n\n return state;\n}\n","/**\n * Merchant CMS pages (dashboard → Pages), fetched by slug for `/page/:slug`.\n *\n * These are NOT in the injected catalog: the edge injects the storefront\n * payload, and the merchant's pages come from a separate service the SDK reads\n * (`getPage` / `getPages`). So the route loads them, and every state it can be\n * in is explicit — a slug that does not resolve renders the theme's not-found\n * copy rather than an empty article.\n *\n * The rendering rules are the Easy lane's, from `enrichCmsPageBlockSettings`\n * plus `blocks/page-tool-page.ts` → `renderCmsPage`:\n *\n * 1. `template_type === 'faq'` and `settings.faq_items` non-empty → the\n * accordion. A row needs BOTH a question and an answer to count, exactly\n * as `normalizeCmsFaqItems` requires.\n * 2. otherwise the body, rendered from MARKDOWN (gfm + breaks, the same\n * `marked` configuration `renderCmsPageMarkdownHtml` uses).\n * 3. otherwise the empty state.\n *\n * What this module does NOT do is sanitize. The markdown pass emits merchant\n * HTML — markdown may contain raw tags — and it is merchant HTML of exactly the\n * same status as `shop.termsHtml` or a product description, so it is sanitized\n * where it is injected, by the component that injects it. That is the rule the\n * whole template follows and the one its HTML-sink guard checks.\n */\n\nimport { getShoppex } from './shoppex';\nimport type { SdkPage } from './sdk';\n\nexport type ShopPageFaqItem = { question: string; answer: string };\n\n/** One resolved CMS page, reduced to what the route renders. */\nexport interface ShopPageModel {\n title: string;\n /**\n * The markdown body as HTML — merchant markup, NOT yet sanitized. The route\n * sanitizes it at the sink; see the module docblock.\n */\n bodyMarkdownHtml: string;\n /** Non-empty only for a `faq` page with usable rows. */\n faqItems: ShopPageFaqItem[];\n}\n\nexport type ShopPageState =\n | { status: 'loading' }\n | { status: 'ready'; page: ShopPageModel }\n /** The API answered that no such page exists (404), or the page has no name. */\n | { status: 'missing' }\n /**\n * The page could not be loaded: the API failed or could not be reached, or\n * the page's body could not be rendered. Not \"missing\": the page may exist.\n */\n | { status: 'error' };\n\n/** Mirrors `normalizeCmsFaqItems`: a row needs BOTH halves or it is not a row. */\nfunction parseFaqItems(value: unknown): ShopPageFaqItem[] {\n if (!Array.isArray(value)) return [];\n const items: ShopPageFaqItem[] = [];\n for (const entry of value) {\n if (entry === null || typeof entry !== 'object') continue;\n const record = entry as { question?: unknown; answer?: unknown };\n const question = typeof record.question === 'string' ? record.question.trim() : '';\n const answer = typeof record.answer === 'string' ? record.answer.trim() : '';\n if (question && answer) items.push({ question, answer });\n }\n return items;\n}\n\n/**\n * The merchant's markdown body as HTML.\n *\n * `marked` runs with gfm and breaks on, which is what `renderCmsPageMarkdownHtml`\n * configures — a page that renders as a table on one lane must not render as a\n * row of pipes on the other. Markdown can emit raw HTML, so this is not a\n * sanitizer and never stands in for one.\n *\n * The parser is imported on DEMAND. It is ~40 kB of JavaScript that only this\n * one route can ever use, and loading it in the main bundle would make every\n * buyer who never opens a CMS page pay for one.\n */\nasync function renderShopPageMarkdownHtml(content: string | null | undefined): Promise<string> {\n const trimmed = typeof content === 'string' ? content.trim() : '';\n if (!trimmed) return '';\n const { marked } = await import('marked');\n return marked.parse(trimmed, { async: false, gfm: true, breaks: true });\n}\n\nasync function buildShopPageModel(page: SdkPage): Promise<ShopPageModel> {\n // FAQ rows are a `faq` template's content, not a fallback for a page whose\n // body failed to render — the same gate `cmsPageFaqItems` applies.\n const faqItems = page.template_type === 'faq' ? parseFaqItems(page.settings?.faq_items) : [];\n return {\n title: page.name,\n // A FAQ page never shows its body, so its markdown is never parsed and the\n // parser is never fetched for one.\n bodyMarkdownHtml: faqItems.length > 0 ? '' : await renderShopPageMarkdownHtml(page.content),\n faqItems,\n };\n}\n\n/**\n * Loads one CMS page by slug.\n *\n * `missing` only when the API says so (404) or the page it returns has no name\n * to show; inventing a title from the slug would publish a heading the merchant\n * never wrote. Any other failure is `error`, so an outage never tells a buyer a\n * real page does not exist.\n */\nexport async function loadShopPage(slug: string): Promise<ShopPageState> {\n let result: Awaited<ReturnType<ReturnType<typeof getShoppex>['getPage']>>;\n try {\n result = await getShoppex().getPage(slug);\n } catch {\n return { status: 'error' };\n }\n if (!result.success) return result.status === 404 ? { status: 'missing' } : { status: 'error' };\n const page = result.data;\n if (!page || typeof page.name !== 'string' || !page.name.trim()) return { status: 'missing' };\n try {\n return { status: 'ready', page: await buildShopPageModel(page) };\n } catch {\n // The markdown parser is a lazy chunk; a failed import is an error, not a\n // missing page.\n return { status: 'error' };\n }\n}\n","/**\n * The parsed storefront payload, provided once at the root.\n *\n * Same discipline as the content contract: the wire payload is parsed strictly\n * in exactly one place, above everything that reads it, so a changed contract\n * surfaces as an explicit error instead of a storefront that renders most of a\n * shop and quietly drops the rest.\n */\n\nimport { createContext, type ReactNode, useContext, useEffect, useState } from 'react';\nimport { buildStorefrontCatalog, type StorefrontCatalog } from './catalog';\nimport { getShoppex } from './shoppex';\nimport {\n getStorefrontSettings,\n getStorefrontShop,\n type StorefrontSettings,\n type StorefrontShop,\n} from './storefront-data';\n\nexport interface StorefrontData {\n settings: StorefrontSettings;\n shop: StorefrontShop;\n catalog: StorefrontCatalog;\n}\n\nexport function readStorefrontData(): StorefrontData {\n const settings = getStorefrontSettings();\n const catalog = buildStorefrontCatalog(settings);\n return { settings, shop: getStorefrontShop(catalog.listedProductCount), catalog };\n}\n\nconst StorefrontContext = createContext<StorefrontData | null>(null);\n\nexport function StorefrontProvider({ data, children }: { data: StorefrontData; children: ReactNode }) {\n return <StorefrontContext.Provider value={data}>{children}</StorefrontContext.Provider>;\n}\n\nexport function useStorefront(): StorefrontData {\n const data = useContext(StorefrontContext);\n if (!data) throw new Error('useStorefront must be used inside StorefrontProvider.');\n return data;\n}\n\n/**\n * Live visitor count for the header pill.\n *\n * Starts at null — \"not answered yet\" — and the pill renders nothing until a\n * real number arrives. A placeholder zero would flash \"0 online\" on every page\n * load, and a placeholder guess would be a fabricated claim. A failed request\n * leaves it null and the pill simply stays away.\n */\nexport function useOnlineUserCount(): number | null {\n const [count, setCount] = useState<number | null>(null);\n\n useEffect(() => {\n let active = true;\n getShoppex()\n .getStorefrontOnlineUsers()\n .then((result) => {\n if (!active || !result.success || !result.data) return;\n const value = result.data.count;\n if (Number.isFinite(value) && value > 0) setCount(Math.floor(value));\n })\n .catch(() => undefined);\n return () => {\n active = false;\n };\n }, []);\n\n return count;\n}\n"],"mappings":"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","names":["isRecord","total","value","useEffect","useMemo","useState","useMemo","useState","useEffect","useCallback","useEffect","useRef","useState","useState","useRef","useEffect","useCallback","createContext","useContext","useEffect","useState","jsx","createContext","useContext","useState","useEffect"]}
1
+ {"version":3,"sources":["../src/account-route-messages.ts","../src/cart.tsx","../src/parse.ts","../src/product-badges.ts","../src/service-status.ts","../src/video-embed.ts","../src/storefront-data.ts","../src/checkout-fields.ts","../src/cart-quote-bounds.ts","../src/checkout-error-state.ts","../src/sanitize-html.ts","../src/catalog.ts","../src/product-detail.ts","../src/cart-view.ts","../src/contact.ts","../src/menus.ts","../src/reviews.ts","../src/shop-pages.ts","../src/stock-alerts.ts","../src/storefront-context.tsx"],"sourcesContent":["export const ACCOUNT_ROUTE_MESSAGE_KEYS = [\n 'account.checkingSession',\n 'common.notLoaded',\n 'common.genericError',\n 'common.retry',\n] as const;\n\nexport type AccountRouteMessageKey = (typeof ACCOUNT_ROUTE_MESSAGE_KEYS)[number];\ntype AccountRouteMessageTuple = readonly [string, string, string, string];\n\n/**\n * A synchronous projection of the four existing catalog keys the lazy route\n * itself needs. The tuple order follows ACCOUNT_ROUTE_MESSAGE_KEYS.\n *\n * Importing the complete catalog here would put all account translations back\n * in the main bundle. The catalog-parity test guards every value against drift.\n */\nconst ACCOUNT_ROUTE_MESSAGES = {\n ar: ['جارٍ التحقق من جلستك', 'تعذّر تحميل هذا', 'تعذّر تحميل هذا. يُرجى المحاولة بعد قليل.', 'أعد المحاولة'],\n de: ['Deine Sitzung wird geprüft', 'Das wurde nicht geladen', 'Das konnte nicht geladen werden. Bitte versuche es gleich noch einmal.', 'Erneut versuchen'],\n en: ['Checking your session', 'This did not load', 'This could not be loaded. Please try again in a moment.', 'Try again'],\n es: ['Comprobando tu sesión', 'Esto no se cargó', 'No se pudo cargar. Inténtalo de nuevo en un momento.', 'Inténtalo de nuevo'],\n fi: ['Tarkistetaan istuntoasi', 'Tämä ei latautunut', 'Tätä ei voitu ladata. Yritä hetken kuluttua uudelleen.', 'Yritä uudelleen'],\n fr: ['Vérification de votre session', 'Le chargement a échoué', 'Le chargement a échoué. Réessayez dans un instant.', 'Réessayer'],\n hi: ['आपका सत्र जाँचा जा रहा है', 'यह लोड नहीं हुआ', 'यह लोड नहीं हो सका। कृपया थोड़ी देर बाद फिर कोशिश करें।', 'फिर कोशिश करें'],\n it: ['Verifica della tua sessione', 'Caricamento non riuscito', 'Non è stato possibile caricare questo contenuto. Riprova tra poco.', 'Riprova'],\n lt: ['Tikriname jūsų seansą', 'Šis turinys neįsikėlė', 'Nepavyko to įkelti. Po akimirkos bandykite dar kartą.', 'Bandykite dar kartą'],\n nl: ['Je sessie wordt gecontroleerd', 'Dit is niet geladen', 'Dit kon niet worden geladen. Probeer het zo meteen opnieuw.', 'Probeer opnieuw'],\n pl: ['Sprawdzamy Twoją sesję', 'To się nie wczytało', 'Nie udało się tego wczytać. Spróbuj ponownie za chwilę.', 'Spróbuj ponownie'],\n pt: ['A verificar a tua sessão', 'Isto não carregou', 'Não foi possível carregar isto. Tenta de novo daqui a pouco.', 'Tentar de novo'],\n ru: ['Проверяем вашу сессию', 'Это не загрузилось', 'Не удалось загрузить это. Повторите попытку через минуту.', 'Повторить'],\n tr: ['Oturumunuz kontrol ediliyor', 'Bu yüklenemedi', 'Bu yüklenemedi. Lütfen birazdan tekrar deneyin.', 'Tekrar deneyin'],\n uk: ['Перевіряємо вашу сесію', 'Це не завантажилося', 'Не вдалося завантажити. Спробуйте ще раз за хвилину.', 'Спробувати ще раз'],\n zh: ['正在检查登录状态', '此内容未加载', '无法加载此内容,请稍后重试。', '重试'],\n} as const satisfies Readonly<Record<string, AccountRouteMessageTuple>>;\n\ntype AccountRouteLocale = keyof typeof ACCOUNT_ROUTE_MESSAGES;\n\nfunction resolveAccountRouteLocale(lang: string | null | undefined): AccountRouteLocale {\n const base = (lang ?? '').trim().toLowerCase().split(/[-_]/)[0] ?? '';\n return base in ACCOUNT_ROUTE_MESSAGES ? base as AccountRouteLocale : 'en';\n}\n\nexport function accountRouteMessage(\n key: AccountRouteMessageKey,\n lang?: string | null,\n): string {\n const activeLang = lang === undefined && typeof document !== 'undefined'\n ? document.documentElement.getAttribute('lang')\n : lang;\n const messageIndex = ACCOUNT_ROUTE_MESSAGE_KEYS.indexOf(key);\n return ACCOUNT_ROUTE_MESSAGES[resolveAccountRouteLocale(activeLang)][messageIndex];\n}\n","/**\n * Cart state, backed entirely by the Storefront SDK.\n *\n * The SDK owns the line list, the coupon and the checkout call; this context\n * owns only what React needs on top of it — the current quote, the drawer's\n * open state, and a single in-flight indicator so buttons can disable\n * themselves. Nothing here computes a price the backend has not confirmed.\n *\n * It also owns the one decision the SDK cannot make for it: whether the hand-off\n * has to stop first to collect a product's required details. See `checkout` and\n * `checkout-fields.ts`.\n *\n * What it does NOT own is which FIELD a cart code is submitted under. Only the\n * server can say whether a string is a merchant coupon or an affiliate referral,\n * and its answer is saved next to the code by `applyCoupon` — so the hand-off\n * reads both back out of SDK storage instead of tracking either in React. See\n * `resolveSubmittedCode`.\n *\n * The Easy-lane clean-minimal storefront presents the cart as a right-hand\n * drawer opened from the header (`sx-cart-drawer`) with `/cart` as a full-page\n * fallback, so this theme has no cart-placement setting to carry.\n *\n * Adding a line does NOT open the drawer. The Easy-lane clean-minimal shell\n * omits `data-sx-cart-open-on-add`, which is the runtime's opt-in for that\n * behaviour — the header count is the whole acknowledgement a buyer gets, and\n * a panel that covers the catalog after every add would be this theme\n * inventing an interruption the reference storefront does not have.\n */\n\nimport {\n createContext,\n type ReactNode,\n useCallback,\n useContext,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from 'react';\nimport {\n buildCustomFieldPayload,\n checkoutFieldWriteRepriced,\n checkoutNeedsDetails as needsCheckoutDetails,\n missingRequiredCheckoutValues,\n resolveCheckoutFields,\n type CheckoutFields,\n type CheckoutFieldValues,\n} from './checkout-fields';\nimport {\n parseCartQuoteLines,\n resolveCartBoundsHeals,\n resolveUnsellableLineIds,\n} from './cart-quote-bounds';\nimport {\n QUOTE_UNAVAILABLE_CHECKOUT_ERROR,\n clearRecoveredQuoteCheckoutError,\n} from './checkout-error-state';\nimport { formatMoney } from './catalog';\nimport { calculateLinePricing, resolvePayWhatYouWantMinimum } from './product-detail';\nimport {\n getProducts,\n isSelectionSoldOut,\n resolveQuantityBounds,\n type ProductAddon,\n type ProductBundleItem,\n type StorefrontPriceVariant,\n type StorefrontProduct,\n} from './storefront-data';\nimport type {\n SdkCartAddOptions,\n SdkCartCodeSource,\n SdkCartItem,\n SdkCartQuote,\n ShoppexSdk,\n} from './sdk';\nimport { getBuyerCurrency, getShoppex } from './shoppex';\n\ntype CartContextValue = {\n lines: SdkCartItem[];\n count: number;\n quote: SdkCartQuote | null;\n /** The server could not price the cart. Written by the refresh, only. */\n quoteError: string | null;\n /**\n * A cart WRITE failed — an add the SDK refused or dropped, a bundle that had\n * to be rolled back. Held apart from `quoteError` on purpose: a refresh must\n * never be able to clear it. See `mutate`.\n */\n mutationError: string | null;\n /**\n * The last code the buyer applied was refused by the quote.\n *\n * Written only by `applyCoupon` and cleared only by the buyer's next cart\n * action (see `mutate`). It cannot be derived from the quote any more,\n * because a refused code is REMOVED from storage the moment it is refused —\n * the very next quote therefore carries no code and would erase the message\n * that explains why.\n */\n couponRejected: boolean;\n /**\n * True when the code the CURRENT quote was priced with is the ambient\n * affiliate referral rather than one the buyer applied.\n *\n * The promo field decides between its entry row and its \"applied\" chip from\n * `quote.coupon_applied`, which was a complete answer while the only code\n * that could ever reach a quote was one the buyer typed. It is not one any\n * more: an accepted ambient referral sets that same flag, and the chip would\n * then congratulate the buyer on a code they never entered, print it blank\n * (the field is empty, because nothing was applied), and offer a Remove\n * button wired to cart-coupon storage the referral does not live in — so it\n * would reappear on the next quote. Kept next to the quote it describes and\n * written by the same statements, so the two can never disagree.\n */\n quotedAmbientAffiliate: boolean;\n /**\n * The shop's checkout maximum, stated once, while the CURRENT quote says this\n * cart is over it. Null while it is within the limit or while no quote has\n * settled.\n *\n * PAYMENT PATH. The summary prints it beside the total — an over-limit cart\n * is a cart the checkout refuses, and the buyer cannot act on a limit nobody\n * names — and `checkout` refuses the hand-off on the same fact, exactly as it\n * refuses an unsellable line. See `resolveCartMaximumWarning`.\n */\n cartMaxWarning: string | null;\n /**\n * The settled quote contains a PHYSICAL line, so its total excludes shipping,\n * which the hosted checkout prices from the buyer's address. False while no\n * quote has settled. The summary says so under the total.\n */\n requiresShipping: boolean;\n checkoutError: string | null;\n /**\n * True when the LAST hand-off was refused because the referral on this cart\n * is the buyer's own (`affiliate_code_not_applicable`).\n *\n * It needs its own flag rather than being read out of `checkoutError`,\n * because it is the one refusal the buyer cannot resolve by re-reading the\n * message: the code is ambient — nobody typed it, no field shows it, and no\n * control removes it — so without an explicit exit the cart refuses every\n * retry forever. The surface renders `clearBlockedReferral` next to the\n * message; the flag is what tells it to.\n */\n selfReferralBlocked: boolean;\n /**\n * The exit. Drops the referral attribution the SDK is carrying, re-quotes\n * without it, and lets the buyer check out again. See the implementation.\n */\n clearBlockedReferral: () => Promise<void>;\n busy: boolean;\n couponInput: string;\n setCouponInput: (value: string) => void;\n drawerOpen: boolean;\n setDrawerOpen: (open: boolean) => void;\n add: (\n product: StorefrontProduct,\n variant: StorefrontPriceVariant | null,\n quantity: number,\n options?: {\n /**\n * Buyer-chosen unit price for a pay-what-you-want product. Part of the\n * cart line identity — the server validates it against the minimum.\n */\n payWhatYouWantPrice?: number;\n /** Extras the buyer ticked in the buy box. Also part of line identity. */\n addons?: ProductAddon[];\n },\n ) => Promise<void>;\n /**\n * Adds EVERY member of a bundle offer, as one operation.\n *\n * Bundle members arrive as a compact summary rather than a full product, so\n * they cannot go through `add` — their quantity bounds are resolved from the\n * member's own catalog product instead, and a member that cannot be resolved\n * is rejected rather than added. The set is all-or-nothing: see the\n * implementation for why a half-added bundle is worse than none of it.\n */\n addBundle: (items: readonly ProductBundleItem[]) => Promise<void>;\n update: (lineId: string, quantity: number) => Promise<void>;\n remove: (lineId: string) => Promise<void>;\n clear: () => Promise<void>;\n applyCoupon: () => Promise<void>;\n removeCoupon: () => Promise<void>;\n /**\n * The per-product details this cart's products require, resolved from the\n * injected catalog. `/checkout` renders the form from it.\n */\n checkoutFields: CheckoutFields;\n /**\n * True when the hand-off must go through `/checkout` to collect those details\n * first. Read it on CLICK rather than from a render closure after a cart\n * write — `readCheckoutNeedsDetails` exists for exactly that.\n */\n checkoutNeedsDetails: boolean;\n /**\n * Hands over to Hosted Checkout. `customFieldValues` writes the buyer's\n * answers onto the cart lines first and is what `/checkout` submits; every\n * other surface hands off with no values, and may only do so when\n * `checkoutNeedsDetails` is false.\n */\n checkout: (customFieldValues?: CheckoutFieldValues) => Promise<void>;\n};\n\n/**\n * Whether the checkout CTA has to stop for details, read from the SDK's cart\n * RIGHT NOW rather than from React state.\n *\n * PAYMENT PATH. Buy Now adds a line and then hands off in the same click. The\n * `checkoutNeedsDetails` on the context describes the cart as it was when the\n * component last rendered, which is the cart BEFORE that add — a product whose\n * required fields the add has just brought into the cart would be handed\n * straight to a checkout that cannot be created.\n */\nexport function readCheckoutNeedsDetails(): boolean {\n return needsCheckoutDetails(resolveCheckoutFields(getShoppex().getCart()));\n}\n\nconst CartContext = createContext<CartContextValue | null>(null);\n\n/**\n * The backend's refusal when the referral on the order belongs to the buyer\n * placing it — an affiliate opening their own storefront on their own `?ref=`\n * link is the ordinary way to reach it.\n *\n * The value is the FULL i18n key, because that is literally what crosses the\n * wire: `from-cart` sends `error_code: CHECKOUT_ERROR_CODES[reason]`, whose\n * values are the `errors.checkout.*` keys, and the SDK client copies\n * `error_code` to `code` verbatim. The short reason name never leaves the\n * server. Compared in full rather than by suffix: this is an exact identifier,\n * and a suffix match would also accept the same leaf under another namespace.\n */\nconst SELF_REFERRAL_REFUSAL_CODE = 'errors.checkout.affiliate_code_not_applicable';\nconst SELF_REFERRAL_MESSAGE = 'This referral code cannot be used on your own order.';\n\n/**\n * The backend's refusals of a SUBMITTED quote proof, with the copy each gets.\n *\n * PAYMENT PATH. `/from-cart` no longer treats a proof it cannot honour as \"no\n * proof sent\" — doing so silently removed the ceiling that stops an invoice\n * being priced above the total the buyer approved. It names the reason, and all\n * of them are resolved the same way: re-quote, so the buyer approves a current\n * total and the retry carries a proof describing the cart as it now is.\n *\n * `price_increased_since_quote` is that ceiling DOING ITS JOB rather than a\n * proof this theme got wrong: the invoice would price above the total the buyer\n * approved, and a promotion expiring between the quote and the click is the\n * ordinary way to reach it. Left out of this map it fell through to the\n * backend's prose with no re-quote, so the stale proof and the stale total both\n * stayed put — every retry submitted the identical ceiling, was refused\n * identically, and the number on screen never moved. A dead end with no\n * buyer-reachable exit, which is the same failure class as the ambient-referral\n * lock-out. Its copy names the PRICE CHANGE rather than a failure, because that\n * is the thing the buyer has to understand before reading the new total.\n *\n * Matched on the CODE — the full `errors.checkout.*` key the wire carries —\n * never on the message, which is localized prose.\n */\n/**\n * Checkout refusals the buyer can only read, not fix from the cart, with copy\n * that names the reason instead of the backend's generic prose. Matched on the\n * `errors.checkout.*` code, like the maps above.\n */\nconst CHECKOUT_REFUSAL_MESSAGES: Record<string, string> = {\n 'errors.checkout.shop_does_not_ship': 'This shop does not ship physical products yet.',\n};\n\nconst QUOTE_PROOF_REFUSAL_MESSAGES: Record<string, string> = {\n 'errors.checkout.quote_token_invalid':\n 'We could not verify the price you approved. The cart has been re-priced — review the total and check out again.',\n 'errors.checkout.quote_token_expired':\n 'The price you approved has expired. Review the updated total, then check out again.',\n 'errors.checkout.quote_token_stale':\n 'Your cart changed after the price was approved. Review the updated total, then check out again.',\n 'errors.checkout.price_increased_since_quote':\n 'The price of your cart went up after you approved the total. Review the updated total, then check out again.',\n};\n\n/**\n * The shop's checkout MAXIMUM, read off a settled quote and turned into the one\n * sentence every cart surface prints and the hand-off refuses with.\n *\n * PAYMENT PATH. `cart_max_exceeded` is advisory in the sense that the quote\n * still prices and returns the cart — but it is the server stating that\n * `/from-cart` WILL refuse this cart, and the limit itself rides along on\n * `cart_maximum_checkout`. This theme read neither: the checkout button stayed\n * live because a quote existed, the click came back as\n * `cart_maximum_checkout_exceeded`, and the buyer was never told what the limit\n * was or that they were over it.\n *\n * Null means \"within the limit\", and only because the producer OMITS the flag\n * in exactly that case. It is not a silence being read as permission: the flag\n * never authorises a checkout, it only ever stops one, so its absence leaves\n * every other guard doing its own job.\n *\n * The amount is printed through the same formatter the totals use, so the limit\n * and the total it sits beside are spelled the same way. Wording matched to the\n * Easy lane's `cart.maxExceeded`, because both lanes are stating one contract.\n * A flag set without a readable amount still BLOCKS — the sentence simply\n * cannot name a number rather than inventing one.\n */\nfunction resolveCartMaximumWarning(quote: SdkCartQuote): string | null {\n if (quote.cart_max_exceeded !== true) return null;\n const maximum = Number(quote.cart_maximum_checkout);\n return Number.isFinite(maximum) && maximum > 0\n ? `This cart is over the ${formatMoney(maximum, quote.currency)} checkout limit. `\n + 'Remove some items to continue.'\n : 'This cart is over this shop’s checkout limit. Remove some items to continue.';\n}\n\nfunction errorMessage(error: unknown, fallback: string): string {\n return error instanceof Error && error.message.trim() ? error.message : fallback;\n}\n\n/**\n * What kind of code the quote accepted, or null when it accepted none.\n *\n * PAYMENT PATH. The buyer types one code into one field, and only the SERVER can\n * say whether that string is one of the merchant's coupons or an affiliate's\n * referral — the theme has neither list. The quote answers with\n * `coupon_source`, and that answer decides which field checkout submits the code\n * under: a referral sent as `coupon` is a payout the affiliate never receives.\n *\n * Both signals have to hold. `coupon_applied` without a recognised source is a\n * shape this theme cannot act on: there is no field to submit an unclassified\n * code under, so it counts as not accepted rather than being guessed into one.\n * Mirrors `packages/storefront-commerce/src/sx/cart-store.ts`.\n */\nfunction readAcceptedCouponSource(quote: SdkCartQuote): SdkCartCodeSource | null {\n if (quote.coupon_applied !== true) return null;\n return quote.coupon_source === 'COUPON' || quote.coupon_source === 'AFFILIATE'\n ? quote.coupon_source\n : null;\n}\n\n/**\n * The code fields the checkout hand-off submits, read out of the SDK's OWN\n * storage. Null when storage holds a code this theme must not submit under\n * either field — see below.\n *\n * PAYMENT PATH, and the reason it reads storage rather than React state.\n *\n * The SDK keeps a cart code and its classification as ONE record and reads them\n * back as one: `setCartCoupon(code, source)` writes `{ code, source }`, and\n * `getStoredCartCode` returns null unless the code is non-empty AND the source\n * is one it recognises (`packages/sdk/src/modules/cart.ts`). So a stored code\n * always arrives with the answer to \"which field\", and it arrives NOW, at the\n * click — no round trip, nothing to wait for, and nothing that can be half\n * settled.\n *\n * It used to be decided from a `couponSource` state written by the quote, and\n * every gap between a stored code and a settled quote was a money bug. A cart\n * write publishes its lines before its quote resolves, and the checkout button\n * is live throughout: in that window the source was null while a code sat in\n * storage, so the hand-off sent an explicitly EMPTY `coupon`. That is not \"no\n * opinion\" — an empty string SUPPRESSES the SDK's own read of storage\n * (`resolveCheckoutCodes`) — so a returning buyer's stored `SAVE10` was dropped\n * from the invoice and they were charged full price. First paint and a second\n * tab applying a code opened the same window.\n *\n * A code stored WITHOUT a source cannot happen through the SDK's reader, which\n * refuses to hand one back. It is still answered rather than guessed, because\n * both guesses cost someone money: submitted as a coupon, the affiliate who\n * brought the sale is never credited; suppressed, the buyer loses their\n * discount. The hand-off stops and says so instead.\n */\n/**\n * One spelling for an ambient referral, so two reads of the SAME storage cannot\n * look different.\n *\n * The SDK lowercases the code on its way into the checkout request\n * (`normalizeAffiliateCode`), and storage holds whatever `?ref=` carried. The\n * comparison below decides whether a hand-off may proceed, so it compares the\n * codes as the REQUEST would spell them rather than as the URL did — otherwise\n * `?ref=QA-Partner` and `?ref=qa-partner` would read as a change of attribution\n * and stop a checkout nothing is wrong with.\n */\nfunction normalizeAmbientAffiliate(value: string | null | undefined): string {\n return value?.trim().toLowerCase() ?? '';\n}\n\nfunction resolveSubmittedCode(\n sdk: ShoppexSdk,\n): { coupon: string; affiliateCode?: string } | null {\n const code = sdk.getCartCoupon()?.trim() ?? '';\n // '' rather than undefined, in every branch: see `checkout`.\n if (!code) return { coupon: '' };\n const source = sdk.getCartCouponSource();\n if (source === 'COUPON') return { coupon: code };\n if (source === 'AFFILIATE') return { coupon: '', affiliateCode: code };\n return null;\n}\n\n/** Everything the SDK folds into a line's `line_id`, and nothing else. */\ntype CartLineIdentityInput = Pick<\n SdkCartItem,\n | 'product_id'\n | 'variant_id'\n | 'price_variant_id'\n | 'addons'\n | 'custom_fields'\n | 'price_data'\n | 'pay_what_you_want_price'\n>;\n\n/**\n * The SDK's cart-line identity, rebuilt field for field.\n *\n * `line_id` is a hash over exactly these seven fields, normalized exactly this\n * way — the SDK's `computeCartLineId` is not on the storefront global, so the\n * theme rebuilds the payload the hash is taken over and compares THAT. Comparing\n * the payload rather than a hash of it is the stricter of the two, which is the\n * right direction for a check that has to reject.\n *\n * Every field is load-bearing, and the price snapshot most of all: to the SDK\n * the same product at a new price is a NEW line, not more units of the old one.\n * A matcher that leaves one out reads back a line the write never touched, and\n * a dropped write borrows that line's units and passes.\n */\nfunction cartLineIdentity(input: CartLineIdentityInput): string {\n const addons = [...(input.addons ?? [])]\n // Addon quantity counts as much as the id: two of an extra is not one.\n .map((addon) => ({ id: addon.id, quantity: addon.quantity ?? 1 }))\n .sort((a, b) => a.id.localeCompare(b.id));\n const customFields = Object.fromEntries(\n Object.entries(input.custom_fields ?? {})\n .filter(([, value]) => typeof value === 'string')\n .sort(([a], [b]) => a.localeCompare(b)),\n );\n const unitPrice = input.price_data?.unit_price;\n const namedPrice = input.pay_what_you_want_price;\n // Absent and non-finite both normalize to null, exactly as the SDK does —\n // otherwise `undefined` (dropped by JSON.stringify) and an explicit null would\n // read as two identities for one line.\n return JSON.stringify({\n product_id: input.product_id,\n variant_id: input.variant_id,\n price_variant_id: input.price_variant_id ?? null,\n addons,\n custom_fields: customFields,\n unit_price: typeof unitPrice === 'number' && Number.isFinite(unitPrice) ? unitPrice : null,\n pay_what_you_want_price:\n typeof namedPrice === 'number' && Number.isFinite(namedPrice) ? namedPrice : null,\n });\n}\n\n/** One bundle member matched to the catalog product it actually refers to. */\ntype ResolvedBundleMember = {\n product: StorefrontProduct;\n variant: StorefrontPriceVariant | null;\n bounds: { min: number; max: number | null };\n};\n\n/**\n * One bundle member, resolved against the catalog it came from.\n *\n * A bundle member arrives as a compact summary — id, title, unit price — with\n * no bounds on it, but it IS an ordinary catalog product: the wire payload\n * carries every product, listed or not, so the member's real minimum, maximum\n * and stock are in there to be read. They have to be read, because the add\n * re-snapshots the line and the SDK adopts THIS write's bounds: an add that\n * sent none STRIPPED the cap off a line the buy box had capped, and the SDK's\n * read clamp then had nothing to hold the buyer to — the stepper carried that\n * line straight past the merchant's per-order maximum.\n *\n * The product itself is returned as well as its bounds, because two more\n * decisions need it: whether the merchant has PAUSED it, and whether it is sold\n * at a price the buyer names.\n *\n * Null when the member is not in the payload, or names a variant its product\n * does not have. A member that cannot be resolved is not written at all: a line\n * without real bounds is precisely the failure this exists to prevent.\n */\nfunction resolveBundleMember(item: ProductBundleItem): ResolvedBundleMember | null {\n // Read on demand rather than once at mount: the payload is parsed strictly\n // and may throw, and the storefront's own boot is where that error belongs.\n const product = getProducts().find((candidate) => candidate.id === item.id);\n if (product === undefined) return null;\n // 'default' is the no-price-variant marker, the same one the buy box sends.\n if (item.variantId === 'default') {\n return { product, variant: null, bounds: resolveQuantityBounds(product, null) };\n }\n const variant = product.priceVariants.find((candidate) => candidate.id === item.variantId);\n if (variant === undefined) return null;\n return { product, variant, bounds: resolveQuantityBounds(product, variant) };\n}\n\n/**\n * The quantity the SDK will actually store for a line it has just written.\n *\n * Mirrors the SDK's own clamp: floor to whole units, raise to the minimum, then\n * cap at the maximum (max wins on the degenerate min > max), never below one.\n * The bounds are the ones passed on THIS write — the theme sends\n * `replace_display_snapshot`, so the SDK adopts them before it clamps.\n */\nfunction clampToLineBounds(quantity: number, minimum: number, maximum: number | null): number {\n let clamped = Math.floor(quantity);\n if (clamped < minimum) clamped = Math.floor(minimum);\n if (maximum !== null && clamped > maximum) clamped = Math.floor(maximum);\n return Math.max(1, clamped);\n}\n\n/**\n * Whether an add landed, decided on TWO signals that have to hold together.\n *\n * PAYMENT PATH. Counting units by rebuilt line identity is not enough on its\n * own, because the SDK does not merge by identity — it merges by the STORED\n * `line_id`, and that value can be a supplied or legacy one that the identity\n * hash no longer reproduces (`packages/sdk/src/modules/cart.ts`: a stored\n * `line_id` is kept verbatim, and `addToCart` looks the line up by it). A cart\n * persisted on this domain before the merchant switched to this template can\n * therefore hold a line that is field-identical to the one being written and\n * that the SDK will NOT merge into. Its units are not this write's units, and a\n * check that counted them let a dropped write pass.\n *\n * So the add succeeded when EITHER:\n *\n * (a) the write changed the SDK's stored cart at all — the serialized cart is\n * not what it was one statement earlier. Any real add moves something: a\n * new line, more units, or a replaced display snapshot; or\n * (b) it changed nothing AND a field-matching line already sits exactly at the\n * ceiling this write's bounds impose. That is the one legitimate no-op —\n * the SDK clamps the add away — and only a line the SDK would actually\n * merge into can produce it, because a line it merges into is the only one\n * whose bounds this write has just replaced.\n *\n * Without a maximum there is no ceiling and (b) can never apply: every\n * successful add must then have changed the cart.\n *\n * Residual corner, stated rather than papered over: a field-identical line\n * under a FOREIGN legacy `line_id`, sitting at exactly the ceiling this write\n * sends, at the same moment the write is silently dropped, is still\n * indistinguishable from the legitimate no-op — both leave the cart untouched\n * with a matching line at the ceiling. Nothing the theme can read tells those\n * two apart; the real fix is the SDK follow-up in the README, where a refused\n * write should surface instead of being swallowed.\n */\nfunction unwrittenAdd(\n serializedBefore: string,\n identity: string,\n minimum: number,\n maximum: number | null,\n message: string,\n): string | null {\n const lines = getShoppex().getCart();\n if (JSON.stringify(lines) !== serializedBefore) return null;\n if (maximum === null) return message;\n const ceiling = clampToLineBounds(maximum, minimum, maximum);\n return lines.some((line) => cartLineIdentity(line) === identity && line.quantity === ceiling)\n ? null\n : message;\n}\n\n/**\n * Writes the buyer's per-product answers onto every cart line that asked for\n * them, and answers whether the cart is now safe to check out.\n *\n * PAYMENT PATH. Returns null on success, or the message to fail the hand-off\n * with — never a silent partial write.\n *\n * Two SDK behaviours shape this:\n *\n * - `custom_fields` is part of a line's IDENTITY. Writing it makes the SDK\n * recompute `line_id` and MERGE the line into any existing line that now\n * matches, so a line id captured before the loop can be gone by the time the\n * loop reaches it. That is not a failure — a line can only vanish INTO one\n * already carrying these exact answers — so it is skipped rather than\n * written, which is also what stops `updateCartItem` from throwing\n * \"Item not found in cart\" mid-way through.\n * - `updateCartItem` can drop a write in silence, exactly as `addToCart` can:\n * the SDK's storage wrapper catches a quota or blocked-storage error and only\n * logs it. So the cart is READ BACK afterwards and every required value is\n * re-validated against what actually landed. Without that, a buyer who filled\n * the form in would be sent to a checkout the backend refuses outright.\n *\n * A merge is harmless to the ANSWERS and never harmless to the PRICE: it sums\n * two lines' quantities, and quantities are hashed into the quote proof this\n * hand-off is about to present. So the cart is compared across the write and a\n * merge stops the hand-off — see `checkoutFieldWriteRepriced`.\n */\nfunction writeCheckoutFieldsToCart(values: CheckoutFieldValues): string | null {\n const sdk = getShoppex();\n const fields = resolveCheckoutFields(sdk.getCart());\n if (!fields.resolved) {\n return 'Some products in your cart are no longer available. Please review your cart.';\n }\n if (fields.groups.length === 0) return null;\n\n const payloads = new Map(fields.groups.map((group) => [\n group.productId,\n buildCustomFieldPayload(group.fields, values[group.productId]),\n ]));\n // The cart as it stood BEFORE any answer landed, and the list this pass walks.\n // One read for both, so the comparison below cannot be about a different cart\n // than the one that was written.\n const before = sdk.getCart();\n for (const line of before) {\n const payload = payloads.get(line.product_id);\n if (payload === undefined) continue;\n // Merged away by an earlier write in this pass — see above.\n if (!sdk.getCart().some((entry) => entry.line_id === line.line_id)) continue;\n sdk.updateCartItem(line.line_id, { custom_fields: payload });\n }\n\n const after = sdk.getCart();\n const missing = missingRequiredCheckoutValues(fields.groups, after);\n if (missing.length > 0) {\n return `Your details could not be saved to the cart: ${missing.join(' ')}`;\n }\n // A merge changed the quantities the current quote — and the proof it minted —\n // were taken for. See `checkoutFieldWriteRepriced`.\n return checkoutFieldWriteRepriced(before, after)\n ? 'Two lines of the same item were combined once your details were saved. '\n + 'Review the updated total, then check out again.'\n : null;\n}\n\n/** A line's stored id and units, as they stood before one write. */\ntype CartLineSnapshot = { lineId: string; quantity: number };\n\n/**\n * How to take one member add back out again: restore `quantity` on the line it\n * merged into, or remove the line outright when the add created it.\n */\ntype BundleMemberUndo = { lineId: string; quantity: number | null };\n\nfunction snapshotLines(lines: readonly SdkCartItem[]): CartLineSnapshot[] {\n return lines.map((line) => ({ lineId: line.line_id, quantity: line.quantity }));\n}\n\n/**\n * What one member add did to the cart, read as the difference across it.\n *\n * Matched on the SDK's stored `line_id` rather than on rebuilt identity for the\n * same reason the read-back cannot rely on identity alone: the id is what the\n * SDK merges by, so it is the only thing that names the line this write\n * actually touched. Null when the add moved nothing — the at-ceiling no-op —\n * which is exactly right: there is nothing to undo.\n */\nfunction diffBundleMemberAdd(\n before: readonly CartLineSnapshot[],\n after: readonly SdkCartItem[],\n): BundleMemberUndo | null {\n const created = after.find((line) => !before.some((entry) => entry.lineId === line.line_id));\n if (created !== undefined) return { lineId: created.line_id, quantity: null };\n for (const line of after) {\n const previous = before.find((entry) => entry.lineId === line.line_id);\n if (previous !== undefined && line.quantity > previous.quantity) {\n return { lineId: line.line_id, quantity: previous.quantity };\n }\n }\n return null;\n}\n\nexport function CartProvider({ children }: { children: ReactNode }) {\n const [lines, setLines] = useState<SdkCartItem[]>([]);\n const [count, setCount] = useState(0);\n const [quote, setQuote] = useState<SdkCartQuote | null>(null);\n const [quoteError, setQuoteError] = useState<string | null>(null);\n const [mutationError, setMutationError] = useState<string | null>(null);\n const [couponRejected, setCouponRejected] = useState(false);\n const [quotedAmbientAffiliate, setQuotedAmbientAffiliate] = useState(false);\n const [cartMaxWarning, setCartMaxWarning] = useState<string | null>(null);\n const [checkoutError, setCheckoutError] = useState<string | null>(null);\n const [selfReferralBlocked, setSelfReferralBlocked] = useState(false);\n /**\n * The ambient referral the CURRENT quote was priced with: `''` when it was\n * priced without one, and null when there is no settled quote at all.\n *\n * PAYMENT PATH, and the reason it is a ref rather than only the boolean above.\n * The quote reads the referral at REFRESH time; `sdk.checkout()` reads the\n * same storage again at CLICK time and submits whatever it finds\n * (`normalizeAffiliateCode` in the SDK). Between those two reads a second tab\n * on the same domain can arrive with a different `?ref=`, or clear the\n * attribution — and the invoice then carries a discount the cart never showed,\n * or drops one it did. So the code the quote used has to survive until the\n * click, next to the quote it belongs to, and be compared against storage\n * before the hand-off. A ref, because the comparison must read what is true\n * NOW: `checkout` is also called from Buy Now after an await, whose closure was\n * captured a render earlier.\n */\n const quotedAffiliate = useRef<string | null>(null);\n /**\n * The stored `line_id`s the CURRENT quote refused (`sellable: false`).\n *\n * PAYMENT PATH. The server's own statement that this cart cannot be ordered as\n * it stands — the option is unorderable, or the cart's lines of one option\n * together ask for more units than its ceiling, which `/from-cart` refuses on\n * the SUM. A settled total is not permission to order units the shelf does not\n * have, so the hand-off stops on it rather than spending the buyer's click on\n * a rejection that arrives from the checkout naming no line.\n *\n * A ref rather than state for the same reason `quotedAffiliate` is one:\n * `checkout` reads it at CLICK time and its closure was captured a render\n * earlier — a Buy Now that adds and hands off in the same click would\n * otherwise consult the verdict for the cart as it stood before the add.\n */\n const unsellableLines = useRef<string[]>([]);\n /**\n * The checkout-maximum verdict of the CURRENT quote, as the sentence the\n * buyer is shown, or null while the cart is within the limit.\n *\n * PAYMENT PATH, and a ref for the same reason `unsellableLines` is one:\n * `checkout` reads it at CLICK time out of a closure captured a render\n * earlier, and Buy Now adds a line and hands off in the same click.\n */\n const cartMaximumWarning = useRef<string | null>(null);\n const [busyCount, setBusyCount] = useState(0);\n const [couponInput, setCouponInput] = useState('');\n const [drawerOpen, setDrawerOpen] = useState(false);\n /**\n * True while the field holds a code the buyer typed but has not applied yet.\n * Every cart mutation refreshes, and the refresh writes the STORED coupon\n * back into the field — so changing a quantity after typing a code silently\n * erased it, and a buyer who did not notice paid full price.\n */\n const couponPendingEdit = useRef(false);\n const refreshVersion = useRef(0);\n const refreshTimer = useRef<number | null>(null);\n const refreshResolvers = useRef<Array<() => void>>([]);\n\n /**\n * Records which referral priced the quote now on screen, in ONE statement.\n *\n * The ref the checkout hand-off checks and the flag the coupon row renders are\n * two views of the same fact, and a site that wrote only one of them would let\n * them disagree — a cart claiming a referral it did not quote with, or a\n * hand-off comparing against a code no quote used.\n */\n const rememberQuotedAffiliate = useCallback((code: string | null) => {\n quotedAffiliate.current = code;\n setQuotedAmbientAffiliate(code !== null && code !== '');\n }, []);\n\n /**\n * Records the checkout-maximum verdict of the quote now on screen, in ONE\n * statement — for the same reason `rememberQuotedAffiliate` exists. The ref\n * the hand-off refuses on and the sentence the summary prints are two views\n * of one fact, and a site that wrote only one of them would let a cart be\n * blocked with nothing on screen saying why, or warned about a limit the\n * click no longer stops on.\n */\n const rememberCartMaximum = useCallback((warning: string | null) => {\n cartMaximumWarning.current = warning;\n setCartMaxWarning(warning);\n }, []);\n\n /**\n * Settles ONE accepted quote over the cart it priced.\n *\n * PAYMENT PATH, and the reason it is a function rather than a passage inside\n * `refreshNow`. A quote reaches this cart down TWO paths — the refresh, and\n * `applyCoupon` settling the candidate quote that accepted the buyer's code —\n * and they carry identical payloads. The apply path used to settle\n * `result.data` on its own, which meant the cart kept the PREVIOUS verdict:\n * a cart that had just become unsellable stayed checkout-enabled until the\n * backend rejected the click, and one that had been replenished stayed\n * blocked by a verdict that no longer held. Two copies of this sequence is\n * what produced that, so there is one.\n *\n * The whole sequence, in order:\n *\n * - THE CART BOUNDS CONTRACT (`CartQuoteLine`, read by\n * `cart-quote-bounds.ts`). Cart lines hold an ADD-TIME SNAPSHOT of\n * their bounds, so a line goes stale the moment stock moves or the merchant\n * edits the limits, and a line restored from an older cart carries none at\n * all. The stepper is driven off those stored values, so a buyer could raise\n * a line past a ceiling that had shrunk under them and only find out at\n * checkout. Every settled quote restates the bounds from the CURRENT row and\n * shelf, through the same resolution `/from-cart` refuses with — so this\n * heals them, REPLACING rather than merging: an absent `max_quantity` means\n * the ceiling was removed, and a merge would keep a stale one forever.\n * - the refused lines the hand-off reads at click time, from the HEALED cart.\n * - the shop's checkout maximum, which the same quote states.\n *\n * A quote that says nothing about lines leaves both the stored bounds and the\n * previous verdict alone. It is not an all-clear.\n *\n * The answer says what the CALLER still owes:\n * - `'settled'` — the quote is on screen and nothing further is needed;\n * - `'clamped'` — a heal moved a quantity, so this quote priced units the cart\n * no longer holds. Nothing is settled, and the caller must re-quote;\n * - `'superseded'` — a newer refresh owns the cart. Nothing was written and\n * nothing is owed;\n * - `'stale-cart'` — the cart moved under the round trip. Nothing was written,\n * and what the caller owes depends on who can answer for the cart that now\n * exists — see `pricedCart`.\n */\n const settleQuotedCart = useCallback((\n data: SdkCartQuote,\n /** The ambient referral THIS quote was priced with. See `refreshNow`. */\n quotedAmbient: string,\n /** The refresh generation this quote belongs to. */\n version: number,\n /**\n * The serialized cart this quote was TAKEN FOR, pinned by the caller in the\n * same synchronous step it asked for the quote.\n *\n * PAYMENT PATH. A quote is a round trip, and the cart lives in localStorage,\n * which every tab on the domain shares — so a second tab can add, remove or\n * re-quantify a line while this one waits, and `refreshVersion` cannot see\n * it. The generation guard only retires a quote once ANOTHER refresh has\n * started; a cross-tab write schedules that refresh through a debounce, and\n * `applyCoupon` bumps the generation itself and then settles under the new\n * one, so both leave a window in which a response for the OLD cart passes\n * the guard by construction.\n *\n * Nothing below may run on that response. The heals write bounds resolved\n * for the old cart — they can clamp a line the buyer just changed in the\n * other tab — and the totals, the refused-line verdict and the\n * checkout-maximum state all describe a cart that no longer exists, yet\n * stay on screen and arm the hand-off until some later quote replaces them.\n * So the pin is checked FIRST, before a single heal is written.\n */\n pricedCart: string,\n ): 'settled' | 'clamped' | 'superseded' | 'stale-cart' => {\n const sdk = getShoppex();\n if (JSON.stringify(sdk.getCart()) !== pricedCart) return 'stale-cart';\n /**\n * Both refusals are answered BEFORE the first heal is written.\n *\n * PAYMENT PATH. A response that may not settle may not write either. The\n * cart pin above catches a cart that moved under the round trip; this\n * catches one a NEWER refresh already owns, and the two are the same\n * question asked of different clocks. Asked after the heals, a superseded\n * response still wrote bounds through the SDK on its way to being told it\n * was superseded — harmless in effect, because bounds do not reprice and\n * the newer refresh restates them, but it is a write from an answer that\n * had already lost.\n */\n if (version !== refreshVersion.current) return 'superseded';\n const quotedLines = parseCartQuoteLines(data.lines);\n let unsellable: string[] | null = null;\n let healClamped = false;\n if (quotedLines.length > 0) {\n const beforeHeal = sdk.getCart();\n for (const heal of resolveCartBoundsHeals(beforeHeal, quotedLines)) {\n sdk.updateCartItem(heal.lineId, {\n min_quantity: heal.minQuantity,\n max_quantity: heal.maxQuantity,\n });\n }\n // Re-read: a heal clamps through the SDK, so the verdict and the lines on\n // screen must both come from the HEALED cart.\n const healedCart = sdk.getCart();\n healClamped = healedCart.length !== beforeHeal.length\n || healedCart.some((line, index) => line.quantity !== beforeHeal[index]?.quantity);\n unsellable = resolveUnsellableLineIds(healedCart, quotedLines);\n }\n /**\n * A heal that CLAMPED a quantity has changed the cart this quote priced.\n *\n * PAYMENT PATH. The total in hand was computed for the pre-heal units, and\n * the proof the SDK is holding is bound to that cart — so neither may\n * settle over the post-heal cart. The lines are re-published (the buyer has\n * to see what the cart now holds), the checkout stays disarmed, and the\n * caller is told to ask for ONE follow-up quote. Bounded there, because the\n * second pass finds the bounds already in agreement and heals nothing.\n *\n * Bounds themselves do not reprice anything — the proof is bound to a hash\n * of the cart's products, options and QUANTITIES — so a heal that moved no\n * quantity needs no re-quote and keeps the ceiling the buyer approved.\n */\n if (healClamped) {\n setLines(sdk.getCart());\n setCount(sdk.getCartItemCount());\n setQuote(null);\n rememberQuotedAffiliate(null);\n // The cart this warning described is gone with the clamped units; the\n // follow-up quote states the maximum for the cart that now exists.\n rememberCartMaximum(null);\n unsellableLines.current = unsellable ?? [];\n return 'clamped';\n }\n setQuote(data);\n setQuoteError(null);\n setCheckoutError(clearRecoveredQuoteCheckoutError);\n rememberQuotedAffiliate(quotedAmbient);\n rememberCartMaximum(resolveCartMaximumWarning(data));\n if (unsellable !== null) unsellableLines.current = unsellable;\n // The heals above may have rewritten stored bounds without moving a\n // quantity; the stepper reads them off the published lines.\n if (quotedLines.length > 0) setLines(sdk.getCart());\n return 'settled';\n }, [rememberCartMaximum, rememberQuotedAffiliate]);\n\n /**\n * Re-reads the SDK's cart and re-quotes it.\n *\n * It owns `quoteError` and retires only the checkout refusal caused by the\n * missing quote once pricing settles. `mutationError` and every unrelated\n * checkout error stay out of its reach: a refresh is not a buyer telling the\n * theme anything, and a quote that answers after a failed write must not wipe\n * the only notice that the write failed. A cart write error is cleared by the\n * NEXT user-initiated mutation (see `mutate`), never by a refresh that happens\n * to run after it.\n */\n const refreshNow = useCallback(async (retriesLeft = 1) => {\n const sdk = getShoppex();\n const version = ++refreshVersion.current;\n const nextLines = sdk.getCart();\n const nextCount = sdk.getCartItemCount();\n /**\n * The cart this refresh is about to ask about, pinned for the settlement.\n *\n * PAYMENT PATH. Taken here rather than at the quote call because only\n * synchronous state writes separate the two, so nothing can move the cart in\n * between — and taken at all because `refreshVersion` does not close this\n * window on its own: a cross-tab write reaches this tab as a `storage` event\n * that schedules a DEBOUNCED refresh, so for the length of that debounce no\n * newer generation exists and a response for the pre-write cart still passes\n * the guard. See `settleQuotedCart`'s `pricedCart`.\n */\n const pricedCart = JSON.stringify(nextLines);\n const coupon = sdk.getCartCoupon()?.trim() ?? '';\n /**\n * The referral the buyer arrived with, recorded — never SUBMITTED — here.\n *\n * PAYMENT PATH. `?ref=` is captured into SDK storage at boot, and the SDK\n * carries it on its OWN `affiliate_code` field in both directions: it sends\n * it with every quote (`getAmbientAffiliateCodeForQuote`) and again with the\n * checkout request. That is what keeps the discount the cart displays equal\n * to the one the invoice charges, and it needs nothing from this theme.\n *\n * What this theme must NOT do is pass it a second time as the quote's\n * `coupon`. Doing so bound the signed quote proof to a requested COUPON as\n * well as a requested affiliate, while the hand-off submits only the\n * affiliate — so `/from-cart` answered `quote_token_stale`, the cart\n * re-quoted, and the identical mismatch came straight back. A buyer whose\n * ambient code no longer resolves (deactivated, expired, mistyped) had no\n * control to clear it and could not check out at all.\n *\n * So it is read for ONE purpose: to remember which referral priced the quote\n * now on screen, so the hand-off can refuse if storage has moved since. It\n * never reaches `couponInput` — the field is the buyer's — and a refusal of\n * it is not a verdict the buyer needs: the quote simply comes back without\n * the discount, no message is raised, and the code is left in storage\n * untouched, because checkout may still accept it under conditions this\n * quote does not describe.\n *\n * Blank while the buyer has a code APPLIED: the quote then prices that code,\n * the hand-off is not in the ambient lane, and the ambient attribution rides\n * along on the invoice exactly as it always has.\n */\n const ambientAffiliate = coupon ? '' : (sdk.getAffiliateCode()?.trim() ?? '');\n\n if (version === refreshVersion.current) {\n setLines(nextLines);\n setCount(nextCount);\n // Never overwrite what the buyer is still typing — and never write the\n // ambient referral here: the field is the buyer's, and a code appearing\n // in it that they did not type reads as one they can edit or remove.\n if (!couponPendingEdit.current) setCouponInput(coupon);\n }\n\n if (nextLines.length === 0) {\n if (version === refreshVersion.current) {\n setQuote(null);\n setQuoteError(null);\n setCheckoutError(clearRecoveredQuoteCheckoutError);\n rememberQuotedAffiliate(null);\n rememberCartMaximum(null);\n unsellableLines.current = [];\n }\n return;\n }\n\n try {\n // ONLY the code the buyer stored. The ambient referral rides the SDK's own\n // `affiliate_code` field — see `ambientAffiliate` above.\n const result = await sdk.quoteCart(coupon || undefined, getBuyerCurrency());\n if (version !== refreshVersion.current) return;\n if (!result.success || !result.data) {\n setQuote(null);\n setQuoteError(result.message?.trim() || 'Cart pricing is unavailable.');\n rememberQuotedAffiliate(null);\n rememberCartMaximum(null);\n unsellableLines.current = [];\n return;\n }\n // Bounds healing, the refused-line verdict and the checkout maximum, all\n // through the ONE implementation the apply path also runs. See\n // `settleQuotedCart`.\n //\n // A `'stale-cart'` answer is DROPPED rather than re-quoted, and that is\n // the whole of what this path owes. Every writer that can move the cart\n // under a refresh schedules its own re-quote — a cross-tab write through\n // the `storage` listener, a same-tab write outside `mutate` through\n // `shoppex:cart-changed`, and `mutate` itself through `scheduleRefresh` —\n // so an answer for the cart that DOES exist is already on its way, and a\n // second one raced against it would only be redundant work.\n if (settleQuotedCart(result.data, ambientAffiliate, version, pricedCart) === 'clamped'\n && retriesLeft > 0) {\n await refreshNow(retriesLeft - 1);\n }\n } catch (error) {\n if (version !== refreshVersion.current) return;\n setQuote(null);\n setQuoteError(errorMessage(error, 'Cart pricing is unavailable.'));\n rememberQuotedAffiliate(null);\n rememberCartMaximum(null);\n unsellableLines.current = [];\n }\n }, [rememberCartMaximum, rememberQuotedAffiliate, settleQuotedCart]);\n\n // Cart mutations arrive in bursts (a stepper held down, a bundle added row by\n // row). Coalescing them into one quote keeps the displayed total from\n // flickering through intermediate states.\n const scheduleRefresh = useCallback(() => new Promise<void>((resolve) => {\n refreshResolvers.current.push(resolve);\n if (refreshTimer.current !== null) {\n window.clearTimeout(refreshTimer.current);\n }\n refreshTimer.current = window.setTimeout(() => {\n refreshTimer.current = null;\n const resolvers = refreshResolvers.current.splice(0);\n void refreshNow().finally(() => {\n resolvers.forEach((finish) => finish());\n });\n }, 60);\n }), [refreshNow]);\n\n useEffect(() => {\n void refreshNow();\n return () => {\n if (refreshTimer.current !== null) window.clearTimeout(refreshTimer.current);\n refreshResolvers.current.splice(0).forEach((resolve) => resolve());\n };\n }, [refreshNow]);\n\n /**\n * Cart writes this context did not make.\n *\n * The cart lives in localStorage, which is shared across every tab on the\n * domain: a buyer with the shop open twice who checks out — or just adds a\n * line — in one tab left the other showing a cart that no longer exists, and\n * its checkout button would then hand off a stale one. `storage` is the\n * browser's own cross-tab signal; `shoppex:cart-changed` is the SDK's, for a\n * write inside THIS tab that did not go through `mutate` (the SDK's own\n * navigation clears the cart after a successful hand-off). Both are the pair\n * the Easy lane's cart store listens to (`connectSxCartStore`).\n *\n * A re-quote follows the re-read for the same reason it does there: the lines\n * changed, so the totals on screen are no longer the quote's.\n */\n useEffect(() => {\n const onExternalChange = () => {\n void scheduleRefresh();\n };\n window.addEventListener('storage', onExternalChange);\n window.addEventListener('shoppex:cart-changed', onExternalChange);\n return () => {\n window.removeEventListener('storage', onExternalChange);\n window.removeEventListener('shoppex:cart-changed', onExternalChange);\n };\n }, [scheduleRefresh]);\n\n /**\n * Runs one SDK cart write, checks it landed, then re-quotes.\n *\n * PAYMENT PATH. A dropped write has TWO shapes and both have to reject:\n *\n * - the SDK throws (a rejected quantity, a line id it cannot find), and\n * - the SDK returns normally having stored nothing. Its localStorage wrapper\n * catches a quota or blocked-storage failure and only logs it, so the write\n * call itself is silent. `verifyWrite` reads the cart back out of the SDK\n * and reports that silence as the failure it is.\n *\n * The rejection is the contract, not an oversight: a caller that CHAINS on the\n * write — Buy Now adds a line and then hands off to checkout — has to be able\n * to stop. A resolved promise after a failed write told Buy Now the line\n * existed, and the buyer was sent to an invoice built from whatever the cart\n * held before, which is the wrong lines and the wrong money.\n *\n * The message is still recorded for the cart surfaces, so the failure is both\n * visible and unignorable. There is no retry here — a write the SDK dropped is\n * reported, never quietly attempted again behind the buyer's back.\n *\n * The failure is recorded in `mutationError`, which only a mutation writes:\n * it is cleared here, at the start of the NEXT buyer-initiated write, and\n * nowhere else. A refresh cannot clear it — see `refreshNow`.\n */\n const mutate = useCallback(async (\n mutation: () => void,\n /**\n * Reads the SDK's cart back and returns the message to fail with when the\n * write is not in it. Called once, immediately after the write, with the\n * serialized cart as it stood one statement BEFORE the write.\n */\n verifyWrite?: (serializedBefore: string) => string | null,\n ) => {\n setBusyCount((value) => value + 1);\n setCheckoutError(null);\n setMutationError(null);\n // A refused code's message survives every refresh (it has to — the code is\n // gone from storage by then), so THIS is what retires it: the next thing the\n // buyer does with their cart.\n setCouponRejected(false);\n try {\n // Taken here rather than at the call site so nothing can slip between the\n // snapshot and the write it is compared against.\n const serializedBefore = verifyWrite ? JSON.stringify(getShoppex().getCart()) : '';\n mutation();\n const unwritten = verifyWrite?.(serializedBefore) ?? null;\n // Refreshed either way: a write that vanished still has to leave every\n // cart surface showing the cart that DOES exist.\n await scheduleRefresh();\n if (unwritten) throw new Error(unwritten);\n } catch (error) {\n setMutationError(errorMessage(error, 'The cart could not be updated.'));\n throw error;\n } finally {\n setBusyCount((value) => Math.max(0, value - 1));\n }\n }, [scheduleRefresh]);\n\n const add = useCallback(async (\n product: StorefrontProduct,\n variant: StorefrontPriceVariant | null,\n quantity: number,\n options?: { payWhatYouWantPrice?: number; addons?: ProductAddon[] },\n ) => {\n if (product.onHold) {\n // PAYMENT PATH. Every surface that offers this product already disables\n // its controls, so this is the single stop behind all of them: a paused\n // product is one the backend refuses to sell, and a line the buyer cannot\n // check out is worse than a button that never worked.\n throw new Error(`${product.name} is not on sale yet.`);\n }\n if (isSelectionSoldOut(product, variant)) {\n // PAYMENT PATH, same stop one step further: the SELECTED option can be out\n // of stock while its product still has units in a sibling. The backend\n // refuses that line at invoice creation, so accepting it here would only\n // move the dead end to checkout — and the buyer would have paid attention\n // to a cart badge that meant nothing.\n throw new Error(\n `${variant === null ? product.name : `${product.name} (${variant.label})`} is out of stock.`,\n );\n }\n const { min: minimum, max: maximum } = resolveQuantityBounds(product, variant);\n const addons = withRequiredAddons(product, options?.addons ?? []);\n /**\n * The buyer's named price for a pay-what-you-want product.\n *\n * PAYMENT PATH. Only the buy box has a field for it; a catalog card, a\n * bundle-group dialog and a related-product tile all add without one. They\n * used to send NO named price at all, and that was wrong twice over: the\n * backend charges such a line at the minimum anyway, so the amount was\n * decided in silence — and `pay_what_you_want_price` is part of the SDK's\n * line identity, so a card add and a buy-box add of the SAME product at the\n * SAME price landed on two different cart lines. The buyer saw one product\n * twice and neither row counted the other's units.\n *\n * So an add without an amount states the floor explicitly: the option's\n * UNDISCOUNTED unit price, which is the number the backend validates\n * against (`resolvePayWhatYouWantUnitPrice`) and the same floor the buy box\n * pre-fills. Same product, same price, one line.\n */\n const payWhatYouWantPrice = options?.payWhatYouWantPrice\n ?? (product.payWhatYouWant ? resolvePayWhatYouWantMinimum(product, variant) : undefined);\n // The price the cart line remembers. Without it the cart can only show a\n // line total of zero until the server quote lands, so it is captured here —\n // on the one path every add goes through — rather than in each caller.\n // For pay-what-you-want the buyer's amount replaces the product price and\n // the extras are added on top exactly once, the same split the backend\n // charges.\n const addonsUnitPrice = addons.reduce((sum, addon) => sum + addon.price, 0);\n const unitPriceSnapshot = payWhatYouWantPrice !== undefined\n ? payWhatYouWantPrice + addonsUnitPrice\n : calculateLinePricing(product, variant, addons, quantity).effectiveUnitPrice;\n const variantId = variant?.id ?? 'default';\n const addOptions: SdkCartAddOptions = {\n ...(variant ? { price_variant_id: variant.id } : {}),\n price_data: { unit_price: unitPriceSnapshot },\n ...(payWhatYouWantPrice !== undefined\n ? { pay_what_you_want_price: payWhatYouWantPrice }\n : {}),\n // Addon ids are part of the line's identity — the SDK hashes them into\n // `line_id`, so a line added with extras never merges with a plain one.\n ...(addons.length > 0\n ? {\n addons: addons.map((addon) => ({ id: addon.id })),\n addon_labels: addons.map((addon) => addon.title),\n }\n : {}),\n title: product.name,\n variant_title: variant?.label,\n image_url: variant?.imageUrl ?? product.imageUrl ?? undefined,\n max_quantity: maximum ?? undefined,\n min_quantity: minimum > 1 ? minimum : undefined,\n replace_display_snapshot: true,\n };\n // The identity is read off the SAME object the write sends, so the two can\n // never describe different lines.\n const identity = cartLineIdentity({ product_id: product.id, variant_id: variantId, ...addOptions });\n await mutate(\n () => getShoppex().addToCart(product.id, variantId, quantity, addOptions),\n (serializedBefore) => unwrittenAdd(\n serializedBefore,\n identity,\n minimum,\n maximum,\n 'This product could not be added to your cart.',\n ),\n );\n }, [mutate]);\n\n const addBundleMember = useCallback(async (item: ProductBundleItem) => {\n const member = resolveBundleMember(item);\n if (member === null || member.product.onHold || isSelectionSoldOut(member.product, member.variant)) {\n // Fail fast, for three reasons that end the same way. A member whose real\n // bounds cannot be read must not be written as a line the merchant's\n // per-order limits no longer apply to; a member the merchant has PAUSED\n // must not be sold at all; a member whose selected option is OUT OF STOCK\n // is a line the backend refuses at invoice creation. A bundle missing any\n // of them can never unlock its discount anyway. `addBundle` turns any of\n // them into the one error the buyer sees, after it has taken the earlier\n // members back out.\n throw new Error(`${item.title} is not available and was not added to your cart.`);\n }\n const { bounds } = member;\n // A pay-what-you-want member has no named price in the offer, and the\n // backend charges the undiscounted minimum for a line without one. Stating\n // that floor explicitly is what puts this add on the SAME cart line the\n // buyer's own add of that product produces — `pay_what_you_want_price` is\n // part of the line identity, so a silent one splits the line in two. See\n // `add`.\n const payWhatYouWantPrice = member.product.payWhatYouWant\n ? resolvePayWhatYouWantMinimum(member.product, member.variant)\n : undefined;\n // PAYMENT PATH: a bundle member carries its required add-ons like any other\n // line (see `withRequiredAddons`), and their price is part of its snapshot.\n const requiredAddons = withRequiredAddons(member.product, []);\n const addonsUnitPrice = requiredAddons.reduce((sum, addon) => sum + addon.price, 0);\n const addOptions: SdkCartAddOptions = {\n ...(item.variantId !== 'default' ? { price_variant_id: item.variantId } : {}),\n // The offer states each member's unit price; a line without that\n // snapshot shows no money in the cart until the quote answers. A named\n // price overrides it, because that is the amount the backend charges.\n ...(payWhatYouWantPrice !== undefined\n ? { price_data: { unit_price: payWhatYouWantPrice + addonsUnitPrice }, pay_what_you_want_price: payWhatYouWantPrice }\n : item.unitPrice > 0 ? { price_data: { unit_price: item.unitPrice + addonsUnitPrice } } : {}),\n ...(requiredAddons.length > 0\n ? {\n addons: requiredAddons.map((addon) => ({ id: addon.id })),\n addon_labels: requiredAddons.map((addon) => addon.title),\n }\n : {}),\n title: item.title,\n ...(item.variantTitle ? { variant_title: item.variantTitle } : {}),\n ...(item.imageUrl ? { image_url: item.imageUrl } : {}),\n // The member's OWN bounds, sent for the same reason the buy box sends\n // them: this write is a full re-snapshot, so whatever it leaves out is\n // CLEARED off the line it merges into.\n max_quantity: bounds.max ?? undefined,\n min_quantity: bounds.min > 1 ? bounds.min : undefined,\n replace_display_snapshot: true,\n };\n // Same exact identity as the buy box uses, read off the same object that is\n // written — a bundle member is a cart line like any other, and matching it\n // on anything less would let an unrelated line answer for this write.\n const identity = cartLineIdentity({\n product_id: item.id,\n variant_id: item.variantId,\n ...addOptions,\n });\n // Checked by exactly the rule the buy box's add is checked by, and only\n // because the bounds above are the member's real ones: the write has to have\n // moved the cart, or this line has to be sitting at the ceiling those bounds\n // impose. See `unwrittenAdd`.\n await mutate(\n () => getShoppex().addToCart(item.id, item.variantId, 1, addOptions),\n (serializedBefore) => unwrittenAdd(\n serializedBefore,\n identity,\n bounds.min,\n bounds.max,\n 'This item could not be added to your cart.',\n ),\n );\n }, [mutate]);\n\n /**\n * Adds a whole bundle offer, all of it or none of it.\n *\n * PAYMENT PATH. The members were added independently and in parallel, and\n * both halves of that were wrong. A member that failed after its siblings\n * landed left the buyer holding PART of a bundle — full price for products\n * they only wanted as a set, since the discount the offer promised is applied\n * by the backend only for the complete set. And because every sibling write\n * refreshed the cart, a sibling that succeeded after the failure wiped the\n * failure off the screen, so the buyer was not even told.\n *\n * So the members go in one at a time, and the first failure ends the\n * operation: everything THIS click already put in comes back out, in reverse\n * order, and the buyer is told once that the bundle could not be added. The\n * undo is derived from the cart itself around each write (`diffBundleMemberAdd`)\n * rather than assumed, so a member that merged into a line the buyer already\n * had gives that line its units back instead of the line being removed with\n * them.\n *\n * Undo is best effort by necessity — it runs on a cart that has just proven it\n * can refuse a write. A failed undo cannot be reported as a second error\n * without contradicting the first, so it is not: the error stands, the refresh\n * that follows puts every surface back on the cart that DOES exist, and the\n * buyer reads it there.\n */\n const addBundle = useCallback(async (items: readonly ProductBundleItem[]) => {\n const undo: BundleMemberUndo[] = [];\n // Held for the whole sequence, so the offer's buttons stay disabled across\n // the members instead of flickering back to enabled between them.\n setBusyCount((value) => value + 1);\n // This click is the buyer-initiated action that clears the last one's\n // failure. Stated here as well as in `mutate`, because a bundle whose first\n // member cannot be resolved never reaches a write.\n setCheckoutError(null);\n setMutationError(null);\n try {\n for (const item of items) {\n const before = snapshotLines(getShoppex().getCart());\n try {\n await addBundleMember(item);\n } catch (error) {\n // The failing member is diffed too: whatever partial effect it did\n // leave belongs to this operation and comes back out with the rest.\n const partial = diffBundleMemberAdd(before, getShoppex().getCart());\n if (partial !== null) undo.push(partial);\n const message = 'This bundle could not be added to your cart.';\n setMutationError(message);\n for (const entry of [...undo].reverse()) {\n try {\n if (entry.quantity === null) getShoppex().removeFromCart(entry.lineId);\n else getShoppex().updateCartItem(entry.lineId, { quantity: entry.quantity });\n } catch {\n // Best effort, and deliberately silent — see above.\n }\n }\n // Runs AFTER the message is set, and must leave it standing: this is\n // the successful refresh that used to erase the failure.\n await scheduleRefresh();\n throw error instanceof Error ? error : new Error(message);\n }\n const applied = diffBundleMemberAdd(before, getShoppex().getCart());\n if (applied !== null) undo.push(applied);\n }\n } finally {\n setBusyCount((value) => Math.max(0, value - 1));\n }\n }, [addBundleMember, scheduleRefresh]);\n\n const update = useCallback(\n async (lineId: string, quantity: number) =>\n mutate(() => getShoppex().updateCartItem(lineId, { quantity })),\n [mutate],\n );\n const remove = useCallback(\n async (lineId: string) => mutate(() => getShoppex().removeFromCart(lineId)),\n [mutate],\n );\n const clear = useCallback(async () => mutate(() => getShoppex().clearCart()), [mutate]);\n\n const editCouponInput = useCallback((value: string) => {\n couponPendingEdit.current = true;\n setCouponInput(value);\n }, []);\n\n /**\n * Applies the code in the field — after the SERVER has said what it is.\n *\n * PAYMENT PATH, and the order of operations is the whole point.\n *\n * The code is quoted FIRST, as a candidate, and nothing is stored until that\n * quote comes back. Only then is it saved, together with the classification\n * the quote gave it (`coupon_source`): a merchant coupon and an affiliate\n * referral are typed into the same box and paid out completely differently,\n * and the SDK defaults an unclassified code to `'COUPON'` — so a referral\n * stored without its source is submitted as a coupon at checkout and the\n * affiliate is never credited for the sale they brought.\n *\n * A code the quote REFUSED is removed from storage and from the field. It\n * used to be kept: flagged as rejected for display, and then submitted at\n * checkout anyway, because checkout reads the stored code. The message that\n * explains the refusal is deliberately the only thing that survives — see\n * `couponRejected`.\n *\n * Both writes are conditional on storage not having moved under them. See the\n * stale check below.\n */\n const applyCoupon = useCallback(async () => {\n const coupon = couponInput.trim();\n // Submitted: the field may track the cart again.\n couponPendingEdit.current = false;\n if (!coupon) {\n await mutate(() => getShoppex().clearCartCoupon());\n return;\n }\n\n setBusyCount((value) => value + 1);\n setCheckoutError(null);\n setMutationError(null);\n setCouponRejected(false);\n try {\n const sdk = getShoppex();\n /**\n * What cart storage held when this apply started.\n *\n * PAYMENT PATH. The quote below is a round trip, and the code lives in\n * localStorage, which every tab on the domain shares. A second tab can\n * apply — or clear — a code while this one waits, and the answer that then\n * comes back was taken against a cart that no longer exists: its verdict,\n * its totals and its storage write all describe the code this tab sent,\n * not the code the buyer now has.\n *\n * Both of the writes below used to run regardless. A refusal of THIS\n * tab's code called `clearCartCoupon` and deleted the OTHER tab's accepted\n * one, so a buyer who had just applied a working code silently lost it and\n * paid full price; an acceptance overwrote it with the older code. So the\n * response is compared against storage before anything acts on it, and a\n * stale one is dropped in silence — the cross-tab write already scheduled\n * its own refresh (see the `storage` listener), which is what puts this\n * tab back on the cart that DOES exist.\n */\n const storedBefore = sdk.getCartCoupon();\n /**\n * What the CART held when this apply started — the second thing this round\n * trip has to pin, and for the same reason as the first.\n *\n * PAYMENT PATH. The code is not the only shared thing a second tab can\n * move while this quote is in flight: the cart is in the same\n * localStorage, and a line added, removed or re-quantified over there\n * makes the response describe a cart that no longer exists. Its totals,\n * its refused-line verdict and its checkout-maximum state are all for the\n * old cart, and its `lines` heal stored bounds that can clamp the very\n * line the buyer just changed in the other tab. `refreshVersion` cannot\n * catch it here: this path BUMPS the generation on acceptance (to retire a\n * refresh that quoted without the code) and then settles under the new\n * one, so the superseded check passes by construction.\n *\n * And unlike the code above, a stale answer here may NOT simply be\n * dropped. The cross-tab write does schedule its own refresh — but that\n * refresh quotes the STORED code, and this apply deliberately stores\n * nothing until the server has accepted: so it would price the cart\n * without the buyer's code and end their click in silence, spinner off,\n * code still in the box, no discount and no statement that it was ever\n * considered. A refusal the buyer can read is a different thing from an\n * apply that evaporated. So the answer is refused and the SAME question is\n * asked again, against the cart that now exists.\n *\n * Acceptance itself is cart-dependent — a minimum-order coupon resolves on\n * the cart it was quoted with — so the pin is checked BEFORE the verdict\n * is read, not just before the totals are settled.\n */\n let pricedCart = JSON.stringify(sdk.getCart());\n // The candidate quote. Nothing is persisted yet, so a refused code never\n // reaches storage in the first place.\n let result = await sdk.quoteCart(coupon, getBuyerCurrency());\n if (sdk.getCartCoupon() !== storedBefore) return;\n if (JSON.stringify(sdk.getCart()) !== pricedCart) {\n // ONE follow-up quote, bounded exactly as the clamp retry is: this is\n // the buyer's click, so it is worth re-asking, and it is one click, so\n // it is not worth chasing a cart another tab is still rewriting.\n pricedCart = JSON.stringify(sdk.getCart());\n result = await sdk.quoteCart(coupon, getBuyerCurrency());\n if (sdk.getCartCoupon() !== storedBefore) return;\n if (JSON.stringify(sdk.getCart()) !== pricedCart) {\n /**\n * Two round trips, both outrun. Nothing is settled and nothing is\n * stored — the code was never approved for any cart that still\n * exists — and the cart is handed back to the refresh path so what is\n * on screen is the cart that DOES exist, priced.\n *\n * The typed code is marked pending again so that refresh does not\n * erase it: the field is the buyer's, and this click ended without a\n * verdict, so it must end where it began — their code in the box, the\n * Apply control live, and no total claiming a discount nobody priced.\n */\n couponPendingEdit.current = true;\n await refreshNow();\n return;\n }\n }\n if (!result.success || !result.data) {\n // The server did not answer. That is a pricing failure, not a verdict on\n // the code: storing it would save something unapproved, and reporting it\n // as refused would blame the buyer for an outage.\n setQuote(null);\n setQuoteError(result.message?.trim() || 'Cart pricing is unavailable.');\n rememberQuotedAffiliate(null);\n rememberCartMaximum(null);\n return;\n }\n const source = readAcceptedCouponSource(result.data);\n if (source === null) {\n sdk.clearCartCoupon();\n setCouponInput('');\n setCouponRejected(true);\n // Re-quote without the code, so the totals stop reflecting a candidate\n // that was refused.\n await refreshNow();\n return;\n }\n sdk.setCartCoupon(coupon, source);\n // Any refresh already in flight quoted the cart WITHOUT this code, so its\n // answer is stale the moment the code is stored. Retiring it here stops it\n // landing afterwards and putting the pre-discount totals back on screen.\n refreshVersion.current += 1;\n /**\n * This quote IS the cart's quote now — it was taken with exactly the code\n * that has just been stored, so re-fetching it would be a second round\n * trip for the same answer. It therefore settles through the SAME\n * sequence a refresh's quote does.\n *\n * PAYMENT PATH. It used to be assigned straight onto the quote state,\n * which left every verdict the payload carries unread: the stored bounds\n * kept a ceiling the shelf no longer had, the refused-line list still\n * described the PREVIOUS quote's cart, and the checkout maximum was never\n * looked at at all. A cart that had just become unsellable stayed\n * checkout-enabled until the backend rejected the click, and one that had\n * been replenished stayed blocked by a verdict that no longer held.\n *\n * `''` for the referral, buyer-applied by definition: this is the one path\n * a typed code takes, so the quote on screen was priced with NO ambient\n * referral.\n *\n * `pricedCart` is the pin this path already checked. It cannot fail here —\n * only `readAcceptedCouponSource`, `setCartCoupon` and the generation bump\n * stand between the check and this call, none of which touch the cart, and\n * none of which yield — but it is passed rather than skipped because the\n * settlement, not its callers, is what must never write over a cart its\n * quote did not describe.\n */\n const settlement = settleQuotedCart(result.data, '', refreshVersion.current, pricedCart);\n // The field shows what was actually STORED — the SDK normalizes a code on\n // the way in (trimmed, uppercased), and that normalized string is the one\n // checkout submits. A chip showing the buyer's typing instead would name a\n // code slightly different from the one on the invoice.\n setCouponInput(sdk.getCartCoupon() ?? coupon);\n // A heal clamped a quantity, so this quote priced units the cart no\n // longer holds. One follow-up quote, exactly as the refresh path bounds\n // it — the second pass finds the bounds already in agreement.\n if (settlement === 'clamped') await refreshNow();\n } catch (error) {\n setQuote(null);\n setQuoteError(errorMessage(error, 'Cart pricing is unavailable.'));\n rememberQuotedAffiliate(null);\n rememberCartMaximum(null);\n } finally {\n setBusyCount((value) => Math.max(0, value - 1));\n }\n }, [couponInput, mutate, refreshNow, rememberCartMaximum, rememberQuotedAffiliate, settleQuotedCart]);\n\n const removeCoupon = useCallback(async () => {\n couponPendingEdit.current = false;\n await mutate(() => getShoppex().clearCartCoupon());\n }, [mutate]);\n\n /**\n * The way OUT of a self-referral refusal.\n *\n * PAYMENT PATH. `affiliate_code_not_applicable` is the only refusal the buyer\n * has no control for. The code is ambient: captured from `?ref=` at boot into\n * storage the SDK owns, never typed, never shown, and re-read on every quote\n * and every hand-off — so the cart re-submits it, and the backend refuses it,\n * for as long as it sits there. Without this, an affiliate who opens their own\n * shop through their own link cannot buy from it at all.\n *\n * The attribution is dropped through the SDK's own `clearAffiliateCode()`\n * rather than by deleting its storage key: the key, its shape and its TTL are\n * the SDK's, and a theme writing them directly is a second implementation of\n * something that already exists.\n *\n * Both lanes it can arrive on are cleared, because the refusal names the\n * `affiliate_code` field and both fill it:\n * - ambient — the stored `?ref=` code, cleared above.\n * - applied — a referral the buyer typed, which the quote classified as\n * `AFFILIATE` and which the hand-off submits under `affiliateCode`. A\n * merchant COUPON is deliberately left alone; it is not what was refused.\n *\n * Then re-quote: `refreshNow` re-reads storage that no longer has a referral,\n * prices the cart without one, and records `''` as the code this quote used —\n * which is exactly what the next hand-off pins onto the request (the SDK's\n * `''` tri-state, \"explicitly none\"). So the retry submits no referral at all,\n * and the total the buyer approves is the total they are charged.\n */\n const clearBlockedReferral = useCallback(async () => {\n const sdk = getShoppex();\n sdk.clearAffiliateCode();\n if (sdk.getCartCouponSource() === 'AFFILIATE') {\n sdk.clearCartCoupon();\n couponPendingEdit.current = false;\n setCouponInput('');\n }\n setSelfReferralBlocked(false);\n setCheckoutError(null);\n setCouponRejected(false);\n await refreshNow();\n }, [refreshNow]);\n\n /**\n * Hands the buyer over to Hosted Checkout.\n *\n * PAYMENT PATH. The theme never takes money and never decides a price: the\n * SDK posts the cart, the backend creates the invoice and returns the hosted\n * checkout URL, and the SDK navigates to it (and only then clears the cart).\n * Everything this function owns is what the cart submits, and the failure\n * surface.\n *\n * - The buyer's per-product details are written onto the CART LINES first,\n * because that is where the invoice reads them from (`custom_fields` per\n * line) — the checkout call has no field for them. A required value that\n * did not land aborts the hand-off rather than creating a request the\n * backend refuses with `custom_fields_validation_failed`.\n * - The code AND the field it is submitted under are read out of the SDK's\n * own storage, at click time. See `resolveSubmittedCode`.\n * - With no code in storage, an explicitly EMPTY coupon is sent. Omitting it\n * would not mean \"none\": the SDK then falls back to reading cart storage\n * itself, and this hand-off has already read it.\n * - There must be a SETTLED quote. An estimate is this theme's arithmetic,\n * not a price, and the invoice would be built from a different one.\n * - The AMBIENT referral the quote used must still be the one storage holds,\n * or the hand-off stops and re-quotes instead of pricing an invoice the\n * cart never showed. See the comparison below — and the pin under it, which\n * is what makes the comparison describe the request that is actually sent.\n * - The referral is ALWAYS named on the request, including as an empty string\n * when the quote was priced without one. Omitted, the SDK reads its own\n * referral storage instead; empty, it submits none. See the pin below.\n * - The currency is the one this page was priced in. Omitted, the SDK falls\n * back to reading `?currency=` off the URL, which a client-side navigation\n * has already dropped — the buyer would be charged in another currency than\n * the one on screen.\n * - A resolved result that is not an actual redirect is treated as a\n * FAILURE. Reporting success without navigating would leave the buyer on a\n * silent page believing an order exists.\n */\n const checkout = useCallback(async (customFieldValues?: CheckoutFieldValues) => {\n setBusyCount((value) => value + 1);\n setCheckoutError(null);\n // A new attempt is not the refused one: the exit control must not outlive\n // the message it belongs to.\n setSelfReferralBlocked(false);\n try {\n const sdk = getShoppex();\n /**\n * No settled quote, no hand-off.\n *\n * PAYMENT PATH. Without one the summary prints an ESTIMATE — the sum of\n * the stored price snapshots, with no coupon, no deal and no referral\n * applied (`buildCartTotals`) — and the invoice is priced by the server\n * from scratch. A pricing outage therefore let a buyer approve a number\n * this theme made up and be charged a different one, with the outage\n * notice sitting right above a live button.\n *\n * `quotedAffiliate.current` IS that fact: `rememberQuotedAffiliate` sets\n * it to null on an empty cart, a refused quote and a thrown one, and to a\n * string only when a quote answered for the lines on screen. The checkout\n * controls are disabled on the same condition, and this is the guard for\n * the paths that are not a button — Buy Now, and the details form.\n */\n if (quotedAffiliate.current === null) {\n setCheckoutError(QUOTE_UNAVAILABLE_CHECKOUT_ERROR);\n return;\n }\n /**\n * A line the quote REFUSED, no hand-off.\n *\n * PAYMENT PATH. `sellable: false` is the server's own statement that this\n * cart cannot be ordered as it stands, and it is the same cart the signed\n * quote proof describes — a settled total is not permission to order units\n * the shelf does not have. Handing off anyway spends the buyer's click on\n * a rejection that arrives from the checkout naming no line at all.\n *\n * NOT a re-quote loop: the cart has to CHANGE for this to clear, so no\n * re-quote is asked for here — the answer would be the same one. The\n * message stands until the buyer reduces or removes the line.\n */\n if (unsellableLines.current.length > 0) {\n setCheckoutError(\n 'Some items in your cart are not available in the quantity requested. '\n + 'Reduce or remove them, then check out again.',\n );\n return;\n }\n /**\n * A cart over the shop's checkout MAXIMUM, no hand-off.\n *\n * PAYMENT PATH, and stopped exactly the way the refused line above is.\n * The quote has already told this cart that `/from-cart` will refuse it\n * (`cart_max_exceeded`), so handing off anyway spends the buyer's click\n * on a `cart_maximum_checkout_exceeded` rejection that names no amount\n * and offers no way forward. The message names the limit instead, and it\n * is the same sentence the summary is printing next to the total.\n *\n * NOT a re-quote loop: the cart has to CHANGE for this to clear, so no\n * re-quote is asked for — the answer would be the same one. The message\n * stands until the buyer removes something.\n */\n if (cartMaximumWarning.current !== null) {\n setCheckoutError(cartMaximumWarning.current);\n return;\n }\n /**\n * The answers go onto the lines BEFORE anything is priced or submitted,\n * and a write that did not leave the cart submittable stops here.\n *\n * PAYMENT PATH. Two shapes of failure, one exit. A write the SDK dropped\n * leaves a required value off a line, which `/from-cart` refuses outright\n * as `custom_fields_validation_failed`. A write that MERGED two lines of\n * one product left every answer in place but summed their quantities,\n * which stales the signed quote proof this hand-off would have carried —\n * `/from-cart` refuses that as `quote_token_stale`, and the buyer pays a\n * round trip for a cart change they did not make.\n *\n * Both re-quote before returning: the cart on screen may have gained\n * answers, lost a line, or both, and every surface has to re-read what\n * actually exists so the total the buyer approves next is the one this\n * cart would be charged.\n */\n if (customFieldValues !== undefined) {\n const failure = writeCheckoutFieldsToCart(customFieldValues);\n if (failure !== null) {\n setCheckoutError(failure);\n await scheduleRefresh();\n return;\n }\n }\n const submitted = resolveSubmittedCode(sdk);\n if (submitted === null) {\n setCheckoutError(\n 'Your discount code could not be verified. Remove it, or apply it again, to continue.',\n );\n return;\n }\n /**\n * The quote on screen and the invoice must be priced with the SAME\n * referral.\n *\n * PAYMENT PATH. When nothing is applied, the hand-off submits no\n * affiliate code of its own and the SDK reads the ambient one out of its\n * storage at THIS moment (`normalizeAffiliateCode`), while the total the\n * buyer is looking at was priced with whatever that storage held when the\n * quote ran. Storage is shared across every tab on the domain: a second\n * tab arriving on `?ref=` — or one that cleared the attribution — moves\n * the invoice off the displayed total in either direction, and the buyer\n * approves a number the order does not use.\n *\n * So the two reads are compared, and a divergence stops the hand-off\n * rather than pricing it: the cart re-quotes with the referral that is\n * actually stored, and the buyer is asked to look at the new total. This\n * is the same compare-and-set the coupon path runs before it writes\n * (`applyCoupon`), applied to the code nobody typed.\n *\n * Only in the ambient lane. A code the buyer APPLIED is submitted\n * explicitly — as `coupon` or as `affiliateCode`, from storage this\n * hand-off already read — so the SDK never falls back for it and the\n * quote was priced with exactly that code.\n */\n const quotedAmbient = quotedAffiliate.current;\n const isAmbientLane = submitted.coupon === '' && submitted.affiliateCode === undefined;\n // ONE read of the ambient referral for the whole hand-off. The check\n // below and the code pinned onto the request must be the same read, or\n // the check answers about a value the request does not carry.\n const ambientNow = normalizeAmbientAffiliate(sdk.getAffiliateCode());\n if (isAmbientLane && ambientNow !== normalizeAmbientAffiliate(quotedAmbient)) {\n setCheckoutError(\n 'The referral discount on this cart changed. Review the updated total, then check out again.',\n );\n await scheduleRefresh();\n return;\n }\n /**\n * The referral the request CARRIES, decided here rather than by the SDK.\n *\n * PAYMENT PATH, and the half of the problem the comparison above cannot\n * solve. Omit `affiliateCode` and `sdk.checkout()` reads the ambient one\n * out of shared storage itself, a second time, on its way into the\n * request (`normalizeAffiliateCode`). The check above is therefore a\n * check on a value nobody submitted: a write landing between the two\n * reads still ships a code the quote never saw. Naming it closes that —\n * an explicitly named code is the only thing the SDK does not re-read\n * around.\n *\n * Which code, per lane, is exactly what is true today:\n * - ambient lane — the referral the CURRENT quote was priced with. Equal\n * to `ambientNow` by the check that just passed; taken from the quote\n * because that is the total the buyer approved.\n * - merchant-coupon lane — the quote deliberately ignored the referral\n * (one code per quote, and an applied coupon outranks it), while the\n * invoice has always carried the attribution anyway. So the ambient\n * read is pinned as-is: same attribution, one read instead of two.\n * - applied-referral lane — `resolveSubmittedCode` already named it.\n *\n * With NO referral in play the empty string is submitted rather than the\n * field being omitted, because the SDK's `affiliateCode` is a tri-state:\n * `''` means \"explicitly none\" and suppresses the storage read, while an\n * omitted field sends the SDK back to storage. Omitting it therefore left\n * one window open — a `?ref=` captured in another tab between this click\n * and the request still rode along on a total priced without it. Pinning\n * \"none\" closes it.\n */\n const pinnedAffiliateCode = submitted.affiliateCode\n ?? (isAmbientLane ? normalizeAmbientAffiliate(quotedAmbient) : ambientNow);\n const result = await sdk.checkout({\n ...submitted,\n affiliateCode: pinnedAffiliateCode,\n currency: getBuyerCurrency(),\n });\n if (!result.success) {\n /**\n * The refusal that would otherwise be a dead end. Every other failure\n * describes something the buyer can act on from the cart they can see;\n * this one names a code nothing on screen shows, so it gets its own\n * copy and an explicit exit rather than the backend's prose plus a\n * retry button that refuses identically.\n */\n if (result.code === SELF_REFERRAL_REFUSAL_CODE) {\n setSelfReferralBlocked(true);\n setCheckoutError(SELF_REFERRAL_MESSAGE);\n return;\n }\n const quoteProofMessage = result.code\n ? QUOTE_PROOF_REFUSAL_MESSAGES[result.code]\n : undefined;\n if (quoteProofMessage) {\n // The proof this hand-off carried was refused. Re-quote, so the next\n // attempt carries one for THIS cart and the buyer approves the total\n // that attempt would actually be charged.\n setCheckoutError(quoteProofMessage);\n await scheduleRefresh();\n return;\n }\n const refusalMessage = result.code ? CHECKOUT_REFUSAL_MESSAGES[result.code] : undefined;\n setCheckoutError(refusalMessage ?? (result.message?.trim() || 'Checkout could not be started.'));\n return;\n }\n if (!result.redirectUrl) {\n setCheckoutError('Checkout could not be started.');\n }\n } catch (error) {\n setCheckoutError(errorMessage(error, 'Checkout could not be started.'));\n } finally {\n setBusyCount((value) => Math.max(0, value - 1));\n }\n }, [scheduleRefresh]);\n\n // Recomputed from the lines rather than stored: the questions a cart raises\n // are a function of what is in it, and a second copy of that could disagree\n // with the cart it describes.\n const checkoutFields = useMemo(() => resolveCheckoutFields(lines), [lines]);\n\n const value = useMemo<CartContextValue>(() => ({\n lines,\n count,\n quote,\n quoteError,\n mutationError,\n couponRejected,\n quotedAmbientAffiliate,\n cartMaxWarning,\n requiresShipping: quote?.requires_shipping === true,\n checkoutError,\n selfReferralBlocked,\n clearBlockedReferral,\n checkoutFields,\n checkoutNeedsDetails: needsCheckoutDetails(checkoutFields),\n busy: busyCount > 0,\n couponInput,\n setCouponInput: editCouponInput,\n drawerOpen,\n setDrawerOpen,\n add,\n addBundle,\n update,\n remove,\n clear,\n applyCoupon,\n removeCoupon,\n checkout,\n }), [\n add,\n addBundle,\n applyCoupon,\n busyCount,\n cartMaxWarning,\n checkout,\n checkoutError,\n checkoutFields,\n clear,\n clearBlockedReferral,\n count,\n couponInput,\n couponRejected,\n drawerOpen,\n editCouponInput,\n lines,\n mutationError,\n quote,\n quoteError,\n quotedAmbientAffiliate,\n remove,\n removeCoupon,\n selfReferralBlocked,\n update,\n ]);\n\n return <CartContext.Provider value={value}>{children}</CartContext.Provider>;\n}\n\nexport function useCart(): CartContextValue {\n const context = useContext(CartContext);\n if (!context) throw new Error('useCart must be used inside CartProvider.');\n return context;\n}\n\n/**\n * Starts a cart mutation from a control that has nothing to chain onto it.\n *\n * Every mutation now REJECTS when the write fails, which is what lets Buy Now\n * stop before checkout. A stepper, a remove button or a bundle offer has no next\n * step to stop: its failure is already rendered from `mutationError` on the\n * drawer and `/cart`, the surfaces those controls live on. This states that at\n * the call site rather than leaving the rejection unhandled — the error is\n * displayed, not swallowed.\n */\n/**\n * The add-ons a cart line carries: the caller's choice plus every add-on the\n * merchant marked required, once each.\n *\n * PAYMENT PATH. The buy box preselects and locks required add-ons, but a card,\n * a bundle dialog or a related tile adds without choosing any. Without this the\n * line, its quote and the invoice omit a surcharge the merchant made mandatory.\n */\nexport function withRequiredAddons(product: StorefrontProduct, chosen: readonly ProductAddon[]): ProductAddon[] {\n const ids = new Set(chosen.map((addon) => addon.id));\n return [...chosen, ...product.addons.filter((addon) => addon.required && !ids.has(addon.id))];\n}\n\nexport function startCartMutation(mutation: Promise<void>): void {\n void mutation.catch(() => undefined);\n}\n","/**\n * Strict parsing helpers shared by the content contract (`src/config`) and the\n * storefront data contract (`src/commerce`).\n *\n * The rule both contracts follow: an ABSENT value may fall back to a documented\n * default, a PRESENT but malformed one throws. Silently repairing bad input\n * would hide a broken content draft or a changed wire contract behind a\n * storefront that merely looks slightly wrong.\n */\n\nexport function isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nexport function requireUnknownRecord(value: unknown, context: string): Record<string, unknown> {\n if (!isRecord(value)) {\n throw new Error(`Invalid Shoppex ${context}: expected an object.`);\n }\n return value;\n}\n\nexport function requireRecord(\n record: Record<string, unknown>,\n key: string,\n): Record<string, unknown> {\n return requireUnknownRecord(record[key], key);\n}\n\nexport function requireArray(record: Record<string, unknown>, key: string): unknown[] {\n const value = record[key];\n if (!Array.isArray(value)) {\n throw new Error(`Invalid Shoppex config: ${key} must be an array.`);\n }\n return value;\n}\n\nexport function requireString(\n record: Record<string, unknown>,\n key: string,\n context: string,\n): string {\n const value = record[key];\n if (typeof value !== 'string' || value.trim().length === 0) {\n throw new Error(`Invalid Shoppex ${context}: ${key} must be a non-empty string.`);\n }\n return value;\n}\n\nexport function requireStringFromKeys(\n record: Record<string, unknown>,\n keys: string[],\n context: string,\n): string {\n for (const key of keys) {\n const value = optionalString(record[key]);\n if (value) return value;\n }\n throw new Error(`Invalid Shoppex ${context}: ${keys.join(' or ')} must be a non-empty string.`);\n}\n\nexport function requireNumber(\n record: Record<string, unknown>,\n key: string,\n context: string,\n): number {\n const value = record[key];\n if (typeof value !== 'number' || !Number.isFinite(value)) {\n throw new Error(`Invalid Shoppex ${context}: ${key} must be a finite number.`);\n }\n return value;\n}\n\n/** A non-empty trimmed string, or null for absent/blank/non-string input. */\nexport function optionalString(value: unknown): string | null {\n return typeof value === 'string' && value.trim().length > 0 ? value.trim() : null;\n}\n\n/** The first usable string among several candidate keys. */\nexport function firstOptionalString(\n record: Record<string, unknown>,\n keys: string[],\n): string | null {\n for (const key of keys) {\n const value = optionalString(record[key]);\n if (value) return value;\n }\n return null;\n}\n\n/**\n * A visible string that may be absent from an older content draft. Absent or\n * blank renders the shipped wording; a present non-string is still an error.\n */\nexport function optionalStringWithDefault(\n value: unknown,\n fallback: string,\n context: string,\n): string {\n if (value === undefined || value === null || value === '') return fallback;\n if (typeof value !== 'string') {\n throw new Error(`Invalid Shoppex ${context}: expected a string.`);\n }\n return value.trim() || fallback;\n}\n\n/**\n * A boolean the serializer ALWAYS emits, so its absence is a broken payload.\n *\n * Defaulting one of these is worse than throwing: a merchant switch that never\n * arrives silently reads as whatever the theme guessed, and the storefront then\n * offers something the backend refuses. `0`/`1` are accepted because the wire\n * carries some flags that way; anything else is an error.\n */\nexport function requireBoolean(value: unknown, context: string): boolean {\n if (typeof value === 'boolean') return value;\n if (value === 0 || value === 1) return value === 1;\n throw new Error(`Invalid Shoppex ${context}: expected a boolean.`);\n}\n\nexport function optionalBoolean(value: unknown, fallback: boolean, context: string): boolean {\n if (value === undefined || value === null) return fallback;\n if (typeof value === 'boolean') return value;\n if (value === 0 || value === 1) return value === 1;\n throw new Error(`Invalid Shoppex ${context}: expected a boolean.`);\n}\n\nexport function parseFiniteNumber(value: unknown, context: string): number {\n const parsed = typeof value === 'number' ? value : Number(value);\n if (!Number.isFinite(parsed)) {\n throw new Error(`Invalid Shoppex ${context}: expected a finite number.`);\n }\n return parsed;\n}\n\n/**\n * One of a fixed option set. Absent falls back to the theme default; anything\n * else throws, because a typo in an enum is a broken content draft, not a\n * missing field — and the wrong branch of a layout switch is worse than a\n * visible error.\n */\nexport function parseEnum<T extends string>(\n value: unknown,\n allowed: readonly T[],\n fallback: T,\n context: string,\n): T {\n if (value === undefined || value === null || value === '') return fallback;\n if (typeof value !== 'string' || !(allowed as readonly string[]).includes(value)) {\n throw new Error(`Invalid Shoppex ${context}: expected one of ${allowed.join(', ')}.`);\n }\n return value as T;\n}\n","/**\n * Merchant-authored product badges (\"New!\", \"Limited\", \"Best value\").\n *\n * Purely decorative merchandising the merchant attaches in the dashboard. They\n * never affect price, stock or fulfilment, and they never speak for the\n * storefront: a badge cannot say a product is available, on sale, or in stock —\n * those remain the theme's own signals, derived from real fields.\n *\n * Ported rather than imported, for the same reason `video-embed.ts` is: this\n * theme builds standalone from `templates/code-storefronts/`, in an offline\n * sandbox, with no workspace dependency on `@shoppex/builder-contracts`. The\n * canonical contract is `packages/builder-contracts/src/product-badges.ts` —\n * keep the wire field names, the caps and the contrast rule in step with it.\n *\n * Everything below reads UNTRUSTED JSON: the payload arrives over the network\n * in `window.__SHOPPEX_INITIAL__`. Entries that do not parse are dropped rather\n * than repaired — a badge with no usable label has no meaning, and inventing\n * one would be a fabricated value on a merchandising surface.\n */\n\n/** Hard cap per product, matching the contract. Card corners are finite. */\nconst MAX_PRODUCT_BADGES = 4;\n\n/** Hard cap per label, matching the contract. Longer labels break card layouts. */\nconst MAX_PRODUCT_BADGE_LABEL_LENGTH = 24;\n\n/**\n * The icon catalog the dashboard offers. A name outside this list is NOT an\n * error — the catalog is append-only upstream, so a storefront artifact built\n * before an icon was added must degrade to a text-only badge rather than throw\n * on a name it has never heard of.\n */\nexport const PRODUCT_BADGE_ICON_NAMES = [\n 'sparkles',\n 'flame',\n 'star',\n 'zap',\n 'tag',\n 'percent',\n 'trending-up',\n 'megaphone',\n 'party-popper',\n 'crown',\n 'gem',\n 'trophy',\n 'award',\n 'medal',\n 'badge-check',\n 'thumbs-up',\n 'heart',\n 'shield',\n 'shield-check',\n 'lock',\n 'handshake',\n 'rocket',\n 'truck',\n 'clock',\n 'timer',\n 'infinity',\n 'refresh-ccw',\n 'download',\n 'package',\n 'send',\n 'headphones',\n 'message-circle',\n 'life-buoy',\n 'users',\n 'gamepad-2',\n 'monitor',\n 'smartphone',\n 'key',\n 'file-text',\n 'book-open',\n 'music',\n 'video',\n 'image',\n 'palette',\n 'code',\n 'wrench',\n 'wallet',\n 'credit-card',\n 'globe',\n 'gift',\n 'bot',\n] as const;\n\nexport type ProductBadgeIconName = (typeof PRODUCT_BADGE_ICON_NAMES)[number];\n\n/** One badge, in this theme's camelCase model. */\nexport interface ProductBadge {\n label: string;\n /** null for a text-only badge, and for any icon name this build cannot draw. */\n icon: ProductBadgeIconName | null;\n /** `#rrggbb` lowercase, or null when the theme owns the colour. */\n backgroundColor: string | null;\n /**\n * The readable foreground for `backgroundColor`, decided by WCAG contrast.\n * null exactly when `backgroundColor` is null — the theme then owns both.\n */\n foregroundColor: string | null;\n showOnProductCard: boolean;\n showOnProductPage: boolean;\n}\n\nconst HEX_SHORT = /^#[0-9a-f]{3}$/i;\nconst HEX_LONG = /^#[0-9a-f]{6}$/i;\n\n/**\n * A merchant colour as lowercase `#rrggbb`, or null when it is absent or\n * unparseable. Strict on purpose: no named colours, no `rgb()`, no gradients.\n * The contrast rule below can only reason about one opaque colour.\n */\nfunction normalizeBadgeColor(value: unknown): string | null {\n if (typeof value !== 'string') return null;\n const trimmed = value.trim();\n if (trimmed.length === 0) return null;\n const candidate = trimmed.startsWith('#') ? trimmed : `#${trimmed}`;\n if (HEX_LONG.test(candidate)) return candidate.toLowerCase();\n if (HEX_SHORT.test(candidate)) {\n const [, r, g, b] = candidate.toLowerCase();\n return `#${r}${r}${g}${g}${b}${b}`;\n }\n return null;\n}\n\nfunction channelLuminance(value: number): number {\n const channel = value / 255;\n return channel <= 0.04045 ? channel / 12.92 : ((channel + 0.055) / 1.055) ** 2.4;\n}\n\n/** WCAG relative luminance of a normalized `#rrggbb` colour. */\nfunction relativeLuminance(hex: string): number {\n const r = Number.parseInt(hex.slice(1, 3), 16);\n const g = Number.parseInt(hex.slice(3, 5), 16);\n const b = Number.parseInt(hex.slice(5, 7), 16);\n return 0.2126 * channelLuminance(r) + 0.7152 * channelLuminance(g) + 0.0722 * channelLuminance(b);\n}\n\n/** Near-black, used when the merchant's colour is light enough to need dark text. */\nconst BADGE_DARK_FOREGROUND = '#0b0b0f';\n/** Pure white, used when the merchant's colour is dark enough to need light text. */\nconst BADGE_LIGHT_FOREGROUND = '#ffffff';\n\n/**\n * The readable foreground for a badge background.\n *\n * The merchant picks the fill from a colour well with no contrast check of its\n * own, so the label colour cannot also be theirs: a mid-yellow badge with white\n * text is unreadable and a navy badge with black text is worse. Both candidates\n * are scored by WCAG contrast ratio against the chosen fill and the higher one\n * wins, which is the same decision the other lane makes for the same badge.\n *\n * Returns null when there is no explicit background — the theme then owns both\n * colours, and substituting one here would override its palette.\n */\nexport function resolveBadgeForeground(backgroundColor: string | null): string | null {\n if (backgroundColor === null) return null;\n const background = relativeLuminance(backgroundColor);\n const contrastWithWhite = 1.05 / (background + 0.05);\n const contrastWithBlack =\n (background + 0.05) / (relativeLuminance(BADGE_DARK_FOREGROUND) + 0.05);\n return contrastWithWhite >= contrastWithBlack ? BADGE_LIGHT_FOREGROUND : BADGE_DARK_FOREGROUND;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\n/**\n * `show_on_*` decides whether a surface renders a badge at all, so an\n * unreadable value must not silently hide the merchant's badge everywhere. The\n * wire carries booleans; the tolerated 1/0/\"true\"/\"false\" spellings are the\n * same ones the contract accepts, and anything else keeps the badge visible\n * (which is the dashboard's own default for a newly created badge).\n */\nfunction readVisibility(value: unknown): boolean {\n if (typeof value === 'boolean') return value;\n if (value === 1 || value === '1' || value === 'true') return true;\n if (value === 0 || value === '0' || value === 'false') return false;\n return true;\n}\n\nfunction readField(entry: Record<string, unknown>, snake: string, camel: string): unknown {\n return entry[snake] !== undefined ? entry[snake] : entry[camel];\n}\n\n/**\n * The `badges` array off the product wire. Missing, malformed or empty all\n * produce `[]` — a product without badges is the overwhelmingly common case and\n * is not an error.\n */\nexport function parseProductBadges(value: unknown): ProductBadge[] {\n if (!Array.isArray(value)) return [];\n\n const badges: ProductBadge[] = [];\n for (const raw of value) {\n if (badges.length >= MAX_PRODUCT_BADGES) break;\n if (!isRecord(raw)) continue;\n\n const rawLabel = raw.label;\n if (typeof rawLabel !== 'string') continue;\n const label = rawLabel.trim().slice(0, MAX_PRODUCT_BADGE_LABEL_LENGTH);\n if (label.length === 0) continue;\n\n const rawIcon = raw.icon;\n const icon =\n typeof rawIcon === 'string'\n && (PRODUCT_BADGE_ICON_NAMES as readonly string[]).includes(rawIcon)\n ? (rawIcon as ProductBadgeIconName)\n : null;\n\n const backgroundColor = normalizeBadgeColor(\n readField(raw, 'background_color', 'backgroundColor'),\n );\n\n badges.push({\n label,\n icon,\n backgroundColor,\n foregroundColor: resolveBadgeForeground(backgroundColor),\n showOnProductCard: readVisibility(readField(raw, 'show_on_product_card', 'showOnProductCard')),\n showOnProductPage: readVisibility(readField(raw, 'show_on_product_page', 'showOnProductPage')),\n });\n }\n return badges;\n}\n\n/** Badges the catalog card shows, in the merchant's order. */\nexport function badgesForCard(badges: readonly ProductBadge[]): ProductBadge[] {\n return badges.filter((badge) => badge.showOnProductCard);\n}\n\n/** Badges the product page shows, in the merchant's order. */\nexport function badgesForProductPage(badges: readonly ProductBadge[]): ProductBadge[] {\n return badges.filter((badge) => badge.showOnProductPage);\n}\n","/**\n * The `/status` page model: the products the merchant gave an explicit service\n * status in the dashboard (`service_status` on the catalog wire).\n *\n * Nothing is derived here. A product without a status is not listed, and a\n * shop that set none gets `null`, so a theme can hide its status route and nav\n * link instead of showing an empty or invented board. The words for the four\n * states (\"Undetected\", \"Updating\", ...) belong to the theme.\n */\n\nimport type { StorefrontProduct } from './storefront-data';\n\nexport const SERVICE_STATES = ['operational', 'updating', 'detected', 'offline'] as const;\n\nexport type ServiceState = (typeof SERVICE_STATES)[number];\n\nexport interface ProductServiceStatus {\n state: ServiceState;\n /** The merchant's note (\"Patch 4.2 in progress\"); null when none. */\n note: string | null;\n /** ISO timestamp of the merchant's last change. */\n updatedAt: string;\n}\n\nexport interface StatusPageEntry {\n product: StorefrontProduct;\n status: ProductServiceStatus;\n}\n\nexport interface StatusPageModel {\n /** Entries in board order: the most urgent state first, then the merchant's shelf order. */\n entries: StatusPageEntry[];\n /** How many products are in each state; every state is present (0 when none). */\n counts: Record<ServiceState, number>;\n /** The states that have at least one product, in board order, for filter chips. */\n presentStates: ServiceState[];\n /** The latest change across all entries (ISO). */\n lastUpdatedAt: string;\n}\n\n/** Board order: what a buyer must know first comes first. */\nconst STATE_ORDER: Record<ServiceState, number> = { detected: 0, offline: 1, updating: 2, operational: 3 };\n\nexport function parseServiceStatus(value: unknown, context: string): ProductServiceStatus | null {\n if (value === null || value === undefined) return null;\n if (typeof value !== 'object' || Array.isArray(value)) {\n throw new Error(`Invalid Shoppex ${context}.service_status: expected an object or null.`);\n }\n const record = value as Record<string, unknown>;\n const state = record.state;\n if (typeof state !== 'string' || !(SERVICE_STATES as readonly string[]).includes(state)) {\n throw new Error(`Invalid Shoppex ${context}.service_status.state: expected one of ${SERVICE_STATES.join(', ')}.`);\n }\n const updatedAt = record.updated_at;\n if (typeof updatedAt !== 'string' || Number.isNaN(Date.parse(updatedAt))) {\n throw new Error(`Invalid Shoppex ${context}.service_status.updated_at: expected an ISO timestamp.`);\n }\n const note = typeof record.note === 'string' && record.note.trim() ? record.note.trim() : null;\n return { state: state as ServiceState, note, updatedAt };\n}\n\nexport function buildStatusPageModel(products: readonly StorefrontProduct[]): StatusPageModel | null {\n const entries: StatusPageEntry[] = [];\n const seen = new Set<string>();\n for (const product of products) {\n if (product.serviceStatus === null || seen.has(product.id)) continue;\n seen.add(product.id);\n entries.push({ product, status: product.serviceStatus });\n }\n if (entries.length === 0) return null;\n\n entries.sort((a, b) => STATE_ORDER[a.status.state] - STATE_ORDER[b.status.state]);\n const counts: Record<ServiceState, number> = { operational: 0, updating: 0, detected: 0, offline: 0 };\n for (const entry of entries) counts[entry.status.state] += 1;\n const presentStates = [...SERVICE_STATES]\n .sort((a, b) => STATE_ORDER[a] - STATE_ORDER[b])\n .filter((state) => counts[state] > 0);\n const lastUpdatedAt = entries\n .map((entry) => entry.status.updatedAt)\n .reduce((latest, value) => (Date.parse(value) > Date.parse(latest) ? value : latest));\n\n return { entries, counts, presentStates, lastUpdatedAt };\n}\n","/**\n * The merchant's product video, turned into an embeddable `src` — and gated.\n *\n * `products.video_link` is a raw string the merchant typed: a YouTube watch URL,\n * a bare video id, a Streamable short link, a Vimeo page. The Easy lane resolves\n * it through `resolveVideoEmbedSrc`\n * (`packages/builder-runtime/src/video-embed.ts`, called by the storefront\n * render context) and then renders the iframe ONLY when the resolved host is on\n * the gallery block's allowlist (`blocks/gallery.ts`, ADR-0045).\n *\n * Both halves are ported here, in that order, because they answer different\n * questions. The parser decides which provider a merchant meant; the allowlist\n * decides whether the storefront is willing to frame it at all. A host that is\n * not on the list renders NOTHING — never the raw string in an iframe, which is\n * what turns a merchant-editable text column into an arbitrary embed.\n *\n * Ported rather than imported: the theme builds standalone from\n * `templates/code-storefronts/`, with no workspace dependency on the renderer.\n * Keep the allowlist and the provider rules in step with the two files above.\n */\n\n/**\n * Exactly the hosts `blocks/gallery.ts` frames, and no others. `https://` is\n * required by the same check — an `http://` embed on an https storefront is a\n * mixed-content frame the browser blocks anyway.\n */\nconst ALLOWED_VIDEO_EMBED_HOSTS = new Set([\n 'www.youtube-nocookie.com',\n 'www.youtube.com',\n 'player.vimeo.com',\n 'streamable.com',\n]);\n\n/** Mirrors `isAllowedVideoEmbedSrc` in the Easy lane's gallery block. */\nfunction isAllowedVideoEmbedSrc(src: string): boolean {\n if (!src.startsWith('https://')) return false;\n const host = src.slice('https://'.length).split('/')[0]!.split('?')[0]!.split('#')[0]!;\n return ALLOWED_VIDEO_EMBED_HOSTS.has(host);\n}\n\nconst YOUTUBE_VIDEO_ID = /^[\\w-]{11}$/;\nconst STREAMABLE_CODE = /^[a-zA-Z0-9]+$/;\nconst VIMEO_ID = /^\\d+$/;\nconst VIMEO_HASH = /^[a-zA-Z0-9]+$/;\n\nfunction toUrl(input: string): URL | null {\n try {\n return input.startsWith('http://') || input.startsWith('https://')\n ? new URL(input)\n : new URL(`https://${input}`);\n } catch {\n return null;\n }\n}\n\n/** YouTube accepts a bare 11-character id, so it is the fallback provider. */\nfunction parseYouTubeVideoId(input: string): string | null {\n const trimmed = input.trim();\n if (!trimmed) return null;\n if (YOUTUBE_VIDEO_ID.test(trimmed)) return trimmed;\n\n const url = toUrl(trimmed);\n if (!url) return null;\n const host = url.hostname.replace(/^www\\./, '');\n\n if (host === 'youtu.be') {\n const candidate = url.pathname.split('/').filter(Boolean)[0] ?? '';\n return YOUTUBE_VIDEO_ID.test(candidate) ? candidate : null;\n }\n if (host !== 'youtube.com' && host !== 'm.youtube.com' && host !== 'music.youtube.com') {\n return null;\n }\n const watchId = url.searchParams.get('v');\n if (watchId && YOUTUBE_VIDEO_ID.test(watchId)) return watchId;\n for (const pattern of [/^\\/embed\\/([\\w-]{11})/, /^\\/shorts\\/([\\w-]{11})/, /^\\/live\\/([\\w-]{11})/]) {\n const match = url.pathname.match(pattern);\n if (match?.[1] && YOUTUBE_VIDEO_ID.test(match[1])) return match[1];\n }\n return null;\n}\n\n/** streamable.com/<code> and streamable.com/e/<code>. */\nfunction parseStreamableCode(url: URL): string | null {\n const segments = url.pathname.split('/').filter(Boolean);\n const code = segments[0] === 'e' ? segments[1] : segments[0];\n return code && STREAMABLE_CODE.test(code) ? code : null;\n}\n\n/**\n * The numeric id, plus the privacy hash an UNLISTED video needs to play. The\n * `?h=` query wins over the path segment, matching Vimeo's own player URLs.\n */\nfunction parseVimeo(url: URL): { id: string; hash?: string } | null {\n const segments = url.pathname.split('/').filter(Boolean);\n for (let index = segments.length - 1; index >= 0; index -= 1) {\n const segment = segments[index]!;\n if (!VIMEO_ID.test(segment)) continue;\n const queryHash = url.searchParams.get('h')?.trim();\n const pathHash = segments[index + 1];\n const hash = queryHash && VIMEO_HASH.test(queryHash)\n ? queryHash\n : pathHash && VIMEO_HASH.test(pathHash)\n ? pathHash\n : undefined;\n return hash ? { id: segment, hash } : { id: segment };\n }\n return null;\n}\n\n/**\n * The iframe `src` for a merchant's `video_link`, or null when there is nothing\n * this storefront is willing to frame.\n *\n * Privacy-enhanced YouTube (`youtube-nocookie.com`) because that is what the\n * render context asks for (`resolveVideoEmbedSrc(..., { privacyEnhanced: true })`)\n * — a code storefront must not set more cookies on a buyer than the Easy lane\n * for the same merchant's video.\n */\nexport function resolveProductVideoEmbedSrc(value: unknown): string | null {\n if (typeof value !== 'string') return null;\n const trimmed = value.trim();\n if (!trimmed) return null;\n\n const url = toUrl(trimmed);\n let src: string | null = null;\n if (url) {\n const host = url.hostname.replace(/^www\\./, '');\n if (host === 'streamable.com') {\n const code = parseStreamableCode(url);\n src = code ? `https://streamable.com/e/${code}` : null;\n } else if (host === 'vimeo.com' || host === 'player.vimeo.com') {\n const parsed = parseVimeo(url);\n src = parsed\n ? parsed.hash\n ? `https://player.vimeo.com/video/${parsed.id}?h=${parsed.hash}`\n : `https://player.vimeo.com/video/${parsed.id}`\n : null;\n }\n }\n if (src === null) {\n const youtubeId = parseYouTubeVideoId(trimmed);\n src = youtubeId ? `https://www.youtube-nocookie.com/embed/${youtubeId}` : null;\n }\n\n // The second gate. It is redundant for everything the parser above produces,\n // and that is the point: if a provider is ever added to the parser without\n // being added to the allowlist, the storefront frames nothing rather than an\n // origin nobody vetted.\n return src !== null && isAllowedVideoEmbedSrc(src) ? src : null;\n}\n","/**\n * The `window.__SHOPPEX_INITIAL__` contract: catalog, store switches and addon\n * slots, injected by the Shoppex storefront (or preview) worker immediately\n * before `</head>` per\n * `packages/builder-contracts/src/storefront-initial-data-html.ts`.\n *\n * This is PLATFORM data, not merchant content — the theme never authors it and\n * must never invent it. Merchant-authored content lives in\n * `src/config/site-config.ts` and is read from the same object under\n * `store.builder_settings.theme.content`.\n */\n\nimport {\n firstOptionalString,\n isRecord,\n optionalBoolean,\n optionalString,\n parseFiniteNumber,\n requireBoolean,\n requireString,\n requireStringFromKeys,\n requireUnknownRecord,\n} from './parse';\nimport { parseProductBadges, type ProductBadge } from './product-badges';\nimport { parseServiceStatus, type ProductServiceStatus } from './service-status';\nimport { resolveProductVideoEmbedSrc } from './video-embed';\nimport type { StorefrontAddon } from './sdk';\n\nexport interface StorefrontPriceVariant {\n id: string;\n label: string;\n /**\n * The merchant's one-line note on this option (\"1 seat\", \"5 seats\"). null\n * when the option has none.\n */\n description: string | null;\n /** What this option states about its term; `inherit` defers to the product. */\n license: LicenseStatement;\n /** Display unit price with the product percent discount already applied. */\n price: number;\n /**\n * Undiscounted display-currency unit price — the input for line-level\n * pricing, where the percent is applied to the whole line like the quote\n * does, and the floor the server holds pay-what-you-want amounts to.\n */\n baseUnitPrice: number;\n stock: number | null;\n quantityMin: number | null;\n quantityMax: number | null;\n imageUrl: string | null;\n isFeatured: boolean;\n}\n\nexport interface VolumeDiscountRule {\n quantity: number;\n type: 'PERCENTAGE' | 'FIXED';\n value: number;\n}\n\nexport type ProductBundleItem = {\n /** Cart id (uniqid) — what addToCart expects. */\n id: string;\n title: string;\n variantId: string;\n unitPrice: number;\n imageUrl: string | null;\n variantTitle: string | null;\n};\n\n/**\n * A \"buy these together\" offer the merchant configured. The discount is applied\n * by the backend when the cart is quoted — the theme only advertises it, it\n * never computes the charged price itself.\n */\nexport type ProductBundle = {\n id: string;\n title: string;\n discountType: 'PERCENTAGE' | 'FIXED';\n discountAmount: number;\n items: ProductBundleItem[];\n};\n\nexport type ProductContentTab = {\n title: string;\n html: string;\n};\n\n/**\n * A paid extra the buyer may add to the line. The price is a per-unit surcharge\n * on top of the resolved variant price, and `required` options are locked on —\n * the buy box shows them checked and refuses to clear them, because the backend\n * charges them either way.\n */\nexport type ProductAddon = {\n id: string;\n title: string;\n price: number;\n required: boolean;\n};\n\nexport type ProductFaq = {\n question: string;\n answer: string;\n};\n\n/**\n * One ACTIVE \"buy N, get M\" deal on this product.\n *\n * The server filters the list to the currently live window before it emits it,\n * so the storefront advertises what `CartQuoteService` would actually apply —\n * it never computes the discount itself. The discounted units sit WITHIN the\n * required quantity (they are not extra items on top), and\n * `maxApplicationsPerCart` caps how often the reward repeats in one cart; both\n * are `applyAutomaticProductDeals` semantics and the wording depends on them.\n *\n * `endsAt` is what makes a row time-limited. Once it passes the backend stops\n * applying the deal, so the WHOLE row has to disappear — not just its\n * countdown — which is why the panel drops expired rows rather than freezing\n * them at 00:00:00.\n */\nexport type ProductAutomaticDeal = {\n id: string;\n /** Merchant title. Empty when they named the rule nothing. */\n title: string;\n requiredQuantity: number;\n discountedQuantity: number;\n /** FREE gives the units away; PERCENTAGE takes `discountValue` percent off. */\n discountType: 'FREE' | 'PERCENTAGE';\n /** Percent off the discounted units. 0 for a FREE deal. */\n discountValue: number;\n /** How often the reward repeats per cart; null when uncapped. */\n maxApplicationsPerCart: number | null;\n /** ISO timestamp the deal stops applying, or null when it does not expire. */\n endsAt: string | null;\n};\n\n/**\n * One detail the merchant requires from the buyer for this product — a Steam\n * id, a server name, a licence holder.\n *\n * PAYMENT PATH. These are not decoration: the backend validates them when the\n * invoice is built (`custom_fields_validation_failed`), so a product with a\n * REQUIRED field cannot be sold at all until its value reaches the cart line.\n * The values ride on the cart line's `custom_fields`, which is also part of the\n * SDK's line identity — see `checkout-fields.ts`.\n *\n * The shape mirrors the SDK's own `StorefrontCustomField`\n * (`packages/sdk/src/utils/storefront-custom-fields.ts`), because that is what\n * the Easy lane validates against and the two lanes must agree about which\n * value a merchant's definition accepts.\n */\nexport type ProductCustomField = {\n name: string;\n /** 'text' | 'textarea' | 'checkbox' | … — anything else renders as text. */\n type: string;\n required: boolean;\n defaultValue: string;\n placeholder: string;\n /** Merchant-authored validation pattern. Absent when they set none. */\n regex: string | null;\n};\n\n/** A category a product belongs to. The id is what the filter chips match on. */\nexport type ProductCategoryRef = {\n id: string;\n title: string;\n};\n\n/**\n * Product kinds the catalog renders differently. `SUBSCRIPTION` earns a badge,\n * `INFO_CARD` is not buyable at all, `SERIALS` reads its stock straight off\n * the product row instead of aggregating variants, and `PHYSICAL` ships to an\n * address the hosted checkout collects, so it is never \"instant\".\n */\nexport type ProductType = 'PRODUCT' | 'SUBSCRIPTION' | 'INFO_CARD' | 'SERIALS' | 'PHYSICAL';\n\nconst PRODUCT_TYPES: readonly ProductType[] = ['PRODUCT', 'SUBSCRIPTION', 'INFO_CARD', 'SERIALS', 'PHYSICAL'];\n\n/**\n * What a product promises about delivery.\n *\n * - `shipped`: a PHYSICAL product. Its digital `delivery_time` is null, which\n * would otherwise read as instant, so the type decides first.\n * - `timed`: the merchant set a positive delivery time. Fractions round UP: a\n * partial wait is still a wait.\n * - `instant`: the payload STATES a delivery time and it is `null` or `0`,\n * the merchant's written \"no wait\".\n * - `unspecified`: the payload carries no delivery time at all. Nobody gave a\n * promise, so none is shown; reading absence as \"instant\" would invent the\n * merchant's answer.\n *\n * Parity with the hosted checkout's `apps/checkout/lib/delivery-promise.ts`,\n * which draws the same line between a written answer and no answer.\n */\nexport type DeliveryPromise =\n | { kind: 'shipped' }\n | { kind: 'timed'; count: number; unit: DeliveryTimeUnit }\n | { kind: 'instant' }\n | { kind: 'unspecified' };\n\nexport function resolveDeliveryPromise(\n product: Pick<StorefrontProduct, 'productType' | 'deliveryTimeStated' | 'deliveryTimeCount' | 'deliveryTimeUnit'>,\n): DeliveryPromise {\n if (product.productType === 'PHYSICAL') return { kind: 'shipped' };\n if (!product.deliveryTimeStated) return { kind: 'unspecified' };\n if (product.deliveryTimeCount !== null) {\n return { kind: 'timed', count: product.deliveryTimeCount, unit: product.deliveryTimeUnit };\n }\n return { kind: 'instant' };\n}\n\n/**\n * What the buyer is handed, from the product's raw delivery type. null for a\n * type this package does not know yet.\n */\nexport type ProductDeliverable =\n | 'serials'\n | 'file'\n | 'service'\n | 'dynamic'\n | 'info_card'\n | 'subscription'\n | 'physical';\n\nconst DELIVERABLES_BY_TYPE: Record<string, ProductDeliverable> = {\n SERIALS: 'serials',\n FILE: 'file',\n SERVICE: 'service',\n DYNAMIC: 'dynamic',\n INFO_CARD: 'info_card',\n SUBSCRIPTION: 'subscription',\n PHYSICAL: 'physical',\n};\n\nfunction parseDeliverable(value: unknown): ProductDeliverable | null {\n const normalized = typeof value === 'string' ? value.trim().toUpperCase() : '';\n return DELIVERABLES_BY_TYPE[normalized] ?? null;\n}\n\n/**\n * The \"You receive\" statement, only where the delivery type says what arrives:\n *\n * - `serials`: delivered text units. `unitLabel` is what the merchant calls\n * one (`label_singular`, e.g. \"License key\"), null to let the theme use its\n * own neutral word.\n * - `download`: a FILE product, delivered as a download link.\n *\n * Every other type returns null: a service, a generated item or an info card\n * does not state its contents on the wire, so a theme shows nothing rather\n * than guessing.\n */\nexport type YouReceive =\n | { kind: 'serials'; unitLabel: string | null }\n | { kind: 'download' };\n\nexport function resolveYouReceive(\n product: Pick<StorefrontProduct, 'deliverable' | 'unitLabelSingular'>,\n): YouReceive | null {\n if (product.deliverable === 'serials') return { kind: 'serials', unitLabel: product.unitLabelSingular };\n if (product.deliverable === 'file') return { kind: 'download' };\n return null;\n}\n\n/**\n * How often a subscription product is charged, and how long it is free first.\n *\n * PARITY CONTRACT. The Easy lane derives the same three facts from the same\n * flat wire fields in\n * `packages/storefront-render-core/src/subscription-billing.ts`, and the\n * wordings below are that module's wordings. A change on either side without\n * the other makes the two lanes describe one product differently, which is the\n * one thing this template may not do.\n */\nexport type SubscriptionIntervalUnit = 'DAY' | 'WEEK' | 'MONTH' | 'YEAR';\n\nconst SUBSCRIPTION_INTERVAL_UNITS: readonly SubscriptionIntervalUnit[] = ['DAY', 'WEEK', 'MONTH', 'YEAR'];\n\nexport interface ProductSubscription {\n /**\n * The billing interval, or null when the merchant configured none. The\n * product contract allows a SUBSCRIPTION product without an interval, so the\n * buy box then prints no cadence rather than inventing one.\n */\n intervalUnit: SubscriptionIntervalUnit | null;\n /** Intervals per charge, at least 1. */\n intervalCount: number;\n /** Free-trial length in days; 0 when the merchant set none. */\n trialDays: number;\n}\n\nconst SUBSCRIPTION_INTERVAL_NOUNS: Record<SubscriptionIntervalUnit, string> = {\n DAY: 'day',\n WEEK: 'week',\n MONTH: 'month',\n YEAR: 'year',\n};\n\n/** The cadence as it reads next to a price: \"per month\", \"every 3 months\". */\nexport function formatSubscriptionCadence(subscription: ProductSubscription): string {\n const unit = subscription.intervalUnit;\n if (unit === null) return '';\n const noun = SUBSCRIPTION_INTERVAL_NOUNS[unit];\n return subscription.intervalCount === 1\n ? `per ${noun}`\n : `every ${subscription.intervalCount} ${noun}s`;\n}\n\n/** \"7-day free trial\" — the plan charges nothing for those days. */\nexport function formatSubscriptionTrial(subscription: ProductSubscription): string {\n const days = subscription.trialDays;\n return days > 0 ? `${days}-day free trial` : '';\n}\n\n/**\n * Recurring billing off the FLAT catalog fields the wire carries\n * (`recurring_interval`, `recurring_interval_count`, `trial_period`). Only a\n * product sold as a subscription gets one: a plain product can keep a stale\n * interval from an earlier edit, and it must never advertise a renewal.\n */\nfunction parseSubscription(\n product: Record<string, unknown>,\n productType: ProductType,\n): ProductSubscription | null {\n if (productType !== 'SUBSCRIPTION') return null;\n const rawUnit = typeof product.recurring_interval === 'string'\n ? product.recurring_interval.toUpperCase()\n : '';\n const unit = SUBSCRIPTION_INTERVAL_UNITS.find((entry) => entry === rawUnit) ?? null;\n const count = normalizePositiveDays(product.recurring_interval_count) ?? 1;\n return {\n intervalUnit: unit,\n intervalCount: count,\n trialDays: normalizePositiveDays(product.trial_period) ?? 0,\n };\n}\n\nexport interface StorefrontProduct {\n /** uniqid — the identifier the cart/SDK use for this product. */\n id: string;\n /** Drives the catalog badge, the buy controls and the stock aggregation. */\n productType: ProductType;\n /**\n * The product's real database id. Distinct from `id` (uniqid); shop feedback\n * references products by this id, so review matching must use it.\n */\n productId: string;\n name: string;\n price: number;\n currency: string;\n description: string | null;\n /**\n * One plain-text line the merchant wrote for under the title\n * (`short_description`). null when they wrote none; never derived from the\n * description.\n */\n shortDescription: string | null;\n /** What the buyer is handed, from the raw delivery type. See `resolveYouReceive`. */\n deliverable: ProductDeliverable | null;\n /** The merchant's word for one delivered unit (\"License key\"); null when unset. */\n unitLabelSingular: string | null;\n /** The merchant's word for several delivered units (\"License keys\"); null when unset. */\n unitLabelPlural: string | null;\n /**\n * The merchant's explicit `/status` state for this product, set in the\n * dashboard. null when they set none: the product is then not on the status\n * board. See `buildStatusPageModel`.\n */\n serviceStatus: ProductServiceStatus | null;\n slug: string | null;\n imageUrl: string | null;\n /**\n * The product's own image attachments, in the merchant's order — the product\n * page gallery. Empty when the product has none; the gallery then falls back\n * to `imageUrl`, the same resolution the Easy lane's gallery producer applies.\n * The wire carries no per-image alt text, so the alt is the product title.\n */\n imageUrls: string[];\n stock: number | null;\n /**\n * Merchant-set \"Low Stock Warning\" threshold (`quantity_warning` on the\n * wire), null when disabled. NOT `stock_delimiter` — that field is the text\n * delimiter splitting the serial-key blob, never a number.\n */\n lowStockThreshold: number | null;\n /** Per-product stock-counter override; null inherits the shop-wide setting. */\n hideStockCounter: boolean | null;\n /**\n * Quantity-tier discounts, sorted ascending by quantity. The backend applies\n * them at charge time, so the displayed line total must apply them too or\n * the buyer sees a higher number than they pay.\n */\n volumeDiscounts: VolumeDiscountRule[];\n /**\n * Buyers may choose their own unit price at or above the resolved\n * product/variant price. Without an amount input the checkout silently\n * charges the minimum, so the buy box must render one when this is set.\n */\n payWhatYouWant: boolean;\n /** Normalized product percent discount (0 when none, and always 0 for PWYW). */\n discountPercent: number;\n /**\n * Undiscounted starting unit price (min variant, or the base for\n * variant-less products) — the line-pricing input for the no-variant case.\n */\n baseUnitPrice: number;\n /**\n * The product ROW's own undiscounted price, before variants are considered.\n * Distinct from `baseUnitPrice`, which already falls back to the cheapest\n * variant: a variant-led product carries 0 here, and that zero is what marks\n * its catalog price as a range (\"$7.00 - $50.00\") rather than a single\n * amount. Collapsing the two would price every variant product from its\n * minimum and silently drop the range.\n */\n basePrice: number;\n priceVariants: StorefrontPriceVariant[];\n quantityMin: number;\n quantityMax: number | null;\n /** Units sold, for social proof. 0 when the shop has no sales for it. */\n soldCount: number;\n /** Merchant flagged this product as a bestseller. */\n bestseller: boolean;\n /**\n * Merchant-authored decorative badges, in their order, at most four. Empty\n * on almost every product. Purely merchandising — see `product-badges.ts`:\n * they never speak for availability, price or fulfilment, so they are read\n * alongside the theme's own signals rather than instead of them.\n */\n badges: ProductBadge[];\n /** Merchant-assigned listing order; lower sorts first. */\n sortPriority: number;\n /** ISO timestamp the product was created, or null when the payload omits it. */\n createdAt: string | null;\n /** Categories this product belongs to. Empty when uncategorised. */\n categories: ProductCategoryRef[];\n /** Bundle offers this product takes part in. Empty when it is in none. */\n bundles: ProductBundle[];\n /**\n * Active quantity deals on this product, in the merchant's order. Empty when\n * the product has none or every rule is malformed — see `ProductAutomaticDeal`.\n */\n automaticDeals: ProductAutomaticDeal[];\n /**\n * The merchant's product video as an embeddable, allowlisted `src`. null when\n * they set none OR when the host is not one the storefront frames — see\n * `video-embed.ts`.\n */\n videoEmbedSrc: string | null;\n /** Paid extras offered alongside this product. Empty when it has none. */\n addons: ProductAddon[];\n /**\n * Details the merchant collects from the buyer for this product. Empty when\n * they configured none. A REQUIRED entry makes the product unsellable until\n * its value is on the cart line — see `ProductCustomField`.\n */\n customFields: ProductCustomField[];\n /**\n * The merchant PAUSED this product (\"Coming soon\"). It is not a supply state\n * and has nothing to do with stock or `orderable`: the backend refuses to\n * sell it at any inventory level, so the storefront must not offer it. Same\n * precedence the document lane applies (`isProductOutOfStock` checks it\n * before it looks at stock at all).\n */\n onHold: boolean;\n /**\n * Original price when a discount applies (price is the discounted one).\n * null when there is no discount — never a fabricated \"was\" price.\n */\n compareAtPrice: number | null;\n /**\n * Average star rating (1-5) derived from REAL feedback only. null when the\n * product has zero feedback — the UI then renders no stars rather than a\n * fake score.\n */\n rating: number | null;\n /** Number of feedback entries the rating is based on (0 when none). */\n ratingCount: number;\n /**\n * The feedback tally as the payload states it: how many buyers were\n * positive, neutral or negative. null exactly when `rating` is null.\n */\n ratingTally: { positive: number; neutral: number; negative: number } | null;\n /** Bulleted selling points shown in the buy box. */\n highlights: string[];\n /**\n * Merchant-defined key/value pairs, set per product in the dashboard under\n * Integrations -> Metadata. Always present ({} when the merchant set none)\n * and every value is a string, so `product.metadata.status` is safe to read\n * without a guard.\n *\n * This is your extension point: values you need that Shoppex has no field for\n * — a release status, an accent colour, a grouping tag. The merchant edits\n * them in the dashboard, so changing one does NOT require rebuilding this\n * storefront. Nothing here is rendered for you; read the keys your shop uses\n * and render them yourself.\n *\n * Public: everything in here is readable by anyone who opens the store.\n */\n metadata: Record<string, string>;\n /** Extra merchant-authored content tabs ({title, html}). */\n descriptionTabs: ProductContentTab[];\n /** Per-product FAQ entries for the details accordion. */\n faqs: ProductFaq[];\n /** What this product states about its licence term. Resolve with resolveLicenseTerm. */\n license: LicenseStatement;\n /** Whether licensing is on at all; a stale term must not be advertised when it is off. */\n licensingEnabled: boolean;\n /**\n * Whether the payload states a delivery time at all (`delivery_time` present,\n * `null` included). false makes the promise `unspecified`.\n */\n deliveryTimeStated: boolean;\n /** Estimated delivery time as a whole positive count, rounded up; null for no wait. */\n deliveryTimeCount: number | null;\n /** Unit `deliveryTimeCount` is counted in: 'minutes' | 'hours' | 'days'. */\n deliveryTimeUnit: DeliveryTimeUnit;\n /**\n * Recurring billing, present only on a product the merchant sells as a\n * subscription. null on every other product.\n */\n subscription: ProductSubscription | null;\n}\n\nexport interface StorefrontSettings {\n cartEnabled: boolean;\n hideOutOfStock: boolean;\n hideStockCounter: boolean;\n /**\n * Buyer-facing switches the MERCHANT owns, not the theme.\n *\n * A storefront that moves to the Advanced lane must not quietly re-enable\n * what its owner turned off, or offer a contact form the backend will refuse.\n * The document lane has honoured all three from the start.\n */\n searchEnabled: boolean;\n contactFormEnabled: boolean;\n requireInvoiceForContact: boolean;\n /**\n * Sold-out products may offer \"Notify me\" (back-in-stock emails). A merchant\n * switch the backend enforces; see `canRequestStockAlert`.\n */\n stockAlertsEnabled: boolean;\n /**\n * The merchant centres bundle cards. A shop-level presentation switch, not a\n * theme setting: the Easy lane reads `ctx.shop.center_group_titles` and\n * re-lays the GROUP card out — product cards are untouched.\n */\n centerGroupTitles: boolean;\n /**\n * The merchant does not want the product's own picture used as its gallery.\n * The Easy lane's gallery producer empties the image list outright when this\n * is on, so the product page shows its placeholder frame instead.\n */\n descriptionSkipDefaultImage: boolean;\n /**\n * Whether pictures INSIDE a product description are shown. Off strips every\n * `<img>` from the description and from each content tab, the same rewrite\n * `stripDescriptionImages` performs on the document lane. It does not touch\n * the gallery, which is `descriptionSkipDefaultImage`'s job.\n */\n descriptionImage: boolean;\n /**\n * The merchant let buyers re-sort the catalog. Off hides the control on every\n * listing surface and pins the order to `defaultSort` — the same AND the Easy\n * lane applies (`enrichPageAllProductsBlockSettings`: the block setting and\n * `shop.sort_enabled` both have to be on).\n */\n sortEnabled: boolean;\n /**\n * The order the merchant wants their catalog listed in, normalized to the\n * four keys every lane sorts by. It is the INITIAL sort of a listing and the\n * only sort when `sortEnabled` is false.\n */\n defaultSort: StorefrontSortKey;\n /**\n * Every currency this shop sells in, the shop's own default first, uppercased\n * and de-duplicated. One entry (or none) means the shop has nothing to switch\n * between and the header renders no control.\n */\n availableCurrencies: string[];\n /** The currency the merchant configured — the one served without a choice. */\n defaultCurrency: string;\n /**\n * The currency THIS page was priced in. It is the buyer's choice, their\n * country's match, or the shop default, resolved at the edge before the\n * catalog was fetched — so it is the only currency the amounts on screen are\n * actually in.\n */\n activeCurrency: string;\n /**\n * The payment methods the shop's connected gateways take (`store.payment_methods`),\n * cards first, then wallets, then crypto. The backend derives them from the\n * gateway codes, so a theme renders marks without a mapping of its own.\n * Merchant fact: an empty list renders no row rather than a reassuring default.\n */\n paymentMethods: StorefrontPaymentMethod[];\n}\n\nexport type StorefrontPaymentMethodKind = 'card' | 'wallet' | 'crypto';\n\n/**\n * One payment method the checkout takes. `id` is a stable lower-case mark id\n * (`visa`, `paypal`, `bitcoin`, `usdt`, ...); `label` is its English name.\n * Unknown ids may appear as the platform adds gateways: render the label.\n */\nexport interface StorefrontPaymentMethod {\n id: string;\n label: string;\n kind: StorefrontPaymentMethodKind;\n}\n\nconst PAYMENT_METHOD_KINDS: readonly StorefrontPaymentMethodKind[] = ['card', 'wallet', 'crypto'];\n\nfunction parsePaymentMethods(value: unknown): StorefrontPaymentMethod[] {\n if (!Array.isArray(value)) {\n throw new Error('Invalid Shoppex store: payment_methods must be an array.');\n }\n return value.map((entry, index) => {\n const context = `store.payment_methods[${index}]`;\n const record = requireUnknownRecord(entry, context);\n const kind = record.kind;\n if (typeof kind !== 'string' || !(PAYMENT_METHOD_KINDS as readonly string[]).includes(kind)) {\n throw new Error(`Invalid Shoppex ${context}: kind must be card, wallet or crypto.`);\n }\n return {\n id: requireString(record, 'id', context),\n label: requireString(record, 'label', context),\n kind: kind as StorefrontPaymentMethodKind,\n };\n });\n}\n\n/**\n * The shop itself: the identity and the aggregate metrics the storefront\n * chrome renders. Merchant-owned facts, never theme content — a theme that\n * authored its own store name or sales count would be publishing a claim the\n * merchant never made.\n */\nexport interface StorefrontShop {\n /** The shop's public display name. */\n name: string;\n /** Branding/domain logo, falling back to the profile picture for logo surfaces. */\n logoUrl: string | null;\n /** Shop profile picture, falling back to the logo only when no avatar exists. */\n avatarUrl: string | null;\n /** Merchant-configured social profiles, in the platform's presentation order. */\n socialLinks: SocialLink[];\n /**\n * The merchant's published terms of service, as authored HTML — null when\n * the shop has no terms or turned them off. `/terms` renders it through\n * `sanitizeMerchantHtml`, never raw.\n */\n termsHtml: string | null;\n /** True when the merchant published terms and the `/terms` route has content. */\n hasPublishedTerms: boolean;\n /**\n * The shop's reward program, or null when it is off / has no perks. Merchant\n * fact: the headline and the perk labels are the merchant's wording, and a\n * theme that supplied its own would advertise a program nobody configured.\n */\n rewardProgram: RewardProgramSummary | null;\n /**\n * The shop's wholesale reseller program, or null when the shop runs none.\n * Merchant fact: only a shop that actually accepts resellers may advertise\n * one, so the footer's \"Become a reseller\" link is gated on this and not on\n * theme content.\n */\n resellerProgram: ResellerProgramSummary | null;\n stats: StorefrontShopStats;\n}\n\n/**\n * The reseller program off the wire, gated the way the Easy lane's footer link\n * gates it (`blocks/reseller-program-link.ts`): a summary is usable only when\n * it names an enrolment mode the portal can serve. Both modes render the SAME\n * link — the portal decides between one-click enrolment and the application\n * form.\n */\nexport type ResellerProgramSummary = {\n enrollmentMode: 'OPEN' | 'APPLICATION';\n};\n\n/**\n * The reward-program teaser's data, gated exactly as the Easy lane gates it\n * (`blocks/reward-program-teaser.ts` + `mapRewardProgramSummary`): the program\n * must be enabled, it must carry a headline, and it must have at least one\n * usable perk. Anything short of that is not a program to advertise, so the\n * whole summary is null and every teaser renders nothing.\n */\nexport type RewardProgramSummary = {\n headline: string;\n items: RewardProgramItem[];\n};\n\n/**\n * One perk. `triggerType` and `actionType` are required by the platform's own\n * mapper — a row missing either is a half-configured rule, not a perk — even\n * though only the label is rendered.\n */\nexport type RewardProgramItem = {\n triggerType: string;\n actionType: string;\n label: string;\n};\n\nexport type SocialNetwork =\n | 'discord'\n | 'telegram'\n | 'youtube'\n | 'twitter'\n | 'instagram'\n | 'tiktok'\n | 'facebook'\n | 'reddit'\n | 'email';\n\nexport type SocialLink = {\n network: SocialNetwork;\n href: string;\n};\n\n/**\n * Aggregate shop metrics for the store-stats strip.\n *\n * Counts are raw numbers, not display strings: formatting belongs to the\n * component. `averageRating` and `reviewCount` are independent on purpose —\n * a shop can carry a rating from transferred feedback without a countable\n * review, and rounding that to \"no reviews\" would drop a real number.\n */\nexport interface StorefrontShopStats {\n productsSold: number;\n customers: number;\n reviewCount: number;\n /** 1-5, or null when the shop has no rating at all. */\n averageRating: number | null;\n /** Products the storefront lists — the theme counts them, the API does not. */\n productCount: number;\n}\n\n/**\n * A merchant-curated bundle of products. The wire payload references its\n * members by uniqid; the members themselves are ordinary catalog products, so\n * they arrive in `products` and are resolved from there.\n */\nexport interface StorefrontGroup {\n id: string;\n title: string;\n description: string | null;\n imageUrl: string | null;\n /** Category the merchant filed the bundle under, if any. */\n categoryId: string | null;\n /** Member product uniqids, in the merchant's order. */\n productIds: string[];\n /** Member count as the API counts it — may exceed the resolvable members. */\n productsCount: number;\n sortPriority: number;\n}\n\nexport interface StorefrontCategory {\n id: string;\n slug: string;\n title: string;\n /** The merchant's category picture. Null leaves the filter chip text-only. */\n imageUrl: string | null;\n productsCount: number;\n groupsCount: number;\n}\n\n/**\n * One entry of the storefront's listing order.\n *\n * `items` is a pure display-order reference list: it decides WHICH products and\n * groups the storefront lists and in what order, and the referenced records are\n * delivered exactly once in `products` / `groups`. A storefront that listed\n * `products` directly would show unlisted rows (bundle members, hidden\n * products) the merchant never put on the shelf.\n */\nexport interface StorefrontListingItem {\n type: 'product' | 'group';\n id: string;\n sortPriority: number;\n}\n\ninterface ShoppexInitialData {\n store?: Record<string, unknown> & {\n builder_settings?: {\n theme?: {\n content?: unknown;\n };\n };\n };\n products?: unknown[];\n groups?: unknown[];\n items?: unknown[];\n categories?: unknown[];\n addons?: {\n items?: StorefrontAddon[];\n };\n}\n\ndeclare global {\n interface Window {\n __SHOPPEX_INITIAL__?: ShoppexInitialData;\n }\n}\n\n/**\n * Whether the page carries an injected payload AT ALL.\n *\n * Not a validity check — an empty object passes. It separates \"nothing injected\n * this document\" from \"something was injected and is wrong\", which are two\n * different faults with two different fixes. See `boot` in `App.tsx`.\n */\nexport function hasInjectedStorefrontData(): boolean {\n return window.__SHOPPEX_INITIAL__ !== undefined;\n}\n\n/** Merchant-authored content, or undefined when the store has none yet. */\nexport function getAuthoredContent(): unknown {\n return window.__SHOPPEX_INITIAL__?.store?.builder_settings?.theme?.content;\n}\n\nexport function getProducts(): StorefrontProduct[] {\n const initialData = window.__SHOPPEX_INITIAL__;\n if (initialData === undefined || initialData.products === undefined) {\n return [];\n }\n if (!Array.isArray(initialData.products)) {\n throw new Error('Invalid Shoppex initial data: products must be an array.');\n }\n\n return initialData.products.map(parseProduct);\n}\n\nexport function getProductGroups(): StorefrontGroup[] {\n const groups = window.__SHOPPEX_INITIAL__?.groups;\n if (groups === undefined) return [];\n if (!Array.isArray(groups)) {\n throw new Error('Invalid Shoppex initial data: groups must be an array.');\n }\n return groups\n .map((value, index) => {\n const context = `group at index ${index}`;\n const group = requireUnknownRecord(value, context);\n const productIds = Array.isArray(group.product_uniqids)\n ? group.product_uniqids.filter((entry): entry is string => typeof entry === 'string' && entry.length > 0)\n : [];\n return {\n id: requireString(group, 'uniqid', context),\n title: requireString(group, 'title', context),\n description: optionalString(group.description),\n imageUrl: firstOptionalString(group, ['cdn_image_url', 'image_url']),\n categoryId: optionalString(group.category_id),\n productIds,\n productsCount: normalizeOptionalNonNegativeNumber(group.products_count) ?? productIds.length,\n sortPriority: normalizeOptionalNonNegativeNumber(group.sort_priority) ?? 0,\n };\n });\n}\n\nexport function getStorefrontCategories(): StorefrontCategory[] {\n const categories = window.__SHOPPEX_INITIAL__?.categories;\n if (categories === undefined) return [];\n if (!Array.isArray(categories)) {\n throw new Error('Invalid Shoppex initial data: categories must be an array.');\n }\n return categories.map((value, index) => {\n const context = `category at index ${index}`;\n const category = requireUnknownRecord(value, context);\n const id = requireString(category, 'uniqid', context);\n return {\n id,\n slug: optionalString(category.slug) ?? id,\n title: requireString(category, 'title', context),\n // Same pair of keys the group records use — `transformStorefrontCategory`\n // emits the CDN URL, and the older spelling is what a cached payload has.\n imageUrl: firstOptionalString(category, ['cdn_image_url', 'image_url']),\n productsCount: normalizeOptionalNonNegativeNumber(category.products_count) ?? 0,\n groupsCount: normalizeOptionalNonNegativeNumber(category.groups_count) ?? 0,\n };\n });\n}\n\nexport function getStorefrontListing(): StorefrontListingItem[] {\n const items = window.__SHOPPEX_INITIAL__?.items;\n if (items === undefined) return [];\n if (!Array.isArray(items)) {\n throw new Error('Invalid Shoppex initial data: items must be an array.');\n }\n return items.map((value, index) => {\n const context = `listing item at index ${index}`;\n const item = requireUnknownRecord(value, context);\n return {\n type: item.item_type === 'GROUP' ? ('group' as const) : ('product' as const),\n id: requireString(item, 'uniqid', context),\n sortPriority: normalizeOptionalNonNegativeNumber(item.sort_priority) ?? index,\n };\n });\n}\n\n/** Presentation order of the social icon row — the platform's, not the theme's. */\nconst SOCIAL_NETWORKS: readonly SocialNetwork[] = [\n 'discord',\n 'telegram',\n 'youtube',\n 'twitter',\n 'instagram',\n 'tiktok',\n 'facebook',\n 'reddit',\n 'email',\n];\n\nexport function getStorefrontShop(productCount: number): StorefrontShop {\n const store = window.__SHOPPEX_INITIAL__?.store;\n if (!store) {\n throw new Error('Invalid Shoppex initial data: store is required.');\n }\n const social = isRecord(store.social) ? store.social : {};\n const feedback = isRecord(store.feedback) ? store.feedback : {};\n // Not the integer helper: a 4.8 average floored to 4 would understate every\n // shop's rating by up to a full star.\n const ratingRaw = Number(store.average_score ?? store.rating);\n const rating = Number.isFinite(ratingRaw) && ratingRaw > 0 ? ratingRaw : null;\n // `tos_enabled` is the merchant's switch, `terms_of_service` the document.\n // A shop that stored terms and then turned them off has not published them,\n // so both have to agree before the storefront links to or renders the page.\n const termsHtml = store.tos_enabled === true ? optionalString(store.terms_of_service) : null;\n\n return {\n name: requireString(store, 'name', 'store'),\n logoUrl: firstOptionalString(store, ['logo', 'avatar']),\n avatarUrl: firstOptionalString(store, ['avatar', 'logo']),\n socialLinks: SOCIAL_NETWORKS.flatMap((network) => {\n const value = optionalString(social[network]);\n if (!value) return [];\n // The merchant types a mail address, not a URL; every other network is\n // stored as a full link.\n const href = network === 'email' && !value.includes(':') ? `mailto:${value}` : value;\n return [{ network, href }];\n }),\n termsHtml,\n hasPublishedTerms: termsHtml !== null,\n rewardProgram: parseRewardProgram(store.reward_program_summary),\n resellerProgram: parseResellerProgram(store.reseller_program_summary),\n stats: {\n productsSold: normalizeOptionalNonNegativeNumber(store.products_sold_count) ?? 0,\n customers: normalizeOptionalNonNegativeNumber(store.customers_count ?? store.buyers_count) ?? 0,\n reviewCount: normalizeOptionalNonNegativeNumber(feedback.total) ?? 0,\n averageRating: rating,\n productCount,\n },\n };\n}\n\n/**\n * The shop's reward program off the wire, gated the way the platform's own\n * mapper gates it (`mapRewardProgramSummary` in\n * `packages/storefront-render-core/src/map-shop-render-settings.ts`): a\n * disabled program, a program with no headline, and a program whose every perk\n * is half-configured all collapse to null. Null is the shape the teasers read\n * as \"render nothing\" — never an empty card with a heading and no perks.\n */\nfunction parseRewardProgram(value: unknown): RewardProgramSummary | null {\n if (!isRecord(value) || value.enabled !== true) return null;\n const headline = optionalString(value.headline);\n if (!headline || !Array.isArray(value.items)) return null;\n const items: RewardProgramItem[] = [];\n for (const entry of value.items) {\n if (!isRecord(entry)) continue;\n const triggerType = firstOptionalString(entry, ['trigger_type', 'triggerType']);\n const actionType = firstOptionalString(entry, ['action_type', 'actionType']);\n const label = optionalString(entry.label);\n if (!triggerType || !actionType || !label) continue;\n items.push({ triggerType, actionType, label });\n }\n return items.length > 0 ? { headline, items } : null;\n}\n\nconst RESELLER_ENROLLMENT_MODES = ['OPEN', 'APPLICATION'] as const;\n\n/**\n * The shop's reseller program off the wire. The field is public shop payload\n * that older payloads simply do not carry, so it is read defensively: absent,\n * null, and an enrolment mode this template does not know all collapse to null,\n * which the footer reads as \"render nothing\".\n */\nfunction parseResellerProgram(value: unknown): ResellerProgramSummary | null {\n if (!isRecord(value)) return null;\n const mode = optionalString(value.enrollment_mode);\n return mode !== null && (RESELLER_ENROLLMENT_MODES as readonly string[]).includes(mode)\n ? { enrollmentMode: mode as ResellerProgramSummary['enrollmentMode'] }\n : null;\n}\n\nconst CURRENCY_CODE = /^[A-Z]{3}$/;\n\n/**\n * The shop's currency list, cleaned to real ISO codes.\n *\n * `available_currencies` is merchant data and arrives as whatever the column\n * holds; anything that is not a three-letter code is dropped rather than\n * offered as a currency nothing can be priced in. The active and default\n * currencies lead the list because a buyer must always be able to switch back\n * to what they are looking at.\n */\nfunction parseCurrencyList(value: unknown, leading: string[]): string[] {\n const entries = Array.isArray(value) ? value : [];\n const codes = [...leading, ...entries]\n .map((entry) => (typeof entry === 'string' ? entry.trim().toUpperCase() : ''))\n .filter((entry) => CURRENCY_CODE.test(entry));\n return [...new Set(codes)];\n}\n\n/** The four orders every Shoppex lane sorts a catalog listing by. */\nexport const STOREFRONT_SORT_KEYS = ['featured', 'newest', 'price-asc', 'price-desc'] as const;\n\nexport type StorefrontSortKey = (typeof STOREFRONT_SORT_KEYS)[number];\n\n/**\n * `store.default_sort` off the wire, normalized to one of the four keys.\n *\n * Field-for-field port of `normalizeStorefrontSortKey`\n * (`packages/storefront-render-core/src/normalize-storefront-sort-key.ts`),\n * because the column does NOT hold those keys: the response contract emits the\n * dashboard's enum (`DEFAULT`, `NEWEST`, `PRICE`, `PRICE_DESC`, `BESTSELLER`,\n * `NAME`, …). Reading it literally would leave every shop on 'featured' and the\n * merchant's chosen order would never reach either lane. An order this theme\n * cannot express — name, stock, type — degrades to the merchant's own shelf\n * order rather than to a different one.\n */\nexport function parseStorefrontSortKey(value: unknown): StorefrontSortKey {\n const raw = optionalString(value);\n if (!raw) return 'featured';\n if ((STOREFRONT_SORT_KEYS as readonly string[]).includes(raw)) return raw as StorefrontSortKey;\n switch (raw.toUpperCase()) {\n case 'NEWEST':\n return 'newest';\n case 'PRICE':\n case 'PRICE_ASC':\n return 'price-asc';\n case 'PRICE_DESC':\n return 'price-desc';\n default:\n return 'featured';\n }\n}\n\nexport function getStorefrontSettings(): StorefrontSettings {\n const store = window.__SHOPPEX_INITIAL__?.store;\n if (!store) {\n throw new Error('Invalid Shoppex initial data: store is required.');\n }\n const activeCurrency = (optionalString(store.currency) ?? '').toUpperCase();\n const defaultCurrency = (optionalString(store.default_currency) ?? activeCurrency).toUpperCase();\n return {\n availableCurrencies: parseCurrencyList(store.available_currencies, [\n activeCurrency,\n defaultCurrency,\n ]),\n defaultCurrency,\n activeCurrency,\n paymentMethods: parsePaymentMethods(store.payment_methods),\n cartEnabled: optionalBoolean(store.cart_enabled, true, 'store.cart_enabled'),\n hideOutOfStock: optionalBoolean(store.hide_out_of_stock, false, 'store.hide_out_of_stock'),\n hideStockCounter: optionalBoolean(store.hide_stock_counter, false, 'store.hide_stock_counter'),\n searchEnabled: optionalBoolean(store.search_enabled, true, 'store.search_enabled'),\n sortEnabled: optionalBoolean(store.sort_enabled, true, 'store.sort_enabled'),\n // The three presentation switches, at the defaults their columns carry.\n centerGroupTitles: optionalBoolean(\n store.center_group_titles,\n false,\n 'store.center_group_titles',\n ),\n descriptionSkipDefaultImage: optionalBoolean(\n store.description_skip_default_image,\n false,\n 'store.description_skip_default_image',\n ),\n descriptionImage: optionalBoolean(store.description_image, true, 'store.description_image'),\n defaultSort: parseStorefrontSortKey(store.default_sort),\n // Both contact switches are MERCHANT settings the serializer always emits.\n // Defaulting them meant a shop that had turned the form off still showed\n // one, and a shop that required an invoice number never asked for it — the\n // visitor only found out from the backend's refusal. A payload without them\n // is broken, so it fails here instead.\n contactFormEnabled: requireBoolean(store.contact_form_enabled, 'store.contact_form_enabled'),\n stockAlertsEnabled: requireBoolean(store.stock_alerts_enabled, 'store.stock_alerts_enabled'),\n requireInvoiceForContact: requireBoolean(\n store.require_invoice_for_contact,\n 'store.require_invoice_for_contact',\n ),\n };\n}\n\nexport function getStorefrontAddons(): StorefrontAddon[] {\n const addons = window.__SHOPPEX_INITIAL__?.addons;\n if (addons === undefined) return [];\n if (!isRecord(addons) || !Array.isArray(addons.items)) {\n throw new Error('Invalid Shoppex initial data: addons.items must be an array.');\n }\n return [...addons.items].sort((a, b) => a.sort_order - b.sort_order);\n}\n\n/**\n * The stock a buyer can actually reach, in ONE number: null is unlimited.\n *\n * Variants are the trap. A variant product's own `stock` column is not the\n * inventory anyone buys — the variants hold it — so a product with two\n * unbounded variants and a `stock` of 0 is not sold out. Serial products are\n * the exception: their stock IS the key pool on the product row.\n */\nexport function resolveDisplayStock(product: StorefrontProduct): number | null {\n if (product.productType === 'SERIALS' || product.priceVariants.length === 0) {\n return product.stock;\n }\n // One unbounded option makes the whole product unbounded; otherwise the\n // options add up, because a buyer may take any of them.\n if (product.priceVariants.some((variant) => variant.stock === null)) return null;\n return product.priceVariants.reduce((total, variant) => total + Math.max(variant.stock ?? 0, 0), 0);\n}\n\nexport function isProductSoldOut(product: StorefrontProduct): boolean {\n return resolveDisplayStock(product) === 0;\n}\n\n/**\n * The stock behind ONE selected option — null is unlimited.\n *\n * A variant without its own counter inherits the product's, and the whole\n * product falls back to the product row when EVERY variant reports zero while\n * the product itself has units — that combination is a catalog that never\n * populated per-variant stock, not a sold-out product.\n */\nexport function resolveVariantStock(\n product: StorefrontProduct,\n variant: StorefrontPriceVariant,\n): number | null {\n const useProductStock = product.priceVariants.length > 0\n && (product.stock ?? 0) !== 0\n && product.priceVariants.every((entry) => (entry.stock ?? 0) === 0);\n if (useProductStock || variant.stock === null) return product.stock;\n return variant.stock;\n}\n\n/**\n * The one sold-out predicate every add path passes.\n *\n * PAYMENT PATH. A product-level read is not enough: an option of an otherwise\n * stocked product can be sold out on its own, and the backend refuses that line\n * at invoice creation (`errors.checkout.insufficient_stock`). Adding it anyway\n * only moves the dead end to checkout.\n */\nexport function isSelectionSoldOut(\n product: StorefrontProduct,\n variant: StorefrontPriceVariant | null,\n): boolean {\n if (variant === null) {\n if (isProductSoldOut(product)) return true;\n // A variant product's orderable range is a per-OPTION question — each option carries its own\n // minimum and its own counter — so only a variant-less product may be judged on the product row.\n if (product.priceVariants.length > 0) return false;\n } else if (resolveVariantStock(product, variant) === 0) {\n return true;\n }\n // PAYMENT PATH, the second way a selection can be unsellable: an EMPTY effective range.\n // `quantityMin: 2` against one remaining unit is {min 2, max 1} — no quantity the buyer is\n // allowed to order can be served. Reading `stock === 0` alone called that option in stock, so the\n // add reached the SDK and only died at invoice creation (`errors.checkout.insufficient_stock`),\n // after the buyer had been told it landed. Pay-what-you-want inherits this: every add path,\n // named price or not, passes through here.\n const { min, max } = resolveQuantityBounds(product, variant);\n return max !== null && min > max;\n}\n\n/**\n * The stock counter resolves per product: a product's own hide_stock_counter\n * wins, null inherits the shop-wide setting — same rule the document lane's\n * readHideStockCounterForProduct applies.\n */\nexport function resolveHideStockCounter(\n product: StorefrontProduct,\n settings: StorefrontSettings,\n): boolean {\n return product.hideStockCounter ?? settings.hideStockCounter;\n}\n\n/**\n * How many units the buyer may put on ONE line, for the selected option.\n *\n * A variant overrides the product's own bounds, and the stock caps whatever the\n * merchant configured: offering a stepper that runs past the inventory produces\n * a cart the quote then rejects. `max: null` is unlimited, never zero.\n */\nexport function resolveQuantityBounds(\n product: StorefrontProduct,\n variant: StorefrontPriceVariant | null,\n): { min: number; max: number | null } {\n const min = variant?.quantityMin ?? product.quantityMin;\n const configuredMax = variant?.quantityMax ?? product.quantityMax;\n // The stock a buyer can actually REACH for this selection, not the raw column: an option without\n // its own counter inherits the product's, and the all-zero legacy shape reads the product row.\n // The sold-out gate resolves the same numbers, so the stepper and the gate can never disagree —\n // a bare `variant.stock` capped the legacy shape at 0 and made every option look unsellable.\n const stockMax = variant === null ? resolveDisplayStock(product) : resolveVariantStock(product, variant);\n const max = configuredMax === null\n ? stockMax\n : stockMax === null\n ? configuredMax\n : Math.min(configuredMax, stockMax);\n return { min, max };\n}\n\nfunction parseProduct(value: unknown, index: number): StorefrontProduct {\n const context = `product at index ${index}`;\n const product = requireUnknownRecord(value, context);\n const id = requireString(product, 'uniqid', context);\n // The real db id — falls back to uniqid if the payload omits it.\n const productId = optionalString(product.id) ?? id;\n // price_discount is a PERCENT (0–100) off the wire prices, which arrive\n // undiscounted — the document lane applies it at display time and the\n // backend applies it again at charge time, so a storefront that skips it\n // shows a higher number than the buyer pays. EXCEPT for pay-what-you-want:\n // the server validates the buyer's amount against the UNDISCOUNTED minimum\n // and deliberately disables the product discount there, so discounting a\n // PWYW price would offer a default the checkout then rejects as too low.\n const discountPercent = product.pay_what_you_want === true\n ? 0\n : normalizeDiscountPercent(product.price_discount);\n const applyDiscount = (amount: number): number =>\n discountPercent > 0 ? roundCurrency(amount * (1 - discountPercent / 100)) : amount;\n const rawVariants = parsePriceVariants(product.price_variants, context);\n // Per-unit rounding is display-only (a quantity of 1 rounds identically at\n // line level); quantity math must go through calculateDisplayLineTotal on\n // baseUnitPrice, or fractional cents disagree with the quote.\n const priceVariants = rawVariants.map((variant) => ({\n ...variant,\n baseUnitPrice: variant.price,\n price: applyDiscount(variant.price),\n }));\n // Variant prices are authoritative whenever variants exist — the catalog\n // payload routinely carries price 0 on the product row of a variant product,\n // so the base price is only the fallback for variant-less products. Same\n // rule as the document lane's resolveProductStartingPrice.\n const basePrice = parseFiniteNumber(product.price_display ?? product.price, `${context}.price`);\n const undiscountedPrice = rawVariants.length\n ? Math.min(...rawVariants.map((variant) => variant.price))\n : basePrice;\n const price = applyDiscount(undiscountedPrice);\n const quantityMin = normalizeMinimum(product.quantity_min, 1, `${context}.quantity_min`);\n const rating = deriveProductRating(product.feedback);\n const productType = parseProductType(product.type);\n\n return {\n id,\n productId,\n productType,\n name: requireString(product, 'title', context),\n price,\n currency: requireString(product, 'currency', context),\n description: optionalString(product.description),\n slug: optionalString(product.slug),\n imageUrl: resolveProductImage(product),\n imageUrls: parseProductImageUrls(product.images),\n stock: applyOrderable(\n normalizeStock(product.stock, `${context}.stock`),\n product.orderable,\n product.supplier_backed,\n ),\n lowStockThreshold: normalizeLowStockThreshold(\n normalizeOptionalNonNegativeNumber(product.quantity_warning),\n ),\n hideStockCounter:\n typeof product.hide_stock_counter === 'boolean' ? product.hide_stock_counter : null,\n volumeDiscounts: parseVolumeDiscounts(product.volume_discounts),\n payWhatYouWant: product.pay_what_you_want === true,\n discountPercent,\n baseUnitPrice: undiscountedPrice,\n basePrice,\n priceVariants,\n quantityMin,\n quantityMax: normalizeMaximum(product.quantity_max, quantityMin, `${context}.quantity_max`),\n soldCount: normalizeOptionalNonNegativeNumber(product.sold_count) ?? 0,\n bestseller: product.bestseller === true,\n badges: parseProductBadges(product.badges),\n sortPriority: normalizeOptionalNonNegativeNumber(product.sort_priority) ?? 0,\n createdAt: optionalString(product.created_at),\n categories: parseCategories(product.categories),\n bundles: parseBundles(product.bundles),\n automaticDeals: parseAutomaticDeals(product.automatic_deals),\n videoEmbedSrc: resolveProductVideoEmbedSrc(product.video_link),\n addons: parseAddons(product.addons),\n customFields: parseCustomFields(product.custom_fields, context),\n // Strict on purpose: the serializer always emits `on_hold`, so an absent\n // flag is the only tolerated shape (false) and a present non-boolean is a\n // changed contract, not a paused product. Guessing here sells a product the\n // merchant paused.\n onHold: optionalBoolean(product.on_hold, false, `${context}.on_hold`),\n compareAtPrice: price < undiscountedPrice ? undiscountedPrice : null,\n rating: rating?.average ?? null,\n ratingCount: rating?.count ?? 0,\n ratingTally: rating?.tally ?? null,\n shortDescription: optionalString(product.short_description),\n deliverable: parseDeliverable(product.type),\n unitLabelSingular: optionalString(product.label_singular),\n unitLabelPlural: optionalString(product.label_plural),\n serviceStatus: parseServiceStatus(product.service_status, context),\n highlights: parseHighlights(product.product_highlights),\n metadata: parseMetadata(product.metadata),\n descriptionTabs: parseContentTabs(product.description_tabs),\n faqs: parseFaqs(product.faqs),\n license: parseProductLicense(product),\n licensingEnabled: product.licensing_enabled === true || product.licensing_enabled === 1,\n deliveryTimeStated: Object.prototype.hasOwnProperty.call(product, 'delivery_time') && product.delivery_time !== undefined,\n deliveryTimeCount: normalizeDeliveryCount(product.delivery_time),\n deliveryTimeUnit: normalizeDeliveryTimeUnit(product.delivery_time_unit),\n subscription: parseSubscription(product, productType),\n };\n}\n\n\n/* -------------------------------------------------------------------------\n * LICENCE TERM\n *\n * Ported verbatim from templates/code-storefronts/nova. The two code-lane\n * templates are independent merchant starting points with no shared package,\n * and this one carried the same defect nova did: it read the product row only,\n * ignored the selected option, dropped lifetime, and never checked\n * licensing_enabled. Keeping the resolution identical is what stops the two\n * templates from stating different terms for the same product.\n * ---------------------------------------------------------------------- */\n\nexport type LicenseTerm =\n | { kind: 'lifetime' }\n | { kind: 'period'; days: number };\n\n/**\n * What ONE row — a product or one of its options — states about its own term.\n *\n * `inherit` is an option saying the product answers for it; only an option may say it, because the\n * product is the root of the term. `unresolved` is a row stating a term nobody can read: an unknown\n * mode, a PERIOD carrying no days, a legacy `'90d'` that was never validated.\n *\n * These three are separate states on purpose. They used to share one nullable column — `NULL` meant\n * \"inherit\" on an option and \"nothing\" on a product — and on 2026-08-18 the unnamed state was\n * rendered as \"Lifetime\": 5,333 licences were sold as perpetual and issued with a 30-day expiry.\n * Neither `inherit` nor `unresolved` may become a term here.\n */\nexport type LicenseStatement =\n | LicenseTerm\n | { kind: 'inherit' }\n | { kind: 'unresolved' };\n\nfunction readLicensePeriodDays(value: unknown): number | null {\n if (typeof value !== 'number' && typeof value !== 'string') return null;\n const days = Number(value);\n return Number.isInteger(days) && days > 0 ? days : null;\n}\n\nfunction readLicenseMode(value: unknown): string | null {\n return typeof value === 'string' && value.trim() !== '' ? value.trim().toUpperCase() : null;\n}\n\n/** `INHERIT` is answered by the caller: only an option may state it. */\nfunction licenseStatementFromMode(mode: string, record: Record<string, unknown>): LicenseStatement {\n if (mode === 'LIFETIME') return { kind: 'lifetime' };\n if (mode !== 'PERIOD') return { kind: 'unresolved' };\n const days = readLicensePeriodDays(record.license_period_days);\n return days === null ? { kind: 'unresolved' } : { kind: 'period', days };\n}\n\n/** What the product row states. A product is the root of the term, so `INHERIT` reads as nothing. */\nfunction parseProductLicense(record: Record<string, unknown>): LicenseStatement {\n const mode = readLicenseMode(record.license_mode);\n if (mode !== null) return licenseStatementFromMode(mode, record);\n const legacy = record.license_period;\n if (typeof legacy === 'string' && legacy.trim().toUpperCase() === 'LIFETIME') return { kind: 'lifetime' };\n if (legacy === 0 || (typeof legacy === 'string' && legacy.trim() === '0')) return { kind: 'lifetime' };\n const days = readLicensePeriodDays(legacy);\n return days === null ? { kind: 'unresolved' } : { kind: 'period', days };\n}\n\n/** What one option states. Legacy `NULL` meant \"inherit from the product\" — now it is named. */\nfunction parseVariantLicense(record: Record<string, unknown>): LicenseStatement {\n const mode = readLicenseMode(record.license_mode);\n if (mode === 'INHERIT') return { kind: 'inherit' };\n if (mode !== null) return licenseStatementFromMode(mode, record);\n const legacy = record.license_period;\n if (legacy === null || legacy === undefined) return { kind: 'inherit' };\n if (legacy === 0 || (typeof legacy === 'string' && legacy.trim() === '0')) return { kind: 'lifetime' };\n const days = readLicensePeriodDays(legacy);\n return days === null ? { kind: 'unresolved' } : { kind: 'period', days };\n}\n\n/**\n * The term the buyer is being sold for the option they have selected, or `null` when nothing about\n * it is resolvable — licensing off, a mode nobody can read, a PERIOD with no days, an option\n * inheriting from a product that states nothing itself.\n *\n * `null` means the page renders NO licence row. It never means \"Lifetime\".\n */\nexport function resolveLicenseTerm(\n product: StorefrontProduct,\n variant: StorefrontPriceVariant | null,\n): LicenseTerm | null {\n if (!product.licensingEnabled) return null;\n const stated = variant ? variant.license : ({ kind: 'inherit' } as const);\n if (stated.kind === 'lifetime' || stated.kind === 'period') return stated;\n if (stated.kind === 'unresolved') return null;\n const inherited = product.license;\n return inherited.kind === 'lifetime' || inherited.kind === 'period' ? inherited : null;\n}\n\n/** \"Lifetime\" / \"30 days\" — the same wording the Easy lane prints for the same product. */\nexport function formatLicenseTerm(term: LicenseTerm): string {\n return term.kind === 'lifetime' ? 'Lifetime' : `${term.days} ${term.days === 1 ? 'day' : 'days'}`;\n}\n\n\nfunction parsePriceVariants(value: unknown, productContext: string): StorefrontPriceVariant[] {\n if (value === null || value === undefined) return [];\n const source = Array.isArray(value)\n ? value\n : isRecord(value) && Array.isArray(value.price_variants)\n ? value.price_variants\n : null;\n if (!source) {\n throw new Error(`Invalid Shoppex ${productContext}.price_variants: expected an array.`);\n }\n return source.map((entry, index) => {\n const context = `${productContext}.price_variants.${index}`;\n const variant = requireUnknownRecord(entry, context);\n const quantityMin = normalizeOptionalMinimum(variant.quantity_min, context);\n // price_display is the buyer-currency amount; price is the shop-currency\n // base. The document lane prefers price_display too. Both fields start\n // undiscounted here; parseProduct applies the percent to `price` only.\n const unitPrice = parseFiniteNumber(\n variant.price_display ?? variant.price,\n `${context}.price`,\n );\n return {\n id: requireString(variant, 'id', context),\n label: requireStringFromKeys(variant, ['label', 'title'], context),\n description: optionalString(variant.description),\n license: parseVariantLicense(variant),\n price: unitPrice,\n baseUnitPrice: unitPrice,\n stock: applyOrderable(\n normalizeStock(variant.stock, `${context}.stock`),\n variant.orderable,\n variant.supplier_backed,\n ),\n quantityMin,\n quantityMax: normalizeMaximum(variant.quantity_max, quantityMin ?? 1, `${context}.quantity_max`),\n imageUrl: optionalString(variant.image_url),\n isFeatured: variant.is_featured === true || variant.isFeatured === true,\n };\n });\n}\n\n/**\n * Categories off a product. The API sends objects ({title, uniqid, …}); a\n * category without both an id and a title is dropped rather than rendered as\n * an unmatchable filter chip.\n */\nfunction parseCategories(value: unknown): ProductCategoryRef[] {\n if (!Array.isArray(value)) return [];\n const categories: ProductCategoryRef[] = [];\n for (const entry of value) {\n if (!isRecord(entry)) continue;\n const id = optionalString(entry.uniqid ?? entry.id);\n const title = optionalString(entry.title);\n if (!id || !title || categories.some((category) => category.id === id)) continue;\n categories.push({ id, title });\n }\n return categories;\n}\n\n/**\n * The product kind. An unknown kind is NOT an error — the platform may ship a\n * new one before this template knows about it — but it must not silently pick\n * up `INFO_CARD` behaviour, so it degrades to the plain buyable product.\n */\nfunction parseProductType(value: unknown): ProductType {\n const normalized = typeof value === 'string' ? value.trim().toUpperCase() : '';\n return (PRODUCT_TYPES as readonly string[]).includes(normalized)\n ? (normalized as ProductType)\n : 'PRODUCT';\n}\n\n/**\n * Bundle offers off a product. A bundle needs at least two resolvable items to\n * mean anything, so partially-resolved ones (a member product was deleted) are\n * dropped rather than advertised as a deal the buyer cannot complete.\n */\nfunction parseBundles(value: unknown): ProductBundle[] {\n if (!Array.isArray(value)) return [];\n const bundles: ProductBundle[] = [];\n for (const entry of value) {\n if (!isRecord(entry)) continue;\n const rawItems = Array.isArray(entry.products) ? entry.products : [];\n const items: ProductBundleItem[] = [];\n for (const rawItem of rawItems) {\n if (!isRecord(rawItem)) continue;\n const id = typeof rawItem.uniqid === 'string' ? rawItem.uniqid.trim() : '';\n const title = typeof rawItem.title === 'string' ? rawItem.title.trim() : '';\n if (!id || !title) continue;\n const unitPrice = Number(rawItem.unit_price);\n items.push({\n id,\n title,\n variantId: typeof rawItem.default_variant_id === 'string' && rawItem.default_variant_id\n ? rawItem.default_variant_id\n : 'default',\n unitPrice: Number.isFinite(unitPrice) && unitPrice >= 0 ? unitPrice : 0,\n imageUrl: optionalString(rawItem.image_url),\n variantTitle: optionalString(rawItem.variant_title),\n });\n }\n if (items.length < 2) continue;\n\n const amount = Number(entry.discount_amount);\n bundles.push({\n id: typeof entry.uniqid === 'string' && entry.uniqid ? entry.uniqid : String(entry.id ?? ''),\n title: typeof entry.title === 'string' && entry.title.trim()\n ? entry.title.trim()\n : 'Bundle offer',\n discountType: entry.discount_type === 'FIXED' ? 'FIXED' : 'PERCENTAGE',\n discountAmount: Number.isFinite(amount) && amount > 0 ? amount : 0,\n items,\n });\n }\n return bundles;\n}\n\n/**\n * Paid extras off a product. An addon without an id cannot be selected or sent\n * to the cart, and one without a title cannot be labelled, so both are required\n * — a half-resolved row would render as an unnamed surcharge.\n */\nfunction parseAddons(value: unknown): ProductAddon[] {\n if (!Array.isArray(value)) return [];\n const addons: ProductAddon[] = [];\n for (const entry of value) {\n if (!isRecord(entry)) continue;\n const id = optionalString(entry.id);\n const title = optionalString(entry.title ?? entry.name);\n if (!id || !title || addons.some((addon) => addon.id === id)) continue;\n const price = Number(entry.price);\n addons.push({\n id,\n title,\n price: Number.isFinite(price) && price > 0 ? price : 0,\n required: entry.required === true,\n });\n }\n return addons;\n}\n\n/**\n * The product's gallery image URLs. Entries arrive as attachment records\n * (`{ id, uniqid, cloudflare_image_id, url }`); one without a resolved `url`\n * has no picture to show and is dropped rather than rendered as a blank frame.\n */\nfunction parseProductImageUrls(value: unknown): string[] {\n if (!Array.isArray(value)) return [];\n const urls: string[] = [];\n for (const entry of value) {\n const url = typeof entry === 'string' ? optionalString(entry) : isRecord(entry) ? optionalString(entry.url) : null;\n if (url && !urls.includes(url)) urls.push(url);\n }\n return urls;\n}\n\n/** Bulleted highlights — strings only, empties dropped. Missing → []. */\n/**\n * Metadata rides the wire as a flat string->string object. Non-string values\n * are dropped rather than coerced so a consumer can rely on the declared type.\n */\nfunction parseMetadata(value: unknown): Record<string, string> {\n if (!isRecord(value)) return {};\n const result: Record<string, string> = {};\n for (const [key, entry] of Object.entries(value)) {\n if (typeof entry === 'string') result[key] = entry;\n }\n return result;\n}\n\nfunction parseHighlights(value: unknown): string[] {\n if (!Array.isArray(value)) return [];\n return value\n .map((entry) =>\n typeof entry === 'string'\n ? entry.trim()\n : isRecord(entry)\n ? optionalString(entry.text ?? entry.title ?? entry.label)\n : null,\n )\n .filter((entry): entry is string => Boolean(entry));\n}\n\n/** Merchant content tabs ({title, content|html}). Missing/malformed → []. */\nfunction parseContentTabs(value: unknown): ProductContentTab[] {\n if (!Array.isArray(value)) return [];\n const tabs: ProductContentTab[] = [];\n for (const entry of value) {\n if (!isRecord(entry)) continue;\n const title = optionalString(entry.title);\n const html = optionalString(entry.content ?? entry.html ?? entry.contentHtml);\n if (title && html) tabs.push({ title, html });\n }\n return tabs;\n}\n\n/**\n * The buyer details this product requires, off the wire.\n *\n * PAYMENT PATH. `custom_fields` arrives as a JSON STRING on the catalog\n * contract (`stringifyJsonField` in the backend's response transformer), not as\n * an array, and it may wrap the list in `{ custom_fields: [...] }`. Every one of\n * those shapes is what the SDK's `normalizeStorefrontCustomFields` accepts, and\n * this is a field-for-field port of it — the Easy lane validates a merchant's\n * definitions with that function, so a definition the two lanes read\n * differently is a product one lane can sell and the other cannot.\n *\n * Ported rather than imported because the theme consumes the SDK as a UMD\n * global and these helpers are module exports of `@shoppex/sdk`, not members of\n * `window.shoppex`.\n *\n * `hidden` fields are dropped (the buyer never fills those in) and so is any\n * entry without a name, which could not be keyed onto a cart line anyway.\n */\nfunction parseCustomFields(value: unknown, context: string): ProductCustomField[] {\n const source = readCustomFieldList(value, context);\n const fields: ProductCustomField[] = [];\n for (const entry of source) {\n if (!isRecord(entry)) continue;\n const name = optionalString(entry.name);\n if (!name) continue;\n const rawType = typeof entry.type === 'string' ? entry.type.trim().toLowerCase() : '';\n const type = rawType || 'text';\n if (type === 'hidden') continue;\n // The definition's own pre-filled value, under any of the three keys the\n // dashboard has written it as.\n const defaultValue = firstOptionalString(entry, ['default_value', 'default', 'value']);\n fields.push({\n name,\n type,\n // The dashboard has stored this flag as a boolean, a number and a string\n // over time; the SDK accepts all three, and reading only one of them\n // would turn a required field into an optional one — which is exactly the\n // failure that lets a product reach checkout without its values.\n required: entry.required === true\n || entry.required === 1\n || entry.required === '1'\n || entry.required === 'true',\n defaultValue: defaultValue ?? '',\n placeholder: typeof entry.placeholder === 'string' ? entry.placeholder : '',\n regex: optionalString(entry.regex),\n });\n }\n return fields;\n}\n\n/** The three shapes `custom_fields` arrives in: a JSON string, a wrapper record, or a bare array. */\nfunction readCustomFieldList(value: unknown, context: string): unknown[] {\n if (Array.isArray(value)) return value;\n if (typeof value === 'string') {\n const text = value.trim();\n if (!text) return [];\n try {\n return readCustomFieldList(JSON.parse(text) as unknown, context);\n } catch {\n // Merchant-authored JSON that no longer parses. It cannot be repaired\n // into field definitions, and inventing none would sell a product whose\n // required details were never asked for — so the payload is rejected.\n throw new Error(`Invalid Shoppex ${context}.custom_fields: expected valid JSON.`);\n }\n }\n if (isRecord(value)) {\n const fields = value.custom_fields ?? value.customFields;\n return Array.isArray(fields) ? fields : [];\n }\n return [];\n}\n\n/**\n * Active quantity deals off a product, from the catalog's `automatic_deals`.\n *\n * The two quantities and the discount are what the wording states, so a rule\n * missing any of them is dropped rather than rendered as an offer with a blank\n * in it — exactly the rows the Easy lane's `dealRow` returns '' for. A rule the\n * theme cannot phrase is a rule the buyer cannot act on, and the backend\n * applies it either way, so dropping it understates rather than misleads.\n */\nfunction parseAutomaticDeals(value: unknown): ProductAutomaticDeal[] {\n if (!Array.isArray(value)) return [];\n const deals: ProductAutomaticDeal[] = [];\n for (const entry of value) {\n if (!isRecord(entry)) continue;\n const requiredQuantity = Number(entry.required_quantity);\n const discountedQuantity = Number(entry.discounted_quantity);\n if (\n !Number.isInteger(requiredQuantity) || requiredQuantity < 1\n || !Number.isInteger(discountedQuantity) || discountedQuantity < 1\n ) {\n continue;\n }\n const isFree = optionalString(entry.discount_type)?.toUpperCase() === 'FREE';\n const discountValue = Number(entry.discount_value);\n // A percentage rule with no percent off is not an offer.\n if (!isFree && !(Number.isFinite(discountValue) && discountValue > 0)) continue;\n const cap = Number(entry.max_applications_per_cart);\n deals.push({\n id: optionalString(entry.uniqid) ?? `deal-${deals.length}`,\n title: optionalString(entry.title) ?? '',\n requiredQuantity,\n discountedQuantity,\n discountType: isFree ? 'FREE' : 'PERCENTAGE',\n discountValue: isFree ? 0 : discountValue,\n maxApplicationsPerCart: Number.isInteger(cap) && cap > 0 ? cap : null,\n endsAt: optionalString(entry.ends_at),\n });\n }\n return deals;\n}\n\n/** Per-product FAQ ({question, answer}). Missing/malformed → []. */\nfunction parseFaqs(value: unknown): ProductFaq[] {\n if (!Array.isArray(value)) return [];\n const faqs: ProductFaq[] = [];\n for (const entry of value) {\n if (!isRecord(entry)) continue;\n const question = optionalString(entry.question);\n const answer = optionalString(entry.answer);\n if (question && answer) faqs.push({ question, answer });\n }\n return faqs;\n}\n\n/**\n * volume_discounts off the wire: either a bare array or `{ volume_discounts:\n * [...] }`, entries keyed `quantity`/`min_quantity` + `value`/`discount` +\n * `type`. Same tolerant shape the document lane's parseVolumeDiscountRules\n * accepts; malformed entries are dropped rather than guessed at.\n */\nfunction parseVolumeDiscounts(value: unknown): VolumeDiscountRule[] {\n const source = Array.isArray(value)\n ? value\n : isRecord(value) && Array.isArray(value.volume_discounts)\n ? value.volume_discounts\n : [];\n const rules: VolumeDiscountRule[] = [];\n for (const entry of source) {\n if (!isRecord(entry)) continue;\n const quantity = Number(entry.quantity ?? entry.min_quantity);\n const amount = Number(entry.value ?? entry.discount ?? entry.amount);\n const typeRaw = String(entry.type ?? entry.discount_type ?? '').toUpperCase();\n const type = typeRaw.includes('PERCENT')\n ? 'PERCENTAGE'\n : typeRaw.includes('FIX') || typeRaw.includes('AMOUNT')\n ? 'FIXED'\n : null;\n if (\n !Number.isFinite(quantity) || quantity < 1\n || !Number.isFinite(amount) || amount <= 0\n || !type\n ) {\n continue;\n }\n rules.push({ quantity: Math.floor(quantity), type, value: amount });\n }\n return rules.sort((a, b) => a.quantity - b.quantity);\n}\n\n/**\n * Exact integer cents for an amount that is itself a valid money value.\n * toPrecision(15) trims the ~1e-16-relative binary noise (10.03 x 50% floats\n * as 5.014999...) so the half-up round agrees with the quote's Decimal\n * ROUND_HALF_UP; legitimate cents survive up to 13-digit totals.\n */\nfunction toCents(value: number): number {\n return Math.round(Number((value * 100).toPrecision(15)));\n}\n\n/** Half-up rounding to cents. Every displayed money amount rounds through here. */\nexport function roundCurrency(value: number): number {\n return toCents(value) / 100;\n}\n\n/**\n * Line total the way the quote computes it: undiscounted subtotal, percent\n * discount applied at LINE level (not per rounded unit — $9.99 at 15% × 10 is\n * $84.92, not 10 × $8.49), then the volume tier. Intermediate values stay\n * unrounded like the quote's Decimal chain; only the final amount rounds —\n * rounding between the two percent stages shifts the cent ($76.43 vs the\n * quoted $76.42 for 15% + 10% on a $99.90 line). Every displayed quantity\n * price must come from here or it disagrees with checkout.\n */\nexport function calculateDisplayLineTotal(\n baseUnitPrice: number,\n quantity: number,\n discountPercent: number,\n rules: VolumeDiscountRule[],\n): number {\n // Integer minor units: the base price is a money value, so its cents are\n // exact and the quantity multiplication stays exact. Percent stages run\n // unrounded on cents; the line rounds once in applyVolumeDiscountCents.\n const subtotalCents = toCents(baseUnitPrice) * quantity;\n const afterPercentCents = discountPercent > 0\n ? subtotalCents * (1 - discountPercent / 100)\n : subtotalCents;\n return applyVolumeDiscountCents(afterPercentCents, quantity, rules) / 100;\n}\n\n/**\n * Cent subtotal with the highest matching volume tier applied — mirrors the\n * document lane's calculateDiscountedLinePricing so the shown total matches\n * what checkout charges. This is the single rounding point of the line: the\n * subtotal arrives unrounded from the percent stage and leaves as whole cents.\n */\nfunction applyVolumeDiscountCents(\n subtotalCents: number,\n quantity: number,\n rules: VolumeDiscountRule[],\n): number {\n let matched: VolumeDiscountRule | null = null;\n for (const rule of rules) {\n if (quantity >= rule.quantity) matched = rule;\n }\n if (!matched) return Math.max(0, Math.round(subtotalCents));\n const discountCents = matched.type === 'PERCENTAGE'\n ? (subtotalCents * Math.min(Math.max(matched.value, 0), 100)) / 100\n : toCents(matched.value);\n return Math.max(0, Math.round(subtotalCents - Math.min(discountCents, subtotalCents)));\n}\n\n/**\n * Honest star rating from Shoppex feedback tallies (positive/neutral/negative).\n * Returns null when there is no feedback at all, so the UI shows no stars\n * rather than a fabricated score. Weights: positive=5, neutral=3, negative=1.\n */\nfunction deriveProductRating(\n value: unknown,\n): { average: number; count: number; tally: { positive: number; neutral: number; negative: number } } | null {\n if (!isRecord(value)) return null;\n const total = Number(value.total);\n if (!Number.isFinite(total) || total <= 0) return null;\n const positive = Number(value.positive) || 0;\n const neutral = Number(value.neutral) || 0;\n const negative = Number(value.negative) || 0;\n const counted = positive + neutral + negative;\n if (counted <= 0) return null;\n const average = (positive * 5 + neutral * 3 + negative * 1) / counted;\n return { average: Math.round(average * 10) / 10, count: counted, tally: { positive, neutral, negative } };\n}\n\nfunction resolveProductImage(product: Record<string, unknown>): string | null {\n const direct = firstOptionalString(product, [\n 'image_url',\n 'imageUrl',\n 'cdn_image_url',\n 'detail_image_url',\n 'thumbnail_url',\n 'thumbnailUrl',\n ]);\n if (direct) return direct;\n if (!Array.isArray(product.images)) return null;\n for (const image of product.images) {\n if (!isRecord(image)) continue;\n const url = optionalString(image.url);\n if (url) return url;\n }\n return null;\n}\n\nfunction normalizeStock(value: unknown, context: string): number | null {\n if (value === null || value === undefined || value === '') return null;\n const stock = parseFiniteNumber(value, context);\n return stock < 0 ? null : Math.floor(stock);\n}\n\n/**\n * Dynamic Delivery: `orderable` is the backend's buyability predicate (local\n * stock covers one unit OR a supplier source fulfils the line) and is what the\n * order-time gates apply — a supplier-backed row holds NO local stock by\n * design, so gating on `stock` alone renders it \"sold out\" and the purchase\n * can never happen. Folded into the stock value the same way the document\n * lane's applyOrderable does: an orderable row without bounding local units\n * becomes unlimited (null); everything downstream then behaves as it already\n * does for unlimited stock.\n */\nfunction applyOrderable(\n stock: number | null,\n orderable: unknown,\n supplierBacked: unknown,\n): number | null {\n if (orderable !== true) return stock;\n // Supplier-backed: local units do not bound the line, at any stock value.\n if (supplierBacked === true) return null;\n return stock === 0 ? null : stock;\n}\n\nfunction normalizeOptionalNonNegativeNumber(value: unknown): number | null {\n if (value === null || value === undefined || value === '') return null;\n const parsed = Number(value);\n return Number.isFinite(parsed) && parsed >= 0 ? Math.floor(parsed) : null;\n}\n\nfunction normalizeMinimum(value: unknown, fallback: number, context: string): number {\n if (value === null || value === undefined || value === '') return fallback;\n const minimum = Math.floor(parseFiniteNumber(value, context));\n if (minimum < 1) {\n throw new Error(`Invalid Shoppex ${context}: expected a positive integer.`);\n }\n return minimum;\n}\n\nfunction normalizeOptionalMinimum(value: unknown, context: string): number | null {\n if (value === null || value === undefined || value === '') return null;\n return normalizeMinimum(value, 1, `${context}.quantity_min`);\n}\n\nfunction normalizeMaximum(value: unknown, minimum: number, context: string): number | null {\n if (value === null || value === undefined || value === '') return null;\n const maximum = Math.floor(parseFiniteNumber(value, context));\n if (maximum < 0) return null;\n if (maximum < minimum) {\n throw new Error(`Invalid Shoppex ${context}: maximum cannot be below minimum.`);\n }\n return maximum;\n}\n\n/** quantity_warning uses 0 for \"disabled\" — map that to null, not a live threshold. */\nfunction normalizeLowStockThreshold(value: number | null): number | null {\n return value !== null && value > 0 ? value : null;\n}\n\n/**\n * price_discount off the wire, clamped to a usable percent. The API contract\n * pins it to an integer 0–100; anything outside that or non-numeric means \"no\n * discount\" rather than a guessed one.\n */\nfunction normalizeDiscountPercent(value: unknown): number {\n if (value === null || value === undefined || value === '') return 0;\n const percent = Number(value);\n if (!Number.isFinite(percent) || percent <= 0) return 0;\n return Math.min(percent, 100);\n}\n\n/** Positive-integer days, or null when unset/zero/invalid. */\n/** Delivery time as the checkout reads it: a positive count rounded UP, anything else no wait. */\nfunction normalizeDeliveryCount(value: unknown): number | null {\n if (value === null || value === undefined || value === '') return null;\n const parsed = Number(value);\n return Number.isFinite(parsed) && parsed > 0 ? Math.ceil(parsed) : null;\n}\n\nfunction normalizePositiveDays(value: unknown): number | null {\n if (value === null || value === undefined || value === '') return null;\n const parsed = Number(value);\n return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : null;\n}\n\n/**\n * Delivery units, mirroring the `products.delivery_time_unit` CHECK constraint.\n * The column is NOT NULL DEFAULT 'days', so an absent value only comes from a\n * payload minted before the unit existed — and days is what it meant.\n */\nexport const DELIVERY_TIME_UNITS = ['minutes', 'hours', 'days'] as const;\nexport type DeliveryTimeUnit = (typeof DELIVERY_TIME_UNITS)[number];\n\nfunction normalizeDeliveryTimeUnit(value: unknown): DeliveryTimeUnit {\n return DELIVERY_TIME_UNITS.includes(value as DeliveryTimeUnit)\n ? (value as DeliveryTimeUnit)\n : 'days';\n}\n\n/** \"1 hour\" / \"3 days\" — the count with its unit, singular when the count is one. */\nexport function formatDeliveryTime(count: number, unit: DeliveryTimeUnit): string {\n return `${count} ${count === 1 ? unit.slice(0, -1) : unit}`;\n}\n","/**\n * The per-product details a buyer has to supply before an invoice can exist.\n *\n * PAYMENT PATH, and the reason this module exists at all: a merchant who marks\n * one product custom field REQUIRED cannot sell that product until its value is\n * on the cart line. `apps/backend/src/services/storefront/invoices/from-cart.ts`\n * validates every line's `custom_fields` and rejects the whole request with\n * `custom_fields_validation_failed` — no invoice is created, so the buyer gets a\n * dead checkout button and the merchant gets no order.\n *\n * WHERE THE VALUES GO. Not into the checkout call: the SDK's `checkout()` has no\n * custom-field option. `mapCartItemsForApi` (`packages/sdk/src/modules/checkout.ts`)\n * reads `custom_fields` off each CART LINE, so the values are written onto the\n * lines with `updateCartItem(lineId, { custom_fields })` BEFORE the hand-off.\n * That is exactly what the Easy lane does — see\n * `packages/storefront-commerce/src/checkout-custom-fields.ts`, whose\n * `writeCustomFieldsToCart` runs on the same contract.\n *\n * WHERE THE DEFINITIONS COME FROM. The Easy lane fetches each cart product\n * (`getProduct(uniqid)`) because it has no catalog in the page. This lane does:\n * `window.__SHOPPEX_INITIAL__` carries every product, listed or not, so the\n * definitions are read straight out of it. There is no request to fail — but\n * there is still a line whose product is not in the payload, and that case\n * FAILS CLOSED for the same reason the Easy lane's failed fetch does: an\n * unresolvable product may carry required fields, and treating \"unknown\" as\n * \"none\" is how a checkout submits without them.\n *\n * VALUES ARE PER PRODUCT, not per cart line. Two lines of the same product (a\n * different option, a different extra) describe the same thing being bought\n * twice, and the merchant asked their question once — so both lines receive the\n * same answers, which is also what keeps them from splitting into two identities\n * the buyer never chose. It differs from the Easy lane, which dedupes by field\n * NAME across the whole cart: two products that both ask \"Steam ID\" are two\n * questions here, and one there.\n */\n\nimport type { SdkCartItem } from './sdk';\nimport { getProducts, type ProductCustomField } from './storefront-data';\n\n/** One product's questions, and the product they belong to. */\nexport interface CheckoutFieldGroup {\n productId: string;\n /** The product's own title — the section heading the buyer reads. */\n productName: string;\n fields: ProductCustomField[];\n}\n\n/**\n * What the cart needs before it can be checked out.\n *\n * `resolved: false` is the fail-closed state: at least one line names a product\n * the payload does not carry, so nothing can be said about what it requires.\n */\nexport type CheckoutFields =\n | { resolved: true; groups: CheckoutFieldGroup[]; hasRequired: boolean }\n | { resolved: false; unresolvedProductIds: string[] };\n\n/** Answers, keyed product id → field name → value. */\nexport type CheckoutFieldValues = Record<string, Record<string, string>>;\n\n/** An empty cart asks nothing. */\nconst NO_FIELDS: CheckoutFields = { resolved: true, groups: [], hasRequired: false };\n\n/**\n * The questions the current cart raises, grouped by product and in cart order.\n *\n * Products with no custom fields produce no group — they are resolved, they\n * simply ask nothing.\n */\nexport function resolveCheckoutFields(lines: readonly SdkCartItem[]): CheckoutFields {\n if (lines.length === 0) return NO_FIELDS;\n // Read on demand rather than once at module load: the payload is parsed\n // strictly and may throw, and the storefront's own boot is where that belongs.\n const products = getProducts();\n\n const groups: CheckoutFieldGroup[] = [];\n const unresolvedProductIds: string[] = [];\n const seen = new Set<string>();\n for (const line of lines) {\n const productId = line.product_id;\n if (seen.has(productId)) continue;\n seen.add(productId);\n const product = products.find((candidate) => candidate.id === productId);\n if (product === undefined) {\n unresolvedProductIds.push(productId);\n continue;\n }\n if (product.customFields.length === 0) continue;\n groups.push({ productId, productName: product.name, fields: product.customFields });\n }\n\n if (unresolvedProductIds.length > 0) return { resolved: false, unresolvedProductIds };\n return {\n resolved: true,\n groups,\n hasRequired: groups.some((group) => group.fields.some((field) => field.required)),\n };\n}\n\n/**\n * Whether the checkout CTA has to route through the details form instead of\n * handing off straight away.\n *\n * True for the fail-closed state as well: a cart whose products cannot be\n * resolved must not be handed to `checkout()` on the assumption that it asks\n * nothing. The form is where that is said out loud.\n *\n * Optional-only fields do NOT gate the hand-off — nothing about the invoice\n * depends on them, and stopping a buyer for a question they may leave blank\n * would be this theme inventing a step. They are still collected whenever the\n * form does open, so a cart that has to stop for one product's required field\n * gathers every other answer in the same pass.\n */\nexport function checkoutNeedsDetails(fields: CheckoutFields): boolean {\n return !fields.resolved || fields.hasRequired;\n}\n\n/**\n * The form's starting values: whatever the cart lines already carry, else the\n * merchant's own default for the field.\n *\n * Pre-filling from the line is what makes the form survive a buyer going back to\n * the cart and returning — the answers are stored on the line, not in React.\n */\nexport function buildInitialCheckoutFieldValues(\n groups: readonly CheckoutFieldGroup[],\n lines: readonly SdkCartItem[],\n): CheckoutFieldValues {\n const values: CheckoutFieldValues = {};\n for (const group of groups) {\n const stored = lines.find((line) => line.product_id === group.productId)?.custom_fields ?? {};\n const entries: Record<string, string> = {};\n for (const field of group.fields) {\n const value = stored[field.name];\n entries[field.name] = typeof value === 'string' && value !== '' ? value : field.defaultValue;\n }\n values[group.productId] = entries;\n }\n return values;\n}\n\n/**\n * Whether a checkbox answer counts as ticked.\n *\n * The stored value is a string, and the four spellings below are the ones the\n * SDK's `isStorefrontCheckboxCustomFieldValueChecked` accepts — a checkbox the\n * two lanes read differently is a required tick one lane honours and the other\n * silently drops.\n */\nexport function isCheckboxValueChecked(value: string | undefined): boolean {\n const normalized = value?.trim().toLowerCase() ?? '';\n return normalized === 'true' || normalized === '1' || normalized === 'yes' || normalized === 'on';\n}\n\n/**\n * The buyer-facing problem with one answer, or null when it is acceptable.\n *\n * A port of the SDK's `validateStorefrontCustomFieldValue`, down to the wording:\n * the Easy lane refuses the same values with the same sentences, and the backend\n * refuses them again when the invoice is built. A malformed merchant regex\n * validates NOTHING rather than blocking every buyer on a pattern that cannot\n * compile — the merchant's mistake must not become the buyer's dead end, and the\n * backend still holds the line.\n */\nexport function validateCheckoutFieldValue(field: ProductCustomField, value: string): string | null {\n if (field.type === 'checkbox') {\n return field.required && !isCheckboxValueChecked(value) ? `${field.name} is required.` : null;\n }\n\n const normalized = value.trim();\n if (field.required && !normalized) return `${field.name} is required.`;\n if (field.regex && normalized) {\n let pattern: RegExp;\n try {\n pattern = new RegExp(field.regex);\n } catch {\n return null;\n }\n if (!pattern.test(normalized)) return `${field.name} has an invalid format.`;\n }\n return null;\n}\n\n/**\n * Every problem the form has to show, keyed `<productId>.<fieldName>` so two\n * products asking the same question keep their own message.\n */\nexport function validateCheckoutFields(\n groups: readonly CheckoutFieldGroup[],\n values: CheckoutFieldValues,\n): Record<string, string> {\n const errors: Record<string, string> = {};\n for (const group of groups) {\n for (const field of group.fields) {\n const message = validateCheckoutFieldValue(\n field,\n values[group.productId]?.[field.name] ?? '',\n );\n if (message) errors[checkoutFieldKey(group.productId, field.name)] = message;\n }\n }\n return errors;\n}\n\nexport function checkoutFieldKey(productId: string, fieldName: string): string {\n return `${productId}.${fieldName}`;\n}\n\n/**\n * One product's answers in the shape a cart line stores.\n *\n * A port of the SDK's `buildStorefrontCustomFieldPayload`: an unticked checkbox\n * and an empty answer are OMITTED rather than stored blank, which is what keeps\n * an optional field the buyer skipped from becoming an empty string the backend\n * has to interpret. A ticked checkbox is always the literal `'true'`.\n */\nexport function buildCustomFieldPayload(\n fields: readonly ProductCustomField[],\n values: Record<string, string> | undefined,\n): Record<string, string> {\n const payload: Record<string, string> = {};\n for (const field of fields) {\n const raw = values?.[field.name] ?? field.defaultValue;\n if (field.type === 'checkbox') {\n if (isCheckboxValueChecked(raw)) payload[field.name] = 'true';\n continue;\n }\n const normalized = raw.trim();\n if (normalized) payload[field.name] = normalized;\n }\n return payload;\n}\n\n/**\n * The required answers that are NOT on the cart, read back out of the SDK.\n *\n * PAYMENT PATH. The write-back is `updateCartItem`, which the SDK can drop the\n * same way it drops an add — its storage wrapper catches a quota or blocked\n * write and only logs it. A hand-off that trusted the write would create no\n * invoice at all (the backend rejects the cart) after telling the buyer their\n * details were taken. So the cart is read back and every required field is\n * checked against the value that actually landed, with the same validator the\n * form used.\n *\n * Returns the buyer-facing messages, empty when the cart is ready to submit.\n */\nexport function missingRequiredCheckoutValues(\n groups: readonly CheckoutFieldGroup[],\n lines: readonly SdkCartItem[],\n): string[] {\n const messages: string[] = [];\n for (const group of groups) {\n const required = group.fields.filter((field) => field.required);\n if (required.length === 0) continue;\n for (const line of lines) {\n if (line.product_id !== group.productId) continue;\n for (const field of required) {\n const message = validateCheckoutFieldValue(field, line.custom_fields?.[field.name] ?? '');\n if (message && !messages.includes(message)) messages.push(message);\n }\n }\n }\n return messages;\n}\n\n/**\n * Whether a details write changed what the cart PRICES.\n *\n * PAYMENT PATH, and the one consequence of the identity merge that is not\n * harmless. Two lines of the same product and option can exist while their\n * `custom_fields` differ — the only way this theme's `add` produces one is a\n * buyer who answered once, came back from a checkout that did not complete, and\n * added the product again from the PDP or through Buy Now, because `add` sends\n * no answers at all. Giving both the SAME answers collapses them into one line\n * whose quantity is the SUM, and the quote proof the SDK is holding is bound to\n * a hash of the cart's products, options and QUANTITIES (`custom_fields` and\n * `delivery_instructions` are deliberately NOT hashed — see\n * `apps/backend/src/services/storefront/cart/cartQuoteToken.ts`). So the answers\n * alone never stale a proof, and a merge always does.\n *\n * Handing off anyway is refused by `/from-cart` as `quote_token_stale`. The\n * hand-off already recovers from that refusal by re-quoting, so nothing is\n * mispriced — but the buyer spends a click and a server round trip on a message\n * about a cart change they did not make. Answering it here instead re-quotes\n * straight away and names what actually happened.\n *\n * Compared on the priced shape rather than on `line_id`s, because every id in\n * the cart changes when the answers are written — that is the identity rehash\n * doing its job, not a repricing. A merge is the only thing this write can do\n * to that shape, and it always removes a line, so the two multisets can never\n * coincide.\n */\nexport function checkoutFieldWriteRepriced(\n before: readonly SdkCartItem[],\n after: readonly SdkCartItem[],\n): boolean {\n const shape = (lines: readonly SdkCartItem[]) => lines\n .map((line) => `${line.product_id} ${line.variant_id} ${line.quantity}`)\n .sort()\n .join('');\n return shape(before) !== shape(after);\n}\n","/**\n * Reading THE CART BOUNDS CONTRACT off a settled quote.\n *\n * PAYMENT PATH. Its own module, free of React and of the SDK singleton, so the\n * rules can be read and exercised on their own: the cart provider hands them the\n * quote's `lines` and the cart it holds, and they answer what has to change.\n *\n * The producer is the backend's `CartQuoteService` (`CartQuoteLine`), and its\n * field rules — which this module exists to honour exactly — are:\n *\n * `variant_id` the RESOLVED option id, or `null` for a product with no\n * variants, which the cart stores under the `'default'` marker.\n * The match has to bridge the two spellings.\n * `min_quantity` OMITTED when it is 1.\n * `max_quantity` OMITTED when nothing bounds the line. `-1` is NEVER emitted;\n * omission IS the unbounded sentinel, and a missing key must\n * never be read as 0 — that says \"sold out\". SHARED when\n * several cart lines name one product+option: they draw from\n * one shelf and the checkout sums them, so the value is that\n * line's SHARE of the option's ceiling.\n * `sellable` false = this line cannot be ordered as the cart stands:\n * either no quantity of the option can be ordered at all, or\n * the cart's lines of this option together ask for more units\n * than the option's ceiling, which the checkout refuses on the\n * SUM. The line is still priced and still counted — it is one\n * the buyer must reduce or remove. It answers for the OPTION\n * and for the whole cart's demand on it, so every line of one\n * option carries the same value.\n *\n * CONSUMER DUTY, from the same contract, and the reason this module exists in\n * the Advanced lane at all: cart lines hold an ADD-TIME SNAPSHOT of their\n * bounds, so a line goes stale the moment stock moves or the merchant edits the\n * limits — and the stepper is driven off those stored values. Heal them by\n * REPLACING, never merging: an absent `max_quantity` means the ceiling was\n * REMOVED, and a merge would keep a stale one forever. And refuse the checkout\n * hand-off while any line is unsellable, because that cart is exactly the one\n * `/from-cart` rejects.\n */\n\nimport type { SdkCartItem, SdkCartQuoteLine } from './sdk';\n\nexport type CartQuoteLineBounds = {\n productId: string;\n /** `'default'` for the contract's `null`, so it compares against a cart line. */\n variantId: string;\n /** The quantity this row was quoted AT, after per-line summing. */\n quantity: number;\n /** `null` = unstated, which is the SDK's own default of 1. */\n minQuantity: number | null;\n /** `null` = unbounded. Never a negative sentinel. */\n maxQuantity: number | null;\n sellable: boolean;\n};\n\n/** One healing write: the line to patch, and the bounds to REPLACE it with. */\nexport type CartBoundsHeal = {\n lineId: string;\n /**\n * `null` DELETES the stored bound — the contract's \"no bound\" — in the SDK's\n * own tri-state (absent = leave alone, null = clear, number = set). It is not\n * `undefined`, because `undefined` is exactly what the SDK reads as \"this\n * update did not mention the field\", so a removed ceiling would stick to the\n * line forever.\n */\n minQuantity: number | null;\n maxQuantity: number | null;\n};\n\n/**\n * The wire omits these rather than nulling them, so anything non-numeric is\n * \"unstated\". A negative value is refused outright: `-1` is not emitted by this\n * contract, and treating one as a ceiling would pin the line to a single unit.\n */\nfunction readOptionalCount(value: unknown): number | null {\n if (typeof value !== 'number' || !Number.isFinite(value) || value < 0) return null;\n return Math.floor(value);\n}\n\n/** Parse `lines` off a settled quote. An absent or malformed list yields none. */\nexport function parseCartQuoteLines(raw: SdkCartQuoteLine[] | undefined): CartQuoteLineBounds[] {\n if (!Array.isArray(raw)) return [];\n const parsed: CartQuoteLineBounds[] = [];\n for (const entry of raw) {\n const row = (entry && typeof entry === 'object' ? entry : {}) as Record<string, unknown>;\n const productId = typeof row.product_id === 'string' ? row.product_id : '';\n if (!productId) continue;\n parsed.push({\n productId,\n variantId: typeof row.variant_id === 'string' && row.variant_id ? row.variant_id : 'default',\n quantity: typeof row.quantity === 'number' && Number.isFinite(row.quantity) ? row.quantity : 0,\n minQuantity: readOptionalCount(row.min_quantity),\n maxQuantity: readOptionalCount(row.max_quantity),\n // Fail closed on a malformed row: only an explicit `true` is sellable. A\n // row this runtime could not read must not re-enable a refused line.\n sellable: row.sellable === true,\n });\n }\n return parsed;\n}\n\n/**\n * The OPTION this quoted row answers for.\n *\n * Deliberately quantity-blind, because `sellable` is: the producer answers it\n * for the OPTION and for the whole cart's demand on it, so every line of one\n * option carries the same verdict. A refusal does not stop being true because\n * the buyer changed how many they wanted, and dropping it on a quantity\n * mismatch would arm a checkout the server refuses.\n */\nfunction matchesOption(line: SdkCartItem, quoted: CartQuoteLineBounds): boolean {\n const variantId = line.variant_id ? line.variant_id : 'default';\n return line.product_id === quoted.productId && variantId === quoted.variantId;\n}\n\n/**\n * The exact LINE this quoted row priced — option AND the quantity it was quoted\n * at, which the producer states explicitly.\n *\n * PAYMENT PATH. Bounds are resolved against the requested quantity, so a quote\n * that answered for 2 units is not an answer about the same line now holding 9.\n * Matching on the option alone would heal a since-changed line from a quote that\n * never saw it. A mismatch skips the heal; the next settled quote answers for\n * what the cart actually holds.\n */\nfunction matchesQuotedLine(line: SdkCartItem, quoted: CartQuoteLineBounds): boolean {\n if (!matchesOption(line, quoted)) return false;\n const quantity = Number.isFinite(line.quantity) ? Math.floor(line.quantity) : null;\n return quantity !== null && quantity === quoted.quantity;\n}\n\n/**\n * The writes needed to bring stored line bounds back in line with the quote.\n *\n * Only lines whose stored bounds actually DIFFER are returned, which is what\n * keeps this from looping: writing the same values back would emit another\n * cart-changed, re-quote, and arrive here again. Cart lines the quote did not\n * mention are left untouched — the quote answers for the cart it was given, and\n * a line added after it was sent has no answer in it yet.\n */\nexport function resolveCartBoundsHeals(\n lines: readonly SdkCartItem[],\n quoted: readonly CartQuoteLineBounds[],\n): CartBoundsHeal[] {\n const heals: CartBoundsHeal[] = [];\n for (const line of lines) {\n const lineId = typeof line.line_id === 'string' ? line.line_id : '';\n if (!lineId) continue;\n const match = quoted.find((entry) => matchesQuotedLine(line, entry));\n if (!match) continue;\n // The SDK's own default is 1 and it is stored as \"unset\", so a minimum of 1\n // is expressed by clearing the field rather than by writing the number.\n const nextMin = match.minQuantity !== null && match.minQuantity > 1 ? match.minQuantity : null;\n const nextMax = match.maxQuantity;\n const storedMin = readOptionalCount(line.min_quantity);\n const storedMax = readOptionalCount(line.max_quantity);\n const currentMin = storedMin !== null && storedMin > 1 ? storedMin : null;\n if (currentMin === nextMin && storedMax === nextMax) continue;\n heals.push({ lineId, minQuantity: nextMin, maxQuantity: nextMax });\n }\n return heals;\n}\n\n/** The stored `line_id`s the quote says cannot be ordered right now. */\nexport function resolveUnsellableLineIds(\n lines: readonly SdkCartItem[],\n quoted: readonly CartQuoteLineBounds[],\n): string[] {\n const ids: string[] = [];\n for (const line of lines) {\n const lineId = typeof line.line_id === 'string' ? line.line_id : '';\n if (!lineId) continue;\n const match = quoted.find((entry) => matchesOption(line, entry));\n // No answer is not a refusal: a line the quote never mentioned is left alone\n // rather than being flagged on this runtime's guess.\n if (match && !match.sellable) ids.push(lineId);\n }\n return ids;\n}\n","export const QUOTE_UNAVAILABLE_CHECKOUT_ERROR =\n 'Cart pricing is unavailable, so this order cannot be priced yet. Try again in a moment.';\n\n/** Retires only the checkout refusal that a newly settled quote resolved. */\nexport function clearRecoveredQuoteCheckoutError(error: string | null): string | null {\n return error === QUOTE_UNAVAILABLE_CHECKOUT_ERROR ? null : error;\n}\n","/**\n * Merchant-authored HTML, made safe to inject.\n *\n * The storefront payload carries product descriptions and content tabs exactly\n * as the merchant stored them; nothing sanitizes them on the way out. The Easy\n * lane sanitizes at RENDER time (`sanitizeStorefrontRichTextHtml`), so a code\n * storefront that skipped this step would make the same shop's content safe on\n * one lane and executable on the other.\n *\n * \"The merchant wrote it, on their own storefront\" is not a safety argument:\n * imported shops carry HTML nobody at the shop authored, and dashboard access is\n * not always the owner's. The allowlist mirrors the Easy lane's — anything\n * outside it is DROPPED rather than escaped, so a stray `onerror` disappears\n * instead of showing up as text.\n */\n\nimport DOMPurify from 'dompurify';\n\nconst HEX_COLOR = /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;\nconst TEXT_ALIGN = /^(left|right|center|justify)$/;\n/**\n * Images resolve against the shop's own CDN, the same rule the Easy lane\n * applies. An arbitrary remote `src` is a beacon: it reports every buyer who\n * opens the page to whoever chose the URL, and it survives sanitization of the\n * event handlers.\n */\nconst CDN_IMAGE_SRC = /^https:\\/\\/imagedelivery\\.net\\/[^/]+\\/[^/]+\\/[^/?#]+(?:\\?.*)?$/i;\n\n/** Only colour and alignment survive; everything else in `style` is dropped. */\nfunction filterStyle(style: string): string {\n const allowed: string[] = [];\n for (const declaration of style.split(';')) {\n const [rawProperty, rawValue] = declaration.split(':');\n if (!rawProperty || !rawValue) continue;\n const property = rawProperty.trim().toLowerCase();\n const value = rawValue.trim();\n\n if ((property === 'color' || property === 'background-color') && HEX_COLOR.test(value)) {\n allowed.push(`${property}: ${value}`);\n continue;\n }\n if (property === 'text-align' && TEXT_ALIGN.test(value.toLowerCase())) {\n allowed.push(`${property}: ${value.toLowerCase()}`);\n }\n }\n return allowed.join('; ');\n}\n\nconst ALLOWED_TAGS = [\n 'h1', 'h2', 'h3', 'p', 'br', 'hr',\n 'ul', 'ol', 'li',\n 'details', 'summary',\n 'strong', 'em', 'u', 's', 'del',\n 'code', 'pre', 'blockquote',\n 'a', 'mark', 'span', 'img',\n 'table', 'thead', 'tbody', 'tr', 'th', 'td',\n];\n\nconst ALLOWED_ATTR = [\n 'href', 'target', 'rel', 'class', 'style',\n 'data-color', 'data-type',\n 'colspan', 'rowspan',\n 'src', 'alt', 'title', 'width', 'height', 'loading',\n 'open',\n];\n\nexport function sanitizeMerchantHtml(value: unknown): string {\n if (typeof value !== 'string' || !value) return '';\n\n const hook = (\n node: unknown,\n data: { attrName?: string; attrValue?: string; keepAttr?: boolean },\n ) => {\n if (data.attrName === 'data-color') {\n if (typeof data.attrValue !== 'string' || !HEX_COLOR.test(data.attrValue)) {\n data.keepAttr = false;\n }\n return;\n }\n if (data.attrName === 'src') {\n const isImage = typeof node === 'object'\n && node !== null\n && 'tagName' in node\n && String((node as { tagName?: unknown }).tagName).toUpperCase() === 'IMG';\n if (isImage && (typeof data.attrValue !== 'string' || !CDN_IMAGE_SRC.test(data.attrValue))) {\n data.keepAttr = false;\n }\n return;\n }\n if (data.attrName !== 'style' || typeof data.attrValue !== 'string') return;\n\n const filtered = filterStyle(data.attrValue);\n if (!filtered) {\n data.keepAttr = false;\n return;\n }\n data.attrValue = filtered;\n };\n\n DOMPurify.addHook('uponSanitizeAttribute', hook);\n try {\n return DOMPurify.sanitize(value, { ALLOWED_TAGS, ALLOWED_ATTR });\n } finally {\n DOMPurify.removeHook('uponSanitizeAttribute', hook);\n }\n}\n\n/**\n * Elements that end a line of prose. Their boundary contributes a SPACE when\n * the markup is flattened, so \"<p>High speed</p><p>24/7 support</p>\" reads\n * \"High speed 24/7 support\" and not \"High speed24/7 support\" — the exact\n * artefact a bare `textContent` produced on catalog cards and in the search\n * haystack, where it also glued two words into one unsearchable token.\n */\nconst BLOCK_BOUNDARY_SELECTOR = 'address,article,aside,blockquote,br,dd,div,dl,dt,'\n + 'figcaption,figure,footer,h1,h2,h3,h4,h5,h6,header,hr,li,main,nav,ol,p,pre,'\n + 'section,table,tbody,td,tfoot,th,thead,tr,ul';\n\n/**\n * Merchant HTML reduced to plain text, safely.\n *\n * Product descriptions arrive as rich text and several surfaces render them as\n * TEXT — a catalog card's two-line summary, the search haystack. Passing the\n * raw string there printed the merchant's own `<p>` tags at the buyer, and\n * passing `textContent` of the parsed markup ran the block boundaries together.\n *\n * So the value is sanitized first (same allowlist as everything else — the\n * string is never parsed as live markup before DOMPurify has seen it) and then\n * flattened with block boundaries counting as whitespace. The result is\n * collapsed to single spaces because CSS line-clamping cannot do it for text.\n */\nexport function flattenMerchantHtmlToText(value: unknown): string {\n const sanitized = sanitizeMerchantHtml(value);\n if (!sanitized) return '';\n const container = document.createElement('div');\n container.innerHTML = sanitized;\n for (const element of container.querySelectorAll(BLOCK_BOUNDARY_SELECTOR)) {\n element.insertAdjacentText('beforebegin', ' ');\n element.insertAdjacentText('afterend', ' ');\n }\n return (container.textContent ?? '').replace(/\\s+/g, ' ').trim();\n}\n\n/**\n * True when the sanitized markup carries no renderable content.\n *\n * A description of `<p></p>` is not a description: the Easy lane's\n * `hasDescription` gate tests the SOURCE text, so a merchant who cleared their\n * editor without clearing its wrapper tags gets the empty state rather than a\n * blank panel.\n */\n/**\n * Drops every picture from merchant HTML.\n *\n * The merchant's \"show images in descriptions\" switch, applied at the same\n * place the document lane applies it (`stripDescriptionImages`): the shop asked\n * for text, so the description and every content tab arrive without their\n * `<img>` elements. Run BEFORE the sanitizer's output is injected, on the\n * sanitized string, so a tag the allowlist would have dropped anyway cannot\n * survive by looking like an image to this pass.\n */\nexport function stripMerchantHtmlImages(html: string): string {\n return html.replace(/<img\\b[^>]*>/gi, '').trim();\n}\n\nexport function isBlankMerchantHtml(html: string): boolean {\n if (!html) return true;\n const container = document.createElement('div');\n container.innerHTML = html;\n return (container.textContent ?? '').trim().length === 0\n && container.querySelector('img, hr, table') === null;\n}\n","/**\n * Catalog view models: the step between the injected wire payload and the\n * cards.\n *\n * Everything a card shows — the price label, the badge, the stock chip, the\n * buy control it gets — is decided here once, so the card components stay\n * markup. Nothing in this module invents a value: a product without a rating\n * has no rating chip, a product without a description has no description line.\n */\n\nimport { flattenMerchantHtmlToText } from './sanitize-html';\nimport { badgesForCard, type ProductBadge } from './product-badges';\nimport {\n getProductGroups,\n getProducts,\n getStorefrontCategories,\n getStorefrontListing,\n isProductSoldOut,\n isSelectionSoldOut,\n resolveDisplayStock,\n resolveHideStockCounter,\n STOREFRONT_SORT_KEYS,\n type StorefrontCategory,\n type StorefrontGroup,\n type StorefrontProduct,\n type StorefrontSettings,\n type StorefrontSortKey,\n} from './storefront-data';\n\n/**\n * Money as the storefront shows it: `$49`, `$49.50`, `€9.99`, `¥1,200`.\n *\n * A whole amount drops its zero cents (`$49`, not `$49.00`); any other amount\n * keeps the currency's own number of decimals, so `$49.50` never turns into\n * `$49.5` and a three-decimal currency keeps all three. \"Whole\" is judged on\n * the amount rounded to those decimals, so float noise like `48.99999999`\n * still reads as `$49`.\n */\nexport function formatMoney(amount: number, currency: string): string {\n const code = currency && currency !== 'AUTO' ? currency : 'USD';\n let format: Intl.NumberFormat;\n try {\n format = new Intl.NumberFormat('en-US', { style: 'currency', currency: code });\n } catch {\n // An unknown ISO code is merchant data, not a bug worth blanking a price\n // over — show the amount with the code in front of it.\n const fixed = amount.toFixed(2);\n return `${code} ${fixed.endsWith('.00') ? fixed.slice(0, -3) : fixed}`;\n }\n const decimals = format.resolvedOptions().maximumFractionDigits ?? 2;\n if (decimals === 0 || !Number.isInteger(Number(amount.toFixed(decimals)))) return format.format(amount);\n return new Intl.NumberFormat('en-US', {\n style: 'currency',\n currency: code,\n minimumFractionDigits: 0,\n maximumFractionDigits: 0,\n }).format(Math.round(amount));\n}\n\n/** Compact counts for the store-stats strip: 12450 → `12k`, 8200 → `8.2k`. */\nexport function formatCompactCount(value: number): string {\n if (value >= 1_000_000) return `${(value / 1_000_000).toFixed(1)}M`;\n if (value >= 1_000) return `${(value / 1_000).toFixed(value >= 10_000 ? 0 : 1)}k`;\n return value.toLocaleString('en-US');\n}\n\nexport type StockTone = 'in_stock' | 'low_stock' | 'sold_out';\n\nexport type StockChip = {\n tone: StockTone;\n label: string;\n};\n\n/**\n * The units at or below which stock reads as LOW when the merchant configured\n * no threshold of their own. Ten is what every Easy-lane stock surface hardcodes\n * (`resolveCatalogStockPresentation`, `resolveStockTone`), so a merchant who\n * never set \"Low Stock Warning\" sees the same wording on both lanes.\n */\nexport const DEFAULT_LOW_STOCK_THRESHOLD = 10;\n\n/**\n * The stock chip on a catalog card. A merchant who hides the stock counter\n * gets the generic label — but a hidden count must never read as unlimited, so\n * only a genuinely unbounded product loses the number for free.\n *\n * The low-stock point is the merchant's own `quantity_warning` when they set\n * one. It is a per-product number precisely because \"running low\" means five\n * for a licence pool and five hundred for a key blob; ignoring it, as this card\n * did, called a 400-unit product low and a 12-unit one plentiful.\n */\nfunction resolveStockChip(\n stock: number | null,\n hideCounter: boolean,\n lowStockThreshold: number | null,\n): StockChip {\n if (stock === 0) return { tone: 'sold_out', label: 'Out of Stock' };\n if (hideCounter || stock === null) return { tone: 'in_stock', label: 'In Stock' };\n const threshold = lowStockThreshold ?? DEFAULT_LOW_STOCK_THRESHOLD;\n return { tone: stock <= threshold ? 'low_stock' : 'in_stock', label: `${stock} In Stock` };\n}\n\n/**\n * The card badge. `Options` and `Sub` are the clean-minimal wordings — the\n * card is 217px wide at four columns, and \"Subscription\" does not fit.\n */\nfunction resolveBadgeLabel(product: StorefrontProduct): string | null {\n if (product.productType === 'INFO_CARD') return 'Info';\n if (product.productType === 'SUBSCRIPTION') return 'Sub';\n if (product.priceVariants.length > 1) return 'Options';\n return null;\n}\n\n/**\n * The catalog price. A variant-led product (no price on the product row, two\n * or more differently-priced options) advertises its RANGE; everything else\n * shows its single starting price.\n */\nfunction resolvePriceLabel(product: StorefrontProduct): string {\n const variantPrices = product.priceVariants.map((variant) => variant.price);\n const positive = variantPrices.filter((price) => price > 0);\n const candidates = positive.length > 0 ? positive : variantPrices;\n if (product.basePrice <= 0 && candidates.length >= 2) {\n const min = Math.min(...candidates);\n const max = Math.max(...candidates);\n if (max > min) {\n return `${formatMoney(min, product.currency)} - ${formatMoney(max, product.currency)}`;\n }\n }\n return formatMoney(product.price, product.currency);\n}\n\nexport interface ProductCardModel {\n kind: 'product';\n key: string;\n product: StorefrontProduct;\n href: string;\n priceLabel: string;\n badgeLabel: string | null;\n /**\n * The merchant's own badges for this surface, already filtered by\n * `show_on_product_card`. Separate from `badgeLabel`, which is the theme's\n * product-TYPE chip: one is authored, the other is derived, and they occupy\n * different corners of the media so neither can hide the other.\n */\n badges: ProductBadge[];\n /** Up to three merchant bullets; the description line renders only when empty. */\n highlights: string[];\n description: string | null;\n rating: number | null;\n /**\n * Absent for info cards, sold-out products and paused ones — each has its own\n * chip or its own action row instead.\n */\n stockChip: StockChip | null;\n soldOut: boolean;\n /**\n * The merchant PAUSED this product. Not a stock state: it outranks stock\n * entirely, because the backend refuses to sell it however many units exist.\n */\n onHold: boolean;\n categoryIds: string[];\n sortPriority: number;\n createdAt: string | null;\n price: number;\n}\n\nexport interface GroupCardModel {\n kind: 'group';\n key: string;\n group: StorefrontGroup;\n priceLabel: string;\n /** Resolvable members, in the merchant's order. */\n members: StorefrontProduct[];\n memberCountLabel: string;\n categoryIds: string[];\n sortPriority: number;\n price: number;\n}\n\nexport type CatalogCardModel = ProductCardModel | GroupCardModel;\n\n/**\n * A card is sold out when nothing on it can be ordered: no stock at all, or no\n * option whose effective quantity range is non-empty (`quantity_min` 2 against\n * one unit left). PAYMENT PATH: the card's quick add goes through `add()`,\n * which refuses exactly these selections, so an active button here would be a\n * control that silently does nothing.\n */\nexport function isCardSoldOut(product: StorefrontProduct): boolean {\n if (isProductSoldOut(product)) return true;\n return product.priceVariants.length > 0\n ? product.priceVariants.every((variant) => isSelectionSoldOut(product, variant))\n : isSelectionSoldOut(product, null);\n}\n\nfunction buildProductCard(\n product: StorefrontProduct,\n settings: StorefrontSettings,\n sortPriority: number,\n): ProductCardModel {\n const soldOut = isCardSoldOut(product);\n const isInfoCard = product.productType === 'INFO_CARD';\n const stock = resolveDisplayStock(product);\n const hideCounter = resolveHideStockCounter(product, settings);\n return {\n kind: 'product',\n key: product.id,\n product,\n href: `/product/${product.slug ?? product.id}`,\n priceLabel: resolvePriceLabel(product),\n badgeLabel: resolveBadgeLabel(product),\n badges: badgesForCard(product.badges),\n highlights: product.highlights.slice(0, 3),\n // Flattened, not raw: the description is rich text and the card renders it\n // as TEXT, so the merchant's own markup used to print as literal tags. See\n // `flattenMerchantHtmlToText` for why plain `textContent` is not enough.\n description: product.highlights.length > 0\n ? null\n : flattenMerchantHtmlToText(product.description) || null,\n rating: product.rating,\n // A paused product's stock is beside the point — announcing \"999 IN STOCK\"\n // over a card that cannot be bought states the opposite of what the action\n // row says. Checked before stock for the same reason the document lane's\n // `isProductOutOfStock` checks it first.\n stockChip: isInfoCard || soldOut || product.onHold\n ? null\n : resolveStockChip(stock, hideCounter, product.lowStockThreshold),\n soldOut,\n onHold: product.onHold,\n categoryIds: product.categories.map((category) => category.id),\n sortPriority,\n createdAt: product.createdAt,\n price: product.price,\n };\n}\n\n/**\n * A bundle's price span across its members. The bundle discount is applied by\n * the backend at quote time, so the card advertises the member prices and\n * never a total it computed itself.\n */\nfunction buildGroupCard(\n group: StorefrontGroup,\n byId: Map<string, StorefrontProduct>,\n sortPriority: number,\n): GroupCardModel {\n const members = group.productIds\n .map((id) => byId.get(id))\n .filter((product): product is StorefrontProduct => product !== undefined);\n const prices = members.map((product) => product.price);\n const currency = members[0]?.currency ?? 'USD';\n const min = prices.length > 0 ? Math.min(...prices) : 0;\n const max = prices.length > 0 ? Math.max(...prices) : 0;\n const memberCount = Math.max(group.productsCount, members.length);\n return {\n kind: 'group',\n key: group.id,\n group,\n priceLabel: prices.length === 0\n ? ''\n : min === max\n ? formatMoney(min, currency)\n : `${formatMoney(min, currency)} - ${formatMoney(max, currency)}`,\n members,\n memberCountLabel: `${memberCount} ${memberCount === 1 ? 'product' : 'products'}`,\n categoryIds: [\n ...new Set([\n ...(group.categoryId ? [group.categoryId] : []),\n ...members.flatMap((product) => product.categories.map((category) => category.id)),\n ]),\n ],\n sortPriority,\n price: min,\n };\n}\n\nexport interface StorefrontCatalog {\n cards: CatalogCardModel[];\n categories: StorefrontCategory[];\n /**\n * Every product the payload carries, including the ones the shelf does not\n * list. A bundle member has a real product page even though it has no card,\n * so product-page lookups read this and not `cards`.\n */\n products: StorefrontProduct[];\n /** Products the storefront lists — the store-stats \"products\" cell counts these. */\n listedProductCount: number;\n}\n\n/**\n * The product a `/product/:slug` route points at.\n *\n * The id is accepted alongside the slug because a product without a slug is\n * linked by uniqid, and a bookmarked uniqid must keep working after the\n * merchant adds one.\n */\nexport function findStorefrontProduct(\n catalog: StorefrontCatalog,\n slugOrId: string,\n): StorefrontProduct | null {\n return catalog.products.find((product) => product.slug === slugOrId)\n ?? catalog.products.find((product) => product.id === slugOrId)\n ?? null;\n}\n\n/**\n * The storefront's catalog, in the merchant's shelf order.\n *\n * `items` decides what is on the shelf. Reading `products` instead would put\n * every bundle member on the home page as a standalone card — they are real\n * catalog products, they are simply not listed on their own.\n */\nexport function buildStorefrontCatalog(settings: StorefrontSettings): StorefrontCatalog {\n const products = getProducts();\n const groups = getProductGroups();\n const listing = getStorefrontListing();\n const productsById = new Map(products.map((product) => [product.id, product]));\n const groupsById = new Map(groups.map((group) => [group.id, group]));\n\n const cards = listing\n .map((item, index): CatalogCardModel | null => {\n if (item.type === 'group') {\n const group = groupsById.get(item.id);\n return group ? buildGroupCard(group, productsById, item.sortPriority ?? index) : null;\n }\n const product = productsById.get(item.id);\n if (!product) return null;\n if (settings.hideOutOfStock && isProductSoldOut(product)) return null;\n return buildProductCard(product, settings, item.sortPriority ?? index);\n })\n .filter((card): card is CatalogCardModel => card !== null);\n\n return {\n cards,\n categories: getStorefrontCategories(),\n products,\n listedProductCount: products.length,\n };\n}\n\nexport const CATALOG_SORTS = STOREFRONT_SORT_KEYS;\n\nexport type CatalogSort = StorefrontSortKey;\n\nexport const CATALOG_SORT_LABELS: Record<CatalogSort, string> = {\n featured: 'Featured',\n newest: 'Newest',\n 'price-asc': 'Price: Low to High',\n 'price-desc': 'Price: High to Low',\n};\n\n/**\n * Sorting never mutates the shelf order: `featured` IS that order, and the\n * others sort a copy of it so switching back is exact.\n */\nexport function sortCatalogCards(cards: CatalogCardModel[], sort: CatalogSort): CatalogCardModel[] {\n if (sort === 'featured') return cards;\n const sorted = [...cards];\n if (sort === 'price-asc') return sorted.sort((a, b) => a.price - b.price);\n if (sort === 'price-desc') return sorted.sort((a, b) => b.price - a.price);\n // Newest: a product without a creation date keeps its shelf position rather\n // than jumping to either end of the list.\n return sorted.sort((a, b) => {\n const left = a.kind === 'product' && a.createdAt ? Date.parse(a.createdAt) : Number.NaN;\n const right = b.kind === 'product' && b.createdAt ? Date.parse(b.createdAt) : Number.NaN;\n if (Number.isNaN(left) && Number.isNaN(right)) return a.sortPriority - b.sortPriority;\n if (Number.isNaN(left)) return 1;\n if (Number.isNaN(right)) return -1;\n return right - left;\n });\n}\n\n/**\n * The category a `?category=` value names, as the CARD models key on it.\n *\n * A shared or bookmarked listing URL may carry the merchant's slug, the\n * category title, or the uniqid — the Easy lane accepts all of them\n * (`resolveAllProductsCategorySelection`) and answers with the uniqid the cards\n * are filtered by. Mirrored here so one storefront's filtered link keeps working\n * after a lane switch. An unknown value selects nothing rather than emptying the\n * grid against a category that does not exist.\n */\nexport function resolveCategorySelection(\n value: string | null,\n categories: readonly StorefrontCategory[],\n): string | null {\n const normalized = value?.trim().toLowerCase();\n if (!normalized) return null;\n const match = categories.find((category) =>\n category.id.toLowerCase() === normalized\n || category.slug.toLowerCase() === normalized\n || category.title.toLowerCase() === normalized);\n return match?.id ?? null;\n}\n\nexport function filterCatalogCards(\n cards: CatalogCardModel[],\n categoryId: string | null,\n): CatalogCardModel[] {\n if (!categoryId) return cards;\n return cards.filter((card) => card.categoryIds.includes(categoryId));\n}\n\n/** Products per page in the full listing. Bundles do not count against it. */\nexport const CATALOG_PAGE_SIZE = 24;\n\nexport interface CatalogPage {\n cards: CatalogCardModel[];\n /** The page actually shown — an out-of-range request lands on the last one. */\n page: number;\n totalPages: number;\n}\n\n/**\n * One page of the full listing.\n *\n * Bundles all sit on page one and never count against the page size: a shop\n * with three bundles and twenty-four products is one page, not two, because a\n * bundle is an entry point into the catalog rather than an item in it. The\n * input must already be filtered and sorted — paging re-orders nothing.\n */\nexport function paginateCatalogCards(cards: CatalogCardModel[], page: number): CatalogPage {\n const products = cards.filter((card) => card.kind === 'product');\n const firstPage: CatalogCardModel[] = [];\n let productsOnFirstPage = 0;\n for (const card of cards) {\n if (card.kind === 'group') {\n firstPage.push(card);\n continue;\n }\n if (productsOnFirstPage < CATALOG_PAGE_SIZE) {\n firstPage.push(card);\n productsOnFirstPage += 1;\n }\n }\n\n const remaining = products.length - productsOnFirstPage;\n const totalPages = remaining > 0 ? 1 + Math.ceil(remaining / CATALOG_PAGE_SIZE) : 1;\n const resolved = Math.min(Math.max(page, 1), totalPages);\n if (resolved === 1) return { cards: firstPage, page: 1, totalPages };\n\n const offset = productsOnFirstPage + (resolved - 2) * CATALOG_PAGE_SIZE;\n return {\n cards: products.slice(offset, offset + CATALOG_PAGE_SIZE),\n page: resolved,\n totalPages,\n };\n}\n","/**\n * Product-page view models: the step between one parsed product and the\n * gallery, buy box, details tabs and related strip.\n *\n * Same division of labour as `catalog.ts` — every decision a product page makes\n * about WHAT to show is made here, so the components stay markup. The two files\n * deliberately differ on stock wording: a catalog card shouts a short chip\n * (\"999 IN STOCK\"), the buy box states the option's availability in a sentence\n * (\"999 in stock\", \"Only 3 left\"). Same source, two audiences.\n */\n\nimport {\n calculateDisplayLineTotal,\n isSelectionSoldOut,\n resolveHideStockCounter,\n resolveQuantityBounds,\n resolveVariantStock,\n roundCurrency,\n type ProductAddon,\n type StorefrontPriceVariant,\n type StorefrontProduct,\n type StorefrontSettings,\n} from './storefront-data';\nimport {\n DEFAULT_LOW_STOCK_THRESHOLD,\n type CatalogCardModel,\n type ProductCardModel,\n type StockTone,\n} from './catalog';\n\n/** Maximum related products the strip advertises, matching the Easy lane. */\nconst RELATED_PRODUCT_LIMIT = 3;\n\n/** The five star slots under the product title; `true` is a filled star. */\nexport type RatingStars = readonly [boolean, boolean, boolean, boolean, boolean];\n\nexport interface BuyBoxVariantModel {\n variant: StorefrontPriceVariant;\n /** Unit price with the product discount applied — what one unit costs today. */\n price: number;\n /** Undiscounted unit price, shown struck through. null when nothing is off. */\n compareAtPrice: number | null;\n stockTone: StockTone;\n stockLabel: string;\n soldOut: boolean;\n}\n\nexport interface ProductRatingModel {\n stars: RatingStars;\n /** Reviews the tally counted — the label pluralises off this. */\n count: number;\n /** 1-5, rounded to one decimal. */\n average: number;\n /**\n * How the tally splits. The bootstrap payload carries these three buckets and\n * no review bodies, so this is the finest grain the code lane can honestly\n * report.\n */\n positive: number;\n neutral: number;\n negative: number;\n}\n\nexport interface ProductDetailModel {\n product: StorefrontProduct;\n /** Gallery images, primary first. Empty when the product has no art at all. */\n images: string[];\n /** Rating derived from real feedback; null when the product has none. */\n rating: ProductRatingModel | null;\n variants: BuyBoxVariantModel[];\n addons: ProductAddon[];\n soldOut: boolean;\n /** Cards for the \"You might also like\" strip, in the merchant's shelf order. */\n related: ProductCardModel[];\n}\n\n/**\n * The buy box's stock line for one option.\n *\n * A merchant who hides the stock counter still gets an availability statement —\n * hiding the NUMBER is not hiding whether the thing can be bought. Only a\n * genuinely unbounded option loses the number for free.\n */\nfunction resolveBuyBoxStock(\n stock: number | null,\n hideCounter: boolean,\n /** The product's own \"Low Stock Warning\"; null falls back to the shared default. */\n lowStockThreshold: number | null,\n): { tone: StockTone; label: string; soldOut: boolean } {\n if (stock === 0) return { tone: 'sold_out', label: 'Out of Stock', soldOut: true };\n if (stock === null || hideCounter) return { tone: 'in_stock', label: 'Available', soldOut: false };\n const threshold = lowStockThreshold ?? DEFAULT_LOW_STOCK_THRESHOLD;\n if (stock <= threshold) return { tone: 'low_stock', label: `Only ${stock} left`, soldOut: false };\n return { tone: 'in_stock', label: `${stock} in stock`, soldOut: false };\n}\n\nfunction buildVariants(\n product: StorefrontProduct,\n settings: StorefrontSettings,\n): BuyBoxVariantModel[] {\n const hideCounter = resolveHideStockCounter(product, settings);\n\n return product.priceVariants.map((variant) => {\n // Same predicate the cart's sold-out guard uses, so the button the buyer sees and the line the\n // cart accepts can never disagree — including the option whose counter is POSITIVE but whose\n // minimum order quantity exceeds it. That option is unsellable, so it is labelled and disabled\n // as sold out rather than advertising a number the buyer is not allowed to order.\n const stock = resolveVariantStock(product, variant);\n const { tone, label, soldOut } = isSelectionSoldOut(product, variant)\n ? { tone: 'sold_out' as StockTone, label: 'Out of Stock', soldOut: true }\n : resolveBuyBoxStock(stock, hideCounter, product.lowStockThreshold);\n return {\n variant,\n price: variant.price,\n compareAtPrice: variant.price < variant.baseUnitPrice ? variant.baseUnitPrice : null,\n stockTone: tone,\n stockLabel: label,\n soldOut,\n };\n });\n}\n\n/**\n * The star row and the tally behind it.\n *\n * Weighted the same way `deriveProductRating` weighs the catalog card, because\n * a product cannot carry one score on the shelf and another on its own page.\n */\nfunction buildRating(product: StorefrontProduct): ProductRatingModel | null {\n if (product.rating === null || product.ratingTally === null || product.ratingCount === 0) return null;\n const filled = Math.round(product.rating);\n // The split is the payload's own tally, never reconstructed from the average:\n // an average of 4.7 fits several splits, and printing one of them would state\n // how many buyers were unhappy without anyone having said so.\n return {\n stars: [1, 2, 3, 4, 5].map((slot) => slot <= filled) as unknown as RatingStars,\n count: product.ratingCount,\n average: product.rating,\n positive: product.ratingTally.positive,\n neutral: product.ratingTally.neutral,\n negative: product.ratingTally.negative,\n };\n}\n\n/**\n * Other products worth looking at.\n *\n * Taken from the SHELF, not from the raw product list: a bundle member is a\n * real product the merchant never put on display, and recommending it here\n * would route buyers around the bundle they were meant to see. Bundles\n * themselves are excluded — the strip is a row of product cards.\n */\nfunction buildRelated(cards: CatalogCardModel[], currentId: string): ProductCardModel[] {\n return cards\n .filter((card): card is ProductCardModel => card.kind === 'product' && card.key !== currentId)\n .slice(0, RELATED_PRODUCT_LIMIT);\n}\n\nexport function buildProductDetail(\n product: StorefrontProduct,\n settings: StorefrontSettings,\n cards: CatalogCardModel[],\n): ProductDetailModel {\n // `images` is the merchant's own gallery; a product that only ever had a\n // single primary picture still deserves a gallery frame, so the primary is\n // the fallback rather than an empty stage.\n //\n // Unless the merchant asked for no product picture at all: their\n // \"skip default image\" switch empties the list outright, exactly as the Easy\n // lane's gallery producer does, and the page renders its placeholder frame.\n const images = settings.descriptionSkipDefaultImage\n ? []\n : product.imageUrls.length > 0\n ? product.imageUrls\n : product.imageUrl\n ? [product.imageUrl]\n : [];\n\n return {\n product,\n images,\n rating: buildRating(product),\n variants: buildVariants(product, settings),\n addons: product.addons,\n // A variant-less product can be unsellable for the same reason an option can — see\n // `isSelectionSoldOut`, which folds the empty-range case into the product-level read.\n soldOut: isSelectionSoldOut(product, null),\n related: buildRelated(cards, product.id),\n };\n}\n\nexport interface LinePricing {\n /** What the cart will charge for the whole line. */\n total: number;\n /** The same line before any discount. Equal to `total` when none applies. */\n originalSubtotal: number;\n /** Per-unit price after discounts, for the \"… each\" line. */\n effectiveUnitPrice: number;\n discountAmount: number;\n hasDiscount: boolean;\n}\n\n/**\n * What the buy box headline shows.\n *\n * The unit price folds the selected variant AND the selected addons before the\n * discount stages run, exactly as the platform's product-form controller does —\n * a storefront that discounted the variant and then added the extras would\n * advertise a total the checkout does not honour.\n */\nexport function calculateLinePricing(\n product: StorefrontProduct,\n variant: StorefrontPriceVariant | null,\n addons: ProductAddon[],\n quantity: number,\n /**\n * The buyer's pay-what-you-want amount: PER UNIT, and for the product alone.\n * The extras are added on top of it here exactly once, which is the same\n * split `pay_what_you_want_price` + `price_data.unit_price` the cart sends\n * and the backend charges.\n */\n payWhatYouWantPrice?: number,\n): LinePricing {\n const addonsTotal = addons.reduce((sum, addon) => sum + addon.price, 0);\n if (payWhatYouWantPrice !== undefined) {\n // A named price is the price. The percent discount is already 0 on a PWYW\n // product (`parseProduct`), and the volume tiers are suppressed here for the\n // same reason: the backend validates the buyer's amount against the\n // UNDISCOUNTED minimum, so a storefront that discounted it would advertise a\n // total the checkout refuses.\n const effectiveUnitPrice = payWhatYouWantPrice + addonsTotal;\n const total = roundCurrency(effectiveUnitPrice * quantity);\n return {\n total,\n originalSubtotal: total,\n effectiveUnitPrice,\n discountAmount: 0,\n hasDiscount: false,\n };\n }\n const unitPrice = (variant?.baseUnitPrice ?? product.baseUnitPrice) + addonsTotal;\n const total = calculateDisplayLineTotal(\n unitPrice,\n quantity,\n product.discountPercent,\n product.volumeDiscounts,\n );\n const originalSubtotal = calculateDisplayLineTotal(unitPrice, quantity, 0, []);\n return {\n total,\n originalSubtotal,\n effectiveUnitPrice: quantity > 0 ? total / quantity : total,\n discountAmount: Math.max(0, originalSubtotal - total),\n hasDiscount: total < originalSubtotal,\n };\n}\n\n/**\n * The floor a pay-what-you-want buyer may name: PER UNIT, extras excluded.\n *\n * It is the selected option's UNDISCOUNTED price — the same number the backend\n * validates the buyer's amount against (`resolvePayWhatYouWantUnitPrice`) and\n * the same one the document lane offers as the default. Reading a discounted\n * price here would pre-fill an amount the checkout then refuses as too low.\n */\nexport function resolvePayWhatYouWantMinimum(\n product: StorefrontProduct,\n variant: StorefrontPriceVariant | null,\n): number {\n return variant?.baseUnitPrice ?? product.baseUnitPrice;\n}\n\n/**\n * The backend's ceiling for a named price (`MAX_PAY_WHAT_YOU_WANT_PRICE`).\n * An amount above it fails when the invoice is built, so the buy box refuses it\n * first instead of handing the buyer to a checkout that cannot be created.\n */\nexport const MAX_PAY_WHAT_YOU_WANT_PRICE = 1_000_000_000;\n\n/** Clamps a buyer-entered quantity into the bounds the line actually allows. */\nexport function clampQuantity(\n product: StorefrontProduct,\n variant: StorefrontPriceVariant | null,\n quantity: number,\n): number {\n const { min, max } = resolveQuantityBounds(product, variant);\n if (!Number.isFinite(quantity)) return min;\n const floored = Math.floor(quantity);\n if (floored < min) return min;\n return max !== null && floored > max ? max : floored;\n}\n","/**\n * View models for the cart drawer and the `/cart` page.\n *\n * The cart lives in the SDK as a flat list of lines plus a server quote. This\n * module turns those two into the rows a buyer reads, and it is the only place\n * that decides what a cart line SAYS.\n *\n * PRICING RULE, and it is the whole reason this file exists: a line total is\n * the stored price snapshot times the quantity, and nothing else. The totals\n * block is the SERVER's quote, verbatim — subtotal, discounts and total all\n * from the SAME quote, on both the drawer and `/cart`.\n *\n * The two are never mixed. The quote's subtotal is pre-discount while the line\n * snapshots already carry the product and volume discounts, so printing the\n * client's sum above the quote's discount rows produces a summary whose\n * arithmetic does not work (Subtotal 9, Discount −1, Total 9) — which is\n * exactly what the drawer used to show. `CartView.subtotal` is therefore the\n * client's own number for ONE purpose: standing in for the whole block while no\n * quote exists, where every row is the client's and the block still adds up.\n *\n * When a line has no snapshot its total is zero and the whole cart is flagged\n * as an estimate rather than presenting a confident wrong number.\n *\n * Cents are settled by the SHARED `roundCurrency`, the same half-up-on-the-\n * decimal-text rule the buy box and the line-pricing maths use. This file used\n * to carry a naive `Math.round(v * 100) / 100` of its own, which is a different\n * function: binary noise puts 10.03 × 50% at 5.014999…, which that version\n * floors to 5.01 while the quote's ROUND_HALF_UP gives 5.02. One rounding rule,\n * or two surfaces disagree by a cent about the same cart.\n */\n\nimport type { SdkCartItem, SdkCartQuote } from './sdk';\nimport { roundCurrency } from './storefront-data';\n\nexport interface CartViewLine {\n lineId: string;\n title: string;\n /** The option, and any extras the buyer ticked, as one caption. Blank hides it. */\n subtitle: string;\n imageUrl: string | null;\n quantity: number;\n lineTotal: number;\n /** False at the line's floor — the stepper removes the line instead. */\n canDecrease: boolean;\n /** False at the stock or per-order ceiling recorded when the line was added. */\n canIncrease: boolean;\n /** The quantity the stepper drops to; below it the line is removed. */\n minimumQuantity: number;\n}\n\nexport interface CartView {\n lines: CartViewLine[];\n /** Sum of the line totals — the client's own number, not the quote's. */\n subtotal: number;\n /**\n * True when a line carries no price snapshot, so `subtotal` understates the\n * cart. The summary says \"estimated\" instead of pretending otherwise.\n */\n subtotalIsEstimate: boolean;\n isEmpty: boolean;\n}\n\nfunction positiveInteger(value: unknown, fallback: number): number {\n return typeof value === 'number' && Number.isFinite(value)\n ? Math.max(1, Math.trunc(value))\n : fallback;\n}\n\nfunction snapshotUnitPrice(line: SdkCartItem): number | null {\n const price = line.price_data?.unit_price;\n return typeof price === 'number' && Number.isFinite(price) ? Math.max(price, 0) : null;\n}\n\nfunction lineSubtitle(line: SdkCartItem): string {\n const parts: string[] = [];\n const variantTitle = line.variant_title?.trim();\n if (variantTitle) parts.push(variantTitle);\n const addons = (line.addon_labels ?? []).map((label) => label.trim()).filter(Boolean);\n if (addons.length > 0) parts.push(addons.join(', '));\n return parts.join(' · ');\n}\n\nexport function buildCartView(lines: SdkCartItem[]): CartView {\n const rows = lines.map((line): CartViewLine => {\n const quantity = positiveInteger(line.quantity, 1);\n const unitPrice = snapshotUnitPrice(line);\n const minimumQuantity = positiveInteger(line.min_quantity, 1);\n const maximumQuantity = typeof line.max_quantity === 'number'\n && Number.isFinite(line.max_quantity)\n ? Math.max(1, Math.trunc(line.max_quantity))\n : Number.POSITIVE_INFINITY;\n return {\n lineId: line.line_id,\n // A line without a display snapshot still has to name itself; the\n // product id is the only truthful stand-in the cart holds.\n title: line.title?.trim() || line.product_id,\n subtitle: lineSubtitle(line),\n imageUrl: line.image_url?.trim() || null,\n quantity,\n lineTotal: roundCurrency((unitPrice ?? 0) * quantity),\n canDecrease: quantity > minimumQuantity,\n canIncrease: quantity < maximumQuantity,\n minimumQuantity,\n };\n });\n\n return {\n lines: rows,\n subtotal: roundCurrency(rows.reduce((sum, row) => sum + row.lineTotal, 0)),\n subtotalIsEstimate: lines.length > 0 && lines.some((line) => snapshotUnitPrice(line) === null),\n isEmpty: rows.length === 0,\n };\n}\n\nexport interface CartTotals {\n /**\n * The subtotal to print. From the quote when one exists (pre-discount, the\n * same basis its discount rows are stated against), otherwise the client's\n * own sum.\n */\n subtotal: number;\n /** Itemised discounts, exactly as the quote named them. */\n discounts: Array<{ label: string; amount: string }>;\n /**\n * Aggregate discount, shown only when the quote reports a reduction it did\n * not itemise. Never rendered alongside `discounts`.\n */\n aggregateDiscount: number | null;\n total: number;\n currency: string;\n /**\n * True while the printed numbers are not a server quote — a quote in flight,\n * a quote that failed, or a line without a price snapshot.\n */\n isEstimate: boolean;\n}\n\nfunction parseAmount(value: string): number {\n const amount = Number(value);\n return Number.isFinite(amount) ? amount : 0;\n}\n\nexport function buildCartTotals(\n view: CartView,\n quote: SdkCartQuote | null,\n fallbackCurrency: string,\n): CartTotals {\n if (!quote) {\n return {\n subtotal: view.subtotal,\n discounts: [],\n aggregateDiscount: null,\n total: view.subtotal,\n currency: fallbackCurrency,\n isEstimate: true,\n };\n }\n\n const discount = parseAmount(quote.discount);\n const itemised = quote.applied_discounts ?? [];\n return {\n subtotal: parseAmount(quote.subtotal),\n discounts: itemised.map((entry) => ({ label: entry.label, amount: entry.amount })),\n // The quote states a reduction it did not break down: show it as one row\n // rather than letting the arithmetic of the block silently not add up.\n aggregateDiscount: itemised.length === 0 && discount > 0 ? discount : null,\n total: parseAmount(quote.total),\n currency: quote.currency || fallbackCurrency,\n isEstimate: view.subtotalIsEstimate,\n };\n}\n","/**\n * The contact form's submit path and the community channels beside it.\n *\n * The Storefront SDK has no contact method, so this posts the storefront\n * ticket endpoint directly — the same request, body and validation the\n * document lane's `sx-contact-form` runs through `submitStorefrontContactTicket`\n * (`packages/sdk/src/utils/storefront-contact.ts`). Keeping the payload shape\n * identical is what makes a message sent from a code storefront indistinguishable\n * from one sent from the Builder.\n */\n\nimport type { SocialLink } from './storefront-data';\n\n/** The message body cap the platform applies before the ticket is stored. */\nconst MESSAGE_MAX_LENGTH = 2000;\n\n/** A subject shorter than this is not a subject; the API titles it generically. */\nconst MIN_SUBJECT_LENGTH = 2;\nconst MAX_TITLE_LENGTH = 30;\nconst DEFAULT_TICKET_TITLE = 'Contact Request';\n\nexport interface ContactFormValues {\n name: string;\n email: string;\n subject: string;\n message: string;\n invoiceId: string;\n}\n\nexport interface ContactSubmissionContext {\n contactEnabled: boolean;\n requireInvoice: boolean;\n}\n\n/**\n * Client-side validation, mirroring `validateStorefrontContactSubmission`.\n *\n * It is a courtesy, not a gate: the backend enforces the same rules, and a\n * form that only *looked* validated would let a disabled shop's visitor watch\n * a request fail with no explanation.\n */\nexport function validateContactSubmission(\n values: ContactFormValues,\n context: ContactSubmissionContext,\n): string | null {\n if (!context.contactEnabled) return 'The contact form is disabled for this store.';\n // The name is marked required in the markup, and the form suppresses native\n // validation so it can own its error surface — which left the asterisk as\n // decoration and let a nameless message through. The Easy lane's form lets\n // the browser enforce the same `required`, so this is where the two agree.\n if (!values.name.trim() || !values.email.trim() || !values.message.trim()) {\n return 'Name, email and message are required.';\n }\n if (context.requireInvoice && !values.invoiceId.trim()) {\n return 'An order or invoice ID is required.';\n }\n // Checked on the message as it will be SENT, name line included: cutting it\n // down to the API limit would deliver a ticket the buyer never wrote while\n // the form reports success.\n if (values.subject.trim().length > MAX_TITLE_LENGTH) {\n return `Keep the subject under ${MAX_TITLE_LENGTH} characters.`;\n }\n if (buildTicketMessage(values).length > MESSAGE_MAX_LENGTH) {\n return `Your message is too long. Keep it under ${MESSAGE_MAX_LENGTH.toLocaleString('en-US')} characters.`;\n }\n return null;\n}\n\nfunction requiredGlobal(name: 'shopSlug' | 'apiBaseUrl'): string {\n const value = window[name];\n if (typeof value !== 'string' || !value.trim()) {\n throw new Error(\n `Shoppex bootstrap global \"${name}\" is missing. This storefront must be served by the Shoppex storefront (or preview) worker.`,\n );\n }\n return value.trim().replace(/\\/+$/, '');\n}\n\n/** The buyer's name rides along inside the body — the ticket has no name field. */\nfunction buildTicketMessage(values: ContactFormValues): string {\n const name = values.name.trim();\n const body = values.message.trim();\n return `${name ? `Name: ${name}\\n\\n` : ''}${body}`;\n}\n\nexport async function submitContactTicket(\n values: ContactFormValues,\n context: ContactSubmissionContext,\n): Promise<void> {\n const error = validateContactSubmission(values, context);\n if (error) throw new Error(error);\n\n const shopSlug = requiredGlobal('shopSlug');\n const apiBaseUrl = requiredGlobal('apiBaseUrl');\n const subject = values.subject.trim();\n const invoiceId = values.invoiceId.trim();\n\n const response = await fetch(\n `${apiBaseUrl}/v1/storefront/shops/name/${encodeURIComponent(shopSlug)}/tickets`,\n {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', Accept: 'application/json' },\n body: JSON.stringify({\n email: values.email.trim().toLowerCase(),\n title: subject.length >= MIN_SUBJECT_LENGTH\n ? subject\n : DEFAULT_TICKET_TITLE,\n message: buildTicketMessage(values),\n ...(invoiceId ? { invoice_id: invoiceId } : {}),\n }),\n },\n );\n\n const payload = (await response.json().catch(() => null)) as\n | { message?: string; error?: string; data?: { uniqid?: string } }\n | null;\n\n if (!response.ok) {\n throw new Error(payload?.message || payload?.error || 'Failed to send your message.');\n }\n if (!payload?.data?.uniqid) {\n throw new Error('Your message was accepted, but the store did not confirm a ticket.');\n }\n}\n\nexport interface SupportChannel {\n label: string;\n href: string;\n /** Mail links open the visitor's client rather than a new tab. */\n isMail: boolean;\n}\n\n/**\n * The channels a buyer can reach the shop on before they buy.\n *\n * Derived from the shop's own social profiles, not from theme content: those\n * handles belong to the merchant and are edited once in the dashboard. The\n * three networks and their order are the platform's\n * (`withShopContactFallbacks` in the document renderer), so the two lanes list\n * the same links in the same order.\n */\nexport function resolveSupportChannels(links: readonly SocialLink[]): SupportChannel[] {\n const byNetwork = new Map(links.map((link) => [link.network, link.href]));\n return (['discord', 'telegram', 'email'] as const).flatMap((network) => {\n const href = byNetwork.get(network);\n if (!href) return [];\n return [{\n label: network === 'email' ? 'Email' : network === 'discord' ? 'Discord' : 'Telegram',\n href,\n isMail: network === 'email',\n }];\n });\n}\n","/**\n * The merchant's navigation menus, and where they sit against theme content.\n *\n * A shop configures menus once in the dashboard and expects every theme to use\n * them. The Easy lane resolves a slot in this order\n * (`build-storefront-navigation.ts` + `resolveDocumentNavigationLinks`):\n *\n * 1. a link list the merchant authored IN THE THEME wins — including an\n * empty one, which means \"no links here\", not \"fall back\";\n * 2. otherwise the merchant's menu for that slot;\n * 3. otherwise the theme's own static links.\n *\n * Mirrored exactly. `hasAuthoredLinks` is what separates (1) from (3): the\n * template's shipped `nav.links` are step 3, not an override, so a shop with a\n * header menu and no theme edit gets its menu — which is the whole gap this\n * closes.\n *\n * The menus are fetched once per page load and shared by both slots. While the\n * request is in flight the theme's own links render: that is the same list step\n * 3 would settle on, so the header is never empty and never shows a spinner\n * where its navigation belongs. A shop WITH a menu therefore swaps its links in\n * once, on first resolve.\n */\n\nimport { useEffect, useMemo, useState } from 'react';\nimport { getShoppex } from './shoppex';\nimport type { SdkMenu, SdkMenuItem } from './sdk';\n\nexport type MenuSlot = 'header' | 'footer';\n\n/** One navigation link as a theme authors it: a label and where it goes. */\nexport type NavLink = {\n label: string;\n href: string;\n};\n\n/**\n * One menu item as a nav link, matching `menuItemToNavLink`: a PAGE item\n * resolves to `/page/<slug>` in the same tab, a link item keeps its url and its\n * stored target, and an item with neither destination is dropped rather than\n * rendered as a dead label.\n */\nfunction toNavLink(item: SdkMenuItem): (NavLink & { target: string | null }) | null {\n const label = typeof item.title === 'string' ? item.title.trim() : '';\n if (!label) return null;\n const pageSlug = typeof item.page_slug === 'string' ? item.page_slug.trim() : '';\n if (pageSlug) return { label, href: `/page/${pageSlug}`, target: null };\n const url = typeof item.url === 'string' ? item.url.trim() : '';\n if (!url) return null;\n return { label, href: url, target: item.target ?? null };\n}\n\nexport type MenuNavLink = NavLink & { target: string | null };\n\nexport function resolveMenuLinksForSlot(menus: readonly SdkMenu[], slot: MenuSlot): MenuNavLink[] {\n const menu = menus.find((entry) => entry.slot === slot);\n if (!menu || !Array.isArray(menu.items)) return [];\n return menu.items\n .flatMap((item) => {\n const link = toNavLink(item);\n return link ? [{ link, sortOrder: item.sort_order ?? 0 }] : [];\n })\n .sort((a, b) => a.sortOrder - b.sortOrder)\n .map((entry) => entry.link);\n}\n\n/**\n * The shop's menus, requested at most once per page load.\n *\n * Shared rather than per-hook because the header and the footer both need them\n * and the SDK's own cache is per-call — two components mounting in the same\n * frame would otherwise fire two requests for one answer. A failed request\n * resolves to no menus: the theme's own links are then the answer, which is the\n * same state a shop that configured none is in.\n */\nlet menusPromise: Promise<SdkMenu[]> | null = null;\n\nfunction loadMerchantMenus(): Promise<SdkMenu[]> {\n menusPromise ??= getShoppex()\n .getMenus()\n .then((result) => (result.success && Array.isArray(result.data) ? result.data : []))\n .catch(() => []);\n return menusPromise;\n}\n\nexport interface NavigationLinks {\n links: readonly MenuNavLink[];\n /**\n * True when the rendered list is the THEME's content, which is what decides\n * whether each entry carries a `data-sx-config` path — a merchant's menu item\n * is edited in the dashboard's navigation screen, not in the design\n * inspector, and pointing the inspector at it would offer an edit that goes\n * nowhere.\n */\n fromTheme: boolean;\n}\n\n/**\n * The links one slot should render, with the precedence above already applied.\n *\n * `themeLinks` is the template's own list. It is returned unchanged when the\n * merchant authored it (step 1) and while the menus are still loading; a\n * non-empty menu replaces it once resolved (step 2); an empty menu leaves it in\n * place (step 3).\n */\nexport function useNavigationLinks(\n slot: MenuSlot,\n themeLinks: readonly NavLink[],\n /** True when the merchant edited this slot's links in the theme content. */\n authored: boolean,\n): NavigationLinks {\n const themeNavLinks = useMemo<MenuNavLink[]>(\n () => themeLinks.map((link) => ({ ...link, target: null })),\n [themeLinks],\n );\n const [menuLinks, setMenuLinks] = useState<MenuNavLink[] | null>(null);\n\n useEffect(() => {\n // An authored override wins outright, so the menus are not even consulted —\n // the same reason `buildSlotLinks` returns before it reads `payload.menus`.\n if (authored) return undefined;\n let active = true;\n void loadMerchantMenus().then((menus) => {\n if (active) setMenuLinks(resolveMenuLinksForSlot(menus, slot));\n });\n return () => {\n active = false;\n };\n }, [authored, slot]);\n\n if (!authored && menuLinks !== null && menuLinks.length > 0) {\n return { links: menuLinks, fromTheme: false };\n }\n return { links: themeNavLinks, fromTheme: true };\n}\n","/**\n * Shop feedback for the `/reviews` page.\n *\n * Shoppex feedback is SHOP-level, not per product, and it is not part of the\n * injected catalog payload — the bootstrap object carries only per-product\n * tallies. The bodies come from a runtime call, so this is the one surface in\n * the theme that renders a list it has to wait for.\n *\n * Nothing here invents a review. A shop with no feedback resolves to an empty\n * list and the page says so; a failed request resolves to an error state and\n * the page says that instead. Neither is padded with sample data.\n */\n\nimport { useCallback, useEffect, useRef, useState } from 'react';\nimport type { SdkShopReview, SdkShopReviewsSummary } from './sdk';\nimport { getShoppex } from './shoppex';\n\nexport interface ShopReview {\n id: string;\n /** Filled-star count, the rating clamped to 0-5 and rounded. */\n starCount: number;\n rating: number;\n comment: string;\n author: string;\n /** Localized short date, e.g. \"Jan 12, 2026\". Blank when the API sent none. */\n dateLabel: string;\n /** Sort key; 0 when the entry carries no usable timestamp. */\n timestamp: number;\n /**\n * The reviewed product's CURRENT title, or null when the review names none.\n * The public feed withholds product ids, so the title is the only link to a\n * product there is; see `reviewsForProduct`.\n */\n productTitle: string | null;\n /** True when the review is tied to a real completed purchase. */\n verified: boolean;\n /** Left by the shop's review automation, not written by the buyer. */\n automated: boolean;\n}\n\nexport type ShopReviewsState =\n | { status: 'loading' }\n | { status: 'error' }\n | { status: 'ready'; reviews: ShopReview[]; average: number | null };\n\n/**\n * How many cards the page shows. The reference storefront pages its list at\n * twelve; a shop with hundreds of reviews should not ship all of them into one\n * screenshot-tall grid.\n */\nexport const SHOP_REVIEW_PAGE_SIZE = 12;\n\nexport interface ShopReviewLabels {\n anonymousAuthor: string;\n emptyComment: string;\n}\n\n/**\n * Accepts the string and number forms the feedback API uses for `created_at`:\n * an ISO date, an epoch in seconds, or an epoch in milliseconds. An\n * unparseable value sorts last and renders no date rather than \"Invalid Date\".\n */\nfunction toTimestamp(value: unknown): number {\n if (typeof value === 'number' && Number.isFinite(value)) {\n return value < 1_000_000_000_000 ? value * 1000 : value;\n }\n if (typeof value !== 'string' || value.trim() === '') return 0;\n const numeric = Number(value);\n if (Number.isFinite(numeric)) {\n return numeric < 1_000_000_000_000 ? numeric * 1000 : numeric;\n }\n const parsed = Date.parse(value);\n return Number.isNaN(parsed) ? 0 : parsed;\n}\n\nfunction formatDateLabel(timestamp: number): string {\n if (!timestamp) return '';\n return new Date(timestamp).toLocaleDateString('en-US', {\n day: 'numeric',\n month: 'short',\n year: 'numeric',\n });\n}\n\nexport function toStarCount(rating: number): number {\n if (!Number.isFinite(rating)) return 0;\n return Math.max(0, Math.min(5, Math.round(rating)));\n}\n\n/**\n * Newest first, and only entries that carry a real rating: a zero-star row is\n * an unrated record, not a one-star verdict, and averaging it in would drag\n * every shop's score down.\n */\nexport function toShopReviews(\n rows: readonly SdkShopReview[],\n labels: ShopReviewLabels,\n): ShopReview[] {\n return rows\n .filter((row) => typeof row.rating === 'number' && Number.isFinite(row.rating) && row.rating > 0)\n .map((row, index) => {\n const timestamp = toTimestamp(row.created_at);\n const comment = (row.comment ?? row.message ?? '').trim();\n const author = (row.author ?? row.customer_name ?? '').trim();\n return {\n id: row.id || `review:${index}`,\n rating: row.rating,\n starCount: toStarCount(row.rating),\n comment: comment.length > 0 ? comment : labels.emptyComment,\n author: author.length > 0 ? author : labels.anonymousAuthor,\n dateLabel: formatDateLabel(timestamp),\n timestamp,\n productTitle: row.product_title?.trim() || null,\n verified: row.verified === true,\n automated: row.is_automated === true,\n };\n })\n .sort((a, b) => b.timestamp - a.timestamp);\n}\n\n/**\n * The written reviews that name this product, newest first.\n *\n * Matched on the product's title because the public feed carries no product\n * ids. That makes it a sample, not a count: the feed leaves out appealed and\n * filtered feedback, and a renamed product loses its older reviews. Surfaces\n * show these as \"latest reviews\" beside the payload's own tally and never\n * derive a number from them.\n */\nexport function reviewsForProduct(reviews: readonly ShopReview[], productTitle: string): ShopReview[] {\n const title = productTitle.trim();\n if (!title) return [];\n return reviews.filter((review) => review.productTitle === title);\n}\n\n/** One page of the shop's reviews, newest first. */\nexport interface ShopReviewsPageResult {\n reviews: ShopReview[];\n /** Pass to the next call; null on the last page. */\n nextCursor: string | null;\n /** Counts over the whole list (or the product's reviews), null when the API sent none. */\n summary: SdkShopReviewsSummary | null;\n}\n\n/**\n * One page of reviews from the feed, optionally for one product (its catalog\n * `id`). Throws when the request fails.\n *\n * A product page must never show another product's reviews, so a filtered\n * request also requires the API to echo the filter back: an API that predates\n * `product` ignores it and would answer with the whole shop's list.\n */\nexport async function loadShopReviewsPage(options: {\n cursor?: string | null;\n product?: string | null;\n labels: ShopReviewLabels;\n}): Promise<ShopReviewsPageResult> {\n const product = options.product?.trim() || null;\n const result = await getShoppex().getShopReviewsPage(options.cursor ?? null, product ? { product } : undefined);\n if (!result.success || !result.data) {\n throw new Error(result.message?.trim() || 'Reviews could not be loaded.');\n }\n if (product !== null && result.data.product !== product) {\n throw new Error('This store does not filter reviews by product yet.');\n }\n const { pagination } = result.data;\n return {\n reviews: toShopReviews(result.data.feedback, options.labels),\n nextCursor: pagination.has_more ? pagination.next_cursor : null,\n summary: result.data.summary ?? null,\n };\n}\n\n/**\n * The newest WRITTEN reviews, at most `limit`, for a sample such as a home\n * section or a product's review tab. Pages through the feed only until it has\n * enough. Automated entries and ratings without words are skipped, since a\n * sample of quotes needs quotes. Resolves null when a page request fails.\n */\nexport async function loadLatestWrittenReviews(\n limit: number,\n labels: ShopReviewLabels,\n options: { product?: string | null } = {},\n): Promise<ShopReview[] | null> {\n const written: ShopReview[] = [];\n const seen = new Set<string>();\n let cursor: string | null = null;\n try {\n while (written.length < limit) {\n const page = await loadShopReviewsPage({ cursor, product: options.product, labels: { anonymousAuthor: labels.anonymousAuthor, emptyComment: '' } });\n written.push(...page.reviews.filter((review) => !review.automated && review.comment !== ''));\n if (!page.nextCursor || seen.has(page.nextCursor)) break;\n seen.add(page.nextCursor);\n cursor = page.nextCursor;\n }\n } catch {\n return null;\n }\n return written.slice(0, limit).map((review) => ({ ...review, comment: review.comment || labels.emptyComment }));\n}\n\nexport type PagedShopReviewsState =\n | { status: 'loading' }\n | { status: 'error' }\n | {\n status: 'ready';\n reviews: ShopReview[];\n summary: SdkShopReviewsSummary | null;\n hasMore: boolean;\n /** True while the next page is on its way. */\n loadingMore: boolean;\n /** Set when the NEXT page failed; the pages already shown stay. */\n moreFailed: boolean;\n };\n\n/**\n * The shop's reviews, one page at a time: the first page on mount (or when\n * `product` changes), the next only when `loadMore` is called. The summary is\n * the API's, so the score and breakdown describe the whole list, not the pages\n * loaded so far.\n */\nexport function usePagedShopReviews(options: {\n product?: string | null;\n labels: ShopReviewLabels;\n}): PagedShopReviewsState & { loadMore: () => void } {\n const product = options.product?.trim() || null;\n const { anonymousAuthor, emptyComment } = options.labels;\n const [state, setState] = useState<PagedShopReviewsState>({ status: 'loading' });\n const [cursor, setCursor] = useState<string | null>(null);\n const generation = useRef(0);\n\n useEffect(() => {\n const current = (generation.current += 1);\n setState({ status: 'loading' });\n setCursor(null);\n loadShopReviewsPage({ product, labels: { anonymousAuthor, emptyComment } })\n .then((page) => {\n if (generation.current !== current) return;\n setCursor(page.nextCursor);\n setState({\n status: 'ready',\n reviews: page.reviews,\n summary: page.summary,\n hasMore: page.nextCursor !== null,\n loadingMore: false,\n moreFailed: false,\n });\n })\n .catch(() => {\n if (generation.current === current) setState({ status: 'error' });\n });\n }, [product, anonymousAuthor, emptyComment]);\n\n const loadMore = useCallback(() => {\n if (state.status !== 'ready' || state.loadingMore || cursor === null) return;\n const current = generation.current;\n setState({ ...state, loadingMore: true, moreFailed: false });\n loadShopReviewsPage({ cursor, product, labels: { anonymousAuthor, emptyComment } })\n .then((page) => {\n if (generation.current !== current) return;\n setCursor(page.nextCursor);\n setState((previous) => previous.status !== 'ready' ? previous : {\n ...previous,\n reviews: [...previous.reviews, ...page.reviews],\n hasMore: page.nextCursor !== null,\n loadingMore: false,\n });\n })\n .catch(() => {\n if (generation.current !== current) return;\n setState((previous) => previous.status !== 'ready' ? previous : { ...previous, loadingMore: false, moreFailed: true });\n });\n }, [state, cursor, product, anonymousAuthor, emptyComment]);\n\n return { ...state, loadMore };\n}\n\nexport function averageRating(reviews: readonly ShopReview[]): number | null {\n if (reviews.length === 0) return null;\n return reviews.reduce((total, review) => total + review.rating, 0) / reviews.length;\n}\n\nexport function useShopReviews(labels: ShopReviewLabels): ShopReviewsState {\n const [state, setState] = useState<ShopReviewsState>({ status: 'loading' });\n const { anonymousAuthor, emptyComment } = labels;\n\n useEffect(() => {\n let active = true;\n getShoppex()\n .getShopReviews()\n .then((result) => {\n if (!active) return;\n if (!result.success || !result.data) {\n setState({ status: 'error' });\n return;\n }\n const reviews = toShopReviews(result.data, { anonymousAuthor, emptyComment });\n setState({ status: 'ready', reviews, average: averageRating(reviews) });\n })\n .catch(() => {\n if (active) setState({ status: 'error' });\n });\n return () => {\n active = false;\n };\n }, [anonymousAuthor, emptyComment]);\n\n return state;\n}\n","/**\n * Merchant CMS pages (dashboard → Pages), fetched by slug for `/page/:slug`.\n *\n * These are NOT in the injected catalog: the edge injects the storefront\n * payload, and the merchant's pages come from a separate service the SDK reads\n * (`getPage` / `getPages`). So the route loads them, and every state it can be\n * in is explicit — a slug that does not resolve renders the theme's not-found\n * copy rather than an empty article.\n *\n * The rendering rules are the Easy lane's, from `enrichCmsPageBlockSettings`\n * plus `blocks/page-tool-page.ts` → `renderCmsPage`:\n *\n * 1. `template_type === 'faq'` and `settings.faq_items` non-empty → the\n * accordion. A row needs BOTH a question and an answer to count, exactly\n * as `normalizeCmsFaqItems` requires.\n * 2. otherwise the body, rendered from MARKDOWN (gfm + breaks, the same\n * `marked` configuration `renderCmsPageMarkdownHtml` uses).\n * 3. otherwise the empty state.\n *\n * What this module does NOT do is sanitize. The markdown pass emits merchant\n * HTML — markdown may contain raw tags — and it is merchant HTML of exactly the\n * same status as `shop.termsHtml` or a product description, so it is sanitized\n * where it is injected, by the component that injects it. That is the rule the\n * whole template follows and the one its HTML-sink guard checks.\n */\n\nimport { getShoppex } from './shoppex';\nimport type { SdkPage } from './sdk';\n\nexport type ShopPageFaqItem = { question: string; answer: string };\n\n/** One resolved CMS page, reduced to what the route renders. */\nexport interface ShopPageModel {\n title: string;\n /**\n * The markdown body as HTML — merchant markup, NOT yet sanitized. The route\n * sanitizes it at the sink; see the module docblock.\n */\n bodyMarkdownHtml: string;\n /** Non-empty only for a `faq` page with usable rows. */\n faqItems: ShopPageFaqItem[];\n}\n\nexport type ShopPageState =\n | { status: 'loading' }\n | { status: 'ready'; page: ShopPageModel }\n /** The API answered that no such page exists (404), or the page has no name. */\n | { status: 'missing' }\n /**\n * The page could not be loaded: the API failed or could not be reached, or\n * the page's body could not be rendered. Not \"missing\": the page may exist.\n */\n | { status: 'error' };\n\n/** Mirrors `normalizeCmsFaqItems`: a row needs BOTH halves or it is not a row. */\nfunction parseFaqItems(value: unknown): ShopPageFaqItem[] {\n if (!Array.isArray(value)) return [];\n const items: ShopPageFaqItem[] = [];\n for (const entry of value) {\n if (entry === null || typeof entry !== 'object') continue;\n const record = entry as { question?: unknown; answer?: unknown };\n const question = typeof record.question === 'string' ? record.question.trim() : '';\n const answer = typeof record.answer === 'string' ? record.answer.trim() : '';\n if (question && answer) items.push({ question, answer });\n }\n return items;\n}\n\n/**\n * The merchant's markdown body as HTML.\n *\n * `marked` runs with gfm and breaks on, which is what `renderCmsPageMarkdownHtml`\n * configures — a page that renders as a table on one lane must not render as a\n * row of pipes on the other. Markdown can emit raw HTML, so this is not a\n * sanitizer and never stands in for one.\n *\n * The parser is imported on DEMAND. It is ~40 kB of JavaScript that only this\n * one route can ever use, and loading it in the main bundle would make every\n * buyer who never opens a CMS page pay for one.\n */\nasync function renderShopPageMarkdownHtml(content: string | null | undefined): Promise<string> {\n const trimmed = typeof content === 'string' ? content.trim() : '';\n if (!trimmed) return '';\n const { marked } = await import('marked');\n return marked.parse(trimmed, { async: false, gfm: true, breaks: true });\n}\n\nasync function buildShopPageModel(page: SdkPage): Promise<ShopPageModel> {\n // FAQ rows are a `faq` template's content, not a fallback for a page whose\n // body failed to render — the same gate `cmsPageFaqItems` applies.\n const faqItems = page.template_type === 'faq' ? parseFaqItems(page.settings?.faq_items) : [];\n return {\n title: page.name,\n // A FAQ page never shows its body, so its markdown is never parsed and the\n // parser is never fetched for one.\n bodyMarkdownHtml: faqItems.length > 0 ? '' : await renderShopPageMarkdownHtml(page.content),\n faqItems,\n };\n}\n\n/**\n * Loads one CMS page by slug.\n *\n * `missing` only when the API says so (404) or the page it returns has no name\n * to show; inventing a title from the slug would publish a heading the merchant\n * never wrote. Any other failure is `error`, so an outage never tells a buyer a\n * real page does not exist.\n */\nexport async function loadShopPage(slug: string): Promise<ShopPageState> {\n let result: Awaited<ReturnType<ReturnType<typeof getShoppex>['getPage']>>;\n try {\n result = await getShoppex().getPage(slug);\n } catch {\n return { status: 'error' };\n }\n if (!result.success) return result.status === 404 ? { status: 'missing' } : { status: 'error' };\n const page = result.data;\n if (!page || typeof page.name !== 'string' || !page.name.trim()) return { status: 'missing' };\n try {\n return { status: 'ready', page: await buildShopPageModel(page) };\n } catch {\n // The markdown parser is a lazy chunk; a failed import is an error, not a\n // missing page.\n return { status: 'error' };\n }\n}\n","/**\n * \"Notify me\" on sold-out products: a buyer leaves an email and gets one email\n * when the product (or the chosen option) can be ordered again.\n *\n * The control appears only where it can work: the shop allows it\n * (`settings.stockAlertsEnabled`, a merchant switch enforced by the backend),\n * and the selection is sold out, not paused. A paused (\"coming soon\") product\n * is not a stock state, and an info card can never be bought, so neither\n * offers an alert.\n *\n * The endpoint answers the same for a new and a repeated request, so the UI\n * says \"We'll email you\" either way.\n */\n\nimport type { StorefrontPriceVariant, StorefrontProduct, StorefrontSettings } from './storefront-data';\n\nexport function canRequestStockAlert(\n settings: Pick<StorefrontSettings, 'stockAlertsEnabled'>,\n product: Pick<StorefrontProduct, 'onHold' | 'productType'>,\n soldOut: boolean,\n): boolean {\n return settings.stockAlertsEnabled && soldOut && !product.onHold && product.productType !== 'INFO_CARD';\n}\n\nexport interface StockAlertRequest {\n /** The product's uniqid (`StorefrontProduct.id`). */\n productId: string;\n /** The sold-out option, or null for the product as a whole. */\n variantId: StorefrontPriceVariant['id'] | null;\n email: string;\n /**\n * The hidden honeypot input's value. Pass it through untouched: people leave\n * it empty, form bots fill it, and the server quietly ignores a filled one.\n */\n honeypot?: string;\n}\n\nexport type StockAlertResult =\n | { ok: true }\n | { ok: false; status: number | null; message: string };\n\nfunction bootstrapGlobal(name: 'shopSlug' | 'apiBaseUrl'): string {\n const value = window[name];\n if (typeof value !== 'string' || !value.trim()) {\n throw new Error(\n `Shoppex bootstrap global \"${name}\" is missing. This storefront must be served by the Shoppex storefront (or preview) worker.`,\n );\n }\n return value.trim().replace(/\\/+$/, '');\n}\n\n/** The page language (`<html lang>`), which the backend uses for the email. */\nfunction pageLocale(): string | null {\n const lang = typeof document === 'undefined' ? '' : document.documentElement.lang.trim();\n return lang || null;\n}\n\nconst GENERIC_FAILURE = 'We could not save your request. Please try again.';\n\nexport async function requestStockAlert(request: StockAlertRequest): Promise<StockAlertResult> {\n const email = request.email.trim();\n if (!email) return { ok: false, status: null, message: 'Enter your email address.' };\n\n let response: Response;\n try {\n const shopSlug = bootstrapGlobal('shopSlug');\n const apiBaseUrl = bootstrapGlobal('apiBaseUrl');\n response = await fetch(\n `${apiBaseUrl}/v1/storefront/shops/name/${encodeURIComponent(shopSlug)}/products/${encodeURIComponent(request.productId)}/stock-alerts`,\n {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', Accept: 'application/json' },\n body: JSON.stringify({\n email,\n variant_id: request.variantId,\n locale: pageLocale(),\n ...(request.honeypot ? { website: request.honeypot } : {}),\n }),\n },\n );\n } catch {\n return { ok: false, status: null, message: GENERIC_FAILURE };\n }\n\n if (response.status === 202) return { ok: true };\n const payload = (await response.json().catch(() => null)) as { error?: unknown; message?: unknown } | null;\n const serverMessage = typeof payload?.error === 'string' && payload.error.trim()\n ? payload.error.trim()\n : typeof payload?.message === 'string' && payload.message.trim() ? payload.message.trim() : null;\n return { ok: false, status: response.status, message: serverMessage ?? GENERIC_FAILURE };\n}\n","/**\n * The parsed storefront payload, provided once at the root.\n *\n * Same discipline as the content contract: the wire payload is parsed strictly\n * in exactly one place, above everything that reads it, so a changed contract\n * surfaces as an explicit error instead of a storefront that renders most of a\n * shop and quietly drops the rest.\n */\n\nimport { createContext, type ReactNode, useContext, useEffect, useState } from 'react';\nimport { buildStorefrontCatalog, type StorefrontCatalog } from './catalog';\nimport { getShoppex } from './shoppex';\nimport {\n getStorefrontSettings,\n getStorefrontShop,\n type StorefrontSettings,\n type StorefrontShop,\n} from './storefront-data';\n\nexport interface StorefrontData {\n settings: StorefrontSettings;\n shop: StorefrontShop;\n catalog: StorefrontCatalog;\n}\n\nexport function readStorefrontData(): StorefrontData {\n const settings = getStorefrontSettings();\n const catalog = buildStorefrontCatalog(settings);\n return { settings, shop: getStorefrontShop(catalog.listedProductCount), catalog };\n}\n\nconst StorefrontContext = createContext<StorefrontData | null>(null);\n\nexport function StorefrontProvider({ data, children }: { data: StorefrontData; children: ReactNode }) {\n return <StorefrontContext.Provider value={data}>{children}</StorefrontContext.Provider>;\n}\n\nexport function useStorefront(): StorefrontData {\n const data = useContext(StorefrontContext);\n if (!data) throw new Error('useStorefront must be used inside StorefrontProvider.');\n return data;\n}\n\n/**\n * Live visitor count for the header pill.\n *\n * Starts at null — \"not answered yet\" — and the pill renders nothing until a\n * real number arrives. A placeholder zero would flash \"0 online\" on every page\n * load, and a placeholder guess would be a fabricated claim. A failed request\n * leaves it null and the pill simply stays away.\n */\nexport function useOnlineUserCount(): number | null {\n const [count, setCount] = useState<number | null>(null);\n\n useEffect(() => {\n let active = true;\n getShoppex()\n .getStorefrontOnlineUsers()\n .then((result) => {\n if (!active || !result.success || !result.data) return;\n const value = result.data.count;\n if (Number.isFinite(value) && value > 0) setCount(Math.floor(value));\n })\n .catch(() => undefined);\n return () => {\n active = false;\n };\n }, []);\n\n return count;\n}\n"],"mappings":"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","names":["isRecord","total","value","useEffect","useMemo","useState","useMemo","useState","useEffect","useCallback","useEffect","useRef","useState","useState","useRef","useEffect","useCallback","createContext","useContext","useEffect","useState","jsx","createContext","useContext","useState","useEffect"]}