Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions .pnpmfile.cjs
Original file line number Diff line number Diff line change
@@ -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 <worktree>/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<string, string>, devDependencies?: Record<string, string> }} 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 } }
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down
2 changes: 2 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
@@ -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: []
6 changes: 6 additions & 0 deletions src/app/[country]/[locale]/(storefront)/cart/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,12 @@ export default function CartPage() {
</dd>
</div>
)}
{(cart.fees ?? []).map((fee) => (
<div key={fee.id} className="flex justify-between">
<dt className="text-gray-500">{fee.label}</dt>
<dd className="text-gray-900">{fee.display_amount}</dd>
</div>
))}
{cart.tax_total && parseFloat(cart.tax_total) > 0 && (
<div className="flex justify-between">
<dt className="text-gray-500">{tc("tax")}</dt>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,12 @@ export function WholesaleCartView() {
<dd>{cart.display_discount_total}</dd>
</div>
)}
{(cart.fees ?? []).map((fee) => (
<div key={fee.id} className="flex justify-between">
<dt className="text-slate-500">{fee.label}</dt>
<dd className="text-slate-900">{fee.display_amount}</dd>
</div>
))}
{cart.tax_total && parseFloat(cart.tax_total) > 0 && (
<div className="flex justify-between">
<dt className="text-slate-500">{tc("tax")}</dt>
Expand Down
3 changes: 3 additions & 0 deletions src/app/dev/emails/fixtures.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
9 changes: 9 additions & 0 deletions src/components/cart/CartDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,15 @@ export function CartDrawer() {
<span>{cart.display_discount_total}</span>
</div>
)}
{(cart?.fees ?? []).map((fee) => (
<div
key={fee.id}
className="flex justify-between items-center text-sm"
>
<span>{fee.label}</span>
<span>{fee.display_amount}</span>
</div>
))}
<div className="flex justify-between items-center">
<span>{tc("shipping")}</span>
<span className="text-gray-500">
Expand Down
7 changes: 7 additions & 0 deletions src/components/checkout/Summary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,13 @@ export function Summary({ cart }: SummaryProps) {
)}
</div>

{(cart.fees ?? []).map((fee) => (
<div key={fee.id} className="flex justify-between text-sm">
<span className="text-gray-700">{fee.label}</span>
<span className="text-gray-900">{fee.display_amount}</span>
</div>
))}

{cart.discount_total && parseFloat(cart.discount_total) !== 0 && (
<div className="flex justify-between text-sm">
<span className="text-gray-700">{tc("discount")}</span>
Expand Down
7 changes: 7 additions & 0 deletions src/components/order/OrderTotals.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@ export function OrderTotals({ order }: OrderTotalsProps) {
<span className="text-gray-900">{order.display_delivery_total}</span>
</div>

{(order.fees ?? []).map((fee) => (
<div key={fee.id} className="flex justify-between text-sm">
<span className="text-gray-500">{fee.label}</span>
<span className="text-gray-900">{fee.display_amount}</span>
</div>
))}

{order.discount_total &&
Number.parseFloat(order.discount_total) !== 0 && (
<div className="flex justify-between text-sm">
Expand Down
14 changes: 14 additions & 0 deletions src/lib/emails/order-confirmation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ interface Address {
phone?: string | null;
}

interface Fee {
id: string;
label: string;
display_amount: string;
}

interface OrderConfirmationEmailProps {
orderNumber: string;
customerName: string;
Expand All @@ -46,6 +52,7 @@ interface OrderConfirmationEmailProps {
items: LineItem[];
displayItemTotal: string;
displayDeliveryTotal: string;
fees?: Fee[];
displayDiscountTotal?: string;
displayTaxTotal: string;
displayTotal: string;
Expand All @@ -62,6 +69,7 @@ export function OrderConfirmationEmail({
items,
displayItemTotal,
displayDeliveryTotal,
fees = [],
displayDiscountTotal,
displayTaxTotal,
displayTotal,
Expand Down Expand Up @@ -149,6 +157,12 @@ export function OrderConfirmationEmail({
<Column style={totalsLabel}>Shipping</Column>
<Column style={totalsValue}>{displayDeliveryTotal}</Column>
</Row>
{fees.map((fee) => (
<Row key={fee.id}>
<Column style={totalsLabel}>{fee.label}</Column>
<Column style={totalsValue}>{fee.display_amount}</Column>
</Row>
))}
{displayDiscountTotal &&
Number.parseFloat(
displayDiscountTotal.replace(/[^0-9.-]/g, ""),
Expand Down
5 changes: 5 additions & 0 deletions src/lib/webhooks/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,11 @@ export async function handleOrderCompleted(event: WebhookEvent<Order>) {
})),
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 ?? "",
Expand Down