create-cartbase 0.1.4 → 0.1.6

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.
@@ -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")
@@ -86,6 +86,12 @@ foreign key → 400 `invalid_publishable_key`.
86
86
  "thumbnail": null,
87
87
  "allow_backorder": false,
88
88
  "manage_inventory": true,
89
+ // Computed server-side by the platform's availability predicate:
90
+ // untracked or backorderable variants are always true; otherwise true
91
+ // iff available stock (kit-aware, reservations subtracted) is above
92
+ // zero. Read this — never re-derive stock client-side (the exact
93
+ // quantity is deliberately not exposed).
94
+ "in_stock": true,
89
95
  "variant_rank": 0,
90
96
  "metadata": null,
91
97
  "options": [
@@ -8,7 +8,7 @@
8
8
  "typecheck": "tsc --noEmit"
9
9
  },
10
10
  "dependencies": {
11
- "@cartbase/storefront": "^0.5.0",
11
+ "@cartbase/storefront": "^0.7.0",
12
12
  "next": "16.2.4",
13
13
  "react": "19.2.4",
14
14
  "react-dom": "19.2.4"
@@ -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"
@@ -80,6 +82,18 @@ export default async function RootLayout({
80
82
  <html lang="en">
81
83
  <body>
82
84
  <ConsentInit />
85
+ {/* Every marketing tag the store configured in the admin, mounted
86
+ from its own config: Meta, TikTok, Google (GA4 + Ads on one
87
+ tag), GTM. Nothing to wire per vendor — saving the ids in
88
+ Settings → Integrations is the whole merchant-side act. Order
89
+ matters: ConsentInit sets the Consent Mode defaults
90
+ synchronously ABOVE this, so every tag below inherits the
91
+ gate. */}
92
+ <StorefrontTags client={client} />
93
+ {/* Captures UTMs and the ad-click ids (ttclid / gclid / gbraid /
94
+ wbraid) that exist ONLY on an ad's landing URL — miss them
95
+ here and no platform can attribute the order to the click. */}
96
+ <TrackInit />
83
97
  <PlatformInit storeId={BARTER_CLIENT_ID} />
84
98
  <Providers cart={cart} consent={consentRes.consent}>
85
99
  <header className="border-b border-border">
@@ -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
+ }