From d696fc61a5fab18addca41f10629fe0180340ea2 Mon Sep 17 00:00:00 2001 From: Damian Legawiec Date: Tue, 25 Aug 2026 17:11:57 +0200 Subject: [PATCH 1/5] Track Spree 6.0 development on this branch Adds an opt-in link to a local @spree/sdk build so the storefront can be developed against unreleased Store API features. Without SPREE_SDK_PATH the install is unchanged, keeping standalone clones and CI on the published SDK. --- .pnpmfile.cjs | 34 ++++++++++++++++++++++++++++++++++ README.md | 14 ++++++++++++++ pnpm-lock.yaml | 2 ++ 3 files changed, 50 insertions(+) create mode 100644 .pnpmfile.cjs diff --git a/.pnpmfile.cjs b/.pnpmfile.cjs new file mode 100644 index 00000000..61b00fa7 --- /dev/null +++ b/.pnpmfile.cjs @@ -0,0 +1,34 @@ +// Point @spree/sdk at a local checkout when SPREE_SDK_PATH is set. +// +// This is the JavaScript twin of the backend's SPREE_PATH: the branch installs +// the published SDK by default — for standalone clones and CI — and only swaps +// in a workspace build when a developer opts in through the environment. That +// keeps the local link out of package.json, so it can never be committed by +// accident. +// +// The Spree monorepo's worktree tooling sets SPREE_SDK_PATH to /packages/sdk +// and runs the SDK's build in watch mode alongside `next dev`. +// +// `link:` rather than `file:` — pnpm symlinks the directory instead of copying +// it, so a rebuilt SDK is picked up without reinstalling. + +const SDK_PACKAGE = '@spree/sdk' + +/** + * @param {{ dependencies?: Record, devDependencies?: Record }} pkg + */ +function readPackage(pkg) { + const sdkPath = process.env.SPREE_SDK_PATH + + if (!sdkPath) return pkg + + for (const field of ['dependencies', 'devDependencies']) { + if (pkg[field]?.[SDK_PACKAGE]) { + pkg[field][SDK_PACKAGE] = `link:${sdkPath}` + } + } + + return pkg +} + +module.exports = { hooks: { readPackage } } diff --git a/README.md b/README.md index 831815d0..5d32ac38 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,8 @@ A production-ready, headless ecommerce storefront for [Spree Commerce](https://s [Live Demo](https://demo.spreecommerce.org) | [Quickstart Docs](https://spreecommerce.org/docs/developer/storefront/nextjs/quickstart) | [TypeScript SDK](https://www.npmjs.com/package/@spree/sdk) +> **You are on `6-0-dev`.** This branch tracks the unreleased Spree 6.0 Store API and SDK, so it can break as those change. For a stable storefront to fork or deploy, use `main`. This branch merges into `main` at the 6.0 release. + ## Why This Storefront **TypeScript SDK.** [@spree/sdk](https://www.npmjs.com/package/@spree/sdk) is an official typed client for every Store API endpoint (OpenAPI 3.0 documented). Autocomplete and type safety in your editor, no codegen step to maintain. @@ -134,6 +136,18 @@ pnpm run dev Open [http://localhost:3001](http://localhost:3001) in your browser. +#### Working against a local SDK + +To develop against unreleased Store API features, point the storefront at a `@spree/sdk` build from a [Spree monorepo](https://github.com/spree/spree) checkout instead of the published package: + +```bash +SPREE_SDK_PATH=/path/to/spree/packages/sdk pnpm install --no-frozen-lockfile +``` + +`.pnpmfile.cjs` rewrites the dependency to a symlink only when `SPREE_SDK_PATH` is set, so the default install is unaffected and nothing local ever reaches `package.json`. The SDK must be built (`pnpm build` in the monorepo, or `--watch` while you work). The install rewrites `pnpm-lock.yaml` with the local path — do not commit that change. + +From a Spree monorepo worktree, `pnpm wt:storefront` does all of this for you, including the API URL and publishable key. + #### HTTPS Development (Apple Pay / Google Pay) Testing Apple Pay / Google Pay locally needs a public HTTPS URL (Stripe verifies the payment method domain from the internet). See [Wallet Payments in Development](https://spreecommerce.org/docs/developer/storefront/nextjs/wallet-payments) for the Cloudflare Tunnel setup. diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2b214506..6ef00316 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,8 @@ overrides: sharp: ^0.35.3 '@hono/node-server': ^2.0.5 +pnpmfileChecksum: sha256-N1/Dc+ZPONiIP8Hd+SAy7UlA4/PNS1kNfPizHE+UtK8= + importers: .: From 4f44c5c5758437250c1a0cd73777d9cdf81ba6d1 Mon Sep 17 00:00:00 2001 From: Damian Legawiec Date: Tue, 25 Aug 2026 17:15:58 +0200 Subject: [PATCH 2/5] Pin the pnpm workspace root to this project Without it pnpm walks up the filesystem and adopts an enclosing workspace, installing that project's dependencies and overrides here instead of these. --- pnpm-workspace.yaml | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 pnpm-workspace.yaml diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 00000000..9dc8c694 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,7 @@ +# This project is a single package, not a monorepo — the empty `packages` list +# is deliberate. Its purpose is to mark this directory as a workspace root so +# pnpm stops walking up the filesystem: checked out inside another pnpm +# workspace (the Spree monorepo's worktree tooling clones it into `storefront/`), +# pnpm would otherwise adopt that parent workspace and install it instead of +# this project, applying its overrides and lockfile here. +packages: [] From d981a9691f9424477de95f4c83efa705fd943048 Mon Sep 17 00:00:00 2001 From: Damian Legawiec Date: Tue, 25 Aug 2026 19:07:47 +0200 Subject: [PATCH 3/5] Copy the local SDK instead of symlinking it Module resolution is pinned to this project's own directory, so a package symlinked outside it fails to resolve and every import of @spree/sdk breaks. --- .pnpmfile.cjs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/.pnpmfile.cjs b/.pnpmfile.cjs index 61b00fa7..f9536ad9 100644 --- a/.pnpmfile.cjs +++ b/.pnpmfile.cjs @@ -7,10 +7,14 @@ // accident. // // The Spree monorepo's worktree tooling sets SPREE_SDK_PATH to /packages/sdk -// and runs the SDK's build in watch mode alongside `next dev`. +// and re-runs the install whenever the SDK is rebuilt. // -// `link:` rather than `file:` — pnpm symlinks the directory instead of copying -// it, so a rebuilt SDK is picked up without reinstalling. +// `file:` rather than `link:`. A symlink is the tempting choice — it would pick +// up SDK rebuilds with no reinstall — but this project pins module resolution +// to its own directory (`turbopack.root`, `output: "standalone"`) and lists +// @spree/sdk in `transpilePackages`, so a package symlinked to somewhere +// outside the project simply does not resolve. `file:` copies it into +// node_modules, where all three of those settings can see it. const SDK_PACKAGE = '@spree/sdk' @@ -24,7 +28,7 @@ function readPackage(pkg) { for (const field of ['dependencies', 'devDependencies']) { if (pkg[field]?.[SDK_PACKAGE]) { - pkg[field][SDK_PACKAGE] = `link:${sdkPath}` + pkg[field][SDK_PACKAGE] = `file:${sdkPath}` } } From 6d6c9430b8bc5f00aa088d4c7d9729721a1a3094 Mon Sep 17 00:00:00 2001 From: Damian Legawiec Date: Tue, 25 Aug 2026 19:14:20 +0200 Subject: [PATCH 4/5] Document rebuilding the local SDK The dependency is copied rather than symlinked, so a rebuild needs a reinstall to take effect. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5d32ac38..bbacd598 100644 --- a/README.md +++ b/README.md @@ -144,7 +144,7 @@ To develop against unreleased Store API features, point the storefront at a `@sp SPREE_SDK_PATH=/path/to/spree/packages/sdk pnpm install --no-frozen-lockfile ``` -`.pnpmfile.cjs` rewrites the dependency to a symlink only when `SPREE_SDK_PATH` is set, so the default install is unaffected and nothing local ever reaches `package.json`. The SDK must be built (`pnpm build` in the monorepo, or `--watch` while you work). The install rewrites `pnpm-lock.yaml` with the local path — do not commit that change. +`.pnpmfile.cjs` redirects the dependency at the local path only when `SPREE_SDK_PATH` is set, so the default install is unaffected and nothing local ever reaches `package.json`. Two things follow from it copying the package rather than symlinking it (this project pins module resolution to its own directory, so a symlink outside it would not resolve): build the SDK first (`pnpm build` in the monorepo), and re-run the install above after each rebuild to pick the new build up. The install also rewrites `pnpm-lock.yaml` with the local path — do not commit that change. From a Spree monorepo worktree, `pnpm wt:storefront` does all of this for you, including the API URL and publishable key. From 16ef075689097a4daac66f9d4402401bb28a6950 Mon Sep 17 00:00:00 2001 From: Damian Legawiec Date: Wed, 9 Sep 2026 13:19:38 +0200 Subject: [PATCH 5/5] Added fees and duties to cart, checkout and order totals Spree 6.0 carts and orders carry a fees array (gift wrapping, handling, cash on delivery, payment surcharges, customs duties). The storefront never rendered them, so a fee changed the total with no line explaining why. Each fee now shows as its own line, under its own label, on the cart page, cart drawer, checkout summary, order totals and the order confirmation email. Co-Authored-By: Claude Fable 5.1 --- .../[country]/[locale]/(storefront)/cart/page.tsx | 6 ++++++ .../wholesale/cart/WholesaleCartView.tsx | 6 ++++++ src/app/dev/emails/fixtures.tsx | 3 +++ src/components/cart/CartDrawer.tsx | 9 +++++++++ src/components/checkout/Summary.tsx | 7 +++++++ src/components/order/OrderTotals.tsx | 7 +++++++ src/lib/emails/order-confirmation.tsx | 14 ++++++++++++++ src/lib/webhooks/handlers.ts | 5 +++++ 8 files changed, 57 insertions(+) diff --git a/src/app/[country]/[locale]/(storefront)/cart/page.tsx b/src/app/[country]/[locale]/(storefront)/cart/page.tsx index a17154cb..654d537d 100644 --- a/src/app/[country]/[locale]/(storefront)/cart/page.tsx +++ b/src/app/[country]/[locale]/(storefront)/cart/page.tsx @@ -178,6 +178,12 @@ export default function CartPage() { )} + {(cart.fees ?? []).map((fee) => ( +
+
{fee.label}
+
{fee.display_amount}
+
+ ))} {cart.tax_total && parseFloat(cart.tax_total) > 0 && (
{tc("tax")}
diff --git a/src/app/[country]/[locale]/(wholesale)/wholesale/cart/WholesaleCartView.tsx b/src/app/[country]/[locale]/(wholesale)/wholesale/cart/WholesaleCartView.tsx index 06f30128..932ed630 100644 --- a/src/app/[country]/[locale]/(wholesale)/wholesale/cart/WholesaleCartView.tsx +++ b/src/app/[country]/[locale]/(wholesale)/wholesale/cart/WholesaleCartView.tsx @@ -161,6 +161,12 @@ export function WholesaleCartView() {
{cart.display_discount_total}
)} + {(cart.fees ?? []).map((fee) => ( +
+
{fee.label}
+
{fee.display_amount}
+
+ ))} {cart.tax_total && parseFloat(cart.tax_total) > 0 && (
{tc("tax")}
diff --git a/src/app/dev/emails/fixtures.tsx b/src/app/dev/emails/fixtures.tsx index a95cd26e..7e0c1f75 100644 --- a/src/app/dev/emails/fixtures.tsx +++ b/src/app/dev/emails/fixtures.tsx @@ -49,6 +49,9 @@ export const emailFixtures: EmailFixture[] = [ ], displayItemTotal: "$134.96", displayDeliveryTotal: "$5.99", + fees: [ + { id: "fee_1", label: "Gift wrapping", display_amount: "$4.00" }, + ], displayDiscountTotal: "-$10.00", displayTaxTotal: "$11.25", displayTotal: "$142.20", diff --git a/src/components/cart/CartDrawer.tsx b/src/components/cart/CartDrawer.tsx index 28fd3ca2..7a70e259 100644 --- a/src/components/cart/CartDrawer.tsx +++ b/src/components/cart/CartDrawer.tsx @@ -248,6 +248,15 @@ export function CartDrawer() { {cart.display_discount_total}
)} + {(cart?.fees ?? []).map((fee) => ( +
+ {fee.label} + {fee.display_amount} +
+ ))}
{tc("shipping")} diff --git a/src/components/checkout/Summary.tsx b/src/components/checkout/Summary.tsx index 1a43f2fa..1e2533c0 100644 --- a/src/components/checkout/Summary.tsx +++ b/src/components/checkout/Summary.tsx @@ -68,6 +68,13 @@ export function Summary({ cart }: SummaryProps) { )}
+ {(cart.fees ?? []).map((fee) => ( +
+ {fee.label} + {fee.display_amount} +
+ ))} + {cart.discount_total && parseFloat(cart.discount_total) !== 0 && (
{tc("discount")} diff --git a/src/components/order/OrderTotals.tsx b/src/components/order/OrderTotals.tsx index 80b0c83b..1ed73a73 100644 --- a/src/components/order/OrderTotals.tsx +++ b/src/components/order/OrderTotals.tsx @@ -22,6 +22,13 @@ export function OrderTotals({ order }: OrderTotalsProps) { {order.display_delivery_total}
+ {(order.fees ?? []).map((fee) => ( +
+ {fee.label} + {fee.display_amount} +
+ ))} + {order.discount_total && Number.parseFloat(order.discount_total) !== 0 && (
diff --git a/src/lib/emails/order-confirmation.tsx b/src/lib/emails/order-confirmation.tsx index 8f21515a..6fbb2b6d 100644 --- a/src/lib/emails/order-confirmation.tsx +++ b/src/lib/emails/order-confirmation.tsx @@ -38,6 +38,12 @@ interface Address { phone?: string | null; } +interface Fee { + id: string; + label: string; + display_amount: string; +} + interface OrderConfirmationEmailProps { orderNumber: string; customerName: string; @@ -46,6 +52,7 @@ interface OrderConfirmationEmailProps { items: LineItem[]; displayItemTotal: string; displayDeliveryTotal: string; + fees?: Fee[]; displayDiscountTotal?: string; displayTaxTotal: string; displayTotal: string; @@ -62,6 +69,7 @@ export function OrderConfirmationEmail({ items, displayItemTotal, displayDeliveryTotal, + fees = [], displayDiscountTotal, displayTaxTotal, displayTotal, @@ -149,6 +157,12 @@ export function OrderConfirmationEmail({ Shipping {displayDeliveryTotal} + {fees.map((fee) => ( + + {fee.label} + {fee.display_amount} + + ))} {displayDiscountTotal && Number.parseFloat( displayDiscountTotal.replace(/[^0-9.-]/g, ""), diff --git a/src/lib/webhooks/handlers.ts b/src/lib/webhooks/handlers.ts index 1104db4b..fa67f24c 100644 --- a/src/lib/webhooks/handlers.ts +++ b/src/lib/webhooks/handlers.ts @@ -78,6 +78,11 @@ export async function handleOrderCompleted(event: WebhookEvent) { })), displayItemTotal: order.display_item_total ?? "", displayDeliveryTotal: order.display_delivery_total ?? "", + fees: (order.fees || []).map((fee) => ({ + id: fee.id, + label: fee.label, + display_amount: fee.display_amount ?? "", + })), displayDiscountTotal: order.display_discount_total ?? undefined, displayTaxTotal: order.display_tax_total ?? "", displayTotal: order.display_total ?? "",