@spree/docs 0.1.248 → 0.1.249
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/api-reference/admin-api/authentication.md +34 -14
- package/dist/api-reference/admin-api/endpoints.md +366 -14
- package/dist/api-reference/admin-api/errors.md +2 -2
- package/dist/api-reference/admin-api/introduction.md +3 -3
- package/dist/api-reference/admin-api/querying.md +6 -6
- package/dist/api-reference/store-api/monetary-amounts.md +5 -5
- package/dist/api-reference/webhooks-events.md +330 -335
- package/dist/developer/agentic/agent-skills.md +5 -2
- package/dist/developer/agentic/llm-docs.md +2 -1
- package/dist/developer/cli/admin-api.md +1 -1
- package/dist/developer/cli/quickstart.md +2 -2
- package/dist/developer/contributing/creating-an-extension.md +292 -146
- package/dist/developer/contributing/developing-spree.md +13 -17
- package/dist/developer/core-concepts/catalogs.md +2 -2
- package/dist/developer/core-concepts/channels.md +3 -3
- package/dist/developer/core-concepts/companies.md +2 -1
- package/dist/developer/core-concepts/delivery-setup.md +2 -2
- package/dist/developer/core-concepts/discounts.md +3 -3
- package/dist/developer/core-concepts/events.md +6 -5
- package/dist/developer/core-concepts/freight.md +3 -2
- package/dist/developer/core-concepts/fulfillments.md +10 -8
- package/dist/developer/core-concepts/imports-exports.md +11 -8
- package/dist/developer/core-concepts/inventory.md +2 -2
- package/dist/developer/core-concepts/media.md +14 -14
- package/dist/developer/core-concepts/orders.md +2 -2
- package/dist/developer/core-concepts/payments.md +1 -2
- package/dist/developer/core-concepts/products.md +6 -6
- package/dist/developer/core-concepts/reporting.md +4 -3
- package/dist/developer/core-concepts/returns-exchanges-claims.md +7 -7
- package/dist/developer/core-concepts/search-filtering.md +3 -3
- package/dist/developer/core-concepts/sellers.md +3 -3
- package/dist/developer/core-concepts/staff-roles.md +4 -2
- package/dist/developer/core-concepts/store-credits-gift-cards.md +1 -1
- package/dist/developer/core-concepts/stores.md +2 -2
- package/dist/developer/core-concepts/translations.md +12 -8
- package/dist/developer/core-concepts/webhooks.md +19 -18
- package/dist/developer/create-spree-app/quickstart.md +2 -7
- package/dist/developer/customization/api.md +1 -1
- package/dist/developer/customization/checkout.md +2 -2
- package/dist/developer/customization/dependencies.md +53 -37
- package/dist/developer/customization/permissions.md +2 -2
- package/dist/developer/dashboard/concepts.md +1 -1
- package/dist/developer/dashboard/customization/navigation.md +3 -2
- package/dist/developer/dashboard/customization/permissions.md +6 -6
- package/dist/developer/dashboard/plugins/publishing.md +4 -4
- package/dist/developer/dashboard/plugins/scaffolding.md +1 -1
- package/dist/developer/dashboard/public-api.md +1 -1
- package/dist/developer/dashboard/recipes/attribute-end-to-end.md +3 -18
- package/dist/developer/deployment/aws.md +1 -1
- package/dist/developer/deployment/aws_ecs.md +3 -3
- package/dist/developer/deployment/background_jobs.md +9 -3
- package/dist/developer/deployment/docker.md +1 -2
- package/dist/developer/deployment/emails.md +3 -1
- package/dist/developer/deployment/environment_variables.md +2 -2
- package/dist/developer/deployment/render.md +2 -2
- package/dist/developer/how-to/build-a-marketplace.md +2 -2
- package/dist/developer/how-to/custom-api-authentication.md +1 -1
- package/dist/developer/how-to/custom-delivery-rate-provider.md +11 -3
- package/dist/developer/how-to/custom-document-numbers.md +1 -1
- package/dist/developer/how-to/custom-order-routing.md +15 -14
- package/dist/developer/how-to/custom-payment-method.md +17 -19
- package/dist/developer/how-to/custom-promotion.md +4 -4
- package/dist/developer/how-to/custom-search-provider.md +12 -5
- package/dist/developer/how-to/custom-stock-splitter.md +25 -24
- package/dist/developer/how-to/sell-digital-products.md +1 -1
- package/dist/developer/multi-tenant/quickstart.md +2 -2
- package/dist/developer/providers/payouts.md +6 -2
- package/dist/developer/sdk/admin/querying-and-errors.md +1 -1
- package/dist/developer/sdk/admin/quickstart.md +4 -4
- package/dist/developer/sdk/authentication.md +5 -2
- package/dist/developer/sdk/store/cart-checkout.md +4 -4
- package/dist/developer/storefront/nextjs/emails.md +4 -2
- package/dist/developer/storefront/nextjs/testing.md +1 -1
- package/dist/developer/upgrades/5.6-to-6.0.md +51 -20
- package/dist/integrations/search/meilisearch.md +4 -4
- package/package.json +1 -1
|
@@ -98,10 +98,10 @@ await adminClient.sellers.suspend('sel_xxx', { reason: 'Unresolved delivery comp
|
|
|
98
98
|
curl -X POST 'https://api.mystore.com/api/v3/admin/sellers/sel_xxx/invite' \
|
|
99
99
|
-H 'X-Spree-API-Key: sk_xxx'
|
|
100
100
|
|
|
101
|
-
curl -X
|
|
101
|
+
curl -X PATCH 'https://api.mystore.com/api/v3/admin/sellers/sel_xxx/approve' \
|
|
102
102
|
-H 'X-Spree-API-Key: sk_xxx'
|
|
103
103
|
|
|
104
|
-
curl -X
|
|
104
|
+
curl -X PATCH 'https://api.mystore.com/api/v3/admin/sellers/sel_xxx/suspend' \
|
|
105
105
|
-H 'X-Spree-API-Key: sk_xxx' \
|
|
106
106
|
-H 'Content-Type: application/json' \
|
|
107
107
|
-d '{ "reason": "Unresolved delivery complaints" }'
|
|
@@ -204,7 +204,7 @@ curl -X POST 'https://marketplace.example.com/api/v3/seller/products' \
|
|
|
204
204
|
-H 'Content-Type: application/json' \
|
|
205
205
|
-d '{ "name": "Handmade Vase" }'
|
|
206
206
|
|
|
207
|
-
curl -X
|
|
207
|
+
curl -X PATCH 'https://marketplace.example.com/api/v3/seller/products/prod_xxx/submit' \
|
|
208
208
|
-H 'Authorization: Bearer $SELLER_JWT' \
|
|
209
209
|
-H 'X-Spree-Seller-Id: sel_xxx'
|
|
210
210
|
```
|
|
@@ -211,9 +211,11 @@ The invitation system publishes [events](events.md) you can subscribe to:
|
|
|
211
211
|
|
|
212
212
|
## Permissions
|
|
213
213
|
|
|
214
|
-
|
|
214
|
+
A role holds flat permission keys from one catalog (`read_orders`, `write_products`, …) — the same vocabulary as secret API key scopes. Every Admin API request checks the staff member's keys on the current store against the endpoint's `read_<resource>` / `write_<resource>` key and returns `403` with `details.required_permission` when it is missing. Staff endpoints (`/admin_users`, `/invitations`, `/roles`) need `read_staff` / `write_staff`.
|
|
215
215
|
|
|
216
|
-
|
|
216
|
+
Permissions apply to staff only. Customers never hold roles: the Store API authorizes them by ownership-scoped queries plus `Spree::Storefront::AccessPolicy`.
|
|
217
|
+
|
|
218
|
+
See the [Customize Permissions guide](../customization/permissions.md) for details on creating custom roles and registering your own permission keys.
|
|
217
219
|
|
|
218
220
|
## Related Documentation
|
|
219
221
|
|
|
@@ -18,7 +18,7 @@ Spree provides two stored value mechanisms that customers can use at checkout:
|
|
|
18
18
|
| Has code | No | Yes |
|
|
19
19
|
| Created by | Admin | Admin |
|
|
20
20
|
| Assignment | Directly to customer | Applied to order via code |
|
|
21
|
-
| Expiration |
|
|
21
|
+
| Expiration | Never expire | Optional, per card |
|
|
22
22
|
|
|
23
23
|
## Store Credits
|
|
24
24
|
|
|
@@ -5,7 +5,7 @@ description: Understand Spree Stores — the top-level tenant boundary that scop
|
|
|
5
5
|
|
|
6
6
|
## Overview
|
|
7
7
|
|
|
8
|
-
The Store is the top-level tenant in Spree. Every resource — products, orders, channels, markets,
|
|
8
|
+
The Store is the top-level tenant in Spree. Every resource — products, orders, channels, markets, categories — 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
9
|
|
|
10
10
|
```mermaid
|
|
11
11
|
erDiagram
|
|
@@ -85,7 +85,7 @@ The store sets the fallback for **storefront access gating** — whether anonymo
|
|
|
85
85
|
|
|
86
86
|
## Store Resources
|
|
87
87
|
|
|
88
|
-
Each store owns its own resources. Products, orders, channels, markets, and
|
|
88
|
+
Each store owns its own resources. Products, orders, channels, markets, and categories in one store are independent from another.
|
|
89
89
|
|
|
90
90
|
| Resource | Relationship |
|
|
91
91
|
|----------|-------------|
|
|
@@ -85,26 +85,30 @@ If a field has no translation for the requested locale, you get the default-lang
|
|
|
85
85
|
|
|
86
86
|
Merchants translate in the dashboard, switching locale on the record they're editing. For bulk work — handing a catalogue to a translation agency — export to CSV, translate, and import it back.
|
|
87
87
|
|
|
88
|
-
Translations can also be written through the Admin API, which is what you'd use to sync from an external translation service:
|
|
88
|
+
Translations can also be written through the Admin API, which is what you'd use to sync from an external translation service. Writes go through one batch endpoint — every entry succeeds or none do — so a product and its option values can be translated in a single request:
|
|
89
89
|
|
|
90
90
|
|
|
91
91
|
```typescript Admin SDK
|
|
92
|
-
await adminClient.
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
92
|
+
await adminClient.translations.batch([
|
|
93
|
+
{
|
|
94
|
+
resource_type: 'product',
|
|
95
|
+
resource_id: 'prod_xxx',
|
|
96
|
+
values: {
|
|
97
|
+
fr: { name: 'Sac Spree', description: 'Un sac fourre-tout élégant…' },
|
|
98
|
+
de: { name: 'Spree Tasche' },
|
|
99
|
+
},
|
|
96
100
|
},
|
|
97
|
-
|
|
101
|
+
])
|
|
98
102
|
|
|
99
103
|
// Which resources and fields accept translations
|
|
100
104
|
const { data: resources } = await adminClient.translatableResources.list()
|
|
101
105
|
```
|
|
102
106
|
|
|
103
107
|
```bash cURL
|
|
104
|
-
curl -X
|
|
108
|
+
curl -X POST 'https://api.mystore.com/api/v3/admin/translations/batch' \
|
|
105
109
|
-H 'X-Spree-API-Key: sk_xxx' \
|
|
106
110
|
-H 'Content-Type: application/json' \
|
|
107
|
-
-d '{ "translations": { "de": { "name": "Baumwolltasche" } } }'
|
|
111
|
+
-d '{ "translations": [ { "resource_type": "product", "resource_id": "prod_xxx", "values": { "de": { "name": "Baumwolltasche" } } } ] }'
|
|
108
112
|
```
|
|
109
113
|
|
|
110
114
|
|
|
@@ -12,7 +12,7 @@ Webhooks are built on top of Spree's [event system](events.md), providing:
|
|
|
12
12
|
- **Multi-store support** - Each store has its own webhook endpoints
|
|
13
13
|
- **Event filtering** - Subscribe to specific events or patterns with wildcards
|
|
14
14
|
- **Secure delivery** - HMAC-SHA256 signatures for payload verification
|
|
15
|
-
- **
|
|
15
|
+
- **Failure handling** - Failed deliveries are recorded and can be re-sent, and an endpoint that keeps failing is disabled
|
|
16
16
|
- **Full audit trail** - Track every delivery attempt with response codes and timing
|
|
17
17
|
|
|
18
18
|
## How Webhooks Work
|
|
@@ -34,10 +34,9 @@ flowchart LR
|
|
|
34
34
|
|
|
35
35
|
subgraph External Service
|
|
36
36
|
H --> I{Response}
|
|
37
|
-
I -->|2xx| J[
|
|
38
|
-
I -->|Error| K[
|
|
39
|
-
K -->|
|
|
40
|
-
K -->|Retry| H
|
|
37
|
+
I -->|2xx| J[Mark Successful]
|
|
38
|
+
I -->|Error| K[Mark Failed]
|
|
39
|
+
K -->|15 failures in a row| L[Disable Endpoint]
|
|
41
40
|
end
|
|
42
41
|
```
|
|
43
42
|
|
|
@@ -51,7 +50,7 @@ flowchart LR
|
|
|
51
50
|
|
|
52
51
|
### Via Admin Panel
|
|
53
52
|
|
|
54
|
-
Navigate to **Settings →
|
|
53
|
+
Navigate to **Settings → Developer → Webhooks** in the admin panel to create and manage webhook endpoints.
|
|
55
54
|
|
|
56
55
|
### Via the Admin API
|
|
57
56
|
|
|
@@ -139,7 +138,7 @@ The `subscriptions` array accepts exact event names and wildcard patterns:
|
|
|
139
138
|
| `['order.placed', 'order.canceled']` | Only those two events |
|
|
140
139
|
| `['order.*']` | All order events |
|
|
141
140
|
| `['*.created']` | All creation events |
|
|
142
|
-
| `['order.*', 'payment.*', '
|
|
141
|
+
| `['order.*', 'payment.*', 'fulfillment.fulfilled']` | Multiple patterns |
|
|
143
142
|
| `[]` or `['*']` | All events |
|
|
144
143
|
|
|
145
144
|
## Webhook Payload
|
|
@@ -154,7 +153,7 @@ Each webhook delivery sends a JSON payload with the following structure. The `da
|
|
|
154
153
|
"data": {
|
|
155
154
|
"id": "or_m3Rp9wXz",
|
|
156
155
|
"number": "R123456789",
|
|
157
|
-
"fulfillment_status": "
|
|
156
|
+
"fulfillment_status": "unfulfilled",
|
|
158
157
|
"payment_status": "paid",
|
|
159
158
|
"total": "99.99",
|
|
160
159
|
"display_total": "$99.99",
|
|
@@ -165,7 +164,7 @@ Each webhook delivery sends a JSON payload with the following structure. The `da
|
|
|
165
164
|
"payments": [ ... ]
|
|
166
165
|
},
|
|
167
166
|
"metadata": {
|
|
168
|
-
"spree_version": "
|
|
167
|
+
"spree_version": "6.0.0"
|
|
169
168
|
}
|
|
170
169
|
}
|
|
171
170
|
```
|
|
@@ -243,7 +242,7 @@ class WebhooksController < ApplicationController
|
|
|
243
242
|
|
|
244
243
|
case event['name']
|
|
245
244
|
when 'order.placed'
|
|
246
|
-
|
|
245
|
+
handle_order_placed(event['data'])
|
|
247
246
|
when 'product.updated'
|
|
248
247
|
handle_product_updated(event['data'])
|
|
249
248
|
end
|
|
@@ -266,11 +265,13 @@ class WebhooksController < ApplicationController
|
|
|
266
265
|
end
|
|
267
266
|
```
|
|
268
267
|
|
|
269
|
-
## Delivery Status
|
|
268
|
+
## Delivery Status
|
|
270
269
|
|
|
271
|
-
###
|
|
270
|
+
### Failed Deliveries
|
|
272
271
|
|
|
273
|
-
|
|
272
|
+
Spree sends each delivery once. Any response other than 2xx — including a timeout or a connection error — marks the delivery as failed, and Spree does not send it again on its own. To send a failed delivery again, re-send it from the delivery log (see below).
|
|
273
|
+
|
|
274
|
+
If an endpoint fails 15 deliveries in a row, Spree disables it and emails the store staff. Fix the endpoint, then turn it back on by marking it active again.
|
|
274
275
|
|
|
275
276
|
### Checking Delivery Status
|
|
276
277
|
|
|
@@ -321,7 +322,7 @@ SSL certificates are verified in production, and not in development — so you c
|
|
|
321
322
|
|
|
322
323
|
## Available Events
|
|
323
324
|
|
|
324
|
-
Webhooks can subscribe to any event in Spree's event system. See [Events](events.md
|
|
325
|
+
Webhooks can subscribe to any event in Spree's event system. See [Webhook Events & Payloads](../../api-reference/webhooks-events.md) for the complete list.
|
|
325
326
|
|
|
326
327
|
Common webhook events include:
|
|
327
328
|
|
|
@@ -330,12 +331,12 @@ Common webhook events include:
|
|
|
330
331
|
| `order.placed` | A customer completed checkout |
|
|
331
332
|
| `order.paid` | The order is fully paid |
|
|
332
333
|
| `order.canceled` | The order was canceled |
|
|
333
|
-
| `order.
|
|
334
|
-
| `fulfillment.
|
|
334
|
+
| `order.fulfilled` | Everything on the order has shipped |
|
|
335
|
+
| `fulfillment.fulfilled` | One parcel went out |
|
|
335
336
|
| `payment.completed` | A payment succeeded |
|
|
336
337
|
| `product.created` / `product.updated` | Catalog changes |
|
|
337
338
|
| `product.out_of_stock` / `product.back_in_stock` | Availability flipped |
|
|
338
|
-
| `
|
|
339
|
+
| `user.created` | A new customer registered |
|
|
339
340
|
| `return.received` | A return arrived |
|
|
340
341
|
|
|
341
342
|
## Testing Webhooks
|
|
@@ -386,7 +387,7 @@ curl -X POST 'https://api.mystore.com/api/v3/admin/webhook_endpoints' \
|
|
|
386
387
|
|
|
387
388
|
- **Verify signatures** — Always verify the `X-Spree-Webhook-Signature` header to ensure the webhook is authentic.
|
|
388
389
|
|
|
389
|
-
- **Handle duplicates** — Use the event `id` to detect and handle duplicate deliveries.
|
|
390
|
+
- **Handle duplicates** — Use the event `id` to detect and handle duplicate deliveries. A delivery that is re-sent carries the same event `id`.
|
|
390
391
|
|
|
391
392
|
- **Subscribe selectively** — Only subscribe to events you need. Use specific patterns rather than `*` when possible.
|
|
392
393
|
|
|
@@ -123,14 +123,9 @@ The project includes [@spree/cli](../cli/quickstart.md) for managing your Spree
|
|
|
123
123
|
|
|
124
124
|
### Admin Dashboard
|
|
125
125
|
|
|
126
|
-
|
|
126
|
+
The dashboard runs as its own dev server: `spree dev` starts it alongside the API at [http://localhost:5173](http://localhost:5173), live-reloading from `apps/dashboard/`. See the [dashboard docs](../dashboard/overview.md).
|
|
127
127
|
|
|
128
|
-
|
|
129
|
-
|---|---|
|
|
130
|
-
| **Email** | `spree@example.com` |
|
|
131
|
-
| **Password** | `spree123` |
|
|
132
|
-
|
|
133
|
-
The dashboard runs as its own dev server: `spree dev` starts it alongside the API at [http://localhost:5173](http://localhost:5173), with the same credentials, live-reloading from `apps/dashboard/`. See the [dashboard docs](../dashboard/overview.md).
|
|
128
|
+
No default admin account is created. On the first run, `spree dev` opens a one-time setup link where you create the admin account. If you missed it, print it again with `spree rails spree:setup:token`.
|
|
134
129
|
|
|
135
130
|
### Store API
|
|
136
131
|
|
|
@@ -141,7 +141,7 @@ The base `ResourceController` provides:
|
|
|
141
141
|
| Pagination | [Pagy](https://github.com/ddnexus/pagy) — `?page=2&limit=25` |
|
|
142
142
|
| Filtering | [Ransack](https://github.com/activerecord-hackery/ransack) — `?q[name_cont]=nike` |
|
|
143
143
|
| Sorting | JSON:API style — `?sort=-name` |
|
|
144
|
-
| Authorization |
|
|
144
|
+
| Authorization | Ownership scoping — `scope` decides what the caller can see (Admin API controllers add the `scoped_resource` permission gate) |
|
|
145
145
|
| Prefixed IDs | Stripe-style — `brand_k5nR8xLq` |
|
|
146
146
|
|
|
147
147
|
Override these methods to customize:
|
|
@@ -34,7 +34,7 @@ Spree::Checkout::Registry.add_requirement(
|
|
|
34
34
|
step: :address,
|
|
35
35
|
field: :vat_number,
|
|
36
36
|
message: 'VAT number is required for business orders',
|
|
37
|
-
satisfied: ->(cart) { cart.
|
|
37
|
+
satisfied: ->(cart) { cart.metadata['vat_number'].present? }
|
|
38
38
|
)
|
|
39
39
|
```
|
|
40
40
|
|
|
@@ -58,7 +58,7 @@ A step bundles its own requirements and says when it is satisfied:
|
|
|
58
58
|
Spree::Checkout::Registry.register_step(
|
|
59
59
|
name: :loyalty,
|
|
60
60
|
before: :payment,
|
|
61
|
-
satisfied: ->(cart) { cart.
|
|
61
|
+
satisfied: ->(cart) { cart.metadata['loyalty_number'].present? },
|
|
62
62
|
requirements: ->(cart) {
|
|
63
63
|
[{ step: 'loyalty', field: 'loyalty_number',
|
|
64
64
|
message: 'Loyalty number is required' }]
|
|
@@ -6,8 +6,8 @@ section: customization
|
|
|
6
6
|
## Overview
|
|
7
7
|
|
|
8
8
|
With Dependencies, you can replace parts of Spree core with your custom code:
|
|
9
|
-
[Services and Workflows](workflows.md),
|
|
10
|
-
|
|
9
|
+
[Services and Workflows](workflows.md), the staff ability class
|
|
10
|
+
and the storefront access policy (used for [Permissions](permissions.md)), and API Serializers (used for
|
|
11
11
|
generating JSON API responses).
|
|
12
12
|
|
|
13
13
|
> **TIP:** Replacing a whole class means keeping your copy in sync with every Spree
|
|
@@ -18,19 +18,19 @@ generating JSON API responses).
|
|
|
18
18
|
|
|
19
19
|
## Application (global) customization
|
|
20
20
|
|
|
21
|
-
This will change every aspect of the application (
|
|
21
|
+
This will change every aspect of the application (the Store API, the Admin API, and everything built on them).
|
|
22
22
|
|
|
23
23
|
In your `config/initializers/spree.rb` file, you can set the following:
|
|
24
24
|
|
|
25
25
|
```ruby
|
|
26
|
-
Spree.
|
|
26
|
+
Spree.carts_update_service = MyStore::CartUpdate
|
|
27
27
|
```
|
|
28
28
|
|
|
29
29
|
or using the block syntax:
|
|
30
30
|
|
|
31
31
|
```ruby
|
|
32
32
|
Spree.dependencies do |dependencies|
|
|
33
|
-
dependencies.
|
|
33
|
+
dependencies.carts_update_service = MyStore::CartUpdate
|
|
34
34
|
end
|
|
35
35
|
```
|
|
36
36
|
|
|
@@ -86,6 +86,18 @@ Spree.cart_add_item_workflow = MyStore::AddItem
|
|
|
86
86
|
> reasons people replace this class, and they don't need maintaining across
|
|
87
87
|
> upgrades.
|
|
88
88
|
|
|
89
|
+
> **WARNING:** A subclass runs its hooks under its **own** key, derived from its class name —
|
|
90
|
+
> `MyStore::AddItem` fires `my_store.add_item.*`, not `carts.add_item.*`. Hooks
|
|
91
|
+
> registered on the original key (by you or by an installed extension) stop
|
|
92
|
+
> firing once you swap the class in. To keep them, declare the original key in
|
|
93
|
+
> the subclass:
|
|
94
|
+
>
|
|
95
|
+
> ```ruby
|
|
96
|
+
class AddItem < Spree::Carts::AddItem
|
|
97
|
+
workflow_key 'carts.add_item'
|
|
98
|
+
end
|
|
99
|
+
```
|
|
100
|
+
|
|
89
101
|
## Using dependencies in your code
|
|
90
102
|
|
|
91
103
|
When you need to use a dependency in your code, you can access it directly via the `Spree` module:
|
|
@@ -95,60 +107,53 @@ When you need to use a dependency in your code, you can access it directly via t
|
|
|
95
107
|
Spree.cart_add_item_workflow.call(cart: cart, variant: variant, quantity: 1)
|
|
96
108
|
|
|
97
109
|
# For API dependencies, use the Spree.api accessor
|
|
98
|
-
Spree.api.
|
|
110
|
+
Spree.api.cart_serializer.new(cart).serializable_hash
|
|
99
111
|
```
|
|
100
112
|
|
|
101
113
|
## Controller level customization
|
|
102
114
|
|
|
103
|
-
If you need to replace
|
|
115
|
+
If you need to replace a serializer in a specific API endpoint only, you can create a [code decorator](decorators.md):
|
|
104
116
|
|
|
105
117
|
```bash
|
|
106
|
-
mkdir -p app/controllers/spree && touch app/controllers/spree/
|
|
118
|
+
mkdir -p app/controllers/spree/api/v3/store && touch app/controllers/spree/api/v3/store/carts_controller_decorator.rb
|
|
107
119
|
```
|
|
108
120
|
|
|
109
121
|
and add the following code to it:
|
|
110
122
|
|
|
111
123
|
```ruby
|
|
112
124
|
module Spree
|
|
113
|
-
module
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
125
|
+
module Api
|
|
126
|
+
module V3
|
|
127
|
+
module Store
|
|
128
|
+
module CartsControllerDecorator
|
|
129
|
+
def serializer_class
|
|
130
|
+
MyNewAwesomeCartSerializer
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
CartsController.prepend(CartsControllerDecorator)
|
|
135
|
+
end
|
|
120
136
|
end
|
|
121
137
|
end
|
|
122
|
-
|
|
123
|
-
CartController.prepend(CartControllerDecorator)
|
|
124
138
|
end
|
|
125
139
|
```
|
|
126
140
|
|
|
127
|
-
This will change the serializer in this API endpoint to `MyNewAwesomeCartSerializer
|
|
141
|
+
This will change the serializer in this API endpoint to `MyNewAwesomeCartSerializer`. Services and workflows are resolved through the global dependencies (e.g. `Spree.cart_add_item_workflow`), so swap those at the application level.
|
|
128
142
|
|
|
129
|
-
Different API endpoints can have different dependency injection points. You can review their [source code](https://github.com/spree/spree/tree/main/api/app/controllers/spree/api/v3) to see what you can replace.
|
|
143
|
+
Different API endpoints can have different dependency injection points. You can review their [source code](https://github.com/spree/spree/tree/main/spree/api/app/controllers/spree/api/v3) to see what you can replace.
|
|
130
144
|
|
|
131
145
|
## API level customization
|
|
132
146
|
|
|
133
|
-
|
|
147
|
+
API serializers have their own injection points under `Spree.api` — Store API serializers (`cart_serializer`, `product_serializer`, …) and Admin API serializers (`admin_order_serializer`, …) — so you can customize one surface without touching the other.
|
|
134
148
|
|
|
135
149
|
In your Spree initializer (`config/initializers/spree.rb`) please add:
|
|
136
150
|
|
|
137
151
|
```ruby
|
|
138
|
-
Spree.api.
|
|
139
|
-
Spree.api.
|
|
152
|
+
Spree.api.cart_serializer = MyNewAwesomeCartSerializer
|
|
153
|
+
Spree.api.admin_order_serializer = MyNewAwesomeAdminOrderSerializer
|
|
140
154
|
```
|
|
141
155
|
|
|
142
|
-
This will swap the default
|
|
143
|
-
|
|
144
|
-
You can mix and match both global and API-level customizations:
|
|
145
|
-
|
|
146
|
-
```ruby
|
|
147
|
-
Spree.cart_add_item_workflow = MyNewAwesomeAddItemToCart
|
|
148
|
-
Spree.api.storefront_cart_add_item_service = AnotherAddItemToCart
|
|
149
|
-
```
|
|
150
|
-
|
|
151
|
-
The second line will have precedence over the first one, and the Storefront API will use `AnotherAddItemToCart` and the rest of the application will use `MyNewAwesomeAddItemToCart`.
|
|
156
|
+
This will swap the default serializers for your custom ones within all API endpoints that use them.
|
|
152
157
|
|
|
153
158
|
## Debugging dependencies
|
|
154
159
|
|
|
@@ -170,8 +175,8 @@ cart_recalculate_workflow Spree::Carts::Recalculate [OVERRIDDEN]
|
|
|
170
175
|
...
|
|
171
176
|
|
|
172
177
|
[API]
|
|
173
|
-
|
|
174
|
-
|
|
178
|
+
cart_serializer Spree::Api::V3::CartSerializer
|
|
179
|
+
product_serializer MyApp::ProductSerializer [OVERRIDDEN]
|
|
175
180
|
...
|
|
176
181
|
```
|
|
177
182
|
|
|
@@ -194,7 +199,7 @@ This shows only the dependencies that have been customized, along with their ori
|
|
|
194
199
|
cart_recalculate_workflow Spree::Carts::Recalculate -> MyApp::CartRecalculate (config/initializers/spree.rb:15)
|
|
195
200
|
|
|
196
201
|
[API OVERRIDES]
|
|
197
|
-
|
|
202
|
+
product_serializer Spree::Api::V3::ProductSerializer -> MyApp::ProductSerializer (config/initializers/spree.rb:20)
|
|
198
203
|
```
|
|
199
204
|
|
|
200
205
|
### Validate all dependencies
|
|
@@ -243,11 +248,16 @@ Seams backed by a [workflow](workflows.md) use a
|
|
|
243
248
|
| `cart_add_item_service` | `cart_add_item_workflow` |
|
|
244
249
|
| `cart_recalculate_service` | `cart_recalculate_workflow` |
|
|
245
250
|
| `carts_complete_service` | `carts_complete_workflow` |
|
|
251
|
+
| `carts_upsert_items_service` | `cart_upsert_items_workflow` |
|
|
246
252
|
| `cart_merge_strategy` | `cart_merge_workflow` |
|
|
247
253
|
| `order_cancel_service` | `order_cancel_workflow` |
|
|
248
254
|
| `order_complete_service` | `order_complete_workflow` |
|
|
249
255
|
| `fulfillment_create_service` | `fulfillment_create_workflow` |
|
|
250
256
|
| `payments_handle_webhook_service` | `payments_handle_webhook_workflow` |
|
|
257
|
+
| `shipment_update_service`, `fulfillment_update_service` | `fulfillment_update_workflow` |
|
|
258
|
+
| `gift_card_apply_service` | `gift_card_apply_workflow` |
|
|
259
|
+
| `gift_card_remove_service` | `gift_card_remove_workflow` |
|
|
260
|
+
| `gift_card_redeem_service` | `gift_card_redeem_workflow` |
|
|
251
261
|
|
|
252
262
|
> **WARNING:** The legacy names stay readable, but **assigning to one no longer has any
|
|
253
263
|
> effect** — the override is recorded and a deprecation warning names the seam to
|
|
@@ -258,6 +268,12 @@ Seams backed by a [workflow](workflows.md) use a
|
|
|
258
268
|
> If you override any of these, move to the `*_workflow` name and make sure your
|
|
259
269
|
> class subclasses the workflow.
|
|
260
270
|
|
|
271
|
+
Two seams are plain renames with an unchanged contract, so an override set
|
|
272
|
+
under the old name is still applied (with a deprecation warning):
|
|
273
|
+
`checkout_add_store_credit_service` → `store_credit_apply_service` and
|
|
274
|
+
`checkout_remove_store_credit_service` → `store_credit_remove_service`. All
|
|
275
|
+
legacy names are removed in Spree 6.1.
|
|
276
|
+
|
|
261
277
|
## Backwards compatibility
|
|
262
278
|
|
|
263
279
|
The legacy string-based syntax is still supported for backwards compatibility:
|
|
@@ -280,5 +296,5 @@ Default values can be easily checked by:
|
|
|
280
296
|
|
|
281
297
|
1. Using the rake task: `bin/rake spree:dependencies:list`
|
|
282
298
|
2. Looking at the source code:
|
|
283
|
-
* [Application (global) dependencies](https://github.com/spree/spree/blob/main/core/lib/spree/core/dependencies.rb)
|
|
284
|
-
* [API level dependencies](https://github.com/spree/spree/blob/main/api/lib/spree/api/dependencies.rb)
|
|
299
|
+
* [Application (global) dependencies](https://github.com/spree/spree/blob/main/spree/core/lib/spree/core/dependencies.rb)
|
|
300
|
+
* [API level dependencies](https://github.com/spree/spree/blob/main/spree/api/lib/spree/api/dependencies.rb)
|
|
@@ -11,7 +11,7 @@ Every grantable capability is a flat key of the form `read_<resource>` or `write
|
|
|
11
11
|
|
|
12
12
|
Keys are per resource, not per action. The money-adjacent areas (`payments`, `refunds`, `gift_cards`, `store_credits`) are separate resources from `orders`, so "view orders but don't refund" needs no special casing.
|
|
13
13
|
|
|
14
|
-
Roles are data, so create them where you manage the rest of your store's data: **Settings →
|
|
14
|
+
Roles are data, so create them where you manage the rest of your store's data: **Settings → Roles** in the dashboard, or the Admin API when you want it scripted.
|
|
15
15
|
|
|
16
16
|
```bash
|
|
17
17
|
spree api post roles --data '{
|
|
@@ -68,6 +68,6 @@ The catalog is deliberately flat: it answers "may this role touch this kind of r
|
|
|
68
68
|
|
|
69
69
|
## How enforcement works
|
|
70
70
|
|
|
71
|
-
Every Admin API controller declares its resource (`scoped_resource :orders`). Each request checks the principal's keys — a staff member's role permissions on the current store, or a secret key's scopes — against `read_<resource>` or `write_<resource>` for the action. A missing key produces a 403 naming it
|
|
71
|
+
Every Admin API controller declares its resource (`scoped_resource :orders`). Each request checks the principal's keys — a staff member's role permissions on the current store, or a secret key's scopes — against `read_<resource>` or `write_<resource>` for the action. A missing key produces a 403 naming it — `details.required_permission` for staff, `details.required_scope` for a secret key. Behind that gate, keys compile to CanCanCan rules for record-level concerns, and `GET /api/v3/admin/me` returns both the rule dump the dashboard mirrors and `permission_keys`, the flat key list.
|
|
72
72
|
|
|
73
73
|
Storefront customers are not part of this system — customer authorization is ownership, enforced by the Store API's scoped lookups, with nothing to configure. The one swappable piece is `Spree::Storefront::AccessPolicy` (via `Spree::Dependencies.storefront_access_policy_class`): a generic `readable?` / `writable?` / `scope` protocol that defaults to "the caller owns the record", with carts and orders adding guest-token access. Replace it only when access must widen beyond the owner, such as company accounts sharing purchases or wishlists.
|
|
@@ -55,7 +55,7 @@ The facade `defineDashboardPlugin({ nav, routes, slots, … })` groups all five
|
|
|
55
55
|
The signed-in admin's identity and abilities are exposed via three providers wrapping the app:
|
|
56
56
|
|
|
57
57
|
- `<AuthProvider>` — the current admin user; `useAuth()` returns `{ user, signIn, signOut, … }`
|
|
58
|
-
- `<PermissionProvider>` —
|
|
58
|
+
- `<PermissionProvider>` — the admin's permissions on the current store; `usePermissions()` returns `{ permissions, rules, permissionKeys, isLoading, refresh }`, and the checks live on `permissions.can(...)`
|
|
59
59
|
- `<StoreProvider>` — current store + timezone + currency; `useStore()` returns `{ storeId, store, … }`
|
|
60
60
|
|
|
61
61
|
When you write a custom page or hook, pull from these. **Never reach into local state for identity** — it changes on store switch or session refresh, and the providers wire all that for you.
|
|
@@ -22,7 +22,7 @@ nav.add({
|
|
|
22
22
|
path: '/analytics', // prefixed with /$storeId at render time
|
|
23
23
|
icon: BarChartIcon,
|
|
24
24
|
position: 650,
|
|
25
|
-
subject: 'Spree::Order', // optional
|
|
25
|
+
subject: 'Spree::Order', // optional permission subject — hides item without read permission
|
|
26
26
|
})
|
|
27
27
|
```
|
|
28
28
|
|
|
@@ -49,6 +49,7 @@ defineDashboardPlugin({
|
|
|
49
49
|
nav.add({
|
|
50
50
|
key: 'analytics',
|
|
51
51
|
label: 'Analytics',
|
|
52
|
+
path: '/analytics', // required, even on a parent
|
|
52
53
|
icon: BarChartIcon,
|
|
53
54
|
position: 650,
|
|
54
55
|
children: [
|
|
@@ -196,7 +197,7 @@ settingsNav.add({
|
|
|
196
197
|
|
|
197
198
|
## Permission gating
|
|
198
199
|
|
|
199
|
-
`subject` on a nav entry checks `permissions.can('read', subject)` — when it fails, the sidebar item is hidden. **This is UX, not authorization.** The backend still enforces
|
|
200
|
+
`subject` on a nav entry checks `permissions.can('read', subject)` — when it fails, the sidebar item is hidden. **This is UX, not authorization.** The backend still enforces permissions on every API call. Hiding the link is a hint, not a security boundary.
|
|
200
201
|
|
|
201
202
|
## Order of operations
|
|
202
203
|
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Permissions
|
|
3
3
|
sidebarTitle: Permissions
|
|
4
|
-
description: How to hide UI behind
|
|
4
|
+
description: How to hide UI behind the current admin's permissions in your customisations — and why hiding is never the same as authorising.
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
> **NOTE:** Examples here import from `@spree/dashboard`, which re-exports the framework and the design system for host applications. A **distributed plugin** imports `@spree/dashboard-core` and `@spree/dashboard-ui` directly instead — it extends the shell rather than shipping it. See [plugin overview](../plugins/overview.md).
|
|
8
8
|
|
|
9
|
-
The dashboard exposes the current admin's
|
|
9
|
+
The dashboard exposes the current admin's permissions to every component via the `usePermissions()` hook: the catalog keys their role holds on the current store (`read_orders`, `write_products`, …) and the class-level ability rules the server compiles from those keys. The registry surfaces — nav entries, settings entries, custom routes — also accept a `subject` shortcut, and nav entries take a generic `if` predicate that reads from `permissions`.
|
|
10
10
|
|
|
11
11
|
## Two layers
|
|
12
12
|
|
|
@@ -15,9 +15,9 @@ UI gating is for **UX**. Backend authorization is for **security**. They are not
|
|
|
15
15
|
| Layer | Where | What it does |
|
|
16
16
|
|---|---|---|
|
|
17
17
|
| UI gating | Dashboard registries (`subject`, `if`) | Hides menu items, columns, buttons — keeps the interface tidy |
|
|
18
|
-
| Authorization | Rails API controllers (
|
|
18
|
+
| Authorization | Rails API controllers (per-controller `read_*`/`write_*` key gate, record-level checks) | Refuses the request when the user doesn't have permission |
|
|
19
19
|
|
|
20
|
-
**Always rely on the backend for security.** Hiding a button is a hint to the user, not a wall. A user with browser dev tools (or the API token) can always hit the endpoint directly —
|
|
20
|
+
**Always rely on the backend for security.** Hiding a button is a hint to the user, not a wall. A user with browser dev tools (or the API token) can always hit the endpoint directly — the API's permission check is what stops them.
|
|
21
21
|
|
|
22
22
|
## The `permissions` object
|
|
23
23
|
|
|
@@ -87,7 +87,7 @@ Use it for combined checks (permission + store state), feature flags, multi-acti
|
|
|
87
87
|
|
|
88
88
|
## Inside components
|
|
89
89
|
|
|
90
|
-
Use the `usePermissions()` hook — it returns `{ permissions, rules, isLoading }
|
|
90
|
+
Use the `usePermissions()` hook — it returns `{ permissions, rules, permissionKeys, isLoading, refresh }`. `permissionKeys` is the flat key list (the same vocabulary as the role editor and API-key scopes), for when `permissionKeys.includes('read_orders')` reads better than a subject check:
|
|
91
91
|
|
|
92
92
|
```tsx
|
|
93
93
|
import { usePermissions } from '@spree/dashboard'
|
|
@@ -110,4 +110,4 @@ If your customization introduces a new model on the backend, register it as a pe
|
|
|
110
110
|
## Reference
|
|
111
111
|
|
|
112
112
|
- [`Permissions` interface](https://github.com/spree/spree/blob/main/packages/dashboard-core/src/providers/permission-provider.tsx)
|
|
113
|
-
- [
|
|
113
|
+
- [Customize Permissions](../../customization/permissions.md) — the backend permission catalog and `register_scope`
|
|
@@ -15,9 +15,9 @@ Your plugin imports React, the dashboard, Tailwind, i18next, etc., but **you don
|
|
|
15
15
|
"peerDependencies": {
|
|
16
16
|
"react": "^19",
|
|
17
17
|
"react-dom": "^19",
|
|
18
|
-
"@spree/dashboard-core": "^0.
|
|
19
|
-
"@spree/dashboard-ui": "^0.
|
|
20
|
-
"@spree/admin-sdk": "^0.
|
|
18
|
+
"@spree/dashboard-core": "^1.0.0-beta.3",
|
|
19
|
+
"@spree/dashboard-ui": "^1.0.0-beta.3",
|
|
20
|
+
"@spree/admin-sdk": "^1.0.0-beta.2",
|
|
21
21
|
"i18next": "^26",
|
|
22
22
|
"react-i18next": "^17",
|
|
23
23
|
"@tanstack/react-query": "^5",
|
|
@@ -27,7 +27,7 @@ Your plugin imports React, the dashboard, Tailwind, i18next, etc., but **you don
|
|
|
27
27
|
}
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
-
Use ranges, not exact versions — pin too tightly and consumers can't upgrade the dashboard without you cutting a release. One
|
|
30
|
+
Use ranges, not exact versions — pin too tightly and consumers can't upgrade the dashboard without you cutting a release. One pre-release caveat: a range like `^1.0.0-beta.3` accepts later `1.0.0` betas and every stable `1.x` release, but not betas of other versions — so keep your `@spree/dashboard-*` peers in step with the dashboard until 1.0 ships. Check the ranges `spree plugin new` scaffolds against the versions your host app installs.
|
|
31
31
|
|
|
32
32
|
`dependencies` is for things your plugin *uses* that the host *doesn't* — small utilities (e.g., `clsx`, `date-fns` if you need a specific version, your own helper packages). When in doubt, peer-dep it. The trade-off is "user has to install one more thing" vs. "user has two copies of React" — always pay the first cost.
|
|
33
33
|
|
|
@@ -84,7 +84,7 @@ import {
|
|
|
84
84
|
```ts
|
|
85
85
|
import {
|
|
86
86
|
useAuth, // current admin user
|
|
87
|
-
usePermissions, //
|
|
87
|
+
usePermissions, // current admin's permissions
|
|
88
88
|
useStore, // current store
|
|
89
89
|
useCommandPalette, // ⌘K palette open/close state
|
|
90
90
|
useGlobalSearch, // global search query state
|