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.
- package/LICENSE +21 -21
- package/README.md +25 -25
- package/dist/index.js +20 -20
- package/package.json +1 -1
- package/template/app/docs/auth.md +105 -105
- package/template/app/docs/carts.md +376 -376
- package/template/app/docs/categories.md +194 -194
- package/template/app/docs/checkout.md +714 -714
- package/template/app/docs/components.md +55 -11
- package/template/app/docs/consent.md +91 -91
- package/template/app/docs/deploy.md +197 -197
- package/template/app/docs/gift-cards.md +153 -153
- package/template/app/docs/metaobjects.md +126 -126
- package/template/app/docs/orders.md +221 -221
- package/template/app/docs/products.md +51 -2
- package/template/app/docs/regions.md +269 -269
- package/template/app/docs/reviews.md +223 -223
- package/template/app/docs/search.md +227 -227
- package/template/app/docs/store.md +47 -47
- package/template/app/docs/subscriptions.md +148 -148
- package/template/app/docs/variables.md +315 -315
- package/template/app/package.json +1 -1
- package/template/app/postcss.config.cjs +11 -11
- package/template/app/src/app/checkout/checkout-page-client.tsx +73 -73
- package/template/app/src/app/checkout/page.tsx +48 -48
- package/template/app/src/app/globals.css +26 -26
- package/template/app/src/app/page.tsx +28 -28
- package/template/app/src/app/products/[handle]/page.tsx +87 -87
- package/template/app/src/app/providers.tsx +68 -64
- package/template/app/src/app/search/page.tsx +23 -23
- package/template/app/src/lib/browser-client.ts +35 -35
- package/template/app/src/lib/config.ts +41 -41
- package/template/app/src/lib/server-client.ts +25 -25
- 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
|
|
608
|
-
`
|
|
609
|
-
|
|
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
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
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
|
-
|
|
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
|
|
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
|
+
```
|