create-cartbase 0.1.16 → 0.1.17
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/LICENSE +21 -21
- package/README.md +25 -25
- package/dist/index.js +20 -20
- package/package.json +24 -24
- package/template/app/docs/auth.md +105 -105
- package/template/app/docs/carts.md +376 -376
- package/template/app/docs/categories.md +194 -194
- package/template/app/docs/checkout.md +714 -714
- package/template/app/docs/components.md +44 -11
- package/template/app/docs/consent.md +91 -91
- package/template/app/docs/deploy.md +197 -197
- package/template/app/docs/gift-cards.md +153 -153
- package/template/app/docs/metaobjects.md +126 -126
- package/template/app/docs/orders.md +221 -221
- package/template/app/docs/products.md +51 -2
- package/template/app/docs/regions.md +269 -269
- package/template/app/docs/reviews.md +223 -223
- package/template/app/docs/search.md +227 -227
- package/template/app/docs/store.md +47 -47
- package/template/app/docs/subscriptions.md +148 -148
- package/template/app/docs/variables.md +315 -315
- package/template/app/package.json +1 -1
- package/template/app/postcss.config.cjs +11 -11
- package/template/app/src/app/checkout/checkout-page-client.tsx +73 -73
- package/template/app/src/app/checkout/page.tsx +48 -48
- package/template/app/src/app/globals.css +26 -26
- package/template/app/src/app/page.tsx +28 -28
- package/template/app/src/app/products/[handle]/page.tsx +87 -87
- package/template/app/src/app/providers.tsx +64 -64
- package/template/app/src/app/search/page.tsx +23 -23
- package/template/app/src/lib/browser-client.ts +35 -35
- package/template/app/src/lib/config.ts +41 -41
- package/template/app/src/lib/server-client.ts +25 -25
- package/template/app/src/lib/cart-actions.ts +0 -47
|
@@ -1,148 +1,148 @@
|
|
|
1
|
-
# Subscriptions
|
|
2
|
-
|
|
3
|
-
The "My subscriptions" surface (subscriptions-portal card): list, detail,
|
|
4
|
-
schedule control, contract edits, cancel/reactivate and payment-method
|
|
5
|
-
recovery. Every endpoint requires a **customer session**
|
|
6
|
-
(`authorization: Bearer <supabase jwt>` — see [auth.md](auth.md)) **plus**
|
|
7
|
-
`x-client-id`. Missing/invalid JWT → `401 {code: "unauthenticated"}`;
|
|
8
|
-
a subscription that isn't the caller's own → **`404 not_found`** (never
|
|
9
|
-
403 — existence is not confirmed across accounts).
|
|
10
|
-
|
|
11
|
-
The API contract (all routes, shapes, error codes):
|
|
12
|
-
`docs/contracts/store-api.md` § Subscriptions portal. This doc is the
|
|
13
|
-
component-facing guide.
|
|
14
|
-
|
|
15
|
-
> The executable blocks prove the **auth boundary** — the docs harness is
|
|
16
|
-
> anonymous; happy paths are pinned by
|
|
17
|
-
> `tests/store/subscriptions-portal.test.ts` and
|
|
18
|
-
> `tests/store/subscription-payment-update.test.ts` with real sessions.
|
|
19
|
-
|
|
20
|
-
## Render actions from `permissions` — never hardcode
|
|
21
|
-
|
|
22
|
-
Every detail payload carries the merchant's live portal policy:
|
|
23
|
-
|
|
24
|
-
```jsonc
|
|
25
|
-
"permissions": {
|
|
26
|
-
"allow_skip": true, // merchant toggles (Settings → Subscriptions)
|
|
27
|
-
"allow_reschedule": true,
|
|
28
|
-
"allow_pause": true,
|
|
29
|
-
"allow_frequency_change": true,
|
|
30
|
-
"allow_line_edits": true,
|
|
31
|
-
"allow_address_change": true,
|
|
32
|
-
"allow_cancel": true, // constant — cancel is a customer right
|
|
33
|
-
"can_update_payment": false // true only for card (Stripe) contracts
|
|
34
|
-
}
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
Components MUST render conditionally from this object — a toggled-off
|
|
38
|
-
action answers `403 {code: "portal_action_disabled"}`, so hiding the button
|
|
39
|
-
is UX, the server is the enforcement. Cancel is ALWAYS shown (the cancel
|
|
40
|
-
law: retention offers may render alongside, never instead). Payment update
|
|
41
|
-
renders only when `can_update_payment` — COD/offline contracts have no card.
|
|
42
|
-
|
|
43
|
-
## GET /api/store/subscriptions — my contracts
|
|
44
|
-
|
|
45
|
-
`{ subscriptions: [...], count }` — newest first. `payment_method` is the
|
|
46
|
-
merchant-facing display name from the payments registry (never `pp_*`).
|
|
47
|
-
|
|
48
|
-
```bash
|
|
49
|
-
STATUS=$(curl -s -o /dev/null -w '%{http_code}' "$BASE/api/store/subscriptions" \
|
|
50
|
-
-H "x-client-id: $CLIENT_ID")
|
|
51
|
-
test "$STATUS" = 401
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
## GET /api/store/subscriptions/:id — the receipt view
|
|
55
|
-
|
|
56
|
-
Sanitized detail: `plan`, `lines` (titles + contracted `unit_price`),
|
|
57
|
-
`cycles` (index, status, date, linked order display id — no internal error
|
|
58
|
-
strings), `upcoming` (next 3 PROJECTED charge dates; empty unless active)
|
|
59
|
-
and `permissions`. Render the cycle list as order history; a `failed` cycle
|
|
60
|
-
plus `can_update_payment` is the cue to surface the payment-update flow
|
|
61
|
-
prominently (that pairing IS dunning recovery).
|
|
62
|
-
|
|
63
|
-
```bash
|
|
64
|
-
STATUS=$(curl -s -o /dev/null -w '%{http_code}' "$BASE/api/store/subscriptions/sub_doesnotexist" \
|
|
65
|
-
-H "x-client-id: $CLIENT_ID")
|
|
66
|
-
test "$STATUS" = 401
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
## Actions
|
|
70
|
-
|
|
71
|
-
All POST, all answer `{ subscription }` (the fresh detail — re-render from
|
|
72
|
-
it, no refetch needed):
|
|
73
|
-
|
|
74
|
-
| Route | Body | Gate |
|
|
75
|
-
|---|---|---|
|
|
76
|
-
| `/:id/skip` | — | `allow_skip` |
|
|
77
|
-
| `/:id/charge-date` | `{ next_charge_at }` | `allow_reschedule` |
|
|
78
|
-
| `/:id/pause` | `{ until? }` | `allow_pause` |
|
|
79
|
-
| `/:id/resume` | — | right |
|
|
80
|
-
| `/:id/cancel` | `{ reason? }` | right |
|
|
81
|
-
| `/:id/reactivate` | `{ next_charge_at? }` | right |
|
|
82
|
-
| `/:id/address` | `{ shipping_address }` | `allow_address_change` |
|
|
83
|
-
| `/:id/lines/:lineId` | `{ quantity?, variant_id? }` | `allow_line_edits` |
|
|
84
|
-
| `/:id/plan` | `{ selling_plan_id }` | `allow_frequency_change` |
|
|
85
|
-
|
|
86
|
-
Component notes:
|
|
87
|
-
|
|
88
|
-
- **Skip** — confirm-dialog copy should show the NEW next date (current
|
|
89
|
-
next date + one plan interval). A cycle mid-payment-retry cannot skip
|
|
90
|
-
(400) — hide skip while the latest cycle is `failed`.
|
|
91
|
-
- **Pause** — offer preset durations (1/2/3 months → `until`); an `until`
|
|
92
|
-
pause auto-resumes server-side, no customer action needed. Indefinite
|
|
93
|
-
pause (no body) needs an explicit Resume.
|
|
94
|
-
- **Reactivate** — render on canceled contracts; default schedule is
|
|
95
|
-
now + interval and it NEVER charges immediately — say so in the copy.
|
|
96
|
-
- **Swap / frequency** — variant options come from the product's variants
|
|
97
|
-
(same product only); frequency options are the product's other selling
|
|
98
|
-
plans (`GET /api/store/products/:id/selling-plans`). Both re-price
|
|
99
|
-
server-side through the one price engine — display the returned
|
|
100
|
-
`unit_price`, never compute prices client-side.
|
|
101
|
-
|
|
102
|
-
```bash
|
|
103
|
-
STATUS=$(curl -s -o /dev/null -w '%{http_code}' -X POST "$BASE/api/store/subscriptions/sub_doesnotexist/cancel" \
|
|
104
|
-
-H "x-client-id: $CLIENT_ID" -H "content-type: application/json" -d '{}')
|
|
105
|
-
test "$STATUS" = 401
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
## Payment-method update (dunning recovery)
|
|
109
|
-
|
|
110
|
-
Two steps, card contracts only:
|
|
111
|
-
|
|
112
|
-
1. `POST /:id/payment-method/session` → `{ session: { setup_intent_id,
|
|
113
|
-
client_secret, publishable_key } }`.
|
|
114
|
-
2. Confirm client-side with Stripe.js — card fields never touch Cartbase:
|
|
115
|
-
|
|
116
|
-
```jsonc
|
|
117
|
-
// stripe = Stripe(session.publishable_key)
|
|
118
|
-
// elements = stripe.elements({ clientSecret: session.client_secret })
|
|
119
|
-
// mount PaymentElement, then:
|
|
120
|
-
// await stripe.confirmSetup({ elements, redirect: "if_required" })
|
|
121
|
-
```
|
|
122
|
-
|
|
123
|
-
3. `POST /:id/payment-method` with `{ setup_intent_id }` → verified +
|
|
124
|
-
stamped; the response is the fresh detail. The next renewal charge (the
|
|
125
|
-
automatic retry ladder or the merchant's "Retry now") uses the new card.
|
|
126
|
-
|
|
127
|
-
Entry points to build: the payment-failed email links here; the detail view
|
|
128
|
-
surfaces it on `failed` cycles; the account shell may badge past-due
|
|
129
|
-
subscriptions.
|
|
130
|
-
|
|
131
|
-
```bash
|
|
132
|
-
STATUS=$(curl -s -o /dev/null -w '%{http_code}' -X POST "$BASE/api/store/subscriptions/sub_doesnotexist/payment-method/session" \
|
|
133
|
-
-H "x-client-id: $CLIENT_ID" -d '')
|
|
134
|
-
test "$STATUS" = 401
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
## Checkout + confirmation touchpoints
|
|
138
|
-
|
|
139
|
-
- **Consent line at checkout**: subscription carts save the card for future
|
|
140
|
-
charges (`setup_future_usage: off_session`) — the checkout MUST show the
|
|
141
|
-
mandate text next to the pay button. [checkout.md](checkout.md) documents
|
|
142
|
-
the duty; the component ships with the portal family.
|
|
143
|
-
- **Order confirmation**: a completed subscription checkout returns
|
|
144
|
-
contracts (cycle 1 = that order) — show "subscription started, next
|
|
145
|
-
charge on <date>" from the order's subscription metadata.
|
|
146
|
-
- **PDP purchase options**: `GET /api/store/products/:id/selling-plans`
|
|
147
|
-
(see [products.md](products.md)) — the plan chosen at PDP rides the cart
|
|
148
|
-
line as `selling_plan_id`.
|
|
1
|
+
# Subscriptions
|
|
2
|
+
|
|
3
|
+
The "My subscriptions" surface (subscriptions-portal card): list, detail,
|
|
4
|
+
schedule control, contract edits, cancel/reactivate and payment-method
|
|
5
|
+
recovery. Every endpoint requires a **customer session**
|
|
6
|
+
(`authorization: Bearer <supabase jwt>` — see [auth.md](auth.md)) **plus**
|
|
7
|
+
`x-client-id`. Missing/invalid JWT → `401 {code: "unauthenticated"}`;
|
|
8
|
+
a subscription that isn't the caller's own → **`404 not_found`** (never
|
|
9
|
+
403 — existence is not confirmed across accounts).
|
|
10
|
+
|
|
11
|
+
The API contract (all routes, shapes, error codes):
|
|
12
|
+
`docs/contracts/store-api.md` § Subscriptions portal. This doc is the
|
|
13
|
+
component-facing guide.
|
|
14
|
+
|
|
15
|
+
> The executable blocks prove the **auth boundary** — the docs harness is
|
|
16
|
+
> anonymous; happy paths are pinned by
|
|
17
|
+
> `tests/store/subscriptions-portal.test.ts` and
|
|
18
|
+
> `tests/store/subscription-payment-update.test.ts` with real sessions.
|
|
19
|
+
|
|
20
|
+
## Render actions from `permissions` — never hardcode
|
|
21
|
+
|
|
22
|
+
Every detail payload carries the merchant's live portal policy:
|
|
23
|
+
|
|
24
|
+
```jsonc
|
|
25
|
+
"permissions": {
|
|
26
|
+
"allow_skip": true, // merchant toggles (Settings → Subscriptions)
|
|
27
|
+
"allow_reschedule": true,
|
|
28
|
+
"allow_pause": true,
|
|
29
|
+
"allow_frequency_change": true,
|
|
30
|
+
"allow_line_edits": true,
|
|
31
|
+
"allow_address_change": true,
|
|
32
|
+
"allow_cancel": true, // constant — cancel is a customer right
|
|
33
|
+
"can_update_payment": false // true only for card (Stripe) contracts
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Components MUST render conditionally from this object — a toggled-off
|
|
38
|
+
action answers `403 {code: "portal_action_disabled"}`, so hiding the button
|
|
39
|
+
is UX, the server is the enforcement. Cancel is ALWAYS shown (the cancel
|
|
40
|
+
law: retention offers may render alongside, never instead). Payment update
|
|
41
|
+
renders only when `can_update_payment` — COD/offline contracts have no card.
|
|
42
|
+
|
|
43
|
+
## GET /api/store/subscriptions — my contracts
|
|
44
|
+
|
|
45
|
+
`{ subscriptions: [...], count }` — newest first. `payment_method` is the
|
|
46
|
+
merchant-facing display name from the payments registry (never `pp_*`).
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
STATUS=$(curl -s -o /dev/null -w '%{http_code}' "$BASE/api/store/subscriptions" \
|
|
50
|
+
-H "x-client-id: $CLIENT_ID")
|
|
51
|
+
test "$STATUS" = 401
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## GET /api/store/subscriptions/:id — the receipt view
|
|
55
|
+
|
|
56
|
+
Sanitized detail: `plan`, `lines` (titles + contracted `unit_price`),
|
|
57
|
+
`cycles` (index, status, date, linked order display id — no internal error
|
|
58
|
+
strings), `upcoming` (next 3 PROJECTED charge dates; empty unless active)
|
|
59
|
+
and `permissions`. Render the cycle list as order history; a `failed` cycle
|
|
60
|
+
plus `can_update_payment` is the cue to surface the payment-update flow
|
|
61
|
+
prominently (that pairing IS dunning recovery).
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
STATUS=$(curl -s -o /dev/null -w '%{http_code}' "$BASE/api/store/subscriptions/sub_doesnotexist" \
|
|
65
|
+
-H "x-client-id: $CLIENT_ID")
|
|
66
|
+
test "$STATUS" = 401
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Actions
|
|
70
|
+
|
|
71
|
+
All POST, all answer `{ subscription }` (the fresh detail — re-render from
|
|
72
|
+
it, no refetch needed):
|
|
73
|
+
|
|
74
|
+
| Route | Body | Gate |
|
|
75
|
+
|---|---|---|
|
|
76
|
+
| `/:id/skip` | — | `allow_skip` |
|
|
77
|
+
| `/:id/charge-date` | `{ next_charge_at }` | `allow_reschedule` |
|
|
78
|
+
| `/:id/pause` | `{ until? }` | `allow_pause` |
|
|
79
|
+
| `/:id/resume` | — | right |
|
|
80
|
+
| `/:id/cancel` | `{ reason? }` | right |
|
|
81
|
+
| `/:id/reactivate` | `{ next_charge_at? }` | right |
|
|
82
|
+
| `/:id/address` | `{ shipping_address }` | `allow_address_change` |
|
|
83
|
+
| `/:id/lines/:lineId` | `{ quantity?, variant_id? }` | `allow_line_edits` |
|
|
84
|
+
| `/:id/plan` | `{ selling_plan_id }` | `allow_frequency_change` |
|
|
85
|
+
|
|
86
|
+
Component notes:
|
|
87
|
+
|
|
88
|
+
- **Skip** — confirm-dialog copy should show the NEW next date (current
|
|
89
|
+
next date + one plan interval). A cycle mid-payment-retry cannot skip
|
|
90
|
+
(400) — hide skip while the latest cycle is `failed`.
|
|
91
|
+
- **Pause** — offer preset durations (1/2/3 months → `until`); an `until`
|
|
92
|
+
pause auto-resumes server-side, no customer action needed. Indefinite
|
|
93
|
+
pause (no body) needs an explicit Resume.
|
|
94
|
+
- **Reactivate** — render on canceled contracts; default schedule is
|
|
95
|
+
now + interval and it NEVER charges immediately — say so in the copy.
|
|
96
|
+
- **Swap / frequency** — variant options come from the product's variants
|
|
97
|
+
(same product only); frequency options are the product's other selling
|
|
98
|
+
plans (`GET /api/store/products/:id/selling-plans`). Both re-price
|
|
99
|
+
server-side through the one price engine — display the returned
|
|
100
|
+
`unit_price`, never compute prices client-side.
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
STATUS=$(curl -s -o /dev/null -w '%{http_code}' -X POST "$BASE/api/store/subscriptions/sub_doesnotexist/cancel" \
|
|
104
|
+
-H "x-client-id: $CLIENT_ID" -H "content-type: application/json" -d '{}')
|
|
105
|
+
test "$STATUS" = 401
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Payment-method update (dunning recovery)
|
|
109
|
+
|
|
110
|
+
Two steps, card contracts only:
|
|
111
|
+
|
|
112
|
+
1. `POST /:id/payment-method/session` → `{ session: { setup_intent_id,
|
|
113
|
+
client_secret, publishable_key } }`.
|
|
114
|
+
2. Confirm client-side with Stripe.js — card fields never touch Cartbase:
|
|
115
|
+
|
|
116
|
+
```jsonc
|
|
117
|
+
// stripe = Stripe(session.publishable_key)
|
|
118
|
+
// elements = stripe.elements({ clientSecret: session.client_secret })
|
|
119
|
+
// mount PaymentElement, then:
|
|
120
|
+
// await stripe.confirmSetup({ elements, redirect: "if_required" })
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
3. `POST /:id/payment-method` with `{ setup_intent_id }` → verified +
|
|
124
|
+
stamped; the response is the fresh detail. The next renewal charge (the
|
|
125
|
+
automatic retry ladder or the merchant's "Retry now") uses the new card.
|
|
126
|
+
|
|
127
|
+
Entry points to build: the payment-failed email links here; the detail view
|
|
128
|
+
surfaces it on `failed` cycles; the account shell may badge past-due
|
|
129
|
+
subscriptions.
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
STATUS=$(curl -s -o /dev/null -w '%{http_code}' -X POST "$BASE/api/store/subscriptions/sub_doesnotexist/payment-method/session" \
|
|
133
|
+
-H "x-client-id: $CLIENT_ID" -d '')
|
|
134
|
+
test "$STATUS" = 401
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Checkout + confirmation touchpoints
|
|
138
|
+
|
|
139
|
+
- **Consent line at checkout**: subscription carts save the card for future
|
|
140
|
+
charges (`setup_future_usage: off_session`) — the checkout MUST show the
|
|
141
|
+
mandate text next to the pay button. [checkout.md](checkout.md) documents
|
|
142
|
+
the duty; the component ships with the portal family.
|
|
143
|
+
- **Order confirmation**: a completed subscription checkout returns
|
|
144
|
+
contracts (cycle 1 = that order) — show "subscription started, next
|
|
145
|
+
charge on <date>" from the order's subscription metadata.
|
|
146
|
+
- **PDP purchase options**: `GET /api/store/products/:id/selling-plans`
|
|
147
|
+
(see [products.md](products.md)) — the plan chosen at PDP rides the cart
|
|
148
|
+
line as `selling_plan_id`.
|