@shop-on/create-theme 0.2.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.2.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": {
@@ -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`
@@ -17,14 +17,14 @@
17
17
  },
18
18
  "dependencies": {
19
19
  "@bprogress/core": "1.3.4",
20
- "@shop-on/i18n": "^0.2.0",
21
- "@shop-on/theme-contract": "^0.2.0",
22
- "@shop-on/theme-loader": "^0.2.0",
23
- "@shop-on/theme-react": "^0.2.0",
24
- "@shop-on/theme-routes": "^0.2.0",
25
- "@shop-on/theme-runtime": "^0.2.0",
26
- "@shop-on/theme-server": "^0.2.0",
27
- "@shop-on/theme-system": "^0.2.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
29
  "@tanstack/react-router": "1.170.45",
30
30
  "@tanstack/react-start": "1.169.3",
@@ -35,8 +35,8 @@
35
35
  "tailwindcss": "4.3.3"
36
36
  },
37
37
  "devDependencies": {
38
- "@shop-on/theme-config": "^0.2.0",
39
- "@shop-on/theme-dev-host": "^0.2.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
41
  "@types/node": "26.6.5",
42
42
  "@types/react": "19.3.0",
@@ -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,