create-cartbase 0.1.16 → 0.1.18

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 (34) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +25 -25
  3. package/dist/index.js +20 -20
  4. package/package.json +1 -1
  5. package/template/app/docs/auth.md +105 -105
  6. package/template/app/docs/carts.md +376 -376
  7. package/template/app/docs/categories.md +194 -194
  8. package/template/app/docs/checkout.md +714 -714
  9. package/template/app/docs/components.md +55 -11
  10. package/template/app/docs/consent.md +91 -91
  11. package/template/app/docs/deploy.md +197 -197
  12. package/template/app/docs/gift-cards.md +153 -153
  13. package/template/app/docs/metaobjects.md +126 -126
  14. package/template/app/docs/orders.md +221 -221
  15. package/template/app/docs/products.md +51 -2
  16. package/template/app/docs/regions.md +269 -269
  17. package/template/app/docs/reviews.md +223 -223
  18. package/template/app/docs/search.md +227 -227
  19. package/template/app/docs/store.md +47 -47
  20. package/template/app/docs/subscriptions.md +148 -148
  21. package/template/app/docs/variables.md +315 -315
  22. package/template/app/package.json +1 -1
  23. package/template/app/postcss.config.cjs +11 -11
  24. package/template/app/src/app/checkout/checkout-page-client.tsx +73 -73
  25. package/template/app/src/app/checkout/page.tsx +48 -48
  26. package/template/app/src/app/globals.css +26 -26
  27. package/template/app/src/app/page.tsx +28 -28
  28. package/template/app/src/app/products/[handle]/page.tsx +87 -87
  29. package/template/app/src/app/providers.tsx +68 -64
  30. package/template/app/src/app/search/page.tsx +23 -23
  31. package/template/app/src/lib/browser-client.ts +35 -35
  32. package/template/app/src/lib/config.ts +41 -41
  33. package/template/app/src/lib/server-client.ts +25 -25
  34. package/template/app/src/lib/cart-actions.ts +0 -47
@@ -603,10 +603,31 @@ Domain doc for every call: [carts.md](carts.md); gift-card tender:
603
603
  no metadata on update; quantity 0 deletes), `deleteLineItem`. Every
604
604
  mutation returns the FULL decorated cart, which becomes the confirmed
605
605
  snapshot — no page refetch needed.
606
+ - **The instant cart (since 0.18.0)** — every change shows at once and
607
+ reaches the platform through ONE queue (`cart-drawer/mutation-queue`,
608
+ `createCartMutationQueue`): one call at a time, each reading the cart the
609
+ answer before it left, so two answers never overwrite each other and a
610
+ second add never creates a second cart. Fast quantity clicks on one line
611
+ send the number the shopper stopped at; the stepper never locks. A line
612
+ added a moment ago carries `pending: true` and a placeholder id
613
+ (`isPendingLine(line)`); its quantity and removal work at once and are
614
+ sent to the real line when the platform has confirmed it.
615
+ - **A completed cart is no cart (since 0.18.1)** — Medusa's rule: the
616
+ platform still answers a completed cart and refuses every change to it
617
+ (`409 cart_completed`), so the drawer never holds one. Reading one, or
618
+ being told the stored cart is gone (`cart_not_found`), empties the drawer
619
+ and calls `onCartEnd`; an add aimed at such a cart starts a new cart with
620
+ the item. The checkout clears the cookie (`clearCartCookie`,
621
+ `lib/cookie-names`) and calls `forget()` the moment the order is placed,
622
+ Medusa's `removeCartId()`.
606
623
  - **Props contract** — `cart?: Cart|null` (server-fetched snapshot; prop
607
- updates win), `client?: StorefrontClient` (enables `addItem`/
608
- `updateQuantity`/`removeItem`/`refresh`), `cartId?` (retrieve-on-mount
609
- when no snapshot), `onCartChange?(cart)` (fires on every confirmed
624
+ updates win unless a change of the provider's own is on its way; leave it
625
+ out and pass `cartId` so the page never waits for the cart),
626
+ `client?: StorefrontClient` (enables `addLine`/`addItem`/
627
+ `updateQuantity`/`removeItem`/`refresh`), `cartId?` (read in the browser
628
+ on mount, through the queue, when no snapshot), `onCartEnd?()` (the cart
629
+ is over: clear the stored id; `onCartEnd={clearCartCookie}` for the
630
+ cookie), `onCartChange?(cart)` (fires on every confirmed
610
631
  change INCLUDING first-add cart creation — persist `cart.id` here),
611
632
  `onOptimisticError?(failure)` (the ops funnel for every failed
612
633
  optimistic mutation — wire to store logging; replaces the source's
@@ -614,10 +635,14 @@ Domain doc for every call: [carts.md](carts.md); gift-card tender:
614
635
  the language mounted by `StorefrontLocaleProvider` is the default),
615
636
  `hrefs?: {checkout, browse, productPrefix}`.
616
637
  - **Hook** — `useCartDrawer()` → `{isOpen, open, close, toggle, cart,
617
- addItem(variantId, qty?, display?), updateQuantity(lineId, qty),
618
- removeItem(lineId), refresh, applyOptimistic, dispatchOptimistic,
619
- labels, hrefs}`. `applyOptimistic(action, serverAction)` stays for
620
- store-owned server actions (PDP add via RSC).
638
+ addLine(variantId, qty?, display?)` (answers `{ok: true, cart}` or
639
+ `{ok: false, error}`, the platform's refusal with its `code`),
640
+ `addItem(variantId, qty?, display?)` (the same, answering a boolean),
641
+ `updateQuantity(lineId, qty), removeItem(lineId), refresh, forget`
642
+ (empties the drawer and calls `onCartEnd`), `canMutate,
643
+ applyOptimistic, dispatchOptimistic, labels, hrefs}`.
644
+ `applyOptimistic(action, serverAction)` stays for store-owned server
645
+ actions; the SDK methods are the fast path.
621
646
  - **Settings** — store default region + enabled currencies (cart create),
622
647
  B2B price lists via attached customer, gift-card product flags
623
648
  (`is_giftcard` lines are non-discountable), automatic promotions,
@@ -664,7 +689,7 @@ Domain doc for every call: [carts.md](carts.md); gift-card tender:
664
689
  per-item upsell slot.
665
690
  - Subcomponents: `item/quantity` (`<CartItemQuantity lineId quantity
666
691
  maxQuantity? />` — stepper floor 1, calls `updateQuantity` with
667
- `{quantity}`), `item/variant` (`<CartItemVariant variantTitle
692
+ `{quantity}` on every click and never locks), `item/variant` (`<CartItemVariant variantTitle
668
693
  options? />` — Cartbase lines carry the flat `variant_title` string, not
669
694
  an embedded variant object), `item/upsell` (`<CartItemUpsell products
670
695
  onAdd />` — feed from `listRelatedProducts`, `variantId` included so
@@ -799,7 +824,7 @@ components read copy only through the context or explicit `labels` props).
799
824
 
800
825
  ### The product page contract — `products/variant-url`, `products/use-product-actions`
801
826
 
802
- Five rules every buy panel is held to, the package's own and a store's:
827
+ Six rules every buy panel is held to, the package's own and a store's:
803
828
 
804
829
  1. **The address is the state.** The chosen variant is named in the URL as
805
830
  `?variant=<id>`, Shopify's parameter, so a link copied from an email, an
@@ -821,11 +846,17 @@ Five rules every buy panel is held to, the package's own and a store's:
821
846
  the admin shows; never the wire's first variant, which is the database's
822
847
  scan order.
823
848
  5. **The server is asked only for what needs it:** adding to the cart.
849
+ 6. **The add is instant** (since 0.18.0). With no `addToCart` passed, the
850
+ add goes through the mounted `CartDrawerProvider` (`addLine`): the
851
+ drawer opens at the click with the product already in it, the platform
852
+ confirms behind it, and no page render runs. A store that passes its
853
+ own `addToCart` server action keeps the old path (await it, open the
854
+ cart, refresh the page so a server-rendered cart follows).
824
855
 
825
856
  `products/variant-url` is the pure half (`VARIANT_PARAM`,
826
857
  `variantParamValue`, `findVariantByParam`, `defaultVariant`, `variantHref`;
827
858
  unit-tested in tests/unit/storefront-product-page-contract.test.ts).
828
- `useProductActions({product, addToCart, initialVariantId?, onAddToCart?,
859
+ `useProductActions({product, addToCart?, initialVariantId?, onAddToCart?,
829
860
  openCart?, syncAddress?})` is the behaviour as a headless hook: the chosen
830
861
  values, `choose`, the resolved `variant`, `quantity`/`setQuantity`,
831
862
  `inStock` (the server's `in_stock` predicate, never re-derived;
@@ -835,6 +866,19 @@ hook and inherits every rule; `ProductActions` below is the package's own
835
866
  panel on the same hook. Client hook; it reads `useSearchParams`, so mount it
836
867
  under a `<Suspense>` boundary on a static page (a dynamic page needs none).
837
868
 
869
+ ### Pure: `products/sets` (sets from kit links)
870
+
871
+ A set is an inventory kit: its variant draws on the stock items of the
872
+ products inside it. Pass `fields: SET_FIELDS` to `listProducts` or
873
+ `retrieveProduct` and every variant carries `inventory_items`; then
874
+ `setContents(product, variant)` names what a set holds (product, variant,
875
+ title, handle, picture, how many), `setContentsByVariant(products)` keys
876
+ that by variant for a page of products, and `setsContaining(product,
877
+ variant)` names the products holding this variant's own item, the sets it
878
+ is in. `linkOwner(link, productId)` is the rule underneath: an item's owner
879
+ is the other product's variant carrying the item's SKU. Unit-tested
880
+ (tests/unit/storefront-sets.test.ts).
881
+
838
882
  ### Pure: `products/variant-matching` (extra module, Cartbase addition)
839
883
 
840
884
  `optionsAsKeymap` / `optionsMatch` / `findMatchingVariant` — the
@@ -850,7 +894,7 @@ with a legacy flat-row fallback. Unit-tested
850
894
  images `priority`.
851
895
  - **SDK calls** — none (props: `product.images`). Server-safe.
852
896
 
853
- ### `<ProductActions product addToCart initialVariantId? disabled? onAddToCart? openCart? showQuantity? />` — `products/product-actions`
897
+ ### `<ProductActions product addToCart? initialVariantId? disabled? onAddToCart? openCart? showQuantity? />` — `products/product-actions`
854
898
 
855
899
  - **Purpose** — the package's own buy panel on `useProductActions`: option
856
900
  rows, price, the quantity stepper (`showQuantity`, on by default), the
@@ -1,91 +1,91 @@
1
- # Consent
2
-
3
- The store's CMP configuration for the built-in Consent Mode v2 banner
4
- (consent-management card). Defaults are **always applied server-side**, so
5
- the payload is complete and renderable even for an unconfigured store
6
- (compliant built-in modal, BG + EN copy shipped).
7
-
8
- ## Storefront wiring (the trap that matters)
9
-
10
- - Mount `<ConsentInit required={consent.enabled}>` as the **first child of
11
- `<body>`** — it sets the synchronous Consent Mode v2 DEFAULT and must
12
- **never wait on this fetch in the browser** (async default = first-hit
13
- consent race). Resolve this config server-side (RSC) and inline the
14
- setting into the document; the prop is required, a layout without it
15
- does not compile.
16
- - **The switch decides the default; a stored choice wins.** `enabled: true`
17
- is a store that collects consent: every visitor starts DENIED until they
18
- choose on the banner. `enabled: false` is a store with no consent gate:
19
- every visitor starts GRANTED, and every configured pixel fires. Either
20
- way a choice already in the `_1c_consent` cookie is what counts, so a
21
- visitor who declined keeps that decision if the banner is switched off.
22
- - Render the built-in banner only when `enabled && mode === "builtin"`.
23
- - `mode: "external"` = the merchant's CMP owns the UI and must write the
24
- same `_1c_consent` cookie (or call `setConsent()`) — all Cartbase-side tag
25
- gating works off that one seam.
26
- - Choices persist 12 months in the cookie. Rybbit (platform analytics)
27
- stays outside consent by design.
28
- - Pair with [integrations.md](integrations.md): `tracking.consent_required`
29
- mirrors `enabled` here, and `<StorefrontTags>` hands it to every pixel
30
- that gates its own SDK (Meta, TikTok, ChatGPT). Google reads the
31
- `<ConsentInit>` default.
32
-
33
- ## GET /api/store/consent — the CMP config
34
-
35
- - **Purpose**: everything the banner needs to render, per locale.
36
- - **Auth**: anon (`x-client-id`).
37
- - **Request**: no params.
38
- - **Response** — the EXACT allowlist (nothing else will ever appear here):
39
-
40
- ```jsonc
41
- {
42
- "consent": {
43
- "enabled": true,
44
- "mode": "builtin", // "builtin" | "external"
45
- "layout": "modal", // "modal" (blocking) | "banner-bottom" (non-blocking)
46
- "privacy_href": "/cookies",
47
- "reject_on_first_layer": false,
48
- "copy": {
49
- "bg": {
50
- "title": "Преди да продължиш",
51
- "body": "…",
52
- "privacy_link_label": "…",
53
- "accept_label": "…", "settings_label": "…", "reject_label": "…",
54
- "settings_title": "…",
55
- "accept_all_label": "…", "save_label": "…", "reject_all_label": "…",
56
- "necessary_label": "…", "necessary_description": "…",
57
- "analytics_label": "…", "analytics_description": "…",
58
- "ads_label": "…", "ads_description": "…"
59
- },
60
- "en": { /* same 16 keys — every field always present per locale */ }
61
- }
62
- }
63
- }
64
- ```
65
-
66
- - **Errors**: `400 missing_client_id` only — the payload itself always
67
- succeeds (a corrupt/missing stored config degrades to defaults, never to
68
- a broken banner).
69
- - **SDK**: `consent.getConsent(client)`
70
- - **Components**: `<ConsentInit required={consent.enabled}>` +
71
- `<ConsentBanner copy={copy[locale]} layout privacyHref
72
- rejectOnFirstLayer>` (consent family; reference impl
73
- `src/components/storefront/consent/`).
74
- - **Settings**: admin → Settings → Consent (enabled/mode/layout/
75
- privacy_href/copy per locale).
76
-
77
- ```bash
78
- # Complete, renderable config — even on an unconfigured store the defaults
79
- # make every documented key present.
80
- BODY=$(curl -sf "$BASE/api/store/consent" -H "x-client-id: $CLIENT_ID")
81
- echo "$BODY" | grep -q '"consent"'
82
- echo "$BODY" | grep -q '"mode"'
83
- echo "$BODY" | grep -q '"layout"'
84
- echo "$BODY" | grep -q '"privacy_href"'
85
- echo "$BODY" | grep -q '"reject_on_first_layer"'
86
- echo "$BODY" | grep -q '"analytics_label"'
87
- echo "$BODY" | grep -q '"ads_description"'
88
- # Missing tenant header → the standard 400.
89
- STATUS=$(curl -s -o /dev/null -w '%{http_code}' "$BASE/api/store/consent")
90
- test "$STATUS" = 400
91
- ```
1
+ # Consent
2
+
3
+ The store's CMP configuration for the built-in Consent Mode v2 banner
4
+ (consent-management card). Defaults are **always applied server-side**, so
5
+ the payload is complete and renderable even for an unconfigured store
6
+ (compliant built-in modal, BG + EN copy shipped).
7
+
8
+ ## Storefront wiring (the trap that matters)
9
+
10
+ - Mount `<ConsentInit required={consent.enabled}>` as the **first child of
11
+ `<body>`** — it sets the synchronous Consent Mode v2 DEFAULT and must
12
+ **never wait on this fetch in the browser** (async default = first-hit
13
+ consent race). Resolve this config server-side (RSC) and inline the
14
+ setting into the document; the prop is required, a layout without it
15
+ does not compile.
16
+ - **The switch decides the default; a stored choice wins.** `enabled: true`
17
+ is a store that collects consent: every visitor starts DENIED until they
18
+ choose on the banner. `enabled: false` is a store with no consent gate:
19
+ every visitor starts GRANTED, and every configured pixel fires. Either
20
+ way a choice already in the `_1c_consent` cookie is what counts, so a
21
+ visitor who declined keeps that decision if the banner is switched off.
22
+ - Render the built-in banner only when `enabled && mode === "builtin"`.
23
+ - `mode: "external"` = the merchant's CMP owns the UI and must write the
24
+ same `_1c_consent` cookie (or call `setConsent()`) — all Cartbase-side tag
25
+ gating works off that one seam.
26
+ - Choices persist 12 months in the cookie. Rybbit (platform analytics)
27
+ stays outside consent by design.
28
+ - Pair with [integrations.md](integrations.md): `tracking.consent_required`
29
+ mirrors `enabled` here, and `<StorefrontTags>` hands it to every pixel
30
+ that gates its own SDK (Meta, TikTok, ChatGPT). Google reads the
31
+ `<ConsentInit>` default.
32
+
33
+ ## GET /api/store/consent — the CMP config
34
+
35
+ - **Purpose**: everything the banner needs to render, per locale.
36
+ - **Auth**: anon (`x-client-id`).
37
+ - **Request**: no params.
38
+ - **Response** — the EXACT allowlist (nothing else will ever appear here):
39
+
40
+ ```jsonc
41
+ {
42
+ "consent": {
43
+ "enabled": true,
44
+ "mode": "builtin", // "builtin" | "external"
45
+ "layout": "modal", // "modal" (blocking) | "banner-bottom" (non-blocking)
46
+ "privacy_href": "/cookies",
47
+ "reject_on_first_layer": false,
48
+ "copy": {
49
+ "bg": {
50
+ "title": "Преди да продължиш",
51
+ "body": "…",
52
+ "privacy_link_label": "…",
53
+ "accept_label": "…", "settings_label": "…", "reject_label": "…",
54
+ "settings_title": "…",
55
+ "accept_all_label": "…", "save_label": "…", "reject_all_label": "…",
56
+ "necessary_label": "…", "necessary_description": "…",
57
+ "analytics_label": "…", "analytics_description": "…",
58
+ "ads_label": "…", "ads_description": "…"
59
+ },
60
+ "en": { /* same 16 keys — every field always present per locale */ }
61
+ }
62
+ }
63
+ }
64
+ ```
65
+
66
+ - **Errors**: `400 missing_client_id` only — the payload itself always
67
+ succeeds (a corrupt/missing stored config degrades to defaults, never to
68
+ a broken banner).
69
+ - **SDK**: `consent.getConsent(client)`
70
+ - **Components**: `<ConsentInit required={consent.enabled}>` +
71
+ `<ConsentBanner copy={copy[locale]} layout privacyHref
72
+ rejectOnFirstLayer>` (consent family; reference impl
73
+ `src/components/storefront/consent/`).
74
+ - **Settings**: admin → Settings → Consent (enabled/mode/layout/
75
+ privacy_href/copy per locale).
76
+
77
+ ```bash
78
+ # Complete, renderable config — even on an unconfigured store the defaults
79
+ # make every documented key present.
80
+ BODY=$(curl -sf "$BASE/api/store/consent" -H "x-client-id: $CLIENT_ID")
81
+ echo "$BODY" | grep -q '"consent"'
82
+ echo "$BODY" | grep -q '"mode"'
83
+ echo "$BODY" | grep -q '"layout"'
84
+ echo "$BODY" | grep -q '"privacy_href"'
85
+ echo "$BODY" | grep -q '"reject_on_first_layer"'
86
+ echo "$BODY" | grep -q '"analytics_label"'
87
+ echo "$BODY" | grep -q '"ads_description"'
88
+ # Missing tenant header → the standard 400.
89
+ STATUS=$(curl -s -o /dev/null -w '%{http_code}' "$BASE/api/store/consent")
90
+ test "$STATUS" = 400
91
+ ```