@spree/docs 0.1.188 → 0.1.190
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.
|
@@ -759,22 +759,29 @@ Events: `newsletter_subscriber.created`, `newsletter_subscriber.updated`, `newsl
|
|
|
759
759
|
}
|
|
760
760
|
```
|
|
761
761
|
|
|
762
|
-
## Digital
|
|
762
|
+
## Digital Asset Events
|
|
763
763
|
|
|
764
|
-
Events: `
|
|
764
|
+
Events: `digital_asset.created`, `digital_asset.updated`, `digital_asset.deleted`
|
|
765
|
+
|
|
766
|
+
The legacy `digital.created` / `digital.updated` / `digital.deleted` names are
|
|
767
|
+
still emitted alongside these for one release and are removed in Spree 6.1 —
|
|
768
|
+
subscribe to the `digital_asset.*` names.
|
|
765
769
|
|
|
766
770
|
```json
|
|
767
771
|
{
|
|
768
772
|
"id": "dig_3xLq8nRt",
|
|
769
773
|
"variant_id": "var_k5nR8xLq",
|
|
770
|
-
"
|
|
771
|
-
"
|
|
774
|
+
"filename": "ebook.pdf",
|
|
775
|
+
"content_type": "application/pdf"
|
|
772
776
|
}
|
|
773
777
|
```
|
|
774
778
|
|
|
775
779
|
## Digital Link Events
|
|
776
780
|
|
|
777
|
-
Events: `digital_link.created`, `digital_link.updated`, `digital_link.deleted`
|
|
781
|
+
Events: `digital_link.created`, `digital_link.updated`, `digital_link.deleted`, `digital_link.downloaded`
|
|
782
|
+
|
|
783
|
+
`digital_link.downloaded` fires each time a customer successfully downloads the
|
|
784
|
+
file, after the access counter has been incremented.
|
|
778
785
|
|
|
779
786
|
```json
|
|
780
787
|
{
|
|
@@ -782,7 +789,8 @@ Events: `digital_link.created`, `digital_link.updated`, `digital_link.deleted`
|
|
|
782
789
|
"access_counter": 3,
|
|
783
790
|
"filename": "ebook.pdf",
|
|
784
791
|
"content_type": "application/pdf",
|
|
785
|
-
"download_url": "/api/v3/store/
|
|
792
|
+
"download_url": "/api/v3/store/digital_links/abc123",
|
|
793
|
+
"expires_at": "2025-01-22T10:30:00Z",
|
|
786
794
|
"authorizable": true,
|
|
787
795
|
"expired": false,
|
|
788
796
|
"access_limit_exceeded": false,
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Build a Custom Digital Asset Provider
|
|
3
|
+
description: Resolve a digital product's deliverable from your own systems — a license server, an entitlement API, an external file host — instead of an uploaded file.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## Overview
|
|
7
|
+
|
|
8
|
+
A digital asset provider decides **how a digital asset's deliverable is produced** at download time. Spree ships with one built-in provider, `Spree::DigitalAssetProvider::File`, which serves an uploaded file from private storage through a short-lived signed URL. That is the default: every asset with a blank `provider_type` uses it, so an uploaded file is simply the provider each asset already had.
|
|
9
|
+
|
|
10
|
+
A custom provider replaces that upload with something you produce on demand — a license key minted by your billing system, an entitlement granted by internal software, a signed link to a file that lives on your own host. The customer's download flows through the same authorized, counted grant either way; only the last step, "hand something over", changes.
|
|
11
|
+
|
|
12
|
+
Reach for a provider when the deliverable comes from outside Spree. If you only need to serve a file that a merchant uploads, the built-in `File` provider already does that — you don't need to build anything.
|
|
13
|
+
|
|
14
|
+
## What you will build
|
|
15
|
+
|
|
16
|
+
Two pieces:
|
|
17
|
+
|
|
18
|
+
| Piece | Responsibility |
|
|
19
|
+
|---|---|
|
|
20
|
+
| `Spree::DigitalAssetProvider::Base` subclass | Produces the deliverable for one authorized download |
|
|
21
|
+
| An initializer | Registers the provider so admins can select it as an asset source |
|
|
22
|
+
|
|
23
|
+
Unlike delivery-rate or tax providers, a digital asset provider does **not** resolve credentials through a `Spree::Integration`. It is host-app glue into your own software and owns its own configuration — an environment variable, an internal endpoint, a shared credential. The base gives it only the asset.
|
|
24
|
+
|
|
25
|
+
## 1. Implement the provider
|
|
26
|
+
|
|
27
|
+
Subclass `Spree::DigitalAssetProvider::Base` and implement `#deliver`. It receives the authorized `Spree::DigitalLink` (the customer's download grant) and returns a `Spree::DigitalDelivery`.
|
|
28
|
+
|
|
29
|
+
```ruby app/models/spree/digital_asset_provider/license_key.rb
|
|
30
|
+
module Spree
|
|
31
|
+
module DigitalAssetProvider
|
|
32
|
+
class LicenseKey < Base
|
|
33
|
+
# This asset carries no uploaded file — its deliverable is minted on
|
|
34
|
+
# demand — so no attachment is required or validated.
|
|
35
|
+
def self.requires_attachment?
|
|
36
|
+
false
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Per-asset configuration the dashboard renders as a form when a
|
|
40
|
+
# merchant adds an asset backed by this provider. See "Per-asset
|
|
41
|
+
# settings" below.
|
|
42
|
+
setting :pool_name, :string
|
|
43
|
+
setting :region, :select, in: %w[us eu], default: 'us'
|
|
44
|
+
|
|
45
|
+
def deliver(digital_link, expires_in:)
|
|
46
|
+
key = LicenseServer.issue(
|
|
47
|
+
pool: digital_asset.provider_settings['pool_name'],
|
|
48
|
+
region: digital_asset.provider_settings['region'],
|
|
49
|
+
order_number: digital_link.line_item.order.number
|
|
50
|
+
)
|
|
51
|
+
return if key.blank?
|
|
52
|
+
|
|
53
|
+
Spree::DigitalDelivery.new(inline_value: key, content_type: 'text/plain')
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
A `Spree::DigitalDelivery` carries exactly one of two shapes:
|
|
61
|
+
|
|
62
|
+
- **A redirect** — set `redirect_url`. The customer is sent to it (an external file host, a signed storage URL). This is what `File` returns.
|
|
63
|
+
- **An inline body** — set `inline_value` and `content_type`. The value is rendered directly as the response body — a license key as `text/plain`, a code image as `image/png`.
|
|
64
|
+
|
|
65
|
+
Return a blank delivery (or `nil`) to mean "nothing to hand over". The download controller treats that as a failure and refuses the download **without spending the customer's allowance** — see [The download contract](#the-download-contract) below.
|
|
66
|
+
|
|
67
|
+
> **WARNING:** `#deliver` must be safe to call before the download is charged. It runs while the grant is only *checked*, not yet *spent*, so a provider that raises or returns blank costs the customer nothing. Do any fallible work — the API call, the mint — inside `#deliver`, not after.
|
|
68
|
+
|
|
69
|
+
## Per-asset settings
|
|
70
|
+
|
|
71
|
+
Some providers need a value that differs from one asset to the next — which license pool this asset draws from, which external product it maps to. Declare each as a `setting`, and the dashboard renders a small form when the merchant adds an asset backed by this provider:
|
|
72
|
+
|
|
73
|
+
```ruby
|
|
74
|
+
setting :pool_name, :string
|
|
75
|
+
setting :region, :select, in: %w[us eu], default: 'us'
|
|
76
|
+
setting :auto_revoke, :boolean, default: false
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Field types are `:string`, `:number`, `:boolean`, and `:select` (pass `in:` for the choices). The merchant's answers are stored on the asset and read back through `digital_asset.provider_settings`, a plain hash keyed by the setting name:
|
|
80
|
+
|
|
81
|
+
```ruby
|
|
82
|
+
digital_asset.provider_settings['pool_name'] # => "winter-sale"
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
A provider that declares no settings skips the form entirely — adding it is a single click. Settings are for per-asset values only; a credential or endpoint shared across every asset belongs in the provider's own configuration (an environment variable, Rails credentials), not a setting.
|
|
86
|
+
|
|
87
|
+
## 2. Register the provider
|
|
88
|
+
|
|
89
|
+
```ruby config/initializers/spree.rb
|
|
90
|
+
Rails.application.config.after_initialize do
|
|
91
|
+
Spree.digital_asset_providers << 'Spree::DigitalAssetProvider::LicenseKey'
|
|
92
|
+
end
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Once registered, the provider appears as a source on the product's **Digital files** card: the **Add** button becomes a menu offering **Upload a file** alongside each registered provider. Picking your provider creates an asset with its `provider_type` set and no file attached.
|
|
96
|
+
|
|
97
|
+
## The download contract
|
|
98
|
+
|
|
99
|
+
Every download follows the same order, and a provider only participates in the last step:
|
|
100
|
+
|
|
101
|
+
1. **Check the grant** — is it still authorizable (attempts left, not reset)?
|
|
102
|
+
2. **Check the window** — is the signed-URL lifetime still positive?
|
|
103
|
+
3. **Produce the deliverable** — call your `#deliver`. This is fallible and must not charge anything.
|
|
104
|
+
4. **Charge the grant** — increment the download counter under a lock.
|
|
105
|
+
5. **Deliver** — redirect, or render the inline body.
|
|
106
|
+
|
|
107
|
+
The order is deliberate: the deliverable is produced *before* the click is spent, so a provider outage or an exhausted external quota returns an honest error and the customer keeps their download. Keep `#deliver` free of side effects that assume the download will succeed.
|
|
108
|
+
|
|
109
|
+
## Provider contract reference
|
|
110
|
+
|
|
111
|
+
| Method | Required | Description |
|
|
112
|
+
|--------|----------|-------------|
|
|
113
|
+
| `#deliver(digital_link, expires_in:)` | Yes | Produce the deliverable for one authorized download. Return a `Spree::DigitalDelivery`, or blank/`nil` for "nothing to hand over". |
|
|
114
|
+
| `self.requires_attachment?` | No | Return `true` if an asset backed by this provider must carry an uploaded file. Defaults to `false`. Drives conditional validation and the admin create form. |
|
|
115
|
+
| `self.setting(key, type, default:, in:)` | No | Declare a per-asset config field the dashboard renders as a form. Types: `:string`, `:number`, `:boolean`, `:select`. Read the merchant's answers via `digital_asset.provider_settings`. |
|
|
116
|
+
| `self.provider_name` | No | Human-readable name for admin UIs. Defaults to a titleized class name, or the outer module for a `SpreeAcme::LicenseProvider`-style gem. |
|
|
117
|
+
|
|
118
|
+
### DigitalDelivery
|
|
119
|
+
|
|
120
|
+
```ruby
|
|
121
|
+
# A redirect (external host, signed storage URL)
|
|
122
|
+
Spree::DigitalDelivery.new(redirect_url: 'https://files.example.com/signed/...')
|
|
123
|
+
|
|
124
|
+
# An inline body (license key, code image)
|
|
125
|
+
Spree::DigitalDelivery.new(inline_value: 'ABCD-1234-EFGH', content_type: 'text/plain')
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
`expires_in` is the lifetime the store allows for any signed URL you build. It is already clamped to the store's cap, so pass it straight through to whatever signs your link — never widen it.
|
|
129
|
+
|
|
130
|
+
## Related documentation
|
|
131
|
+
|
|
132
|
+
- [Digital products](/use-case/digital-products/capabilities) — what the feature does for merchants
|
|
133
|
+
- [Build a Custom Delivery Rate Provider](/v6/developer/how-to/custom-delivery-rate-provider) — a provider strategy that *does* use an integration, for contrast
|
|
@@ -278,6 +278,11 @@ A handler receives the workflow (so it can read `order`, `items`, `created_by`,
|
|
|
278
278
|
- **The reimbursement email is now `Spree::ReturnMailer#refunded_email`**, sent on `return.refunded` (in the optional `spree_emails` gem, like the other transactional mail).
|
|
279
279
|
- **`Refund#originator`** points at the new records.
|
|
280
280
|
- Events are `return.requested` / `.approved` / `.received` / `.refunded` / `.canceled`, and the matching `exchange.*` and `claim.*` families.
|
|
281
|
+
- **Digital downloads redirect instead of streaming.** The download endpoint now answers `302` with a short-lived signed URL rather than sending the file bytes directly, so a large download no longer occupies a web worker. Clients that read the response body must follow redirects (most HTTP clients and every browser already do). The link's own lifetime is unchanged; `digital_asset_link_expire_time` (default 300 seconds) — previously unused — now sets how long the signed URL stays valid, and is capped at one hour because that URL is a bearer credential. The signed URL is additionally clamped so it can never outlive the download link that issued it.
|
|
282
|
+
- **Download limits can be set per asset.** `authorized_clicks` and `authorized_days` on a digital asset override the store's download settings; left blank, the store settings apply as before.
|
|
283
|
+
- **A successful download publishes `digital_link.downloaded`**, so downloads are visible to webhooks and subscribers for the first time.
|
|
284
|
+
- **Customers are emailed their download links** when the order is placed, from the new `Spree::DigitalAssetMailer` in the optional `spree_emails` gem. Hosts that already send their own download email should either suppress it (`send_consumer_transactional_emails`) or drop their own. The dashboard's order page can re-send it.
|
|
285
|
+
- **Digital assets are managed through the Admin API and dashboard**, and signed-in customers can list everything they have bought at `GET /api/v3/store/customers/me/digital_links`.
|
|
281
286
|
|
|
282
287
|
## Dependency injection changes
|
|
283
288
|
|
|
@@ -560,6 +565,11 @@ Every rename keeps the legacy name working for one release with a deprecation wa
|
|
|
560
565
|
| `CustomFieldDefinition#name`, `#metafield_type`, `#display_on` | `#label`, `#field_type`, `#storefront_visible` (columns renamed) |
|
|
561
566
|
| `Spree::SearchProvider::Meilisearch` | `SpreeMeilisearch::SearchProvider` (moved to the `spree_meilisearch` gem) |
|
|
562
567
|
| `Spree::SearchProvider::ProductPresenter` | `SpreeMeilisearch::ProductPresenter` (moved to the `spree_meilisearch` gem) |
|
|
568
|
+
| `Spree::Digital` | `Spree::DigitalAsset` (table `spree_digitals` → `spree_digital_assets`, `digital_id` → `digital_asset_id`) |
|
|
569
|
+
| `Variant#digitals`, `Product#digitals`, `DigitalLink#digital` | `#digital_assets` / `#digital_asset` |
|
|
570
|
+
| `digital.created` / `.updated` / `.deleted` events | `digital_asset.*` (both emitted for one release) |
|
|
571
|
+
| `PermittedAttributes.digital_attributes` | `.digital_asset_attributes` |
|
|
572
|
+
| `GET /api/v3/store/digitals/:token` | `GET /api/v3/store/digital_links/:token` (old path keeps working) |
|
|
563
573
|
|
|
564
574
|
## Meilisearch moved to its own gem
|
|
565
575
|
|