@base44/app-plugin-commerce 0.2.6 → 0.3.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.
Files changed (36) hide show
  1. package/README.md +4 -4
  2. package/package.json +2 -2
  3. package/scripts/install.js +15 -0
  4. package/skills/commerce/SKILL.md +31 -13
  5. package/skills/commerce/docs/api-admin.md +1 -1
  6. package/skills/commerce/docs/api-storefront.md +12 -12
  7. package/skills/commerce/install/01-install.md +5 -2
  8. package/skills/commerce/install/02-storefront.md +179 -220
  9. package/skills/commerce/install/03-data.md +1 -1
  10. package/skills/commerce/references/catalog-rendering.md +37 -43
  11. package/skills/commerce/references/reviews.md +21 -14
  12. package/skills/commerce/references/store-settings.md +1 -1
  13. package/skills/commerce/references/storefront-verification.md +21 -15
  14. package/src/commerce/storefront/StorefrontProvider.jsx +65 -128
  15. package/src/commerce/storefront/cartUI.jsx +26 -117
  16. package/src/commerce/storefront/index.js +61 -98
  17. package/src/commerce/storefront/pickers.jsx +53 -81
  18. package/src/commerce/storefront/useCartLine.js +23 -130
  19. package/src/commerce/storefront/useCheckout.jsx +50 -43
  20. package/src/commerce/storefront/useOrderReturn.js +17 -7
  21. package/src/commerce/storefront/useProduct.js +54 -119
  22. package/src/commerce/storefront/useProductList.js +15 -28
  23. package/src/commerce/utils/address-spec.js +1 -1
  24. package/src/commerce/utils/images.js +1 -1
  25. package/src/commerce/utils/index.js +9 -9
  26. package/src/commerce/utils/price.js +2 -1
  27. package/src/commerce/utils/specs.js +41 -91
  28. package/src/commerce/utils/totals.js +7 -4
  29. package/src/commerce/storefront/useAddressForm.js +0 -175
  30. package/src/commerce/storefront/usePlaceOrder.js +0 -55
  31. package/src/commerce/storefront/useProductGallery.js +0 -78
  32. package/src/commerce/storefront/useProductPrice.js +0 -58
  33. package/src/commerce/storefront/useProductReviews.js +0 -242
  34. package/src/commerce/storefront/useStorefrontSeo.js +0 -204
  35. package/src/commerce/storefront/useTotalsLines.js +0 -109
  36. package/src/commerce/storefront/useUpsell.js +0 -90
@@ -1,59 +1,28 @@
1
- import React, {
2
- createContext,
3
- useCallback,
4
- useContext,
5
- useEffect,
6
- useId,
7
- useMemo,
8
- useRef,
9
- useState,
10
- } from "react";
1
+ import React, { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
11
2
  import { useLocation } from "react-router-dom";
12
- import { useCart } from "./StorefrontProvider";
13
3
 
14
4
  /**
15
- * Cart drawer/panel state, headless. Every store with a cart drawer needs the
16
- * same non-visual machinery, and it is where hand-written drawers reliably go
17
- * wrong: the drawer must close on route change, be **inert** when closed (not
18
- * merely invisible — hidden-but-mounted buttons stay clickable and tab-able),
19
- * carry dialog semantics, close on Esc, and return focus to its trigger.
20
- * `useCartUI` owns all of it; you own every pixel.
5
+ * Cart drawer/panel state, headless: `open` plus the handlers to change it,
6
+ * and the two behaviors every drawer needs but a hand-written one forgets —
7
+ * it closes when the route changes (`closeOnNavigate`) and it opens when an
8
+ * item lands in the cart (`openOnAdd`, wired through `useAddToCart`; pass false
9
+ * for a navigate-to-bag flow). Esc closes it. You own every element, class and
10
+ * attribute.
21
11
  *
22
12
  * Mount `<CartUIProvider>` once, inside `<StorefrontProvider>`, around the
23
- * layout. Then the layout is three spreads:
13
+ * layout; the layout then renders the panel off `open`.
24
14
  *
25
- * function StoreLayout() {
26
- * const ui = useCartUI();
27
- * return (<>
28
- * <header>… <button {...ui.triggerProps} className="…">Bag ({itemCount})</button></header>
29
- * <Outlet />
30
- * <div {...ui.overlayProps} className={ui.open ? "…" : "…"} />
31
- * <aside {...ui.panelProps} className={ui.open ? "…translate-x-0" : "…translate-x-full"}>
32
- * <button {...ui.closeButtonProps} className="…">×</button>
33
- * {…your cart rows: useCart + CartLine…}
34
- * </aside>
35
- * </>);
36
- * }
15
+ * ⚑ **Render the drawer conditionally — `{ui.open && …}`.** The classic drawer
16
+ * bug is a panel translated off-screen but still mounted: its buttons stay
17
+ * clickable, tab-able and visible to screen readers. Unmounting when closed is
18
+ * the trivial fix; if you keep it mounted to animate the slide, that concern is
19
+ * yours again — set the `inert` attribute while closed. The overlay is a
20
+ * click-away surface (`aria-hidden`, no tab stop) — it is not the close control;
21
+ * a named close button inside the panel is.
37
22
  *
38
- * What each prop set carries:
39
- * - `triggerProps` — onClick (toggle), `aria-expanded`/`aria-controls`/
40
- * `aria-haspopup`, an item-count `aria-label`, and the ref used to restore
41
- * focus on close. Spread your own `aria-label` after it to override.
42
- * - `panelProps` — `role="dialog"`, `aria-modal`, `aria-label`, `data-state`
43
- * (`"open"|"closed"` — style/animate off it), focus target on open, and the
44
- * **inert** DOM property while closed, so a translated-off-screen drawer's
45
- * controls genuinely stop existing for clicks, Tab and screen readers. Keep
46
- * the panel mounted (animate with classes); don't also conditionally unmount.
47
- * - `overlayProps` — click-to-close, `aria-hidden`, no tab stop. The overlay is
48
- * not a second "close" control; the named close button is `closeButtonProps`.
49
- * - Esc closes; route changes close (`closeOnNavigate`); a successful
50
- * add-to-cart opens (`openOnAdd`, via `useAddToCartButton` — pass false for a
51
- * navigate-to-bag flow instead).
52
- *
53
- * `useCartUI()` → `{ open, openCart, closeCart, toggleCart, triggerProps,
54
- * overlayProps, panelProps, closeButtonProps }`. `useCartUIOptional()` returns
55
- * null instead of throwing (how `useAddToCartButton` integrates without
56
- * requiring the provider).
23
+ * `useCartUI()` → `{ open, openCart, closeCart, toggleCart }`.
24
+ * `useCartUIOptional()` returns null instead of throwing (how `useAddToCart`
25
+ * integrates without requiring the provider).
57
26
  *
58
27
  * A store whose cart is a page, not a drawer, skips this provider entirely.
59
28
  */
@@ -75,32 +44,14 @@ function CloseOnNavigate({ close }) {
75
44
  }
76
45
 
77
46
  /**
78
- * @param {{closeOnNavigate?: boolean, openOnAdd?: boolean, label?: string,
79
- * children: React.ReactNode}} props `label` names the dialog for assistive
80
- * tech and the default trigger/close labels (default "Cart").
47
+ * @param {{closeOnNavigate?: boolean, openOnAdd?: boolean,
48
+ * children: React.ReactNode}} props
81
49
  */
82
- export function CartUIProvider({ closeOnNavigate = true, openOnAdd = true, label = "Cart", children }) {
50
+ export function CartUIProvider({ closeOnNavigate = true, openOnAdd = true, children }) {
83
51
  const [open, setOpen] = useState(false);
84
52
  const openCart = useCallback(() => setOpen(true), []);
85
53
  const closeCart = useCallback(() => setOpen(false), []);
86
54
  const toggleCart = useCallback(() => setOpen((o) => !o), []);
87
- const { itemCount } = useCart();
88
-
89
- const panelId = useId();
90
- const panelRef = useRef(null);
91
- const triggerRef = useRef(null);
92
- const openRef = useRef(open);
93
- openRef.current = open;
94
-
95
- // `inert` is set as a DOM property (not a JSX attribute): React 18 has no
96
- // boolean `inert` support, and the property form works on 18 and 19 alike.
97
- const setPanelRef = useCallback((node) => {
98
- panelRef.current = node;
99
- if (node) node.inert = !openRef.current;
100
- }, []);
101
- useEffect(() => {
102
- if (panelRef.current) panelRef.current.inert = !open;
103
- }, [open]);
104
55
 
105
56
  // Esc closes — the keyboard's way out is Esc and the named close button.
106
57
  useEffect(() => {
@@ -112,58 +63,16 @@ export function CartUIProvider({ closeOnNavigate = true, openOnAdd = true, label
112
63
  return () => window.removeEventListener("keydown", onKey);
113
64
  }, [open]);
114
65
 
115
- // Focus follows the dialog: into the panel on open, back to the trigger on
116
- // close — without it, focus is stranded inside an inert subtree.
117
- const wasOpen = useRef(false);
118
- useEffect(() => {
119
- if (open) {
120
- wasOpen.current = true;
121
- panelRef.current?.focus?.();
122
- } else if (wasOpen.current) {
123
- wasOpen.current = false;
124
- triggerRef.current?.focus?.();
125
- }
126
- }, [open]);
127
-
128
- const value = useMemo(() => {
129
- const state = open ? "open" : "closed";
130
- return {
66
+ const value = useMemo(
67
+ () => ({
131
68
  open,
132
69
  openCart,
133
70
  closeCart,
134
71
  toggleCart,
135
72
  onItemAdded: openOnAdd ? openCart : null,
136
- triggerProps: {
137
- type: "button",
138
- ref: triggerRef,
139
- onClick: toggleCart,
140
- "aria-haspopup": "dialog",
141
- "aria-expanded": open,
142
- "aria-controls": panelId,
143
- "aria-label": `${label}, ${itemCount} ${itemCount === 1 ? "item" : "items"}`,
144
- },
145
- overlayProps: {
146
- onClick: closeCart,
147
- "aria-hidden": true,
148
- tabIndex: -1,
149
- "data-state": state,
150
- },
151
- panelProps: {
152
- ref: setPanelRef,
153
- id: panelId,
154
- role: "dialog",
155
- "aria-modal": true,
156
- "aria-label": label,
157
- tabIndex: -1,
158
- "data-state": state,
159
- },
160
- closeButtonProps: {
161
- type: "button",
162
- onClick: closeCart,
163
- "aria-label": `Close ${label.toLowerCase()}`,
164
- },
165
- };
166
- }, [open, openCart, closeCart, toggleCart, openOnAdd, panelId, setPanelRef, label, itemCount]);
73
+ }),
74
+ [open, openCart, closeCart, toggleCart, openOnAdd],
75
+ );
167
76
 
168
77
  return (
169
78
  <CartUIContext.Provider value={value}>
@@ -1,97 +1,56 @@
1
1
  /**
2
2
  * Storefront React layer — **headless**: hooks and render-prop components that
3
- * own the store's logic and hand you the data; they render nothing and carry
4
- * no styling. Every element, class and word of copy in the storefront is
5
- * written by you, against these APIs. Ships with the Base44 Commerce Template
6
- * next to the framework-free `@/commerce/utils` (which it builds on); needs
7
- * React and nothing else.
3
+ * own the store's logic and hand you the data. They render nothing, carry no
4
+ * styling, and ship no customer-facing copy — every element, class and word in
5
+ * the storefront is written by you, against these APIs. Ships with the Base44
6
+ * Commerce Plugin next to the framework-free `@/commerce/utils` (which it
7
+ * builds on); needs React and nothing else.
8
8
  *
9
9
  * The split: **logic is premade, UI never is.** Checkout repricing, variant
10
- * resolution, cart state, review policies, order-return verification — done
11
- * here, and hand-rolling any of it is where storefront bugs cluster. What a
12
- * checkout or a product page *looks like* is the store's identity, and no two
13
- * stores should share it — so nothing here emits markup. Each hook returns a
14
- * complete view-model (statuses to branch on, ready-to-map arrays, handlers,
15
- * error objects), and each doc comment states the render rules that keep the
16
- * store correct (e.g. an unbuyable variant option renders *disabled, not
17
- * hidden*; a receipt page must render `paymentInstructions`).
10
+ * resolution, cart state, order-return verification — done here, and
11
+ * hand-rolling any of it is where storefront bugs cluster. What a checkout or a
12
+ * product page *looks like* is the store's identity, and no two stores should
13
+ * share it — so nothing here emits markup, and every state arrives as a **code**
14
+ * (`state`, `status`, `hint.code`, `blockers`) that you write the words for.
18
15
  *
19
- * Setup (once, above every storefront route — on a pathless layout route,
20
- * wrapping the layout that renders <Outlet/>; as a child of <Routes> React
21
- * Router throws "is not a <Route> component"):
16
+ * `StorefrontProvider` mounts once, above every storefront route (see its own
17
+ * doc comment — getting the mounting wrong is the one setup error worth
18
+ * knowing). Each hook's doc comment is its contract; the map:
22
19
  *
23
- * import { StorefrontProvider } from "@/commerce/storefront";
24
- * import { base44 } from "@/api/base44Client";
25
- * <Route element={<StorefrontProvider base44={base44}><StoreLayout /></StorefrontProvider>}>
26
- * <Route path="/" element={<Home />} /> …
27
- * </Route>
28
- * // no shared layout? <StorefrontProvider …> <Routes>…</Routes> </StorefrontProvider>
29
- *
30
- * Most hooks hand back **ready-to-spread prop sets** next to the raw data —
31
- * `f.inputProps`, `m.radioProps`, `buy.buttonProps`, `ui.panelProps`,
32
- * `list.moreProps` — so your markup is elements + classes and the wiring
33
- * (handlers, ids, aria, disabled logic) can't be re-derived wrong. Spread
34
- * first, put your `className` after.
35
- *
36
- * ## Hooks
37
- * - `useStorefront` / `useStoreInfo` / `useFormatMoney` / `useMoney` — the
20
+ * - `useStorefront` / `useStoreInfo` / `useFormatMoney` / `useCountries` — the
38
21
  * shared client, cached store info (the ONLY source of payment gateways,
39
- * currency and countries), money in the store's currency.
40
- * - `useProductList` / `useCategories` / `useRibbons` — a listing with paging
41
- * (`moreProps` — spread it and paging can't be dropped), filters,
42
- * `refreshing`, and failure as a visible state.
43
- * - `useProduct` / `useAddToCart` / `useAddToCartButton` / `useProductPrice` /
44
- * `useProductGallery` / `useProductSpecs` — the product page: fetch +
45
- * variant selection + quantity + price + gallery, race-safe, with
46
- * `status: "not_found"` and every add-to-cart failure handled; the buy
47
- * button as `buttonProps` + `state`/`label`. `variantAxes(view, pick)`
48
- * (from `@/commerce/utils`, re-exported here) turns the resolved view into
49
- * a render-ready model for the selector you write; `useProductSpecs` adds
50
- * normalized `pick`/`get` lookup over `productSpecs`.
51
- * - `useProductReviews` — the review list and the submit form, with the store's
52
- * policy as a prop and field errors matching the server's codes.
53
- * - `useCart` / `useCartLine` / `useCoupon` — the shared cart (branch on
54
- * `status`, render `lines` and `notices` — money pre-formatted per line),
55
- * quantity steppers that clamp and recover, and the coupon field a store
56
- * with coupons must have.
57
- * - `useCartUI` + `<CartUIProvider>` — the cart drawer's non-visual machinery:
58
- * open state, trigger/overlay/panel/close prop sets with dialog semantics,
59
- * inert-when-closed, Esc, close-on-navigate, open-on-add.
22
+ * currency and countries), money in the store's currency, and the country
23
+ * list as an array that is never null.
24
+ * - `useProductList` / `useCategories` / `useRibbons` — a listing with paging,
25
+ * the whole server-side filter surface, and failure as a visible state.
26
+ * - `useProduct` / `useAddToCart` — the product page: fetch + variant selection
27
+ * + quantity, race-safe; the buy box as `state`/`disabled`/`addToCart`.
28
+ * - `useCart` / `useCartLine` / `CartLine` — the shared cart (branch on
29
+ * `status`, map `cart.items` into your own rows) plus quantity steppers that
30
+ * clamp, coalesce rapid clicks and roll back on rejection.
31
+ * - `useCartUI` + `<CartUIProvider>` — a cart drawer's non-visual state.
60
32
  * - `useCheckout` / `CheckoutProvider` / `useCheckoutContext` — the guided
61
- * checkout: address state with automatic debounced shipping/tax
62
- * recalculation, shipping and payment choice, a `canPlaceOrder` gate with
63
- * named blockers, `placeOrder` with both navigations handled (online →
64
- * provider redirect, manual → `/order-received`).
65
- * - `usePlaceOrder` — the gate as one surface: `buttonProps`, `label`,
66
- * `stage` (guards the just-placed-order frames), `blockers` in words.
67
- * - `useAddressForm` / `useCountries` / `useTotalsLines` /
68
- * `useCheckoutBlockers` — the address form as a field spec with per-field
69
- * `inputProps`/`selectProps`/`labelProps`/`errorProps` (state included,
70
- * country options never null), one totals projection for cart and order,
71
- * and blocker codes turned into copy.
72
- * - `useUpsell` — one product offered beside another surface: fetch-or-row,
73
- * already-in-cart matched by id, one-click add.
74
- * - `useOrderReturn` — the mandatory `/order-received` page in one hook:
75
- * status, order, `lines`, `paymentLink`, `paymentInstructions`, noindex.
76
- * - `useStorefrontSeo` + `productSeo` / `collectionSeo` / `orderSeo` — titles,
77
- * meta and product structured data; receipts are `noindex`.
33
+ * checkout: automatic shipping/tax recalculation, shipping and payment
34
+ * choice, a `canPlaceOrder` gate with named blockers, `placeOrder`.
35
+ * - `useOrderReturn` — the mandatory `/order-received` page in one hook.
36
+ * - `ShippingMethodPicker` / `PaymentMethodPicker` — render-prop components for
37
+ * the two checkout choices that are store data, never hardcoded.
78
38
  *
79
- * ## Render-prop components (headless — children is a function, no markup ships)
80
- * - `ShippingMethodPicker` / `PaymentMethodPicker` — the two checkout choices
81
- * that are store data, never hardcoded; options arrive decorated with
82
- * `radioProps`/`labelProps`/`costLabel` and a single `hint` message.
83
- * - `CartLine` — per-line `useCartLine` binding for your cart rows, so a
84
- * `lines.map(...)` never calls a hook in a loop.
39
+ * Re-exported from `@/commerce/utils` so one import line covers a page:
40
+ * `variantAxes` (axes → options with selected/disabled/stock derived),
41
+ * `productPrice` (the from-price and incomplete-selection range rules),
42
+ * `productImages` + `imageIndex` (an image's position in that list — how a
43
+ * gallery follows the variant selection without owning a second copy of it),
44
+ * `productSpecs` + `findSpec` (meta keys are free text, so featuring a
45
+ * particular spec needs a tolerant lookup, not an equality test),
46
+ * `attributesLabel`, `cartTotalsLines` / `orderTotalsLines`,
47
+ * `addressFieldSpec` (the checkout's field list, including the state/province
48
+ * field that silently mis-prices US/CA/AU orders when it is left out), and
49
+ * `storefrontErrorCode` / `storefrontErrorMessage` for the calls you make
50
+ * yourself — every rejection from the client carries a code worth branching on.
85
51
  *
86
- * ## Helpers re-exported from `@/commerce/utils`
87
- * - `variantAxes(view, pick)` — axes → options with selected/disabled/stock
88
- * state derived, for the variant selector you write.
89
- * - `productSpecs(product)` — `meta_data` → descriptive rows carrying an
90
- * inferred `type` (`numeric` with `number`/`unit` split out, `duration`,
91
- * `location`, `list` with `items`, `text`), so a weight can be a figure and
92
- * a composition bars instead of every modifier being one grey table row.
93
- * - `productImages(product)` — images normalized to `{src, name, alt}` and
94
- * de-duplicated; `[]` means render your placeholder.
52
+ * The catalog and product surfaces are where the design freedom lives: these
53
+ * hooks hand you resolved data, and the rendering is entirely yours.
95
54
  */
96
55
  export {
97
56
  StorefrontProvider,
@@ -99,10 +58,10 @@ export {
99
58
  useStorefront,
100
59
  useStoreInfo,
101
60
  useFormatMoney,
61
+ useCountries,
102
62
  useCart,
103
63
  } from "./StorefrontProvider";
104
64
  export { useCheckout, CheckoutProvider, useCheckoutContext } from "./useCheckout";
105
- export { usePlaceOrder } from "./usePlaceOrder";
106
65
  export { useOrderReturn, orderReceivedUrl } from "./useOrderReturn";
107
66
  export { ShippingMethodPicker, PaymentMethodPicker } from "./pickers";
108
67
  export { CartUIProvider, useCartUI } from "./cartUI";
@@ -115,19 +74,23 @@ export {
115
74
 
116
75
  // ── catalog ────────────────────────────────────────────────────────────────
117
76
  export { useProductList, useCategories, useRibbons } from "./useProductList";
118
- export { useProduct, useAddToCart, useAddToCartButton, useProductSpecs } from "./useProduct";
119
- export { useUpsell } from "./useUpsell";
120
- export { useProductPrice, useMoney } from "./useProductPrice";
121
- export { useProductGallery } from "./useProductGallery";
122
- export { useProductReviews } from "./useProductReviews";
77
+ export { useProduct, useAddToCart } from "./useProduct";
123
78
 
124
- // ── cart & checkout ────────────────────────────────────────────────────────
125
- export { useCartLine, useCoupon, CartLine } from "./useCartLine";
126
- export { useAddressForm, useCountries } from "./useAddressForm";
127
- export { useTotalsLines, useCheckoutBlockers, blockerMessage } from "./useTotalsLines";
128
-
129
- // ── SEO ────────────────────────────────────────────────────────────────────
130
- export { useStorefrontSeo, productSeo, collectionSeo, orderSeo } from "./useStorefrontSeo";
79
+ // ── cart ───────────────────────────────────────────────────────────────────
80
+ export { useCartLine, CartLine } from "./useCartLine";
131
81
 
132
82
  // ── view-model helpers (framework-free, from @/commerce/utils) ─────────────
133
- export { variantAxes, productSpecs, productImages } from "@/commerce/utils";
83
+ export {
84
+ variantAxes,
85
+ productPrice,
86
+ productImages,
87
+ imageIndex,
88
+ productSpecs,
89
+ findSpec,
90
+ attributesLabel,
91
+ cartTotalsLines,
92
+ orderTotalsLines,
93
+ addressFieldSpec,
94
+ storefrontErrorCode,
95
+ storefrontErrorMessage,
96
+ } from "@/commerce/utils";
@@ -8,11 +8,25 @@ import { useFormatMoney } from "./StorefrontProvider";
8
8
  * whole UI — but they encode the branching every checkout must do, so a page
9
9
  * can't skip a `shipping_status` state or invent a payment method.
10
10
  *
11
- * Every option comes **decorated with ready-to-spread prop sets**
12
- * (`radioProps` / `labelProps`) and pre-formatted money (`costLabel`), so the
13
- * child is markup + classes and nothing else. `hint` is the one message the
14
- * picker wants shown right now (or null) — render it and the status branching
15
- * is done.
11
+ * Every option comes decorated with plain states and one handler — `selected`
12
+ * (boolean), `select()` (pick it) and money formatted in the store's currency
13
+ * (`costLabel`) — so the child is markup + classes and nothing else.
14
+ *
15
+ * `hint` is the one thing the picker needs said right now, as a **code**, never
16
+ * as copy: `{ code, severity: "info"|"error", serverMessage }`. The words are
17
+ * the store's — write one line per code (there are three) in the store's own
18
+ * voice. `serverMessage` is set only when the backend explained the situation
19
+ * itself (an undeliverable address); it is more specific than anything you can
20
+ * write, so prefer it when present:
21
+ *
22
+ * const SHIPPING_HINTS = { // your words, once, near the checkout
23
+ * missing_address: "Enter your address to see delivery options.",
24
+ * none_available: "We can't deliver to that address yet.",
25
+ * syncing: "Updating delivery options…",
26
+ * };
27
+ * {hint && <p role={hint.severity === "error" ? "alert" : "status"}>
28
+ * {hint.serverMessage ?? SHIPPING_HINTS[hint.code]}
29
+ * </p>}
16
30
  *
17
31
  * Both read the nearest <CheckoutProvider>, or take an explicit `checkout`
18
32
  * prop when you called `useCheckout` yourself.
@@ -27,39 +41,25 @@ function resolveCheckout(name, prop, ctx) {
27
41
  }
28
42
 
29
43
  /**
30
- * Shipping options. Renders null for a virtual cart (`not_needed`) and while
31
- * the cart is loading; otherwise calls `children` with:
32
- *
33
- * {
34
- * status, // "missing_address" | "choice_required" | "chosen"
35
- * // | "auto_selected" | "none_available"
36
- * methods, // [{ id, title, cost, costLabel, selected,
37
- * // radioProps, labelProps }] — what this address is offered
38
- * chosen, // the chosen/auto-selected entry (with costLabel), or null
39
- * selected, // alias of `chosen` — the same name the payment picker uses
40
- * choose, // (id) => Promise — call with a method's id on pick
41
- * mustChoose, // status === "choice_required" → render methods as a picker
42
- * single, // exactly one method offered — already chosen; skip the
43
- * // picker but still show `chosen.title` and `chosen.costLabel`
44
- * syncing, // an address edit is being repriced — show a subtle busy state
45
- * hint, // { code, message, severity: "info"|"error" } | null — the
46
- * // one notice to show now (address missing / not deliverable
47
- * // / repricing); render `hint.message`, done
48
- * addressError, // { code, message } | null — also surfaces on the address
49
- * } // form's country field
44
+ * Shipping options. Renders null for a virtual cart (`not_needed`) and while the
45
+ * cart is loading; otherwise calls `children` with:
50
46
  *
51
- * The child's whole job:
52
- *
53
- * {hint && <p role={hint.severity === "error" ? "alert" : "status"}>{hint.message}</p>}
54
- * {mustChoose && methods.map(m => (
55
- * <label key={m.id} {...m.labelProps} className="…">
56
- * <input {...m.radioProps} className="…" /> {m.title} <span>{m.costLabel}</span>
57
- * </label>
58
- * ))}
59
- * {!mustChoose && chosen && <p>{chosen.title} {chosen.costLabel}</p>}
47
+ * status "missing_address" | "choice_required" | "chosen"
48
+ * | "auto_selected" | "none_available"
49
+ * methods [{ id, title, cost, costLabel, selected, select }]
50
+ * chosen the chosen/auto-selected entry, or null
51
+ * selected alias of `chosen` (the name the payment picker uses)
52
+ * choose (id) => Promise
53
+ * mustChoose status === "choice_required" → render methods as a picker
54
+ * single exactly one method offered — already chosen
55
+ * syncing an address edit is being repriced
56
+ * hint { code, severity, serverMessage } | null (see above)
57
+ * addressError { code, message } | null — the server's own words
60
58
  *
61
59
  * `single` and `mustChoose` are never both true, and `single` guarantees
62
- * `chosen` — a single option still *shows* what it is, never a picker of one.
60
+ * `chosen` — a single option still *shows* what it is (title + `costLabel`),
61
+ * never a picker of one. Never render `cart.chosen_shipping_method` directly:
62
+ * it is the rate's id, which is why `chosen` is handed to you resolved.
63
63
  */
64
64
  export function ShippingMethodPicker({ checkout: checkoutProp, children }) {
65
65
  const checkout = resolveCheckout("ShippingMethodPicker", checkoutProp, useCheckoutContextOptional());
@@ -79,23 +79,15 @@ export function ShippingMethodPicker({ checkout: checkoutProp, children }) {
79
79
  ...m,
80
80
  selected: m.id === chosen?.id,
81
81
  costLabel: formatMoney(m.cost),
82
- radioProps: {
83
- type: "radio",
84
- name: "shipping-method",
85
- id: `shipping-method-${m.id}`,
86
- value: m.id,
87
- checked: m.id === chosen?.id,
88
- onChange: () => choose(m.id),
89
- },
90
- labelProps: { htmlFor: `shipping-method-${m.id}` },
82
+ select: () => choose(m.id),
91
83
  };
92
84
  const hint =
93
85
  status === "missing_address"
94
- ? { code: "missing_address", severity: "info", message: "Delivery options appear once your address is entered." }
86
+ ? { code: "missing_address", severity: "info", serverMessage: null }
95
87
  : status === "none_available" || addressError?.code === "shipping_not_available"
96
- ? { code: "none_available", severity: "error", message: addressError?.message ?? "We don't deliver to that address yet." }
88
+ ? { code: "none_available", severity: "error", serverMessage: addressError?.message ?? null }
97
89
  : syncing
98
- ? { code: "syncing", severity: "info", message: "Updating delivery options…" }
90
+ ? { code: "syncing", severity: "info", serverMessage: null }
99
91
  : null;
100
92
  const decoratedChosen = decorate(chosen) ?? null;
101
93
  return children({
@@ -113,34 +105,22 @@ export function ShippingMethodPicker({ checkout: checkoutProp, children }) {
113
105
  }
114
106
 
115
107
  /**
116
- * Payment methods — every gateway the admin has ENABLED, from store info
117
- * (their only source). Renders null while store info loads; otherwise calls
118
- * `children` with:
119
- *
120
- * {
121
- * gateways, // [{ slug, title, description, online, selected,
122
- * // radioProps, labelProps }] — admin-owned data, decorated
123
- * value, // the selected slug ("" while none)
124
- * select, // (slug) => void
125
- * selected, // the selected gateway entry, or null
126
- * single, // exactly one gateway — already selected; skip the picker but
127
- * // still show its title so the customer knows how they pay
128
- * hint, // { code, message, severity } | null — set when there are no
129
- * } // gateways: checkout is unavailable, say so
130
- *
131
- * The child's whole job:
108
+ * Payment methods — every gateway the admin has ENABLED, from store info (their
109
+ * only source). Renders null while store info loads; otherwise calls `children`
110
+ * with:
132
111
  *
133
- * {hint && <p role="alert">{hint.message}</p>}
134
- * {!single && gateways.map(g => (
135
- * <label key={g.slug} {...g.labelProps} className="…">
136
- * <input {...g.radioProps} className="…" /> {g.title} <span>{g.description}</span>
137
- * </label>
138
- * ))}
139
- * {single && selected && <p>{selected.title} — {selected.description}</p>}
112
+ * gateways [{ slug, title, description, online, selected, select }]
113
+ * value the selected slug ("" while none)
114
+ * select (slug) => void
115
+ * selected the selected gateway entry, or null
116
+ * single exactly one gateway — already selected
117
+ * hint { code: "none_available", severity, serverMessage } | null —
118
+ * no gateways at all: say checkout is unavailable, in your words
140
119
  *
141
120
  * `single` guarantees `value` and `selected` — never render the one-gateway
142
121
  * branch as "nothing selected yet" — and both re-resolve when store info
143
- * changes. Titles and descriptions are the admin's copy: render them, don't
122
+ * changes. A default-seeded store offers `offline` only, so never hardcode a
123
+ * card option. Titles and descriptions are the admin's copy: render them, don't
144
124
  * invent your own.
145
125
  */
146
126
  export function PaymentMethodPicker({ checkout: checkoutProp, children }) {
@@ -155,19 +135,11 @@ export function PaymentMethodPicker({ checkout: checkoutProp, children }) {
155
135
  const decorated = gateways.map((g) => ({
156
136
  ...g,
157
137
  selected: g.slug === value,
158
- radioProps: {
159
- type: "radio",
160
- name: "payment-method",
161
- id: `payment-method-${g.slug}`,
162
- value: g.slug,
163
- checked: g.slug === value,
164
- onChange: () => select(g.slug),
165
- },
166
- labelProps: { htmlFor: `payment-method-${g.slug}` },
138
+ select: () => select(g.slug),
167
139
  }));
168
140
  const hint =
169
141
  gateways.length === 0
170
- ? { code: "none_available", severity: "error", message: "No payment method is available right now." }
142
+ ? { code: "none_available", severity: "error", serverMessage: null }
171
143
  : null;
172
144
  return children({
173
145
  gateways: decorated,