sanity-plugin-stripe 0.1.0 → 0.2.0

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/react.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"react.js","names":[],"sources":["../src/react/cart.tsx","../src/react/ClearCartOnReturn.tsx","../src/react/useBuyBox.ts","../src/react/useCheckout.ts"],"sourcesContent":["import {\n createContext,\n useContext,\n useMemo,\n useState,\n useSyncExternalStore,\n type ReactNode,\n} from 'react'\n\nimport {lineKey, MAX_QUANTITY, sanitizeLines, type CartLine} from '../core/cart'\n\n/**\n * The cart lives in localStorage, so it survives reloads and stays in step across tabs.\n * It holds product references and quantities only; prices are always read again at checkout.\n */\n\nconst EMPTY: CartLine[] = []\n\ninterface CartStore {\n subscribe: (listener: () => void) => () => void\n getSnapshot: () => CartLine[]\n getServerSnapshot: () => CartLine[]\n set: (next: CartLine[]) => void\n}\n\nfunction createCartStore(storageKey: string, max: number): CartStore {\n let lines = EMPTY\n let loaded = false\n const listeners = new Set<() => void>()\n // An empty cart is always the same array, so clearing an empty cart doesn't re-render.\n const clean = (input: unknown) => {\n const result = sanitizeLines(input, max)\n return result.length ? result : EMPTY\n }\n\n const read = () => {\n try {\n lines = clean(JSON.parse(window.localStorage.getItem(storageKey) ?? '[]'))\n } catch {\n lines = EMPTY\n }\n loaded = true\n }\n const emit = () => listeners.forEach((listener) => listener())\n // Another tab changed the cart.\n const onStorage = (event: StorageEvent) => {\n if (event.key !== storageKey) return\n read()\n emit()\n }\n\n return {\n subscribe(listener) {\n if (listeners.size === 0) window.addEventListener('storage', onStorage)\n listeners.add(listener)\n return () => {\n listeners.delete(listener)\n if (listeners.size === 0) window.removeEventListener('storage', onStorage)\n }\n },\n getSnapshot() {\n if (!loaded) read()\n return lines\n },\n getServerSnapshot: () => EMPTY,\n set(next) {\n lines = clean(next)\n loaded = true\n try {\n window.localStorage.setItem(storageKey, JSON.stringify(lines))\n } catch {\n // Private browsing or full storage: the cart still works until the page closes.\n }\n emit()\n },\n }\n}\n\nconst CartContext = createContext<{store: CartStore; max: number} | null>(null)\n\nexport interface CartProviderProps {\n children: ReactNode\n /** localStorage key. Change it to start everyone with an empty cart. Default `cart`. */\n storageKey?: string\n /** Most of one item a buyer can order at once. Default 10. */\n maxQuantity?: number\n}\n\n/** Holds the cart for everything inside it. Put it in the site's layout. */\nexport function CartProvider({\n children,\n storageKey = 'cart',\n maxQuantity = MAX_QUANTITY,\n}: CartProviderProps) {\n const [store] = useState(() => createCartStore(storageKey, maxQuantity))\n const value = useMemo(() => ({store, max: maxQuantity}), [store, maxQuantity])\n return <CartContext.Provider value={value}>{children}</CartContext.Provider>\n}\n\ntype LineRef = Pick<CartLine, 'productId' | 'variantKey'>\n\nexport interface Cart {\n lines: CartLine[]\n /** Number of items, counting quantities. */\n count: number\n /** False during server rendering and hydration, before the stored cart has been read. */\n ready: boolean\n /** Most of one item a buyer can order at once. */\n maxQuantity: number\n /** Adds to the line for this product and variant. `limit` caps the total, for example at the stock count. */\n add: (line: CartLine, limit?: number) => void\n setQuantity: (line: LineRef, quantity: number) => void\n remove: (line: LineRef) => void\n clear: () => void\n}\n\nconst subscribeNothing = () => () => {}\n\n/** The cart, and the ways to change it. Needs a `<CartProvider>` higher up the tree. */\nexport function useCart(): Cart {\n const context = useContext(CartContext)\n if (!context) throw new Error('useCart needs a <CartProvider> higher up the tree.')\n const {store, max} = context\n const lines = useSyncExternalStore(store.subscribe, store.getSnapshot, store.getServerSnapshot)\n const ready = useSyncExternalStore(\n subscribeNothing,\n () => true,\n () => false,\n )\n\n return useMemo(() => {\n const current = () => store.getSnapshot()\n const same = (a: LineRef, b: LineRef) => lineKey(a) === lineKey(b)\n /** The cart with one line's quantity changed. */\n const withQuantity = (line: LineRef, quantity: number) =>\n current().map((candidate) =>\n same(candidate, line) ? Object.assign({}, candidate, {quantity}) : candidate,\n )\n return {\n lines,\n count: lines.reduce((sum, line) => sum + line.quantity, 0),\n ready,\n maxQuantity: max,\n add(line, limit = max) {\n const existing = current().find((candidate) => same(candidate, line))\n const quantity = Math.min((existing?.quantity ?? 0) + line.quantity, limit, max)\n if (quantity < 1) return\n store.set(existing ? withQuantity(line, quantity) : [...current(), {...line, quantity}])\n },\n setQuantity(line, quantity) {\n if (quantity < 1) {\n store.set(current().filter((candidate) => !same(candidate, line)))\n return\n }\n store.set(withQuantity(line, Math.min(quantity, max)))\n },\n remove(line) {\n store.set(current().filter((candidate) => !same(candidate, line)))\n },\n clear() {\n store.set(EMPTY)\n },\n }\n }, [lines, ready, store, max])\n}\n","import {useEffect} from 'react'\n\nimport {useCart} from './cart'\n\nexport interface ClearCartOnReturnProps {\n /** The query parameter Stripe's `success_url` carries. Default `session_id`. */\n param?: string\n}\n\n/**\n * Empties the cart on the page Stripe sends buyers back to, when the address carries a Checkout\n * Session id. It draws nothing. The order itself is handled by the webhook, not by this page.\n */\nexport function ClearCartOnReturn({param = 'session_id'}: ClearCartOnReturnProps) {\n const {ready, clear} = useCart()\n useEffect(() => {\n if (ready && new URLSearchParams(window.location.search).has(param)) clear()\n }, [ready, clear, param])\n return null\n}\n","import {useState} from 'react'\n\nimport {\n lineKey,\n lineStatus,\n type CartProduct,\n type CartVariant,\n type LineStatus,\n} from '../core/cart'\nimport {useCart} from './cart'\n\nexport interface BuyBox {\n /** The product's variants, each with whether it can be bought. Empty for a product without any. */\n variants: Array<CartVariant & {available: boolean}>\n /** The chosen variant: the first one on sale, until the buyer picks another. */\n variantKey: string | undefined\n chooseVariant: (variantKey: string) => void\n /** Nothing of this product can be bought. */\n soldOut: boolean\n /** Whether the chosen product and variant can be bought, its price and how many. */\n status: LineStatus\n /** How many of the chosen product and variant the cart holds. */\n inCart: number\n /** The cart already holds as many as can be ordered. */\n full: boolean\n /** True after `add`, until the buyer picks another variant. */\n added: boolean\n add: (quantity?: number) => void\n}\n\n/**\n * What a product page's buy button needs: the variants, which one is chosen, whether it can be\n * bought, and adding it to the cart. The markup is yours.\n */\nexport function useBuyBox(product: CartProduct): BuyBox {\n const cart = useCart()\n const max = cart.maxQuantity\n const variants = (product.commerce?.variants ?? []).map((variant) =>\n Object.assign({}, variant, {available: lineStatus(product, variant._key, max).available}),\n )\n const firstAvailable = variants.find((variant) => variant.available)\n const [chosen, setChosen] = useState<string>()\n const [added, setAdded] = useState(false)\n\n const variantKey = chosen ?? firstAvailable?._key\n const status = lineStatus(product, variantKey, max)\n const soldOut = variants.length ? !firstAvailable : !status.available\n const line = {productId: product._id, ...(variantKey ? {variantKey} : {})}\n const inCart = cart.lines.find((candidate) => lineKey(candidate) === lineKey(line))?.quantity ?? 0\n\n return {\n variants,\n variantKey,\n chooseVariant(key) {\n setChosen(key)\n setAdded(false)\n },\n soldOut,\n status,\n inCart,\n full: status.available && inCart >= status.maxQuantity,\n added,\n add(quantity = 1) {\n if (!status.available) return\n cart.add({...line, quantity}, status.maxQuantity)\n setAdded(true)\n },\n }\n}\n","import {useCallback, useEffect, useState} from 'react'\n\nimport {readCheckoutProblems, type CheckoutProblem} from '../core/cart'\nimport {asString, isObject} from '../core/guards'\nimport {useCart} from './cart'\n\nexport type CheckoutState =\n | {status: 'idle'}\n /** Asking the site for a Checkout Session, then on the way to Stripe. */\n | {status: 'loading'}\n | {\n status: 'error'\n /** `region`, `empty`, `unavailable`, `invalid`, or `offline` when the site can't be reached. */\n error: string\n /** For the buyer. */\n message: string\n /** For the developer: only sent by a checkout route with `debug` on. */\n detail?: string\n /** The cart lines the checkout turned down, and why. */\n problems: CheckoutProblem[]\n }\n\nexport interface UseCheckoutOptions {\n /** The site's checkout route. Default `/api/checkout`. */\n endpoint?: string\n messages?: {\n /** No shipping region chosen. */\n region?: string\n /** The site answered without saying why. */\n unavailable?: string\n /** The site couldn't be reached. */\n offline?: string\n }\n}\n\nexport interface Checkout {\n state: CheckoutState\n /** Sends the cart and the chosen region to the checkout route, then goes to Stripe's page. */\n start: (regionId: string | undefined) => Promise<void>\n /** Back to `idle`, for example when the buyer changes the cart after an error. */\n reset: () => void\n}\n\nconst MESSAGES = {\n region: 'Choose where to ship the order.',\n unavailable: \"Checkout isn't available right now. Please try again later.\",\n offline: \"Couldn't reach the shop. Check your connection and try again.\",\n}\n\n/** Starts Stripe Checkout for the cart, and says what went wrong when it can't. */\nexport function useCheckout(options: UseCheckoutOptions = {}): Checkout {\n const {endpoint = '/api/checkout'} = options\n const messages = {...MESSAGES, ...options.messages}\n const {lines} = useCart()\n const [state, setState] = useState<CheckoutState>({status: 'idle'})\n\n // Back from Stripe's page, a browser can restore this one exactly as it was left: still loading.\n useEffect(() => {\n const restore = (event: PageTransitionEvent) => {\n if (event.persisted) setState({status: 'idle'})\n }\n window.addEventListener('pageshow', restore)\n return () => window.removeEventListener('pageshow', restore)\n }, [])\n\n const start = async (regionId: string | undefined) => {\n if (!regionId) {\n setState({status: 'error', error: 'region', message: messages.region, problems: []})\n return\n }\n setState({status: 'loading'})\n try {\n const response = await fetch(endpoint, {\n method: 'POST',\n headers: {'Content-Type': 'application/json'},\n body: JSON.stringify({lines, regionId}),\n })\n const body: unknown = await response.json().catch(() => null)\n const data = isObject(body) ? body : {}\n if (response.ok && typeof data.url === 'string') {\n // Stay in the loading state while the browser goes to Stripe.\n window.location.assign(data.url)\n return\n }\n setState({\n status: 'error',\n error: asString(data.error) ?? 'unavailable',\n message: asString(data.message) ?? messages.unavailable,\n ...(typeof data.detail === 'string' ? {detail: data.detail} : {}),\n problems: readCheckoutProblems(data.problems),\n })\n } catch {\n setState({status: 'error', error: 'offline', message: messages.offline, problems: []})\n }\n }\n\n const reset = useCallback(() => setState({status: 'idle'}), [])\n return {state, start, reset}\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"react.js","names":[],"sources":["../src/react/cart.tsx","../src/react/ClearCartOnReturn.tsx","../src/react/useBuyBox.ts","../src/react/useCheckout.ts"],"sourcesContent":["import {\n createContext,\n useContext,\n useMemo,\n useState,\n useSyncExternalStore,\n type ReactNode,\n} from 'react'\n\nimport {lineKey, MAX_QUANTITY, sanitizeLines, type CartLine} from '../core/cart'\n\n/**\n * The cart lives in localStorage, so it survives reloads and stays in step across tabs.\n * It holds product references and quantities only; prices are always read again at checkout.\n */\n\nconst EMPTY: CartLine[] = []\n\ninterface CartStore {\n subscribe: (listener: () => void) => () => void\n getSnapshot: () => CartLine[]\n getServerSnapshot: () => CartLine[]\n set: (next: CartLine[]) => void\n}\n\nfunction createCartStore(storageKey: string, max: number): CartStore {\n let lines = EMPTY\n let loaded = false\n const listeners = new Set<() => void>()\n // An empty cart is always the same array, so clearing an empty cart doesn't re-render.\n const clean = (input: unknown) => {\n const result = sanitizeLines(input, max)\n return result.length ? result : EMPTY\n }\n\n const read = () => {\n try {\n lines = clean(JSON.parse(window.localStorage.getItem(storageKey) ?? '[]'))\n } catch {\n lines = EMPTY\n }\n loaded = true\n }\n const emit = () => listeners.forEach((listener) => listener())\n // Another tab changed the cart.\n const onStorage = (event: StorageEvent) => {\n if (event.key !== storageKey) return\n read()\n emit()\n }\n\n return {\n subscribe(listener) {\n if (listeners.size === 0) window.addEventListener('storage', onStorage)\n listeners.add(listener)\n return () => {\n listeners.delete(listener)\n if (listeners.size === 0) window.removeEventListener('storage', onStorage)\n }\n },\n getSnapshot() {\n if (!loaded) read()\n return lines\n },\n getServerSnapshot: () => EMPTY,\n set(next) {\n lines = clean(next)\n loaded = true\n try {\n window.localStorage.setItem(storageKey, JSON.stringify(lines))\n } catch {\n // Private browsing or full storage: the cart still works until the page closes.\n }\n emit()\n },\n }\n}\n\nconst CartContext = createContext<{store: CartStore; max: number} | null>(null)\n\nexport interface CartProviderProps {\n children: ReactNode\n /** localStorage key. Change it to start everyone with an empty cart. Default `cart`. */\n storageKey?: string\n /** Most of one item a buyer can order at once. Default 10. */\n maxQuantity?: number\n}\n\n/** Holds the cart for everything inside it. Put it in the site's layout. */\nexport function CartProvider({\n children,\n storageKey = 'cart',\n maxQuantity = MAX_QUANTITY,\n}: CartProviderProps) {\n const [store] = useState(() => createCartStore(storageKey, maxQuantity))\n const value = useMemo(() => ({store, max: maxQuantity}), [store, maxQuantity])\n return <CartContext.Provider value={value}>{children}</CartContext.Provider>\n}\n\ntype LineRef = Pick<CartLine, 'productId' | 'variantKey' | 'plan'>\n\nexport interface Cart {\n lines: CartLine[]\n /** Number of items, counting quantities. */\n count: number\n /** False during server rendering and hydration, before the stored cart has been read. */\n ready: boolean\n /** Most of one item a buyer can order at once. */\n maxQuantity: number\n /**\n * Adds to the line for this product, variant and way of buying (once, or `plan: 'subscribe'`).\n * `limit` caps the total, for example at the stock count.\n */\n add: (line: CartLine, limit?: number) => void\n setQuantity: (line: LineRef, quantity: number) => void\n remove: (line: LineRef) => void\n clear: () => void\n}\n\nconst subscribeNothing = () => () => {}\n\n/** The cart, and the ways to change it. Needs a `<CartProvider>` higher up the tree. */\nexport function useCart(): Cart {\n const context = useContext(CartContext)\n if (!context) throw new Error('useCart needs a <CartProvider> higher up the tree.')\n const {store, max} = context\n const lines = useSyncExternalStore(store.subscribe, store.getSnapshot, store.getServerSnapshot)\n const ready = useSyncExternalStore(\n subscribeNothing,\n () => true,\n () => false,\n )\n\n return useMemo(() => {\n const current = () => store.getSnapshot()\n const same = (a: LineRef, b: LineRef) => lineKey(a) === lineKey(b)\n /** The cart with one line's quantity changed. */\n const withQuantity = (line: LineRef, quantity: number) =>\n current().map((candidate) =>\n same(candidate, line) ? Object.assign({}, candidate, {quantity}) : candidate,\n )\n return {\n lines,\n count: lines.reduce((sum, line) => sum + line.quantity, 0),\n ready,\n maxQuantity: max,\n add(line, limit = max) {\n const existing = current().find((candidate) => same(candidate, line))\n const quantity = Math.min((existing?.quantity ?? 0) + line.quantity, limit, max)\n if (quantity < 1) return\n store.set(existing ? withQuantity(line, quantity) : [...current(), {...line, quantity}])\n },\n setQuantity(line, quantity) {\n if (quantity < 1) {\n store.set(current().filter((candidate) => !same(candidate, line)))\n return\n }\n store.set(withQuantity(line, Math.min(quantity, max)))\n },\n remove(line) {\n store.set(current().filter((candidate) => !same(candidate, line)))\n },\n clear() {\n store.set(EMPTY)\n },\n }\n }, [lines, ready, store, max])\n}\n","import {useEffect} from 'react'\n\nimport {useCart} from './cart'\n\nexport interface ClearCartOnReturnProps {\n /** The query parameter Stripe's `success_url` carries. Default `session_id`. */\n param?: string\n}\n\n/**\n * Empties the cart on the page Stripe sends buyers back to, when the address carries a Checkout\n * Session id. It draws nothing. The order itself is handled by the webhook, not by this page.\n */\nexport function ClearCartOnReturn({param = 'session_id'}: ClearCartOnReturnProps) {\n const {ready, clear} = useCart()\n useEffect(() => {\n if (ready && new URLSearchParams(window.location.search).has(param)) clear()\n }, [ready, clear, param])\n return null\n}\n","import {useState} from 'react'\n\nimport {\n lineKey,\n lineStatus,\n readSubscription,\n type CartProduct,\n type CartVariant,\n type Interval,\n type LineStatus,\n} from '../core/cart'\nimport {useCart} from './cart'\n\nexport interface BuyBox {\n /** The product's variants, each with whether it can be bought. Empty for a product without any. */\n variants: Array<CartVariant & {available: boolean}>\n /** The chosen variant: the first one on sale, until the buyer picks another. */\n variantKey: string | undefined\n chooseVariant: (variantKey: string) => void\n /** The subscription the product offers: its price per delivery and how often. Null without one. */\n subscription: {price: number; interval: Interval} | null\n /** Whether the buyer buys once or subscribes. `once` until they choose. */\n plan: 'once' | 'subscribe'\n choosePlan: (plan: 'once' | 'subscribe') => void\n /** Nothing of this product can be bought. */\n soldOut: boolean\n /** Whether the chosen product, variant and plan can be bought, its price and how many. */\n status: LineStatus\n /** How many of the chosen product, variant and plan the cart holds. */\n inCart: number\n /** The cart already holds as many as can be ordered. */\n full: boolean\n /** True after `add`, until the buyer picks another variant or plan. */\n added: boolean\n add: (quantity?: number) => void\n}\n\n/**\n * What a product page's buy button needs: the variants, which one is chosen, buying once or\n * subscribing, whether it can be bought, and adding it to the cart. The markup is yours.\n */\nexport function useBuyBox(product: CartProduct): BuyBox {\n const cart = useCart()\n const max = cart.maxQuantity\n const variants = (product.commerce?.variants ?? []).map((variant) =>\n Object.assign({}, variant, {available: lineStatus(product, variant._key, max).available}),\n )\n const firstAvailable = variants.find((variant) => variant.available)\n const [chosen, setChosen] = useState<string>()\n const [wanted, setWanted] = useState<'once' | 'subscribe'>('once')\n const [added, setAdded] = useState(false)\n\n const subscription = readSubscription(product.commerce?.subscription)\n // A product that stops offering a subscription falls back to buying once.\n const plan = subscription ? wanted : 'once'\n const subscribe: {plan?: 'subscribe'} = plan === 'subscribe' ? {plan} : {}\n const variantKey = chosen ?? firstAvailable?._key\n const status = lineStatus(product, variantKey, max, subscribe.plan)\n const soldOut = variants.length\n ? !firstAvailable\n : !lineStatus(product, variantKey, max).available\n const line = {productId: product._id, ...(variantKey ? {variantKey} : {}), ...subscribe}\n const inCart = cart.lines.find((candidate) => lineKey(candidate) === lineKey(line))?.quantity ?? 0\n\n return {\n variants,\n variantKey,\n chooseVariant(key) {\n setChosen(key)\n setAdded(false)\n },\n subscription,\n plan,\n choosePlan(next) {\n setWanted(next)\n setAdded(false)\n },\n soldOut,\n status,\n inCart,\n full: status.available && inCart >= status.maxQuantity,\n added,\n add(quantity = 1) {\n if (!status.available) return\n cart.add({...line, quantity}, status.maxQuantity)\n setAdded(true)\n },\n }\n}\n","import {useCallback, useEffect, useState} from 'react'\n\nimport {readCheckoutProblems, type CheckoutProblem} from '../core/cart'\nimport {asString, isObject} from '../core/guards'\nimport {useCart} from './cart'\n\nexport type CheckoutState =\n | {status: 'idle'}\n /** Asking the site for a Checkout Session, then on the way to Stripe. */\n | {status: 'loading'}\n | {\n status: 'error'\n /** `region`, `empty`, `unavailable`, `invalid`, or `offline` when the site can't be reached. */\n error: string\n /** For the buyer. */\n message: string\n /** For the developer: only sent by a checkout route with `debug` on. */\n detail?: string\n /** The cart lines the checkout turned down, and why. */\n problems: CheckoutProblem[]\n }\n\nexport interface UseCheckoutOptions {\n /** The site's checkout route. Default `/api/checkout`. */\n endpoint?: string\n messages?: {\n /** No shipping region chosen. */\n region?: string\n /** The site answered without saying why. */\n unavailable?: string\n /** The site couldn't be reached. */\n offline?: string\n }\n}\n\nexport interface Checkout {\n state: CheckoutState\n /** Sends the cart and the chosen region to the checkout route, then goes to Stripe's page. */\n start: (regionId: string | undefined) => Promise<void>\n /** Back to `idle`, for example when the buyer changes the cart after an error. */\n reset: () => void\n}\n\nconst MESSAGES = {\n region: 'Choose where to ship the order.',\n unavailable: \"Checkout isn't available right now. Please try again later.\",\n offline: \"Couldn't reach the shop. Check your connection and try again.\",\n}\n\n/** Starts Stripe Checkout for the cart, and says what went wrong when it can't. */\nexport function useCheckout(options: UseCheckoutOptions = {}): Checkout {\n const {endpoint = '/api/checkout'} = options\n const messages = {...MESSAGES, ...options.messages}\n const {lines} = useCart()\n const [state, setState] = useState<CheckoutState>({status: 'idle'})\n\n // Back from Stripe's page, a browser can restore this one exactly as it was left: still loading.\n useEffect(() => {\n const restore = (event: PageTransitionEvent) => {\n if (event.persisted) setState({status: 'idle'})\n }\n window.addEventListener('pageshow', restore)\n return () => window.removeEventListener('pageshow', restore)\n }, [])\n\n const start = async (regionId: string | undefined) => {\n if (!regionId) {\n setState({status: 'error', error: 'region', message: messages.region, problems: []})\n return\n }\n setState({status: 'loading'})\n try {\n const response = await fetch(endpoint, {\n method: 'POST',\n headers: {'Content-Type': 'application/json'},\n body: JSON.stringify({lines, regionId}),\n })\n const body: unknown = await response.json().catch(() => null)\n const data = isObject(body) ? body : {}\n if (response.ok && typeof data.url === 'string') {\n // Stay in the loading state while the browser goes to Stripe.\n window.location.assign(data.url)\n return\n }\n setState({\n status: 'error',\n error: asString(data.error) ?? 'unavailable',\n message: asString(data.message) ?? messages.unavailable,\n ...(typeof data.detail === 'string' ? {detail: data.detail} : {}),\n problems: readCheckoutProblems(data.problems),\n })\n } catch {\n setState({status: 'error', error: 'offline', message: messages.offline, problems: []})\n }\n }\n\n const reset = useCallback(() => setState({status: 'idle'}), [])\n return {state, start, reset}\n}\n"],"mappings":"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"}
@@ -0,0 +1,157 @@
1
+ /** The cart's types and rules. Used by the Studio, the browser and the server alike. */
2
+ type Availability = 'onSale' | 'soldOut';
3
+ /** How often a subscription is delivered and charged. */
4
+ interface Interval {
5
+ unit: 'week' | 'month';
6
+ /** 6 with `week` means every 6 weeks. */
7
+ every: number;
8
+ }
9
+ /** What the browser keeps in the cart: references only, never prices. */
10
+ interface CartLine {
11
+ /** Sanity document id of the product. */
12
+ productId: string;
13
+ /** `_key` of the chosen variant, when the product has variants. */
14
+ variantKey?: string;
15
+ /** Set when the buyer subscribes. Left out when the product is bought once. */
16
+ plan?: 'subscribe';
17
+ quantity: number;
18
+ }
19
+ /** The subscription a product offers, as the Studio saves it. */
20
+ interface CartSubscription {
21
+ enabled?: boolean | null;
22
+ /** Per delivery, in currency units. */
23
+ price?: number | null;
24
+ every?: number | null;
25
+ unit?: string | null;
26
+ }
27
+ interface CartVariant {
28
+ _key: string;
29
+ label?: string | null;
30
+ availability?: Availability | null;
31
+ }
32
+ /**
33
+ * A product as the cart and the checkout read it from Sanity: see `cartProductProjection`. It
34
+ * says whether the product is on sale, never how many are left: counts are private.
35
+ */
36
+ interface CartProduct {
37
+ _id: string;
38
+ title?: string | null;
39
+ slug?: string | null;
40
+ imageUrl?: string | null;
41
+ commerce?: {
42
+ availability?: Availability | null;
43
+ /** In currency units (25 means €25). */
44
+ price?: number | null;
45
+ taxClass?: string | null;
46
+ variants?: CartVariant[] | null;
47
+ subscription?: CartSubscription | null;
48
+ } | null;
49
+ }
50
+ /** The subscription a product offers, when it is switched on and filled in. Null otherwise. */
51
+ declare function readSubscription(subscription: CartSubscription | null | undefined): {
52
+ price: number;
53
+ interval: Interval;
54
+ } | null;
55
+ /** "every 6 weeks", "every month": how an interval reads in English. */
56
+ declare function describeInterval(interval: Interval): string;
57
+ /**
58
+ * Products as a query with `cartProductProjection` returns them. Anything that isn't a document
59
+ * is dropped; the fields are checked where they are used, in `lineStatus`.
60
+ */
61
+ declare function readCartProducts(value: unknown): CartProduct[];
62
+ /** One cart line per product, variant and way of buying. */
63
+ declare function lineKey(line: Pick<CartLine, 'productId' | 'variantKey' | 'plan'>): string;
64
+ type Unavailable = 'missing' | 'soldOut' | 'noPrice' | 'chooseVariant' | 'variantMissing' |
65
+ /** The line subscribes, and the product doesn't offer a subscription (any more). */
66
+ 'noSubscription';
67
+ type LineStatus = {
68
+ available: true;
69
+ maxQuantity: number;
70
+ /** Once, or per delivery when `interval` is set. */
71
+ price: number;
72
+ variant?: CartVariant;
73
+ /** Set when the line is a subscription. */
74
+ interval?: Interval;
75
+ } | {
76
+ available: false;
77
+ reason: Unavailable;
78
+ };
79
+ /** Why the checkout turned a cart line down. */
80
+ interface CheckoutProblem {
81
+ productId: string;
82
+ variantKey?: string;
83
+ plan?: 'subscribe';
84
+ name?: string;
85
+ /**
86
+ * `otherInterval`: the cart holds subscriptions that repeat at different intervals, and one
87
+ * checkout takes one interval.
88
+ */
89
+ reason: Unavailable | 'tooMany' | 'otherInterval';
90
+ /** For `tooMany`: how many can be ordered. Left out when the shop hides what is left. */
91
+ maxQuantity?: number;
92
+ }
93
+ /** A cart line with everything a cart page shows for it. */
94
+ interface CartRow {
95
+ line: CartLine;
96
+ product: CartProduct | undefined;
97
+ status: LineStatus;
98
+ name: string;
99
+ /** The line's quantity, capped at what can be ordered. 0 when the line can't be bought. */
100
+ quantity: number;
101
+ /** Price times quantity, in currency units. 0 when the line can't be bought. */
102
+ total: number;
103
+ }
104
+ /** Joins the cart's lines with their products, as read from Sanity for the cart page. */
105
+ declare function cartRows(lines: readonly CartLine[], products: readonly CartProduct[], max?: number): CartRow[];
106
+ /** What the cart's lines add up to, in currency units, before shipping. */
107
+ declare function cartSubtotal(rows: readonly CartRow[]): number;
108
+ /** A shipping region with one flat rate per order. */
109
+ interface ShippingRegion {
110
+ id: string;
111
+ title: string;
112
+ /** ISO 3166-1 alpha-2 codes. Empty for the "everywhere else" region until resolved. */
113
+ countries: string[];
114
+ everywhereElse: boolean;
115
+ /** In currency units. */
116
+ rate: number;
117
+ /** Goods adding up to this amount or more are delivered free. In currency units. */
118
+ freeFrom?: number;
119
+ }
120
+ /** How a subscription pays for delivery: nothing extra, or the region's rate with every delivery. */
121
+ type SubscriptionDelivery = 'free' | 'rate';
122
+ /** The shop settings document, as `SHOP_SETTINGS_QUERY` returns it. */
123
+ interface ShopSettings {
124
+ shippingRegions?: Array<{
125
+ _key: string;
126
+ title?: string | null;
127
+ everywhereElse?: boolean | null;
128
+ countries?: string[] | null;
129
+ rate?: number | null;
130
+ freeFrom?: number | null;
131
+ }> | null;
132
+ autoSoldOut?: boolean | null;
133
+ subscriptionDelivery?: SubscriptionDelivery | null;
134
+ }
135
+ /** The shop settings as Sanity returned them, or null when they aren't published yet. */
136
+ declare function readShopSettings(value: unknown): ShopSettings | null;
137
+ /** What a cart page needs of each region: no country lists. */
138
+ type RegionOption = Pick<ShippingRegion, 'id' | 'title' | 'rate' | 'freeFrom'>;
139
+ /** The regions a buyer can choose from, the same ones the checkout accepts. */
140
+ declare function regionOptions(settings: ShopSettings | null | undefined): RegionOption[];
141
+ /** What delivery costs for an order, and how far the order is from free delivery. */
142
+ interface ShippingQuote {
143
+ /** What the region charges for this order, in currency units. 0 when it is free. */
144
+ amount: number;
145
+ /**
146
+ * How much more makes delivery free, in currency units: 0 once it is, null when the region has
147
+ * no "free from" amount.
148
+ */
149
+ missing: number | null;
150
+ }
151
+ /**
152
+ * What a region charges for goods adding up to `subtotal`, for the cart page. The checkout works
153
+ * it out again on the server. It goes by the goods before any promotion code: a code is typed on
154
+ * Stripe's page, after the delivery charge is set.
155
+ */
156
+ declare function shippingQuote(region: Pick<ShippingRegion, 'rate' | 'freeFrom'>, subtotal: number): ShippingQuote;
157
+ export { readSubscription as C, readCartProducts as S, Unavailable as _, SubscriptionDelivery as a, describeInterval as b, shippingQuote as c, CartProduct as d, CartRow as f, LineStatus as g, Interval as h, ShopSettings as i, Availability as l, CheckoutProblem as m, ShippingQuote as n, readShopSettings as o, CartVariant as p, ShippingRegion as r, regionOptions as s, RegionOption as t, CartLine as u, cartRows as v, lineKey as x, cartSubtotal as y };
@@ -0,0 +1,207 @@
1
+ import { a as asString, i as asNumber, n as STRIPE_SHIPPING_COUNTRIES, o as isObject, r as asArray } from "./countries-Csjmv4lY.js";
2
+ /** Stripe charges a subscription at least once every three years. */
3
+ const LONGEST = {
4
+ week: 156,
5
+ month: 36
6
+ };
7
+ /** The subscription a product offers, when it is switched on and filled in. Null otherwise. */
8
+ function readSubscription(subscription) {
9
+ if (!subscription?.enabled) return null;
10
+ let { price, every, unit } = subscription;
11
+ return typeof price != "number" || !Number.isFinite(price) || price < 0 || unit !== "week" && unit !== "month" || typeof every != "number" || !Number.isInteger(every) || every < 1 || every > LONGEST[unit] ? null : {
12
+ price,
13
+ interval: {
14
+ unit,
15
+ every
16
+ }
17
+ };
18
+ }
19
+ /** "every 6 weeks", "every month": how an interval reads in English. */
20
+ function describeInterval(interval) {
21
+ return interval.every === 1 ? `every ${interval.unit}` : `every ${interval.every} ${interval.unit}s`;
22
+ }
23
+ /**
24
+ * Products as a query with `cartProductProjection` returns them. Anything that isn't a document
25
+ * is dropped; the fields are checked where they are used, in `lineStatus`.
26
+ */
27
+ function readCartProducts(value) {
28
+ return asArray(value).filter((item) => isObject(item) && typeof item._id == "string");
29
+ }
30
+ /** One cart line per product, variant and way of buying. */
31
+ function lineKey(line) {
32
+ return `${line.productId}:${line.variantKey ?? ""}${line.plan ? `:${line.plan}` : ""}`;
33
+ }
34
+ /**
35
+ * Turns anything (stored JSON, a request body) into valid cart lines: drops what doesn't parse,
36
+ * merges duplicates and keeps quantities between 1 and `max`.
37
+ *
38
+ * A product id with a dot is dropped: a published document's id has none, so a draft or a copy
39
+ * inside a release can never be put in a cart, whatever the site's Sanity client is able to read.
40
+ */
41
+ function sanitizeLines(input, max = 10) {
42
+ let merged = /* @__PURE__ */ new Map();
43
+ for (let item of asArray(input).slice(0, 100)) {
44
+ if (!isObject(item)) continue;
45
+ let { productId, variantKey, plan, quantity } = item;
46
+ if (typeof productId != "string" || !productId || productId.length > 200 || productId.includes(".") || variantKey !== void 0 && (typeof variantKey != "string" || variantKey.length > 200) || typeof quantity != "number" || !Number.isFinite(quantity)) continue;
47
+ let line = {
48
+ productId,
49
+ quantity: 0,
50
+ ...variantKey ? { variantKey } : {},
51
+ ...plan === "subscribe" ? { plan } : {}
52
+ }, key = lineKey(line), total = (merged.get(key)?.quantity ?? 0) + Math.floor(quantity);
53
+ total < 1 || merged.set(key, {
54
+ ...line,
55
+ quantity: Math.min(total, max)
56
+ });
57
+ }
58
+ return [...merged.values()];
59
+ }
60
+ /**
61
+ * Whether a product (and variant) is on sale, as far as anyone can see. Stock counts are private,
62
+ * so they are checked by the checkout route, on the server.
63
+ */
64
+ function lineStatus(product, variantKey, max = 10, plan) {
65
+ let commerce = product?.commerce;
66
+ if (!product || !commerce) return {
67
+ available: !1,
68
+ reason: "missing"
69
+ };
70
+ if (commerce.availability === "soldOut") return {
71
+ available: !1,
72
+ reason: "soldOut"
73
+ };
74
+ let subscription = plan === "subscribe" ? readSubscription(commerce.subscription) : null;
75
+ if (plan === "subscribe" && !subscription) return {
76
+ available: !1,
77
+ reason: "noSubscription"
78
+ };
79
+ let price = subscription ? subscription.price : commerce.price;
80
+ if (typeof price != "number") return {
81
+ available: !1,
82
+ reason: "noPrice"
83
+ };
84
+ let variants = commerce.variants ?? [], variant;
85
+ if (variants.length) {
86
+ if (!variantKey) return {
87
+ available: !1,
88
+ reason: "chooseVariant"
89
+ };
90
+ if (variant = variants.find((candidate) => candidate._key === variantKey), !variant) return {
91
+ available: !1,
92
+ reason: "variantMissing"
93
+ };
94
+ if (variant.availability === "soldOut") return {
95
+ available: !1,
96
+ reason: "soldOut"
97
+ };
98
+ } else if (variantKey) return {
99
+ available: !1,
100
+ reason: "variantMissing"
101
+ };
102
+ return {
103
+ available: !0,
104
+ maxQuantity: max,
105
+ price,
106
+ variant,
107
+ ...subscription ? { interval: subscription.interval } : {}
108
+ };
109
+ }
110
+ /** Display name for a line: the product title, plus the variant label when there is one. */
111
+ function lineName(product, variant) {
112
+ let title = product.title ?? "Product";
113
+ return variant?.label ? `${title} – ${variant.label}` : title;
114
+ }
115
+ /** The problems in a checkout route's reply, as the browser reads them. */
116
+ function readCheckoutProblems(value) {
117
+ return asArray(value).filter((item) => isObject(item) && typeof item.productId == "string" && typeof item.reason == "string");
118
+ }
119
+ /** Joins the cart's lines with their products, as read from Sanity for the cart page. */
120
+ function cartRows(lines, products, max = 10) {
121
+ let byId = new Map(products.map((product) => [product._id, product]));
122
+ return lines.map((line) => {
123
+ let product = byId.get(line.productId), status = lineStatus(product, line.variantKey, max, line.plan), name = product ? lineName(product, status.available ? status.variant : void 0) : "Item", quantity = status.available ? Math.min(line.quantity, status.maxQuantity) : 0;
124
+ return {
125
+ line,
126
+ product,
127
+ status,
128
+ name,
129
+ quantity,
130
+ total: status.available ? status.price * quantity : 0
131
+ };
132
+ });
133
+ }
134
+ /** What the cart's lines add up to, in currency units, before shipping. */
135
+ function cartSubtotal(rows) {
136
+ return rows.reduce((sum, row) => sum + row.total, 0);
137
+ }
138
+ /** The shop settings as Sanity returned them, or null when they aren't published yet. */
139
+ function readShopSettings(value) {
140
+ return isObject(value) ? {
141
+ shippingRegions: asArray(value.shippingRegions).flatMap((region) => isObject(region) && typeof region._key == "string" ? [{
142
+ _key: region._key,
143
+ title: asString(region.title),
144
+ everywhereElse: region.everywhereElse === !0,
145
+ countries: asArray(region.countries).filter((country) => typeof country == "string"),
146
+ rate: asNumber(region.rate),
147
+ freeFrom: asNumber(region.freeFrom)
148
+ }] : []),
149
+ autoSoldOut: typeof value.autoSoldOut == "boolean" ? value.autoSoldOut : null,
150
+ subscriptionDelivery: value.subscriptionDelivery === "rate" || value.subscriptionDelivery === "free" ? value.subscriptionDelivery : null
151
+ } : null;
152
+ }
153
+ /** Shipping regions from the shop settings, leaving out any that aren't filled in. */
154
+ function regionsFromSettings(settings) {
155
+ return (settings?.shippingRegions ?? []).flatMap((region) => region.title && typeof region.rate == "number" ? [{
156
+ id: region._key,
157
+ title: region.title,
158
+ countries: region.countries ?? [],
159
+ everywhereElse: !!region.everywhereElse,
160
+ rate: region.rate,
161
+ ...typeof region.freeFrom == "number" && region.freeFrom > 0 ? { freeFrom: region.freeFrom } : {}
162
+ }] : []);
163
+ }
164
+ /**
165
+ * Fills in the "everywhere else" region with every country Stripe ships to that no other region
166
+ * lists, and drops country codes Stripe doesn't accept. Regions left without countries are removed.
167
+ */
168
+ function resolveRegions(regions) {
169
+ let supported = new Set(STRIPE_SHIPPING_COUNTRIES), listed = new Set(regions.filter((region) => !region.everywhereElse).flatMap((region) => region.countries));
170
+ return regions.map((region) => ({
171
+ ...region,
172
+ countries: region.everywhereElse ? STRIPE_SHIPPING_COUNTRIES.filter((country) => !listed.has(country)) : region.countries.filter((country) => supported.has(country))
173
+ })).filter((region) => region.countries.length > 0);
174
+ }
175
+ /** The regions a buyer can choose from, the same ones the checkout accepts. */
176
+ function regionOptions(settings) {
177
+ return resolveRegions(regionsFromSettings(settings)).map(({ id, title, rate, freeFrom }) => {
178
+ let option = {
179
+ id,
180
+ title,
181
+ rate
182
+ };
183
+ return freeFrom !== void 0 && (option.freeFrom = freeFrom), option;
184
+ });
185
+ }
186
+ /**
187
+ * What a region charges for goods adding up to `subtotal`, for the cart page. The checkout works
188
+ * it out again on the server. It goes by the goods before any promotion code: a code is typed on
189
+ * Stripe's page, after the delivery charge is set.
190
+ */
191
+ function shippingQuote(region, subtotal) {
192
+ if (region.freeFrom === void 0) return {
193
+ amount: region.rate,
194
+ missing: null
195
+ };
196
+ let missing = Math.round((region.freeFrom - subtotal) * 1e4) / 1e4;
197
+ return missing <= 0 ? {
198
+ amount: 0,
199
+ missing: 0
200
+ } : {
201
+ amount: region.rate,
202
+ missing
203
+ };
204
+ }
205
+ export { shippingQuote as a, describeInterval as c, lineStatus as d, readCartProducts as f, sanitizeLines as h, resolveRegions as i, lineKey as l, readSubscription as m, regionOptions as n, cartRows as o, readCheckoutProblems as p, regionsFromSettings as r, cartSubtotal as s, readShopSettings as t, lineName as u };
206
+
207
+ //# sourceMappingURL=regions-CFDYMokP.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"regions-CFDYMokP.js","names":[],"sources":["../src/core/cart.ts","../src/core/regions.ts"],"sourcesContent":["import {asArray, isObject} from './guards'\n\n/** The cart's types and rules. Used by the Studio, the browser and the server alike. */\n\nexport type Availability = 'onSale' | 'soldOut'\n\n/** How often a subscription is delivered and charged. */\nexport interface Interval {\n unit: 'week' | 'month'\n /** 6 with `week` means every 6 weeks. */\n every: number\n}\n\n/** What the browser keeps in the cart: references only, never prices. */\nexport interface CartLine {\n /** Sanity document id of the product. */\n productId: string\n /** `_key` of the chosen variant, when the product has variants. */\n variantKey?: string\n /** Set when the buyer subscribes. Left out when the product is bought once. */\n plan?: 'subscribe'\n quantity: number\n}\n\n/** The subscription a product offers, as the Studio saves it. */\nexport interface CartSubscription {\n enabled?: boolean | null\n /** Per delivery, in currency units. */\n price?: number | null\n every?: number | null\n unit?: string | null\n}\n\nexport interface CartVariant {\n _key: string\n label?: string | null\n availability?: Availability | null\n}\n\n/**\n * A product as the cart and the checkout read it from Sanity: see `cartProductProjection`. It\n * says whether the product is on sale, never how many are left: counts are private.\n */\nexport interface CartProduct {\n _id: string\n title?: string | null\n slug?: string | null\n imageUrl?: string | null\n commerce?: {\n availability?: Availability | null\n /** In currency units (25 means €25). */\n price?: number | null\n taxClass?: string | null\n variants?: CartVariant[] | null\n subscription?: CartSubscription | null\n } | null\n}\n\n/** Stripe charges a subscription at least once every three years. */\nconst LONGEST: Record<Interval['unit'], number> = {week: 156, month: 36}\n\n/** The subscription a product offers, when it is switched on and filled in. Null otherwise. */\nexport function readSubscription(\n subscription: CartSubscription | null | undefined,\n): {price: number; interval: Interval} | null {\n if (!subscription?.enabled) return null\n const {price, every, unit} = subscription\n if (typeof price !== 'number' || !Number.isFinite(price) || price < 0) return null\n if (unit !== 'week' && unit !== 'month') return null\n if (typeof every !== 'number' || !Number.isInteger(every) || every < 1 || every > LONGEST[unit])\n return null\n return {price, interval: {unit, every}}\n}\n\n/** \"every 6 weeks\", \"every month\": how an interval reads in English. */\nexport function describeInterval(interval: Interval): string {\n return interval.every === 1\n ? `every ${interval.unit}`\n : `every ${interval.every} ${interval.unit}s`\n}\n\n/**\n * Products as a query with `cartProductProjection` returns them. Anything that isn't a document\n * is dropped; the fields are checked where they are used, in `lineStatus`.\n */\nexport function readCartProducts(value: unknown): CartProduct[] {\n return asArray(value).filter(\n (item): item is CartProduct => isObject(item) && typeof item._id === 'string',\n )\n}\n\n/** Most of one item a buyer can order at once. */\nexport const MAX_QUANTITY = 10\n\n/** One cart line per product, variant and way of buying. */\nexport function lineKey(line: Pick<CartLine, 'productId' | 'variantKey' | 'plan'>): string {\n return `${line.productId}:${line.variantKey ?? ''}${line.plan ? `:${line.plan}` : ''}`\n}\n\n/**\n * Turns anything (stored JSON, a request body) into valid cart lines: drops what doesn't parse,\n * merges duplicates and keeps quantities between 1 and `max`.\n *\n * A product id with a dot is dropped: a published document's id has none, so a draft or a copy\n * inside a release can never be put in a cart, whatever the site's Sanity client is able to read.\n */\nexport function sanitizeLines(input: unknown, max = MAX_QUANTITY): CartLine[] {\n const merged = new Map<string, CartLine>()\n for (const item of asArray(input).slice(0, 100)) {\n if (!isObject(item)) continue\n const {productId, variantKey, plan, quantity} = item\n if (typeof productId !== 'string' || !productId || productId.length > 200) continue\n if (productId.includes('.')) continue\n if (variantKey !== undefined && (typeof variantKey !== 'string' || variantKey.length > 200))\n continue\n if (typeof quantity !== 'number' || !Number.isFinite(quantity)) continue\n const line: CartLine = {\n productId,\n quantity: 0,\n ...(variantKey ? {variantKey} : {}),\n ...(plan === 'subscribe' ? {plan} : {}),\n }\n const key = lineKey(line)\n const existing = merged.get(key)\n const total = (existing?.quantity ?? 0) + Math.floor(quantity)\n if (total < 1) continue\n merged.set(key, {...line, quantity: Math.min(total, max)})\n }\n return [...merged.values()]\n}\n\nexport type Unavailable =\n | 'missing'\n | 'soldOut'\n | 'noPrice'\n | 'chooseVariant'\n | 'variantMissing'\n /** The line subscribes, and the product doesn't offer a subscription (any more). */\n | 'noSubscription'\n\nexport type LineStatus =\n | {\n available: true\n maxQuantity: number\n /** Once, or per delivery when `interval` is set. */\n price: number\n variant?: CartVariant\n /** Set when the line is a subscription. */\n interval?: Interval\n }\n | {available: false; reason: Unavailable}\n\n/**\n * Whether a product (and variant) is on sale, as far as anyone can see. Stock counts are private,\n * so they are checked by the checkout route, on the server.\n */\nexport function lineStatus(\n product: CartProduct | undefined,\n variantKey?: string,\n max = MAX_QUANTITY,\n plan?: CartLine['plan'],\n): LineStatus {\n const commerce = product?.commerce\n if (!product || !commerce) return {available: false, reason: 'missing'}\n if (commerce.availability === 'soldOut') return {available: false, reason: 'soldOut'}\n const subscription = plan === 'subscribe' ? readSubscription(commerce.subscription) : null\n if (plan === 'subscribe' && !subscription) return {available: false, reason: 'noSubscription'}\n const price = subscription ? subscription.price : commerce.price\n if (typeof price !== 'number') return {available: false, reason: 'noPrice'}\n\n const variants = commerce.variants ?? []\n let variant: CartVariant | undefined\n if (variants.length) {\n if (!variantKey) return {available: false, reason: 'chooseVariant'}\n variant = variants.find((candidate) => candidate._key === variantKey)\n if (!variant) return {available: false, reason: 'variantMissing'}\n if (variant.availability === 'soldOut') return {available: false, reason: 'soldOut'}\n } else if (variantKey) {\n return {available: false, reason: 'variantMissing'}\n }\n\n return {\n available: true,\n maxQuantity: max,\n price,\n variant,\n ...(subscription ? {interval: subscription.interval} : {}),\n }\n}\n\n/** Display name for a line: the product title, plus the variant label when there is one. */\nexport function lineName(\n product: Pick<CartProduct, 'title'>,\n variant?: Pick<CartVariant, 'label'>,\n): string {\n const title = product.title ?? 'Product'\n return variant?.label ? `${title} – ${variant.label}` : title\n}\n\n/** Why the checkout turned a cart line down. */\nexport interface CheckoutProblem {\n productId: string\n variantKey?: string\n plan?: 'subscribe'\n name?: string\n /**\n * `otherInterval`: the cart holds subscriptions that repeat at different intervals, and one\n * checkout takes one interval.\n */\n reason: Unavailable | 'tooMany' | 'otherInterval'\n /** For `tooMany`: how many can be ordered. Left out when the shop hides what is left. */\n maxQuantity?: number\n}\n\n/** The problems in a checkout route's reply, as the browser reads them. */\nexport function readCheckoutProblems(value: unknown): CheckoutProblem[] {\n return asArray(value).filter(\n (item): item is CheckoutProblem =>\n isObject(item) && typeof item.productId === 'string' && typeof item.reason === 'string',\n )\n}\n\n/** A cart line with everything a cart page shows for it. */\nexport interface CartRow {\n line: CartLine\n product: CartProduct | undefined\n status: LineStatus\n name: string\n /** The line's quantity, capped at what can be ordered. 0 when the line can't be bought. */\n quantity: number\n /** Price times quantity, in currency units. 0 when the line can't be bought. */\n total: number\n}\n\n/** Joins the cart's lines with their products, as read from Sanity for the cart page. */\nexport function cartRows(\n lines: readonly CartLine[],\n products: readonly CartProduct[],\n max = MAX_QUANTITY,\n): CartRow[] {\n const byId = new Map(products.map((product) => [product._id, product]))\n return lines.map((line) => {\n const product = byId.get(line.productId)\n const status = lineStatus(product, line.variantKey, max, line.plan)\n const name = product ? lineName(product, status.available ? status.variant : undefined) : 'Item'\n const quantity = status.available ? Math.min(line.quantity, status.maxQuantity) : 0\n const total = status.available ? status.price * quantity : 0\n return {line, product, status, name, quantity, total}\n })\n}\n\n/** What the cart's lines add up to, in currency units, before shipping. */\nexport function cartSubtotal(rows: readonly CartRow[]): number {\n return rows.reduce((sum, row) => sum + row.total, 0)\n}\n","import {STRIPE_SHIPPING_COUNTRIES} from './countries'\nimport {asArray, asNumber, asString, isObject} from './guards'\n\n/** A shipping region with one flat rate per order. */\nexport interface ShippingRegion {\n id: string\n title: string\n /** ISO 3166-1 alpha-2 codes. Empty for the \"everywhere else\" region until resolved. */\n countries: string[]\n everywhereElse: boolean\n /** In currency units. */\n rate: number\n /** Goods adding up to this amount or more are delivered free. In currency units. */\n freeFrom?: number\n}\n\n/** How a subscription pays for delivery: nothing extra, or the region's rate with every delivery. */\nexport type SubscriptionDelivery = 'free' | 'rate'\n\n/** The shop settings document, as `SHOP_SETTINGS_QUERY` returns it. */\nexport interface ShopSettings {\n shippingRegions?: Array<{\n _key: string\n title?: string | null\n everywhereElse?: boolean | null\n countries?: string[] | null\n rate?: number | null\n freeFrom?: number | null\n }> | null\n autoSoldOut?: boolean | null\n subscriptionDelivery?: SubscriptionDelivery | null\n}\n\n/** The shop settings as Sanity returned them, or null when they aren't published yet. */\nexport function readShopSettings(value: unknown): ShopSettings | null {\n if (!isObject(value)) return null\n return {\n shippingRegions: asArray(value.shippingRegions).flatMap((region) =>\n isObject(region) && typeof region._key === 'string'\n ? [\n {\n _key: region._key,\n title: asString(region.title),\n everywhereElse: region.everywhereElse === true,\n countries: asArray(region.countries).filter(\n (country): country is string => typeof country === 'string',\n ),\n rate: asNumber(region.rate),\n freeFrom: asNumber(region.freeFrom),\n },\n ]\n : [],\n ),\n autoSoldOut: typeof value.autoSoldOut === 'boolean' ? value.autoSoldOut : null,\n subscriptionDelivery:\n value.subscriptionDelivery === 'rate' || value.subscriptionDelivery === 'free'\n ? value.subscriptionDelivery\n : null,\n }\n}\n\n/** Shipping regions from the shop settings, leaving out any that aren't filled in. */\nexport function regionsFromSettings(settings: ShopSettings | null | undefined): ShippingRegion[] {\n return (settings?.shippingRegions ?? []).flatMap((region) =>\n region.title && typeof region.rate === 'number'\n ? [\n {\n id: region._key,\n title: region.title,\n countries: region.countries ?? [],\n everywhereElse: Boolean(region.everywhereElse),\n rate: region.rate,\n ...(typeof region.freeFrom === 'number' && region.freeFrom > 0\n ? {freeFrom: region.freeFrom}\n : {}),\n },\n ]\n : [],\n )\n}\n\n/**\n * Fills in the \"everywhere else\" region with every country Stripe ships to that no other region\n * lists, and drops country codes Stripe doesn't accept. Regions left without countries are removed.\n */\nexport function resolveRegions(regions: readonly ShippingRegion[]): ShippingRegion[] {\n const supported = new Set<string>(STRIPE_SHIPPING_COUNTRIES)\n const listed = new Set(\n regions.filter((region) => !region.everywhereElse).flatMap((region) => region.countries),\n )\n return regions\n .map((region) => ({\n ...region,\n countries: region.everywhereElse\n ? STRIPE_SHIPPING_COUNTRIES.filter((country) => !listed.has(country))\n : region.countries.filter((country) => supported.has(country)),\n }))\n .filter((region) => region.countries.length > 0)\n}\n\n/** What a cart page needs of each region: no country lists. */\nexport type RegionOption = Pick<ShippingRegion, 'id' | 'title' | 'rate' | 'freeFrom'>\n\n/** The regions a buyer can choose from, the same ones the checkout accepts. */\nexport function regionOptions(settings: ShopSettings | null | undefined): RegionOption[] {\n return resolveRegions(regionsFromSettings(settings)).map(({id, title, rate, freeFrom}) => {\n const option: RegionOption = {id, title, rate}\n if (freeFrom !== undefined) option.freeFrom = freeFrom\n return option\n })\n}\n\n/** What delivery costs for an order, and how far the order is from free delivery. */\nexport interface ShippingQuote {\n /** What the region charges for this order, in currency units. 0 when it is free. */\n amount: number\n /**\n * How much more makes delivery free, in currency units: 0 once it is, null when the region has\n * no \"free from\" amount.\n */\n missing: number | null\n}\n\n/**\n * What a region charges for goods adding up to `subtotal`, for the cart page. The checkout works\n * it out again on the server. It goes by the goods before any promotion code: a code is typed on\n * Stripe's page, after the delivery charge is set.\n */\nexport function shippingQuote(\n region: Pick<ShippingRegion, 'rate' | 'freeFrom'>,\n subtotal: number,\n): ShippingQuote {\n if (region.freeFrom === undefined) return {amount: region.rate, missing: null}\n // Amounts are typed with a few decimals; a sum of them can land a hair under the amount.\n const missing = Math.round((region.freeFrom - subtotal) * 10000) / 10000\n return missing <= 0 ? {amount: 0, missing: 0} : {amount: region.rate, missing}\n}\n"],"mappings":"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"}