create-cartbase 0.1.16 → 0.1.17
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 +24 -24
- 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 +44 -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 +64 -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,21 @@ 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.
|
|
606
615
|
- **Props contract** — `cart?: Cart|null` (server-fetched snapshot; prop
|
|
607
|
-
updates win
|
|
608
|
-
`
|
|
609
|
-
|
|
616
|
+
updates win unless a change of the provider's own is on its way; leave it
|
|
617
|
+
out and pass `cartId` so the page never waits for the cart),
|
|
618
|
+
`client?: StorefrontClient` (enables `addLine`/`addItem`/
|
|
619
|
+
`updateQuantity`/`removeItem`/`refresh`), `cartId?` (read in the browser
|
|
620
|
+
on mount, through the queue, when no snapshot), `onCartChange?(cart)` (fires on every confirmed
|
|
610
621
|
change INCLUDING first-add cart creation — persist `cart.id` here),
|
|
611
622
|
`onOptimisticError?(failure)` (the ops funnel for every failed
|
|
612
623
|
optimistic mutation — wire to store logging; replaces the source's
|
|
@@ -614,10 +625,13 @@ Domain doc for every call: [carts.md](carts.md); gift-card tender:
|
|
|
614
625
|
the language mounted by `StorefrontLocaleProvider` is the default),
|
|
615
626
|
`hrefs?: {checkout, browse, productPrefix}`.
|
|
616
627
|
- **Hook** — `useCartDrawer()` → `{isOpen, open, close, toggle, cart,
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
628
|
+
addLine(variantId, qty?, display?)` (answers `{ok: true, cart}` or
|
|
629
|
+
`{ok: false, error}`, the platform's refusal with its `code`),
|
|
630
|
+
`addItem(variantId, qty?, display?)` (the same, answering a boolean),
|
|
631
|
+
`updateQuantity(lineId, qty), removeItem(lineId), refresh, canMutate,
|
|
632
|
+
applyOptimistic, dispatchOptimistic, labels, hrefs}`.
|
|
633
|
+
`applyOptimistic(action, serverAction)` stays for store-owned server
|
|
634
|
+
actions; the SDK methods are the fast path.
|
|
621
635
|
- **Settings** — store default region + enabled currencies (cart create),
|
|
622
636
|
B2B price lists via attached customer, gift-card product flags
|
|
623
637
|
(`is_giftcard` lines are non-discountable), automatic promotions,
|
|
@@ -664,7 +678,7 @@ Domain doc for every call: [carts.md](carts.md); gift-card tender:
|
|
|
664
678
|
per-item upsell slot.
|
|
665
679
|
- Subcomponents: `item/quantity` (`<CartItemQuantity lineId quantity
|
|
666
680
|
maxQuantity? />` — stepper floor 1, calls `updateQuantity` with
|
|
667
|
-
`{quantity}`), `item/variant` (`<CartItemVariant variantTitle
|
|
681
|
+
`{quantity}` on every click and never locks), `item/variant` (`<CartItemVariant variantTitle
|
|
668
682
|
options? />` — Cartbase lines carry the flat `variant_title` string, not
|
|
669
683
|
an embedded variant object), `item/upsell` (`<CartItemUpsell products
|
|
670
684
|
onAdd />` — feed from `listRelatedProducts`, `variantId` included so
|
|
@@ -799,7 +813,7 @@ components read copy only through the context or explicit `labels` props).
|
|
|
799
813
|
|
|
800
814
|
### The product page contract — `products/variant-url`, `products/use-product-actions`
|
|
801
815
|
|
|
802
|
-
|
|
816
|
+
Six rules every buy panel is held to, the package's own and a store's:
|
|
803
817
|
|
|
804
818
|
1. **The address is the state.** The chosen variant is named in the URL as
|
|
805
819
|
`?variant=<id>`, Shopify's parameter, so a link copied from an email, an
|
|
@@ -821,11 +835,17 @@ Five rules every buy panel is held to, the package's own and a store's:
|
|
|
821
835
|
the admin shows; never the wire's first variant, which is the database's
|
|
822
836
|
scan order.
|
|
823
837
|
5. **The server is asked only for what needs it:** adding to the cart.
|
|
838
|
+
6. **The add is instant** (since 0.18.0). With no `addToCart` passed, the
|
|
839
|
+
add goes through the mounted `CartDrawerProvider` (`addLine`): the
|
|
840
|
+
drawer opens at the click with the product already in it, the platform
|
|
841
|
+
confirms behind it, and no page render runs. A store that passes its
|
|
842
|
+
own `addToCart` server action keeps the old path (await it, open the
|
|
843
|
+
cart, refresh the page so a server-rendered cart follows).
|
|
824
844
|
|
|
825
845
|
`products/variant-url` is the pure half (`VARIANT_PARAM`,
|
|
826
846
|
`variantParamValue`, `findVariantByParam`, `defaultVariant`, `variantHref`;
|
|
827
847
|
unit-tested in tests/unit/storefront-product-page-contract.test.ts).
|
|
828
|
-
`useProductActions({product, addToCart
|
|
848
|
+
`useProductActions({product, addToCart?, initialVariantId?, onAddToCart?,
|
|
829
849
|
openCart?, syncAddress?})` is the behaviour as a headless hook: the chosen
|
|
830
850
|
values, `choose`, the resolved `variant`, `quantity`/`setQuantity`,
|
|
831
851
|
`inStock` (the server's `in_stock` predicate, never re-derived;
|
|
@@ -835,6 +855,19 @@ hook and inherits every rule; `ProductActions` below is the package's own
|
|
|
835
855
|
panel on the same hook. Client hook; it reads `useSearchParams`, so mount it
|
|
836
856
|
under a `<Suspense>` boundary on a static page (a dynamic page needs none).
|
|
837
857
|
|
|
858
|
+
### Pure: `products/sets` (sets from kit links)
|
|
859
|
+
|
|
860
|
+
A set is an inventory kit: its variant draws on the stock items of the
|
|
861
|
+
products inside it. Pass `fields: SET_FIELDS` to `listProducts` or
|
|
862
|
+
`retrieveProduct` and every variant carries `inventory_items`; then
|
|
863
|
+
`setContents(product, variant)` names what a set holds (product, variant,
|
|
864
|
+
title, handle, picture, how many), `setContentsByVariant(products)` keys
|
|
865
|
+
that by variant for a page of products, and `setsContaining(product,
|
|
866
|
+
variant)` names the products holding this variant's own item, the sets it
|
|
867
|
+
is in. `linkOwner(link, productId)` is the rule underneath: an item's owner
|
|
868
|
+
is the other product's variant carrying the item's SKU. Unit-tested
|
|
869
|
+
(tests/unit/storefront-sets.test.ts).
|
|
870
|
+
|
|
838
871
|
### Pure: `products/variant-matching` (extra module, Cartbase addition)
|
|
839
872
|
|
|
840
873
|
`optionsAsKeymap` / `optionsMatch` / `findMatchingVariant` — the
|
|
@@ -850,7 +883,7 @@ with a legacy flat-row fallback. Unit-tested
|
|
|
850
883
|
images `priority`.
|
|
851
884
|
- **SDK calls** — none (props: `product.images`). Server-safe.
|
|
852
885
|
|
|
853
|
-
### `<ProductActions product addToCart initialVariantId? disabled? onAddToCart? openCart? showQuantity? />` — `products/product-actions`
|
|
886
|
+
### `<ProductActions product addToCart? initialVariantId? disabled? onAddToCart? openCart? showQuantity? />` — `products/product-actions`
|
|
854
887
|
|
|
855
888
|
- **Purpose** — the package's own buy panel on `useProductActions`: option
|
|
856
889
|
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
|
+
```
|