diff --git a/.pnpmfile.cjs b/.pnpmfile.cjs new file mode 100644 index 00000000..f9536ad9 --- /dev/null +++ b/.pnpmfile.cjs @@ -0,0 +1,38 @@ +// 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 re-runs the install whenever the SDK is rebuilt. +// +// `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' + +/** + * @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] = `file:${sdkPath}` + } + } + + return pkg +} + +module.exports = { hooks: { readPackage } } diff --git a/README.md b/README.md index 831815d0..bbacd598 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` 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. + #### 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: .: 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: [] 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 ?? "",