@spree/docs 0.1.187 → 0.1.189

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 Product Events
762
+ ## Digital Asset Events
763
763
 
764
- Events: `digital.created`, `digital.updated`, `digital.deleted`
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
- "created_at": "2025-01-01T00:00:00Z",
771
- "updated_at": "2025-01-01T00:00:00Z"
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/digital_downloads/abc123",
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spree/docs",
3
- "version": "0.1.187",
3
+ "version": "0.1.189",
4
4
  "description": "Spree Commerce developer documentation for AI agents and local reference",
5
5
  "type": "module",
6
6
  "license": "CC-BY-4.0",