@spree/docs 0.1.192 → 0.1.194
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.
|
@@ -716,6 +716,115 @@ The API automatically returns the correct price based on the current currency an
|
|
|
716
716
|
|
|
717
717
|
See the [Pricing](pricing.md) guide for details on Price Lists, Price Rules, and market-specific pricing.
|
|
718
718
|
|
|
719
|
+
## Digital products
|
|
720
|
+
|
|
721
|
+
**A digital product is an ordinary product whose variant delivers without shipping — its [delivery profile](fulfillments.md) is a digital one, so buying it grants the customer a download instead of dispatching a parcel.** Nothing about the catalog model changes — you still have a product, its variants, and its prices. What that variant hands over is usually a **digital asset** it carries (an e‑book, a design file) or a value a provider mints on demand (a license key) — but the file is the optional deliverable, not what makes the product digital.
|
|
722
|
+
|
|
723
|
+
Two ideas are worth separating up front, because they are independent:
|
|
724
|
+
|
|
725
|
+
- **Being digital** is a delivery decision. A variant is digital when its [delivery profile](fulfillments.md) is a digital one — that variant needs no shipping address, and a cart made up entirely of digital variants skips the delivery step at checkout.
|
|
726
|
+
- **Carrying downloadable files** is a catalog decision. Any variant can own digital assets — including a physical one, so a boxed product can ship a warranty PDF or a setup guide alongside the goods.
|
|
727
|
+
|
|
728
|
+
Most digital products are both: a digital variant that carries the files it delivers. But the two are decoupled on purpose, so "ships nothing" and "hands over a file" can be mixed as a merchant needs.
|
|
729
|
+
|
|
730
|
+
```mermaid
|
|
731
|
+
erDiagram
|
|
732
|
+
Product ||--|{ Variant : "has"
|
|
733
|
+
Variant ||--o{ DigitalAsset : "carries"
|
|
734
|
+
DigitalAsset ||--o{ DigitalLink : "granted as"
|
|
735
|
+
LineItem ||--o{ DigitalLink : "purchased in"
|
|
736
|
+
DigitalAsset {
|
|
737
|
+
string provider_type "blank = uploaded file"
|
|
738
|
+
int authorized_clicks "nullable, falls back to store"
|
|
739
|
+
int authorized_days "nullable, falls back to store"
|
|
740
|
+
}
|
|
741
|
+
DigitalLink {
|
|
742
|
+
string token "the download credential"
|
|
743
|
+
int access_counter "downloads spent"
|
|
744
|
+
}
|
|
745
|
+
```
|
|
746
|
+
|
|
747
|
+
### Assets live on the variant
|
|
748
|
+
|
|
749
|
+
A digital asset (`Spree::DigitalAsset`) belongs to a variant and holds either an uploaded file or a reference to a provider that produces the deliverable on demand (see [Where the file comes from](#where-the-file-comes-from) below). Uploaded files go to **private storage** — they are only ever served through a short‑lived, signed link, never a public URL.
|
|
750
|
+
|
|
751
|
+
You attach assets to a variant from the product's **Digital files** card in the dashboard, or through the Admin API nested under the product:
|
|
752
|
+
|
|
753
|
+
|
|
754
|
+
```typescript Admin SDK
|
|
755
|
+
// 1. Ask for a private-storage upload slot, then PUT the file to the returned URL.
|
|
756
|
+
const { direct_upload, signed_id } = await adminClient.directUploads.create({
|
|
757
|
+
private: true, // digital files are only ever served through a signed link
|
|
758
|
+
blob: { filename: 'guide.pdf', byte_size: file.size, checksum, content_type: 'application/pdf' },
|
|
759
|
+
})
|
|
760
|
+
await fetch(direct_upload.url, { method: 'PUT', headers: direct_upload.headers, body: file })
|
|
761
|
+
|
|
762
|
+
// 2. Attach the uploaded blob to the product's default variant.
|
|
763
|
+
await adminClient.products.digitalAssets.create('prod_86Rf07xd4z', {
|
|
764
|
+
signed_id,
|
|
765
|
+
authorized_clicks: 5, // optional — omit to inherit the store default
|
|
766
|
+
authorized_days: 30,
|
|
767
|
+
})
|
|
768
|
+
```
|
|
769
|
+
|
|
770
|
+
```bash cURL
|
|
771
|
+
curl -X POST 'https://api.mystore.com/api/v3/admin/products/prod_86Rf07xd4z/digital_assets' \
|
|
772
|
+
-H 'X-Spree-API-Key: sk_xxx' \
|
|
773
|
+
-H 'Content-Type: application/json' \
|
|
774
|
+
-d '{ "signed_id": "eyJfcmFpbHMi...", "authorized_clicks": 5, "authorized_days": 30 }'
|
|
775
|
+
```
|
|
776
|
+
|
|
777
|
+
|
|
778
|
+
> **WARNING:** Digital files must be uploaded to **private storage**. The Admin API refuses a blob that landed on the public service — attaching never moves a file between services, so a public upload would stay publicly readable while looking attached. Request the direct upload with `private: true`.
|
|
779
|
+
|
|
780
|
+
Replacing an asset's file keeps every download link that was already issued working: links resolve through the asset, not the underlying blob, so a merchant can swap a corrected file mid‑sale without breaking anyone's access.
|
|
781
|
+
|
|
782
|
+
### A purchase grants download links
|
|
783
|
+
|
|
784
|
+
When an order is placed, digital assets fulfill themselves. The digital fulfillment provider runs automatically (it needs no address and no manual action) and, for each purchased unit, creates one **download link** (`Spree::DigitalLink`) per asset on the variant. Buy three copies of a two‑file bundle and the buyer gets six links. Re‑running fulfillment is idempotent — it never duplicates links.
|
|
785
|
+
|
|
786
|
+
A download link is the customer's **grant**: a globally unique token, a counter of downloads spent, and its own copy of the allowance. The token *is* the credential — it identifies the store on its own, which is why an emailed link works without any API key.
|
|
787
|
+
|
|
788
|
+
Customers reach their files two ways:
|
|
789
|
+
|
|
790
|
+
- **By email.** On order placement, `Spree::DigitalAssetMailer` sends a "your files are ready" message with the links — a message of its own, separate from the order confirmation, so it can be re‑sent from the order page later.
|
|
791
|
+
- **From their account.** A signed‑in customer sees every link they have ever been granted, across all their orders, through the customer downloads endpoint.
|
|
792
|
+
|
|
793
|
+
> **NOTE:** The files-ready email is only sent when the order actually has digital links and the store has consumer transactional emails enabled. A store that delivers files through its own storefront or webhooks can leave it off.
|
|
794
|
+
|
|
795
|
+
### Downloading, and the allowance
|
|
796
|
+
|
|
797
|
+
A download is a `GET` against the link's token. Every download runs the same guarded sequence, and the customer's allowance is only spent once a deliverable is actually in hand:
|
|
798
|
+
|
|
799
|
+
1. **The grant must be live** — attempts remaining, and not past its expiry.
|
|
800
|
+
2. **The signed‑URL window must be open** — clamped to whatever is shorter, the store's link lifetime or the link's own remaining days.
|
|
801
|
+
3. **The deliverable is produced** — see providers below. This step is allowed to fail.
|
|
802
|
+
4. **The download is charged** — the counter is incremented under a lock.
|
|
803
|
+
5. **The file is handed over** — a redirect to a signed URL, or an inline body.
|
|
804
|
+
|
|
805
|
+
The ordering is deliberate: producing the deliverable comes *before* charging the click, so a provider outage or a missing file returns an honest error and **costs the customer nothing**. Two allowances govern access, both falling back to store settings when the asset leaves them blank:
|
|
806
|
+
|
|
807
|
+
| Field | Meaning | Store default |
|
|
808
|
+
| --- | --- | :---: |
|
|
809
|
+
| `authorized_clicks` | How many times the file may be downloaded | `5` |
|
|
810
|
+
| `authorized_days` | How long after purchase the link stays valid | `7` |
|
|
811
|
+
|
|
812
|
+
Store‑wide defaults and their on/off switches live at **Settings → Store**; the separate `digital_asset_link_expire_time` preference caps how long a single signed URL lives (default 5 minutes, never more than an hour) because that URL is a bearer credential. When a customer runs out of downloads or a file was replaced mid‑flight, an admin can restore access by resetting the link from the order page, which zeroes the counter and restarts the clock.
|
|
813
|
+
|
|
814
|
+
Each successful download publishes an event you can subscribe to:
|
|
815
|
+
|
|
816
|
+
| Event | Published when |
|
|
817
|
+
| --- | --- |
|
|
818
|
+
| `digital_link.downloaded` | A customer successfully downloads a file (after the click is charged) |
|
|
819
|
+
|
|
820
|
+
### Where the file comes from
|
|
821
|
+
|
|
822
|
+
By default a digital asset delivers its uploaded file. But the last step — "hand something over" — is pluggable through a **digital asset provider**, so the deliverable can instead be minted on demand: a license key from your billing system, an entitlement from internal software, or a signed link to a file on your own host. The purchase, the grant, the allowance, and the email are identical either way; only the production of the deliverable changes.
|
|
823
|
+
|
|
824
|
+
A blank `provider_type` on an asset means the built‑in file provider — the uploaded‑file behavior described above. Registering your own provider adds it as a source on the Digital files card, so a merchant can pick it when adding an asset.
|
|
825
|
+
|
|
826
|
+
> **TIP:** To build one, see the [Build a Custom Digital Asset Provider](../how-to/custom-digital-asset-provider.md) how‑to. It covers the `#deliver` contract, per‑asset settings, and registration.
|
|
827
|
+
|
|
719
828
|
## Categories
|
|
720
829
|
|
|
721
830
|
There are two ways to group products, and they answer different questions.
|
|
@@ -934,6 +1043,7 @@ See [Sales Channels](channels.md) for the full channel lifecycle, including defa
|
|
|
934
1043
|
- [Pricing](pricing.md) — Price Lists, Price Rules, and market-specific pricing
|
|
935
1044
|
- [Inventory](inventory.md) — Stock management and backorders
|
|
936
1045
|
- [Media](media.md) — Image management
|
|
1046
|
+
- [Build a Custom Digital Asset Provider](../how-to/custom-digital-asset-provider.md) — Deliver a license key or external file instead of an uploaded one
|
|
937
1047
|
- [Translations](translations.md) — Translating product content
|
|
938
1048
|
- [Search & Filtering](search-filtering.md) — Full-text search and Ransack filtering
|
|
939
1049
|
- [Store SDK Products](../sdk/store/products.md) — Listing, fetching, filtering, and categories via `client.products`
|
|
@@ -76,7 +76,7 @@ setting :region, :select, in: %w[us eu], default: 'us'
|
|
|
76
76
|
setting :auto_revoke, :boolean, default: false
|
|
77
77
|
```
|
|
78
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:
|
|
79
|
+
Field types are `:string`, `:number`, `:boolean`, and `:select` (pass `in:` for the choices). Your declarations become the provider's `settings_schema`, which the admin `providers` endpoint exposes so the dashboard can render the form. The merchant's answers are stored on the asset — under one key, `metadata['provider']`, so they never collide with other developer metadata — and read back through `digital_asset.provider_settings`, a plain hash keyed by the setting name:
|
|
80
80
|
|
|
81
81
|
```ruby
|
|
82
82
|
digital_asset.provider_settings['pool_name'] # => "winter-sale"
|
|
@@ -94,6 +94,8 @@ end
|
|
|
94
94
|
|
|
95
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
96
|
|
|
97
|
+
> **NOTE:** Registration is what makes a `provider_type` valid. An asset validates that its `provider_type` names a registered provider, so an unregistered or misspelled class name is rejected with a 422 rather than failing at download time. A blank `provider_type` is always the built-in `File` provider.
|
|
98
|
+
|
|
97
99
|
## The download contract
|
|
98
100
|
|
|
99
101
|
Every download follows the same order, and a provider only participates in the last step:
|
|
@@ -130,4 +132,5 @@ Spree::DigitalDelivery.new(inline_value: 'ABCD-1234-EFGH', content_type: 'text/p
|
|
|
130
132
|
## Related documentation
|
|
131
133
|
|
|
132
134
|
- [Digital products](/use-case/digital-products/capabilities) — what the feature does for merchants
|
|
133
|
-
- [
|
|
135
|
+
- [Products](../core-concepts/products.md#digital-products) — how digital assets, links, and downloads fit together
|
|
136
|
+
- [Build a Custom Delivery Rate Provider](custom-delivery-rate-provider.md) — a provider strategy that *does* use an integration, for contrast
|
|
@@ -3,18 +3,75 @@ title: Sell Digital Products
|
|
|
3
3
|
description: Digital goods in Spree — a digital delivery profile, instant fulfillment without a shipping address, and delivering files or entitlements after payment.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
Digital products skip the warehouse: no stock to reserve, no address to collect, no carrier to call. In Spree 6 that behavior comes from the product's delivery profile — assign a digital profile and checkout, fulfillment and the customer experience adapt.
|
|
6
|
+
Digital products skip the warehouse: no stock to reserve, no address to collect, no carrier to call. In Spree 6 that behavior comes from the product's delivery profile — assign a digital profile and checkout, fulfillment and the customer experience adapt. This guide walks through selling an uploaded file end to end.
|
|
7
|
+
|
|
8
|
+
For the concepts behind each piece — assets, links, allowances, providers — see [Products → Digital products](../core-concepts/products.md#digital-products). This page is the practical walkthrough.
|
|
7
9
|
|
|
8
10
|
## What you will build
|
|
9
11
|
|
|
10
|
-
- **
|
|
11
|
-
- **
|
|
12
|
-
- **
|
|
13
|
-
- **Mixed carts** — physical and digital items in one order,
|
|
12
|
+
- **A digital variant** — one whose delivery profile requires no shipping address, so a fully digital cart completes checkout without a delivery step.
|
|
13
|
+
- **A downloadable file on it** — uploaded to private storage and attached as a digital asset.
|
|
14
|
+
- **Automatic delivery** — a download link granted on order placement, and an email that sends it.
|
|
15
|
+
- **Mixed carts** — physical and digital items in one order, handled without extra work.
|
|
16
|
+
|
|
17
|
+
## 1. Make the product digital
|
|
18
|
+
|
|
19
|
+
A variant is digital when its [delivery profile](../core-concepts/fulfillments.md) is a digital one. A store ships with a built-in **Digital** delivery profile; assign it to the product and the product's variants stop asking for a shipping address.
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
```typescript Admin SDK
|
|
23
|
+
// Assign the store's digital delivery profile to the product.
|
|
24
|
+
const { data: profiles } = await adminClient.deliveryProfiles.list()
|
|
25
|
+
const digital = profiles.find((p) => p.kind === 'digital')
|
|
26
|
+
|
|
27
|
+
await adminClient.products.update('prod_86Rf07xd4z', {
|
|
28
|
+
delivery_profile_id: digital.id,
|
|
29
|
+
})
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
```bash cURL
|
|
33
|
+
curl -X PATCH 'https://api.mystore.com/api/v3/admin/products/prod_86Rf07xd4z' \
|
|
34
|
+
-H 'X-Spree-API-Key: sk_xxx' \
|
|
35
|
+
-H 'Content-Type: application/json' \
|
|
36
|
+
-d '{ "delivery_profile_id": "dp_digital" }'
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
> **NOTE:** A product's delivery profile is auto-assigned when it is created — from its product type's template, or the store default. You only set it explicitly to change it. Selling from the dashboard, this is the **Shipping profile** field on the product page.
|
|
41
|
+
|
|
42
|
+
## 2. Attach the file
|
|
43
|
+
|
|
44
|
+
Upload the file to **private storage** and attach it to the variant as a digital asset. Private storage matters: a digital file is only ever served through a short-lived signed link, so the Admin API refuses a blob that landed on the public bucket.
|
|
45
|
+
|
|
46
|
+
```typescript Admin SDK
|
|
47
|
+
// Request a private upload slot, PUT the file, then attach the blob.
|
|
48
|
+
const { direct_upload, signed_id } = await adminClient.directUploads.create({
|
|
49
|
+
private: true,
|
|
50
|
+
blob: { filename: 'guide.pdf', byte_size: file.size, checksum, content_type: 'application/pdf' },
|
|
51
|
+
})
|
|
52
|
+
await fetch(direct_upload.url, { method: 'PUT', headers: direct_upload.headers, body: file })
|
|
53
|
+
|
|
54
|
+
await adminClient.products.digitalAssets.create('prod_86Rf07xd4z', { signed_id })
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Leave `authorized_clicks` and `authorized_days` off to inherit the store's download limits, or set them per file to mix evergreen downloads with time-limited ones. See [the allowance](../core-concepts/products.md#downloading-and-the-allowance) for how the limits resolve.
|
|
58
|
+
|
|
59
|
+
> **TIP:** You can attach a file to a **physical** variant too — a manual or warranty PDF that ships alongside the goods. Carrying files and being digital are independent: the physical variant still needs an address, and it also grants a download link.
|
|
60
|
+
|
|
61
|
+
## 3. Let the order deliver itself
|
|
62
|
+
|
|
63
|
+
There is no step three to configure. When the order is placed, the digital fulfillment provider runs automatically — it needs no address and no manual dispatch — and grants the buyer one download link per file, per purchased unit. `Spree::DigitalAssetMailer` then emails the links as a message of its own, so it survives a replaced order-confirmation template and can be re-sent from the order page.
|
|
64
|
+
|
|
65
|
+
The customer downloads by following the link's token; each download is counted against the allowance and publishes a `digital_link.downloaded` event. If they exhaust their downloads or you replace the file mid-sale, an admin restores access by resetting the link from the order page.
|
|
66
|
+
|
|
67
|
+
> **NOTE:** To deliver something other than an uploaded file — a license key minted on demand, an entitlement from your own system — the delivery step is pluggable. See [Build a Custom Digital Asset Provider](custom-digital-asset-provider.md).
|
|
68
|
+
|
|
69
|
+
## 4. Mixed carts work on their own
|
|
70
|
+
|
|
71
|
+
A cart with both physical and digital items needs nothing special. Spree splits the order into separate fulfillments per delivery profile, so the physical items collect an address and ship while the digital items skip straight to granting links. The delivery step only appears when at least one item actually needs shipping; an all-digital cart never sees it.
|
|
14
72
|
|
|
15
|
-
##
|
|
73
|
+
## Related documentation
|
|
16
74
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
4. Handle mixed physical and digital orders
|
|
75
|
+
- [Products → Digital products](../core-concepts/products.md#digital-products) — the concepts: assets, links, allowances, providers
|
|
76
|
+
- [Build a Custom Digital Asset Provider](custom-digital-asset-provider.md) — deliver a key or external file instead of an upload
|
|
77
|
+
- [Fulfillments](../core-concepts/fulfillments.md) — delivery profiles, providers, and how orders are fulfilled
|