create-cartbase 0.1.5 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/README.md +13 -8
  2. package/dist/index.js +28 -20
  3. package/package.json +3 -3
  4. package/template/app/AGENTS.md +29 -0
  5. package/template/app/CLAUDE.md +19 -8
  6. package/template/app/docs/BUILD-A-STOREFRONT.md +233 -226
  7. package/template/app/docs/README.md +1 -0
  8. package/template/app/docs/components.md +1134 -1090
  9. package/template/app/docs/deploy.md +15 -10
  10. package/template/app/docs/integrations.md +15 -3
  11. package/template/app/docs/platform.md +1 -1
  12. package/template/app/docs/store.md +47 -0
  13. package/template/app/docs/variables.md +3 -3
  14. package/template/app/next.config.ts +10 -14
  15. package/template/app/package.json +4 -3
  16. package/template/app/src/app/checkout/checkout-page-client.tsx +0 -20
  17. package/template/app/src/app/globals.css +1 -1
  18. package/template/app/src/app/layout.tsx +73 -18
  19. package/template/app/src/app/order/[id]/confirmed/page.tsx +92 -78
  20. package/template/app/src/lib/config.ts +30 -21
  21. package/template/app/next-env.d.ts +0 -6
  22. package/template/app/smoke.mjs +0 -158
  23. package/template/app/src/app/checkout/mypos-demo-tab.tsx +0 -101
  24. package/template/app/src/app/gallery/[slug]/page.tsx +0 -172
  25. package/template/app/src/app/gallery/_components/specimens.tsx +0 -254
  26. package/template/app/src/app/gallery/_components/status.tsx +0 -47
  27. package/template/app/src/app/gallery/_lib/catalog.ts +0 -59
  28. package/template/app/src/app/gallery/_lib/registry.ts +0 -401
  29. package/template/app/src/app/gallery/design-system/page.tsx +0 -353
  30. package/template/app/src/app/gallery/layout.tsx +0 -83
  31. package/template/app/src/app/gallery/page.tsx +0 -81
  32. package/template/app/tsconfig.tsbuildinfo +0 -1
@@ -5,7 +5,10 @@ Cartbase builds and hosts your storefront. You send the app's source files
5
5
  running site: first on a **permanent preview URL**, then, when you press
6
6
  **Publish** (or call the publish endpoint), on the store's live domain.
7
7
  You never touch build servers, DNS, or hosting configuration; the platform
8
- provisions all of it on your first deploy.
8
+ provisions all of it the moment the store is created, and makes the first
9
+ deploy itself: the store's starting point, a complete storefront on your
10
+ own (still empty) catalog, already running on the preview link. Pull it
11
+ with the CLI, change anything, deploy it back.
9
12
 
10
13
  Two URLs exist per store, both created automatically:
11
14
 
@@ -56,24 +59,25 @@ Error codes you can hit: `empty_bundle`, `bundle_too_large`,
56
59
 
57
60
  ## Environment — provided, not configured
58
61
 
59
- Hosted storefronts receive the three runbook inputs automatically at
60
- build time; do **not** put them in the bundle (`.env*` files are blocked
62
+ Hosted storefronts receive their store's values automatically at build
63
+ time; do **not** put them in the bundle (`.env*` files are blocked
61
64
  anyway):
62
65
 
63
66
  | Variable | Value |
64
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
+ | `NEXT_PUBLIC_CARTBASE_PUBLISHABLE_KEY` | The store's publishable key, the one input a storefront needs |
69
+ | `NEXT_PUBLIC_CARTBASE_URL` | The platform origin (a constant; injected so a hosted build never guesses) |
70
+ | `NEXT_PUBLIC_CARTBASE_CLIENT_ID` | The store's id, the platform's own door; a storefront never needs to copy it |
68
71
 
69
72
  Only these public values ever reach a storefront build — secret keys are
70
73
  never injected, so code that expects one is a design error.
71
74
 
72
75
  ## Deploy to preview
73
76
 
74
- `POST /api/admin/storefront/deploys` — the one ingestion door. The first
75
- call on a store also provisions its hosting (takes a few extra seconds);
76
- every later call is just a deploy.
77
+ `POST /api/admin/storefront/deploys` — the one ingestion door. Hosting is
78
+ provisioned when the store is born, so every call is just a deploy; on a
79
+ store whose hosting setup failed, the first call resumes it (a few extra
80
+ seconds).
77
81
 
78
82
  ```bash
79
83
  # doc-noexec — admin-session auth; run from an authenticated context.
@@ -153,7 +157,8 @@ curl -s "$BASE/api/admin/storefront"
153
157
  }
154
158
  ```
155
159
 
156
- `storefront` is `null` until the store's first deploy. `status:
160
+ `storefront` is present from the store's birth (`null` only on a store
161
+ created before hosting-at-birth that has never deployed). `status:
157
162
  "provisioning"` / `"failed"` (with `last_error`) describe hosting setup,
158
163
  not builds; a failed provisioning resumes automatically on the next
159
164
  deploy attempt.
@@ -56,6 +56,10 @@ key-by-key.
56
56
  "ga4": { "measurementId": "G-…" },
57
57
  "klaviyo": { "publicKey": "…" },
58
58
  "googleAds": { "conversionId": "AW-…", "conversionLabel": "…" },
59
+ // Pixel id only. The TikTok Events API token stays on the platform
60
+ // and is used by the order.placed forwarder, exactly like Meta's
61
+ // CAPI token, which has never appeared on this endpoint.
62
+ "tiktok": { "pixelId": "C…" },
59
63
  "consent_required": true // always present — mirrors the consent CMP's enabled
60
64
  }
61
65
  }
@@ -64,7 +68,10 @@ key-by-key.
64
68
  - **Errors**: `400 missing_client_id` · `400 invalid_publishable_key`.
65
69
  - **SDK**: `integrations.getIntegrationsConfig(client)`
66
70
  - **Components**: carrier/locker pickers, COD fee row in checkout, tracking
67
- mounts (`<MetaPixel>/<GA4>/<Gtm>` behind the consent gate).
71
+ mounts (`<MetaPixel>/<GoogleTag>/<TikTokPixel>/<Gtm>` behind the consent
72
+ gate). `<GoogleTag measurementId adsConversionId>` takes BOTH Google
73
+ destinations: one loader, two `config` calls, which is Google's own
74
+ instruction and is what lets Consent Mode gate the Ads tag for free.
68
75
  - **Settings**: admin → Settings → Integrations (per-provider enable +
69
76
  config); consent settings drive `tracking.consent_required`; Rybbit is
70
77
  deliberately ABSENT (platform analytics, not a tenant integration).
@@ -72,8 +79,13 @@ key-by-key.
72
79
  Tracking wiring contract: mount tags only through the consent gate when
73
80
  `consent_required` (see [consent.md](consent.md)); Purchase events MUST use
74
81
  `eventID = "purchase_" + order.display_id` so Meta dedupes browser Pixel vs
75
- server CAPI; write TrackingAttribution keys into `cart.metadata`
76
- (consent-gated) so server events inherit fbp/fbc/anon-id/ga signals.
82
+ server CAPI, and `event_id = "tt_purchase_" + order.display_id` for TikTok;
83
+ write TrackingAttribution keys into `cart.metadata` (consent-gated) so
84
+ server events inherit fbp/fbc/anon-id/ga signals AND the ad-click ids
85
+ (`tt_ttclid`, `tt_ttp`, `google_gclid`, `google_gbraid`, `google_wbraid`)
86
+ that let TikTok and Google tie a conversion back to the click that caused
87
+ it. Capture those with `captureClickIdsFromUrl()` beside
88
+ `captureUtmsFromUrl()`.
77
89
 
78
90
  ```bash
79
91
  BODY=$(curl -sf "$BASE/api/store/integrations" -H "x-client-id: $CLIENT_ID")
@@ -88,7 +88,7 @@ window.Cartbase").
88
88
  ```tsx
89
89
  // app/layout.tsx, inside <body>, first children:
90
90
  <ConsentInit />
91
- <PlatformInit storeId={CARTBASE_CLIENT_ID} />
91
+ <PlatformInit storeId={store.id} /> // from GET /api/store/store (store.md)
92
92
  ```
93
93
 
94
94
  Also build-time/render-only — no store-API curl to demonstrate; the exact
@@ -0,0 +1,47 @@
1
+ # Store
2
+
3
+ The store's own identity: its name, its slug and its brand (what the
4
+ merchant set under Settings → Brand). One anonymous read, usually at
5
+ layout level, so a storefront titles, heads and foots itself with the
6
+ merchant's name and never hardcodes one. Logo URLs are public. Colors are
7
+ `#rrggbb`. Every field except `name` and `slug` may be null.
8
+
9
+ SDK module: `@cartbase/storefront/api/store` (from 0.8.0; on 0.7.0 call
10
+ `client.request("/api/store/store")` with the same shape).
11
+
12
+ ---
13
+
14
+ ## GET /api/store/store
15
+
16
+ - **Purpose** — read the store's id, name, slug and brand.
17
+ - **Auth** — anon: the store's publishable key (`x-publishable-api-key`).
18
+ - **Request** — `GET /api/store/store`
19
+ - **Response 200**
20
+
21
+ ```jsonc
22
+ {
23
+ "store": {
24
+ "id": "1e7a4c02-9b31-4f7e-8d2a-5c6f90ab12cd", // the store's public id (what PlatformInit mounts)
25
+ "name": "Demo Store",
26
+ "slug": "demo-store",
27
+ "brand": {
28
+ "logo_url": "https://…/brand/…/logo.png", // or null
29
+ "logo_square_url": null,
30
+ "color_primary": "#111111", // or null
31
+ "color_secondary": null,
32
+ "slogan": null
33
+ }
34
+ }
35
+ }
36
+ ```
37
+
38
+ - **Errors** — `404 store_not_found` when the key names no live store.
39
+
40
+ ```ts
41
+ import { getStore } from "@cartbase/storefront/api/store"
42
+
43
+ export async function generateMetadata() {
44
+ const { store } = await getStore(await getServerClient())
45
+ return createStorefrontMetadata({ title: store.name, description: store.brand.slogan ?? undefined })
46
+ }
47
+ ```
@@ -58,7 +58,7 @@ Every customer notification carries it. Money is formatted in the order's own cu
58
58
  | `{{ order.email }}` | The address the order was placed with. | email | |
59
59
  | `{{ order.date }}` | When the order was placed. | date | |
60
60
  | `{{ order.status }}` | Lifecycle state: pending, completed, canceled or archived. | text | |
61
- | `{{ order.payment_status }}` | Paid, Awaiting payment, Refunded and so on, in the same words the admin uses. | text | |
61
+ | `{{ order.payment_status }}` | Paid, Unpaid, Partially paid, Refunded and so on, in the same words the admin uses. | text | |
62
62
  | `{{ order.fulfillment_status }}` | Unfulfilled, Shipped, Delivered and so on, in the same words the admin uses. | text | |
63
63
  | `{{ order.locale }}` | Language the customer shopped in, when the storefront sends one. | text | |
64
64
  | `{{ order.cancelled_at }}` | When the order was cancelled, if it was. | datetime | |
@@ -70,7 +70,7 @@ Every customer notification carries it. Money is formatted in the order's own cu
70
70
  | `{{ order.shipping }}` | What delivery cost the customer. | money | |
71
71
  | `{{ order.discount }}` | Total discount applied to the order. | money | |
72
72
  | `{{ order.payment_method_fee }}` | The fee of the payment method the customer chose (a COD courier fee, a handling fee on a manual method). | money | |
73
- | `{{ order.paid }}` | How much has actually been captured so far. | money | |
73
+ | `{{ order.paid }}` | How much has actually been received so far. | money | |
74
74
  | `{{ order.refunded }}` | How much has been refunded. | money | |
75
75
  | `{{ order.balance_due }}` | What is still outstanding on the order. | money | |
76
76
 
@@ -271,7 +271,7 @@ Staff operational alerts only.
271
271
  | Order Shipped | Order, Customer, Shipping address, Billing address, Delivery, Store, Brand |
272
272
  | Order Delivered | Order, Customer, Shipping address, Billing address, Delivery, Store, Brand |
273
273
  | Order Refunded | Order, Customer, Shipping address, Billing address, Delivery, Store, Brand |
274
- | Order Cancelled | Order, Customer, Shipping address, Billing address, Delivery, Store, Brand |
274
+ | Order Canceled | Order, Customer, Shipping address, Billing address, Delivery, Store, Brand |
275
275
  | Admin: New Order | Order, Customer, Shipping address, Billing address, Delivery, Store, Brand |
276
276
  | Welcome Email | Customer, Store, Brand |
277
277
  | Password Reset | Customer, Store, Brand |
@@ -1,31 +1,27 @@
1
- import path from "node:path"
2
1
  import type { NextConfig } from "next"
3
2
 
4
3
  const nextConfig: NextConfig = {
5
- // Monorepo: pin the workspace root (Next otherwise infers it from a
6
- // stray lockfile OUTSIDE the repo and mis-roots the dev module graph).
7
- outputFileTracingRoot: path.join(__dirname, "../.."),
8
- // The smoke driver browses via 127.0.0.1; allow dev-resource access.
9
- allowedDevOrigins: ["127.0.0.1"],
10
- // @cartbase/storefront is source-shipped TypeScript (workspace package) —
11
- // the app's Next build transpiles it.
4
+ // @cartbase/storefront is source-shipped TypeScript the app's Next
5
+ // build transpiles it.
12
6
  transpilePackages: ["@cartbase/storefront"],
13
7
  images: {
14
- // Reference app: seeded product images live on arbitrary demo hosts.
15
- // A real store should allowlist its media domain via remotePatterns.
8
+ // Product images come from the Cartbase media CDN and, until you
9
+ // allowlist your own hosts under remotePatterns, from anywhere the
10
+ // catalog points. Tighten this when the media domain is known.
16
11
  unoptimized: true,
17
12
  },
18
13
  // The Cartbase store API ships NO CORS headers — browser-side SDK calls
19
14
  // must be same-origin. Proxy them through the app origin (the browser
20
15
  // client's baseUrl is window.location.origin); server-side SDK calls go
21
- // straight to NEXT_PUBLIC_CARTBASE_URL and are unaffected.
16
+ // straight to the platform origin and are unaffected. The origin is the
17
+ // platform's constant; NEXT_PUBLIC_CARTBASE_URL only overrides it for a
18
+ // local or staging platform.
22
19
  async rewrites() {
23
- const barterUrl = process.env.NEXT_PUBLIC_CARTBASE_URL
24
- if (!barterUrl) return []
20
+ const cartbaseUrl = process.env.NEXT_PUBLIC_CARTBASE_URL || "https://admin.cartbase.ai"
25
21
  return [
26
22
  {
27
23
  source: "/api/store/:path*",
28
- destination: `${barterUrl}/api/store/:path*`,
24
+ destination: `${cartbaseUrl}/api/store/:path*`,
29
25
  },
30
26
  ]
31
27
  },
@@ -2,13 +2,14 @@
2
2
  "name": "cartbase-storefront",
3
3
  "version": "0.1.0",
4
4
  "private": true,
5
- "description": "Reference storefront built from docs/storefront/BUILD-A-STOREFRONT.md alone — the adoption proof for @cartbase/storefront.",
6
5
  "scripts": {
7
- "dev": "next dev --webpack -p 4778",
6
+ "build": "next build",
7
+ "start": "next start",
8
+ "dev": "next dev",
8
9
  "typecheck": "tsc --noEmit"
9
10
  },
10
11
  "dependencies": {
11
- "@cartbase/storefront": "^0.6.0",
12
+ "@cartbase/storefront": "^0.8.0",
12
13
  "next": "16.2.4",
13
14
  "react": "19.2.4",
14
15
  "react-dom": "19.2.4"
@@ -7,7 +7,6 @@ import type { StorePaymentEntry } from "@cartbase/storefront/api/checkout"
7
7
  import { CheckoutProvider } from "@cartbase/storefront/checkout/context"
8
8
  import { CheckoutClient } from "@cartbase/storefront/checkout/checkout-client"
9
9
  import { browserClient } from "@/lib/browser-client"
10
- import { MyposDemoTab } from "./mypos-demo-tab"
11
10
 
12
11
  /** sessionStorage key the confirmation page reads (guests have no order
13
12
  * read endpoint — the completeCart() response is the only order handle,
@@ -42,25 +41,6 @@ export function CheckoutPageClient({
42
41
  paymentMethodFilter={(methods) =>
43
42
  methods?.filter((m) => "payment_method_id" in m) ?? null
44
43
  }
45
- // Provider-research demo (2026-08-04): the myPOS sandbox as a third
46
- // radio card, so the embedded-provider seam can be judged inside
47
- // the real checkout. Off unless NEXT_PUBLIC_MYPOS_DEMO=1.
48
- extraPaymentTabs={
49
- process.env.NEXT_PUBLIC_MYPOS_DEMO === "1"
50
- ? [
51
- {
52
- id: "mypos-demo",
53
- label: "Card via myPOS (demo)",
54
- content: (
55
- <MyposDemoTab
56
- amount={Number(cart.total) || 23.45}
57
- currency={cart.currency_code}
58
- />
59
- ),
60
- },
61
- ]
62
- : undefined
63
- }
64
44
  onOrderPlaced={(order) => {
65
45
  // Guest order handle = the completeCart() response (orders.md).
66
46
  // Stash it (+ the decorated cart lines for the items list) for
@@ -17,7 +17,7 @@
17
17
  * to scan it to know which classes exist. Without this the components render
18
18
  * with no styles at all — the classes are in the markup but never generated.
19
19
  */
20
- @source "../../../../packages/storefront/src";
20
+ @source "../../node_modules/@cartbase/storefront/src";
21
21
 
22
22
  /*
23
23
  * Dark mode is class-based: put `.dark` on <html> and every token swaps.
@@ -5,6 +5,8 @@ import { retrieveCart, type Cart } from "@cartbase/storefront/api/carts"
5
5
  import { getConsent } from "@cartbase/storefront/api/consent"
6
6
  import { getMenu, type Menu } from "@cartbase/storefront/api/menus"
7
7
  import { ConsentInit } from "@cartbase/storefront/tracking/consent-init"
8
+ import { StorefrontTags } from "@cartbase/storefront/tracking/storefront-tags"
9
+ import { TrackInit } from "@cartbase/storefront/tracking/track-init"
8
10
  import { CartButtonClient } from "@cartbase/storefront/common/cart-button-client"
9
11
  import { createStorefrontMetadata, PlatformInit } from "@cartbase/storefront/platform"
10
12
  import { BARTER_CLIENT_ID, readCartCookie } from "@/lib/config"
@@ -15,10 +17,56 @@ import "./globals.css"
15
17
  // createStorefrontMetadata (platform-fingerprints card) stamps
16
18
  // `<meta name="generator" content="Cartbase" />` on every page — never
17
19
  // hand-write `generator` here.
18
- export const metadata: Metadata = createStorefrontMetadata({
19
- title: "Barter Example Store",
20
- description: "Reference storefront built on @cartbase/storefront",
21
- })
20
+ export async function generateMetadata(): Promise<Metadata> {
21
+ const store = await fetchStore()
22
+ return createStorefrontMetadata({
23
+ title: store.name,
24
+ description: store.brand.slogan ?? undefined,
25
+ })
26
+ }
27
+
28
+ /**
29
+ * The store's own identity (store.md): name, slug, brand. Read once per
30
+ * request; a storefront never hardcodes its name. Until @cartbase/storefront
31
+ * 0.8.0 ships `api/store`, the call goes through the client's low-level
32
+ * request with the documented shape.
33
+ */
34
+ type StoreIdentity = {
35
+ /** The store's public id: what PlatformInit mounts (one key: never an env input). */
36
+ id: string
37
+ name: string
38
+ slug: string
39
+ brand: {
40
+ logo_url: string | null
41
+ logo_square_url: string | null
42
+ color_primary: string | null
43
+ color_secondary: string | null
44
+ slogan: string | null
45
+ }
46
+ }
47
+
48
+ async function fetchStore(): Promise<StoreIdentity> {
49
+ const client = await getServerClient()
50
+ try {
51
+ const { store } = await client.request<{ store: StoreIdentity }>("/api/store/store")
52
+ return store
53
+ } catch {
54
+ // Never crash the layout on a transient API error; the name is
55
+ // decoration, the store still renders.
56
+ return {
57
+ id: BARTER_CLIENT_ID ?? "",
58
+ name: "Store",
59
+ slug: "",
60
+ brand: {
61
+ logo_url: null,
62
+ logo_square_url: null,
63
+ color_primary: null,
64
+ color_secondary: null,
65
+ slogan: null,
66
+ },
67
+ }
68
+ }
69
+ }
22
70
 
23
71
  /**
24
72
  * Runbook steps 3–4: bootstrap store config at layout level, then mount
@@ -65,7 +113,8 @@ export default async function RootLayout({
65
113
  // the rename (platform-fingerprints card).
66
114
  const cartId = readCartCookie((name) => jar.get(name)?.value) ?? null
67
115
 
68
- const [consentRes, mainMenu, footerMenu, cart] = await Promise.all([
116
+ const [store, consentRes, mainMenu, footerMenu, cart] = await Promise.all([
117
+ fetchStore(),
69
118
  getConsent(client),
70
119
  fetchMenu("main-menu"),
71
120
  fetchMenu("footer"),
@@ -80,12 +129,24 @@ export default async function RootLayout({
80
129
  <html lang="en">
81
130
  <body>
82
131
  <ConsentInit />
83
- <PlatformInit storeId={BARTER_CLIENT_ID} />
132
+ {/* Every marketing tag the store configured in the admin, mounted
133
+ from its own config: Meta, TikTok, Google (GA4 + Ads on one
134
+ tag), GTM. Nothing to wire per vendor — saving the ids in
135
+ Settings → Integrations is the whole merchant-side act. Order
136
+ matters: ConsentInit sets the Consent Mode defaults
137
+ synchronously ABOVE this, so every tag below inherits the
138
+ gate. */}
139
+ <StorefrontTags client={client} />
140
+ {/* Captures UTMs and the ad-click ids (ttclid / gclid / gbraid /
141
+ wbraid) that exist ONLY on an ad's landing URL — miss them
142
+ here and no platform can attribute the order to the click. */}
143
+ <TrackInit />
144
+ <PlatformInit storeId={store.id} />
84
145
  <Providers cart={cart} consent={consentRes.consent}>
85
146
  <header className="border-b border-border">
86
147
  <div className="max-w-7xl mx-auto px-4 h-16 flex items-center justify-between gap-6">
87
148
  <Link href="/" className="font-semibold text-lg">
88
- Barter Example Store
149
+ {store.name}
89
150
  </Link>
90
151
  <div className="flex items-center gap-6">
91
152
  <MenuNav menu={mainMenu} />
@@ -96,17 +157,11 @@ export default async function RootLayout({
96
157
  Search
97
158
  </Link>
98
159
  {/*
99
- The workbench, reachable from the shop rather than by
100
- knowing the URL. This app is the reference store AND the
101
- place the library gets designed; the link is what makes the
102
- second half discoverable.
160
+ The component workbench lives at /gallery in THIS app only
161
+ (the seed excludes it, src/lib/storefront/seed.ts), so the
162
+ shop's own nav never links to it: a merchant's store must
163
+ not carry a dead link.
103
164
  */}
104
- <Link
105
- href="/gallery"
106
- className="text-sm text-muted-foreground hover:text-foreground"
107
- >
108
- Library
109
- </Link>
110
165
  <CartButtonClient cart={cart} />
111
166
  </div>
112
167
  </div>
@@ -114,7 +169,7 @@ export default async function RootLayout({
114
169
  <main>{children}</main>
115
170
  <footer className="border-t border-border mt-12">
116
171
  <div className="max-w-7xl mx-auto px-4 py-8 text-sm text-muted-foreground flex items-center justify-between">
117
- <span>Barter Example Store</span>
172
+ <span>{store.name}</span>
118
173
  <MenuNav menu={footerMenu} />
119
174
  </div>
120
175
  </footer>
@@ -1,78 +1,92 @@
1
- "use client"
2
-
3
- import { useEffect, useState } from "react"
4
- import Link from "next/link"
5
- import type { CompletedOrder, CartLineItem } from "@cartbase/storefront/api/carts"
6
- import {
7
- OrderCompletedTemplate,
8
- } from "@cartbase/storefront/order/order-completed-template"
9
- import {
10
- displayItemFromCartLine,
11
- } from "@cartbase/storefront/order/order-item"
12
- import {
13
- orderTotalsFromSummary,
14
- } from "@cartbase/storefront/order/order-totals"
15
- import { LAST_ORDER_STORAGE_KEY } from "../../../checkout/checkout-page-client"
16
-
17
- /**
18
- * Order confirmation (runbook step 8.6 + orders.md): guests have NO
19
- * anonymous order read — the `completeCart()` response is the only order
20
- * handle, so the checkout page stashes it in sessionStorage and this page
21
- * renders it through the order family. Totals come from the order's
22
- * summary snapshot (`orderTotalsFromSummary`); items keep the decorated
23
- * cart lines' server-computed totals (`displayItemFromCartLine`).
24
- */
25
- type Stash = { order: CompletedOrder; cartItems: CartLineItem[] }
26
-
27
- export default function OrderConfirmedPage() {
28
- const [stash, setStash] = useState<Stash | null | "missing">(null)
29
-
30
- useEffect(() => {
31
- try {
32
- const raw = sessionStorage.getItem(LAST_ORDER_STORAGE_KEY)
33
- setStash(raw ? (JSON.parse(raw) as Stash) : "missing")
34
- } catch {
35
- setStash("missing")
36
- }
37
- }, [])
38
-
39
- if (stash === null) return null // first paint before sessionStorage read
40
-
41
- if (stash === "missing") {
42
- return (
43
- <div className="max-w-xl mx-auto px-4 py-16 text-center">
44
- <h1 className="text-xl font-semibold mb-2">Order placed</h1>
45
- <p className="text-muted-foreground mb-6">
46
- Your order was placed successfully. A confirmation email is on its
47
- way.
48
- </p>
49
- <Link href="/" className="underline">
50
- Continue shopping
51
- </Link>
52
- </div>
53
- )
54
- }
55
-
56
- const { order, cartItems } = stash
57
- const totals = orderTotalsFromSummary(order.summary) ?? {
58
- total: null,
59
- }
60
- // The completeCart() order carries FLATTENED items (checkout.md), not the
61
- // order-detail pivot shape the template's default converter expects —
62
- // pass the normalized cart-line items instead and drop `order.items`.
63
- const orderForTemplate = { ...order, items: undefined }
64
-
65
- return (
66
- <OrderCompletedTemplate
67
- order={orderForTemplate}
68
- totals={totals}
69
- items={cartItems.map(displayItemFromCartLine)}
70
- // This reference store checks out via one merchant method (pp_* kill:
71
- // methods are merchant-named, provider-less); a multi-tender store
72
- // should stash the chosen tender from its checkout state alongside
73
- // the order.
74
- paymentMethodName={"Cash on delivery"}
75
- storeHref="/"
76
- />
77
- )
78
- }
1
+ "use client"
2
+
3
+ import { useEffect, useState } from "react"
4
+ import Link from "next/link"
5
+ import type { CompletedOrder, CartLineItem } from "@cartbase/storefront/api/carts"
6
+ import {
7
+ OrderCompletedTemplate,
8
+ } from "@cartbase/storefront/order/order-completed-template"
9
+ import {
10
+ displayItemFromCartLine,
11
+ } from "@cartbase/storefront/order/order-item"
12
+ import {
13
+ orderTotalsFromSummary,
14
+ } from "@cartbase/storefront/order/order-totals"
15
+ import { TrackOrderPurchase } from "@cartbase/storefront/tracking/track-order-purchase"
16
+ import { useTrackingConfig } from "@cartbase/storefront/tracking/use-tracking-config"
17
+ import { browserClient } from "@/lib/browser-client"
18
+ import { LAST_ORDER_STORAGE_KEY } from "../../../checkout/checkout-page-client"
19
+
20
+ /**
21
+ * Order confirmation (runbook step 8.6 + orders.md): guests have NO
22
+ * anonymous order read — the `completeCart()` response is the only order
23
+ * handle, so the checkout page stashes it in sessionStorage and this page
24
+ * renders it through the order family. Totals come from the order's
25
+ * summary snapshot (`orderTotalsFromSummary`); items keep the decorated
26
+ * cart lines' server-computed totals (`displayItemFromCartLine`).
27
+ */
28
+ type Stash = { order: CompletedOrder; cartItems: CartLineItem[] }
29
+
30
+ export default function OrderConfirmedPage() {
31
+ const [stash, setStash] = useState<Stash | null | "missing">(null)
32
+ // The store's public tag config — Google Ads needs the account id
33
+ // and the purchase label from it, and null means the conversion is
34
+ // skipped rather than fired malformed.
35
+ const tracking = useTrackingConfig(browserClient)
36
+
37
+ useEffect(() => {
38
+ try {
39
+ const raw = sessionStorage.getItem(LAST_ORDER_STORAGE_KEY)
40
+ setStash(raw ? (JSON.parse(raw) as Stash) : "missing")
41
+ } catch {
42
+ setStash("missing")
43
+ }
44
+ }, [])
45
+
46
+ if (stash === null) return null // first paint before sessionStorage read
47
+
48
+ if (stash === "missing") {
49
+ return (
50
+ <div className="max-w-xl mx-auto px-4 py-16 text-center">
51
+ <h1 className="text-xl font-semibold mb-2">Order placed</h1>
52
+ <p className="text-muted-foreground mb-6">
53
+ Your order was placed successfully. A confirmation email is on its
54
+ way.
55
+ </p>
56
+ <Link href="/" className="underline">
57
+ Continue shopping
58
+ </Link>
59
+ </div>
60
+ )
61
+ }
62
+
63
+ const { order, cartItems } = stash
64
+ const totals = orderTotalsFromSummary(order.summary) ?? {
65
+ total: null,
66
+ }
67
+ // The completeCart() order carries FLATTENED items (checkout.md), not the
68
+ // order-detail pivot shape the template's default converter expects —
69
+ // pass the normalized cart-line items instead and drop `order.items`.
70
+ const orderForTemplate = { ...order, items: undefined }
71
+
72
+ return (
73
+ <>
74
+ {/* The purchase event, every configured vendor at once, fired once
75
+ per order per browser. Dedup keys match the platform's
76
+ server-side Purchase exactly, so the browser event and the
77
+ server event collapse into ONE conversion. */}
78
+ <TrackOrderPurchase order={order} items={cartItems} config={tracking} />
79
+ <OrderCompletedTemplate
80
+ order={orderForTemplate}
81
+ totals={totals}
82
+ items={cartItems.map(displayItemFromCartLine)}
83
+ // This reference store checks out via one merchant method (pp_* kill:
84
+ // methods are merchant-named, provider-less); a multi-tender store
85
+ // should stash the chosen tender from its checkout state alongside
86
+ // the order.
87
+ paymentMethodName={"Cash on delivery"}
88
+ storeHref="/"
89
+ />
90
+ </>
91
+ )
92
+ }