@cartbase/storefront 0.14.0 → 0.16.0
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/README.md +9 -3
- package/package.json +4 -1
- package/src/api/customers.ts +9 -13
- package/src/api/products.ts +330 -310
- package/src/cart-drawer/context.tsx +11 -1
- package/src/cart-drawer/item/variant.tsx +8 -3
- package/src/checkout/checkout-client.tsx +10 -1
- package/src/checkout/line-item-card.tsx +5 -2
- package/src/checkout/order-summary.tsx +9 -5
- package/src/common/country-select.tsx +11 -65
- package/src/common/index.ts +2 -0
- package/src/common/market-select.tsx +57 -0
- package/src/lib/variant-caption.ts +32 -0
- package/src/locales/bg.ts +3 -0
- package/src/locales/context.ts +37 -0
- package/src/locales/es.ts +3 -0
- package/src/locales/provider.tsx +17 -21
- package/src/locales/types.ts +24 -21
- package/src/order/order-completed-template.tsx +10 -4
- package/src/order/order-item.tsx +4 -4
- package/src/products/index.ts +14 -0
- package/src/products/labels.ts +7 -0
- package/src/products/product-actions-wrapper.tsx +62 -58
- package/src/products/product-actions.tsx +104 -177
- package/src/products/product-info.tsx +10 -5
- package/src/products/product-template.tsx +162 -147
- package/src/products/related-products.tsx +9 -9
- package/src/products/use-product-actions.ts +164 -0
- package/src/products/variant-url.ts +74 -0
- package/src/reviews-ui/photo-upload.tsx +2 -1
- package/src/reviews-ui/review-list.tsx +4 -2
- package/src/reviews-ui/review-widget.tsx +4 -1
- package/src/reviews-ui/review-wizard.tsx +3 -2
- package/src/store/category-template.tsx +8 -1
- package/src/store/collection-template.tsx +8 -1
- package/src/store/paginated-products.tsx +3 -2
- package/src/store/search-template.tsx +10 -4
- package/src/store/store-template.tsx +10 -4
- package/src/tracking/chatgpt-pixel.tsx +17 -10
- package/src/tracking/consent-init.tsx +62 -44
- package/src/tracking/consent.ts +58 -7
- package/src/tracking/index.ts +2 -0
- package/src/tracking/meta-pixel.tsx +27 -15
- package/src/tracking/storefront-tags.tsx +14 -7
- package/src/tracking/tiktok-pixel.tsx +15 -7
- package/src/tracking/types.ts +5 -2
package/src/api/products.ts
CHANGED
|
@@ -1,310 +1,330 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @cartbase/storefront/api/products — products + product variants.
|
|
3
|
-
*
|
|
4
|
-
* The catalog read surface. Response shapes are the canonical store product
|
|
5
|
-
* object every discovery endpoint reuses (search results, collection
|
|
6
|
-
* membership pages, related products) — build ONE product-card renderer
|
|
7
|
-
* against `StoreProduct` and reuse it everywhere.
|
|
8
|
-
*
|
|
9
|
-
* Pricing: pass `currency_code` (or `region_id`) to receive
|
|
10
|
-
* `variant.calculated_price` (group-aware when a customer JWT is attached).
|
|
11
|
-
* Raw `prices` embeds contain BASE rows only — price-list rows are stripped
|
|
12
|
-
* server-side (leak rule) and surface exclusively through
|
|
13
|
-
* `calculated_price`. Docs: docs/storefront/products.md.
|
|
14
|
-
*/
|
|
15
|
-
import type { StorefrontClient } from "./http"
|
|
16
|
-
import type {
|
|
17
|
-
CalculatedPrice,
|
|
18
|
-
IsoDateString,
|
|
19
|
-
ListEnvelope,
|
|
20
|
-
PaginationQuery,
|
|
21
|
-
PricingContextQuery,
|
|
22
|
-
} from "./types"
|
|
23
|
-
|
|
24
|
-
/** Raw base price row (`prices` table). Amounts are EUR-style major units. */
|
|
25
|
-
export interface StorePriceRow {
|
|
26
|
-
id: string
|
|
27
|
-
amount: number
|
|
28
|
-
currency_code: string
|
|
29
|
-
min_quantity: number | null
|
|
30
|
-
max_quantity: number | null
|
|
31
|
-
price_set_id: string
|
|
32
|
-
/** Always null on the store surface — price-list rows are stripped. */
|
|
33
|
-
price_list_id: string | null
|
|
34
|
-
created_at: IsoDateString
|
|
35
|
-
updated_at: IsoDateString
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* The wire shape of `variant.prices`: a price-set LINK embed, not a flat
|
|
40
|
-
* array. Base rows live at `prices[].price_set.prices[]`.
|
|
41
|
-
*/
|
|
42
|
-
export interface StoreVariantPriceSetLink {
|
|
43
|
-
price_set: { prices: StorePriceRow[] | null } | null
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/** `variant.options[]` — the option VALUE with its parent option embedded. */
|
|
47
|
-
export interface StoreVariantOptionValueLink {
|
|
48
|
-
value: {
|
|
49
|
-
id: string
|
|
50
|
-
value: string
|
|
51
|
-
option_id: string
|
|
52
|
-
metadata: Record<string, unknown> | null
|
|
53
|
-
option: StoreProductOption | null
|
|
54
|
-
} | null
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export interface StoreProductOptionValue {
|
|
58
|
-
id: string
|
|
59
|
-
value: string
|
|
60
|
-
option_id: string
|
|
61
|
-
metadata: Record<string, unknown> | null
|
|
62
|
-
created_at: IsoDateString
|
|
63
|
-
updated_at: IsoDateString
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export interface StoreProductOption {
|
|
67
|
-
id: string
|
|
68
|
-
title: string
|
|
69
|
-
product_id: string
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
*
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
*/
|
|
179
|
-
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
/**
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
*
|
|
229
|
-
*
|
|
230
|
-
*
|
|
231
|
-
*
|
|
232
|
-
*
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
*
|
|
247
|
-
*
|
|
248
|
-
*
|
|
249
|
-
* `
|
|
250
|
-
*
|
|
251
|
-
*
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
):
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
*/
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @cartbase/storefront/api/products — products + product variants.
|
|
3
|
+
*
|
|
4
|
+
* The catalog read surface. Response shapes are the canonical store product
|
|
5
|
+
* object every discovery endpoint reuses (search results, collection
|
|
6
|
+
* membership pages, related products) — build ONE product-card renderer
|
|
7
|
+
* against `StoreProduct` and reuse it everywhere.
|
|
8
|
+
*
|
|
9
|
+
* Pricing: pass `currency_code` (or `region_id`) to receive
|
|
10
|
+
* `variant.calculated_price` (group-aware when a customer JWT is attached).
|
|
11
|
+
* Raw `prices` embeds contain BASE rows only — price-list rows are stripped
|
|
12
|
+
* server-side (leak rule) and surface exclusively through
|
|
13
|
+
* `calculated_price`. Docs: docs/storefront/products.md.
|
|
14
|
+
*/
|
|
15
|
+
import type { StorefrontClient } from "./http"
|
|
16
|
+
import type {
|
|
17
|
+
CalculatedPrice,
|
|
18
|
+
IsoDateString,
|
|
19
|
+
ListEnvelope,
|
|
20
|
+
PaginationQuery,
|
|
21
|
+
PricingContextQuery,
|
|
22
|
+
} from "./types"
|
|
23
|
+
|
|
24
|
+
/** Raw base price row (`prices` table). Amounts are EUR-style major units. */
|
|
25
|
+
export interface StorePriceRow {
|
|
26
|
+
id: string
|
|
27
|
+
amount: number
|
|
28
|
+
currency_code: string
|
|
29
|
+
min_quantity: number | null
|
|
30
|
+
max_quantity: number | null
|
|
31
|
+
price_set_id: string
|
|
32
|
+
/** Always null on the store surface — price-list rows are stripped. */
|
|
33
|
+
price_list_id: string | null
|
|
34
|
+
created_at: IsoDateString
|
|
35
|
+
updated_at: IsoDateString
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The wire shape of `variant.prices`: a price-set LINK embed, not a flat
|
|
40
|
+
* array. Base rows live at `prices[].price_set.prices[]`.
|
|
41
|
+
*/
|
|
42
|
+
export interface StoreVariantPriceSetLink {
|
|
43
|
+
price_set: { prices: StorePriceRow[] | null } | null
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** `variant.options[]` — the option VALUE with its parent option embedded. */
|
|
47
|
+
export interface StoreVariantOptionValueLink {
|
|
48
|
+
value: {
|
|
49
|
+
id: string
|
|
50
|
+
value: string
|
|
51
|
+
option_id: string
|
|
52
|
+
metadata: Record<string, unknown> | null
|
|
53
|
+
option: StoreProductOption | null
|
|
54
|
+
} | null
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface StoreProductOptionValue {
|
|
58
|
+
id: string
|
|
59
|
+
value: string
|
|
60
|
+
option_id: string
|
|
61
|
+
metadata: Record<string, unknown> | null
|
|
62
|
+
created_at: IsoDateString
|
|
63
|
+
updated_at: IsoDateString
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface StoreProductOption {
|
|
67
|
+
id: string
|
|
68
|
+
title: string
|
|
69
|
+
product_id: string
|
|
70
|
+
/** The merchant's order of the options on the product page. */
|
|
71
|
+
position: number
|
|
72
|
+
metadata: Record<string, unknown> | null
|
|
73
|
+
created_at: IsoDateString
|
|
74
|
+
updated_at: IsoDateString
|
|
75
|
+
/** Present on `product.options[]`; absent inside variant option links. */
|
|
76
|
+
values?: StoreProductOptionValue[]
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface StoreProductImage {
|
|
80
|
+
id: string
|
|
81
|
+
url: string
|
|
82
|
+
rank: number
|
|
83
|
+
product_id: string
|
|
84
|
+
metadata: Record<string, unknown> | null
|
|
85
|
+
created_at: IsoDateString
|
|
86
|
+
updated_at: IsoDateString
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface StoreProductVariant {
|
|
90
|
+
id: string
|
|
91
|
+
title: string
|
|
92
|
+
product_id: string
|
|
93
|
+
sku: string | null
|
|
94
|
+
barcode: string | null
|
|
95
|
+
ean: string | null
|
|
96
|
+
upc: string | null
|
|
97
|
+
thumbnail: string | null
|
|
98
|
+
allow_backorder: boolean
|
|
99
|
+
manage_inventory: boolean
|
|
100
|
+
variant_rank: number
|
|
101
|
+
weight: number | null
|
|
102
|
+
length: number | null
|
|
103
|
+
height: number | null
|
|
104
|
+
width: number | null
|
|
105
|
+
hs_code: string | null
|
|
106
|
+
origin_country: string | null
|
|
107
|
+
mid_code: string | null
|
|
108
|
+
material: string | null
|
|
109
|
+
metadata: Record<string, unknown> | null
|
|
110
|
+
created_at: IsoDateString
|
|
111
|
+
updated_at: IsoDateString
|
|
112
|
+
options: StoreVariantOptionValueLink[]
|
|
113
|
+
/** Base-price rows via the price-set link embed (leak rule applied). */
|
|
114
|
+
prices: StoreVariantPriceSetLink[]
|
|
115
|
+
/**
|
|
116
|
+
* Present when a pricing context was resolvable (`currency_code` or
|
|
117
|
+
* `region_id` given); null otherwise, and null when no price matches the
|
|
118
|
+
* context. Varies by customer group — never cache shared.
|
|
119
|
+
*/
|
|
120
|
+
calculated_price: CalculatedPrice | null
|
|
121
|
+
/**
|
|
122
|
+
* THE availability predicate, computed server-side (untracked or
|
|
123
|
+
* backorderable ⇒ true; otherwise kit-aware available stock > 0).
|
|
124
|
+
* Optional only for wire back-compat with platforms that predate it —
|
|
125
|
+
* when absent, components fall back to the optimistic legacy behavior.
|
|
126
|
+
*/
|
|
127
|
+
in_stock?: boolean
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export interface StoreProduct {
|
|
131
|
+
id: string
|
|
132
|
+
title: string
|
|
133
|
+
subtitle: string | null
|
|
134
|
+
description: string | null
|
|
135
|
+
handle: string
|
|
136
|
+
status: "published"
|
|
137
|
+
thumbnail: string | null
|
|
138
|
+
is_giftcard: boolean
|
|
139
|
+
discountable: boolean
|
|
140
|
+
type_id: string | null
|
|
141
|
+
external_id: string | null
|
|
142
|
+
/** The brand, as plain text on the product (product-vendor card). */
|
|
143
|
+
vendor: string | null
|
|
144
|
+
weight: number | null
|
|
145
|
+
length: number | null
|
|
146
|
+
height: number | null
|
|
147
|
+
width: number | null
|
|
148
|
+
hs_code: string | null
|
|
149
|
+
origin_country: string | null
|
|
150
|
+
mid_code: string | null
|
|
151
|
+
material: string | null
|
|
152
|
+
/** SEO override; null = fall back to `title`. */
|
|
153
|
+
seo_title: string | null
|
|
154
|
+
/** SEO override; null = fall back to the plain-text `description`. */
|
|
155
|
+
seo_description: string | null
|
|
156
|
+
metadata: Record<string, unknown> | null
|
|
157
|
+
/**
|
|
158
|
+
* The store's custom fields under their definition keys
|
|
159
|
+
* (`custom.product_label_1`), values as stored: a string for text, a
|
|
160
|
+
* number, a boolean, an array for a list, an id for a metaobject
|
|
161
|
+
* reference. Public definitions only (Settings, Custom fields switches one
|
|
162
|
+
* off); always present, `{}` when the product has none. Served by every
|
|
163
|
+
* product read since 2026-09-14 (docs/storefront/products.md).
|
|
164
|
+
*/
|
|
165
|
+
metafields: Record<string, unknown>
|
|
166
|
+
created_at: IsoDateString
|
|
167
|
+
updated_at: IsoDateString
|
|
168
|
+
variants: StoreProductVariant[]
|
|
169
|
+
images: StoreProductImage[]
|
|
170
|
+
options: StoreProductOption[]
|
|
171
|
+
/**
|
|
172
|
+
* EVERY collection the product belongs to, in the shape categories and
|
|
173
|
+
* tags already had. It was a single `collection` object resolved through
|
|
174
|
+
* products.collection_id, a column that held one of a product's
|
|
175
|
+
* collections at most and none at all for the ones a catalogue migration
|
|
176
|
+
* or a smart rule placed; the membership join is the one store since
|
|
177
|
+
* 2026-09-15 (docs/storefront/products.md).
|
|
178
|
+
*/
|
|
179
|
+
collections: Array<{ collection: { id: string; title: string; handle: string } | null }>
|
|
180
|
+
categories: Array<{ category: { id: string; name: string; handle: string } | null }>
|
|
181
|
+
tags: Array<{ tag: { id: string; value: string } | null }>
|
|
182
|
+
type: { id: string; value: string } | null
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export interface ListProductsQuery extends PricingContextQuery, PaginationQuery {
|
|
186
|
+
/** Case-insensitive substring match on title (full search: use /search). */
|
|
187
|
+
q?: string
|
|
188
|
+
/** Product ids (joined as CSV on the wire). */
|
|
189
|
+
id?: string[]
|
|
190
|
+
handle?: string
|
|
191
|
+
collection_id?: string[]
|
|
192
|
+
category_id?: string[]
|
|
193
|
+
tag_id?: string[]
|
|
194
|
+
type_id?: string[]
|
|
195
|
+
/**
|
|
196
|
+
* Sort column; prefix with `-` for descending (e.g. `-created_at`,
|
|
197
|
+
* the default).
|
|
198
|
+
*/
|
|
199
|
+
order?: string
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export interface ProductListResponse extends ListEnvelope {
|
|
203
|
+
products: StoreProduct[]
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export interface ProductResponse {
|
|
207
|
+
product: StoreProduct
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export interface ListProductVariantsQuery extends PricingContextQuery, PaginationQuery {
|
|
211
|
+
/** Variant ids as CSV in ONE string (wire format of the route). */
|
|
212
|
+
id?: string
|
|
213
|
+
product_id?: string
|
|
214
|
+
sku?: string
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export interface ProductVariantListResponse extends ListEnvelope {
|
|
218
|
+
variants: StoreProductVariant[]
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export interface ProductVariantResponse {
|
|
222
|
+
variant: StoreProductVariant
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* List published products (newest first by default).
|
|
227
|
+
*
|
|
228
|
+
* Auth: anon (`x-client-id`); optional `x-publishable-api-key` restricts the
|
|
229
|
+
* catalog to products linked to the key's sales channels (a channel-scoped
|
|
230
|
+
* key with no matching products returns an empty list). Optional Bearer JWT
|
|
231
|
+
* makes `calculated_price` customer-group-aware.
|
|
232
|
+
*
|
|
233
|
+
* Errors: 400 `missing_client_id`, 400 `validation_failed`,
|
|
234
|
+
* 400 `invalid_publishable_key`, 400 `invalid_region`.
|
|
235
|
+
* Settings: price lists (B2B pricing), sales-channel product links,
|
|
236
|
+
* publishable-key channel bindings.
|
|
237
|
+
*/
|
|
238
|
+
export async function listProducts(
|
|
239
|
+
client: StorefrontClient,
|
|
240
|
+
query?: ListProductsQuery
|
|
241
|
+
): Promise<ProductListResponse> {
|
|
242
|
+
return client.get("/api/store/products", { query: { ...query } })
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Retrieve one product by id (`prod_…`) or handle — storefronts deep-link by
|
|
247
|
+
* handle.
|
|
248
|
+
*
|
|
249
|
+
* Auth: anon (`x-client-id`); optional publishable key + Bearer JWT as on
|
|
250
|
+
* the list. Errors: 404 `not_found` (unknown, draft — RLS-hidden — or
|
|
251
|
+
* outside the key's channels: invisible, not forbidden),
|
|
252
|
+
* 400 `invalid_publishable_key`, 400 `invalid_region`.
|
|
253
|
+
*/
|
|
254
|
+
export async function retrieveProduct(
|
|
255
|
+
client: StorefrontClient,
|
|
256
|
+
idOrHandle: string,
|
|
257
|
+
query?: PricingContextQuery
|
|
258
|
+
): Promise<ProductResponse> {
|
|
259
|
+
return client.get(`/api/store/products/${encodeURIComponent(idOrHandle)}`, {
|
|
260
|
+
query: { ...query },
|
|
261
|
+
})
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* List product variants (cart-line hydration). `id` accepts a CSV of variant
|
|
266
|
+
* ids for multi-lookup. Ordered by `variant_rank`.
|
|
267
|
+
*
|
|
268
|
+
* Auth: anon (`x-client-id`); optional Bearer JWT for group-aware
|
|
269
|
+
* `calculated_price`. NOTE: this listing is NOT publishable-key
|
|
270
|
+
* channel-scoped (scope applies to product reads). Errors:
|
|
271
|
+
* 400 `missing_client_id`, 400 `validation_failed`, 400 `invalid_region`.
|
|
272
|
+
*/
|
|
273
|
+
export async function listProductVariants(
|
|
274
|
+
client: StorefrontClient,
|
|
275
|
+
query?: ListProductVariantsQuery
|
|
276
|
+
): Promise<ProductVariantListResponse> {
|
|
277
|
+
return client.get("/api/store/product-variants", { query: { ...query } })
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Retrieve one variant by id, options + base prices hydrated,
|
|
282
|
+
* `calculated_price` when a pricing context is given.
|
|
283
|
+
*
|
|
284
|
+
* Auth: anon (`x-client-id`). Errors: 404 `not_found`, 400 `invalid_region`.
|
|
285
|
+
*/
|
|
286
|
+
export async function retrieveProductVariant(
|
|
287
|
+
client: StorefrontClient,
|
|
288
|
+
variantId: string,
|
|
289
|
+
query?: PricingContextQuery
|
|
290
|
+
): Promise<ProductVariantResponse> {
|
|
291
|
+
return client.get(`/api/store/product-variants/${encodeURIComponent(variantId)}`, {
|
|
292
|
+
query: { ...query },
|
|
293
|
+
})
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** A subscription offer on a product (PDP purchase options). */
|
|
297
|
+
export interface StoreSellingPlan {
|
|
298
|
+
id: string
|
|
299
|
+
name: string
|
|
300
|
+
interval: "day" | "week" | "month" | "year"
|
|
301
|
+
interval_count: number
|
|
302
|
+
/** null = catalog price (the plan only sets the cadence). */
|
|
303
|
+
pricing_type: "percent" | "fixed" | null
|
|
304
|
+
pricing_value: number | null
|
|
305
|
+
min_cycles: number | null
|
|
306
|
+
max_cycles: number | null
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export interface SellingPlanListResponse {
|
|
310
|
+
selling_plans: StoreSellingPlan[]
|
|
311
|
+
count: number
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* The subscription plans a product can be purchased with — render as PDP
|
|
316
|
+
* purchase options (one-time vs each plan). The chosen plan id goes on the
|
|
317
|
+
* cart line (`carts.addLineItem` `selling_plan_id`); the server applies the
|
|
318
|
+
* plan price. Empty list = product is one-time only.
|
|
319
|
+
*
|
|
320
|
+
* Auth: anon (`x-client-id`); publishable-key channel scope applies (same
|
|
321
|
+
* rule as the product read). Errors: 404 `not_found`.
|
|
322
|
+
*/
|
|
323
|
+
export async function listSellingPlans(
|
|
324
|
+
client: StorefrontClient,
|
|
325
|
+
idOrHandle: string
|
|
326
|
+
): Promise<SellingPlanListResponse> {
|
|
327
|
+
return client.get(
|
|
328
|
+
`/api/store/products/${encodeURIComponent(idOrHandle)}/selling-plans`
|
|
329
|
+
)
|
|
330
|
+
}
|