@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 +4 -4
- package/template/.claude/skills/theme-dev/SKILL.md +1 -1
- package/template/.claude/skills/theme-dev/recipes/customer-auth.md +15 -10
- package/template/.claude/skills/theme-dev/recipes/dynamic-sources.md +1 -1
- package/template/.claude/skills/theme-dev/recipes/quality.md +2 -2
- package/template/README.md +3 -3
- package/template/package.json +22 -22
- package/template/src/route-adapter.tsx +2 -0
- package/template/src/routeTree.gen.ts +22 -0
- package/template/src/routes/__root.tsx +4 -1
- package/template/src/routes/{-$locale}/checkout.tsx +12 -0
- package/template/src/server/checkout-redirect.ts +21 -0
- package/template/src/server/customer.ts +2 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shop-on/create-theme",
|
|
3
|
-
"version": "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.
|
|
19
|
+
"@types/node": "26.6.5",
|
|
20
20
|
"typescript": "7.0.2",
|
|
21
|
-
"vitest": "5.0.
|
|
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
|
|
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
|
|
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:
|
|
54
|
-
|
|
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
|
package/template/README.md
CHANGED
|
@@ -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
|
package/template/package.json
CHANGED
|
@@ -17,36 +17,36 @@
|
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
|
19
19
|
"@bprogress/core": "1.3.4",
|
|
20
|
-
"@shop-on/i18n": "^0.
|
|
21
|
-
"@shop-on/theme-contract": "^0.
|
|
22
|
-
"@shop-on/theme-loader": "^0.
|
|
23
|
-
"@shop-on/theme-react": "^0.
|
|
24
|
-
"@shop-on/theme-routes": "^0.
|
|
25
|
-
"@shop-on/theme-runtime": "^0.
|
|
26
|
-
"@shop-on/theme-server": "^0.
|
|
27
|
-
"@shop-on/theme-system": "^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.
|
|
30
|
-
"@tanstack/react-start": "1.
|
|
31
|
-
"dotenv": "18.0.
|
|
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.
|
|
34
|
-
"react-dom": "19.3.
|
|
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.
|
|
39
|
-
"@shop-on/theme-dev-host": "^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.
|
|
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.
|
|
45
|
-
"gql.tada": "1.11.
|
|
46
|
-
"happy-dom": "
|
|
47
|
-
"oxfmt": "^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.
|
|
50
|
-
"vitest": "5.0.
|
|
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({
|
|
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,
|