@spree/docs 0.1.12 → 0.1.13

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.
@@ -165,18 +165,22 @@ When editing a resource (e.g., a product), metafields appear in a dedicated sect
165
165
 
166
166
  ## Metafields vs Metadata
167
167
 
168
- | Feature | Metafields | Metadata |
168
+ Spree has two permanent, complementary systems for custom data — **metadata for machines, metafields for humans**. They serve different purposes and are not interchangeable. Neither is going away.
169
+
170
+ | Feature | Metafields | [Metadata](../customization/metadata.md) |
169
171
  |---------|-----------|----------|
170
- | Structure | Strongly typed, schema-defined | Flat key-value JSON |
171
- | Validation | Type-specific validation | None |
172
- | Visibility | Configurable (front/back/both) | Write-only in Store API |
173
- | Admin UI | Dedicated management forms | JSON preview |
174
- | Data Types | 6 specific types | Any JSON-serializable data |
175
- | Organization | Namespaced (`namespace.key`) | Flat structure |
172
+ | **Purpose** | Merchant-defined structured attributes | Developer escape hatch — integration IDs, sync state |
173
+ | **Schema** | Defined via MetafieldDefinitions | Schemaless JSON — no definition required |
174
+ | **Validation** | Type-specific (text, number, boolean, etc.) | None accepts any JSON-serializable data |
175
+ | **Visibility** | Configurable (admin-only or public) | Write-only in Store API, readable in Admin API |
176
+ | **Admin UI** | Dedicated management forms | JSON preview |
177
+ | **Data Types** | 6 specific types | Any JSON value |
178
+ | **Organization** | Namespaced (`namespace.key`) | Flat key-value structure |
179
+ | **Queryable** | Via SQL joins, Ransack scopes, search providers | Via JSONB operators (PostgreSQL) |
176
180
 
177
181
  **Use Metafields** when you need type validation, visibility control, admin UI forms, or organized namespacing.
178
182
 
179
- **Use Metadata** for external system IDs, tracking attribution, or simple write-and-forget data.
183
+ **Use [Metadata](../customization/metadata.md)** for external system IDs, tracking attribution, syncing with integrations, or simple write-and-forget data that only backend systems need to read.
180
184
 
181
185
  > **WARNING:** Product Properties are deprecated and will be removed in Spree 6.0. For new projects, always use Metafields. For existing projects, plan to migrate using the [migration guide](../upgrades/5.1-to-5.2.md#3-migrate-to-metafields-or-keep-using-properties).
182
186
 
@@ -35,7 +35,7 @@ And add the following code to it:
35
35
 
36
36
  ```ruby
37
37
  class MyAddToCartService < Spree::Cart::AddItem
38
- def call(order:, variant:, quantity: nil, public_metadata: {}, private_metadata: {}, options: {})
38
+ def call(order:, variant:, quantity: nil, metadata: {}, options: {})
39
39
  ApplicationRecord.transaction do
40
40
  run :add_to_line_item
41
41
  run Spree.cart_recalculate_service
@@ -6,6 +6,8 @@ title: Metadata
6
6
 
7
7
  Metadata provides simple, unstructured key-value storage on Spree resources — similar to [Stripe's metadata](https://docs.stripe.com/api/metadata). It's ideal for storing integration IDs, tracking data, or any arbitrary information that doesn't need validation or admin UI.
8
8
 
9
+ Metadata is a **permanent, first-class system** in Spree. It is designed to coexist alongside [Metafields](../core-concepts/metafields.md) (structured, typed, admin-managed). The two systems serve different purposes and are not interchangeable — think of it as **metadata for machines, metafields for humans**.
10
+
9
11
  Metadata is **write-only** in the Store API — you can set it when creating or updating resources, but it is never returned in Store API responses. It is visible in Admin API responses for administrative use.
10
12
 
11
13
  > **INFO:** For structured, type-safe custom attributes with admin UI support, use [Metafields](../core-concepts/metafields.md) instead.
@@ -144,7 +146,7 @@ When there is no metadata, the field is `null`.
144
146
 
145
147
  ### Reading and writing
146
148
 
147
- Every model that includes `Spree::Metadata` has a `metadata` accessor (alias for `private_metadata`):
149
+ Every model that includes `Spree::Metadata` has a `metadata` accessor. Always use `metadata` in Ruby code — do not call `public_metadata=` or `private_metadata=` directly. (SQL queries still reference the underlying `private_metadata` column name — see querying examples below.)
148
150
 
149
151
  ```ruby
150
152
  order = Spree::Order.find_by!(number: 'R123456')
@@ -188,14 +190,17 @@ Metadata updates use **merge semantics** — existing keys are preserved, new ke
188
190
 
189
191
  ## Metadata vs Metafields
190
192
 
193
+ Spree has two permanent, complementary systems for custom data. They are not interchangeable and neither is going away.
194
+
191
195
  | | Metadata | [Metafields](../core-concepts/metafields.md) |
192
196
  |---|---|---|
193
- | **Use case** | Integration data, tracking, simple key-value | Structured custom attributes with admin UI |
194
- | **Validation** | None | Type-specific (text, number, boolean, JSON) |
197
+ | **Purpose** | Developer escape hatch — integration data, sync state, ad-hoc flags | Merchant-defined structured attributes with admin UI |
198
+ | **Schema** | Schemaless JSON — no definition required | Defined via MetafieldDefinitions (typed, validated) |
199
+ | **Validation** | None — accepts any JSON-serializable data | Type-specific (text, number, boolean, rich text, JSON) |
195
200
  | **Visibility** | Write-only in Store API, readable in Admin API | Configurable (front-end, back-end, both) |
196
- | **Admin UI** | Viewable in JSON preview | Full admin management |
197
- | **Schema** | Schemaless JSON | Defined via MetafieldDefinitions |
198
- | **API pattern** | Stripe-style flat key-value | Shopify-style typed resources |
201
+ | **Admin UI** | JSON preview only | Dedicated management forms |
202
+ | **API pattern** | Stripe-style: `metadata: { key: value }` | Expand-based: `?expand=metafields` |
203
+ | **Queryable** | Via JSONB operators (PostgreSQL) | Via SQL joins, Ransack scopes, search providers |
199
204
 
200
205
  ### When to use metadata
201
206
 
@@ -203,13 +208,15 @@ Metadata updates use **merge semantics** — existing keys are preserved, new ke
203
208
  - Tracking attribution data (UTM parameters, referral source)
204
209
  - Passing context from the storefront that doesn't need validation
205
210
  - Any write-and-forget data that only needs to be read by backend systems
211
+ - Syncing state with external integrations (webhooks, ETL pipelines)
206
212
 
207
213
  ### When to use metafields
208
214
 
209
- - Custom product specifications shown to customers
210
- - Admin-managed fields with validation
211
- - Data that needs to appear in the admin UI
212
- - Querying/filtering by custom attributes
215
+ - Custom product specifications shown to customers (material, dimensions, certifications)
216
+ - Admin-managed fields with validation and type safety
217
+ - Data that needs to appear in the admin UI with dedicated form inputs
218
+ - Querying/filtering by custom attributes (search facets, product filtering)
219
+ - CSV import/export of structured product data
213
220
 
214
221
  ## Supported resources
215
222
 
@@ -220,17 +227,21 @@ The Store API currently supports writing metadata on:
220
227
  - **Carts** — on creation and update (`POST /api/v3/store/carts`, `PATCH /api/v3/store/carts/:id`)
221
228
  - **Items** — on create and update (`POST/PATCH /api/v3/store/carts/:id/items`)
222
229
 
223
- ## Migration from public_metadata
230
+ ## Deprecation: public_metadata
231
+
232
+ > **WARNING:** `public_metadata` is deprecated and will be removed in Spree 6.0. Use `metadata` instead.
233
+
234
+ The `public_metadata` column was never exposed in Store API responses and in practice served the same purpose as `private_metadata`. It will be removed in Spree 6.0 and calling `public_metadata=` will emit a deprecation warning.
224
235
 
225
- The `public_metadata` column is deprecated. Metadata is no longer returned in Store API responses. If you were using `public_metadata` for data that needs to be visible to customers, migrate to [Metafields](../core-concepts/metafields.md) with `display_on: 'both'`.
236
+ Always use the single `metadata` accessor for all schemaless key-value storage. If you need data visible to customers on the storefront, use [Metafields](../core-concepts/metafields.md) instead.
226
237
 
227
238
  ```ruby
228
- # Before (deprecated)
239
+ # Deprecated — will be removed in 6.0
229
240
  order.public_metadata = { 'gift_message' => 'Happy Birthday!' }
230
241
 
231
- # After — use metadata for write-only storage
242
+ # Use metadata for internal storage
232
243
  order.metadata = { 'gift_message' => 'Happy Birthday!' }
233
244
 
234
- # After — use metafields for customer-visible structured data
245
+ # Use metafields for customer-visible structured data
235
246
  order.set_metafield('custom.gift_message', 'Happy Birthday!')
236
247
  ```
@@ -8,14 +8,14 @@ description: Server-first architecture, project structure, and auth flow
8
8
  The storefront follows a **server-first architecture** where all API calls are made server-side. The Spree API key is never exposed to the browser.
9
9
 
10
10
  ```
11
- Browser → Server Action → @spree/next → Spree API
12
- (with httpOnly cookies)
11
+ Browser → Server Action → @spree/sdk → Spree API
12
+ (with httpOnly cookies via @spree/next helpers)
13
13
  ```
14
14
 
15
- - **Server Actions** (`src/lib/data/`) — thin wrappers around `@spree/next` data functions
15
+ - **Server Actions** (`src/lib/data/`) — call `@spree/sdk` directly with auth/cookie helpers from `@spree/next`
16
16
  - **httpOnly Cookies** — auth tokens, cart tokens, and locale are stored securely
17
17
  - **No Client-Side API Calls** — the Spree API key stays on the server
18
- - **Auto-Localization** — locale and country are read from cookies automatically by `@spree/next`
18
+ - **Auto-Localization** — locale and country are read from cookies via `getLocaleOptions()`
19
19
 
20
20
  ## Project Structure
21
21
 
@@ -59,7 +59,7 @@ src/
59
59
  └── lib/
60
60
  ├── analytics/ # GTM integration
61
61
  ├── constants.ts # App constants
62
- ├── data/ # Server Actions
62
+ ├── data/ # Server Actions (call @spree/sdk directly)
63
63
  │ ├── addresses.ts # Address CRUD
64
64
  │ ├── cart.ts # Cart operations
65
65
  │ ├── checkout.ts # Checkout flow
@@ -71,8 +71,7 @@ src/
71
71
  │ ├── orders.ts # Order history
72
72
  │ ├── payment.ts # Payment processing
73
73
  │ ├── products.ts # Product queries
74
- │ ├── store.ts # Store configuration
75
- │ ├── categories.ts # Categories
74
+ │ ├── categories.ts # Categories
76
75
  │ └── utils.ts # Shared helpers (actionResult, withFallback)
77
76
  └── utils/ # Client utilities
78
77
  ├── address.ts # Address formatting
@@ -85,21 +84,26 @@ src/
85
84
  ## Authentication Flow
86
85
 
87
86
  1. User submits login form
88
- 2. Server action calls `@spree/next` which authenticates with the Spree API
89
- 3. JWT token is stored in an httpOnly cookie by `@spree/next`
90
- 4. Subsequent requests include the token automatically
87
+ 2. Server action calls `@spree/sdk` to authenticate
88
+ 3. JWT token is stored in an httpOnly cookie via `@spree/next` cookie helpers
89
+ 4. Subsequent requests use `withAuthRefresh()` which reads the token from cookies automatically
91
90
  5. Token is never accessible to client-side JavaScript
92
91
 
93
92
  ```typescript
94
93
  // src/lib/data/customer.ts
95
- import { login as _login, getCustomer as _getCustomer } from '@spree/next'
94
+ import { getClient, withAuthRefresh, setAccessToken, setRefreshToken } from '@spree/next'
96
95
 
97
96
  export async function login(email: string, password: string) {
98
- return _login(email, password) // token stored in httpOnly cookie automatically
97
+ const result = await getClient().auth.login({ email, password })
98
+ await setAccessToken(result.token)
99
+ await setRefreshToken(result.refresh_token)
100
+ return { success: true, user: result.user }
99
101
  }
100
102
 
101
103
  export async function getCustomer() {
102
- return _getCustomer() // reads token from cookie automatically
104
+ return withAuthRefresh(async (options) => {
105
+ return getClient().customer.get(options) // reads token from cookie
106
+ })
103
107
  }
104
108
  ```
105
109
 
@@ -117,17 +121,17 @@ A middleware (`src/proxy.ts`) uses `createSpreeMiddleware` from `@spree/next` to
117
121
 
118
122
  ## Server Actions
119
123
 
120
- All data fetching is done through server actions in `src/lib/data/`. These are thin wrappers around `@spree/next` functions locale and currency are resolved automatically from cookies:
124
+ All data fetching is done through server actions in `src/lib/data/`. These call `@spree/sdk` directly, using `@spree/next` helpers for auth and locale:
121
125
 
122
126
  ```typescript
123
- // Products
127
+ // Products — use getLocaleOptions() for locale-aware reads
124
128
  import { getProducts, getProduct, getProductFilters } from '@/lib/data/products'
125
129
 
126
130
  const products = await getProducts({ limit: 12 })
127
131
  const product = await getProduct('product-slug')
128
132
  const filters = await getProductFilters()
129
133
 
130
- // Cart
134
+ // Cart — use getCartOptions()/requireCartId() for cart operations
131
135
  import { getCart, addToCart, updateCartItem, removeCartItem } from '@/lib/data/cart'
132
136
 
133
137
  const cart = await getCart()
@@ -135,14 +139,14 @@ await addToCart('var_xxx', 1)
135
139
  await updateCartItem('li_xxx', 2)
136
140
  await removeCartItem('li_xxx')
137
141
 
138
- // Authentication
142
+ // Authentication — use withAuthRefresh() for authenticated endpoints
139
143
  import { login, register, logout, getCustomer } from '@/lib/data/customer'
140
144
 
141
145
  await login('user@example.com', 'password')
142
146
  const customer = await getCustomer()
143
147
  await logout()
144
148
 
145
- // Addresses
149
+ // Addresses — use withAuthRefresh() for customer data
146
150
  import { getAddresses, createAddress, updateAddress, deleteAddress } from '@/lib/data/addresses'
147
151
 
148
152
  const addresses = await getAddresses()
@@ -111,7 +111,7 @@ To customize API behavior, modify the server actions in `src/lib/data/`. Each fi
111
111
  | `gift-cards.ts` | Gift card management |
112
112
  | `utils.ts` | Shared helpers (error handling, fallbacks) |
113
113
 
114
- These server actions wrap the `@spree/next` package functions. You can add custom logic, caching strategies, or additional transformations as needed.
114
+ These server actions call `@spree/sdk` directly, using `@spree/next` helpers for auth cookies and locale resolution. You can add custom logic, caching strategies, or additional transformations as needed.
115
115
 
116
116
  ## Adding New Pages
117
117
 
@@ -122,10 +122,10 @@ src/app/[country]/[locale]/(storefront)/your-new-page/page.tsx
122
122
  ```
123
123
 
124
124
  ```typescript
125
- import { listProducts } from '@spree/next';
125
+ import { getProducts } from '@/lib/data/products';
126
126
 
127
127
  export default async function YourNewPage() {
128
- const products = await listProducts({ limit: 6 });
128
+ const products = await getProducts({ limit: 6 });
129
129
 
130
130
  return (
131
131
  <div>
@@ -12,7 +12,7 @@ The [Spree Storefront](https://github.com/spree/storefront) is a production-read
12
12
  - **Tailwind CSS 4** - Utility-first styling
13
13
  - **TypeScript 5** - Full type safety
14
14
  - **[@spree/sdk](https://github.com/spree/spree/tree/main/packages/sdk)** - Official Spree Commerce SDK
15
- - **[@spree/next](https://github.com/spree/spree/tree/main/packages/next)** - Server actions, caching, and cookie-based auth
15
+ - **[@spree/next](https://github.com/spree/spree/tree/main/packages/next)** - Cookie-based auth, middleware, and webhook helpers
16
16
  - **Sentry** - Error tracking and performance monitoring
17
17
 
18
18
  ## Features
@@ -1,9 +1,11 @@
1
1
  ---
2
2
  title: "@spree/next Package"
3
- description: Next.js integration library — server actions, caching, and cookie-based auth
3
+ description: Next.js integration library — cookie-based auth, middleware, and webhook helpers
4
4
  ---
5
5
 
6
- The `@spree/next` package provides a complete Next.js integration for Spree Commerce with server actions, automatic cookie management, and full TypeScript support.
6
+ The `@spree/next` package provides the server-side plumbing for building a Next.js storefront with `@spree/sdk`: JWT token lifecycle, httpOnly cookie management, locale/country middleware, and webhook verification.
7
+
8
+ Your storefront calls `@spree/sdk` directly for all API operations. `@spree/next` handles the Next.js-specific concerns — cookies, auth refresh, and middleware — so you don't have to.
7
9
 
8
10
  ## Installation
9
11
 
@@ -42,211 +44,137 @@ initSpreeNext({
42
44
  });
43
45
  ```
44
46
 
45
- ## Data Functions
47
+ ## Architecture
46
48
 
47
- Plain async functions for reading data in Server Components. Wrap with `"use cache"` in your app for caching.
49
+ `@spree/next` provides four things:
48
50
 
49
- ### Products
51
+ 1. **`getClient()`** — singleton `@spree/sdk` Client instance (auto-inits from env vars)
52
+ 2. **Auth helpers** — `withAuthRefresh()` for JWT lifecycle, cookie read/write for tokens
53
+ 3. **Locale resolution** — `getLocaleOptions()` reads country/locale from cookies
54
+ 4. **Framework helpers** — `createSpreeMiddleware()` for URL routing, `createWebhookHandler()` for webhooks
50
55
 
51
- ```typescript
52
- import { listProducts, getProduct, getProductFilters } from '@spree/next';
56
+ Your storefront's server actions call `@spree/sdk` directly, using these helpers for auth and cookies:
53
57
 
54
- const products = await listProducts({ limit: 25 });
55
- const product = await getProduct('spree-tote', { expand: ['variants', 'media'] });
56
- const filters = await getProductFilters({ category_id: 'ctg_123' });
58
+ ```
59
+ Server Action getClient().products.list(params, localeOptions)
60
+ Server Action withAuthRefresh(fn) getClient().customer.get(options)
61
+ Server Action → getCartOptions() → getClient().carts.items.create(...)
57
62
  ```
58
63
 
59
- ### Categories
64
+ ## Auth Helpers
60
65
 
61
- ```typescript
62
- import { listCategories, getCategory, listCategoryProducts } from '@spree/next';
66
+ ### `withAuthRefresh(fn)`
63
67
 
64
- const categories = await listCategories({ depth_eq: 1 });
65
- const category = await getCategory('clothing/shirts');
66
- const products = await listCategoryProducts('clothing/shirts', { limit: 12 });
67
- ```
68
+ Wraps an authenticated SDK call with automatic token management:
68
69
 
69
- ### Store & Geography
70
+ 1. Reads JWT from cookie
71
+ 2. Proactively refreshes if expiring within 5 minutes
72
+ 3. Executes your function with `{ token }` options
73
+ 4. On 401: refreshes token and retries once
74
+ 5. On refresh failure: clears cookies and throws
70
75
 
71
76
  ```typescript
72
- import { getStore, listCountries, getCountry, listCurrencies, listLocales } from '@spree/next';
77
+ 'use server';
73
78
 
74
- const store = await getStore();
75
- const countries = await listCountries();
76
- const usa = await getCountry('US');
77
- const currencies = await listCurrencies();
78
- const locales = await listLocales();
79
- ```
79
+ import { getClient, withAuthRefresh } from '@spree/next';
80
80
 
81
- ### Using in Server Components
81
+ export async function getCustomer() {
82
+ return withAuthRefresh(async (options) => {
83
+ return getClient().customer.get(options);
84
+ });
85
+ }
82
86
 
83
- ```typescript
84
- import { listProducts, listCategories } from '@spree/next';
85
-
86
- export default async function ProductsPage() {
87
- const products = await listProducts({ limit: 12 });
88
- const categories = await listCategories({ depth_eq: 1 });
89
-
90
- return (
91
- <div>
92
- {products.data.map((product) => (
93
- <div key={product.id}>{product.name}</div>
94
- ))}
95
- </div>
96
- );
87
+ export async function listAddresses() {
88
+ return withAuthRefresh(async (options) => {
89
+ return getClient().customer.addresses.list(undefined, options);
90
+ });
97
91
  }
98
92
  ```
99
93
 
100
- ## Server Actions
94
+ ### `getAuthOptions()`
101
95
 
102
- Server actions handle mutations and auth-dependent reads. They automatically manage cookies for cart tokens and JWT authentication.
96
+ Lower-level helper that returns `{ token }` or `{}` with proactive refresh. Use when you need the token but want to handle errors yourself.
103
97
 
104
- ### Cart
98
+ ## Cookie Management
105
99
 
106
- ```typescript
107
- import { getCart, getOrCreateCart, addItem, updateItem, removeItem, clearCart, associateCart } from '@spree/next';
108
-
109
- const cart = await getCart(); // null if no cart
110
- const cart = await getOrCreateCart(); // creates if needed
111
- await addItem(variantId, quantity);
112
- await updateItem(lineItemId, { quantity: 2 });
113
- await removeItem(lineItemId);
114
- await clearCart();
115
- await associateCart(); // link guest cart to logged-in user
116
- ```
117
-
118
- ### Checkout
119
-
120
- Checkout functions use the implicit cart resolved from cookies. No `orderId` is needed.
100
+ All cookie helpers are async (they use Next.js `cookies()` API):
121
101
 
122
102
  ```typescript
123
103
  import {
124
- getCheckout,
125
- updateCheckout,
126
- selectDeliveryRate,
127
- applyDiscountCode,
128
- removeDiscountCode,
129
- applyGiftCard,
130
- removeGiftCard,
131
- complete,
104
+ // Cart
105
+ getCartToken, getCartId, setCartCookies, clearCartCookies,
106
+ getCartOptions, // { spreeToken, token } for cart SDK calls
107
+ requireCartId, // throws if no cart
108
+
109
+ // Auth
110
+ getAccessToken, setAccessToken, clearAccessToken,
111
+ getRefreshToken, setRefreshToken, clearRefreshToken,
132
112
  } from '@spree/next';
133
-
134
- const checkout = await getCheckout();
135
- await updateCheckout({ shipping_address: { ... }, billing_address: { ... } });
136
- await selectDeliveryRate(fulfillmentId, deliveryRateId);
137
- await applyDiscountCode('SAVE20');
138
- await removeDiscountCode('SAVE20');
139
- await applyGiftCard('GC-ABCD-1234');
140
- await removeGiftCard('gc_abc123');
141
- await complete();
142
113
  ```
143
114
 
144
- ### Authentication
115
+ ### Cart operations pattern
145
116
 
146
117
  ```typescript
147
- import { login, register, logout, getCustomer, updateCustomer } from '@spree/next';
148
-
149
- await login(email, password);
150
- await register(email, password, passwordConfirmation);
151
- await logout();
152
- const customer = await getCustomer(); // null if not logged in
153
- await updateCustomer({ first_name: 'John' });
154
- ```
118
+ 'use server';
155
119
 
156
- ### Addresses
120
+ import { getClient, getCartOptions, requireCartId } from '@spree/next';
157
121
 
158
- ```typescript
159
- import { listAddresses, getAddress, createAddress, updateAddress, deleteAddress } from '@spree/next';
160
-
161
- const addresses = await listAddresses();
162
- const address = await getAddress(addressId);
163
- await createAddress({ first_name: 'John', address1: '123 Main St', ... });
164
- await updateAddress(addressId, { city: 'Brooklyn' });
165
- await deleteAddress(addressId);
166
- ```
167
-
168
- ### Orders
169
-
170
- ```typescript
171
- import { listOrders, getOrder } from '@spree/next';
172
-
173
- const orders = await listOrders();
174
- const order = await getOrder(orderId);
122
+ export async function updateCartItem(lineItemId: string, quantity: number) {
123
+ const options = await getCartOptions();
124
+ const cartId = await requireCartId();
125
+ return getClient().carts.items.update(cartId, lineItemId, { quantity }, options);
126
+ }
175
127
  ```
176
128
 
177
- ### Payment Sessions
178
-
179
- Payment session functions use the implicit cart. No `orderId` is needed.
129
+ ### Auth operations pattern
180
130
 
181
131
  ```typescript
182
- import {
183
- createPaymentSession,
184
- getPaymentSession,
185
- updatePaymentSession,
186
- completePaymentSession,
187
- } from '@spree/next';
188
-
189
- const session = await createPaymentSession({ payment_method_id: 'pm_123' });
190
-
191
- // Access provider data (e.g., Stripe client secret)
192
- const clientSecret = session.external_data.client_secret;
193
-
194
- const current = await getPaymentSession(sessionId);
195
- await updatePaymentSession(sessionId, { amount: '50.00' });
196
- await completePaymentSession(sessionId, { session_result: 'success' });
197
- ```
132
+ 'use server';
198
133
 
199
- ### Payment Setup Sessions
134
+ import { getClient, setAccessToken, setRefreshToken, getCartToken, getCartId } from '@spree/next';
200
135
 
201
- For saving payment methods outside of checkout (e.g., "Add a credit card" in account settings):
136
+ export async function login(email: string, password: string) {
137
+ const result = await getClient().auth.login({ email, password });
138
+ await setAccessToken(result.token);
139
+ await setRefreshToken(result.refresh_token);
202
140
 
203
- ```typescript
204
- import {
205
- createPaymentSetupSession,
206
- getPaymentSetupSession,
207
- completePaymentSetupSession,
208
- } from '@spree/next';
141
+ // Associate guest cart if one exists
142
+ const cartToken = await getCartToken();
143
+ const cartId = await getCartId();
144
+ if (cartToken && cartId) {
145
+ await getClient().carts.associate(cartId, {
146
+ token: result.token,
147
+ spreeToken: cartToken,
148
+ }).catch(() => {}); // non-fatal
149
+ }
209
150
 
210
- const session = await createPaymentSetupSession({ payment_method_id: 'pm_123' });
211
- const current = await getPaymentSetupSession(sessionId);
212
- await completePaymentSetupSession(sessionId, { session_result: 'success' });
151
+ return { success: true, user: result.user };
152
+ }
213
153
  ```
214
154
 
215
- ### Credit Cards & Gift Cards
216
-
217
- ```typescript
218
- import { listCreditCards, deleteCreditCard } from '@spree/next';
219
- import { listGiftCards, getGiftCard } from '@spree/next';
155
+ ## Locale Resolution
220
156
 
221
- const cards = await listCreditCards();
222
- await deleteCreditCard(cardId);
223
-
224
- const giftCards = await listGiftCards();
225
- const giftCard = await getGiftCard(giftCardId);
226
- ```
227
-
228
- ## Using Server Actions in Forms
157
+ `getLocaleOptions()` reads country and locale from cookies (set by middleware or explicitly). All data-fetching server actions should use this:
229
158
 
230
159
  ```typescript
231
- import { addItem, getCart } from '@spree/next';
160
+ 'use server';
232
161
 
233
- export default async function CartPage() {
234
- const cart = await getCart();
162
+ import { getClient, getLocaleOptions } from '@spree/next';
235
163
 
236
- async function handleAddItem(formData: FormData) {
237
- 'use server';
238
- await addItem(formData.get('variantId') as string, 1);
239
- }
164
+ export async function getProducts(params?: { limit?: number }) {
165
+ const options = await getLocaleOptions();
166
+ return getClient().products.list(params, options);
167
+ }
240
168
 
241
- return <form action={handleAddItem}>...</form>;
169
+ export async function getCategory(idOrPermalink: string) {
170
+ const options = await getLocaleOptions();
171
+ return getClient().categories.get(idOrPermalink, undefined, options);
242
172
  }
243
173
  ```
244
174
 
245
- ## Localization
246
-
247
- ### Automatic (recommended)
175
+ ## Middleware
248
176
 
249
- Data functions automatically read locale and country from cookies. Use the included middleware to set cookies based on the URL:
177
+ Handles URL-based country/locale routing. Detects country from cookies, geo headers (Vercel/Cloudflare), or defaults, then redirects to `/{country}/{locale}/...`:
250
178
 
251
179
  ```typescript
252
180
  // middleware.ts
@@ -262,31 +190,6 @@ export const config = {
262
190
  };
263
191
  ```
264
192
 
265
- Then data functions work without any locale arguments:
266
-
267
- ```typescript
268
- // Locale/country auto-read from cookies — no options needed
269
- const products = await listProducts({ limit: 10 });
270
- const category = await getCategory('clothing/shirts');
271
- ```
272
-
273
- Use the `setLocale` server action in country/language switchers:
274
-
275
- ```typescript
276
- import { setLocale } from '@spree/next';
277
-
278
- await setLocale({ country: 'de', locale: 'de' });
279
- ```
280
-
281
- ### Manual override
282
-
283
- You can still pass locale options explicitly — they override the auto-detected values:
284
-
285
- ```typescript
286
- const products = await listProducts({ limit: 10 }, { locale: 'fr', country: 'FR' });
287
- const category = await getCategory('clothing/shirts', {}, { locale: 'de', country: 'DE' });
288
- ```
289
-
290
193
  ## Webhooks
291
194
 
292
195
  Handle Spree webhook events in your Next.js app with `@spree/next/webhooks`:
@@ -331,31 +234,14 @@ For more details, see [Webhooks](../../core-concepts/webhooks.md).
331
234
 
332
235
  ## TypeScript
333
236
 
334
- All types are re-exported from `@spree/sdk`:
237
+ Import types directly from `@spree/sdk`:
335
238
 
336
239
  ```typescript
337
240
  import type {
338
- Product,
339
- Order,
340
- LineItem,
341
- Variant,
342
- Category,
343
- Country,
344
- Currency,
345
- Locale,
346
- Address,
347
- Customer,
348
- CreditCard,
349
- GiftCard,
350
- Fulfillment,
351
- DeliveryRate,
352
- Payment,
353
- PaymentMethod,
354
- PaymentSession,
355
- PaymentSetupSession,
356
- PaginatedResponse,
357
- ProductFiltersResponse,
358
- AddressParams,
359
- SpreeError,
360
- } from '@spree/next';
241
+ Product, Order, Cart, LineItem, Variant,
242
+ Category, Country, Currency, Address, Customer,
243
+ CreditCard, GiftCard, Fulfillment, DeliveryRate,
244
+ Payment, PaymentSession, PaginatedResponse,
245
+ AddressParams, SpreeError,
246
+ } from '@spree/sdk';
361
247
  ```