create-cartbase 0.1.15 → 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 +50 -13
- 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/next.config.ts +6 -4
- 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
|
|
@@ -765,8 +779,12 @@ components read copy only through the context or explicit `labels` props).
|
|
|
765
779
|
- **SDK calls** — none (props: `product.thumbnail` / `product.images`).
|
|
766
780
|
- **Props** — `size: "small"|"medium"|"large"|"full"|"square"` (aspect +
|
|
767
781
|
width), `isFeatured` (11/14 aspect), `className`.
|
|
768
|
-
- **Mount rules** — server-safe;
|
|
769
|
-
media
|
|
782
|
+
- **Mount rules** — server-safe; renders through `StoreImage`
|
|
783
|
+
(`@cartbase/storefront/lib/media-image`), `next/image` with resizing on
|
|
784
|
+
for the Cartbase media CDN (`cartbase.cloud`, allowed in the scaffold's
|
|
785
|
+
`next.config` images) and off for any other host, so a picture from
|
|
786
|
+
elsewhere renders as it is instead of failing the page. Use `StoreImage`
|
|
787
|
+
for your own pictures too, or add your hosts to `remotePatterns`.
|
|
770
788
|
|
|
771
789
|
### `<PreviewPrice price />` — `products/preview-price`
|
|
772
790
|
|
|
@@ -795,7 +813,7 @@ components read copy only through the context or explicit `labels` props).
|
|
|
795
813
|
|
|
796
814
|
### The product page contract — `products/variant-url`, `products/use-product-actions`
|
|
797
815
|
|
|
798
|
-
|
|
816
|
+
Six rules every buy panel is held to, the package's own and a store's:
|
|
799
817
|
|
|
800
818
|
1. **The address is the state.** The chosen variant is named in the URL as
|
|
801
819
|
`?variant=<id>`, Shopify's parameter, so a link copied from an email, an
|
|
@@ -817,11 +835,17 @@ Five rules every buy panel is held to, the package's own and a store's:
|
|
|
817
835
|
the admin shows; never the wire's first variant, which is the database's
|
|
818
836
|
scan order.
|
|
819
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).
|
|
820
844
|
|
|
821
845
|
`products/variant-url` is the pure half (`VARIANT_PARAM`,
|
|
822
846
|
`variantParamValue`, `findVariantByParam`, `defaultVariant`, `variantHref`;
|
|
823
847
|
unit-tested in tests/unit/storefront-product-page-contract.test.ts).
|
|
824
|
-
`useProductActions({product, addToCart
|
|
848
|
+
`useProductActions({product, addToCart?, initialVariantId?, onAddToCart?,
|
|
825
849
|
openCart?, syncAddress?})` is the behaviour as a headless hook: the chosen
|
|
826
850
|
values, `choose`, the resolved `variant`, `quantity`/`setQuantity`,
|
|
827
851
|
`inStock` (the server's `in_stock` predicate, never re-derived;
|
|
@@ -831,6 +855,19 @@ hook and inherits every rule; `ProductActions` below is the package's own
|
|
|
831
855
|
panel on the same hook. Client hook; it reads `useSearchParams`, so mount it
|
|
832
856
|
under a `<Suspense>` boundary on a static page (a dynamic page needs none).
|
|
833
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
|
+
|
|
834
871
|
### Pure: `products/variant-matching` (extra module, Cartbase addition)
|
|
835
872
|
|
|
836
873
|
`optionsAsKeymap` / `optionsMatch` / `findMatchingVariant` — the
|
|
@@ -846,7 +883,7 @@ with a legacy flat-row fallback. Unit-tested
|
|
|
846
883
|
images `priority`.
|
|
847
884
|
- **SDK calls** — none (props: `product.images`). Server-safe.
|
|
848
885
|
|
|
849
|
-
### `<ProductActions product addToCart initialVariantId? disabled? onAddToCart? openCart? showQuantity? />` — `products/product-actions`
|
|
886
|
+
### `<ProductActions product addToCart? initialVariantId? disabled? onAddToCart? openCart? showQuantity? />` — `products/product-actions`
|
|
850
887
|
|
|
851
888
|
- **Purpose** — the package's own buy panel on `useProductActions`: option
|
|
852
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
|
+
```
|