@shop-on/create-theme 0.1.0 → 0.3.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shop-on/create-theme",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Scaffolds a Shop-On storefront theme: `npm create @shop-on/theme <dir>`.",
5
5
  "license": "SEE LICENSE IN dist/LICENSE.md",
6
6
  "bin": {
@@ -16,9 +16,9 @@
16
16
  "./package.json": "./package.json"
17
17
  },
18
18
  "devDependencies": {
19
- "@types/node": "26.6.3",
19
+ "@types/node": "26.6.5",
20
20
  "typescript": "7.0.2",
21
- "vitest": "5.0.3"
21
+ "vitest": "5.0.5"
22
22
  },
23
23
  "engines": {
24
24
  "node": "^20.19.0 || >=22.12.0"
@@ -26,6 +26,6 @@
26
26
  "scripts": {
27
27
  "template": "node scripts/template.mts",
28
28
  "check-types": "tsc --noEmit",
29
- "test": "vitest run"
29
+ "test": "vitest run --maxWorkers=2"
30
30
  }
31
31
  }
@@ -260,7 +260,7 @@ A theme change is not finished when it type-checks — the artifact carries a ve
260
260
  1. Bump `package.json#version` (semver). Fix → patch; new section, block or setting → minor.
261
261
  2. Prepend a `## X.Y.Z` section to `release-notes.md`. The platform stores only the section matching the uploaded version, so each one carries its own notes.
262
262
  3. `pnpm build` — postbuild emits `.output/theme.json` at the new version, and fails fast if `theme.config.json#locales` omits `"en"`. Check `theme.config.json#locales` matches the catalogs you ship first: a declared locale with no catalog renders default-language copy.
263
- 4. Package. **Don't improvise the tarball.** Two things a hand-rolled `tar` gets wrong: entries must sit **flat at the tarball root** (`-C .output`, pack `.`), and macOS needs `COPYFILE_DISABLE=1` + `--no-mac-metadata` to keep AppleDouble `._*` entries out (drop `--no-mac-metadata` on GNU tar): `COPYFILE_DISABLE=1 tar -czf ../<name>-X.Y.Z.tar.gz -C .output --exclude='.DS_Store' --no-mac-metadata .`. Uploading a version to the theme store isn't open to third-party developers yet; the tarball is as far as this goes for now.
263
+ 4. Package. **Don't hand-roll the tarball**: `npx @shop-on/theme-cli theme pack` writes `<name>-X.Y.Z.tar.gz` in the theme directory (`--out <path>` to put it elsewhere, `--build` to build first). Entries sit flat at the root with no macOS metadata, the same on every OS, and it refuses a build missing a file the theme store requires or older than `package.json`. Uploading a version to the theme store isn't open to third-party developers yet; the tarball is as far as this goes for now.
264
264
 
265
265
  Write the notes **for the merchant, not the diff**: what changes in their store, in their words. Match the file — plain bullets under the heading, no `### Fixed` subsections, no file names, no component names. "Prices now say when tax is included", not "added `taxIncludedCaption` to the price block".
266
266
 
@@ -11,13 +11,13 @@ rather than reimplementing the package surface.
11
11
 
12
12
  ## The package surface (already built — consume, don't reimplement)
13
13
 
14
- | Subpath | Gives you |
15
- | ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
16
- | `@shop-on/theme-server/customer-session` | `read/write/clearCustomerSession` — encrypted `customer_session` cookie (HMAC label `"customer"` off `STOREFRONT_CUSTOMER_SESSION_SECRET`). **Reads never throw** (missing secret / bad cookie → `undefined` = anonymous); writes fail loudly |
17
- | `@shop-on/theme-server/customer` | `probeCurrentCustomer()` (3-branch: authoritative null → clears cookie; valid → returns customer, NO cookie rewrite; thrown → preserves cookie, null) + `revokeCustomerSession()` |
18
- | `@shop-on/theme-server/customer-handoff` | `consumeCustomerHandoff(nonce)` — consumes the one-time `customer_session_handoff` nonce, cross-shop-guards on `STORE_SLUG`, writes the cookie. A thrown internal call degrades to `{ok:false, reason:"consume_error"}` — never lets an API blip error the landing page |
19
- | `@shop-on/theme-server/account` | `getAccountRedirectUrl({subpath?, returnToPath?})` (validated + absolutized return_to), `normalizeReturnToPath`, `handleCustomerLogoutPost(request)` (the ENTIRE logout POST: Origin gate → revoke → clear both cookies → auto-submit form), `renderLogoutForm` |
20
- | `@shop-on/theme-server/cart-customer` | `resolveCartForCustomer({session, customerToken, locale?})` (own cart as-is / attach anonymous cart / claim stale-or-fresh-device cart) + `mintOrClaimCart({session, customerToken?})` (claim-before-create; anonymous = straight mint). Transient transport failures PRESERVE the guest-cart cookie — only authoritative API answers claim or clear |
14
+ | Subpath | Gives you |
15
+ | ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
16
+ | `@shop-on/theme-server/customer-session` | `read/write/clearCustomerSession` — encrypted `customer_session` cookie (HMAC label `"customer"` off `STOREFRONT_CUSTOMER_SESSION_SECRET`). **Reads never throw** (missing secret / bad cookie → `undefined` = anonymous); writes fail loudly |
17
+ | `@shop-on/theme-server/customer` | `probeCurrentCustomer()` (3-branch: authoritative null → clears cookie; valid → returns customer, NO cookie rewrite; thrown → preserves cookie, null) + `revokeCustomerSession()` |
18
+ | `@shop-on/theme-server/customer-handoff` | `consumeCustomerHandoff(nonce, state)` — checks `state` (the URL's `?state=`) against the `account_state` cookie `getAccountRedirectUrl` set, then consumes the one-time `customer_session_handoff` nonce, cross-shop-guards on `STORE_SLUG`, writes the cookie. A thrown internal call degrades to `{ok:false, reason:"consume_error"}` — never lets an API blip error the landing page |
19
+ | `@shop-on/theme-server/account` | `getAccountRedirectUrl({subpath?, returnToPath?})` (validated + absolutized return_to, with a fresh `state` bound to the `account_state` cookie), `normalizeReturnToPath`, `handleCustomerLogoutPost(request)` (the ENTIRE logout POST: Origin gate → revoke → clear both cookies → auto-submit form), `renderLogoutForm` |
20
+ | `@shop-on/theme-server/cart-customer` | `resolveCartForCustomer({session, customerToken, locale?})` (own cart as-is / attach anonymous cart / claim stale-or-fresh-device cart) + `mintOrClaimCart({session, customerToken?})` (claim-before-create; anonymous = straight mint). Transient transport failures PRESERVE the guest-cart cookie — only authoritative API answers claim or clear |
21
21
 
22
22
  Env: `STORE_SLUG` + `STOREFRONT_CUSTOMER_SESSION_SECRET` (injected by the platform
23
23
  in prod; `.env` in dev, copied from `.env.example`). No new theme deps — `@shop-on/customer-runtime`
@@ -41,7 +41,8 @@ rides inside theme-server.
41
41
  `return handleCustomerLogoutPost(request)` (dynamic import).
42
42
  4. **Root route** — `validateSearch` gains `customer_handoff` +
43
43
  `clear_customer_session` (string passthrough); `beforeLoad` consumes the
44
- nonce then redirect-replaces to the stripped URL (use the RAW
44
+ nonce, passing `location.search.state` beside it (without it the handoff is
45
+ dropped unconsumed and the buyer lands signed out), then redirect-replaces to the stripped URL (use the RAW
45
46
  `location.searchStr` so child-route params survive); loader starts
46
47
  `probeCurrentCustomerFn()` in wave 1 and **chains the cart off the probe**
47
48
  (`customerPromise.then(c => resolveCartFn({customerId: c?.id ?? null}))`) so
@@ -50,8 +51,12 @@ rides inside theme-server.
50
51
  `resolveCart` branches anonymous-read vs `resolveCartForCustomer`;
51
52
  cookie-less adds go through `mintOrClaimCart({session, customerToken})`
52
53
  (handles anonymous too); forward `customerToken` into every shared op.
53
- Checkout: pass `{customerToken}` to `startCheckoutHandoff` so the API folds
54
- the buyer into the handoff.
54
+ Checkout: the `/checkout` route reads `checkout_state` in `validateSearch`
55
+ and passes it to `startCheckoutHandoff({ state, customerToken })` (`state`
56
+ is a required key). Without a state it returns checkout's `/begin`, which
57
+ binds the browser and comes back with one; with it, it mints (the token lets
58
+ the API fold the buyer into the handoff). Checkout rejects a handoff minted
59
+ without the state.
55
60
  6. **Header UI** — an `AccountControl` snippet: anonymous = icon-link to
56
61
  `/account?return_to=…`; signed-in = **lazy** dropdown chunk (keep the menu
57
62
  primitive out of the anonymous bundle). Merchant `account_menu` `link_list`
@@ -173,7 +173,7 @@ GID is in place before its `${id}Data` resolver runs:
173
173
  1. **Fetch metaobject entries** — `loadMetaobjectFieldSources(template, product)`
174
174
  collects the union of pinned entry ids (`collectMetaobjectEntryIds`, binding +
175
175
  inline forms) and traversed entry ids (`collectTraversedEntryIds(template,
176
- product.metafields)` — field-bearing `product_metafield` refs), early-returns
176
+ product.metafields)` — field-bearing `product_metafield` refs), early-returns
177
177
  `{}` when none, else batch-fetches (`fetchMetaobject`, deduped + `memoTTL`-
178
178
  cached, capped at 50 with a warning) and builds `{ entry: { key: { type, value } } }`.
179
179
  2. **Whole-value pass — BEFORE the picker-data resolvers.**
@@ -25,7 +25,7 @@ pnpm build # postbuild enforces the build lines
25
25
  The output is one line per finding, `file:line rule message`, followed by the
26
26
  recipe for each rule that fired.
27
27
 
28
- The **audit** lines come from `shop-on theme audit`, in the `@shop-on/cli`
28
+ The **audit** lines come from `shop-on theme audit`, in the `@shop-on/theme-cli`
29
29
  package:
30
30
 
31
31
  ```bash
@@ -41,7 +41,7 @@ missing instead of measuring an empty page. The password template is left
41
41
  out, since it only renders while protection is on, and the audit refuses a
42
42
  protected store.
43
43
 
44
- `@shop-on/cli` isn't published yet. Until it is, measure the **audit** lines
44
+ `@shop-on/theme-cli` isn't published yet. Until it is, measure the **audit** lines
45
45
  by hand, as each one says.
46
46
 
47
47
  ## Performance
@@ -8,16 +8,16 @@ published `@shop-on/theme-*` packages.
8
8
  ## Quick start
9
9
 
10
10
  ```sh
11
- npx @shop-on/cli theme init my-theme # scaffold, log in if needed, write .env, install
11
+ npx @shop-on/theme-cli theme init my-theme # scaffold, log in if needed, write .env, install
12
12
  cd my-theme
13
- npx @shop-on/cli theme dev # open this theme in your store's theme editor
13
+ npx @shop-on/theme-cli theme dev # open this theme in your store's theme editor
14
14
  ```
15
15
 
16
16
  The first time, `theme init` opens your browser on your store's admin:
17
17
  approve the CLI and pick the store. Your staff role needs **Manage online store**. It then writes
18
18
  `.env` with a storefront credential for that store. `.env` holds secrets:
19
19
  never commit it. A store holds at most 5 of these credentials;
20
- `npx @shop-on/cli theme credentials revoke <client-id>` frees one.
20
+ `npx @shop-on/theme-cli theme credentials revoke <client-id>` frees one.
21
21
 
22
22
  `theme dev` runs the theme behind a `cloudflared` tunnel (install it first)
23
23
  and opens the hosted theme editor on a developer preview of it. Confirm the
@@ -17,36 +17,36 @@
17
17
  },
18
18
  "dependencies": {
19
19
  "@bprogress/core": "1.3.4",
20
- "@shop-on/i18n": "^0.1.0",
21
- "@shop-on/theme-contract": "^0.1.0",
22
- "@shop-on/theme-loader": "^0.1.0",
23
- "@shop-on/theme-react": "^0.1.0",
24
- "@shop-on/theme-routes": "^0.1.0",
25
- "@shop-on/theme-runtime": "^0.1.0",
26
- "@shop-on/theme-server": "^0.1.0",
27
- "@shop-on/theme-system": "^0.1.0",
20
+ "@shop-on/i18n": "^0.3.0",
21
+ "@shop-on/theme-contract": "^0.3.0",
22
+ "@shop-on/theme-loader": "^0.3.0",
23
+ "@shop-on/theme-react": "^0.3.0",
24
+ "@shop-on/theme-routes": "^0.3.0",
25
+ "@shop-on/theme-runtime": "^0.3.0",
26
+ "@shop-on/theme-server": "^0.3.0",
27
+ "@shop-on/theme-system": "^0.3.0",
28
28
  "@tailwindcss/vite": "4.3.3",
29
- "@tanstack/react-router": "1.170.41",
30
- "@tanstack/react-start": "1.168.60",
31
- "dotenv": "18.0.5",
29
+ "@tanstack/react-router": "1.170.45",
30
+ "@tanstack/react-start": "1.169.3",
31
+ "dotenv": "18.0.8",
32
32
  "nitro": "3.0.260903-beta",
33
- "react": "19.3.0",
34
- "react-dom": "19.3.0",
33
+ "react": "19.3.1",
34
+ "react-dom": "19.3.1",
35
35
  "tailwindcss": "4.3.3"
36
36
  },
37
37
  "devDependencies": {
38
- "@shop-on/theme-config": "^0.1.0",
39
- "@shop-on/theme-dev-host": "^0.1.0",
38
+ "@shop-on/theme-config": "^0.3.0",
39
+ "@shop-on/theme-dev-host": "^0.3.0",
40
40
  "@spiriit/vite-plugin-svg-spritemap": "7.2.0",
41
- "@types/node": "26.6.3",
41
+ "@types/node": "26.6.5",
42
42
  "@types/react": "19.3.0",
43
43
  "@types/react-dom": "19.3.0",
44
- "@vitejs/plugin-react": "6.1.1",
45
- "gql.tada": "1.11.3",
46
- "happy-dom": "20.10.6",
47
- "oxfmt": "^0.71.0",
44
+ "@vitejs/plugin-react": "6.1.2",
45
+ "gql.tada": "1.11.4",
46
+ "happy-dom": "21.0.4",
47
+ "oxfmt": "^0.74.0",
48
48
  "typescript": "7.0.2",
49
- "vite": "8.3.1",
50
- "vitest": "5.0.3"
49
+ "vite": "8.3.5",
50
+ "vitest": "5.0.5"
51
51
  }
52
52
  }
@@ -19,6 +19,7 @@ import {
19
19
  } from "@/src/lib/listing-search";
20
20
  import { isPreviewSearch, paginatedPath, templateSeoHead } from "@/src/lib/seo-head";
21
21
  import { getAccountRedirectUrlFn } from "@/src/server/customer";
22
+ import { startCheckoutFn } from "@/src/server/checkout-redirect";
22
23
  import {
23
24
  getArticleTemplateFn,
24
25
  getBlogPageTemplateFn,
@@ -70,6 +71,7 @@ export const routeAdapter: ThemeRouteAdapter = {
70
71
  parseBlogSearch,
71
72
  },
72
73
  account: { getAccountRedirectUrlFn },
74
+ checkout: { startCheckoutFn },
73
75
  // Add a `Wrapper` here if sections need to know they're in a picker preview.
74
76
  preview: { Unavailable: PreviewUnavailable },
75
77
  };
@@ -19,6 +19,7 @@ import { Route as AnalyticsVisitsRouteImport } from './routes/analytics.visits'
19
19
  import { Route as Char123LocaleChar125IndexRouteImport } from './routes/{-$locale}/index'
20
20
  import { Route as Char123LocaleChar125404RouteImport } from './routes/{-$locale}/404'
21
21
  import { Route as Char123LocaleChar125CartRouteImport } from './routes/{-$locale}/cart'
22
+ import { Route as Char123LocaleChar125CheckoutRouteImport } from './routes/{-$locale}/checkout'
22
23
  import { Route as Char123LocaleChar125PasswordRouteImport } from './routes/{-$locale}/password'
23
24
  import { Route as Char123LocaleChar125SearchRouteImport } from './routes/{-$locale}/search'
24
25
  import { Route as Char123LocaleChar125AccountIndexRouteImport } from './routes/{-$locale}/account/index'
@@ -87,6 +88,12 @@ const Char123LocaleChar125CartRoute =
87
88
  path: '/cart',
88
89
  getParentRoute: () => Char123LocaleChar125Route,
89
90
  } as any)
91
+ const Char123LocaleChar125CheckoutRoute =
92
+ Char123LocaleChar125CheckoutRouteImport.update({
93
+ id: '/checkout',
94
+ path: '/checkout',
95
+ getParentRoute: () => Char123LocaleChar125Route,
96
+ } as any)
90
97
  const Char123LocaleChar125PasswordRoute =
91
98
  Char123LocaleChar125PasswordRouteImport.update({
92
99
  id: '/password',
@@ -190,6 +197,7 @@ export interface FileRoutesByFullPath {
190
197
  '/analytics/visits': typeof AnalyticsVisitsRoute
191
198
  '/{-$locale}/404': typeof Char123LocaleChar125404Route
192
199
  '/{-$locale}/cart': typeof Char123LocaleChar125CartRoute
200
+ '/{-$locale}/checkout': typeof Char123LocaleChar125CheckoutRoute
193
201
  '/{-$locale}/password': typeof Char123LocaleChar125PasswordRoute
194
202
  '/{-$locale}/search': typeof Char123LocaleChar125SearchRoute
195
203
  '/{-$locale}/': typeof Char123LocaleChar125IndexRoute
@@ -216,6 +224,7 @@ export interface FileRoutesByTo {
216
224
  '/analytics/visits': typeof AnalyticsVisitsRoute
217
225
  '/{-$locale}/404': typeof Char123LocaleChar125404Route
218
226
  '/{-$locale}/cart': typeof Char123LocaleChar125CartRoute
227
+ '/{-$locale}/checkout': typeof Char123LocaleChar125CheckoutRoute
219
228
  '/{-$locale}/password': typeof Char123LocaleChar125PasswordRoute
220
229
  '/{-$locale}/search': typeof Char123LocaleChar125SearchRoute
221
230
  '/{-$locale}': typeof Char123LocaleChar125IndexRoute
@@ -244,6 +253,7 @@ export interface FileRoutesById {
244
253
  '/analytics/visits': typeof AnalyticsVisitsRoute
245
254
  '/{-$locale}/404': typeof Char123LocaleChar125404Route
246
255
  '/{-$locale}/cart': typeof Char123LocaleChar125CartRoute
256
+ '/{-$locale}/checkout': typeof Char123LocaleChar125CheckoutRoute
247
257
  '/{-$locale}/password': typeof Char123LocaleChar125PasswordRoute
248
258
  '/{-$locale}/search': typeof Char123LocaleChar125SearchRoute
249
259
  '/{-$locale}/': typeof Char123LocaleChar125IndexRoute
@@ -273,6 +283,7 @@ export interface FileRouteTypes {
273
283
  | '/analytics/visits'
274
284
  | '/{-$locale}/404'
275
285
  | '/{-$locale}/cart'
286
+ | '/{-$locale}/checkout'
276
287
  | '/{-$locale}/password'
277
288
  | '/{-$locale}/search'
278
289
  | '/{-$locale}/'
@@ -299,6 +310,7 @@ export interface FileRouteTypes {
299
310
  | '/analytics/visits'
300
311
  | '/{-$locale}/404'
301
312
  | '/{-$locale}/cart'
313
+ | '/{-$locale}/checkout'
302
314
  | '/{-$locale}/password'
303
315
  | '/{-$locale}/search'
304
316
  | '/{-$locale}'
@@ -326,6 +338,7 @@ export interface FileRouteTypes {
326
338
  | '/analytics/visits'
327
339
  | '/{-$locale}/404'
328
340
  | '/{-$locale}/cart'
341
+ | '/{-$locale}/checkout'
329
342
  | '/{-$locale}/password'
330
343
  | '/{-$locale}/search'
331
344
  | '/{-$locale}/'
@@ -426,6 +439,13 @@ declare module '@tanstack/react-router' {
426
439
  preLoaderRoute: typeof Char123LocaleChar125CartRouteImport
427
440
  parentRoute: typeof Char123LocaleChar125Route
428
441
  }
442
+ '/{-$locale}/checkout': {
443
+ id: '/{-$locale}/checkout'
444
+ path: '/checkout'
445
+ fullPath: '/{-$locale}/checkout'
446
+ preLoaderRoute: typeof Char123LocaleChar125CheckoutRouteImport
447
+ parentRoute: typeof Char123LocaleChar125Route
448
+ }
429
449
  '/{-$locale}/password': {
430
450
  id: '/{-$locale}/password'
431
451
  path: '/password'
@@ -537,6 +557,7 @@ declare module '@tanstack/react-router' {
537
557
  interface Char123LocaleChar125RouteChildren {
538
558
  Char123LocaleChar125404Route: typeof Char123LocaleChar125404Route
539
559
  Char123LocaleChar125CartRoute: typeof Char123LocaleChar125CartRoute
560
+ Char123LocaleChar125CheckoutRoute: typeof Char123LocaleChar125CheckoutRoute
540
561
  Char123LocaleChar125PasswordRoute: typeof Char123LocaleChar125PasswordRoute
541
562
  Char123LocaleChar125SearchRoute: typeof Char123LocaleChar125SearchRoute
542
563
  Char123LocaleChar125IndexRoute: typeof Char123LocaleChar125IndexRoute
@@ -558,6 +579,7 @@ interface Char123LocaleChar125RouteChildren {
558
579
  const Char123LocaleChar125RouteChildren: Char123LocaleChar125RouteChildren = {
559
580
  Char123LocaleChar125404Route: Char123LocaleChar125404Route,
560
581
  Char123LocaleChar125CartRoute: Char123LocaleChar125CartRoute,
582
+ Char123LocaleChar125CheckoutRoute: Char123LocaleChar125CheckoutRoute,
561
583
  Char123LocaleChar125PasswordRoute: Char123LocaleChar125PasswordRoute,
562
584
  Char123LocaleChar125SearchRoute: Char123LocaleChar125SearchRoute,
563
585
  Char123LocaleChar125IndexRoute: Char123LocaleChar125IndexRoute,
@@ -70,9 +70,12 @@ export const Route = createRootRouteWithContext<RouterContext>()({
70
70
  const search = location.search as {
71
71
  customer_handoff?: string;
72
72
  clear_customer_session?: string;
73
+ state?: string;
73
74
  };
74
75
  if (search.customer_handoff) {
75
- await consumeCustomerHandoffFn({ data: { nonce: search.customer_handoff } });
76
+ await consumeCustomerHandoffFn({
77
+ data: { nonce: search.customer_handoff, state: search.state },
78
+ });
76
79
  }
77
80
  if (!search.customer_handoff && !search.clear_customer_session) return;
78
81
 
@@ -0,0 +1,12 @@
1
+ import { createFileRoute } from "@tanstack/react-router";
2
+ import { checkoutRouteOptions } from "@shop-on/theme-routes";
3
+ import { routeAdapter } from "@/src/route-adapter";
4
+
5
+ // The cart → checkout bounce, via checkout's `/begin` on the first visit. See
6
+ // docs/theme-cart.md#checkout-handoff.
7
+ const { validateSearch, beforeLoad } = checkoutRouteOptions(routeAdapter.checkout);
8
+
9
+ export const Route = createFileRoute("/{-$locale}/checkout")({
10
+ validateSearch,
11
+ beforeLoad,
12
+ });
@@ -0,0 +1,21 @@
1
+ // Server-side cart→checkout bounce. The cart-cookie read + `cartCheckout` call
2
+ // + result mapping live in the shared `@shop-on/theme-server/checkout` helper;
3
+ // this forwards the buyer token so a signed-in checkout arrives prefilled
4
+ // (anonymous buyers forward nothing). The node-only helpers are dynamic-
5
+ // imported inside the handler so this module stays safe to import from the
6
+ // client-side `/checkout` route.
7
+ import { createServerFn } from "@tanstack/react-start";
8
+ import type { CheckoutHandoffResult } from "@shop-on/theme-server/checkout";
9
+
10
+ export type { CheckoutHandoffResult };
11
+
12
+ // `state` is the `checkout_state` checkout's `/begin` sent back; without one the
13
+ // result is the `/begin` URL itself (what a browser-side caller gets).
14
+ export const startCheckoutFn = createServerFn({ method: "POST" })
15
+ .validator((data: { state?: string } | undefined) => ({ state: data?.state }))
16
+ .handler(async ({ data }): Promise<CheckoutHandoffResult> => {
17
+ const { readCustomerSession } = await import("@shop-on/theme-server/customer-session");
18
+ const { startCheckoutHandoff } = await import("@shop-on/theme-server/checkout");
19
+ const session = await readCustomerSession();
20
+ return startCheckoutHandoff({ state: data.state, customerToken: session?.token });
21
+ });
@@ -32,10 +32,10 @@ export const probeCurrentCustomerFn = createServerFn({ method: "GET" }).handler(
32
32
  // Consume a `customer_session_handoff` nonce (root `beforeLoad`), writing the
33
33
  // `customer_session` cookie. Cross-shop-guarded against `STORE_SLUG`.
34
34
  export const consumeCustomerHandoffFn = createServerFn({ method: "POST" })
35
- .validator((data: { nonce: string }) => data)
35
+ .validator((data: { nonce: string; state?: string }) => data)
36
36
  .handler(async ({ data }): Promise<ConsumeCustomerHandoffResult> => {
37
37
  const { consumeCustomerHandoff } = await import("@shop-on/theme-server/customer-handoff");
38
- return consumeCustomerHandoff(data.nonce);
38
+ return consumeCustomerHandoff(data.nonce, data.state);
39
39
  });
40
40
 
41
41
  // Build the central account-app URL the `/account/*` bouncers redirect to,