create-cartbase 0.1.0 → 0.1.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.
- package/dist/index.js +3 -3
- package/package.json +1 -1
- package/template/app/docs/BUILD-A-STOREFRONT.md +5 -5
- package/template/app/docs/README.md +77 -76
- package/template/app/docs/carts.md +3 -3
- package/template/app/docs/checkout.md +2 -2
- package/template/app/docs/components.md +7 -6
- package/template/app/docs/deploy.md +4 -4
- package/template/app/docs/platform.md +126 -0
- package/template/app/docs/regions.md +4 -5
- package/template/app/next.config.ts +3 -3
- package/template/app/package.json +1 -1
- package/template/app/src/app/checkout/page.tsx +51 -49
- package/template/app/src/app/layout.tsx +113 -105
- package/template/app/src/lib/cart-actions.ts +47 -43
- package/template/app/src/lib/config.ts +10 -5
package/dist/index.js
CHANGED
|
@@ -68,9 +68,9 @@ as flags they are written into .env.local, otherwise placeholders are.`);
|
|
|
68
68
|
const key = typeof flags.get("key") === "string" ? flags.get("key") : "";
|
|
69
69
|
fs.writeFileSync(path.join(target, ".env.local"), [
|
|
70
70
|
"# Your store's inputs — Cartbase admin, Settings.",
|
|
71
|
-
`
|
|
72
|
-
`
|
|
73
|
-
`
|
|
71
|
+
`NEXT_PUBLIC_CARTBASE_URL=${url}`,
|
|
72
|
+
`NEXT_PUBLIC_CARTBASE_CLIENT_ID=${clientId}`,
|
|
73
|
+
`NEXT_PUBLIC_CARTBASE_PUBLISHABLE_KEY=${key}`,
|
|
74
74
|
"",
|
|
75
75
|
].join("\n"));
|
|
76
76
|
const filled = url && clientId;
|
package/package.json
CHANGED
|
@@ -12,9 +12,9 @@ What you need before starting (all three are in your Cartbase admin under
|
|
|
12
12
|
|
|
13
13
|
| Input | Example | Where it goes |
|
|
14
14
|
|---|---|---|
|
|
15
|
-
| API origin | `https://admin.bitfar.co` | `
|
|
16
|
-
| Store client id | `1e7a4c02-9b31-4f7e-8d2a-5c6f90ab12cd` (uuid) | `
|
|
17
|
-
| Publishable API key | `pk_…` (optional, channel scope) | `
|
|
15
|
+
| API origin | `https://admin.bitfar.co` | `NEXT_PUBLIC_CARTBASE_URL` |
|
|
16
|
+
| Store client id | `1e7a4c02-9b31-4f7e-8d2a-5c6f90ab12cd` (uuid) | `NEXT_PUBLIC_CARTBASE_CLIENT_ID` |
|
|
17
|
+
| Publishable API key | `pk_…` (optional, channel scope) | `NEXT_PUBLIC_CARTBASE_PUBLISHABLE_KEY` |
|
|
18
18
|
|
|
19
19
|
Sanity-check the store before writing any code:
|
|
20
20
|
|
|
@@ -73,7 +73,7 @@ whole checkout orchestration) only work same-origin. Unless your
|
|
|
73
73
|
storefront is served from the Cartbase deployment origin itself, proxy the
|
|
74
74
|
store surface through your own origin — in Next.js one rewrite does it —
|
|
75
75
|
and give the BROWSER client `window.location.origin` as `baseUrl`
|
|
76
|
-
(server-side calls hit `
|
|
76
|
+
(server-side calls hit `NEXT_PUBLIC_CARTBASE_URL` directly and are
|
|
77
77
|
unaffected; see `examples/storefront/next.config.ts` for the working
|
|
78
78
|
rewrite):
|
|
79
79
|
|
|
@@ -81,7 +81,7 @@ rewrite):
|
|
|
81
81
|
// next.config.ts — proxy browser SDK traffic to the API origin
|
|
82
82
|
async rewrites() {
|
|
83
83
|
return [{ source: "/api/store/:path*",
|
|
84
|
-
destination: `${process.env.
|
|
84
|
+
destination: `${process.env.NEXT_PUBLIC_CARTBASE_URL}/api/store/:path*` }]
|
|
85
85
|
}
|
|
86
86
|
```
|
|
87
87
|
|
|
@@ -1,76 +1,77 @@
|
|
|
1
|
-
# docs/storefront/ — the bulletproof storefront docs
|
|
2
|
-
|
|
3
|
-
**Audience: an AGENT building a storefront from a blank Next.js app.** These
|
|
4
|
-
docs are the entire knowledge transfer — every call shape, every curl, every
|
|
5
|
-
setting, every component contract. If a storefront can't be built from these
|
|
6
|
-
files alone, the fix is a doc fix, never tribal knowledge.
|
|
7
|
-
|
|
8
|
-
Start at **[BUILD-A-STOREFRONT.md](BUILD-A-STOREFRONT.md)** — the runbook.
|
|
9
|
-
Domain files below are its reference chapters.
|
|
10
|
-
|
|
11
|
-
## Executable-docs contract (docs-truth CI)
|
|
12
|
-
|
|
13
|
-
Docs that can lie aren't bulletproof, so every ```bash block in every file
|
|
14
|
-
here is **extracted and executed** against the real test server by
|
|
15
|
-
`tests/docs/storefront-curls.test.ts`. A drifted doc FAILS the build.
|
|
16
|
-
|
|
17
|
-
Rules for doc authors (agents included):
|
|
18
|
-
|
|
19
|
-
1. All ```bash blocks in one file form ONE script, executed top-to-bottom
|
|
20
|
-
with `bash -euo pipefail`. Later blocks may use variables exported by
|
|
21
|
-
earlier blocks (`CART_ID=$(curl … | grep -o …)`).
|
|
22
|
-
2. The harness pre-exports: `BASE` (test-server origin), `CLIENT_ID` (dev
|
|
23
|
-
tenant id), `PUBLISHABLE_KEY` (the B2B-channel dev key — channel-scoped,
|
|
24
|
-
use only where the doc discusses key scoping).
|
|
25
|
-
3. Every curl uses `-sf` (silent + fail-on-HTTP-error) unless the block
|
|
26
|
-
demonstrates an error case — then capture the status explicitly
|
|
27
|
-
(`-o /dev/null -w '%{http_code}'`) and assert it (`test "$STATUS" = 404`).
|
|
28
|
-
4. Assert shape, not just liveness: pipe to `grep -q '"key"'` (or `node -e`
|
|
29
|
-
for anything structural). A block that checks nothing proves nothing.
|
|
30
|
-
5. A block that must NOT run (illustrative only, external side effects)
|
|
31
|
-
starts with `# doc-noexec` on its first line. Use sparingly — every
|
|
32
|
-
noexec block is a hole in the truth gate.
|
|
33
|
-
6. Blocks must be idempotent-safe on the shared dev tenant: create what you
|
|
34
|
-
read, suffix names with `$RUN` (pre-exported unique stamp), and clean up
|
|
35
|
-
in a final block when you created durable rows.
|
|
36
|
-
|
|
37
|
-
## Per-domain file format
|
|
38
|
-
|
|
39
|
-
One file per domain. For each endpoint, in order:
|
|
40
|
-
|
|
41
|
-
- **Purpose** — one sentence, when a storefront calls it.
|
|
42
|
-
- **Auth** — which headers (anon `x-client-id` / publishable key / Bearer).
|
|
43
|
-
- **Request** — method, path, query/body shape (jsonc block).
|
|
44
|
-
- **Response** — shape (jsonc block), with field notes.
|
|
45
|
-
- **Working curl** — executable per the contract above.
|
|
46
|
-
- **Errors** — status + `code` for every contract-listed failure.
|
|
47
|
-
- **SDK** — the `@cartbase/storefront/api` function that wraps it.
|
|
48
|
-
- **Components** — which `@cartbase/storefront` UI components consume it.
|
|
49
|
-
- **Settings** — admin settings that change its behavior (checkout rules,
|
|
50
|
-
locales, consent, accounts mode…).
|
|
51
|
-
|
|
52
|
-
## Files
|
|
53
|
-
|
|
54
|
-
| File | Domain |
|
|
55
|
-
|---|---|
|
|
56
|
-
| [BUILD-A-STOREFRONT.md](BUILD-A-STOREFRONT.md) | The agent runbook — blank app → completed checkout |
|
|
57
|
-
| [products.md](products.md) | Products, variants, pricing context |
|
|
58
|
-
| [search.md](search.md) | Search, facets, related products |
|
|
59
|
-
| [collections.md](collections.md) | Collections + membership listings |
|
|
60
|
-
| [categories.md](categories.md) | Categories, tags, types |
|
|
61
|
-
| [regions.md](regions.md) | Regions, currencies, locales |
|
|
62
|
-
| [carts.md](carts.md) | Cart lifecycle + line items |
|
|
63
|
-
| [gift-cards.md](gift-cards.md) | Gift-card tender on carts |
|
|
64
|
-
| [checkout.md](checkout.md) | Shipping options, payment providers/collections, prepare-checkout orchestration, complete |
|
|
65
|
-
| [orders.md](orders.md) | Order reads, display-id lookup, transfers |
|
|
66
|
-
| [customers.md](customers.md) | Customer profile, addresses, documents |
|
|
67
|
-
| [subscriptions.md](subscriptions.md) | Subscription portal: schedule control, contract edits, payment-method recovery |
|
|
68
|
-
| [auth.md](auth.md) | Passwordless code login + session discipline |
|
|
69
|
-
| [content.md](content.md) | Pages + blogs |
|
|
70
|
-
| [menus.md](menus.md) | Navigation menus |
|
|
71
|
-
| [metaobjects.md](metaobjects.md) | Merchant-defined content types |
|
|
72
|
-
| [reviews.md](reviews.md) | Review widget, token wizard, photo rewards |
|
|
73
|
-
| [integrations.md](integrations.md) | Store config: carriers, COD, tracking block, lockers |
|
|
74
|
-
| [consent.md](consent.md) | Consent Mode v2 banner config |
|
|
75
|
-
| [redirects.md](redirects.md) | 404-path URL redirects |
|
|
76
|
-
| [components.md](components.md) | UI component families: contracts + required SDK calls |
|
|
1
|
+
# docs/storefront/ — the bulletproof storefront docs
|
|
2
|
+
|
|
3
|
+
**Audience: an AGENT building a storefront from a blank Next.js app.** These
|
|
4
|
+
docs are the entire knowledge transfer — every call shape, every curl, every
|
|
5
|
+
setting, every component contract. If a storefront can't be built from these
|
|
6
|
+
files alone, the fix is a doc fix, never tribal knowledge.
|
|
7
|
+
|
|
8
|
+
Start at **[BUILD-A-STOREFRONT.md](BUILD-A-STOREFRONT.md)** — the runbook.
|
|
9
|
+
Domain files below are its reference chapters.
|
|
10
|
+
|
|
11
|
+
## Executable-docs contract (docs-truth CI)
|
|
12
|
+
|
|
13
|
+
Docs that can lie aren't bulletproof, so every ```bash block in every file
|
|
14
|
+
here is **extracted and executed** against the real test server by
|
|
15
|
+
`tests/docs/storefront-curls.test.ts`. A drifted doc FAILS the build.
|
|
16
|
+
|
|
17
|
+
Rules for doc authors (agents included):
|
|
18
|
+
|
|
19
|
+
1. All ```bash blocks in one file form ONE script, executed top-to-bottom
|
|
20
|
+
with `bash -euo pipefail`. Later blocks may use variables exported by
|
|
21
|
+
earlier blocks (`CART_ID=$(curl … | grep -o …)`).
|
|
22
|
+
2. The harness pre-exports: `BASE` (test-server origin), `CLIENT_ID` (dev
|
|
23
|
+
tenant id), `PUBLISHABLE_KEY` (the B2B-channel dev key — channel-scoped,
|
|
24
|
+
use only where the doc discusses key scoping).
|
|
25
|
+
3. Every curl uses `-sf` (silent + fail-on-HTTP-error) unless the block
|
|
26
|
+
demonstrates an error case — then capture the status explicitly
|
|
27
|
+
(`-o /dev/null -w '%{http_code}'`) and assert it (`test "$STATUS" = 404`).
|
|
28
|
+
4. Assert shape, not just liveness: pipe to `grep -q '"key"'` (or `node -e`
|
|
29
|
+
for anything structural). A block that checks nothing proves nothing.
|
|
30
|
+
5. A block that must NOT run (illustrative only, external side effects)
|
|
31
|
+
starts with `# doc-noexec` on its first line. Use sparingly — every
|
|
32
|
+
noexec block is a hole in the truth gate.
|
|
33
|
+
6. Blocks must be idempotent-safe on the shared dev tenant: create what you
|
|
34
|
+
read, suffix names with `$RUN` (pre-exported unique stamp), and clean up
|
|
35
|
+
in a final block when you created durable rows.
|
|
36
|
+
|
|
37
|
+
## Per-domain file format
|
|
38
|
+
|
|
39
|
+
One file per domain. For each endpoint, in order:
|
|
40
|
+
|
|
41
|
+
- **Purpose** — one sentence, when a storefront calls it.
|
|
42
|
+
- **Auth** — which headers (anon `x-client-id` / publishable key / Bearer).
|
|
43
|
+
- **Request** — method, path, query/body shape (jsonc block).
|
|
44
|
+
- **Response** — shape (jsonc block), with field notes.
|
|
45
|
+
- **Working curl** — executable per the contract above.
|
|
46
|
+
- **Errors** — status + `code` for every contract-listed failure.
|
|
47
|
+
- **SDK** — the `@cartbase/storefront/api` function that wraps it.
|
|
48
|
+
- **Components** — which `@cartbase/storefront` UI components consume it.
|
|
49
|
+
- **Settings** — admin settings that change its behavior (checkout rules,
|
|
50
|
+
locales, consent, accounts mode…).
|
|
51
|
+
|
|
52
|
+
## Files
|
|
53
|
+
|
|
54
|
+
| File | Domain |
|
|
55
|
+
|---|---|
|
|
56
|
+
| [BUILD-A-STOREFRONT.md](BUILD-A-STOREFRONT.md) | The agent runbook — blank app → completed checkout |
|
|
57
|
+
| [products.md](products.md) | Products, variants, pricing context |
|
|
58
|
+
| [search.md](search.md) | Search, facets, related products |
|
|
59
|
+
| [collections.md](collections.md) | Collections + membership listings |
|
|
60
|
+
| [categories.md](categories.md) | Categories, tags, types |
|
|
61
|
+
| [regions.md](regions.md) | Regions, currencies, locales |
|
|
62
|
+
| [carts.md](carts.md) | Cart lifecycle + line items |
|
|
63
|
+
| [gift-cards.md](gift-cards.md) | Gift-card tender on carts |
|
|
64
|
+
| [checkout.md](checkout.md) | Shipping options, payment providers/collections, prepare-checkout orchestration, complete |
|
|
65
|
+
| [orders.md](orders.md) | Order reads, display-id lookup, transfers |
|
|
66
|
+
| [customers.md](customers.md) | Customer profile, addresses, documents |
|
|
67
|
+
| [subscriptions.md](subscriptions.md) | Subscription portal: schedule control, contract edits, payment-method recovery |
|
|
68
|
+
| [auth.md](auth.md) | Passwordless code login + session discipline |
|
|
69
|
+
| [content.md](content.md) | Pages + blogs |
|
|
70
|
+
| [menus.md](menus.md) | Navigation menus |
|
|
71
|
+
| [metaobjects.md](metaobjects.md) | Merchant-defined content types |
|
|
72
|
+
| [reviews.md](reviews.md) | Review widget, token wizard, photo rewards |
|
|
73
|
+
| [integrations.md](integrations.md) | Store config: carriers, COD, tracking block, lockers |
|
|
74
|
+
| [consent.md](consent.md) | Consent Mode v2 banner config |
|
|
75
|
+
| [redirects.md](redirects.md) | 404-path URL redirects |
|
|
76
|
+
| [components.md](components.md) | UI component families: contracts + required SDK calls |
|
|
77
|
+
| [platform.md](platform.md) | Platform fingerprints: generator meta, window.Cartbase, x-cartbase-version header, cart cookie naming |
|
|
@@ -305,11 +305,11 @@ curl -sf -X DELETE "$BASE/api/store/carts/$CART_ID/line-items/$LINE_ID" \
|
|
|
305
305
|
cart promotion and recomputes totals (automatic promotions layer in on
|
|
306
306
|
their own — only coded ones travel through here).
|
|
307
307
|
- **Auth** — anon `x-client-id`.
|
|
308
|
-
- **Request** — `{promo_codes: string[]}` on BOTH verbs (
|
|
309
|
-
|
|
308
|
+
- **Request** — `{promo_codes: string[]}` on BOTH verbs (the DELETE reads
|
|
309
|
+
its body, not query params).
|
|
310
310
|
- **Response** — `200 {cart}` (decorated; `cart.promotions` is the raw
|
|
311
311
|
pivot embed `[{promotion: {...}}]`). Unknown codes on REMOVE silently
|
|
312
|
-
no-op
|
|
312
|
+
no-op by design; on ADD they error.
|
|
313
313
|
- **Errors** — 404 `cart_not_found`; 404 `promotion_not_found` (unknown
|
|
314
314
|
code on add), 400 `promotion_inactive` (draft/expired code on add), 400
|
|
315
315
|
zod (empty array / non-string entries).
|
|
@@ -35,7 +35,7 @@ While checkout stays mounted: `syncPaymentAmount()` after anything that
|
|
|
35
35
|
changes the total; `refreshPaymentIfTerminal()` from Stripe Elements
|
|
36
36
|
`loaderror` (never proactively).
|
|
37
37
|
|
|
38
|
-
**Manual (
|
|
38
|
+
**Manual (step-by-step, for custom flows):** `updateCart` (address+email) →
|
|
39
39
|
`addShippingMethod` → `createPaymentCollection` → `initiatePaymentSession`
|
|
40
40
|
→ `completeCart`. Both paths are executed against the live server below.
|
|
41
41
|
|
|
@@ -430,7 +430,7 @@ curl -sf -X POST "$BASE/api/store/carts/$CART_ID/refresh-payment-if-terminal" \
|
|
|
430
430
|
|
|
431
431
|
---
|
|
432
432
|
|
|
433
|
-
## Manual path — collections + sessions (
|
|
433
|
+
## Manual path — collections + sessions (step-by-step)
|
|
434
434
|
|
|
435
435
|
### POST /api/store/payment-collections
|
|
436
436
|
|
|
@@ -195,7 +195,7 @@ format.
|
|
|
195
195
|
- `lib/get-product-price` — `getProductPrice({product, variantId})` /
|
|
196
196
|
`getPricesForVariant` → formatted `VariantPrice` (cheapest + selected)
|
|
197
197
|
from server-computed `calculated_price`. Reads Cartbase's flat
|
|
198
|
-
`price_list_type` with
|
|
198
|
+
`price_list_type` with a legacy nested shape as fallback. Affected by:
|
|
199
199
|
price lists / B2B pricing context (what `calculated_price` contains),
|
|
200
200
|
currency + region query context.
|
|
201
201
|
- `lib/get-percentage-diff` — sale badge math.
|
|
@@ -206,11 +206,11 @@ format.
|
|
|
206
206
|
authority for paginated listings.
|
|
207
207
|
- `lib/payment-constants` — `isStripeLike` / `isPaypal` / `isManual` +
|
|
208
208
|
`paymentInfoMap`. Translated to Cartbase ids: matches `pp_stripe` exactly
|
|
209
|
-
(code truth `src/lib/stripe/providers.ts`) plus
|
|
209
|
+
(code truth `src/lib/stripe/providers.ts`) plus legacy provider prefixes;
|
|
210
210
|
`pp_system_default` = COD. Affected by: enabled payment providers +
|
|
211
211
|
checkout rules (which ids ever reach the client).
|
|
212
212
|
- `lib/store-api-error` — `storeApiError(err)`: display-boundary normalizer
|
|
213
|
-
(capitalized message + terminal period)
|
|
213
|
+
(capitalized message + terminal period).
|
|
214
214
|
Catch `StoreApiError` directly instead when branching on `status`/`code`.
|
|
215
215
|
- `lib/hooks/use-intersection`, `lib/hooks/use-toggle-state` — viewport +
|
|
216
216
|
toggle micro-hooks (client).
|
|
@@ -303,7 +303,7 @@ code-first through the error-copy maps, never raw API strings.
|
|
|
303
303
|
`StripeElementsScope` mounts `<Elements mode:"payment">` where needed
|
|
304
304
|
(`passthrough` renders children scope-less on COD-only stores);
|
|
305
305
|
`StripeContext` boolean = "Stripe.js ready".
|
|
306
|
-
- **SDK calls** — none (env: `NEXT_PUBLIC_STRIPE_KEY
|
|
306
|
+
- **SDK calls** — none (env: `NEXT_PUBLIC_STRIPE_KEY`; legacy env names
|
|
307
307
|
kept as fallbacks).
|
|
308
308
|
- **Mount rules** — `PaymentWrapper` wraps the page ONCE with
|
|
309
309
|
`amount={optimisticTotalCents}` (cents at this Stripe boundary only);
|
|
@@ -668,7 +668,7 @@ components read copy only through the context or explicit `labels` props).
|
|
|
668
668
|
`optionsAsKeymap` / `optionsMatch` / `findMatchingVariant` — the
|
|
669
669
|
option-choice → variant resolution extracted from product-actions, reading
|
|
670
670
|
Cartbase's option-value LINK shape (`variant.options[].value.{option_id,value}`)
|
|
671
|
-
with
|
|
671
|
+
with a legacy flat-row fallback. Unit-tested
|
|
672
672
|
(tests/unit/storefront-catalog.test.ts).
|
|
673
673
|
|
|
674
674
|
### `<ImageGallery images />` — `products/image-gallery`
|
|
@@ -861,7 +861,8 @@ search UIs (chips, drawers) that want the same URL contract.
|
|
|
861
861
|
|
|
862
862
|
## Family: order (`@cartbase/storefront/order/*`) — SHIPPED
|
|
863
863
|
|
|
864
|
-
Order confirmation + account order views, production-proven.
|
|
864
|
+
Order confirmation + account order views, production-proven. Some
|
|
865
|
+
platforms ship ONE `StoreOrder` object carrying computed line
|
|
865
866
|
totals, order totals, shipping methods and payments; Cartbase splits those
|
|
866
867
|
across surfaces, so the family takes them as separate props — the Cartbase
|
|
867
868
|
`StoreOrderDetail` (`api/orders`) carries items as version-pivot rows
|
|
@@ -25,7 +25,7 @@ headers the rest of this corpus uses. In practice you'll deploy through
|
|
|
25
25
|
the admin UI (**Storefront** in the sidebar), the Cartbase CLI, or an
|
|
26
26
|
agent connection — all three are wrappers over exactly these calls.
|
|
27
27
|
Users who belong to several stores name the target store with an
|
|
28
|
-
`x-
|
|
28
|
+
`x-cartbase-store: <client-id>` header.
|
|
29
29
|
|
|
30
30
|
## What a deploy is
|
|
31
31
|
|
|
@@ -62,9 +62,9 @@ anyway):
|
|
|
62
62
|
|
|
63
63
|
| Variable | Value |
|
|
64
64
|
|---|---|
|
|
65
|
-
| `
|
|
66
|
-
| `
|
|
67
|
-
| `
|
|
65
|
+
| `NEXT_PUBLIC_CARTBASE_URL` | The store's API origin |
|
|
66
|
+
| `NEXT_PUBLIC_CARTBASE_CLIENT_ID` | The store's client id |
|
|
67
|
+
| `NEXT_PUBLIC_CARTBASE_PUBLISHABLE_KEY` | The store's publishable key, when one exists |
|
|
68
68
|
|
|
69
69
|
Only these public values ever reach a storefront build — secret keys are
|
|
70
70
|
never injected, so code that expects one is a design error.
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# Platform fingerprints — Cartbase detection
|
|
2
|
+
|
|
3
|
+
Every Cartbase-powered storefront emits a small, deliberate set of signals
|
|
4
|
+
so platform-detection tools (Wappalyzer, BuiltWith) classify the site as
|
|
5
|
+
**Ecommerce** — the same mechanism Shopify uses (`window.Shopify`,
|
|
6
|
+
`X-ShopId`, `_shopify_s`). Full spec: `docs/cards/platform-fingerprints.md`.
|
|
7
|
+
Category discipline: these are the ONLY signals emitted — nothing that
|
|
8
|
+
reads as a framework, CMS, or website builder, and NEVER on `/api/admin/*`.
|
|
9
|
+
|
|
10
|
+
SDK modules: `@cartbase/storefront/platform`, `@cartbase/storefront/lib/cookie-names`.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Response header — `x-cartbase-version`
|
|
15
|
+
|
|
16
|
+
- **Purpose** — every `/api/store/*` response carries the platform version,
|
|
17
|
+
so a single HTTP response proves the platform without loading a page.
|
|
18
|
+
- **Auth** — none required to observe the header; it's stamped regardless
|
|
19
|
+
of whether the underlying call succeeds or errors (both `withLogging`
|
|
20
|
+
return paths).
|
|
21
|
+
- **Value** — the platform's own semver (`src/lib/platform/identity.ts` →
|
|
22
|
+
`PLATFORM_VERSION`).
|
|
23
|
+
- **Scope** — `/api/store/*` only. `/api/admin/*` never carries it (admin
|
|
24
|
+
is not a storefront).
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
# A successful call carries the header.
|
|
28
|
+
HEADERS=$(curl -sfD - -o /dev/null "$BASE/api/store/consent" -H "x-client-id: $CLIENT_ID")
|
|
29
|
+
echo "$HEADERS" | grep -qi '^x-cartbase-version: '
|
|
30
|
+
|
|
31
|
+
# An ERROR response (missing x-client-id → 400 missing_client_id) still
|
|
32
|
+
# carries it — the fingerprint is not a happy-path-only afterthought.
|
|
33
|
+
ERR_HEADERS=$(curl -sD - -o /dev/null "$BASE/api/store/consent")
|
|
34
|
+
echo "$ERR_HEADERS" | grep -qi '^x-cartbase-version: '
|
|
35
|
+
|
|
36
|
+
# Admin surface never carries it (even though the same withLogging wrapper
|
|
37
|
+
# runs the admin route too, once auth resolves).
|
|
38
|
+
ADMIN_HEADERS=$(curl -sD - -o /dev/null "$BASE/api/admin/users/me")
|
|
39
|
+
! echo "$ADMIN_HEADERS" | grep -qi '^x-cartbase-version: '
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Meta tag — `<meta name="generator" content="Cartbase" />`
|
|
45
|
+
|
|
46
|
+
- **Purpose** — the classic "what built this site" signal, read by every
|
|
47
|
+
detection tool's HTML scanner.
|
|
48
|
+
- **SDK** — `createStorefrontMetadata(overrides?: Metadata): Metadata`
|
|
49
|
+
(`@cartbase/storefront/platform`). Call it in the root layout instead of
|
|
50
|
+
hand-writing `export const metadata` — `generator` always resolves to
|
|
51
|
+
`PLATFORM_NAME` ("Cartbase") and cannot be shadowed by a stray key in
|
|
52
|
+
`overrides` (the helper's own assignment applies AFTER the spread).
|
|
53
|
+
- **Components** — every storefront's `app/layout.tsx` (see
|
|
54
|
+
`examples/storefront/src/app/layout.tsx` for the reference wiring).
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
// app/layout.tsx
|
|
58
|
+
import { createStorefrontMetadata } from "@cartbase/storefront/platform"
|
|
59
|
+
|
|
60
|
+
export const metadata: Metadata = createStorefrontMetadata({
|
|
61
|
+
title: "My Store",
|
|
62
|
+
description: "...",
|
|
63
|
+
})
|
|
64
|
+
// → renders <meta name="generator" content="Cartbase" /> on every page
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
This is a build-time Next.js Metadata API call, not a store-API endpoint —
|
|
68
|
+
no curl to demonstrate; verified by
|
|
69
|
+
`tests/unit/storefront-lib.test.ts` ("platform — generator metadata +
|
|
70
|
+
window.Cartbase").
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## JS global — `window.Cartbase`
|
|
75
|
+
|
|
76
|
+
- **Purpose** — the strongest detector signal (how `window.Shopify` works):
|
|
77
|
+
a synchronous inline script sets `window.Cartbase = {version, storeId}`
|
|
78
|
+
before hydration.
|
|
79
|
+
- **Shape** — `{ version: string, storeId: string }`. No PII, no email, no
|
|
80
|
+
secret — store id + platform version only.
|
|
81
|
+
- **SDK** — `<PlatformInit storeId={...} />` (`@cartbase/storefront/platform`),
|
|
82
|
+
a server component shaped exactly like `<ConsentInit />`
|
|
83
|
+
(`@cartbase/storefront/tracking/consent-init`): one synchronous inline
|
|
84
|
+
`<script>`, mounted once near the top of `<body>`.
|
|
85
|
+
- **Components** — mount once per app, next to `<ConsentInit />`. Never
|
|
86
|
+
mount in an admin bundle.
|
|
87
|
+
|
|
88
|
+
```tsx
|
|
89
|
+
// app/layout.tsx, inside <body>, first children:
|
|
90
|
+
<ConsentInit />
|
|
91
|
+
<PlatformInit storeId={CARTBASE_CLIENT_ID} />
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Also build-time/render-only — no store-API curl to demonstrate; the exact
|
|
95
|
+
script shape (`window.Cartbase={"version":"...","storeId":"..."};`, no
|
|
96
|
+
extra keys) is verified by `tests/unit/storefront-lib.test.ts`.
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## Cookies — `_cartbase_cart` (was `_barter_cart_id`)
|
|
101
|
+
|
|
102
|
+
- **Purpose** — the cart-id cookie. The app still OWNS *setting* it (the
|
|
103
|
+
SDK never persists the cart — see [carts.md](carts.md)); the library
|
|
104
|
+
owns the NAME so every consumer emits the same fingerprint instead of
|
|
105
|
+
inventing its own prefix.
|
|
106
|
+
- **SDK** — `CART_COOKIE`, `LEGACY_CART_COOKIE`, `readCartCookie(get)`
|
|
107
|
+
(`@cartbase/storefront/lib/cookie-names`). `readCartCookie` prefers the
|
|
108
|
+
new name and falls back to the legacy `_barter_cart_id` name, so an
|
|
109
|
+
existing visitor's cart survives the rename. Every WRITE uses the new
|
|
110
|
+
name only.
|
|
111
|
+
- **`SESSION_COOKIE` (`_cartbase_session`)** — the name is reserved for a
|
|
112
|
+
future cookie-backed customer session. Nothing sets it today: auth is
|
|
113
|
+
pure Bearer-JWT, persisted by the consuming app (see
|
|
114
|
+
[auth.md](auth.md)). Defined now so a future session mechanism launches
|
|
115
|
+
with the fingerprint-correct name.
|
|
116
|
+
|
|
117
|
+
```ts
|
|
118
|
+
import { CART_COOKIE, readCartCookie } from "@cartbase/storefront/lib/cookie-names"
|
|
119
|
+
|
|
120
|
+
const cartId = readCartCookie((name) => jar.get(name)?.value) // reads either name
|
|
121
|
+
jar.set(CART_COOKIE, cart.id, { path: "/", maxAge: THIRTY_DAYS }) // writes the new name only
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
No store-API curl to demonstrate (this is a client-cookie contract, not a
|
|
125
|
+
server response); verified by `tests/unit/storefront-lib.test.ts`
|
|
126
|
+
("platform — cookie names + back-compat read").
|
|
@@ -29,8 +29,8 @@ SDK module: `@cartbase/storefront/api/regions`.
|
|
|
29
29
|
```
|
|
30
30
|
|
|
31
31
|
- **Response** — ordered by name. NOTE: Cartbase regions carry NO embedded
|
|
32
|
-
`countries` array
|
|
33
|
-
|
|
32
|
+
`countries` array — country/tax scope lives server-side in
|
|
33
|
+
`tax_regions`.
|
|
34
34
|
|
|
35
35
|
```jsonc
|
|
36
36
|
{
|
|
@@ -202,6 +202,5 @@ echo "$LOCALES" | grep -q '"en"'
|
|
|
202
202
|
- **Errors** — 400 `missing_client_id`.
|
|
203
203
|
- **SDK** — `listLocales(client)`.
|
|
204
204
|
- **Components** — locale switcher; `StorefrontClient`'s `getLocale` hook.
|
|
205
|
-
- **Settings** — Admin → Settings → Store → locales (
|
|
206
|
-
|
|
207
|
-
global locale catalog).
|
|
205
|
+
- **Settings** — Admin → Settings → Store → locales (each store manages
|
|
206
|
+
its own locale list in `store_locales`).
|
|
@@ -15,12 +15,12 @@ const nextConfig: NextConfig = {
|
|
|
15
15
|
// A real store should allowlist its media domain via remotePatterns.
|
|
16
16
|
unoptimized: true,
|
|
17
17
|
},
|
|
18
|
-
// The
|
|
18
|
+
// The Cartbase store API ships NO CORS headers — browser-side SDK calls
|
|
19
19
|
// must be same-origin. Proxy them through the app origin (the browser
|
|
20
20
|
// client's baseUrl is window.location.origin); server-side SDK calls go
|
|
21
|
-
// straight to
|
|
21
|
+
// straight to NEXT_PUBLIC_CARTBASE_URL and are unaffected.
|
|
22
22
|
async rewrites() {
|
|
23
|
-
const barterUrl = process.env.
|
|
23
|
+
const barterUrl = process.env.NEXT_PUBLIC_CARTBASE_URL
|
|
24
24
|
if (!barterUrl) return []
|
|
25
25
|
return [
|
|
26
26
|
{
|
|
@@ -1,49 +1,51 @@
|
|
|
1
|
-
import { cookies } from "next/headers"
|
|
2
|
-
import { redirect } from "next/navigation"
|
|
3
|
-
import { retrieveCart } from "@cartbase/storefront/api/carts"
|
|
4
|
-
import {
|
|
5
|
-
listPaymentProviders,
|
|
6
|
-
listShippingOptions,
|
|
7
|
-
} from "@cartbase/storefront/api/checkout"
|
|
8
|
-
import { getIntegrationsConfig } from "@cartbase/storefront/api/integrations"
|
|
9
|
-
import {
|
|
10
|
-
import { getServerClient } from "@/lib/server-client"
|
|
11
|
-
import { CheckoutPageClient } from "./checkout-page-client"
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Checkout (runbook step 8 / checkout.md): list shipping options and
|
|
15
|
-
* payment providers WITH `cart_id` (server-side rule filtering), fetch the
|
|
16
|
-
* integrations `cod` block, and hand everything to the orchestrated
|
|
17
|
-
* client layout. Redirect completed/missing carts server-side
|
|
18
|
-
* (checkout-client mount rule).
|
|
19
|
-
*/
|
|
20
|
-
export default async function CheckoutPage() {
|
|
21
|
-
const jar = await cookies()
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
1
|
+
import { cookies } from "next/headers"
|
|
2
|
+
import { redirect } from "next/navigation"
|
|
3
|
+
import { retrieveCart } from "@cartbase/storefront/api/carts"
|
|
4
|
+
import {
|
|
5
|
+
listPaymentProviders,
|
|
6
|
+
listShippingOptions,
|
|
7
|
+
} from "@cartbase/storefront/api/checkout"
|
|
8
|
+
import { getIntegrationsConfig } from "@cartbase/storefront/api/integrations"
|
|
9
|
+
import { readCartCookie } from "@/lib/config"
|
|
10
|
+
import { getServerClient } from "@/lib/server-client"
|
|
11
|
+
import { CheckoutPageClient } from "./checkout-page-client"
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Checkout (runbook step 8 / checkout.md): list shipping options and
|
|
15
|
+
* payment providers WITH `cart_id` (server-side rule filtering), fetch the
|
|
16
|
+
* integrations `cod` block, and hand everything to the orchestrated
|
|
17
|
+
* client layout. Redirect completed/missing carts server-side
|
|
18
|
+
* (checkout-client mount rule).
|
|
19
|
+
*/
|
|
20
|
+
export default async function CheckoutPage() {
|
|
21
|
+
const jar = await cookies()
|
|
22
|
+
// Back-compat: prefer `_cartbase_cart`, fall back to the legacy
|
|
23
|
+
// `_barter_cart_id` name (platform-fingerprints card).
|
|
24
|
+
const cartId = readCartCookie((name) => jar.get(name)?.value)
|
|
25
|
+
if (!cartId) redirect("/")
|
|
26
|
+
|
|
27
|
+
const client = await getServerClient()
|
|
28
|
+
const cart = await retrieveCart(client, cartId)
|
|
29
|
+
.then((res) => res.cart)
|
|
30
|
+
.catch(() => null)
|
|
31
|
+
if (!cart || (cart.items ?? []).length === 0) redirect("/")
|
|
32
|
+
if (cart.completed_at) redirect("/")
|
|
33
|
+
|
|
34
|
+
const [shippingOptions, paymentProviders, integrations] = await Promise.all([
|
|
35
|
+
listShippingOptions(client, { cart_id: cart.id }),
|
|
36
|
+
listPaymentProviders(client, {
|
|
37
|
+
cart_id: cart.id,
|
|
38
|
+
region_id: cart.region_id ?? undefined,
|
|
39
|
+
}),
|
|
40
|
+
getIntegrationsConfig(client),
|
|
41
|
+
])
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<CheckoutPageClient
|
|
45
|
+
cart={cart}
|
|
46
|
+
shippingOptions={shippingOptions.shipping_options}
|
|
47
|
+
paymentProviders={paymentProviders.payment_providers}
|
|
48
|
+
codConfig={integrations.cod}
|
|
49
|
+
/>
|
|
50
|
+
)
|
|
51
|
+
}
|
|
@@ -1,105 +1,113 @@
|
|
|
1
|
-
import type { Metadata } from "next"
|
|
2
|
-
import Link from "next/link"
|
|
3
|
-
import { cookies } from "next/headers"
|
|
4
|
-
import { retrieveCart, type Cart } from "@cartbase/storefront/api/carts"
|
|
5
|
-
import { getConsent } from "@cartbase/storefront/api/consent"
|
|
6
|
-
import { getMenu, type Menu } from "@cartbase/storefront/api/menus"
|
|
7
|
-
import { ConsentInit } from "@cartbase/storefront/tracking/consent-init"
|
|
8
|
-
import { CartButtonClient } from "@cartbase/storefront/common/cart-button-client"
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import "./
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
{item.title}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
1
|
+
import type { Metadata } from "next"
|
|
2
|
+
import Link from "next/link"
|
|
3
|
+
import { cookies } from "next/headers"
|
|
4
|
+
import { retrieveCart, type Cart } from "@cartbase/storefront/api/carts"
|
|
5
|
+
import { getConsent } from "@cartbase/storefront/api/consent"
|
|
6
|
+
import { getMenu, type Menu } from "@cartbase/storefront/api/menus"
|
|
7
|
+
import { ConsentInit } from "@cartbase/storefront/tracking/consent-init"
|
|
8
|
+
import { CartButtonClient } from "@cartbase/storefront/common/cart-button-client"
|
|
9
|
+
import { createStorefrontMetadata, PlatformInit } from "@cartbase/storefront/platform"
|
|
10
|
+
import { BARTER_CLIENT_ID, readCartCookie } from "@/lib/config"
|
|
11
|
+
import { getServerClient } from "@/lib/server-client"
|
|
12
|
+
import { Providers } from "./providers"
|
|
13
|
+
import "./globals.css"
|
|
14
|
+
|
|
15
|
+
// createStorefrontMetadata (platform-fingerprints card) stamps
|
|
16
|
+
// `<meta name="generator" content="Cartbase" />` on every page — never
|
|
17
|
+
// hand-write `generator` here.
|
|
18
|
+
export const metadata: Metadata = createStorefrontMetadata({
|
|
19
|
+
title: "Barter Example Store",
|
|
20
|
+
description: "Reference storefront built on @cartbase/storefront",
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Runbook steps 3–4: bootstrap store config at layout level, then mount
|
|
25
|
+
* order inside <body>: ConsentInit FIRST (static, synchronous), then the
|
|
26
|
+
* consent-gated UI, then navigation. A store with no menus 404s every
|
|
27
|
+
* handle — render no nav, never crash (menus.md).
|
|
28
|
+
*/
|
|
29
|
+
async function fetchMenu(handle: string): Promise<Menu | null> {
|
|
30
|
+
const client = await getServerClient()
|
|
31
|
+
try {
|
|
32
|
+
const { menu } = await getMenu(client, handle)
|
|
33
|
+
return menu
|
|
34
|
+
} catch {
|
|
35
|
+
return null // unknown/deleted handle (404) → graceful "no nav"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function MenuNav({ menu }: { menu: Menu | null }) {
|
|
40
|
+
if (!menu || menu.items.length === 0) return null
|
|
41
|
+
return (
|
|
42
|
+
<nav className="flex items-center gap-4">
|
|
43
|
+
{menu.items.map((item) => (
|
|
44
|
+
<Link
|
|
45
|
+
key={`${item.title}-${item.url}`}
|
|
46
|
+
href={item.url}
|
|
47
|
+
className="text-sm text-muted-foreground hover:text-foreground"
|
|
48
|
+
>
|
|
49
|
+
{item.title}
|
|
50
|
+
</Link>
|
|
51
|
+
))}
|
|
52
|
+
</nav>
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export default async function RootLayout({
|
|
57
|
+
children,
|
|
58
|
+
}: {
|
|
59
|
+
children: React.ReactNode
|
|
60
|
+
}) {
|
|
61
|
+
const client = await getServerClient()
|
|
62
|
+
const jar = await cookies()
|
|
63
|
+
// Back-compat: prefer the new `_cartbase_cart` cookie, fall back to the
|
|
64
|
+
// legacy `_barter_cart_id` name so an existing visitor's cart survives
|
|
65
|
+
// the rename (platform-fingerprints card).
|
|
66
|
+
const cartId = readCartCookie((name) => jar.get(name)?.value) ?? null
|
|
67
|
+
|
|
68
|
+
const [consentRes, mainMenu, footerMenu, cart] = await Promise.all([
|
|
69
|
+
getConsent(client),
|
|
70
|
+
fetchMenu("main-menu"),
|
|
71
|
+
fetchMenu("footer"),
|
|
72
|
+
cartId
|
|
73
|
+
? retrieveCart(client, cartId)
|
|
74
|
+
.then((res): Cart | null => res.cart)
|
|
75
|
+
.catch(() => null) // stale cookie → empty cart state
|
|
76
|
+
: Promise.resolve(null),
|
|
77
|
+
])
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<html lang="en">
|
|
81
|
+
<body>
|
|
82
|
+
<ConsentInit />
|
|
83
|
+
<PlatformInit storeId={BARTER_CLIENT_ID} />
|
|
84
|
+
<Providers cart={cart} consent={consentRes.consent}>
|
|
85
|
+
<header className="border-b border-border">
|
|
86
|
+
<div className="max-w-7xl mx-auto px-4 h-16 flex items-center justify-between gap-6">
|
|
87
|
+
<Link href="/" className="font-semibold text-lg">
|
|
88
|
+
Barter Example Store
|
|
89
|
+
</Link>
|
|
90
|
+
<div className="flex items-center gap-6">
|
|
91
|
+
<MenuNav menu={mainMenu} />
|
|
92
|
+
<Link
|
|
93
|
+
href="/search"
|
|
94
|
+
className="text-sm text-muted-foreground hover:text-foreground"
|
|
95
|
+
>
|
|
96
|
+
Search
|
|
97
|
+
</Link>
|
|
98
|
+
<CartButtonClient cart={cart} />
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
</header>
|
|
102
|
+
<main>{children}</main>
|
|
103
|
+
<footer className="border-t border-border mt-12">
|
|
104
|
+
<div className="max-w-7xl mx-auto px-4 py-8 text-sm text-muted-foreground flex items-center justify-between">
|
|
105
|
+
<span>Barter Example Store</span>
|
|
106
|
+
<MenuNav menu={footerMenu} />
|
|
107
|
+
</div>
|
|
108
|
+
</footer>
|
|
109
|
+
</Providers>
|
|
110
|
+
</body>
|
|
111
|
+
</html>
|
|
112
|
+
)
|
|
113
|
+
}
|
|
@@ -1,43 +1,47 @@
|
|
|
1
|
-
"use server"
|
|
2
|
-
|
|
3
|
-
import { revalidatePath } from "next/cache"
|
|
4
|
-
import { cookies } from "next/headers"
|
|
5
|
-
import { addLineItem, createCart } from "@cartbase/storefront/api/carts"
|
|
6
|
-
import { CART_COOKIE, CART_COOKIE_MAX_AGE } from "./config"
|
|
7
|
-
import { getServerClient } from "./server-client"
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* PDP add-to-cart server action (carts.md: create the cart lazily on the
|
|
11
|
-
* first add — region falls back to the store default; persist `cart.id`
|
|
12
|
-
* in a cookie). Passed into `ProductTemplate`'s `addToCart` seam.
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
1
|
+
"use server"
|
|
2
|
+
|
|
3
|
+
import { revalidatePath } from "next/cache"
|
|
4
|
+
import { cookies } from "next/headers"
|
|
5
|
+
import { addLineItem, createCart } from "@cartbase/storefront/api/carts"
|
|
6
|
+
import { CART_COOKIE, CART_COOKIE_MAX_AGE, readCartCookie } from "./config"
|
|
7
|
+
import { getServerClient } from "./server-client"
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* PDP add-to-cart server action (carts.md: create the cart lazily on the
|
|
11
|
+
* first add — region falls back to the store default; persist `cart.id`
|
|
12
|
+
* in a cookie). Passed into `ProductTemplate`'s `addToCart` seam.
|
|
13
|
+
*
|
|
14
|
+
* Reads via `readCartCookie` (new `_cartbase_cart` name, falls back to the
|
|
15
|
+
* legacy `_barter_cart_id` — platform-fingerprints card); every WRITE uses
|
|
16
|
+
* the new name only, so a visitor's cart survives the rename.
|
|
17
|
+
*/
|
|
18
|
+
export async function addToCartAction(input: {
|
|
19
|
+
variantId: string
|
|
20
|
+
quantity: number
|
|
21
|
+
}): Promise<void> {
|
|
22
|
+
const client = await getServerClient()
|
|
23
|
+
const jar = await cookies()
|
|
24
|
+
const cartId = readCartCookie((name) => jar.get(name)?.value)
|
|
25
|
+
|
|
26
|
+
if (cartId) {
|
|
27
|
+
await addLineItem(client, cartId, {
|
|
28
|
+
variant_id: input.variantId,
|
|
29
|
+
quantity: input.quantity,
|
|
30
|
+
})
|
|
31
|
+
} else {
|
|
32
|
+
const { cart } = await createCart(client, {
|
|
33
|
+
currency_code: "eur",
|
|
34
|
+
items: [{ variant_id: input.variantId, quantity: input.quantity }],
|
|
35
|
+
})
|
|
36
|
+
jar.set(CART_COOKIE, cart.id, {
|
|
37
|
+
path: "/",
|
|
38
|
+
sameSite: "lax",
|
|
39
|
+
httpOnly: false, // the browser-side drawer persists the same cookie
|
|
40
|
+
maxAge: CART_COOKIE_MAX_AGE,
|
|
41
|
+
})
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Layout refetches the cart snapshot → CartDrawerProvider sees the new
|
|
45
|
+
// product-line count and auto-opens the drawer.
|
|
46
|
+
revalidatePath("/", "layout")
|
|
47
|
+
}
|
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
/** Shared storefront configuration (BUILD-A-STOREFRONT.md — inputs table). */
|
|
2
2
|
|
|
3
|
-
export const BARTER_URL = process.env.
|
|
4
|
-
export const BARTER_CLIENT_ID = process.env.
|
|
3
|
+
export const BARTER_URL = process.env.NEXT_PUBLIC_CARTBASE_URL ?? ""
|
|
4
|
+
export const BARTER_CLIENT_ID = process.env.NEXT_PUBLIC_CARTBASE_CLIENT_ID ?? ""
|
|
5
5
|
export const BARTER_PUBLISHABLE_KEY =
|
|
6
|
-
process.env.
|
|
6
|
+
process.env.NEXT_PUBLIC_CARTBASE_PUBLISHABLE_KEY || undefined
|
|
7
7
|
|
|
8
|
-
/**
|
|
9
|
-
|
|
8
|
+
/**
|
|
9
|
+
* Cart-id cookie — owned by the app (the SDK never persists the cart);
|
|
10
|
+
* the NAME is owned by the library so every consumer emits the same wire
|
|
11
|
+
* fingerprint (platform-fingerprints card). `readCartCookie` is the
|
|
12
|
+
* backward-compat reader for the legacy `_barter_cart_id` name.
|
|
13
|
+
*/
|
|
14
|
+
export { CART_COOKIE, readCartCookie } from "@cartbase/storefront/lib/cookie-names"
|
|
10
15
|
export const CART_COOKIE_MAX_AGE = 60 * 60 * 24 * 30 // 30 days
|
|
11
16
|
|
|
12
17
|
/** Locale cookie read by the clients' `getLocale`. */
|