@spree/docs 0.1.132 → 0.1.134
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/dist/developer/core-concepts/channels.md +49 -0
- package/dist/developer/core-concepts/stores.md +11 -2
- package/dist/developer/storefront/nextjs/architecture.md +22 -15
- package/dist/developer/storefront/nextjs/customization.md +8 -82
- package/dist/developer/storefront/nextjs/deployment.md +41 -60
- package/dist/developer/storefront/nextjs/emails.md +86 -0
- package/dist/developer/storefront/nextjs/environment-variables.md +88 -0
- package/dist/developer/storefront/nextjs/multi-region.md +75 -0
- package/dist/developer/storefront/nextjs/quickstart.md +11 -7
- package/dist/developer/storefront/nextjs/testing.md +81 -0
- package/dist/developer/storefront/nextjs/wallet-payments.md +52 -0
- package/dist/developer/storefront/nextjs/wholesale.md +82 -0
- package/dist/developer/upgrades/5.5-to-5.6.md +177 -0
- package/package.json +1 -1
|
@@ -20,6 +20,8 @@ Every store ships with one default channel named *Online Store*. You can add mor
|
|
|
20
20
|
| `code` | URL-safe slug, stable identifier sent via the `X-Spree-Channel` header | `pos` |
|
|
21
21
|
| `active` | When `false`, the channel stops accepting orders | `true` |
|
|
22
22
|
| `default` | Exactly one channel per store is the default. Used as a fallback when no channel header is present and as the auto-publish target for new products | `true` |
|
|
23
|
+
| `storefront_access` | Controls what an anonymous visitor may see: `public`, `prices_hidden`, or `login_required`. Unset inherits the store's setting. See [Storefront Access Gating](#storefront-access-gating) | `login_required` |
|
|
24
|
+
| `guest_checkout` | Whether an order may be placed without an account on this channel. Unset inherits the store's setting | `false` |
|
|
23
25
|
| `preferred_order_routing_strategy` | Optional per-channel override of the store's [Order Routing](shipments.md#order-routing) strategy | `Spree::OrderRouting::Strategy::Rules` |
|
|
24
26
|
|
|
25
27
|
`code` is normalized to a URL-safe slug on save — `POS` becomes `pos`, `Point of Sale!` becomes `point-of-sale`. Leaving `code` blank derives it from `name`.
|
|
@@ -89,6 +91,52 @@ Every order is attributed to one channel. The channel is set from the `X-Spree-C
|
|
|
89
91
|
|
|
90
92
|
This attribution drives reporting (best-selling by channel, revenue per channel) and per-channel order routing — see [Order Routing](shipments.md#order-routing).
|
|
91
93
|
|
|
94
|
+
### Storefront Access Gating
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
A channel's `storefront_access` decides what an **anonymous** visitor — a request with no authenticated customer — may see. Logged-in customers are never gated. The posture is one of three values:
|
|
98
|
+
|
|
99
|
+
| Mode | Guest sees catalog | Guest sees prices | Use case |
|
|
100
|
+
|---|---|---|---|
|
|
101
|
+
| `public` | Yes | Yes | The default. An open storefront — anyone can browse; guests can also check out when `guest_checkout` is enabled. |
|
|
102
|
+
| `prices_hidden` | Yes | No — prices come back `null` | A catalog you want discoverable, with pricing revealed only after sign-in (e.g. a trade catalog for lead generation). |
|
|
103
|
+
| `login_required` | No — reads rejected with `401` | No | A fully gated surface — a guest can't read the catalog at all (e.g. a members-only or B2B wholesale portal). |
|
|
104
|
+
|
|
105
|
+
The gate is enforced by the **Store API**, not the storefront, so a storefront app can't loosen it:
|
|
106
|
+
|
|
107
|
+
- **`login_required`** — every gated read returns `401` for an unauthenticated request. Endpoints that must stay reachable before sign-in (authentication, password reset, reference data like countries and currencies) are exempt.
|
|
108
|
+
- **`prices_hidden`** — reads succeed, but every money field is serialized as `null` for a guest. The storefront renders these as a sign-in prompt rather than a price.
|
|
109
|
+
|
|
110
|
+
A companion control, **`guest_checkout`**, decides whether an order can be placed without an account on the channel. It's independent of `storefront_access` — a `public` channel can still require accounts, and the two are resolved separately.
|
|
111
|
+
|
|
112
|
+
#### Store fallback
|
|
113
|
+
|
|
114
|
+
Both controls fall back to the owning [Store](stores.md) when the channel's own value is unset — the same inheritance pattern as the channel's order-routing strategy. `storefront_access` resolves to the channel value, then the store value, and finally `public` when neither is set. This lets you set a store-wide default (e.g. "all channels require login") and override per channel where needed.
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
```typescript Admin SDK
|
|
118
|
+
// Gate a channel behind sign-in, and require accounts at checkout
|
|
119
|
+
await adminClient.channels.update('ch_wholesale', {
|
|
120
|
+
storefront_access: 'login_required',
|
|
121
|
+
guest_checkout: false,
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
// Clear the channel value to inherit the store's default
|
|
125
|
+
await adminClient.channels.update('ch_wholesale', {
|
|
126
|
+
storefront_access: null,
|
|
127
|
+
})
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
```bash cURL
|
|
131
|
+
curl -X PATCH 'https://api.mystore.com/api/v3/admin/channels/ch_wholesale' \
|
|
132
|
+
-H 'X-Spree-API-Key: sk_xxx' \
|
|
133
|
+
-H 'Content-Type: application/json' \
|
|
134
|
+
-d '{ "storefront_access": "login_required", "guest_checkout": false }'
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
Switching a channel between modes takes effect immediately — the posture is resolved per request, so no cache warm-up or redeploy is needed. The Next.js storefront's [wholesale portal](../storefront/nextjs/wholesale.md) is a worked example of a `login_required` / `prices_hidden` channel driving the UI.
|
|
139
|
+
|
|
92
140
|
## Publishing Products on Channels
|
|
93
141
|
|
|
94
142
|
### Dashboard
|
|
@@ -172,3 +220,4 @@ The write contract is **full-set**: the array represents the complete desired st
|
|
|
172
220
|
- [Order Routing](shipments.md#order-routing) — Channels can override the store's routing strategy
|
|
173
221
|
- [Store SDK: Products](../sdk/store/products.md) — Channel-scoped product listing and filtering
|
|
174
222
|
- [Admin SDK: Resources](../sdk/admin/resources.md) — How `adminClient.channels.addProducts` and other resource methods are structured
|
|
223
|
+
- [Wholesale Portal](../storefront/nextjs/wholesale.md) — A gated channel driving the Next.js storefront's B2B surface
|
|
@@ -5,7 +5,9 @@ description: Understand Spree Stores — the top-level tenant boundary that scop
|
|
|
5
5
|
|
|
6
6
|
## Overview
|
|
7
7
|
|
|
8
|
-
The
|
|
8
|
+
The Store is the top-level tenant in Spree. Every resource — products, orders, channels, markets, taxonomies — belongs to exactly one store. A store owns its [channels](channels.md) (online, POS, wholesale, …), its [markets](markets.md) (region/currency/locale), and its [product catalog](products.md).
|
|
9
|
+
|
|
10
|
+
## Store Attributes
|
|
9
11
|
|
|
10
12
|
| Attribute | Description |
|
|
11
13
|
|-----------|-------------|
|
|
@@ -19,6 +21,8 @@ The StThe Store is the top-level tenant in Spree. Every resource — products, o
|
|
|
19
21
|
| `mail_from_address` | Sender address for transactional emails |
|
|
20
22
|
| `logo_url` | URL to the store's logo |
|
|
21
23
|
| `facebook`, `twitter`, `instagram` | Social media links |
|
|
24
|
+
| `storefront_access` | Store-wide default for anonymous [storefront access gating](channels.md#storefront-access-gating) (`public`, `prices_hidden`, `login_required`). Each channel can override it |
|
|
25
|
+
| `guest_checkout` | Store-wide default for whether orders can be placed without an account. Each channel can override it |
|
|
22
26
|
|
|
23
27
|
## Fetching Store Information
|
|
24
28
|
|
|
@@ -54,6 +58,10 @@ Two different ways to split a store, often confused:
|
|
|
54
58
|
|
|
55
59
|
A single Online Store channel can serve multiple markets (one storefront → many regions). Conversely, POS and Online channels can share the same market (same currency/locale, different selling surfaces).
|
|
56
60
|
|
|
61
|
+
### Storefront access defaults
|
|
62
|
+
|
|
63
|
+
The store sets the fallback for **storefront access gating** — whether anonymous visitors may browse, see prices, or must sign in first — via `storefront_access` (`public`, `prices_hidden`, `login_required`) and the companion `guest_checkout` control. Each channel inherits these unless it sets its own value, so you can gate a whole store or just one channel (e.g. a `login_required` wholesale channel on an otherwise `public` store). The full behavior of each mode and how the Store API enforces it lives in [Channels → Storefront Access Gating](channels.md#storefront-access-gating).
|
|
64
|
+
|
|
57
65
|
## Store Resources
|
|
58
66
|
|
|
59
67
|
Each store owns its own resources. Products, orders, channels, markets, and taxonomies in one store are independent from another.
|
|
@@ -64,7 +72,7 @@ Each store owns its own resources. Products, orders, channels, markets, and taxo
|
|
|
64
72
|
| [**Markets**](markets.md) | A store has many markets, each defining a geographic region with its own currency and locale |
|
|
65
73
|
| [**Orders**](orders.md) | An order belongs to one store and one channel |
|
|
66
74
|
| [**Products**](products.md) | A product belongs to one store. Its visibility across channels is controlled by [publications](channels.md#publishing-products-on-channels). |
|
|
67
|
-
| [**
|
|
75
|
+
| [**Categories**](products.md#categories) | A category belongs to one store |
|
|
68
76
|
| [**Payment Methods**](payments.md) | A payment method belongs to one store |
|
|
69
77
|
| [**Shipping Methods**](shipments.md) | A shipping method belongs to one store |
|
|
70
78
|
| [**Promotions**](promotions.md) | A promotion belongs to one store |
|
|
@@ -82,4 +90,5 @@ If you need one Spree backend to serve **multiple distinct merchant brands** —
|
|
|
82
90
|
- [Markets](markets.md) — Multi-region commerce within a store
|
|
83
91
|
- [Products](products.md) — Product catalog
|
|
84
92
|
- [Orders](orders.md) — Order management and checkout
|
|
93
|
+
- [Wholesale Portal](../storefront/nextjs/wholesale.md) — A gated B2B storefront surface built on channel access gating
|
|
85
94
|
- [Admin SDK](../sdk/admin/quickstart.md) — TypeScript client for the Admin API used to read and update store configuration
|
|
@@ -38,20 +38,29 @@ src/
|
|
|
38
38
|
│ │ │ └── [slug]/ # Product details
|
|
39
39
|
│ │ ├── t/[...permalink]/ # Category pages
|
|
40
40
|
│ │ └── categories/ # Category overview
|
|
41
|
-
│
|
|
42
|
-
│
|
|
43
|
-
│
|
|
41
|
+
│ ├── (checkout)/ # Checkout layout (no header/footer)
|
|
42
|
+
│ │ ├── checkout/[id]/ # Checkout flow
|
|
43
|
+
│ │ └── order-placed/[id]/ # Order confirmation
|
|
44
|
+
│ └── (wholesale)/ # Opt-in B2B portal (gated — see Wholesale guide)
|
|
45
|
+
│ └── wholesale/
|
|
46
|
+
│ ├── page.tsx # Trade catalog
|
|
47
|
+
│ ├── products/[slug]/ # Wholesale PDP
|
|
48
|
+
│ ├── cart/ # Wholesale cart
|
|
49
|
+
│ ├── quick-order/ # SKU quick-order form
|
|
50
|
+
│ ├── apply/ # Trade-account application
|
|
51
|
+
│ └── _components/ # Gate, header, sign-in wall, pending, guest browse
|
|
44
52
|
├── components/
|
|
45
53
|
│ ├── cart/ # CartDrawer
|
|
46
54
|
│ ├── checkout/ # AddressStep, DeliveryStep, PaymentStep, etc.
|
|
47
55
|
│ ├── layout/ # Header, Footer, CountrySwitcher
|
|
48
56
|
│ ├── navigation/ # Breadcrumbs
|
|
49
|
-
│ ├── products/ # ProductCard, ProductGrid, Filters, MediaGallery,
|
|
57
|
+
│ ├── products/ # ProductCard, ProductGrid, Filters, MediaGallery, HiddenPricePrompt
|
|
50
58
|
│ └── search/ # SearchBar
|
|
51
59
|
├── contexts/
|
|
52
60
|
│ ├── AuthContext.tsx # Auth state
|
|
53
61
|
│ ├── CartContext.tsx # Client-side cart state sync
|
|
54
62
|
│ ├── CheckoutContext.tsx # Checkout flow state
|
|
63
|
+
│ ├── HiddenPricingContext.tsx # Prices-hidden signal for wholesale guests
|
|
55
64
|
│ └── StoreContext.tsx # Store/locale/currency state
|
|
56
65
|
├── hooks/
|
|
57
66
|
│ ├── useCarouselProducts.ts # Product carousel data
|
|
@@ -72,7 +81,11 @@ src/
|
|
|
72
81
|
│ ├── payment.ts # Payment processing
|
|
73
82
|
│ ├── products.ts # Product queries
|
|
74
83
|
│ ├── categories.ts # Categories
|
|
75
|
-
│
|
|
84
|
+
│ ├── wholesale.ts # Wholesale channel + catalog + quick-order
|
|
85
|
+
│ ├── utils.ts # Shared helpers (actionResult, withFallback)
|
|
86
|
+
│ └── … # One file per domain — see src/lib/data/
|
|
87
|
+
├── spree/ # Spree integration (client, cookies, auth refresh, middleware, surface)
|
|
88
|
+
├── wholesale.ts # Approval check + volume-pricing helpers
|
|
76
89
|
└── utils/ # Client utilities
|
|
77
90
|
├── address.ts # Address formatting
|
|
78
91
|
├── cookies.ts # Cookie helpers
|
|
@@ -81,6 +94,8 @@ src/
|
|
|
81
94
|
└── product-query.ts # Product filter query builder
|
|
82
95
|
```
|
|
83
96
|
|
|
97
|
+
The `(wholesale)` route group is an opt-in B2B portal, off unless a wholesale channel is configured. Its gating, surfaces, and pricing modes are covered in the [Wholesale Portal](wholesale.md) guide.
|
|
98
|
+
|
|
84
99
|
## Authentication Flow
|
|
85
100
|
|
|
86
101
|
1. User submits login form
|
|
@@ -107,17 +122,9 @@ export async function getCustomer() {
|
|
|
107
122
|
}
|
|
108
123
|
```
|
|
109
124
|
|
|
110
|
-
## Multi-Region
|
|
111
|
-
|
|
112
|
-
The storefront supports multiple countries and currencies via URL segments:
|
|
113
|
-
|
|
114
|
-
```
|
|
115
|
-
/us/en/products # US store, English
|
|
116
|
-
/de/de/products # German store, German
|
|
117
|
-
/uk/en/products # UK store, English
|
|
118
|
-
```
|
|
125
|
+
## Multi-Region
|
|
119
126
|
|
|
120
|
-
|
|
127
|
+
The storefront serves multiple countries, currencies, and languages from one deployment via `/{country}/{locale}` URL segments and an edge middleware that detects and persists the visitor's region. See the [Multi-Region](multi-region.md) guide.
|
|
121
128
|
|
|
122
129
|
## Server Actions
|
|
123
130
|
|
|
@@ -1,35 +1,19 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Customization
|
|
3
|
-
description:
|
|
3
|
+
description: Customize and extend the Spree Next.js Storefront
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
The storefront is yours to modify — the code ships in your project so you can restyle it, swap components, and change the data layer directly. Scaffold a project with [`create-spree-app`](quickstart.md#installation), then edit the storefront under `apps/storefront/`.
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
## Tracking Upstream Updates
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
Go to [github.com/spree/storefront](https://github.com/spree/storefront) and click **Fork**.
|
|
13
|
-
|
|
14
|
-
### 2. Clone Your Fork
|
|
15
|
-
|
|
16
|
-
```bash
|
|
17
|
-
git clone https://github.com/YOUR_USERNAME/storefront.git
|
|
18
|
-
cd storefront
|
|
19
|
-
npm install
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
### 3. Add Upstream Remote
|
|
10
|
+
The storefront evolves upstream. To keep pulling improvements while you customize, own the code in your own Git repository (a fork of [spree/storefront](https://github.com/spree/storefront), or your own repo with the storefront as an upstream remote):
|
|
23
11
|
|
|
24
12
|
```bash
|
|
13
|
+
# Point an "upstream" remote at the official storefront
|
|
25
14
|
git remote add upstream https://github.com/spree/storefront.git
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
### 4. Pull Upstream Updates
|
|
29
15
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
```bash
|
|
16
|
+
# Pull in the latest changes
|
|
33
17
|
git fetch upstream
|
|
34
18
|
git merge upstream/main
|
|
35
19
|
```
|
|
@@ -138,66 +122,8 @@ export default async function YourNewPage() {
|
|
|
138
122
|
|
|
139
123
|
## Transactional Emails
|
|
140
124
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
### Templates
|
|
144
|
-
|
|
145
|
-
Email templates are React components in `src/lib/emails/`:
|
|
146
|
-
|
|
147
|
-
| File | Event | Description |
|
|
148
|
-
|------|-------|-------------|
|
|
149
|
-
| `order-confirmation.tsx` | `order.completed` | Items, totals, addresses, delivery method |
|
|
150
|
-
| `order-canceled.tsx` | `order.canceled` | Cancellation notice with items |
|
|
151
|
-
| `shipment-shipped.tsx` | `order.shipped` | Tracking number and link |
|
|
152
|
-
| `password-reset.tsx` | `customer.password_reset_requested` | Reset button and fallback link |
|
|
153
|
-
| `newsletter-confirmation.tsx` | `newsletter_subscriber.subscription_requested` | Double opt-in confirmation link |
|
|
154
|
-
|
|
155
|
-
Customize templates by editing these files directly. They use `@react-email/components` for email-safe layout primitives.
|
|
156
|
-
|
|
157
|
-
### Previewing
|
|
158
|
-
|
|
159
|
-
```bash
|
|
160
|
-
npm run email:dev
|
|
161
|
-
```
|
|
162
|
-
|
|
163
|
-
Opens the react-email dev server with mock data for all templates at `http://localhost:3000`.
|
|
164
|
-
|
|
165
|
-
### Webhook Handler
|
|
166
|
-
|
|
167
|
-
The webhook route (`src/app/api/webhooks/spree/route.ts`) uses `createWebhookHandler` from `src/lib/spree/webhooks`:
|
|
168
|
-
|
|
169
|
-
```typescript
|
|
170
|
-
import { createWebhookHandler } from '@/lib/spree/webhooks'
|
|
171
|
-
|
|
172
|
-
export const POST = createWebhookHandler({
|
|
173
|
-
secret: process.env.SPREE_WEBHOOK_SECRET!,
|
|
174
|
-
handlers: {
|
|
175
|
-
'order.completed': handleOrderCompleted,
|
|
176
|
-
'order.canceled': handleOrderCanceled,
|
|
177
|
-
'order.shipped': handleOrderShipped,
|
|
178
|
-
'customer.password_reset_requested': handlePasswordReset,
|
|
179
|
-
'newsletter_subscriber.subscription_requested': handleNewsletterConfirmation,
|
|
180
|
-
},
|
|
181
|
-
})
|
|
182
|
-
```
|
|
183
|
-
|
|
184
|
-
To add a new email type:
|
|
185
|
-
|
|
186
|
-
1. Create a template in `src/lib/emails/`
|
|
187
|
-
2. Add a handler function in `src/lib/webhooks/handlers.ts`
|
|
188
|
-
3. Register the event in `route.ts`
|
|
189
|
-
4. Subscribe to the event in Spree Admin → Webhooks
|
|
190
|
-
|
|
191
|
-
### Local Development
|
|
192
|
-
|
|
193
|
-
In dev, emails are written to `.next/emails/` as HTML files — no Resend key needed. Use [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/) to receive webhooks locally:
|
|
194
|
-
|
|
195
|
-
```bash
|
|
196
|
-
cloudflared tunnel --url http://localhost:3001
|
|
197
|
-
```
|
|
198
|
-
|
|
199
|
-
For full setup details, see [Sending out Emails](../../deployment/emails.md).
|
|
125
|
+
The storefront can render and send its own order, shipment, and account emails with react-email and Resend, driven by Spree webhooks. See the dedicated [Transactional Emails](emails.md) guide.
|
|
200
126
|
|
|
201
127
|
## Building a Custom Storefront
|
|
202
128
|
|
|
203
|
-
If you prefer to build from scratch instead of
|
|
129
|
+
If you prefer to build from scratch instead of using the starter, you can use the `@spree/sdk` package directly in any Next.js application. The storefront's `src/lib/spree/` directory contains reusable helpers for cookie-based auth, locale resolution, middleware, and webhook verification that you can copy into your own project.
|
|
@@ -5,26 +5,7 @@ description: Deploy the Spree Next.js Storefront to Vercel, Docker, or any Node.
|
|
|
5
5
|
|
|
6
6
|
## Environment Variables
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
### Required
|
|
11
|
-
|
|
12
|
-
| Variable | Description |
|
|
13
|
-
|----------|-------------|
|
|
14
|
-
| `SPREE_API_URL` | Your Spree API endpoint (e.g., `https://api.mystore.com`) |
|
|
15
|
-
| `SPREE_PUBLISHABLE_KEY` | Publishable API key from your Spree admin |
|
|
16
|
-
|
|
17
|
-
> **NOTE:** These are server-side only variables — no `NEXT_PUBLIC_` prefix needed since all API calls happen in Server Actions.
|
|
18
|
-
|
|
19
|
-
### Optional
|
|
20
|
-
|
|
21
|
-
| Variable | Description | Default |
|
|
22
|
-
|----------|-------------|---------|
|
|
23
|
-
| `GTM_ID` | Google Tag Manager container ID | _(disabled)_ |
|
|
24
|
-
| `SENTRY_DSN` | Sentry DSN for error tracking | _(disabled)_ |
|
|
25
|
-
| `SENTRY_ORG` | Sentry organization slug | _(none)_ |
|
|
26
|
-
| `SENTRY_PROJECT` | Sentry project slug | _(none)_ |
|
|
27
|
-
| `SENTRY_AUTH_TOKEN` | Sentry auth token (for source maps in CI) | _(none)_ |
|
|
8
|
+
At minimum, set `SPREE_API_URL` and `SPREE_PUBLISHABLE_KEY` in your hosting platform's dashboard or `.env` file. See [Environment Variables](environment-variables.md) for the full reference — analytics, error tracking, wholesale, emails, and SEO.
|
|
28
9
|
|
|
29
10
|
## Production Build
|
|
30
11
|
|
|
@@ -43,7 +24,13 @@ Vercel is the recommended deployment platform for Next.js applications.
|
|
|
43
24
|
|
|
44
25
|
1. Push your code to GitHub
|
|
45
26
|
2. Go to [vercel.com/new](https://vercel.com/new) and import your repository
|
|
46
|
-
3. Add environment variables
|
|
27
|
+
3. Add environment variables:
|
|
28
|
+
- `SPREE_API_URL` and `SPREE_PUBLISHABLE_KEY` (required)
|
|
29
|
+
- `SPREE_WEBHOOK_SECRET`, `RESEND_API_KEY`, `EMAIL_FROM` — for [transactional emails](emails.md)
|
|
30
|
+
- `GTM_ID` — optional, Google Tag Manager
|
|
31
|
+
- `SENTRY_DSN`, `SENTRY_ORG`, `SENTRY_PROJECT`, `SENTRY_AUTH_TOKEN` — optional, error tracking with readable stack traces
|
|
32
|
+
|
|
33
|
+
See the [Environment Variables reference](environment-variables.md) for the full list.
|
|
47
34
|
4. Click **Deploy**
|
|
48
35
|
|
|
49
36
|
Vercel automatically detects the Next.js framework and configures the build settings.
|
|
@@ -61,56 +48,50 @@ Every pull request gets a unique preview URL, making it easy to test storefront
|
|
|
61
48
|
|
|
62
49
|
## Docker
|
|
63
50
|
|
|
64
|
-
|
|
51
|
+
A multi-stage `Dockerfile` ships at the repo root. It uses Next.js standalone output to produce a small (~240 MB) image on `node:22-alpine`, runs as a non-root user, and exposes port `3001`.
|
|
65
52
|
|
|
66
|
-
|
|
67
|
-
FROM node:20-alpine AS base
|
|
53
|
+
> **NOTE:** `SPREE_API_URL` and `SPREE_PUBLISHABLE_KEY` are required at **build time** — the storefront prerenders pages against the Spree API. Point them at a Spree instance reachable from wherever you run `docker build` (hosted Spree, a tunnel, or `host.docker.internal` for a local backend on Docker Desktop).
|
|
68
54
|
|
|
69
|
-
|
|
70
|
-
WORKDIR /app
|
|
71
|
-
COPY package.json package-lock.json ./
|
|
72
|
-
RUN npm ci --production=false
|
|
73
|
-
|
|
74
|
-
FROM base AS builder
|
|
75
|
-
WORKDIR /app
|
|
76
|
-
COPY --from=deps /app/node_modules ./node_modules
|
|
77
|
-
COPY . .
|
|
78
|
-
RUN npm run build
|
|
55
|
+
Build and run:
|
|
79
56
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
57
|
+
```bash
|
|
58
|
+
docker build \
|
|
59
|
+
--build-arg SPREE_API_URL=https://your-spree.example.com \
|
|
60
|
+
--build-arg SPREE_PUBLISHABLE_KEY=your_publishable_key \
|
|
61
|
+
-t spree-storefront .
|
|
83
62
|
|
|
84
|
-
|
|
85
|
-
|
|
63
|
+
docker run -p 3001:3001 --env-file .env.local spree-storefront
|
|
64
|
+
```
|
|
86
65
|
|
|
87
|
-
|
|
88
|
-
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
|
89
|
-
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
|
66
|
+
### Sentry source maps at build time
|
|
90
67
|
|
|
91
|
-
|
|
92
|
-
EXPOSE 3000
|
|
93
|
-
ENV PORT=3000
|
|
94
|
-
ENV HOSTNAME="0.0.0.0"
|
|
68
|
+
`SENTRY_AUTH_TOKEN` is passed via a BuildKit secret so it never lands in image layers or the build cache. The other Sentry vars are regular build args:
|
|
95
69
|
|
|
96
|
-
|
|
70
|
+
```bash
|
|
71
|
+
SENTRY_AUTH_TOKEN=... docker build \
|
|
72
|
+
--build-arg SPREE_API_URL=... \
|
|
73
|
+
--build-arg SPREE_PUBLISHABLE_KEY=... \
|
|
74
|
+
--build-arg SENTRY_DSN=... \
|
|
75
|
+
--build-arg SENTRY_ORG=... \
|
|
76
|
+
--build-arg SENTRY_PROJECT=... \
|
|
77
|
+
--secret id=sentry_auth_token,env=SENTRY_AUTH_TOKEN \
|
|
78
|
+
-t spree-storefront .
|
|
97
79
|
```
|
|
98
80
|
|
|
99
|
-
|
|
100
|
-
>
|
|
101
|
-
> ```typescript
|
|
102
|
-
const nextConfig = {
|
|
103
|
-
output: 'standalone',
|
|
104
|
-
}
|
|
105
|
-
```
|
|
81
|
+
### Building against a local Spree backend
|
|
106
82
|
|
|
107
|
-
|
|
83
|
+
On Docker Desktop (macOS/Windows), reach the host's Spree via `host.docker.internal`:
|
|
108
84
|
|
|
109
85
|
```bash
|
|
110
|
-
docker build
|
|
111
|
-
docker
|
|
112
|
-
-
|
|
113
|
-
-
|
|
86
|
+
docker build \
|
|
87
|
+
--add-host=host.docker.internal:host-gateway \
|
|
88
|
+
--build-arg SPREE_API_URL=http://host.docker.internal:3000 \
|
|
89
|
+
--build-arg SPREE_PUBLISHABLE_KEY=your_publishable_key \
|
|
90
|
+
-t spree-storefront .
|
|
91
|
+
|
|
92
|
+
docker run -p 3001:3001 \
|
|
93
|
+
--add-host=host.docker.internal:host-gateway \
|
|
94
|
+
--env-file .env.local \
|
|
114
95
|
spree-storefront
|
|
115
96
|
```
|
|
116
97
|
|
|
@@ -131,7 +112,7 @@ npm run build
|
|
|
131
112
|
npm start
|
|
132
113
|
```
|
|
133
114
|
|
|
134
|
-
The server listens on port `
|
|
115
|
+
The server listens on port `3001`. Set the `PORT` environment variable to change it.
|
|
135
116
|
|
|
136
117
|
## CI/CD
|
|
137
118
|
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Transactional Emails
|
|
3
|
+
description: Render and send order, shipment, and account emails from the Spree Next.js Storefront with react-email, Resend, and Spree webhooks
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
The storefront can own its customer-facing transactional emails instead of the Spree backend. Emails are rendered with [react-email](https://react.email) and sent via [Resend](https://resend.com); the Spree backend delivers order, shipment, and account events to the storefront over [webhooks](../../core-concepts/webhooks.md).
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
Spree Backend → Webhook POST → /api/webhooks/spree → render email → send via Resend
|
|
10
|
+
(signed HMAC) (signature verified) (react-email) (or write to disk in dev)
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Templates
|
|
14
|
+
|
|
15
|
+
Email templates are React components in `src/lib/emails/`:
|
|
16
|
+
|
|
17
|
+
| File | Event | Description |
|
|
18
|
+
|------|-------|-------------|
|
|
19
|
+
| `order-confirmation.tsx` | `order.completed` | Items, totals, addresses, delivery method |
|
|
20
|
+
| `order-canceled.tsx` | `order.canceled` | Cancellation notice with items |
|
|
21
|
+
| `shipment-shipped.tsx` | `order.shipped` | Tracking number and link |
|
|
22
|
+
| `password-reset.tsx` | `customer.password_reset_requested` | Reset button and fallback link |
|
|
23
|
+
|
|
24
|
+
Customize a template by editing its file directly — they use `@react-email/components` for email-safe layout primitives.
|
|
25
|
+
|
|
26
|
+
## Previewing
|
|
27
|
+
|
|
28
|
+
Run the storefront in development and open [http://localhost:3001/dev/emails](http://localhost:3001/dev/emails):
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npm run dev
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Each template renders with sample data via `@react-email/render`. The route is gated to non-production environments.
|
|
35
|
+
|
|
36
|
+
## Configuration
|
|
37
|
+
|
|
38
|
+
Add these to `.env.local`:
|
|
39
|
+
|
|
40
|
+
```env
|
|
41
|
+
SPREE_WEBHOOK_SECRET=your_webhook_endpoint_secret_key
|
|
42
|
+
RESEND_API_KEY=re_your_resend_api_key # production only
|
|
43
|
+
EMAIL_FROM=Your Store <orders@your-domain.com> # production only
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
In development no `RESEND_API_KEY` is needed — emails are written to `.next/emails/` as HTML files with a `file://` link logged to the console.
|
|
47
|
+
|
|
48
|
+
## Webhook Handler
|
|
49
|
+
|
|
50
|
+
The webhook route (`src/app/api/webhooks/spree/route.ts`) wires events to handlers with `createWebhookHandler` from `src/lib/spree/webhooks`. Signature verification and event routing are handled for you:
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
import { createWebhookHandler } from '@/lib/spree/webhooks'
|
|
54
|
+
|
|
55
|
+
const handler = createWebhookHandler({
|
|
56
|
+
secret: process.env.SPREE_WEBHOOK_SECRET!,
|
|
57
|
+
handlers: {
|
|
58
|
+
'order.completed': handleOrderCompleted,
|
|
59
|
+
'order.canceled': handleOrderCanceled,
|
|
60
|
+
'order.shipped': handleOrderShipped,
|
|
61
|
+
'customer.password_reset_requested': handlePasswordReset,
|
|
62
|
+
},
|
|
63
|
+
})
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
To add a new email type:
|
|
67
|
+
|
|
68
|
+
1. Create a template in `src/lib/emails/`.
|
|
69
|
+
2. Add a handler function in `src/lib/webhooks/handlers.ts`.
|
|
70
|
+
3. Register the event in `route.ts`.
|
|
71
|
+
4. Subscribe to the event in **Spree Admin → Settings → Developers → Webhooks**.
|
|
72
|
+
|
|
73
|
+
## Setup
|
|
74
|
+
|
|
75
|
+
1. **Create a webhook endpoint** in Spree Admin → Settings → Developers → Webhooks. Subscribe to `order.completed`, `order.canceled`, `order.shipped`, and `customer.password_reset_requested`, and copy the secret key into `SPREE_WEBHOOK_SECRET`.
|
|
76
|
+
|
|
77
|
+
2. **Receive webhooks locally.** Expose the storefront with a public URL so Spree can reach it — the simplest option is a [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/):
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
brew install cloudflared
|
|
81
|
+
cloudflared tunnel --url http://localhost:3001
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Use the tunnel URL as the webhook endpoint URL in Spree Admin.
|
|
85
|
+
|
|
86
|
+
For the backend perspective on delivering these events, see [Sending out Emails](../../deployment/emails.md).
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Environment Variables
|
|
3
|
+
description: Every environment variable the Spree Next.js Storefront reads, with defaults and when to set it
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Configuration lives in `.env.local` (copy `.env.example` to start). Variables **without** a `NEXT_PUBLIC_` prefix are server-side only and never reach the browser; `NEXT_PUBLIC_` variables are inlined into the client bundle at build time, so only put non-secret values there.
|
|
7
|
+
|
|
8
|
+
## Required
|
|
9
|
+
|
|
10
|
+
| Variable | Description |
|
|
11
|
+
|----------|-------------|
|
|
12
|
+
| `SPREE_API_URL` | Your Spree API endpoint (e.g. `http://localhost:3000` in dev, `https://api.mystore.com` in production) |
|
|
13
|
+
| `SPREE_PUBLISHABLE_KEY` | Publishable API key from your Spree admin |
|
|
14
|
+
|
|
15
|
+
> **NOTE:** In a Docker build these two are also required at **build time** — the storefront prerenders pages against the Spree API. See [Deployment](deployment.md).
|
|
16
|
+
|
|
17
|
+
## Store defaults
|
|
18
|
+
|
|
19
|
+
Used by the middleware for initial redirects before API data loads, and as build-time fallbacks for sitemap/SEO generation.
|
|
20
|
+
|
|
21
|
+
| Variable | Description | Default |
|
|
22
|
+
|----------|-------------|---------|
|
|
23
|
+
| `NEXT_PUBLIC_DEFAULT_COUNTRY` | Default country ISO code — should match your store's `default_country_iso` | `us` |
|
|
24
|
+
| `NEXT_PUBLIC_DEFAULT_LOCALE` | Default locale code — should match your store's `default_locale` | `en` |
|
|
25
|
+
| `NEXT_PUBLIC_SITE_URL` | Public site URL, used for sitemap and `robots.txt` generation (e.g. `https://mystore.com`) | _(required for sitemap)_ |
|
|
26
|
+
| `NEXT_PUBLIC_STORE_NAME` | Store name used in metadata fallbacks | `Spree Store` |
|
|
27
|
+
| `NEXT_PUBLIC_STORE_DESCRIPTION` | Store description used in metadata fallbacks | _(sample text)_ |
|
|
28
|
+
|
|
29
|
+
## SEO & social
|
|
30
|
+
|
|
31
|
+
Optional overrides for site metadata and the Organization JSON-LD. When unset, values fall back to store settings from the Spree API.
|
|
32
|
+
|
|
33
|
+
| Variable | Description |
|
|
34
|
+
|----------|-------------|
|
|
35
|
+
| `STORE_SEO_TITLE` | Default `<title>` |
|
|
36
|
+
| `STORE_META_DESCRIPTION` | Default meta description |
|
|
37
|
+
| `STORE_META_KEYWORDS` | Default meta keywords |
|
|
38
|
+
| `STORE_TWITTER` | Twitter/X handle or URL |
|
|
39
|
+
| `STORE_FACEBOOK` | Facebook page URL |
|
|
40
|
+
| `STORE_INSTAGRAM` | Instagram URL |
|
|
41
|
+
| `STORE_LOGO_URL` | Logo URL for Organization JSON-LD |
|
|
42
|
+
| `STORE_SUPPORT_EMAIL` | Customer support email |
|
|
43
|
+
|
|
44
|
+
## Wholesale B2B portal
|
|
45
|
+
|
|
46
|
+
The [wholesale portal](wholesale.md) is an opt-in addon, off by default.
|
|
47
|
+
|
|
48
|
+
| Variable | Description | Default |
|
|
49
|
+
|----------|-------------|---------|
|
|
50
|
+
| `SPREE_WHOLESALE_CHANNEL` | Enable switch — the code of a gated Spree channel to bind the `/wholesale` surface to. Unset means DTC-only: every wholesale entry point is hidden and the routes 404 | _(disabled)_ |
|
|
51
|
+
| `SPREE_WHOLESALE_PUBLISHABLE_KEY` | Channel-scoped publishable key for the wholesale surface. Optional — the channel header selects the channel, so this falls back to `SPREE_PUBLISHABLE_KEY` | _(falls back to `SPREE_PUBLISHABLE_KEY`)_ |
|
|
52
|
+
|
|
53
|
+
## Transactional emails
|
|
54
|
+
|
|
55
|
+
See the [Transactional Emails](emails.md) guide for the full setup.
|
|
56
|
+
|
|
57
|
+
| Variable | Description | Default |
|
|
58
|
+
|----------|-------------|---------|
|
|
59
|
+
| `SPREE_WEBHOOK_SECRET` | Webhook endpoint secret key that signs incoming Spree webhooks | _(disabled)_ |
|
|
60
|
+
| `RESEND_API_KEY` | [Resend](https://resend.com) API key for sending emails in production | _(dev: writes to disk)_ |
|
|
61
|
+
| `EMAIL_FROM` | "From" address for transactional emails (e.g. `Store <orders@mystore.com>`) | `orders@example.com` |
|
|
62
|
+
|
|
63
|
+
## Payments
|
|
64
|
+
|
|
65
|
+
Publishable keys for client-side payment SDKs, read when the matching gateway is enabled. Set the key for whichever provider(s) you use.
|
|
66
|
+
|
|
67
|
+
| Variable | Description |
|
|
68
|
+
|----------|-------------|
|
|
69
|
+
| `NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY` | Stripe publishable key (`pk_…`) |
|
|
70
|
+
|
|
71
|
+
## Analytics
|
|
72
|
+
|
|
73
|
+
| Variable | Description | Default |
|
|
74
|
+
|----------|-------------|---------|
|
|
75
|
+
| `GTM_ID` | Google Tag Manager container ID (e.g. `GTM-XXXXXXX`). Leave empty to disable | _(disabled)_ |
|
|
76
|
+
|
|
77
|
+
## Error tracking (Sentry)
|
|
78
|
+
|
|
79
|
+
| Variable | Description | Default |
|
|
80
|
+
|----------|-------------|---------|
|
|
81
|
+
| `SENTRY_DSN` | Sentry DSN — set to enable error tracking (e.g. `https://key@o0.ingest.sentry.io/0`) | _(disabled)_ |
|
|
82
|
+
| `SENTRY_ORG` | Sentry organization slug (for source map uploads) | _(none)_ |
|
|
83
|
+
| `SENTRY_PROJECT` | Sentry project slug (for source map uploads) | _(none)_ |
|
|
84
|
+
| `SENTRY_AUTH_TOKEN` | Sentry auth token (for source map uploads in CI) | _(none)_ |
|
|
85
|
+
| `SENTRY_SEND_DEFAULT_PII` | Send PII (IP addresses, cookies, user data) to Sentry server-side | `false` |
|
|
86
|
+
| `NEXT_PUBLIC_SENTRY_SEND_DEFAULT_PII` | Send PII to Sentry client-side | `false` |
|
|
87
|
+
|
|
88
|
+
> **WARNING:** PII collection is disabled by default. Only set the `SENTRY_SEND_DEFAULT_PII` variables to `true` if you have appropriate user consent or a privacy policy covering this data.
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Multi-Region
|
|
3
|
+
description: How the Spree Next.js Storefront serves multiple countries, currencies, and languages from a single deployment via URL segments and edge middleware
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
The storefront serves multiple countries, currencies, and languages from a single deployment. Region is encoded in the URL, detected automatically for first-time visitors by middleware, and mapped to a [Spree Market](../../core-concepts/markets.md) — the model that bundles a country, currency, and locale — so every server-side fetch resolves prices and content for the right region.
|
|
7
|
+
|
|
8
|
+
## URL structure
|
|
9
|
+
|
|
10
|
+
Every route is prefixed with a country and locale segment:
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
/us/en/products # US market, English
|
|
14
|
+
/de/de/products # German market, German
|
|
15
|
+
/uk/en/products # UK market, English
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
The `[country]` and `[locale]` segments are the first two dynamic params of the App Router tree (`src/app/[country]/[locale]/…`). Because region lives in the path, every URL is shareable and independently cacheable, and search engines index each region separately.
|
|
19
|
+
|
|
20
|
+
## The middleware
|
|
21
|
+
|
|
22
|
+
A visitor who lands on a bare path (no region prefix) is redirected to the correct `/{country}/{locale}/…` URL by an edge middleware. It's wired in `src/proxy.ts`:
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
import { createSpreeMiddleware } from '@/lib/spree/middleware'
|
|
26
|
+
import { getDefaultCountry, getDefaultLocale } from '@/lib/store'
|
|
27
|
+
|
|
28
|
+
export const proxy = createSpreeMiddleware({
|
|
29
|
+
defaultCountry: getDefaultCountry(),
|
|
30
|
+
defaultLocale: getDefaultLocale(),
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
export const config = {
|
|
34
|
+
matcher: ['/((?!api/|_next/static|_next/image|favicon.ico|.*\\..*$).*)'],
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
`createSpreeMiddleware` (from `src/lib/spree`) does three things:
|
|
39
|
+
|
|
40
|
+
- **Redirects** bare paths to `/{country}/{locale}/…`.
|
|
41
|
+
- **Detects** the visitor's country and locale (see below).
|
|
42
|
+
- **Syncs** `spree_country` and `spree_locale` cookies with the URL segments, so server-side fetches via `getLocaleOptions()` resolve the same market the URL asks for.
|
|
43
|
+
|
|
44
|
+
Requests that already carry a `/{country}/{locale}` prefix pass through untouched — the middleware only refreshes the cookies to match.
|
|
45
|
+
|
|
46
|
+
### Detection chain
|
|
47
|
+
|
|
48
|
+
For a request without a region prefix, each value is resolved from the first source that has it:
|
|
49
|
+
|
|
50
|
+
| Value | Resolution order |
|
|
51
|
+
|-------|------------------|
|
|
52
|
+
| **Country** | `spree_country` cookie → `x-vercel-ip-country` / `cf-ipcountry` geo header → default |
|
|
53
|
+
| **Locale** | `spree_locale` cookie → `Accept-Language` header (primary tag) → default |
|
|
54
|
+
|
|
55
|
+
Geo headers are set by the hosting edge — `x-vercel-ip-country` on Vercel, `cf-ipcountry` behind Cloudflare. On a host that provides neither, detection falls back to `Accept-Language` for locale and the configured default for country. A returning visitor's cookie always wins, so a manual region change sticks.
|
|
56
|
+
|
|
57
|
+
### Configuration
|
|
58
|
+
|
|
59
|
+
`createSpreeMiddleware` accepts:
|
|
60
|
+
|
|
61
|
+
| Option | Description | Default |
|
|
62
|
+
|--------|-------------|---------|
|
|
63
|
+
| `defaultCountry` | Fallback country ISO when nothing else matches | `us` |
|
|
64
|
+
| `defaultLocale` | Fallback locale when nothing else matches | `en` |
|
|
65
|
+
| `staticRoutes` | Path prefixes to skip | `['/_next', '/api', '/dev', '/favicon.ico']` |
|
|
66
|
+
|
|
67
|
+
In the storefront these defaults come from `NEXT_PUBLIC_DEFAULT_COUNTRY` and `NEXT_PUBLIC_DEFAULT_LOCALE` (see [Environment Variables](environment-variables.md)). The `matcher` in `proxy.ts` additionally excludes API routes, Next.js internals, and any path with a file extension, so the middleware only runs on page navigations.
|
|
68
|
+
|
|
69
|
+
## Switching regions
|
|
70
|
+
|
|
71
|
+
The `CountrySwitcher` component (in `src/components/layout/`) lets a visitor change region manually. Selecting a new region navigates to the matching URL prefix; the middleware then updates the `spree_country` / `spree_locale` cookies so the choice persists across future visits.
|
|
72
|
+
|
|
73
|
+
## How region reaches the data layer
|
|
74
|
+
|
|
75
|
+
Region-aware reads don't take a country/locale argument — they call `getLocaleOptions()` from `src/lib/spree`, which reads the `spree_country` / `spree_locale` cookies the middleware keeps in sync with the URL. That value is passed to `@spree/sdk`, which sends it to the Store API so prices, currency, and translated content come back for the right [market](../../core-concepts/markets.md). See [Architecture](architecture.md) for the wider server-first data flow.
|
|
@@ -34,19 +34,19 @@ The [Spree Storefront](https://github.com/spree/storefront) is a production-read
|
|
|
34
34
|
|
|
35
35
|
## Installation
|
|
36
36
|
|
|
37
|
-
###
|
|
37
|
+
### Recommended: create-spree-app
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
The fastest way to get a full project — Spree backend, this Next.js storefront, and the `spree` CLI, wired together — is [`create-spree-app`](../../create-spree-app/quickstart.md):
|
|
40
40
|
|
|
41
41
|
```bash
|
|
42
|
-
|
|
43
|
-
cd storefront
|
|
44
|
-
npm install
|
|
42
|
+
npx create-spree-app my-store
|
|
45
43
|
```
|
|
46
44
|
|
|
47
|
-
|
|
45
|
+
The storefront is included by default (pass `--no-storefront` to skip it). It's scaffolded into `apps/storefront/` with its `.env.local` already pointing at the backend, so it boots against real data with no manual key wiring.
|
|
46
|
+
|
|
47
|
+
### Standalone
|
|
48
48
|
|
|
49
|
-
|
|
49
|
+
To run the storefront on its own — against an existing Spree backend, or to work on the storefront repo directly — clone it and install:
|
|
50
50
|
|
|
51
51
|
```bash
|
|
52
52
|
git clone https://github.com/spree/storefront.git
|
|
@@ -54,6 +54,8 @@ cd storefront
|
|
|
54
54
|
npm install
|
|
55
55
|
```
|
|
56
56
|
|
|
57
|
+
Then follow [Configuration](#configuration) to point it at your Spree API. If you plan to customize and track upstream changes, [fork first](customization.md#tracking-upstream-updates).
|
|
58
|
+
|
|
57
59
|
## Configuration
|
|
58
60
|
|
|
59
61
|
Copy the environment file:
|
|
@@ -81,6 +83,8 @@ npm run dev
|
|
|
81
83
|
|
|
82
84
|
Open [http://localhost:3001](http://localhost:3001) in your browser.
|
|
83
85
|
|
|
86
|
+
> **NOTE:** Testing Apple Pay / Google Pay locally needs a public HTTPS URL. See [Wallet Payments in Development](wallet-payments.md).
|
|
87
|
+
|
|
84
88
|
## Production Build
|
|
85
89
|
|
|
86
90
|
```bash
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Testing
|
|
3
|
+
description: Run the Spree Next.js Storefront's unit tests with Vitest and its end-to-end suite with Playwright against a real Spree backend
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
The storefront ships two test layers: fast unit/integration tests with Vitest, and a browser end-to-end suite with Playwright that runs against a real Spree backend booted in Docker.
|
|
7
|
+
|
|
8
|
+
## Unit & integration (Vitest)
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npm test # one-shot
|
|
12
|
+
npm run test:watch # watch mode
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## End-to-end (Playwright)
|
|
16
|
+
|
|
17
|
+
The E2E suite drives a browser against a real Spree backend. `npm run e2e:up` boots the backend — Postgres, Redis, and the official `ghcr.io/spree/spree:latest` image from `e2e-backend/docker-compose.yml` — then seeds it and mints an API key through the official [`@spree/cli`](../../cli/quickstart.md) (`spree seed`, `spree sample-data`, `spree api-key create`), installed as a dev dependency. No `create-spree-app` setup is required.
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
# 1. Export a Stripe test-mode key pair from your own Stripe sandbox.
|
|
21
|
+
# Both keys must belong to the same account — Stripe no longer
|
|
22
|
+
# publishes a working sample secret key, and a mismatched pair makes
|
|
23
|
+
# the checkout payment step fail.
|
|
24
|
+
export STRIPE_PUBLISHABLE_KEY=pk_test_…
|
|
25
|
+
export STRIPE_SECRET_KEY=sk_test_…
|
|
26
|
+
|
|
27
|
+
# 2. Boot Spree + Postgres + Redis, seed sample data, register a Stripe
|
|
28
|
+
# payment gateway, mint a publishable key, and write .env.e2e.
|
|
29
|
+
npm run e2e:up
|
|
30
|
+
|
|
31
|
+
# 3. Run the suite. Playwright boots `next dev` against .env.e2e.
|
|
32
|
+
npm run test:e2e
|
|
33
|
+
|
|
34
|
+
# Optional: interactive UI mode.
|
|
35
|
+
npm run test:e2e:ui
|
|
36
|
+
|
|
37
|
+
# Tear everything down.
|
|
38
|
+
npm run e2e:down
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
The checkout test pays with card `4242 4242 4242 4242` through Stripe's [test mode](https://docs.stripe.com/keys). PaymentIntents land in whichever Stripe test account owns the keys you exported.
|
|
42
|
+
|
|
43
|
+
### CI
|
|
44
|
+
|
|
45
|
+
In CI, set `STRIPE_SECRET_KEY` as a repository secret and `STRIPE_PUBLISHABLE_KEY` as a repository variable (**Settings → Secrets and variables → Actions**). The E2E job skips itself on fork PRs, where GitHub never exposes repository secrets.
|
|
46
|
+
|
|
47
|
+
## Testing against a customized backend
|
|
48
|
+
|
|
49
|
+
> **NOTE:** By default the E2E suite runs against the stock official Spree image (`ghcr.io/spree/spree:latest`), so it verifies the storefront against a **vanilla Spree** — not a backend with your own extensions, serializers, or seed data.
|
|
50
|
+
|
|
51
|
+
The E2E stack reads a `SPREE_IMAGE` env var and falls back to the stock image when it's unset. Set it to any image that exposes the Store API on the container's port `3000`, and the suite runs against that backend instead.
|
|
52
|
+
|
|
53
|
+
### Your own Spree image
|
|
54
|
+
|
|
55
|
+
If you already publish a customized Spree image:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
SPREE_IMAGE=ghcr.io/your-org/your-spree:latest npm run e2e:up
|
|
59
|
+
npm run test:e2e
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### A `create-spree-app` project's backend
|
|
63
|
+
|
|
64
|
+
A project scaffolded with [`create-spree-app`](../../create-spree-app/quickstart.md) has its customizable Spree in `backend/` (built from `backend/Dockerfile`) and this storefront in `apps/storefront/`. Build that backend into a local image and point the suite at it — this is what tests the storefront against the **exact Spree you deploy**, extensions and all:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
# From the project root — build the customized backend image
|
|
68
|
+
docker build -t project-spree:e2e ./backend
|
|
69
|
+
|
|
70
|
+
# From apps/storefront — run E2E against it
|
|
71
|
+
cd apps/storefront
|
|
72
|
+
SPREE_IMAGE=project-spree:e2e npm run e2e:up
|
|
73
|
+
npm run test:e2e
|
|
74
|
+
npm run e2e:down
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
> **NOTE:** New `create-spree-app` projects wire this into the generated storefront CI workflow automatically — the workflow builds `backend/Dockerfile` and runs the storefront E2E against that image rather than stock Spree.
|
|
78
|
+
|
|
79
|
+
### Interactive / manual
|
|
80
|
+
|
|
81
|
+
To click through the storefront against a running backend (no E2E harness), point it there directly: set `SPREE_API_URL` and `SPREE_PUBLISHABLE_KEY` in `.env.local` (see [Environment Variables](environment-variables.md)) and run `npm run dev`.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Wallet Payments in Development
|
|
3
|
+
description: Test Apple Pay and Google Pay against a local Spree Next.js Storefront using a public HTTPS URL
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Apple Pay and Google Pay require HTTPS **and a publicly-reachable URL** — Stripe verifies the payment method domain from the internet, so `localhost` and locally-trusted certificates (e.g. `mkcert` + `lvh.me`) won't pass domain verification. The simplest way to expose your local storefront with a valid public HTTPS URL is [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/).
|
|
7
|
+
|
|
8
|
+
## Setup
|
|
9
|
+
|
|
10
|
+
1. Install `cloudflared`:
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
brew install cloudflared
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
2. Start the dev server normally (HTTP on port 3001):
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm run dev
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
3. In a second terminal, expose it through a quick tunnel:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
cloudflared tunnel --url http://localhost:3001
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The output will contain a URL like `https://<random-words>.trycloudflare.com`.
|
|
29
|
+
|
|
30
|
+
4. Register that URL in your [Stripe Payment method domains](https://dashboard.stripe.com/settings/payment_methods/domains).
|
|
31
|
+
|
|
32
|
+
5. Open the tunnel URL in your browser and test the Express Checkout buttons in the cart.
|
|
33
|
+
|
|
34
|
+
## Stable tunnel URLs
|
|
35
|
+
|
|
36
|
+
`next.config.ts` already allows `*.trycloudflare.com` via `allowedDevOrigins`, so quick tunnels work out of the box. Every time you restart `cloudflared tunnel --url ...` you get a new random subdomain — if you need a stable URL (to avoid re-registering in Stripe on each run), set up a [named tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/do-more-with-tunnels/trycloudflare/#using-named-tunnels) on your own domain.
|
|
37
|
+
|
|
38
|
+
## The Spree backend must also be publicly reachable
|
|
39
|
+
|
|
40
|
+
The storefront's server-side fetches go to `SPREE_API_URL`, but image URLs and a few other backend-served paths (e.g. the Apple Pay domain-verification file under `/.well-known/apple-developer-merchantid-domain-association`) are fetched by the browser directly and must resolve from the public internet.
|
|
41
|
+
|
|
42
|
+
Point `SPREE_API_URL` at a hosted Spree (e.g. `*.spree.sh`, `*.vendo.dev`, your own staging) or expose your local Spree with another tunnel:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
cloudflared tunnel --url http://localhost:3000
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
When tunneling a local Rails app, allow the tunnel host in the backend's `.env`:
|
|
49
|
+
|
|
50
|
+
```env
|
|
51
|
+
RAILS_DEVELOPMENT_HOSTS=.trycloudflare.com
|
|
52
|
+
```
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Wholesale Portal
|
|
3
|
+
description: The opt-in B2B wholesale surface and how channel-backed surfaces work
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## What the Wholesale Portal Is
|
|
7
|
+
|
|
8
|
+
The storefront ships an optional **wholesale portal** at `/wholesale` — a gated B2B surface for approved trade buyers. It runs on a separate Spree sales channel but shares the same storefront app and Spree backend as the public direct-to-consumer (DTC) store, so a single deployment serves both audiences: the open catalog for shoppers and a login-gated trade catalog with volume pricing for wholesale buyers.
|
|
9
|
+
|
|
10
|
+
The portal is an **opt-in addon**. It is off by default; a DTC-only storefront never renders any wholesale UI.
|
|
11
|
+
|
|
12
|
+
> **NOTE:** The wholesale portal is a **reference implementation**, not a fixed feature. It demonstrates one shape — a gated B2B surface running *alongside* an open DTC store — but the same building blocks (channels, [surfaces](#surfaces-and-channel-switching), and [access gating](#gating-modes)) support the whole spectrum. You can invert the default and make the storefront closed or limited: gate the *primary* DTC channel with `login_required` or `prices_hidden` for a members-only or invite-only shop, drop the public catalog entirely and ship a login-first B2B storefront, or run several gated channels with no open surface at all. Treat this page as a worked example to adapt, not a prescription — the storefront is yours to reshape.
|
|
13
|
+
|
|
14
|
+
## Surfaces and Channel Switching
|
|
15
|
+
|
|
16
|
+
A **surface** is a distinct sales context backed by its own Spree [channel](../../core-concepts/channels.md). The storefront models two out of the box — `dtc` (the public storefront) and `wholesale` (the trade portal) — but the mechanism is general: any channel on your backend can back its own surface.
|
|
17
|
+
|
|
18
|
+
The surface selects which SDK client a request goes through. A channel-bound client sends the channel code on every request via the `X-Spree-Channel` header, and the Store API [resolves the request against that channel](../../core-concepts/channels.md#resolution-at-request-time) — its catalog, pricing, and access rules. In the SDK this is the `channel` client option:
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import { createClient } from '@spree/sdk'
|
|
22
|
+
|
|
23
|
+
const wholesaleClient = createClient({
|
|
24
|
+
baseUrl: process.env.SPREE_API_URL,
|
|
25
|
+
publishableKey: process.env.SPREE_PUBLISHABLE_KEY,
|
|
26
|
+
channel: 'wholesale', // sent as X-Spree-Channel on every request
|
|
27
|
+
})
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Alternatively, a **channel-bound publishable key** carries the channel server-side: when a key is scoped to a channel on the backend, the API assigns that channel to the request without needing the header. Either path works; the header is the simplest and is what the storefront uses by default.
|
|
31
|
+
|
|
32
|
+
Because each surface has its own client, cart cookies, and cache keys, a customer can hold an open DTC cart and an open wholesale cart at the same time without them mixing. The customer session (JWT) is shared — it's the same person signing in — so only the cart splits, never the auth.
|
|
33
|
+
|
|
34
|
+
To add your own channel-backed surface, follow the same pattern: create the channel on the backend, add a client bound to its code, and thread the surface through the routes that should target it.
|
|
35
|
+
|
|
36
|
+
## Enabling Wholesale
|
|
37
|
+
|
|
38
|
+
Wholesale turns on through a single environment variable:
|
|
39
|
+
|
|
40
|
+
- **`SPREE_WHOLESALE_CHANNEL`** — the enable switch. Set it to the code of a gated channel on your backend (e.g. `wholesale`). There is **no default**: when it is unset, the storefront runs DTC-only — the wholesale nav link, footer link, and homepage section are hidden, and every `/wholesale` route returns 404.
|
|
41
|
+
- **`SPREE_WHOLESALE_PUBLISHABLE_KEY`** — optional. The channel header alone selects the channel, so this falls back to `SPREE_PUBLISHABLE_KEY`. Set it only to bind a channel-scoped publishable key.
|
|
42
|
+
|
|
43
|
+
The backend must have a [channel](../../core-concepts/channels.md) whose code matches `SPREE_WHOLESALE_CHANNEL`. Its [`storefront_access`](../../core-concepts/channels.md#storefront-access-gating) posture decides how much a guest can see — the portal supports both `login_required` (guests are walled off entirely) and `prices_hidden` (guests browse the catalog but not prices). See [Gating Modes](#gating-modes) below. Spree installs seed a `login_required` wholesale channel by default; switching to `prices_hidden` is a single flag on that same channel — no new channel or seed data.
|
|
44
|
+
|
|
45
|
+
## How Gating Works End to End
|
|
46
|
+
|
|
47
|
+
Two independent checks gate the portal — one at the **channel** (how much of the catalog a guest may see), one at the **customer** (whether a buyer may transact at trade pricing). The channel check is set by the channel's [gating mode](#gating-modes); the customer check is always the same.
|
|
48
|
+
|
|
49
|
+
**Channel access** is enforced by the Store API, not the storefront — the storefront can't loosen it. Depending on the channel's `storefront_access` posture, a guest is either rejected outright (`login_required`) or served the catalog with money fields returned as `null` (`prices_hidden`). This is a general channel feature; see [Channels → Storefront Access Gating](../../core-concepts/channels.md#storefront-access-gating) for how it's resolved and enforced.
|
|
50
|
+
|
|
51
|
+
**Buyer approval.** Being logged in isn't enough; a buyer must be *approved*. Approval is membership in the **Wholesale** [customer group](../../core-concepts/pricing.md#customer-group-rule) — an admin adds an applicant to the group to approve them. The storefront reads `customer_groups` on `customers/me` and branches accordingly:
|
|
52
|
+
|
|
53
|
+
- **Guest** — sees the sign-in / apply wall (`login_required`), or the read-only catalog with sign-in-for-pricing prompts (`prices_hidden`).
|
|
54
|
+
- **Signed in, not in the group** — sees an application-pending state. Signing in never skips approval: an authenticated but unapproved buyer cannot transact under either mode.
|
|
55
|
+
- **Signed in and in the group** — gets the full portal: trade catalog, quick order, and the wholesale cart.
|
|
56
|
+
|
|
57
|
+
**Trade pricing** is unlocked by volume. The seeded model grants a trade price once a line reaches a minimum quantity of the same item (10 or more per item in the sample data). The applicable volume rule lives on a backend [Price List](../../core-concepts/pricing.md#price-lists); the storefront surfaces the trade price the API returns for a qualifying quantity.
|
|
58
|
+
|
|
59
|
+
## Gating Modes
|
|
60
|
+
|
|
61
|
+
A channel's `storefront_access` posture is one of three values. The wholesale portal supports the two gated modes; the third describes a fully public channel like DTC.
|
|
62
|
+
|
|
63
|
+
| Mode | Guest sees catalog | Guest sees prices | Guest can order |
|
|
64
|
+
|---|---|---|---|
|
|
65
|
+
| `login_required` | No — hard `401`, sign-in wall | No | No |
|
|
66
|
+
| `prices_hidden` | Yes — read-only | No — "sign in for pricing" | No — "sign in to order" |
|
|
67
|
+
| `public` | Yes | Yes | Yes (subject to guest checkout) |
|
|
68
|
+
|
|
69
|
+
**`login_required`** is the strictest posture and the seeded default. The Store API rejects any unauthenticated request to the channel with `401`, so a guest can't read wholesale catalog or pricing at all. The storefront shows the sign-in / apply wall in place of every gated page.
|
|
70
|
+
|
|
71
|
+
**`prices_hidden`** opens the catalog to guests while keeping pricing private. The channel doesn't reject unauthenticated requests, but the API serializes every money field as `null` for a guest. In the portal, a guest browses the catalog and product pages read-only: each price renders a **"sign in for pricing"** prompt, and add-to-cart becomes **"sign in to order."** Ordering surfaces (cart, quick order) still require sign-in — a guest is redirected to the sign-in flow rather than shown an empty wholesale cart. Approval is unchanged: signing in reveals list prices, and joining the Wholesale group unlocks trade pricing. This mode suits a trade catalog you want discoverable (for SEO or lead generation) without exposing negotiated pricing.
|
|
72
|
+
|
|
73
|
+
**`public`** is an ungated channel — the posture the DTC store runs on. It's listed here for completeness; a wholesale channel would not normally use it.
|
|
74
|
+
|
|
75
|
+
Switching a wholesale channel between the two supported modes is a single change to [`storefront_access`](../../core-concepts/channels.md#storefront-access-gating) on the backend — set on the channel, or inherited from the [store's default](../../core-concepts/stores.md#storefront-access-defaults). Nothing in the storefront needs redeploying — the portal reads the channel's posture at request time and adapts. Login and signup always run through the same approval flow regardless of mode.
|
|
76
|
+
|
|
77
|
+
## Related Documentation
|
|
78
|
+
|
|
79
|
+
- [Channels](../../core-concepts/channels.md) — The sales channel that backs the wholesale surface, and the general [Storefront Access Gating](../../core-concepts/channels.md#storefront-access-gating) mechanism the portal builds on
|
|
80
|
+
- [Stores](../../core-concepts/stores.md) — Store-level [defaults](../../core-concepts/stores.md#storefront-access-defaults) that channels inherit
|
|
81
|
+
- [Pricing](../../core-concepts/pricing.md) — [Price Lists](../../core-concepts/pricing.md#price-lists) and the [Customer Group Rule](../../core-concepts/pricing.md#customer-group-rule) behind approval and trade pricing
|
|
82
|
+
- [Store SDK: Configuration](../../sdk/configuration.md) — `setChannel` and the channel client option used to bind a surface
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Upgrading to Spree 5.6
|
|
3
|
+
description: Step-by-step guide to upgrading a Spree 5.5 application to Spree 5.6, including gem updates, migrations, data backfills, and breaking changes to review.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
> **INFO:** Before proceeding to upgrade, please ensure you're at [Spree 5.5](5.4-to-5.5.md).
|
|
7
|
+
|
|
8
|
+
The upgrade is usually completed in four steps:
|
|
9
|
+
|
|
10
|
+
1. **Update the Ruby gems** which power Spree API
|
|
11
|
+
2. **Run database migrations** — to migrate your existing schema to the new version
|
|
12
|
+
3. **Run data backfills** — to move your existing data into the new schema and power new features
|
|
13
|
+
4. Apply optional configuration and review behavior changes — to take advantage of new features and avoid surprises
|
|
14
|
+
|
|
15
|
+
## How to upgrade
|
|
16
|
+
|
|
17
|
+
For applications created via `create-spree-app` command we greatly recommend using the Spree CLI to perform the upgrade. It provides a guided experience with prompts and handles the first three steps for you. If you prefer to run the commands manually or not using docker for local development, you can follow the "Without Spree CLI" path.
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
```bash Spree CLI (Docker)
|
|
21
|
+
spree upgrade
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
```bash Without Spree CLI
|
|
25
|
+
# cd backend if you're in the monorepo root
|
|
26
|
+
bundle update
|
|
27
|
+
bundle exec rake spree:install:migrations && bin/rails db:migrate
|
|
28
|
+
bundle exec rake spree:upgrade
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
The **Spree CLI** path runs all three commands for you with prompts. Recommended for local development. If you don't have the CLI yet, either install it globally or run it through `npx`:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
# install once and use `spree …` everywhere
|
|
36
|
+
npm install -g @spree/cli
|
|
37
|
+
|
|
38
|
+
# or invoke without installing (each command runs through npx)
|
|
39
|
+
npx @spree/cli upgrade
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The **Without Spree CLI** path is the bare equivalent. Use this on production: `bundle update` and `db:migrate` are part of your existing deploy pipeline (Heroku release phase, K8s init container, Capistrano hook, Render auto-migrate). Once the 5.6 release is up, run `bundle exec rake spree:upgrade` from a one-off dyno / job container / `kubectl exec` to perform the data backfills.
|
|
43
|
+
|
|
44
|
+
Skipping versions and re-running are both safe — `bundle exec rake spree:upgrade` figures out what still needs to happen and does nothing on data that's already migrated.
|
|
45
|
+
|
|
46
|
+
## What the upgrade does
|
|
47
|
+
|
|
48
|
+
This is reference material — what `bundle exec rake spree:upgrade` (and equivalently `spree upgrade`) actually executes on your data. Skip if you trust the tool; read on if something failed or you're curious. Every step is idempotent, so re-running the full manifest is safe.
|
|
49
|
+
|
|
50
|
+
### Backfill store ownership on role assignments
|
|
51
|
+
|
|
52
|
+
Spree 5.6 resolves admin roles per store. The migrations add `store_id` to `spree_role_users`, but existing assignments have a NULL value until backfilled:
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
```bash Spree CLI (Docker)
|
|
56
|
+
spree rake spree:role_users:backfill_store_ids
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
```bash Without Spree CLI
|
|
60
|
+
bundle exec rake spree:role_users:backfill_store_ids
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
Sets `spree_role_users.store_id` from the store resource so `Spree::Ability` resolves roles by store. Store-scoped assignments only — extensions (e.g. `spree_multi_vendor`) backfill their own resource types. Until this runs, store admins stay authorized via the `spree_admin?` fallback.
|
|
65
|
+
|
|
66
|
+
### Backfill store ownership on promotions and payment methods
|
|
67
|
+
|
|
68
|
+
Spree 5.6 moves `Spree::Promotion` and `Spree::PaymentMethod` from multi-store sharing to single-owner `belongs_to :store`. The migrations add a `store_id` column to each; this task populates it:
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
```bash Spree CLI (Docker)
|
|
72
|
+
spree rake spree:upgrade:populate_single_store_associations
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
```bash Without Spree CLI
|
|
76
|
+
bundle exec rake spree:upgrade:populate_single_store_associations
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
Sets `store_id` on `Spree::Promotion` and `Spree::PaymentMethod` from the legacy `spree_promotions_stores` / `spree_payment_methods_stores` join tables.
|
|
81
|
+
|
|
82
|
+
> **WARNING:** Until this runs, migrated rows have a NULL `store_id` and are hidden from store-scoped lookups — a payment method with no `store_id` is unavailable at checkout. Run it right after `db:migrate`.
|
|
83
|
+
|
|
84
|
+
Records shared across several stores keep **one** owner (promotions: the earliest `spree_promotions_stores` row by `created_at`, then lowest `store_id`; payment methods: the lowest `store_id`, since that join has no timestamps) unless [`spree_multi_store`](#multi-store-deployments) is installed. Each shared record is logged so the loss is visible.
|
|
85
|
+
|
|
86
|
+
### Backfill store ownership on taxons
|
|
87
|
+
|
|
88
|
+
Categories carry a direct `store_id` in 5.6. The task sets it from each taxon's taxonomy, then resolves taxonomy-less rows through their parent chain:
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
```bash Spree CLI (Docker)
|
|
92
|
+
spree rake spree:taxons:backfill_store_id
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
```bash Without Spree CLI
|
|
96
|
+
bundle exec rake spree:taxons:backfill_store_id
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
Until this runs, existing taxons keep resolving their store via the taxonomy join (`Taxon.for_store` fallback); taxonomy-less categories (`Spree::Category`) rely on the direct `store_id`, so the backfill is required for them.
|
|
101
|
+
|
|
102
|
+
### Recompute taxon product counts
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
```bash Spree CLI (Docker)
|
|
106
|
+
spree rake spree:taxons:backfill_products_count
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
```bash Without Spree CLI
|
|
110
|
+
bundle exec rake spree:taxons:backfill_products_count
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
Recomputes the descendant-inclusive `products_count` counter cache on every taxon, in batches.
|
|
115
|
+
|
|
116
|
+
### Backfill the store tenant on product tags
|
|
117
|
+
|
|
118
|
+
Spree 5.6 bounds product tag autocomplete to the owning store. Product taggings now carry a store `tenant` (like order taggings already did); this task sets it on tags created before the upgrade:
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
```bash Spree CLI (Docker)
|
|
122
|
+
spree rake spree:upgrade:backfill_product_tag_tenants
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
```bash Without Spree CLI
|
|
126
|
+
bundle exec rake spree:upgrade:backfill_product_tag_tenants
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
Sets the `tenant` column on existing `Spree::Product` taggings from each product's `store_id`. New product taggings tenant themselves automatically — only rows created before the upgrade need this. Until it runs, product tags created before the upgrade are hidden from the store's tag-autocomplete vocabulary (they reappear once it completes; storefront and admin tag *display* are unaffected).
|
|
131
|
+
|
|
132
|
+
## Update the Spree SDK
|
|
133
|
+
|
|
134
|
+
Spree 5.6 ships alongside `@spree/sdk` 1.2. The backend upgrade never touches your frontend source — bump the SDK in every JavaScript consumer of the Store API and take it through your normal PR/CI cycle:
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
# create-spree-app projects: the Next.js storefront
|
|
138
|
+
cd apps/storefront
|
|
139
|
+
npm install @spree/sdk@^1.2
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
If you maintain a separate storefront repo or other integrations, repeat there.
|
|
143
|
+
|
|
144
|
+
| Spree backend | `@spree/sdk` |
|
|
145
|
+
|---|---|
|
|
146
|
+
| 5.4 | 1.0.x |
|
|
147
|
+
| 5.5 | 1.1+ |
|
|
148
|
+
| 5.6 | 1.2+ |
|
|
149
|
+
|
|
150
|
+
## Behavior changes to review
|
|
151
|
+
|
|
152
|
+
These don't require any rake task — but storefronts, integrations, and merchant-facing dashboards may need code changes to handle them correctly.
|
|
153
|
+
|
|
154
|
+
### Promotions and payment methods belong to a single store
|
|
155
|
+
|
|
156
|
+
`Spree::Promotion` and `Spree::PaymentMethod` are now single-owner (`belongs_to :store`). Create them through the store association so the owner is set:
|
|
157
|
+
|
|
158
|
+
```ruby
|
|
159
|
+
store.payment_methods.create!(...)
|
|
160
|
+
store.promotions.create!(...)
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
A record built off the association but not yet saved is invisible to `available_for_store?` until persisted. The `store_ids=` writer that used to fan a record across stores is removed; multi-store sharing moves to the [`spree_multi_store`](#multi-store-deployments) extension. The `spree_promotions_stores` / `spree_payment_methods_stores` join tables are kept as legacy compat surface and dropped in a later release.
|
|
164
|
+
|
|
165
|
+
### Admin roles resolve per store
|
|
166
|
+
|
|
167
|
+
With `store_id` on role assignments, an admin's abilities are scoped to the store they're assigned to. Until [the role backfill](#backfill-store-ownership-on-role-assignments) runs, existing store admins remain authorized through the `spree_admin?` fallback, so there is no lockout — but run it so role resolution is correct going forward.
|
|
168
|
+
|
|
169
|
+
### Product tag autocomplete is store-scoped
|
|
170
|
+
|
|
171
|
+
The Admin API tag-autocomplete endpoint (`GET /api/v3/admin/tags`) now returns only tags used within the current store for store-owned taggables (products and orders). Customer tags remain global. If an integration relied on this endpoint returning tags across every store, that cross-store vocabulary is no longer exposed. Run [the tag-tenant backfill](#backfill-the-store-tenant-on-product-tags) so pre-upgrade product tags are included.
|
|
172
|
+
|
|
173
|
+
## Multi-store deployments
|
|
174
|
+
|
|
175
|
+
Sharing a single promotion, payment method, or product across multiple stores is no longer part of Spree core — 5.6 assigns each of these to one owning store. If you run several stores from one installation and need shared records, install the `spree_multi_store` extension **before** running the backfills; without it, the backfills assign each shared record to a single owner store and the others lose it (every such case is logged).
|
|
176
|
+
|
|
177
|
+
For single-store deployments this is invisible — the backfills assign every record to your one store, and you can move on.
|