create-cartbase 0.1.20 → 0.1.22

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.
Files changed (35) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +25 -25
  3. package/dist/index.js +20 -20
  4. package/package.json +24 -24
  5. package/template/app/docs/auth.md +105 -105
  6. package/template/app/docs/carts.md +23 -4
  7. package/template/app/docs/categories.md +194 -194
  8. package/template/app/docs/checkout.md +714 -714
  9. package/template/app/docs/components.md +270 -24
  10. package/template/app/docs/consent.md +91 -91
  11. package/template/app/docs/deploy.md +197 -197
  12. package/template/app/docs/gift-cards.md +153 -153
  13. package/template/app/docs/metaobjects.md +126 -126
  14. package/template/app/docs/orders.md +221 -221
  15. package/template/app/docs/regions.md +269 -269
  16. package/template/app/docs/reviews.md +258 -238
  17. package/template/app/docs/search.md +227 -227
  18. package/template/app/docs/store.md +23 -1
  19. package/template/app/docs/subscriptions.md +148 -148
  20. package/template/app/docs/variables.md +331 -315
  21. package/template/app/package.json +1 -1
  22. package/template/app/postcss.config.cjs +11 -11
  23. package/template/app/src/app/checkout/checkout-empty.tsx +36 -0
  24. package/template/app/src/app/checkout/checkout-page-client.tsx +80 -73
  25. package/template/app/src/app/checkout/error.tsx +23 -0
  26. package/template/app/src/app/checkout/page.tsx +16 -7
  27. package/template/app/src/app/globals.css +26 -26
  28. package/template/app/src/app/layout.tsx +126 -126
  29. package/template/app/src/app/page.tsx +37 -37
  30. package/template/app/src/app/products/[handle]/page.tsx +89 -89
  31. package/template/app/src/app/search/page.tsx +33 -33
  32. package/template/app/src/lib/browser-client.ts +35 -35
  33. package/template/app/src/lib/catalog.ts +120 -120
  34. package/template/app/src/lib/server-client.ts +25 -25
  35. package/template/app/src/lib/store-client.ts +16 -16
@@ -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`.