@thorprovider/storefront 0.3.1 → 0.4.0-mvp.2
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/README.md +321 -152
- package/VENDOR.md +69 -0
- package/assets/openspec/config.yaml +38 -0
- package/assets/templates/AGENTS.md +76 -0
- package/assets/templates/app/app/[[...slug]]/page.tsx +54 -2
- package/assets/templates/app/app/[[...slug]]/renderer.tsx +13 -2
- package/assets/templates/app/app/globals.css +5 -8
- package/assets/templates/app/app/layout.tsx +17 -7
- package/assets/templates/app/app/robots.ts +19 -0
- package/assets/templates/app/app/sitemap.ts +71 -0
- package/assets/templates/app/lib/__STOREFRONT__/actions.ts +612 -0
- package/assets/templates/app/lib/__STOREFRONT__/cart-state.ts +66 -0
- package/assets/templates/app/lib/__STOREFRONT__/catalog.ts +50 -57
- package/assets/templates/app/lib/__STOREFRONT__/commerce.ts +679 -0
- package/assets/templates/app/lib/__STOREFRONT__/composition.ts +26 -0
- package/assets/templates/app/lib/__STOREFRONT__/handlers.core.ts +197 -0
- package/assets/templates/app/lib/__STOREFRONT__/handlers.ts +5 -36
- package/assets/templates/app/lib/__STOREFRONT__/icon-map.ts +27 -0
- package/assets/templates/app/lib/__STOREFRONT__/mappers.ts +13 -11
- package/assets/templates/app/lib/__STOREFRONT__/registry.tsx +264 -51
- package/assets/templates/app/lib/__STOREFRONT__/runtime.ts +6 -3
- package/assets/templates/app/lib/__STOREFRONT__/schema.ts +136 -0
- package/assets/templates/app/lib/__STOREFRONT__/section-observation.ts +182 -0
- package/assets/templates/app/lib/__STOREFRONT__/section-sources.ts +130 -0
- package/assets/templates/app/lib/__STOREFRONT__/session.core.ts +470 -0
- package/assets/templates/app/lib/__STOREFRONT__/session.ts +169 -0
- package/assets/templates/app/lib/__STOREFRONT__/shell/content.ts +120 -0
- package/assets/templates/app/lib/__STOREFRONT__/shell/props.ts +38 -0
- package/assets/templates/app/lib/__STOREFRONT__/spec/index.ts +129 -10
- package/assets/templates/app/lib/__STOREFRONT__/spec/page-root.ts +5 -1
- package/assets/templates/app/lib/__STOREFRONT__/spec/seo.ts +191 -0
- package/assets/templates/app/lib/__STOREFRONT__/state.core.ts +257 -0
- package/assets/templates/app/lib/__STOREFRONT__/state.ts +7 -31
- package/assets/templates/app/lib/__STOREFRONT__/store-context.ts +161 -0
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/cta.client.tsx +25 -0
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/cta.tsx +16 -0
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/faq.client.tsx +24 -0
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/faq.tsx +5 -18
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/index.ts +18 -0
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/logoCloud.tsx +26 -14
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/pricing.tsx +36 -18
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/products.client.tsx +100 -0
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/products.tsx +19 -0
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/schema.cta.ts +19 -0
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/schema.faq.ts +18 -0
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/schema.products.ts +39 -0
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/statistics.tsx +35 -14
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/testimonials.tsx +31 -13
- package/assets/templates/app/lib/__STOREFRONT__/views/types.ts +5 -3
- package/assets/templates/features/checkout/handlers.ts +159 -0
- package/assets/templates/features/checkout/state.ts +61 -0
- package/assets/templates/features/customer-auth/handlers.ts +59 -0
- package/assets/templates/features/customer-auth/state.ts +45 -0
- package/assets/templates/features/recommendations/state.ts +11 -0
- package/assets/templates/views/account/content.ts +35 -0
- package/assets/templates/views/account/index.tsx +114 -0
- package/assets/templates/views/account/registry.tsx +439 -0
- package/assets/templates/views/account/schema.ts +53 -0
- package/assets/templates/views/cart/content.ts +11 -0
- package/assets/templates/views/cart/data.ts +23 -0
- package/assets/templates/views/cart/index.tsx +53 -0
- package/assets/templates/views/cart/props.ts +12 -0
- package/assets/templates/views/cart/registry.tsx +81 -0
- package/assets/templates/views/cart/schema.ts +52 -0
- package/assets/templates/views/category/assembly/assembly.json +29 -0
- package/assets/templates/views/category/assembly/index.skeleton.tsx +251 -0
- package/assets/templates/views/category/assembly/registry.skeleton.tsx +34 -0
- package/assets/templates/views/category/assembly/slots/breadcrumb/Breadcrumb/binding.json +5 -0
- package/assets/templates/views/category/assembly/slots/breadcrumb/Breadcrumb/catalog.part +4 -0
- package/assets/templates/views/category/assembly/slots/breadcrumb/Breadcrumb/element.part +7 -0
- package/assets/templates/views/category/assembly/slots/breadcrumb/Breadcrumb/registry.part +18 -0
- package/assets/templates/views/category/assembly/slots/filter/CategoryFilter2/catalog.part +5 -0
- package/assets/templates/views/category/assembly/slots/filter/CategoryFilter2/element.part +34 -0
- package/assets/templates/views/category/assembly/slots/filter/CategoryFilter2/registry.part +71 -0
- package/assets/templates/views/category/content.ts +30 -0
- package/assets/templates/views/category/data.ts +12 -0
- package/assets/templates/views/category/index.tsx +299 -0
- package/assets/templates/views/category/props.ts +23 -0
- package/assets/templates/views/category/registry.tsx +122 -0
- package/assets/templates/views/category/schema.ts +91 -0
- package/assets/templates/views/checkout/content.ts +105 -0
- package/assets/templates/views/checkout/data.ts +23 -0
- package/assets/templates/views/checkout/index.tsx +111 -0
- package/assets/templates/views/checkout/props.ts +49 -0
- package/assets/templates/views/checkout/registry.tsx +217 -0
- package/assets/templates/views/checkout/schema.ts +229 -0
- package/assets/templates/views/home/assembly/assembly.json +33 -0
- package/assets/templates/views/home/assembly/index.skeleton.tsx +90 -0
- package/assets/templates/views/home/assembly/registry.skeleton.tsx +25 -0
- package/assets/templates/views/home/assembly/slots/benefits/Feature1/binding.json +5 -0
- package/assets/templates/views/home/assembly/slots/benefits/Feature1/catalog.part +4 -0
- package/assets/templates/views/home/assembly/slots/benefits/Feature1/element.part +4 -0
- package/assets/templates/views/home/assembly/slots/benefits/Feature1/registry.part +11 -0
- package/assets/templates/views/home/assembly/slots/benefits/Feature2/binding.json +5 -0
- package/assets/templates/views/home/assembly/slots/benefits/Feature2/catalog.part +4 -0
- package/assets/templates/views/home/assembly/slots/benefits/Feature2/element.part +4 -0
- package/assets/templates/views/home/assembly/slots/benefits/Feature2/registry.part +10 -0
- package/assets/templates/views/home/assembly/slots/categories/ProductCategory2/binding.json +5 -0
- package/assets/templates/views/home/assembly/slots/categories/ProductCategory2/catalog.part +4 -0
- package/assets/templates/views/home/assembly/slots/categories/ProductCategory2/element.part +5 -0
- package/assets/templates/views/home/assembly/slots/categories/ProductCategory2/registry.part +16 -0
- package/assets/templates/views/home/assembly/slots/hero/StorefrontHero1/binding.json +5 -0
- package/assets/templates/views/home/assembly/slots/hero/StorefrontHero1/catalog.part +4 -0
- package/assets/templates/views/home/assembly/slots/hero/StorefrontHero1/element.part +5 -0
- package/assets/templates/views/home/assembly/slots/hero/StorefrontHero1/registry.part +26 -0
- package/assets/templates/views/home/content.ts +70 -0
- package/assets/templates/views/home/data.ts +24 -0
- package/assets/templates/views/home/index.tsx +57 -85
- package/assets/templates/views/home/props.ts +22 -0
- package/assets/templates/views/home/registry.tsx +38 -22
- package/assets/templates/views/home/schema.ts +93 -0
- package/assets/templates/views/product/content.ts +25 -0
- package/assets/templates/views/product/data.ts +50 -0
- package/assets/templates/views/product/index.tsx +148 -0
- package/assets/templates/views/product/props.ts +27 -0
- package/assets/templates/views/product/registry.tsx +103 -0
- package/assets/templates/views/product/schema.ts +121 -0
- package/assets/templates/views/static/content.ts +99 -0
- package/assets/templates/views/static/index.tsx +60 -0
- package/assets/templates/views/static/registry.tsx +66 -0
- package/assets/templates/views/static/schema.ts +45 -0
- package/bin/storefront.js +2424 -148
- package/examples/merchandising/category-led.json +37 -0
- package/examples/merchandising/collection-led.json +37 -0
- package/package.json +15 -9
- package/recipes/archetypes.json +28 -14
- package/recipes/catalog.json +400 -0
- package/recipes/catalog.mjs +128 -0
- package/recipes/catalog.schema.json +64 -0
- package/recipes/feature.schema.json +89 -0
- package/recipes/features.json +162 -0
- package/recipes/manifest.schema.json +69 -0
- package/recipes/operations.schema.json +43 -0
- package/recipes/recipe.schema.json +11 -2
- package/recipes/recipes.json +76 -19
- package/recipes/sections.json +34 -5
- package/recipes/validate.mjs +838 -32
- package/scripts/candidate.mjs +236 -0
- package/scripts/composers.mjs +160 -0
- package/scripts/env-file.mjs +86 -0
- package/scripts/file-classes.mjs +121 -0
- package/scripts/logical-controls.mjs +61 -0
- package/scripts/manifest.mjs +298 -0
- package/scripts/migrate-english-routes.mjs +276 -0
- package/scripts/operation-registry.mjs +256 -0
- package/scripts/operations.mjs +720 -0
- package/scripts/ownership-conflict.mjs +356 -0
- package/scripts/pinning.mjs +573 -0
- package/scripts/plan-apply.mjs +987 -0
- package/scripts/preflight.mjs +51 -0
- package/scripts/reconcile.mjs +549 -0
- package/scripts/render-templates.mjs +338 -68
- package/scripts/schema-validate.mjs +132 -0
- package/scripts/section-compat.mjs +300 -0
- package/scripts/section-config.mjs +253 -0
- package/scripts/section-operations.mjs +508 -0
- package/scripts/starter.mjs +17 -0
- package/scripts/sync-assets.mjs +22 -15
- package/scripts/theme/color.mjs +185 -0
- package/scripts/theme/css-theme.mjs +233 -0
- package/scripts/theme/fonts.mjs +123 -0
- package/scripts/theme/import-export.mjs +125 -0
- package/scripts/theme/preset.mjs +93 -0
- package/scripts/theme/theme-plan.mjs +292 -0
- package/scripts/theme/tokens.mjs +137 -0
- package/scripts/view-assembly.mjs +337 -0
- package/scripts/view-selection.mjs +211 -0
- package/skills/sf-feature/SKILL.md +80 -0
- package/skills/sf-setup/SKILL.md +79 -0
- package/skills/sf-theme-gen/SKILL.md +100 -30
- package/skills/sf-theme-gen/references/shadcn-theming.md +87 -0
- package/skills/{shadcn-theming/references/templates → sf-theme-gen/references}/theme-setup.md +15 -19
- package/skills/{shadcn-theming → sf-theme-gen}/references/theming-guide.md +6 -11
- package/skills/sf-view-gen/SKILL.md +146 -93
- package/skills/sf-view-gen/assets/recipes/archetypes.json +28 -14
- package/skills/sf-view-gen/assets/recipes/catalog.json +400 -0
- package/skills/sf-view-gen/assets/recipes/catalog.schema.json +64 -0
- package/skills/sf-view-gen/assets/recipes/features.json +162 -0
- package/skills/sf-view-gen/assets/recipes/recipes.json +76 -19
- package/skills/sf-view-gen/assets/recipes/sections.json +34 -5
- package/skills/sf-view-gen/assets/templates/views/sections/cta.client.tsx +25 -0
- package/skills/sf-view-gen/assets/templates/views/sections/cta.tsx +16 -0
- package/skills/sf-view-gen/assets/templates/views/sections/faq.client.tsx +13 -9
- package/skills/sf-view-gen/assets/templates/views/sections/faq.tsx +2 -7
- package/skills/sf-view-gen/assets/templates/views/sections/logoCloud.client.tsx +12 -8
- package/skills/sf-view-gen/assets/templates/views/sections/logoCloud.tsx +2 -7
- package/skills/sf-view-gen/assets/templates/views/sections/pricing.client.tsx +14 -10
- package/skills/sf-view-gen/assets/templates/views/sections/pricing.tsx +2 -9
- package/skills/sf-view-gen/assets/templates/views/sections/products.client.tsx +100 -0
- package/skills/sf-view-gen/assets/templates/views/sections/products.tsx +19 -0
- package/skills/sf-view-gen/assets/templates/views/sections/schema.cta.ts +19 -0
- package/skills/sf-view-gen/assets/templates/views/sections/schema.faq.ts +18 -0
- package/skills/sf-view-gen/assets/templates/views/sections/schema.logoCloud.ts +19 -0
- package/skills/sf-view-gen/assets/templates/views/sections/schema.pricing.ts +26 -0
- package/skills/sf-view-gen/assets/templates/views/sections/schema.products.ts +39 -0
- package/skills/sf-view-gen/assets/templates/views/sections/schema.statistics.ts +25 -0
- package/skills/sf-view-gen/assets/templates/views/sections/schema.testimonials.ts +23 -0
- package/skills/sf-view-gen/assets/templates/views/sections/statistics.client.tsx +16 -8
- package/skills/sf-view-gen/assets/templates/views/sections/statistics.tsx +2 -7
- package/skills/sf-view-gen/assets/templates/views/sections/testimonials.client.tsx +13 -9
- package/skills/sf-view-gen/assets/templates/views/sections/testimonials.tsx +2 -5
- package/skills/{json-render-core/SKILL.md → sf-view-gen/references/json-render-core.md} +4 -27
- package/skills/{json-render-next/SKILL.md → sf-view-gen/references/json-render-next.md} +5 -8
- package/skills/{json-render-react/SKILL.md → sf-view-gen/references/json-render-react.md} +3 -5
- package/skills/{json-render-shadcn/SKILL.md → sf-view-gen/references/json-render-shadcn.md} +3 -5
- package/skills/sf-view-gen/references/views.md +58 -33
- package/skills/sf-view-gen/scripts/catalog.mjs +128 -0
- package/skills/sf-view-gen/scripts/schema-validate.mjs +132 -0
- package/skills/sf-view-gen/scripts/validate.mjs +838 -32
- package/skills/storefront-workflow/SKILL.md +179 -0
- package/skills/{storefront-best-practices/reference/design.md → storefront-workflow/references/design-consistency.md} +13 -29
- package/CHANGELOG.md +0 -61
- package/commands/sf-add-view.md +0 -26
- package/commands/sf-theme.md +0 -20
- package/commands/sf-view.md +0 -25
- package/skills/building-storefronts/SKILL.md +0 -178
- package/skills/building-storefronts/references/frontend-integration.md +0 -229
- package/skills/json-render-remotion/SKILL.md +0 -111
- package/skills/json-render-solid/SKILL.md +0 -204
- package/skills/nextjs-shadcn/SKILL.md +0 -367
- package/skills/nextjs-shadcn/references/architecture.md +0 -499
- package/skills/nextjs-shadcn/references/project-setup.md +0 -162
- package/skills/nextjs-shadcn/references/shadcn-platform.md +0 -261
- package/skills/nextjs-shadcn/references/sidebar.md +0 -274
- package/skills/nextjs-shadcn/references/styling.md +0 -555
- package/skills/sf-view-gen/assets/templates/views/category/index.tsx +0 -86
- package/skills/sf-view-gen/assets/templates/views/category/registry.tsx +0 -48
- package/skills/sf-view-gen/assets/templates/views/checkout/index.tsx +0 -192
- package/skills/sf-view-gen/assets/templates/views/checkout/registry.tsx +0 -45
- package/skills/sf-view-gen/assets/templates/views/product/index.tsx +0 -122
- package/skills/sf-view-gen/assets/templates/views/product/registry.tsx +0 -49
- package/skills/sf-view-gen/assets/templates/views/user-panel/index.tsx +0 -168
- package/skills/sf-view-gen/assets/templates/views/user-panel/registry.tsx +0 -49
- package/skills/shadcn-component-discovery/SKILL.md +0 -273
- package/skills/shadcn-component-discovery/references/registries.md +0 -226
- package/skills/shadcn-theming/SKILL.md +0 -104
- package/skills/storefront-best-practices/SKILL.md +0 -421
- package/skills/storefront-best-practices/reference/components/breadcrumbs.md +0 -123
- package/skills/storefront-best-practices/reference/components/cart-popup.md +0 -189
- package/skills/storefront-best-practices/reference/components/country-selector.md +0 -298
- package/skills/storefront-best-practices/reference/components/footer.md +0 -112
- package/skills/storefront-best-practices/reference/components/hero.md +0 -241
- package/skills/storefront-best-practices/reference/components/megamenu.md +0 -239
- package/skills/storefront-best-practices/reference/components/navbar.md +0 -397
- package/skills/storefront-best-practices/reference/components/popups.md +0 -221
- package/skills/storefront-best-practices/reference/components/product-card.md +0 -125
- package/skills/storefront-best-practices/reference/components/product-reviews.md +0 -217
- package/skills/storefront-best-practices/reference/components/product-slider.md +0 -174
- package/skills/storefront-best-practices/reference/components/search.md +0 -101
- package/skills/storefront-best-practices/reference/connecting-to-backend.md +0 -391
- package/skills/storefront-best-practices/reference/features/promotions.md +0 -307
- package/skills/storefront-best-practices/reference/features/wishlist.md +0 -230
- package/skills/storefront-best-practices/reference/layouts/account.md +0 -380
- package/skills/storefront-best-practices/reference/layouts/cart.md +0 -316
- package/skills/storefront-best-practices/reference/layouts/checkout.md +0 -486
- package/skills/storefront-best-practices/reference/layouts/home-page.md +0 -264
- package/skills/storefront-best-practices/reference/layouts/order-confirmation.md +0 -231
- package/skills/storefront-best-practices/reference/layouts/product-details.md +0 -527
- package/skills/storefront-best-practices/reference/layouts/product-listing.md +0 -520
- package/skills/storefront-best-practices/reference/layouts/static-pages.md +0 -356
- package/skills/storefront-best-practices/reference/medusa.md +0 -307
- /package/skills/{storefront-best-practices/reference → storefront-workflow/references}/mobile-responsiveness.md +0 -0
- /package/skills/{storefront-best-practices/reference → storefront-workflow/references}/seo.md +0 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// Thor Commerce view module: checkout
|
|
2
|
+
// Server Component module: elements + catalog entries + route + loader only.
|
|
3
|
+
// The client-side component mappings live in ./registry.tsx ('use client'); this
|
|
4
|
+
// module imports them as data so the RSC graph never evaluates @json-render/react
|
|
5
|
+
// on the server. Registered by importing it in ../index.ts. Recipe: recipes.json -> "checkout".
|
|
6
|
+
//
|
|
7
|
+
// The initial render does NOT read cookies here (this module is part of the
|
|
8
|
+
// client catalog graph). The catch-all page injects the read-only cart state;
|
|
9
|
+
// `CheckoutBoot` then loads the checkout data or reconciles a completed receipt
|
|
10
|
+
// through Server Actions.
|
|
11
|
+
|
|
12
|
+
import { getDataSource } from '../../store-context'
|
|
13
|
+
import { pageRoot } from '../../spec/page-root'
|
|
14
|
+
import type { StorefrontElement } from '../../spec/types'
|
|
15
|
+
import type { ViewModule } from '../types'
|
|
16
|
+
import { registry } from './registry'
|
|
17
|
+
import { checkoutConfirmationProps, checkoutViewProps } from './props'
|
|
18
|
+
import {
|
|
19
|
+
checkoutConfirmationPropsSchema,
|
|
20
|
+
checkoutEmptyPropsSchema,
|
|
21
|
+
checkoutPropsSchema,
|
|
22
|
+
} from './schema'
|
|
23
|
+
|
|
24
|
+
// The element tree is derived wiring; the rendered props and validation rules
|
|
25
|
+
// come from the user-owned ./props.ts module so they survive sync and removal.
|
|
26
|
+
// Four states: bootstrap (headless), confirmation (order), empty cart, or form.
|
|
27
|
+
const elements: Record<string, StorefrontElement> = {
|
|
28
|
+
'checkout.boot': {
|
|
29
|
+
type: 'CheckoutBoot',
|
|
30
|
+
props: {},
|
|
31
|
+
},
|
|
32
|
+
'checkout.confirmation': {
|
|
33
|
+
type: 'CheckoutConfirmation',
|
|
34
|
+
visible: { $state: '/lastReceipt' },
|
|
35
|
+
props: checkoutConfirmationProps,
|
|
36
|
+
on: { continueShopping: { action: 'continueShopping' } },
|
|
37
|
+
},
|
|
38
|
+
'checkout.empty': {
|
|
39
|
+
type: 'CheckoutEmpty',
|
|
40
|
+
visible: {
|
|
41
|
+
$and: [
|
|
42
|
+
{ $state: '/lastReceipt', not: true },
|
|
43
|
+
{ $state: '/cartQuantity', eq: 0 },
|
|
44
|
+
// Show the empty state when there is no active cart and no receipt to
|
|
45
|
+
// recover, or when an active cart exists (even empty). A signed receipt
|
|
46
|
+
// with no active cart is reconciled by the boot, not shown as empty.
|
|
47
|
+
{
|
|
48
|
+
$or: [{ $state: '/receiptAvailable', not: true }, { $state: '/cartId' }],
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
},
|
|
52
|
+
props: {},
|
|
53
|
+
on: { continueShopping: { action: 'continueShopping' } },
|
|
54
|
+
},
|
|
55
|
+
'checkout.view': {
|
|
56
|
+
type: 'Checkout',
|
|
57
|
+
visible: {
|
|
58
|
+
$and: [{ $state: '/lastReceipt', not: true }, { $state: '/cartQuantity', gt: 0 }],
|
|
59
|
+
},
|
|
60
|
+
props: checkoutViewProps,
|
|
61
|
+
on: {
|
|
62
|
+
placeOrder: { action: 'placeOrder' },
|
|
63
|
+
reconcileOrder: { action: 'reconcileOrder' },
|
|
64
|
+
syncCheckoutAddress: { action: 'syncCheckoutAddress' },
|
|
65
|
+
selectShippingMethod: { action: 'selectShippingMethod' },
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const catalog = {
|
|
71
|
+
CheckoutBoot: {
|
|
72
|
+
props: checkoutEmptyPropsSchema,
|
|
73
|
+
description: 'Headless checkout bootstrap: loads form data or reconciles a receipt.',
|
|
74
|
+
},
|
|
75
|
+
Checkout: {
|
|
76
|
+
props: checkoutPropsSchema,
|
|
77
|
+
description: 'Checkout form with order summary, shipping and payment steps.',
|
|
78
|
+
},
|
|
79
|
+
CheckoutEmpty: {
|
|
80
|
+
props: checkoutEmptyPropsSchema,
|
|
81
|
+
description: 'Empty-cart state for the checkout route.',
|
|
82
|
+
},
|
|
83
|
+
CheckoutConfirmation: {
|
|
84
|
+
props: checkoutConfirmationPropsSchema,
|
|
85
|
+
description: 'Order confirmation with fulfilment progress.',
|
|
86
|
+
},
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export const checkout: ViewModule = {
|
|
90
|
+
name: 'checkout',
|
|
91
|
+
elements,
|
|
92
|
+
catalog,
|
|
93
|
+
registry,
|
|
94
|
+
route: {
|
|
95
|
+
path: '/checkout',
|
|
96
|
+
loaderName: 'checkout',
|
|
97
|
+
layout: 'default',
|
|
98
|
+
metadata: {
|
|
99
|
+
title: 'Finalizar compra',
|
|
100
|
+
description: 'Completa tus datos de envío y pago para recibir tu pedido.',
|
|
101
|
+
},
|
|
102
|
+
entry: pageRoot(
|
|
103
|
+
'checkout',
|
|
104
|
+
['checkout.boot', 'checkout.confirmation', 'checkout.empty', 'checkout.view'],
|
|
105
|
+
elements,
|
|
106
|
+
),
|
|
107
|
+
},
|
|
108
|
+
loader: {
|
|
109
|
+
checkout: async () => ({ dataSource: getDataSource(), dataError: null }),
|
|
110
|
+
},
|
|
111
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Thor Commerce view props: checkout
|
|
2
|
+
// User-owned element props for the checkout view. Reconcile seeds this file once
|
|
3
|
+
// and never rewrites it; the derived view module (./index.tsx) imports these
|
|
4
|
+
// objects and binds them into the element tree, so edits here survive
|
|
5
|
+
// `storefront sync` and unit removal. Plain data only: no React and no server
|
|
6
|
+
// imports.
|
|
7
|
+
|
|
8
|
+
import { checkoutContent, confirmationContent } from './content'
|
|
9
|
+
|
|
10
|
+
export const checkoutViewProps = {
|
|
11
|
+
...checkoutContent,
|
|
12
|
+
// Backend-authoritative summary: line totals and cart totals come from the
|
|
13
|
+
// backend cart state, never computed here.
|
|
14
|
+
orderSummary: {
|
|
15
|
+
items: { $state: '/cart' },
|
|
16
|
+
totals: { $state: '/cartTotals' },
|
|
17
|
+
},
|
|
18
|
+
countries: { $state: '/checkoutCountries' },
|
|
19
|
+
shippingOptions: { $state: '/checkoutShippingOptions' },
|
|
20
|
+
selectedShippingOptionId: { $state: '/checkoutSelectedShippingOptionId' },
|
|
21
|
+
paymentMethods: { $state: '/checkoutPaymentMethods' },
|
|
22
|
+
fieldErrors: { $state: '/checkoutFieldErrors' },
|
|
23
|
+
values: {
|
|
24
|
+
email: { $state: '/checkout/email' },
|
|
25
|
+
firstName: { $state: '/checkout/firstName' },
|
|
26
|
+
lastName: { $state: '/checkout/lastName' },
|
|
27
|
+
phone: { $state: '/checkout/phone' },
|
|
28
|
+
address: { $state: '/checkout/address' },
|
|
29
|
+
city: { $state: '/checkout/city' },
|
|
30
|
+
state: { $state: '/checkout/state' },
|
|
31
|
+
zipCode: { $state: '/checkout/zipCode' },
|
|
32
|
+
country: { $state: '/checkout/country' },
|
|
33
|
+
paymentMethod: { $state: '/checkout/paymentMethod' },
|
|
34
|
+
cardNumber: { $state: '/checkout/cardNumber' },
|
|
35
|
+
expiryDate: { $state: '/checkout/expiryDate' },
|
|
36
|
+
cvv: { $state: '/checkout/cvv' },
|
|
37
|
+
cardName: { $state: '/checkout/cardName' },
|
|
38
|
+
saveInfo: { $state: '/checkout/saveInfo' },
|
|
39
|
+
sameAsBilling: { $state: '/checkout/sameAsBilling' },
|
|
40
|
+
newsletter: { $state: '/checkout/newsletter' },
|
|
41
|
+
promoCode: { $state: '/checkout/promoCode' },
|
|
42
|
+
},
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const checkoutConfirmationProps = {
|
|
46
|
+
// One atomic state path carries the order and its backend lines together.
|
|
47
|
+
receipt: { $state: '/lastReceipt' },
|
|
48
|
+
...confirmationContent,
|
|
49
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
// Thor Commerce view registry fragment: checkout
|
|
4
|
+
// Client boundary for the checkout view. Imports value bindings from
|
|
5
|
+
// @json-render/react and components from @thorprovider/blocks — both must stay
|
|
6
|
+
// on the client side of the RSC graph. Kept separate from index.tsx so the view
|
|
7
|
+
// spec (elements/catalog/route/loader) stays a pure Server Component module.
|
|
8
|
+
|
|
9
|
+
import { useEffect, useRef } from 'react'
|
|
10
|
+
import {
|
|
11
|
+
Button,
|
|
12
|
+
CheckoutForm1 as CheckoutBlock,
|
|
13
|
+
CheckoutConfirmation as CheckoutConfirmationBlock,
|
|
14
|
+
} from '@thorprovider/blocks'
|
|
15
|
+
import { useStateStore } from '@json-render/react'
|
|
16
|
+
import { loadCheckoutAction, reconcileReceiptAction } from '../../actions'
|
|
17
|
+
import type { CartStatePatch, PlaceOrderResult } from '../../commerce'
|
|
18
|
+
import type { ViewRegistry } from '../types'
|
|
19
|
+
import { checkoutConfirmationPropsSchema, checkoutPropsSchema } from './schema'
|
|
20
|
+
|
|
21
|
+
// Address fields whose change can complete the address; when the full address
|
|
22
|
+
// is present the handler syncs it to the backend and loads eligible shipping.
|
|
23
|
+
const ADDRESS_FIELDS = new Set([
|
|
24
|
+
'email',
|
|
25
|
+
'firstName',
|
|
26
|
+
'lastName',
|
|
27
|
+
'phone',
|
|
28
|
+
'address',
|
|
29
|
+
'city',
|
|
30
|
+
'state',
|
|
31
|
+
'zipCode',
|
|
32
|
+
'country',
|
|
33
|
+
])
|
|
34
|
+
|
|
35
|
+
export const registry: ViewRegistry = {
|
|
36
|
+
/**
|
|
37
|
+
* Headless bootstrap rendered on the checkout route. It loads the checkout
|
|
38
|
+
* data (countries/payment methods/cart) for an active cart, or reconciles a
|
|
39
|
+
* completed receipt through the cookie-authorized Server Action. It never
|
|
40
|
+
* reads a bare order/cart id.
|
|
41
|
+
*/
|
|
42
|
+
CheckoutBoot: () => {
|
|
43
|
+
const { state, set } = useStateStore()
|
|
44
|
+
const booted = useRef(false)
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
if (booted.current) return
|
|
47
|
+
booted.current = true
|
|
48
|
+
|
|
49
|
+
const applyResult = (result: PlaceOrderResult) => {
|
|
50
|
+
set('/checkoutPending', false)
|
|
51
|
+
if (result.status === 'order') {
|
|
52
|
+
// ONE atomic write carries the order and its backend lines, so an SSR
|
|
53
|
+
// refresh cannot drop the lines.
|
|
54
|
+
set('/lastReceipt', { order: result.order, lines: result.lines })
|
|
55
|
+
set('/orderStatus', 'complete')
|
|
56
|
+
set('/checkoutError', null)
|
|
57
|
+
set('/checkoutErrorKind', null)
|
|
58
|
+
set('/cart', [])
|
|
59
|
+
set('/cartTotals', null)
|
|
60
|
+
set('/cartId', null)
|
|
61
|
+
set('/cartQuantity', 0)
|
|
62
|
+
set('/cartCompleted', true)
|
|
63
|
+
set('/checkoutSelectedShippingOptionId', null)
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
set('/orderStatus', result.status === 'cart-error' ? 'error' : result.status)
|
|
67
|
+
set('/checkoutErrorKind', result.status)
|
|
68
|
+
set('/checkoutError', result.message)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const bootError = (scope: string) => (error: unknown) => {
|
|
72
|
+
// Never swallow: log a non-secret context line and surface a message.
|
|
73
|
+
console.error(
|
|
74
|
+
`[storefront/checkout] ${scope} failed`,
|
|
75
|
+
error instanceof Error ? error.message : error,
|
|
76
|
+
)
|
|
77
|
+
set('/checkoutPending', false)
|
|
78
|
+
set('/checkoutError', 'No se pudo cargar el checkout. Inténtalo de nuevo.')
|
|
79
|
+
set('/checkoutErrorKind', 'error')
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// 1. An active cart the backend already committed is an unresolved
|
|
83
|
+
// completion: reconcile IT first (never a blind retry).
|
|
84
|
+
// 2. Otherwise ANY active cart (even empty) loads the checkout form; the
|
|
85
|
+
// signed receipt is NOT auto-recovered while an active cart exists.
|
|
86
|
+
// 3. Only with no active cart does a signed receipt proof recover the
|
|
87
|
+
// completed order.
|
|
88
|
+
if (state.cartId && state.cartCompleted) {
|
|
89
|
+
set('/checkoutPending', true)
|
|
90
|
+
void reconcileReceiptAction().then(applyResult).catch(bootError('reconcileReceipt'))
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
if (state.cartId) {
|
|
94
|
+
set('/checkoutPending', true)
|
|
95
|
+
void loadCheckoutAction()
|
|
96
|
+
.then((result) => {
|
|
97
|
+
set('/checkoutPending', false)
|
|
98
|
+
if (!result.ok) {
|
|
99
|
+
set('/checkoutError', result.error)
|
|
100
|
+
set('/checkoutErrorKind', 'error')
|
|
101
|
+
return
|
|
102
|
+
}
|
|
103
|
+
const cart = result.cart as CartStatePatch
|
|
104
|
+
set('/cart', cart.cart)
|
|
105
|
+
set('/cartTotals', cart.cartTotals)
|
|
106
|
+
set('/cartId', cart.cartId)
|
|
107
|
+
set('/cartQuantity', cart.cartQuantity)
|
|
108
|
+
set('/cartSelectedShippingOptionId', cart.cartSelectedShippingOptionId)
|
|
109
|
+
set('/checkoutSelectedShippingOptionId', cart.cartSelectedShippingOptionId)
|
|
110
|
+
if (result.countries) set('/checkoutCountries', result.countries)
|
|
111
|
+
if (result.paymentMethods) set('/checkoutPaymentMethods', result.paymentMethods)
|
|
112
|
+
if (result.shippingOptions) set('/checkoutShippingOptions', result.shippingOptions)
|
|
113
|
+
set('/checkoutError', null)
|
|
114
|
+
})
|
|
115
|
+
.catch(bootError('loadCheckout'))
|
|
116
|
+
return
|
|
117
|
+
}
|
|
118
|
+
if (state.receiptAvailable) {
|
|
119
|
+
set('/checkoutPending', true)
|
|
120
|
+
void reconcileReceiptAction().then(applyResult).catch(bootError('reconcileReceipt'))
|
|
121
|
+
}
|
|
122
|
+
}, [])
|
|
123
|
+
return null
|
|
124
|
+
},
|
|
125
|
+
Checkout: ({ props, emit }) => {
|
|
126
|
+
const { state, set } = useStateStore()
|
|
127
|
+
const parsed = checkoutPropsSchema.parse(props)
|
|
128
|
+
const unresolved = state.checkoutErrorKind === 'unresolved'
|
|
129
|
+
const cartError = typeof state.cartError === 'string' ? state.cartError : undefined
|
|
130
|
+
const checkoutError = typeof state.checkoutError === 'string' ? state.checkoutError : undefined
|
|
131
|
+
return (
|
|
132
|
+
<CheckoutBlock
|
|
133
|
+
title={parsed.title}
|
|
134
|
+
description={parsed.description}
|
|
135
|
+
orderSummary={parsed.orderSummary}
|
|
136
|
+
totalsState={state.cartPending ? 'pending' : cartError ? 'error' : 'idle'}
|
|
137
|
+
totalsErrorLabel={cartError}
|
|
138
|
+
onRetryTotals={() => emit('syncCheckoutAddress')}
|
|
139
|
+
countries={parsed.countries}
|
|
140
|
+
shippingOptions={parsed.shippingOptions}
|
|
141
|
+
selectedShippingOptionId={parsed.selectedShippingOptionId ?? undefined}
|
|
142
|
+
onSelectShippingOption={(id) => {
|
|
143
|
+
set('/checkoutSelectedShippingOptionId', id)
|
|
144
|
+
emit('selectShippingMethod')
|
|
145
|
+
}}
|
|
146
|
+
paymentMethods={parsed.paymentMethods}
|
|
147
|
+
values={parsed.values}
|
|
148
|
+
fieldErrors={parsed.fieldErrors}
|
|
149
|
+
fieldDescriptions={parsed.fieldDescriptions}
|
|
150
|
+
collectCardFields={false}
|
|
151
|
+
onChange={(field, value) => {
|
|
152
|
+
set(`/checkout/${field}`, value)
|
|
153
|
+
if (ADDRESS_FIELDS.has(field)) emit('syncCheckoutAddress')
|
|
154
|
+
}}
|
|
155
|
+
onSubmit={() => emit('placeOrder')}
|
|
156
|
+
submitting={Boolean(state.checkoutPending)}
|
|
157
|
+
submitError={checkoutError}
|
|
158
|
+
// An ambiguous completion offers RECONCILE only; a definite failure
|
|
159
|
+
// (cart-with-error or pre-completion error) offers a controlled retry.
|
|
160
|
+
onRetrySubmit={unresolved ? () => emit('reconcileOrder') : () => emit('placeOrder')}
|
|
161
|
+
submitErrorActionLabel={unresolved ? 'Reconciliar' : 'Reintentar'}
|
|
162
|
+
/>
|
|
163
|
+
)
|
|
164
|
+
},
|
|
165
|
+
CheckoutEmpty: ({ emit }) => (
|
|
166
|
+
<div className="mx-auto max-w-md px-4 py-16 text-center">
|
|
167
|
+
<h1 className="text-2xl font-bold">Tu carrito está vacío</h1>
|
|
168
|
+
<p className="text-muted-foreground mt-2 text-sm">
|
|
169
|
+
Agrega productos para finalizar la compra.
|
|
170
|
+
</p>
|
|
171
|
+
<Button className="mt-6" onClick={() => emit('continueShopping')}>
|
|
172
|
+
Seguir comprando
|
|
173
|
+
</Button>
|
|
174
|
+
</div>
|
|
175
|
+
),
|
|
176
|
+
CheckoutConfirmation: ({ props, emit }) => {
|
|
177
|
+
const parsed = checkoutConfirmationPropsSchema.parse(props)
|
|
178
|
+
// Forward every neutral label from user-owned content to the block, so the
|
|
179
|
+
// curated flow never falls back to the block's non-neutral defaults.
|
|
180
|
+
return (
|
|
181
|
+
<CheckoutConfirmationBlock
|
|
182
|
+
order={parsed.receipt.order}
|
|
183
|
+
orderLines={parsed.receipt.lines}
|
|
184
|
+
fulfilmentDetails={parsed.fulfilmentDetails}
|
|
185
|
+
titleLabel={parsed.titleLabel}
|
|
186
|
+
orderPlacedLabel={parsed.orderPlacedLabel}
|
|
187
|
+
orderPlacedNoEmailLabel={parsed.orderPlacedNoEmailLabel}
|
|
188
|
+
itemsLabel={parsed.itemsLabel}
|
|
189
|
+
quantityLabel={parsed.quantityLabel}
|
|
190
|
+
subtotalLabel={parsed.subtotalLabel}
|
|
191
|
+
discountLabel={parsed.discountLabel}
|
|
192
|
+
shippingLabel={parsed.shippingLabel}
|
|
193
|
+
taxLabel={parsed.taxLabel}
|
|
194
|
+
totalLabel={parsed.totalLabel}
|
|
195
|
+
freeLabel={parsed.freeLabel}
|
|
196
|
+
amountUnavailableLabel={parsed.amountUnavailableLabel}
|
|
197
|
+
shippingAddressLabel={parsed.shippingAddressLabel}
|
|
198
|
+
billingAddressLabel={parsed.billingAddressLabel}
|
|
199
|
+
continueShoppingLabel={parsed.continueShoppingLabel}
|
|
200
|
+
contactSupportLabel={parsed.contactSupportLabel}
|
|
201
|
+
supportLabel={parsed.supportLabel}
|
|
202
|
+
progressLabel={parsed.progressLabel}
|
|
203
|
+
estimatedDeliveryLabel={parsed.estimatedDeliveryLabel}
|
|
204
|
+
currentStageLabel={parsed.currentStageLabel}
|
|
205
|
+
inProgressLabel={parsed.inProgressLabel}
|
|
206
|
+
expectedByLabel={parsed.expectedByLabel}
|
|
207
|
+
completedLabel={parsed.completedLabel}
|
|
208
|
+
statusLabel={parsed.statusLabel}
|
|
209
|
+
locationLabel={parsed.locationLabel}
|
|
210
|
+
trackOrderLabel={parsed.trackOrderLabel}
|
|
211
|
+
continueShoppingHref={parsed.continueShoppingHref ?? '#'}
|
|
212
|
+
supportHref={parsed.supportHref}
|
|
213
|
+
onContinueShopping={() => emit('continueShopping')}
|
|
214
|
+
/>
|
|
215
|
+
)
|
|
216
|
+
},
|
|
217
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
// Thor Commerce view schema fragment: checkout
|
|
2
|
+
// Zod mirrors of the block contracts for this view — the single source for the
|
|
3
|
+
// catalog entries and the client-side registry parsing (json-render does not
|
|
4
|
+
// validate props against the catalog itself, so the registry parses here).
|
|
5
|
+
// Kept free of React and server imports so the Server Component module and the
|
|
6
|
+
// 'use client' registry can share one contract. Block zod contracts are
|
|
7
|
+
// mirrored, not imported: @thorprovider/blocks ships zod v3 while the scaffold
|
|
8
|
+
// runs zod v4 and cross-major schema assignment breaks the catalog contract.
|
|
9
|
+
|
|
10
|
+
import { z } from 'zod'
|
|
11
|
+
|
|
12
|
+
// Canonical Money: a decimal string in major units, never cents.
|
|
13
|
+
const moneySchema = z.object({
|
|
14
|
+
amount: z.string(),
|
|
15
|
+
currencyCode: z.string(),
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
// Mirrors the CheckoutForm1FormData block contract. Card fields stay in the
|
|
19
|
+
// shape (the block requires every key) but are never collected at launch.
|
|
20
|
+
export const checkoutFormDataSchema = z.object({
|
|
21
|
+
email: z.string(),
|
|
22
|
+
firstName: z.string(),
|
|
23
|
+
lastName: z.string(),
|
|
24
|
+
phone: z.string(),
|
|
25
|
+
address: z.string(),
|
|
26
|
+
city: z.string(),
|
|
27
|
+
state: z.string(),
|
|
28
|
+
zipCode: z.string(),
|
|
29
|
+
country: z.string(),
|
|
30
|
+
paymentMethod: z.string(),
|
|
31
|
+
cardNumber: z.string(),
|
|
32
|
+
expiryDate: z.string(),
|
|
33
|
+
cvv: z.string(),
|
|
34
|
+
cardName: z.string(),
|
|
35
|
+
saveInfo: z.boolean(),
|
|
36
|
+
sameAsBilling: z.boolean(),
|
|
37
|
+
newsletter: z.boolean(),
|
|
38
|
+
promoCode: z.string(),
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
// Mirrors CheckoutForm1OrderSummaryItem: backend line total preferred, legacy
|
|
42
|
+
// numeric price accepted only as a fallback; optional image omitted when absent.
|
|
43
|
+
export const orderSummaryItemSchema = z.object({
|
|
44
|
+
id: z.union([z.string(), z.number()]),
|
|
45
|
+
name: z.string(),
|
|
46
|
+
variant: z.string().optional(),
|
|
47
|
+
quantity: z.number(),
|
|
48
|
+
lineTotal: moneySchema.optional(),
|
|
49
|
+
price: z.number().optional(),
|
|
50
|
+
image: z.string().optional(),
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
// Mirrors CheckoutForm1Totals (backend-authoritative).
|
|
54
|
+
export const checkoutTotalsSchema = z.object({
|
|
55
|
+
subtotal: moneySchema,
|
|
56
|
+
discount: moneySchema.optional(),
|
|
57
|
+
tax: moneySchema.optional(),
|
|
58
|
+
shipping: moneySchema.optional(),
|
|
59
|
+
total: moneySchema,
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
export const orderSummarySchema = z.object({
|
|
63
|
+
items: z.array(orderSummaryItemSchema),
|
|
64
|
+
totals: checkoutTotalsSchema.optional(),
|
|
65
|
+
shipping: z.number().optional(),
|
|
66
|
+
tax: z.number().optional(),
|
|
67
|
+
discount: z.number().optional(),
|
|
68
|
+
promoDiscount: z.number().optional(),
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
export const countryOptionSchema = z.object({
|
|
72
|
+
value: z.string(),
|
|
73
|
+
label: z.string(),
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
// Mirrors CheckoutForm1ShippingOption (backend shipping method id).
|
|
77
|
+
export const shippingOptionSchema = z.object({
|
|
78
|
+
id: z.string(),
|
|
79
|
+
name: z.string(),
|
|
80
|
+
price: moneySchema.optional(),
|
|
81
|
+
description: z.string().optional(),
|
|
82
|
+
estimatedDays: z.string().optional(),
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
// Mirrors CheckoutFieldErrors: per-field structured messages.
|
|
86
|
+
export const checkoutFieldErrorsSchema = z.record(
|
|
87
|
+
z.string(),
|
|
88
|
+
z.array(z.object({ message: z.string().optional() })),
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
// Mirrors the PaymentMethod contract from @thorprovider/types.
|
|
92
|
+
export const paymentMethodSchema = z.object({
|
|
93
|
+
id: z.string(),
|
|
94
|
+
name: z.string(),
|
|
95
|
+
type: z.enum([
|
|
96
|
+
'card',
|
|
97
|
+
'wallet',
|
|
98
|
+
'bank_transfer',
|
|
99
|
+
'buy_now_pay_later',
|
|
100
|
+
'cash_on_delivery',
|
|
101
|
+
'crypto',
|
|
102
|
+
'other',
|
|
103
|
+
]),
|
|
104
|
+
isEnabled: z.boolean(),
|
|
105
|
+
requiresSetup: z.boolean(),
|
|
106
|
+
description: z.string().optional(),
|
|
107
|
+
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
108
|
+
features: z
|
|
109
|
+
.object({
|
|
110
|
+
saveForLater: z.boolean().optional(),
|
|
111
|
+
installmentPayment: z.boolean().optional(),
|
|
112
|
+
recurring: z.boolean().optional(),
|
|
113
|
+
})
|
|
114
|
+
.optional(),
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
export const checkoutPropsSchema = z.object({
|
|
118
|
+
title: z.string().optional(),
|
|
119
|
+
description: z.string().optional(),
|
|
120
|
+
orderSummary: orderSummarySchema.optional(),
|
|
121
|
+
countries: z.array(countryOptionSchema).optional(),
|
|
122
|
+
shippingOptions: z.array(shippingOptionSchema).optional(),
|
|
123
|
+
// Nullable: the state initializes to `null` and the block prop is optional.
|
|
124
|
+
selectedShippingOptionId: z.string().nullable().optional(),
|
|
125
|
+
paymentMethods: z.array(paymentMethodSchema).optional(),
|
|
126
|
+
values: checkoutFormDataSchema,
|
|
127
|
+
fieldErrors: checkoutFieldErrorsSchema.optional(),
|
|
128
|
+
fieldDescriptions: z.record(z.string(), z.string()).optional(),
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
// The empty-cart surface has no props; declared so the catalog is complete.
|
|
132
|
+
export const checkoutEmptyPropsSchema = z.object({})
|
|
133
|
+
|
|
134
|
+
export const fulfilmentStageSchema = z.enum(['Confirmed', 'Packed', 'Shipped', 'Delivered'])
|
|
135
|
+
|
|
136
|
+
const orderAddressSchema = z.object({
|
|
137
|
+
name: z.string(),
|
|
138
|
+
lines: z.array(z.string()),
|
|
139
|
+
method: z.string().optional(),
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
const orderTotalsSchema = z.object({
|
|
143
|
+
subtotal: moneySchema.optional(),
|
|
144
|
+
discount: moneySchema.optional(),
|
|
145
|
+
shipping: moneySchema.optional(),
|
|
146
|
+
tax: moneySchema.optional(),
|
|
147
|
+
total: moneySchema.optional(),
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
// Mirrors the CheckoutConfirmation Order block contract: backend Money totals,
|
|
151
|
+
// optional fields rendered only when the backend supplies them (no fabricated
|
|
152
|
+
// estimate or stage).
|
|
153
|
+
export const storefrontOrderSchema = z.object({
|
|
154
|
+
number: z.string(),
|
|
155
|
+
placedAt: z.string(),
|
|
156
|
+
email: z.string().optional(),
|
|
157
|
+
currentStage: fulfilmentStageSchema.optional(),
|
|
158
|
+
estimatedDelivery: z.string().nullable().optional(),
|
|
159
|
+
trackingHref: z.string().optional(),
|
|
160
|
+
shipping: orderAddressSchema.optional(),
|
|
161
|
+
billing: orderAddressSchema.optional(),
|
|
162
|
+
totals: orderTotalsSchema,
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
export const orderLineSchema = z.object({
|
|
166
|
+
id: z.string(),
|
|
167
|
+
name: z.string(),
|
|
168
|
+
variant: z.string().optional(),
|
|
169
|
+
quantity: z.number(),
|
|
170
|
+
lineTotal: moneySchema,
|
|
171
|
+
unitPrice: moneySchema.optional(),
|
|
172
|
+
image: z.string().optional(),
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
export const fulfilmentStageDetailSchema = z.object({
|
|
176
|
+
stage: fulfilmentStageSchema,
|
|
177
|
+
summary: z.string(),
|
|
178
|
+
location: z.string(),
|
|
179
|
+
at: z.string().nullable(),
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
// The atomic receipt: the order and its backend lines arrive together.
|
|
183
|
+
export const receiptSchema = z.object({
|
|
184
|
+
order: storefrontOrderSchema,
|
|
185
|
+
lines: z.array(orderLineSchema),
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
// Mirrors the CheckoutConfirmation props, including every neutral label the
|
|
189
|
+
// host forwards. Labels are declared here so `z.object` does not strip them;
|
|
190
|
+
// the registry forwards them to the block instead of relying on its defaults.
|
|
191
|
+
export const checkoutConfirmationPropsSchema = z.object({
|
|
192
|
+
receipt: receiptSchema,
|
|
193
|
+
fulfilmentDetails: z.record(z.string(), fulfilmentStageDetailSchema).optional(),
|
|
194
|
+
titleLabel: z.string().optional(),
|
|
195
|
+
orderPlacedLabel: z.string().optional(),
|
|
196
|
+
orderPlacedNoEmailLabel: z.string().optional(),
|
|
197
|
+
itemsLabel: z.string().optional(),
|
|
198
|
+
quantityLabel: z.string().optional(),
|
|
199
|
+
subtotalLabel: z.string().optional(),
|
|
200
|
+
discountLabel: z.string().optional(),
|
|
201
|
+
shippingLabel: z.string().optional(),
|
|
202
|
+
taxLabel: z.string().optional(),
|
|
203
|
+
totalLabel: z.string().optional(),
|
|
204
|
+
freeLabel: z.string().optional(),
|
|
205
|
+
amountUnavailableLabel: z.string().optional(),
|
|
206
|
+
shippingAddressLabel: z.string().optional(),
|
|
207
|
+
billingAddressLabel: z.string().optional(),
|
|
208
|
+
continueShoppingLabel: z.string().optional(),
|
|
209
|
+
contactSupportLabel: z.string().optional(),
|
|
210
|
+
supportLabel: z.string().optional(),
|
|
211
|
+
progressLabel: z.string().optional(),
|
|
212
|
+
estimatedDeliveryLabel: z.string().optional(),
|
|
213
|
+
currentStageLabel: z.string().optional(),
|
|
214
|
+
inProgressLabel: z.string().optional(),
|
|
215
|
+
expectedByLabel: z.string().optional(),
|
|
216
|
+
completedLabel: z.string().optional(),
|
|
217
|
+
statusLabel: z.string().optional(),
|
|
218
|
+
locationLabel: z.string().optional(),
|
|
219
|
+
trackOrderLabel: z.string().optional(),
|
|
220
|
+
continueShoppingHref: z.string().optional(),
|
|
221
|
+
supportHref: z.string().optional(),
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
export type CheckoutFormData = z.infer<typeof checkoutFormDataSchema>
|
|
225
|
+
export type OrderSummary = z.infer<typeof orderSummarySchema>
|
|
226
|
+
export type CheckoutProps = z.infer<typeof checkoutPropsSchema>
|
|
227
|
+
export type StorefrontOrder = z.infer<typeof storefrontOrderSchema>
|
|
228
|
+
export type OrderLine = z.infer<typeof orderLineSchema>
|
|
229
|
+
export type CheckoutConfirmationProps = z.infer<typeof checkoutConfirmationPropsSchema>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"view": "home",
|
|
3
|
+
"slots": [
|
|
4
|
+
{ "id": "home.hero", "slot": "hero", "default": "StorefrontHero1" },
|
|
5
|
+
{ "id": "home.benefits", "slot": "benefits", "default": "Feature1" },
|
|
6
|
+
{ "id": "home.categories", "slot": "categories", "default": "ProductCategory2" }
|
|
7
|
+
],
|
|
8
|
+
"sections": {
|
|
9
|
+
"faq": {
|
|
10
|
+
"elementType": "Faq",
|
|
11
|
+
"registryModule": "../sections/faq.client",
|
|
12
|
+
"registryExport": "faqRegistry",
|
|
13
|
+
"propsModule": "./content",
|
|
14
|
+
"defaultProps": "{ items: [] }"
|
|
15
|
+
},
|
|
16
|
+
"cta": {
|
|
17
|
+
"elementType": "Cta",
|
|
18
|
+
"registryModule": "../sections/cta.client",
|
|
19
|
+
"registryExport": "ctaRegistry",
|
|
20
|
+
"propsModule": "./content",
|
|
21
|
+
"defaultProps": "{}"
|
|
22
|
+
},
|
|
23
|
+
"products": {
|
|
24
|
+
"elementType": "Products",
|
|
25
|
+
"registryModule": "../sections/products.client",
|
|
26
|
+
"registryExport": "productsRegistry",
|
|
27
|
+
"propsModule": "./content",
|
|
28
|
+
"defaultProps": "{}",
|
|
29
|
+
"dataBound": true,
|
|
30
|
+
"defaultVariant": "ProductCard1"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|