@01.software/init 0.9.2 → 0.10.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.
Files changed (116) hide show
  1. package/dist/ai-docs.d.ts +13 -0
  2. package/dist/ai-docs.js +0 -0
  3. package/dist/browser-auth-CJDrpp5T.d.ts +11 -0
  4. package/dist/{chunk-UA7WNT2F.js → chunk-4LHYICUL.js} +1 -1
  5. package/dist/chunk-4LHYICUL.js.map +1 -0
  6. package/dist/{chunk-R4GGO33X.js → chunk-NJ4X7VNK.js} +1 -1
  7. package/dist/{chunk-R4GGO33X.js.map → chunk-NJ4X7VNK.js.map} +1 -1
  8. package/dist/chunk-Q6MSORYN.js +0 -0
  9. package/dist/chunk-STM4DKVZ.js +183 -0
  10. package/dist/chunk-STM4DKVZ.js.map +1 -0
  11. package/dist/{chunk-ENQSB4OF.js → chunk-WDWJ73KP.js} +40 -214
  12. package/dist/chunk-WDWJ73KP.js.map +1 -0
  13. package/dist/create-app-templates/ecommerce/AGENTS.md +88 -0
  14. package/dist/create-app-templates/ecommerce/CHANGELOG.md +30 -0
  15. package/dist/create-app-templates/ecommerce/CLAUDE.md +1 -0
  16. package/dist/create-app-templates/ecommerce/README.md +139 -0
  17. package/dist/create-app-templates/ecommerce/app/api/auth/login/route.ts +30 -0
  18. package/dist/create-app-templates/ecommerce/app/api/auth/logout/route.ts +18 -0
  19. package/dist/create-app-templates/ecommerce/app/api/auth/register/route.ts +41 -0
  20. package/dist/create-app-templates/ecommerce/app/api/cart/clear/route.ts +12 -0
  21. package/dist/create-app-templates/ecommerce/app/api/cart/items/route.ts +45 -0
  22. package/dist/create-app-templates/ecommerce/app/api/cart/route.ts +14 -0
  23. package/dist/create-app-templates/ecommerce/app/api/checkout/payment-return/route.ts +86 -0
  24. package/dist/create-app-templates/ecommerce/app/api/checkout/reconcile/route.ts +50 -0
  25. package/dist/create-app-templates/ecommerce/app/api/checkout/route.ts +41 -0
  26. package/dist/create-app-templates/ecommerce/app/cart/page.tsx +10 -0
  27. package/dist/create-app-templates/ecommerce/app/checkout/page.tsx +10 -0
  28. package/dist/create-app-templates/ecommerce/app/checkout/success/page.tsx +34 -0
  29. package/dist/create-app-templates/ecommerce/app/favicon.ico +0 -0
  30. package/dist/create-app-templates/ecommerce/app/globals.css +67 -0
  31. package/dist/create-app-templates/ecommerce/app/layout.tsx +23 -0
  32. package/dist/create-app-templates/ecommerce/app/login/page.tsx +11 -0
  33. package/dist/create-app-templates/ecommerce/app/page.tsx +5 -0
  34. package/dist/create-app-templates/ecommerce/app/products/[slug]/page.tsx +46 -0
  35. package/dist/create-app-templates/ecommerce/app/products/page.tsx +45 -0
  36. package/dist/create-app-templates/ecommerce/app/register/page.tsx +11 -0
  37. package/dist/create-app-templates/ecommerce/app/webhook/payment/route.ts +20 -0
  38. package/dist/create-app-templates/ecommerce/app-config.ts +54 -0
  39. package/dist/create-app-templates/ecommerce/components/auth/auth-form.tsx +109 -0
  40. package/dist/create-app-templates/ecommerce/components/cart/cart-content.tsx +119 -0
  41. package/dist/create-app-templates/ecommerce/components/checkout/checkout-form.tsx +267 -0
  42. package/dist/create-app-templates/ecommerce/components/checkout/checkout-reconcile.tsx +78 -0
  43. package/dist/create-app-templates/ecommerce/components/layout/account-nav.tsx +48 -0
  44. package/dist/create-app-templates/ecommerce/components/layout/account-slot.tsx +12 -0
  45. package/dist/create-app-templates/ecommerce/components/layout/cart-link.tsx +13 -0
  46. package/dist/create-app-templates/ecommerce/components/layout/page-shell.tsx +11 -0
  47. package/dist/create-app-templates/ecommerce/components/layout/site-header.tsx +22 -0
  48. package/dist/create-app-templates/ecommerce/components/product/add-to-cart.tsx +116 -0
  49. package/dist/create-app-templates/ecommerce/components/product/product-card.tsx +50 -0
  50. package/dist/create-app-templates/ecommerce/components/product/product-gallery.tsx +39 -0
  51. package/dist/create-app-templates/ecommerce/data/mock-catalog.json +173 -0
  52. package/dist/create-app-templates/ecommerce/eslint.config.mjs +18 -0
  53. package/dist/create-app-templates/ecommerce/lib/cart/cookie.ts +40 -0
  54. package/dist/create-app-templates/ecommerce/lib/cart/normalize.ts +32 -0
  55. package/dist/create-app-templates/ecommerce/lib/cart/parse-cart-request.ts +56 -0
  56. package/dist/create-app-templates/ecommerce/lib/cart/route-helpers.ts +17 -0
  57. package/dist/create-app-templates/ecommerce/lib/cart/select-provider.ts +44 -0
  58. package/dist/create-app-templates/ecommerce/lib/cart/server-cart.ts +96 -0
  59. package/dist/create-app-templates/ecommerce/lib/cart/sync-on-login.server.ts +34 -0
  60. package/dist/create-app-templates/ecommerce/lib/cart/use-cart.tsx +151 -0
  61. package/dist/create-app-templates/ecommerce/lib/checkout/checkout-errors.ts +22 -0
  62. package/dist/create-app-templates/ecommerce/lib/checkout/checkout-provider.ts +28 -0
  63. package/dist/create-app-templates/ecommerce/lib/checkout/parse-checkout-payload.ts +76 -0
  64. package/dist/create-app-templates/ecommerce/lib/checkout/start-checkout.ts +63 -0
  65. package/dist/create-app-templates/ecommerce/lib/checkout/types.ts +3 -0
  66. package/dist/create-app-templates/ecommerce/lib/commerce/adapters/mock.ts +336 -0
  67. package/dist/create-app-templates/ecommerce/lib/commerce/adapters/software-mappers.ts +312 -0
  68. package/dist/create-app-templates/ecommerce/lib/commerce/adapters/software.ts +913 -0
  69. package/dist/create-app-templates/ecommerce/lib/commerce/product-summary.ts +37 -0
  70. package/dist/create-app-templates/ecommerce/lib/commerce/provider.server.ts +60 -0
  71. package/dist/create-app-templates/ecommerce/lib/commerce/provider.ts +96 -0
  72. package/dist/create-app-templates/ecommerce/lib/commerce/stock.ts +37 -0
  73. package/dist/create-app-templates/ecommerce/lib/commerce/types.ts +206 -0
  74. package/dist/create-app-templates/ecommerce/lib/commerce/variant-selection.ts +23 -0
  75. package/dist/create-app-templates/ecommerce/lib/customer/auth-actions.ts +131 -0
  76. package/dist/create-app-templates/ecommerce/lib/customer/cart-sync.ts +44 -0
  77. package/dist/create-app-templates/ecommerce/lib/customer/client.server.ts +109 -0
  78. package/dist/create-app-templates/ecommerce/lib/customer/current-customer.ts +15 -0
  79. package/dist/create-app-templates/ecommerce/lib/customer/route-guard.ts +58 -0
  80. package/dist/create-app-templates/ecommerce/lib/customer/route-helpers.ts +75 -0
  81. package/dist/create-app-templates/ecommerce/lib/customer/session.ts +108 -0
  82. package/dist/create-app-templates/ecommerce/lib/format.ts +7 -0
  83. package/dist/create-app-templates/ecommerce/lib/payment/adapters/mock.ts +84 -0
  84. package/dist/create-app-templates/ecommerce/lib/payment/adapters/portone.ts +254 -0
  85. package/dist/create-app-templates/ecommerce/lib/payment/adapters/tosspayments.ts +287 -0
  86. package/dist/create-app-templates/ecommerce/lib/payment/amount-gate.ts +86 -0
  87. package/dist/create-app-templates/ecommerce/lib/payment/provider.server.ts +51 -0
  88. package/dist/create-app-templates/ecommerce/lib/payment/provider.ts +18 -0
  89. package/dist/create-app-templates/ecommerce/lib/payment/sync-order-payment.ts +96 -0
  90. package/dist/create-app-templates/ecommerce/lib/payment/types.ts +71 -0
  91. package/dist/create-app-templates/ecommerce/lib/server-only-guard.ts +20 -0
  92. package/dist/create-app-templates/ecommerce/next-env.d.ts +6 -0
  93. package/dist/create-app-templates/ecommerce/next.config.ts +16 -0
  94. package/dist/create-app-templates/ecommerce/package.json +33 -0
  95. package/dist/create-app-templates/ecommerce/postcss.config.mjs +7 -0
  96. package/dist/create-app-templates/ecommerce/tests/customer-auth.test.ts +263 -0
  97. package/dist/create-app-templates/ecommerce/tests/customer-cart.test.ts +392 -0
  98. package/dist/create-app-templates/ecommerce/tests/domain.test.ts +1537 -0
  99. package/dist/create-app-templates/ecommerce/tsconfig.json +35 -0
  100. package/dist/create-app-templates/registry.json +66 -0
  101. package/dist/create-app.d.ts +40 -0
  102. package/dist/create-app.js +652 -0
  103. package/dist/create-app.js.map +1 -0
  104. package/dist/detect-Bjxp9wcS.d.ts +13 -0
  105. package/dist/file-ops.d.ts +21 -0
  106. package/dist/file-ops.js +1 -1
  107. package/dist/index.d.ts +2 -0
  108. package/dist/index.js +4 -3
  109. package/dist/index.js.map +1 -1
  110. package/dist/init.d.ts +40 -0
  111. package/dist/init.js +4 -3
  112. package/dist/templates.d.ts +27 -0
  113. package/dist/templates.js +1 -1
  114. package/package.json +31 -15
  115. package/dist/chunk-ENQSB4OF.js.map +0 -1
  116. package/dist/chunk-UA7WNT2F.js.map +0 -1
@@ -0,0 +1,30 @@
1
+ # ecommerce
2
+
3
+ ## 0.1.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [b011902]
8
+ - Updated dependencies [9d1b632]
9
+ - Updated dependencies [d28ad57]
10
+ - Updated dependencies [9cb8a41]
11
+ - Updated dependencies [a36d724]
12
+ - Updated dependencies [426c3ed]
13
+ - Updated dependencies [9261a15]
14
+ - Updated dependencies [8d8e700]
15
+ - Updated dependencies [9f3186c]
16
+ - Updated dependencies [a2fd60b]
17
+ - Updated dependencies [b1bdd49]
18
+ - Updated dependencies [e3b16a5]
19
+ - Updated dependencies [27a6502]
20
+ - Updated dependencies [5aa136f]
21
+ - Updated dependencies [87da012]
22
+ - Updated dependencies [3f261a6]
23
+ - Updated dependencies [17afb8e]
24
+ - Updated dependencies [a968dfd]
25
+ - Updated dependencies [4a13b7b]
26
+ - Updated dependencies [1d8ee45]
27
+ - Updated dependencies [923b0a2]
28
+ - Updated dependencies [2986732]
29
+ - Updated dependencies [8ded73e]
30
+ - @01.software/sdk@0.42.0
@@ -0,0 +1 @@
1
+ @AGENTS.md
@@ -0,0 +1,139 @@
1
+ # Ecommerce Template
2
+
3
+ Minimal Next.js commerce storefront template with a pluggable payment-adapter
4
+ seam. A single source scaffolds either supported payment provider; the active
5
+ provider for a generated app is recorded in `app-config.ts` and its keys live in
6
+ `.env.local.example`.
7
+
8
+ ## What is included
9
+
10
+ - App Router storefront routes for product listing, product detail, cart,
11
+ checkout, and payment success.
12
+ - Adapter boundaries under `lib/commerce` and `lib/payment`.
13
+ - Demo commerce and payment adapters backed by `data/mock-catalog.json`.
14
+ - Automatic `@01.software/sdk/server` commerce adapter when 01.software SDK
15
+ credentials are present.
16
+ - A payment-adapter registry (`lib/payment/provider.server.ts`) that selects a
17
+ provider from environment credentials, with first-class adapters for:
18
+ - **PortOne** — `@portone/browser-sdk/v2` on the client, `@portone/server-sdk`
19
+ on the server.
20
+ - **TossPayments** — the TossPayments browser SDK on the client, the
21
+ TossPayments payment REST API on the server.
22
+ - A theming seam: a CSS-variable token layer in `app/globals.css` plus Tailwind.
23
+ - Server-side order pricing, stock checks, and payment sync invariants.
24
+ - Local persisted cart state with Zustand.
25
+
26
+ ## Scaffolding vs. this in-repo source
27
+
28
+ This directory is the single in-repo source and intentionally ships **both**
29
+ payment adapters so it builds and tests with every provider present.
30
+ `create-01-software-app` copies it, prunes the unused payment adapter (and its
31
+ dependency), generates `app-config.ts` and `.env.local.example` from
32
+ `templates/registry.json`, and pins the SDK version — producing a self-contained
33
+ single-provider app.
34
+
35
+ To add a new payment provider, see **Add a payment provider** below.
36
+
37
+ ## Run locally
38
+
39
+ ```bash
40
+ pnpm install
41
+ pnpm dev
42
+ ```
43
+
44
+ Without SDK or payment credentials, the app uses demo providers:
45
+
46
+ ```bash
47
+ # 01.software keys omitted -> demo catalog
48
+ # payment keys omitted -> local demo payment completion
49
+ ```
50
+
51
+ Mock orders are persisted to `.mock-orders.json` so redirect and success routes
52
+ can observe the same order during local development. This file is ignored and is
53
+ not a production store.
54
+
55
+ To use the Console ecommerce SDK adapter, set both SDK keys:
56
+
57
+ ```bash
58
+ NEXT_PUBLIC_SOFTWARE_PUBLISHABLE_KEY=pk_...
59
+ SOFTWARE_SECRET_KEY=sk_...
60
+ SOFTWARE_API_URL=https://your-console-origin.example
61
+ SOFTWARE_SHIPPING_AMOUNT=3000
62
+ SOFTWARE_FREE_SHIPPING_ABOVE_AMOUNT=100000
63
+ ```
64
+
65
+ `SOFTWARE_API_URL`, `SOFTWARE_SHIPPING_AMOUNT`, and
66
+ `SOFTWARE_FREE_SHIPPING_ABOVE_AMOUNT` are optional. The SDK adapter resolves
67
+ payment return/webhook requests through the SDK `transactions` collection first,
68
+ then falls back to a local `.software-orders.json` development index. This file
69
+ is ignored and is not a production store.
70
+
71
+ ## Payment provider keys
72
+
73
+ ### PortOne
74
+
75
+ ```bash
76
+ PORTONE_API_SECRET=...
77
+ NEXT_PUBLIC_PORTONE_STORE_ID=store_...
78
+ NEXT_PUBLIC_PORTONE_CHANNEL_KEY=channel-key-...
79
+ NEXT_PUBLIC_PORTONE_PAY_METHOD=CARD
80
+ PORTONE_WEBHOOK_SECRET=...
81
+ ```
82
+
83
+ `NEXT_PUBLIC_PORTONE_PAY_METHOD` is optional and defaults to `CARD`.
84
+ `PORTONE_WEBHOOK_SECRET` is required in production. For local development without
85
+ signed webhooks, set `PORTONE_ALLOW_UNSIGNED_WEBHOOKS=true`. When the secret is
86
+ present, the webhook route verifies PortOne webhook signatures through
87
+ `@portone/server-sdk`.
88
+
89
+ ### TossPayments
90
+
91
+ ```bash
92
+ TOSSPAYMENTS_SECRET_KEY=test_sk_...
93
+ NEXT_PUBLIC_TOSSPAYMENTS_CLIENT_KEY=test_ck_...
94
+ NEXT_PUBLIC_TOSSPAYMENTS_CUSTOMER_KEY=customer_...
95
+ ```
96
+
97
+ `NEXT_PUBLIC_TOSSPAYMENTS_CUSTOMER_KEY` is optional. If omitted, the browser SDK
98
+ uses TossPayments' anonymous customer key. TossPayments redirects back with
99
+ `paymentKey`, `orderId`, and `amount`; the template validates the returned
100
+ amount against the stored order and confirms the payment on
101
+ `/api/checkout/payment-return` before showing the success page.
102
+ `TOSSPAYMENTS_API_BASE_URL` is optional and defaults to
103
+ `https://api.tosspayments.com/v1`.
104
+
105
+ ## Add a payment provider
106
+
107
+ 1. Add `lib/payment/adapters/<pg>.ts` implementing the `PaymentProvider` port
108
+ and exporting `create<Pg>Provider` plus `has<Pg>Credentials`.
109
+ 2. Register it in `lib/payment/provider.server.ts` inside a matching pair of
110
+ `scaffold:provider:<id>` markers (import + registry entry).
111
+ 3. Add a `ClientPaymentRequest` union case in `lib/payment/types.ts`.
112
+ 4. Add a client branch (and its `scaffold:provider:<id>` markers) in
113
+ `components/checkout/checkout-form.tsx`.
114
+ 5. Add the provider to `templates/registry.json` so the scaffolder offers it.
115
+
116
+ The template depends on the published `@01.software/sdk` package so it can be
117
+ copied out of this monorepo and installed as a standalone Next.js app.
118
+
119
+ The starter `software` product listing uses a simple products query followed by
120
+ per-product detail lookups so the UI can share one normalized product-detail
121
+ shape. It isolates individual detail failures with `Promise.allSettled`. For a
122
+ larger storefront, replace that listing path with the Console tenant's preferred
123
+ catalog/listing API shape.
124
+
125
+ ## Verify
126
+
127
+ ```bash
128
+ pnpm test
129
+ pnpm check-types
130
+ pnpm lint
131
+ pnpm build
132
+ ```
133
+
134
+ ## Important boundary
135
+
136
+ The cart stores only `variantId` and `quantity`. Checkout sends those lines plus
137
+ customer and shipping details to the server; server code re-fetches variants,
138
+ checks stock, recomputes subtotal/shipping/total, creates a pending order, and
139
+ then synchronizes payment state.
@@ -0,0 +1,30 @@
1
+ import { syncActiveCartCookieOnLogin } from "@/lib/cart/sync-on-login.server.ts";
2
+ import { login } from "@/lib/customer/auth-actions";
3
+ import { assertSameOriginJson } from "@/lib/customer/route-guard.ts";
4
+ import { authErrorResponse, parseLogin } from "@/lib/customer/route-helpers.ts";
5
+ import { setSessionToken } from "@/lib/customer/session";
6
+
7
+ // Log in: delegate to `client.customer.auth.login`, then stamp the customer JWT
8
+ // into the HttpOnly session cookie and slave the active-cart cookie to the
9
+ // customer's server-owned cart (union a pre-login guest cart, else load theirs).
10
+ // CSRF-guarded (same-origin + JSON).
11
+ export async function POST(request: Request) {
12
+ try {
13
+ assertSameOriginJson(request);
14
+ const input = parseLogin(await request.json());
15
+ const result = await login(input);
16
+ if (!result.ok) {
17
+ return Response.json(
18
+ { code: "login_failed", message: result.message },
19
+ { status: result.status },
20
+ );
21
+ }
22
+ if (result.token) {
23
+ await setSessionToken(result.token);
24
+ await syncActiveCartCookieOnLogin(result.token);
25
+ }
26
+ return Response.json({ customer: result.customer });
27
+ } catch (error) {
28
+ return authErrorResponse(error);
29
+ }
30
+ }
@@ -0,0 +1,18 @@
1
+ import { assertSameOriginJson } from "@/lib/customer/route-guard.ts";
2
+ import { authErrorResponse } from "@/lib/customer/route-helpers.ts";
3
+ import { clearSessionAndCart } from "@/lib/customer/session";
4
+
5
+ // Log out: clear the HttpOnly session cookie AND the active-cart cookie together.
6
+ // The customer JWT is stateless, so dropping the cookie ends the session
7
+ // client-side; the cart-token handle is slaved to the session, so it is dropped
8
+ // atomically too (no later session inherits this identity's cart). CSRF-guarded
9
+ // (same-origin + JSON) so a cross-site request cannot force-logout the customer.
10
+ export async function POST(request: Request) {
11
+ try {
12
+ assertSameOriginJson(request);
13
+ await clearSessionAndCart();
14
+ return Response.json({ ok: true });
15
+ } catch (error) {
16
+ return authErrorResponse(error);
17
+ }
18
+ }
@@ -0,0 +1,41 @@
1
+ import { syncActiveCartCookieOnLogin } from "@/lib/cart/sync-on-login.server.ts";
2
+ import { login, register } from "@/lib/customer/auth-actions";
3
+ import { assertSameOriginJson } from "@/lib/customer/route-guard.ts";
4
+ import {
5
+ authErrorResponse,
6
+ parseRegister,
7
+ } from "@/lib/customer/route-helpers.ts";
8
+ import { setSessionToken } from "@/lib/customer/session";
9
+
10
+ // Register: delegate to `client.customer.auth.register`, then log the new
11
+ // customer in to mint a session JWT (register returns no token) and stamp it
12
+ // into the HttpOnly session cookie. CSRF-guarded (same-origin + JSON).
13
+ export async function POST(request: Request) {
14
+ try {
15
+ assertSameOriginJson(request);
16
+ const input = parseRegister(await request.json());
17
+ const created = await register(input);
18
+ if (!created.ok) {
19
+ return Response.json(
20
+ { code: "register_failed", message: created.message },
21
+ { status: created.status },
22
+ );
23
+ }
24
+
25
+ // Registration may require verification before login succeeds; if the
26
+ // follow-up login fails, the account still exists — report success without a
27
+ // session so the UI can route the customer to the login page.
28
+ const session = await login({
29
+ email: input.email,
30
+ password: input.password,
31
+ });
32
+ if (session.ok && session.token) {
33
+ await setSessionToken(session.token);
34
+ await syncActiveCartCookieOnLogin(session.token);
35
+ return Response.json({ customer: session.customer });
36
+ }
37
+ return Response.json({ customer: created.customer, requiresLogin: true });
38
+ } catch (error) {
39
+ return authErrorResponse(error);
40
+ }
41
+ }
@@ -0,0 +1,12 @@
1
+ import { clearActiveCart } from "@/lib/cart/server-cart";
2
+ import { cartErrorResponse } from "@/lib/cart/route-helpers.ts";
3
+
4
+ // Empty the owned cart (no-op when none is owned).
5
+ export async function POST() {
6
+ try {
7
+ const cart = await clearActiveCart();
8
+ return Response.json({ cart });
9
+ } catch (error) {
10
+ return cartErrorResponse(error);
11
+ }
12
+ }
@@ -0,0 +1,45 @@
1
+ import {
2
+ addItemToCart,
3
+ removeCartItemFromCart,
4
+ updateCartItemQuantity,
5
+ } from "@/lib/cart/server-cart";
6
+ import {
7
+ parseAddItem,
8
+ parseRemoveItem,
9
+ parseUpdateItem,
10
+ } from "@/lib/cart/parse-cart-request.ts";
11
+ import { cartErrorResponse } from "@/lib/cart/route-helpers.ts";
12
+
13
+ // Add a line. Creates + stamps the guest cart cookie when none is owned yet.
14
+ export async function POST(request: Request) {
15
+ try {
16
+ const cart = await addItemToCart(parseAddItem(await request.json()));
17
+ return Response.json({ cart });
18
+ } catch (error) {
19
+ return cartErrorResponse(error);
20
+ }
21
+ }
22
+
23
+ // Update a line quantity by its cart-item handle.
24
+ export async function PATCH(request: Request) {
25
+ try {
26
+ const cart = await updateCartItemQuantity(
27
+ parseUpdateItem(await request.json()),
28
+ );
29
+ return Response.json({ cart });
30
+ } catch (error) {
31
+ return cartErrorResponse(error);
32
+ }
33
+ }
34
+
35
+ // Remove a line by its cart-item handle.
36
+ export async function DELETE(request: Request) {
37
+ try {
38
+ const cart = await removeCartItemFromCart(
39
+ parseRemoveItem(await request.json()),
40
+ );
41
+ return Response.json({ cart });
42
+ } catch (error) {
43
+ return cartErrorResponse(error);
44
+ }
45
+ }
@@ -0,0 +1,14 @@
1
+ import { getCartFromCookie } from "@/lib/cart/server-cart";
2
+ import { cartErrorResponse } from "@/lib/cart/route-helpers.ts";
3
+
4
+ // The cart lives server-side (`commerce.cart.*`); this returns the current
5
+ // guest cart resolved from the HttpOnly ownership cookie, or `{ cart: null }`
6
+ // when none is owned yet.
7
+ export async function GET() {
8
+ try {
9
+ const cart = await getCartFromCookie();
10
+ return Response.json({ cart });
11
+ } catch (error) {
12
+ return cartErrorResponse(error);
13
+ }
14
+ }
@@ -0,0 +1,86 @@
1
+ import { redirect } from "next/navigation";
2
+ import {
3
+ AmountMismatchError,
4
+ assertAmountEquals,
5
+ } from "@/lib/payment/amount-gate";
6
+ import { getPaymentProvider } from "@/lib/payment/provider.server";
7
+
8
+ /**
9
+ * PG redirect return. For providers that require a server-side capture (e.g.
10
+ * Toss `/payments/confirm` with the returned `paymentKey`), this runs the shared
11
+ * amount-equality gate, captures, then hands off to the success page which
12
+ * reconciles the order via `syncOrderPayment` (server re-fetch →
13
+ * `orders.confirmPayment`).
14
+ *
15
+ * The redirect query is fully client-tamperable, so its `amount` is NEVER
16
+ * forwarded to the capture. We re-fetch the payment from the PG, assert the
17
+ * query amount (when present) equals the re-fetched amount (#1544 / I6 amount
18
+ * gate), and then capture with the PG-verified amount. The order is ultimately
19
+ * placed against the Console quote, never this query value.
20
+ */
21
+ export async function GET(request: Request) {
22
+ const { searchParams } = new URL(request.url);
23
+ const paymentId =
24
+ searchParams.get("paymentId") ?? searchParams.get("orderId");
25
+ const orderNumber = searchParams.get("orderNumber") ?? undefined;
26
+ const providerPaymentId = searchParams.get("paymentKey") ?? undefined;
27
+ const amountValue = searchParams.get("amount");
28
+ const clientAmount = amountValue ? Number(amountValue) : undefined;
29
+
30
+ if (!paymentId) {
31
+ return Response.json({ code: "missing_payment_id" }, { status: 400 });
32
+ }
33
+ if (amountValue && !Number.isFinite(clientAmount)) {
34
+ return Response.json({ code: "invalid_amount" }, { status: 400 });
35
+ }
36
+
37
+ // Capture is required only when the provider returns a capture token
38
+ // (`paymentKey`) or a client amount to validate. Providers without a
39
+ // return-side capture (PortOne, mock) fall straight through to the success
40
+ // page, where `syncOrderPayment` re-fetches, gates, and places.
41
+ if (providerPaymentId || clientAmount != null) {
42
+ try {
43
+ const provider = getPaymentProvider();
44
+ // Re-fetch the authoritative amount from the PG — the trust anchor.
45
+ const verified = await provider.getPayment(paymentId);
46
+ if (!verified) {
47
+ return Response.json(
48
+ { code: "payment_not_found" },
49
+ { status: 404 },
50
+ );
51
+ }
52
+ // Amount gate: a tampered redirect query is rejected before any capture.
53
+ if (clientAmount != null) {
54
+ assertAmountEquals({
55
+ context: `return:${provider.provider}:${paymentId}`,
56
+ expected: verified.amount,
57
+ actual: clientAmount,
58
+ });
59
+ }
60
+ // Capture with the PG-verified amount, never the client query value.
61
+ await provider.confirmPayment({
62
+ paymentId,
63
+ providerPaymentId,
64
+ amount: verified.amount,
65
+ });
66
+ } catch (error) {
67
+ if (error instanceof AmountMismatchError) {
68
+ return Response.json(
69
+ { code: "amount_mismatch" },
70
+ { status: 422 },
71
+ );
72
+ }
73
+ const message =
74
+ error instanceof Error ? error.message : "Payment confirmation failed";
75
+ return Response.json(
76
+ { code: "payment_confirmation_failed", message },
77
+ { status: 422 },
78
+ );
79
+ }
80
+ }
81
+
82
+ const params = new URLSearchParams({ paymentId });
83
+ if (orderNumber) params.set("orderNumber", orderNumber);
84
+
85
+ redirect(`/checkout/success?${params.toString()}`);
86
+ }
@@ -0,0 +1,50 @@
1
+ import { getCommerceProvider } from "@/lib/commerce/provider.server";
2
+ import { AmountMismatchError } from "@/lib/payment/amount-gate";
3
+ import { getPaymentProvider } from "@/lib/payment/provider.server";
4
+ import { syncOrderPayment } from "@/lib/payment/sync-order-payment.ts";
5
+
6
+ /**
7
+ * Order reconciliation, moved off the success-page RSC render into a route
8
+ * handler (#1544 / I6). Mutating during Server Component render is unsound
9
+ * (renders may run more than once); the success page now renders a client
10
+ * trigger that POSTs here exactly once. `syncOrderPayment` is itself idempotent
11
+ * — the Console dedups by `pgPaymentId` / `providerEventId` — so a retry or a
12
+ * concurrent webhook still collapses to a single `paid` transition. The shared
13
+ * amount-equality gate runs inside `syncOrderPayment` on this success path.
14
+ */
15
+ export async function POST(request: Request) {
16
+ let body: { paymentId?: unknown; orderNumber?: unknown };
17
+ try {
18
+ body = (await request.json()) as typeof body;
19
+ } catch {
20
+ return Response.json({ code: "invalid_request" }, { status: 400 });
21
+ }
22
+
23
+ const paymentId =
24
+ typeof body.paymentId === "string" ? body.paymentId : undefined;
25
+ const orderNumber =
26
+ typeof body.orderNumber === "string" ? body.orderNumber : undefined;
27
+ if (!paymentId) {
28
+ return Response.json({ code: "missing_payment_id" }, { status: 400 });
29
+ }
30
+
31
+ try {
32
+ const result = await syncOrderPayment({
33
+ paymentId,
34
+ orderNumber,
35
+ commerceProvider: getCommerceProvider(),
36
+ paymentProvider: getPaymentProvider(),
37
+ });
38
+ return Response.json(result);
39
+ } catch (error) {
40
+ if (error instanceof AmountMismatchError) {
41
+ return Response.json({ code: "amount_mismatch" }, { status: 422 });
42
+ }
43
+ const message =
44
+ error instanceof Error ? error.message : "Reconciliation failed";
45
+ return Response.json(
46
+ { code: "reconciliation_failed", message },
47
+ { status: 422 },
48
+ );
49
+ }
50
+ }
@@ -0,0 +1,41 @@
1
+ import { getCheckoutCommerceProvider } from "@/lib/checkout/checkout-provider";
2
+ import {
3
+ getCheckoutErrorMessage,
4
+ getCheckoutErrorStatus,
5
+ } from "@/lib/checkout/checkout-errors";
6
+ import { startCheckout } from "@/lib/checkout/start-checkout.ts";
7
+ import {
8
+ dropCartCookie,
9
+ getCheckoutCartToken,
10
+ } from "@/lib/cart/server-cart";
11
+ import { getPaymentProvider } from "@/lib/payment/provider.server";
12
+
13
+ export async function POST(request: Request) {
14
+ const cartToken = await getCheckoutCartToken();
15
+ if (!cartToken) {
16
+ return Response.json(
17
+ { code: "cart_not_found", message: "No active cart to check out" },
18
+ { status: 409 },
19
+ );
20
+ }
21
+
22
+ try {
23
+ const result = await startCheckout({
24
+ cartToken,
25
+ payload: await request.json(),
26
+ commerceProvider: await getCheckoutCommerceProvider(),
27
+ paymentProvider: getPaymentProvider(),
28
+ });
29
+
30
+ // Checkout consumed the cart (Shopify cart→checkout boundary). Drop the
31
+ // ownership cookie so the next visit starts a fresh cart.
32
+ await dropCartCookie();
33
+
34
+ return Response.json(result);
35
+ } catch (error) {
36
+ const message = getCheckoutErrorMessage(error);
37
+ const status = getCheckoutErrorStatus(error);
38
+
39
+ return Response.json({ code: "checkout_failed", message }, { status });
40
+ }
41
+ }
@@ -0,0 +1,10 @@
1
+ import { CartContent } from "@/components/cart/cart-content";
2
+ import { PageShell } from "@/components/layout/page-shell";
3
+
4
+ export default function CartPage() {
5
+ return (
6
+ <PageShell>
7
+ <CartContent />
8
+ </PageShell>
9
+ );
10
+ }
@@ -0,0 +1,10 @@
1
+ import { CheckoutForm } from "@/components/checkout/checkout-form";
2
+ import { PageShell } from "@/components/layout/page-shell";
3
+
4
+ export default function CheckoutPage() {
5
+ return (
6
+ <PageShell>
7
+ <CheckoutForm />
8
+ </PageShell>
9
+ );
10
+ }
@@ -0,0 +1,34 @@
1
+ import { PageShell } from "@/components/layout/page-shell";
2
+ import { CheckoutReconcile } from "@/components/checkout/checkout-reconcile";
3
+
4
+ /**
5
+ * Payment return landing. Reconciliation is intentionally NOT performed during
6
+ * render — a Server Component render must be side-effect free and may run more
7
+ * than once. The client `CheckoutReconcile` trigger POSTs to the reconcile
8
+ * route handler once on mount instead (#1544 / I6).
9
+ */
10
+ export default async function CheckoutSuccessPage({
11
+ searchParams,
12
+ }: {
13
+ searchParams: Promise<{ paymentId?: string; orderNumber?: string }>;
14
+ }) {
15
+ const params = await searchParams;
16
+ const paymentId = params.paymentId;
17
+
18
+ if (!paymentId) {
19
+ return (
20
+ <PageShell>
21
+ <section>
22
+ <h1>Missing payment</h1>
23
+ <p>The return URL did not include a payment ID.</p>
24
+ </section>
25
+ </PageShell>
26
+ );
27
+ }
28
+
29
+ return (
30
+ <PageShell>
31
+ <CheckoutReconcile paymentId={paymentId} orderNumber={params.orderNumber} />
32
+ </PageShell>
33
+ );
34
+ }
@@ -0,0 +1,67 @@
1
+ @import "tailwindcss";
2
+
3
+ /*
4
+ * Theming seam.
5
+ *
6
+ * The CSS-variable token layer below is the rebrand surface: change a value in
7
+ * `:root` and every page that consumes the token re-skins. The `@theme inline`
8
+ * block exposes the same tokens to Tailwind utilities (e.g. `bg-background`,
9
+ * `text-brand`) so component styling can be added without leaving this layer.
10
+ * The storefront keeps its semantic-HTML / a11y baseline; styling is additive.
11
+ */
12
+ @theme inline {
13
+ --color-background: var(--background);
14
+ --color-foreground: var(--foreground);
15
+ --color-muted: var(--muted);
16
+ --color-muted-foreground: var(--muted-foreground);
17
+ --color-border: var(--border);
18
+ --color-brand: var(--brand);
19
+ --color-brand-foreground: var(--brand-foreground);
20
+ --font-sans: var(--font-sans);
21
+ }
22
+
23
+ :root {
24
+ /* Brand */
25
+ --brand: #1f2937;
26
+ --brand-foreground: #ffffff;
27
+
28
+ /* Surfaces */
29
+ --background: #ffffff;
30
+ --foreground: #111827;
31
+ --muted: #f3f4f6;
32
+ --muted-foreground: #6b7280;
33
+ --border: rgba(17, 24, 39, 0.12);
34
+
35
+ /* Type */
36
+ --font-sans:
37
+ ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue",
38
+ Arial, sans-serif;
39
+
40
+ /* Spacing rhythm shared by page sections */
41
+ --space-page: 1.5rem;
42
+ --measure: 72ch;
43
+ }
44
+
45
+ @media (prefers-color-scheme: dark) {
46
+ :root {
47
+ --brand: #e5e7eb;
48
+ --brand-foreground: #111827;
49
+ --background: #0b0f19;
50
+ --foreground: #f3f4f6;
51
+ --muted: #161b27;
52
+ --muted-foreground: #9ca3af;
53
+ --border: rgba(243, 244, 246, 0.14);
54
+ }
55
+ }
56
+
57
+ body {
58
+ background: var(--background);
59
+ color: var(--foreground);
60
+ font-family: var(--font-sans);
61
+ line-height: 1.5;
62
+ -webkit-font-smoothing: antialiased;
63
+ }
64
+
65
+ main {
66
+ padding: var(--space-page);
67
+ }
@@ -0,0 +1,23 @@
1
+ import type { Metadata } from "next";
2
+ import { appConfig } from "@/app-config";
3
+ import { CartProvider } from "@/lib/cart/use-cart";
4
+ import "./globals.css";
5
+
6
+ export const metadata: Metadata = {
7
+ title: appConfig.title,
8
+ description: "A minimal adapter-based commerce storefront template.",
9
+ };
10
+
11
+ export default function RootLayout({
12
+ children,
13
+ }: Readonly<{
14
+ children: React.ReactNode;
15
+ }>) {
16
+ return (
17
+ <html lang="en">
18
+ <body>
19
+ <CartProvider>{children}</CartProvider>
20
+ </body>
21
+ </html>
22
+ );
23
+ }