@cartbase/storefront 0.17.0 → 0.18.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.
Files changed (107) hide show
  1. package/LICENSE +21 -21
  2. package/package.json +258 -256
  3. package/src/api/auth.ts +108 -108
  4. package/src/api/carts.ts +523 -523
  5. package/src/api/categories.ts +184 -184
  6. package/src/api/checkout.ts +526 -526
  7. package/src/api/collections.ts +130 -130
  8. package/src/api/consent.ts +75 -75
  9. package/src/api/content.ts +125 -125
  10. package/src/api/customers.ts +303 -303
  11. package/src/api/gift-cards.ts +112 -112
  12. package/src/api/http.ts +180 -180
  13. package/src/api/index.ts +30 -30
  14. package/src/api/menus.ts +77 -77
  15. package/src/api/metaobjects.ts +136 -136
  16. package/src/api/orders.ts +290 -290
  17. package/src/api/products.ts +43 -1
  18. package/src/api/redirects.ts +37 -37
  19. package/src/api/regions.ts +200 -200
  20. package/src/api/reviews.ts +259 -259
  21. package/src/api/search.ts +163 -163
  22. package/src/api/store.ts +35 -35
  23. package/src/api/types.ts +91 -91
  24. package/src/cart-drawer/context.tsx +277 -164
  25. package/src/cart-drawer/cross-sell-carousel.tsx +211 -211
  26. package/src/cart-drawer/cross-sell-sidebar.tsx +158 -158
  27. package/src/cart-drawer/gift-wrap.tsx +82 -82
  28. package/src/cart-drawer/index.ts +71 -68
  29. package/src/cart-drawer/item/index.tsx +162 -162
  30. package/src/cart-drawer/item/quantity.tsx +8 -14
  31. package/src/cart-drawer/item/upsell.tsx +110 -110
  32. package/src/cart-drawer/labels.ts +123 -123
  33. package/src/cart-drawer/mutation-queue.ts +78 -0
  34. package/src/cart-drawer/notes.tsx +131 -131
  35. package/src/cart-drawer/sticky-footer.tsx +73 -73
  36. package/src/cart-drawer/summary-breakdown.tsx +197 -197
  37. package/src/checkout/address-error-copy.ts +117 -117
  38. package/src/checkout/boxnow-locker-selector.tsx +410 -410
  39. package/src/checkout/compare-addresses.ts +40 -40
  40. package/src/checkout/discount-section.tsx +218 -218
  41. package/src/checkout/error-copy-codes.ts +63 -63
  42. package/src/checkout/geocode.ts +154 -154
  43. package/src/checkout/gift-card-section.tsx +224 -224
  44. package/src/checkout/index.ts +72 -72
  45. package/src/checkout/labels.ts +495 -495
  46. package/src/checkout/payment-button.tsx +372 -372
  47. package/src/checkout/payment-error-copy.ts +154 -154
  48. package/src/checkout/promotion-error-copy.ts +91 -91
  49. package/src/checkout/use-checkout-orchestration.ts +1592 -1592
  50. package/src/common/country-flag.tsx +52 -52
  51. package/src/common/country-select.tsx +11 -11
  52. package/src/common/index.ts +20 -20
  53. package/src/common/market-select.tsx +57 -57
  54. package/src/index.ts +12 -12
  55. package/src/lib/country-name.ts +59 -59
  56. package/src/lib/get-product-price.ts +133 -133
  57. package/src/lib/media-image.tsx +39 -39
  58. package/src/lib/payment-constants.ts +53 -53
  59. package/src/lib/platform.ts +13 -13
  60. package/src/lib/price.tsx +39 -39
  61. package/src/lib/store-api-error.ts +36 -36
  62. package/src/lib/variant-caption.ts +32 -32
  63. package/src/locales/bg.ts +467 -467
  64. package/src/locales/context.ts +37 -37
  65. package/src/locales/en.ts +26 -26
  66. package/src/locales/es.ts +466 -466
  67. package/src/locales/index.ts +19 -19
  68. package/src/locales/provider.tsx +59 -59
  69. package/src/locales/types.ts +77 -77
  70. package/src/order/index.ts +62 -62
  71. package/src/order/labels.ts +79 -79
  72. package/src/order/order-totals.tsx +250 -250
  73. package/src/primitives/select-field.tsx +93 -93
  74. package/src/products/image-gallery.tsx +43 -43
  75. package/src/products/index.ts +68 -59
  76. package/src/products/option-select.tsx +67 -67
  77. package/src/products/product-actions-wrapper.tsx +63 -62
  78. package/src/products/product-actions.tsx +151 -150
  79. package/src/products/product-price.tsx +69 -69
  80. package/src/products/product-promises.tsx +61 -61
  81. package/src/products/product-specs.tsx +65 -65
  82. package/src/products/product-tabs.tsx +123 -123
  83. package/src/products/product-template.tsx +163 -162
  84. package/src/products/purchase-options.tsx +130 -130
  85. package/src/products/sets.ts +110 -0
  86. package/src/products/use-product-actions.ts +199 -164
  87. package/src/products/variant-matching.ts +71 -71
  88. package/src/products/variant-url.ts +74 -74
  89. package/src/reviews-ui/index.ts +73 -73
  90. package/src/store/category-template.tsx +136 -136
  91. package/src/store/index.ts +40 -40
  92. package/src/tracking/chatgpt-pixel.tsx +99 -99
  93. package/src/tracking/consent-init.tsx +62 -62
  94. package/src/tracking/events.ts +348 -348
  95. package/src/tracking/ga4.tsx +93 -93
  96. package/src/tracking/google-ads.ts +84 -84
  97. package/src/tracking/gtm.tsx +60 -60
  98. package/src/tracking/inline-script.ts +49 -49
  99. package/src/tracking/oaiq.ts +206 -206
  100. package/src/tracking/tiktok-pixel.tsx +91 -91
  101. package/src/tracking/track-init.tsx +56 -56
  102. package/src/tracking/track-order-purchase.tsx +122 -122
  103. package/src/tracking/ttq.ts +180 -180
  104. package/src/tracking/use-tracking-config.ts +54 -54
  105. package/theme/index.css +25 -25
  106. package/theme/theme.css +150 -150
  107. package/theme/tokens.css +106 -106
package/src/api/orders.ts CHANGED
@@ -1,290 +1,290 @@
1
- /**
2
- * @cartbase/storefront/api/orders — the authenticated customer's orders.
3
- *
4
- * EVERY function here requires a customer session
5
- * (`authorization: Bearer <jwt>` via the client's `getAuthToken`) — there
6
- * is no anonymous order read; a guest's only order handle is the
7
- * `completeCart()` response. Missing/invalid JWT → 401 `unauthenticated`.
8
- *
9
- * Route ground truth: src/app/api/store/orders/**. Doc:
10
- * docs/storefront/orders.md.
11
- *
12
- * `GET /api/store/orders/display/:displayId` was contract-listed but
13
- * missing from the codebase when this module was first written; the route
14
- * was built at the batch-9 merge and `retrieveOrderByDisplayId` wraps it.
15
- */
16
- import type { StorefrontClient } from "./http"
17
- import type { IsoDateString, ListEnvelope, MajorUnitAmount } from "./types"
18
-
19
- // ---------------------------------------------------------------------------
20
- // DTOs — ground truth: orders/route.ts (list select `*`), orders/[id]/route.ts
21
- // (STORE_ORDER_DETAIL_SELECT), orders/[id]/transfer/*.
22
- // ---------------------------------------------------------------------------
23
-
24
- /** Order row as listed (plain `orders` columns, no embeds). */
25
- export interface StoreOrderSummaryRow {
26
- id: string
27
- /** Human-facing autoincrement. */
28
- display_id: number
29
- status: string
30
- email: string | null
31
- currency_code: string
32
- customer_id: string | null
33
- sales_channel_id: string | null
34
- region_id: string | null
35
- metadata: Record<string, unknown> | null
36
- created_at: IsoDateString
37
- updated_at: IsoDateString
38
- [key: string]: unknown
39
- }
40
-
41
- /** `order_line_items` row (title/prices/product snapshot at purchase time). */
42
- export interface StoreOrderLineItem {
43
- id: string
44
- title: string | null
45
- subtitle: string | null
46
- product_title: string | null
47
- product_handle: string | null
48
- thumbnail: string | null
49
- variant_id: string | null
50
- variant_title: string | null
51
- variant_sku: string | null
52
- unit_price: MajorUnitAmount
53
- is_giftcard?: boolean
54
- requires_shipping?: boolean
55
- metadata: Record<string, unknown> | null
56
- [key: string]: unknown
57
- }
58
-
59
- /** Version pivot row: quantity + the embedded line item. */
60
- export interface StoreOrderItem {
61
- id: string
62
- order_id: string
63
- quantity: number
64
- line_item: StoreOrderLineItem | null
65
- [key: string]: unknown
66
- }
67
-
68
- export interface StoreFulfillmentLabel {
69
- tracking_number: string | null
70
- tracking_url: string | null
71
- }
72
-
73
- /** Store-safe fulfillment subset: lifecycle timestamps + tracking labels. */
74
- export interface StoreOrderFulfillment {
75
- fulfillment: {
76
- id: string
77
- packed_at: IsoDateString | null
78
- shipped_at: IsoDateString | null
79
- delivered_at: IsoDateString | null
80
- canceled_at: IsoDateString | null
81
- labels: StoreFulfillmentLabel[]
82
- } | null
83
- }
84
-
85
- /** `order_addresses` row. */
86
- export interface StoreOrderAddress {
87
- id: string
88
- first_name: string | null
89
- last_name: string | null
90
- company: string | null
91
- address_1: string | null
92
- address_2: string | null
93
- city: string | null
94
- country_code: string | null
95
- province: string | null
96
- postal_code: string | null
97
- phone: string | null
98
- [key: string]: unknown
99
- }
100
-
101
- /**
102
- * Order detail — a store-safe subset of the admin select: items (with
103
- * embedded line_item), fulfillments with tracking, both addresses. No
104
- * internal joins (customer row, payment internals) and NO internal staff
105
- * notes (timeline comments never cross this surface).
106
- */
107
- export interface StoreOrderDetail extends StoreOrderSummaryRow {
108
- items: StoreOrderItem[]
109
- fulfillments: StoreOrderFulfillment[]
110
- shipping_address: StoreOrderAddress | null
111
- billing_address: StoreOrderAddress | null
112
- }
113
-
114
- export interface ListOrdersQuery {
115
- /** 1–200, default 20. */
116
- limit?: number
117
- /** Default 0. */
118
- offset?: number
119
- /** Exact-match filter on `orders.status` (e.g. `pending`, `completed`, `canceled`). */
120
- status?: string
121
- }
122
-
123
- export interface OrderListResponse extends ListEnvelope {
124
- orders: StoreOrderSummaryRow[]
125
- }
126
-
127
- export interface OrderResponse {
128
- order: StoreOrderDetail
129
- }
130
-
131
- // ---------------------------------------------------------------------------
132
- // Reads
133
- // ---------------------------------------------------------------------------
134
-
135
- /**
136
- * List MY orders (newest first). → 200 `{orders, count, offset, limit}` —
137
- * plain order rows, no embeds (fetch the detail for items/tracking).
138
- * Auth: REQUIRED Bearer JWT (+ `x-client-id`). Scoped to the session
139
- * customer — another customer's orders are invisible, not 403.
140
- * Errors: 401 `unauthenticated`, 400 `validation_failed` (bad
141
- * limit/offset).
142
- */
143
- export async function listOrders(
144
- client: StorefrontClient,
145
- query: ListOrdersQuery = {}
146
- ): Promise<OrderListResponse> {
147
- return client.get("/api/store/orders", { query: { ...query } })
148
- }
149
-
150
- /**
151
- * Retrieve MY order with items, fulfillments (+ tracking labels) and
152
- * addresses. → 200 `{order}`.
153
- * Auth: REQUIRED Bearer JWT (+ `x-client-id`).
154
- * Errors: 401 `unauthenticated`, 404 `not_found` (unknown id OR an order
155
- * belonging to another customer — ownership is part of the lookup, so
156
- * cross-customer reads are indistinguishable from missing).
157
- */
158
- export async function retrieveOrder(
159
- client: StorefrontClient,
160
- orderId: string
161
- ): Promise<OrderResponse> {
162
- return client.get(`/api/store/orders/${orderId}`)
163
- }
164
-
165
- /**
166
- * Retrieve MY order by its human number: the autoincrement `display_id`
167
- * (numeric segment) or a `custom_display_id` (any segment; a numeric
168
- * custom id wins over a colliding display_id — it is the number the store
169
- * showed the customer). Same detail embeds + ownership scope as
170
- * `retrieveOrder`.
171
- * → 200 `{order}`.
172
- * Auth: REQUIRED Bearer JWT (+ `x-client-id`).
173
- * Errors: 401 `unauthenticated`, 404 `not_found` (unknown number OR another
174
- * customer's order — indistinguishable by design).
175
- */
176
- export async function retrieveOrderByDisplayId(
177
- client: StorefrontClient,
178
- displayId: string | number
179
- ): Promise<OrderResponse> {
180
- return client.get(`/api/store/orders/display/${displayId}`)
181
- }
182
-
183
- // ---------------------------------------------------------------------------
184
- // Transfers — move a (guest) order to the authenticated customer.
185
- // Flow: the CLAIMING customer calls request → a token is issued for the
186
- // order's email holder → the claimer (whose email must match the order's)
187
- // calls accept with the token; decline/cancel end a pending transfer.
188
- // ---------------------------------------------------------------------------
189
-
190
- export interface RequestOrderTransferInput {
191
- /** Free-text shown to the recipient; stored on the transfer action. */
192
- description?: string
193
- }
194
-
195
- /**
196
- * request → `{order:{id, transfer:{requested:true}}}` — ALWAYS this shape,
197
- * for the first request and for duplicates alike. The transfer token is
198
- * email-delivered only and never crosses the store surface.
199
- */
200
- export interface RequestOrderTransferResponse {
201
- order: {
202
- id: string
203
- transfer: { requested: true }
204
- }
205
- }
206
-
207
- /**
208
- * Request the order be transferred to ME (the authenticated customer).
209
- * Idempotent: an existing pending transfer is returned, not duplicated.
210
- * → 200.
211
- * Auth: REQUIRED Bearer JWT (+ `x-client-id`).
212
- * Errors: 401 `unauthenticated`, 404 `not_found`, 400 `already_owned`
213
- * (the order already belongs to this customer).
214
- */
215
- export async function requestOrderTransfer(
216
- client: StorefrontClient,
217
- orderId: string,
218
- input: RequestOrderTransferInput = {}
219
- ): Promise<RequestOrderTransferResponse> {
220
- return client.post(`/api/store/orders/${orderId}/transfer/request`, input)
221
- }
222
-
223
- export interface CancelOrderTransferResponse {
224
- order: { id: string; transfer: { canceled: true } }
225
- }
226
-
227
- /**
228
- * Cancel MY pending transfer request (only the requester may cancel).
229
- * → 200.
230
- * Auth: REQUIRED Bearer JWT (+ `x-client-id`).
231
- * Errors: 401 `unauthenticated`, 404 `not_found` (order or no pending
232
- * transfer), 403 `forbidden` (not the requester).
233
- */
234
- export async function cancelOrderTransfer(
235
- client: StorefrontClient,
236
- orderId: string
237
- ): Promise<CancelOrderTransferResponse> {
238
- return client.post(`/api/store/orders/${orderId}/transfer/cancel`)
239
- }
240
-
241
- export interface AcceptOrderTransferInput {
242
- /** The transfer token received by email (≥16 chars). */
243
- token: string
244
- }
245
-
246
- /** accept → the full updated order row (now owned by the caller). */
247
- export interface AcceptOrderTransferResponse {
248
- order: StoreOrderSummaryRow
249
- }
250
-
251
- /**
252
- * Accept a pending transfer with the emailed token — assigns the order to
253
- * the authenticated customer. The caller's email must equal the order's
254
- * original email (a leaked token alone is not enough). → 200 `{order}`.
255
- * Auth: REQUIRED Bearer JWT (+ `x-client-id`).
256
- * Errors: 401 `unauthenticated`, 404 `not_found` (order / no pending
257
- * transfer), 403 `invalid_token` | `email_mismatch`, 400
258
- * `validation_failed`.
259
- */
260
- export async function acceptOrderTransfer(
261
- client: StorefrontClient,
262
- orderId: string,
263
- input: AcceptOrderTransferInput
264
- ): Promise<AcceptOrderTransferResponse> {
265
- return client.post(`/api/store/orders/${orderId}/transfer/accept`, input)
266
- }
267
-
268
- export interface DeclineOrderTransferInput {
269
- /** The transfer token received by email (≥16 chars). */
270
- token: string
271
- }
272
-
273
- export interface DeclineOrderTransferResponse {
274
- order: { id: string; transfer: { declined: true } }
275
- }
276
-
277
- /**
278
- * Decline a pending transfer (the order's original email holder rejects
279
- * it) — validates the token, then removes the pending action. → 200.
280
- * Auth: REQUIRED Bearer JWT (+ `x-client-id`).
281
- * Errors: 401 `unauthenticated`, 404 `not_found`, 403 `invalid_token`,
282
- * 400 `validation_failed`.
283
- */
284
- export async function declineOrderTransfer(
285
- client: StorefrontClient,
286
- orderId: string,
287
- input: DeclineOrderTransferInput
288
- ): Promise<DeclineOrderTransferResponse> {
289
- return client.post(`/api/store/orders/${orderId}/transfer/decline`, input)
290
- }
1
+ /**
2
+ * @cartbase/storefront/api/orders — the authenticated customer's orders.
3
+ *
4
+ * EVERY function here requires a customer session
5
+ * (`authorization: Bearer <jwt>` via the client's `getAuthToken`) — there
6
+ * is no anonymous order read; a guest's only order handle is the
7
+ * `completeCart()` response. Missing/invalid JWT → 401 `unauthenticated`.
8
+ *
9
+ * Route ground truth: src/app/api/store/orders/**. Doc:
10
+ * docs/storefront/orders.md.
11
+ *
12
+ * `GET /api/store/orders/display/:displayId` was contract-listed but
13
+ * missing from the codebase when this module was first written; the route
14
+ * was built at the batch-9 merge and `retrieveOrderByDisplayId` wraps it.
15
+ */
16
+ import type { StorefrontClient } from "./http"
17
+ import type { IsoDateString, ListEnvelope, MajorUnitAmount } from "./types"
18
+
19
+ // ---------------------------------------------------------------------------
20
+ // DTOs — ground truth: orders/route.ts (list select `*`), orders/[id]/route.ts
21
+ // (STORE_ORDER_DETAIL_SELECT), orders/[id]/transfer/*.
22
+ // ---------------------------------------------------------------------------
23
+
24
+ /** Order row as listed (plain `orders` columns, no embeds). */
25
+ export interface StoreOrderSummaryRow {
26
+ id: string
27
+ /** Human-facing autoincrement. */
28
+ display_id: number
29
+ status: string
30
+ email: string | null
31
+ currency_code: string
32
+ customer_id: string | null
33
+ sales_channel_id: string | null
34
+ region_id: string | null
35
+ metadata: Record<string, unknown> | null
36
+ created_at: IsoDateString
37
+ updated_at: IsoDateString
38
+ [key: string]: unknown
39
+ }
40
+
41
+ /** `order_line_items` row (title/prices/product snapshot at purchase time). */
42
+ export interface StoreOrderLineItem {
43
+ id: string
44
+ title: string | null
45
+ subtitle: string | null
46
+ product_title: string | null
47
+ product_handle: string | null
48
+ thumbnail: string | null
49
+ variant_id: string | null
50
+ variant_title: string | null
51
+ variant_sku: string | null
52
+ unit_price: MajorUnitAmount
53
+ is_giftcard?: boolean
54
+ requires_shipping?: boolean
55
+ metadata: Record<string, unknown> | null
56
+ [key: string]: unknown
57
+ }
58
+
59
+ /** Version pivot row: quantity + the embedded line item. */
60
+ export interface StoreOrderItem {
61
+ id: string
62
+ order_id: string
63
+ quantity: number
64
+ line_item: StoreOrderLineItem | null
65
+ [key: string]: unknown
66
+ }
67
+
68
+ export interface StoreFulfillmentLabel {
69
+ tracking_number: string | null
70
+ tracking_url: string | null
71
+ }
72
+
73
+ /** Store-safe fulfillment subset: lifecycle timestamps + tracking labels. */
74
+ export interface StoreOrderFulfillment {
75
+ fulfillment: {
76
+ id: string
77
+ packed_at: IsoDateString | null
78
+ shipped_at: IsoDateString | null
79
+ delivered_at: IsoDateString | null
80
+ canceled_at: IsoDateString | null
81
+ labels: StoreFulfillmentLabel[]
82
+ } | null
83
+ }
84
+
85
+ /** `order_addresses` row. */
86
+ export interface StoreOrderAddress {
87
+ id: string
88
+ first_name: string | null
89
+ last_name: string | null
90
+ company: string | null
91
+ address_1: string | null
92
+ address_2: string | null
93
+ city: string | null
94
+ country_code: string | null
95
+ province: string | null
96
+ postal_code: string | null
97
+ phone: string | null
98
+ [key: string]: unknown
99
+ }
100
+
101
+ /**
102
+ * Order detail — a store-safe subset of the admin select: items (with
103
+ * embedded line_item), fulfillments with tracking, both addresses. No
104
+ * internal joins (customer row, payment internals) and NO internal staff
105
+ * notes (timeline comments never cross this surface).
106
+ */
107
+ export interface StoreOrderDetail extends StoreOrderSummaryRow {
108
+ items: StoreOrderItem[]
109
+ fulfillments: StoreOrderFulfillment[]
110
+ shipping_address: StoreOrderAddress | null
111
+ billing_address: StoreOrderAddress | null
112
+ }
113
+
114
+ export interface ListOrdersQuery {
115
+ /** 1–200, default 20. */
116
+ limit?: number
117
+ /** Default 0. */
118
+ offset?: number
119
+ /** Exact-match filter on `orders.status` (e.g. `pending`, `completed`, `canceled`). */
120
+ status?: string
121
+ }
122
+
123
+ export interface OrderListResponse extends ListEnvelope {
124
+ orders: StoreOrderSummaryRow[]
125
+ }
126
+
127
+ export interface OrderResponse {
128
+ order: StoreOrderDetail
129
+ }
130
+
131
+ // ---------------------------------------------------------------------------
132
+ // Reads
133
+ // ---------------------------------------------------------------------------
134
+
135
+ /**
136
+ * List MY orders (newest first). → 200 `{orders, count, offset, limit}` —
137
+ * plain order rows, no embeds (fetch the detail for items/tracking).
138
+ * Auth: REQUIRED Bearer JWT (+ `x-client-id`). Scoped to the session
139
+ * customer — another customer's orders are invisible, not 403.
140
+ * Errors: 401 `unauthenticated`, 400 `validation_failed` (bad
141
+ * limit/offset).
142
+ */
143
+ export async function listOrders(
144
+ client: StorefrontClient,
145
+ query: ListOrdersQuery = {}
146
+ ): Promise<OrderListResponse> {
147
+ return client.get("/api/store/orders", { query: { ...query } })
148
+ }
149
+
150
+ /**
151
+ * Retrieve MY order with items, fulfillments (+ tracking labels) and
152
+ * addresses. → 200 `{order}`.
153
+ * Auth: REQUIRED Bearer JWT (+ `x-client-id`).
154
+ * Errors: 401 `unauthenticated`, 404 `not_found` (unknown id OR an order
155
+ * belonging to another customer — ownership is part of the lookup, so
156
+ * cross-customer reads are indistinguishable from missing).
157
+ */
158
+ export async function retrieveOrder(
159
+ client: StorefrontClient,
160
+ orderId: string
161
+ ): Promise<OrderResponse> {
162
+ return client.get(`/api/store/orders/${orderId}`)
163
+ }
164
+
165
+ /**
166
+ * Retrieve MY order by its human number: the autoincrement `display_id`
167
+ * (numeric segment) or a `custom_display_id` (any segment; a numeric
168
+ * custom id wins over a colliding display_id — it is the number the store
169
+ * showed the customer). Same detail embeds + ownership scope as
170
+ * `retrieveOrder`.
171
+ * → 200 `{order}`.
172
+ * Auth: REQUIRED Bearer JWT (+ `x-client-id`).
173
+ * Errors: 401 `unauthenticated`, 404 `not_found` (unknown number OR another
174
+ * customer's order — indistinguishable by design).
175
+ */
176
+ export async function retrieveOrderByDisplayId(
177
+ client: StorefrontClient,
178
+ displayId: string | number
179
+ ): Promise<OrderResponse> {
180
+ return client.get(`/api/store/orders/display/${displayId}`)
181
+ }
182
+
183
+ // ---------------------------------------------------------------------------
184
+ // Transfers — move a (guest) order to the authenticated customer.
185
+ // Flow: the CLAIMING customer calls request → a token is issued for the
186
+ // order's email holder → the claimer (whose email must match the order's)
187
+ // calls accept with the token; decline/cancel end a pending transfer.
188
+ // ---------------------------------------------------------------------------
189
+
190
+ export interface RequestOrderTransferInput {
191
+ /** Free-text shown to the recipient; stored on the transfer action. */
192
+ description?: string
193
+ }
194
+
195
+ /**
196
+ * request → `{order:{id, transfer:{requested:true}}}` — ALWAYS this shape,
197
+ * for the first request and for duplicates alike. The transfer token is
198
+ * email-delivered only and never crosses the store surface.
199
+ */
200
+ export interface RequestOrderTransferResponse {
201
+ order: {
202
+ id: string
203
+ transfer: { requested: true }
204
+ }
205
+ }
206
+
207
+ /**
208
+ * Request the order be transferred to ME (the authenticated customer).
209
+ * Idempotent: an existing pending transfer is returned, not duplicated.
210
+ * → 200.
211
+ * Auth: REQUIRED Bearer JWT (+ `x-client-id`).
212
+ * Errors: 401 `unauthenticated`, 404 `not_found`, 400 `already_owned`
213
+ * (the order already belongs to this customer).
214
+ */
215
+ export async function requestOrderTransfer(
216
+ client: StorefrontClient,
217
+ orderId: string,
218
+ input: RequestOrderTransferInput = {}
219
+ ): Promise<RequestOrderTransferResponse> {
220
+ return client.post(`/api/store/orders/${orderId}/transfer/request`, input)
221
+ }
222
+
223
+ export interface CancelOrderTransferResponse {
224
+ order: { id: string; transfer: { canceled: true } }
225
+ }
226
+
227
+ /**
228
+ * Cancel MY pending transfer request (only the requester may cancel).
229
+ * → 200.
230
+ * Auth: REQUIRED Bearer JWT (+ `x-client-id`).
231
+ * Errors: 401 `unauthenticated`, 404 `not_found` (order or no pending
232
+ * transfer), 403 `forbidden` (not the requester).
233
+ */
234
+ export async function cancelOrderTransfer(
235
+ client: StorefrontClient,
236
+ orderId: string
237
+ ): Promise<CancelOrderTransferResponse> {
238
+ return client.post(`/api/store/orders/${orderId}/transfer/cancel`)
239
+ }
240
+
241
+ export interface AcceptOrderTransferInput {
242
+ /** The transfer token received by email (≥16 chars). */
243
+ token: string
244
+ }
245
+
246
+ /** accept → the full updated order row (now owned by the caller). */
247
+ export interface AcceptOrderTransferResponse {
248
+ order: StoreOrderSummaryRow
249
+ }
250
+
251
+ /**
252
+ * Accept a pending transfer with the emailed token — assigns the order to
253
+ * the authenticated customer. The caller's email must equal the order's
254
+ * original email (a leaked token alone is not enough). → 200 `{order}`.
255
+ * Auth: REQUIRED Bearer JWT (+ `x-client-id`).
256
+ * Errors: 401 `unauthenticated`, 404 `not_found` (order / no pending
257
+ * transfer), 403 `invalid_token` | `email_mismatch`, 400
258
+ * `validation_failed`.
259
+ */
260
+ export async function acceptOrderTransfer(
261
+ client: StorefrontClient,
262
+ orderId: string,
263
+ input: AcceptOrderTransferInput
264
+ ): Promise<AcceptOrderTransferResponse> {
265
+ return client.post(`/api/store/orders/${orderId}/transfer/accept`, input)
266
+ }
267
+
268
+ export interface DeclineOrderTransferInput {
269
+ /** The transfer token received by email (≥16 chars). */
270
+ token: string
271
+ }
272
+
273
+ export interface DeclineOrderTransferResponse {
274
+ order: { id: string; transfer: { declined: true } }
275
+ }
276
+
277
+ /**
278
+ * Decline a pending transfer (the order's original email holder rejects
279
+ * it) — validates the token, then removes the pending action. → 200.
280
+ * Auth: REQUIRED Bearer JWT (+ `x-client-id`).
281
+ * Errors: 401 `unauthenticated`, 404 `not_found`, 403 `invalid_token`,
282
+ * 400 `validation_failed`.
283
+ */
284
+ export async function declineOrderTransfer(
285
+ client: StorefrontClient,
286
+ orderId: string,
287
+ input: DeclineOrderTransferInput
288
+ ): Promise<DeclineOrderTransferResponse> {
289
+ return client.post(`/api/store/orders/${orderId}/transfer/decline`, input)
290
+ }
@@ -125,6 +125,37 @@ export interface StoreProductVariant {
125
125
  * when absent, components fall back to the optimistic legacy behavior.
126
126
  */
127
127
  in_stock?: boolean
128
+ /**
129
+ * The variant's links to stock items, Medusa's inventory kit, present only
130
+ * when the read asked for them with `fields: SET_FIELDS`
131
+ * (`products/sets.ts`). A set's variant has one link per product inside
132
+ * it; a product sold on its own stock has one link to its own item.
133
+ */
134
+ inventory_items?: StoreInventoryItemLink[]
135
+ }
136
+
137
+ /** A variant drawing on a stock item, as a kit link names it. */
138
+ export interface StoreInventoryLinkedVariant {
139
+ id: string
140
+ product_id: string
141
+ title: string | null
142
+ sku: string | null
143
+ product: { id: string; title: string; handle: string; thumbnail: string | null } | null
144
+ }
145
+
146
+ /** One link from a variant to a stock item, with how many units it takes. */
147
+ export interface StoreInventoryItemLink {
148
+ inventory_item_id: string
149
+ required_quantity: number
150
+ inventory: {
151
+ id: string
152
+ /** Every stock item is born with its own variant's SKU. */
153
+ sku: string | null
154
+ title: string | null
155
+ thumbnail: string | null
156
+ /** Every live variant of a published product drawing on this item. */
157
+ variants: StoreInventoryLinkedVariant[]
158
+ } | null
128
159
  }
129
160
 
130
161
  export interface StoreProduct {
@@ -197,6 +228,17 @@ export interface ListProductsQuery extends PricingContextQuery, PaginationQuery
197
228
  * the default).
198
229
  */
199
230
  order?: string
231
+ /**
232
+ * Medusa's field selection. Only the kit links change the answer: pass
233
+ * `SET_FIELDS` (`products/sets.ts`) to receive `variants[].inventory_items`.
234
+ */
235
+ fields?: string
236
+ }
237
+
238
+ /** The one product read's query: the pricing context and the field selection. */
239
+ export interface RetrieveProductQuery extends PricingContextQuery {
240
+ /** As on the list: `SET_FIELDS` adds `variants[].inventory_items`. */
241
+ fields?: string
200
242
  }
201
243
 
202
244
  export interface ProductListResponse extends ListEnvelope {
@@ -254,7 +296,7 @@ export async function listProducts(
254
296
  export async function retrieveProduct(
255
297
  client: StorefrontClient,
256
298
  idOrHandle: string,
257
- query?: PricingContextQuery
299
+ query?: RetrieveProductQuery
258
300
  ): Promise<ProductResponse> {
259
301
  return client.get(`/api/store/products/${encodeURIComponent(idOrHandle)}`, {
260
302
  query: { ...query },