@shoppexio/storefront-react 0.0.0 → 0.1.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/CHANGELOG.md +20 -0
- package/LICENSE +21 -0
- package/README.md +137 -0
- package/dist/account.css +1171 -0
- package/dist/account.d.ts +23 -0
- package/dist/account.js +12685 -0
- package/dist/account.js.map +1 -0
- package/dist/chunk-D6XKDVAV.js +181 -0
- package/dist/chunk-D6XKDVAV.js.map +1 -0
- package/dist/index.d.ts +3087 -0
- package/dist/index.js +2801 -0
- package/dist/index.js.map +1 -0
- package/eject/account/account-availability.ts +171 -0
- package/eject/account/account-downloads.ts +118 -0
- package/eject/account/account-favorites.ts +99 -0
- package/eject/account/account-loyalty.ts +241 -0
- package/eject/account/account-page.tsx +410 -0
- package/eject/account/account-payment-method.ts +99 -0
- package/eject/account/account-referrals.ts +68 -0
- package/eject/account/account-reseller-invite.ts +88 -0
- package/eject/account/account-reseller.ts +685 -0
- package/eject/account/account-rewards.ts +104 -0
- package/eject/account/account-settings.ts +181 -0
- package/eject/account/account-signin-link.ts +122 -0
- package/eject/account/account-subscriptions.ts +130 -0
- package/eject/account/account-support.ts +155 -0
- package/eject/account/account-warranties.ts +118 -0
- package/eject/account/customer-account.ts +93 -0
- package/eject/account/dashboard.tsx +139 -0
- package/eject/account/downloads.tsx +343 -0
- package/eject/account/favorites.tsx +201 -0
- package/eject/account/i18n.ts +175 -0
- package/eject/account/icons.tsx +218 -0
- package/eject/account/login.tsx +253 -0
- package/eject/account/messages/ar.json +464 -0
- package/eject/account/messages/de.json +464 -0
- package/eject/account/messages/en.json +464 -0
- package/eject/account/messages/es.json +464 -0
- package/eject/account/messages/fi.json +464 -0
- package/eject/account/messages/fr.json +464 -0
- package/eject/account/messages/hi.json +464 -0
- package/eject/account/messages/it.json +464 -0
- package/eject/account/messages/lt.json +464 -0
- package/eject/account/messages/nl.json +464 -0
- package/eject/account/messages/pl.json +464 -0
- package/eject/account/messages/pt.json +464 -0
- package/eject/account/messages/ru.json +464 -0
- package/eject/account/messages/tr.json +464 -0
- package/eject/account/messages/uk.json +464 -0
- package/eject/account/messages/zh.json +464 -0
- package/eject/account/orders.tsx +791 -0
- package/eject/account/overview.tsx +147 -0
- package/eject/account/referrals.tsx +327 -0
- package/eject/account/reseller.tsx +1365 -0
- package/eject/account/rewards.tsx +641 -0
- package/eject/account/settings.tsx +699 -0
- package/eject/account/shared.tsx +398 -0
- package/eject/account/subscriptions.tsx +421 -0
- package/eject/account/support.tsx +453 -0
- package/package.json +75 -1
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,3087 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Which account surface a URL names.
|
|
6
|
+
*
|
|
7
|
+
* The account lives under `/dashboard`, and the panel is a PATH SEGMENT:
|
|
8
|
+
* `/dashboard/orders`, `/dashboard/orders/<uniqid>`, `/dashboard/settings`.
|
|
9
|
+
* `/dashboard` itself is the overview. This is the platform's route shape and it
|
|
10
|
+
* is identical on both storefront lanes (ADR-0064), so a theme that invents its
|
|
11
|
+
* own account paths breaks links it does not own.
|
|
12
|
+
*
|
|
13
|
+
* The older query spelling — `?tab=<tab>` and `?order=<id>` — is still ACCEPTED,
|
|
14
|
+
* because it sits in order emails already sent and in the platform's post-login
|
|
15
|
+
* redirect. It is read as input and never written: a URL carrying it is
|
|
16
|
+
* canonicalized to the path form, by replacement rather than a push, so the back
|
|
17
|
+
* button does not bounce the buyer between the two spellings. The path always
|
|
18
|
+
* wins when a URL carries both.
|
|
19
|
+
*
|
|
20
|
+
* `/customer-portal` is the oldest spelling of the same route and folds in with
|
|
21
|
+
* its tail and query intact.
|
|
22
|
+
*
|
|
23
|
+
* A locale prefix (`/de/dashboard/orders`) is part of the served URL on a
|
|
24
|
+
* localized storefront. The router's basename normally strips it before any of
|
|
25
|
+
* this sees it, so the parser accepts a pathname with or without one and
|
|
26
|
+
* canonicalization keeps whatever prefix it was given.
|
|
27
|
+
*
|
|
28
|
+
* Everything here is pure string work, so `tests/account-route.test.ts` can pin
|
|
29
|
+
* the parts email links depend on.
|
|
30
|
+
*/
|
|
31
|
+
declare const ACCOUNT_PATH = "/dashboard";
|
|
32
|
+
/** The older spelling of the same route. Folded, never served separately. */
|
|
33
|
+
declare const ACCOUNT_PATH_ALIAS = "/customer-portal";
|
|
34
|
+
/**
|
|
35
|
+
* Every tab the Shoppex account surface defines, in navigation order.
|
|
36
|
+
*
|
|
37
|
+
* Listed in full so a link to a real platform tab can be told apart from a typo
|
|
38
|
+
* and answered differently.
|
|
39
|
+
*/
|
|
40
|
+
declare const ACCOUNT_TABS: readonly ["overview", "orders", "downloads", "subscriptions", "rewards", "referrals", "favorites", "support", "settings", "reseller"];
|
|
41
|
+
type AccountTab = (typeof ACCOUNT_TABS)[number];
|
|
42
|
+
/**
|
|
43
|
+
* The catalog key a tab's label lives under.
|
|
44
|
+
*
|
|
45
|
+
* A key rather than a string: this file is imported by the router, the
|
|
46
|
+
* navigation and the "not part of this store" notice, and each of them needs
|
|
47
|
+
* the label in the locale the edge served — see `account/i18n.ts`.
|
|
48
|
+
*/
|
|
49
|
+
declare function accountTabLabelKey(tab: AccountTab): string;
|
|
50
|
+
/**
|
|
51
|
+
* The tabs the account renders.
|
|
52
|
+
*
|
|
53
|
+
* All ten platform tabs. Adding a panel means adding its tab here AND
|
|
54
|
+
* rendering it, in the same change: a tab listed without a panel would navigate
|
|
55
|
+
* to the "not built" notice, which is worse than not offering it.
|
|
56
|
+
*
|
|
57
|
+
* Reseller payloads are locally narrowed because shared contracts do not cover
|
|
58
|
+
* them, but the worker now allowlists their SDK routes.
|
|
59
|
+
*/
|
|
60
|
+
declare const IMPLEMENTED_ACCOUNT_TABS: readonly ["overview", "orders", "downloads", "subscriptions", "rewards", "referrals", "favorites", "support", "settings", "reseller"];
|
|
61
|
+
type ImplementedAccountTab = (typeof IMPLEMENTED_ACCOUNT_TABS)[number];
|
|
62
|
+
declare const DEFAULT_ACCOUNT_TAB: ImplementedAccountTab;
|
|
63
|
+
/**
|
|
64
|
+
* What the account page should show.
|
|
65
|
+
*
|
|
66
|
+
* `not-built` and `unknown-tab` are separate on purpose. The first is a real
|
|
67
|
+
* platform tab this storefront chose not to render, the second is a value that
|
|
68
|
+
* names no tab at all. Collapsing them into "show the overview instead" would
|
|
69
|
+
* hide a broken link behind a page that looks like it worked.
|
|
70
|
+
*/
|
|
71
|
+
type AccountView = {
|
|
72
|
+
kind: 'panel';
|
|
73
|
+
tab: ImplementedAccountTab;
|
|
74
|
+
} | {
|
|
75
|
+
kind: 'order';
|
|
76
|
+
orderId: string;
|
|
77
|
+
} | {
|
|
78
|
+
kind: 'not-built';
|
|
79
|
+
tab: AccountTab;
|
|
80
|
+
} | {
|
|
81
|
+
kind: 'unknown-tab';
|
|
82
|
+
};
|
|
83
|
+
declare function isAccountTab(value: string): value is AccountTab;
|
|
84
|
+
declare function isImplementedAccountTab(value: string): value is ImplementedAccountTab;
|
|
85
|
+
interface AccountRouteParts {
|
|
86
|
+
/** What preceded `/dashboard`: '' or a locale prefix such as '/de'. */
|
|
87
|
+
prefix: string;
|
|
88
|
+
/** Whether the URL used the retired `/customer-portal` spelling. */
|
|
89
|
+
alias: boolean;
|
|
90
|
+
/** The segments after `/dashboard`, decoded. */
|
|
91
|
+
tail: string[];
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Split an account URL into its prefix and the segments below `/dashboard`.
|
|
95
|
+
*
|
|
96
|
+
* Null when the path is not the account route at all. The account segment is
|
|
97
|
+
* only recognised first or directly after a locale segment — searching for it
|
|
98
|
+
* anywhere would make `/product/dashboard-pro` an account URL.
|
|
99
|
+
*/
|
|
100
|
+
declare function parseAccountPath(pathname: string): AccountRouteParts | null;
|
|
101
|
+
/**
|
|
102
|
+
* What to render for a URL.
|
|
103
|
+
*
|
|
104
|
+
* The path is read first and the query only when the path names no panel, so a
|
|
105
|
+
* canonical URL is never reinterpreted by a stale parameter riding along on it.
|
|
106
|
+
*/
|
|
107
|
+
declare function resolveAccountView(pathname: string, search: URLSearchParams): AccountView;
|
|
108
|
+
/** `/dashboard/<tab>`. The overview is the bare path, not `/dashboard/overview`. */
|
|
109
|
+
declare function accountTabHref(tab: AccountTab): string;
|
|
110
|
+
/** `/dashboard/orders/<id>`, the one order-detail spelling. */
|
|
111
|
+
declare function accountOrderHref(orderId: string): string;
|
|
112
|
+
/**
|
|
113
|
+
* The canonical path for a URL that is not written the canonical way, or null
|
|
114
|
+
* when it already is.
|
|
115
|
+
*
|
|
116
|
+
* Three URLs need rewriting: the `/customer-portal` alias, a `?tab=`/`?order=`
|
|
117
|
+
* query, and the redundant `/dashboard/overview`. The caller replaces rather
|
|
118
|
+
* than pushes — a push would put the retired spelling in the history and send
|
|
119
|
+
* the back button straight back into it.
|
|
120
|
+
*
|
|
121
|
+
* A URL naming nothing real (`?tab=orderz`, `/dashboard/nope`) has no canonical
|
|
122
|
+
* form and keeps the address it came with: rewriting it to the overview would
|
|
123
|
+
* turn a broken link into one that looks like it worked.
|
|
124
|
+
*
|
|
125
|
+
* The prefix the URL came with is preserved, so a buyer on `/de/dashboard` is
|
|
126
|
+
* not quietly moved out of their locale.
|
|
127
|
+
*/
|
|
128
|
+
declare function canonicalAccountPath(pathname: string, search: URLSearchParams): string | null;
|
|
129
|
+
/**
|
|
130
|
+
* The storefront routes the account links to outside itself: the catalog
|
|
131
|
+
* behind every "browse the store" action, and a product page per favorite.
|
|
132
|
+
* React Router does not check a `to`, so a host that does not route one of
|
|
133
|
+
* these answers the account's own buttons with its not-found page. A host
|
|
134
|
+
* checks its router against this list (ForkMe: `tests/account-routes.test.ts`).
|
|
135
|
+
*/
|
|
136
|
+
declare const ACCOUNT_STOREFRONT_ROUTES: readonly ["/all-products", "/product/:slug"];
|
|
137
|
+
/**
|
|
138
|
+
* The artifact capabilities the account relies on. A storefront that mounts
|
|
139
|
+
* the account lists each of them in its `public/shoppex.capabilities.json`;
|
|
140
|
+
* the storefront worker reads that file from the artifact and only then hands
|
|
141
|
+
* the account what the capability promises. `account-reseller-invite-cookie`
|
|
142
|
+
* is pinned to `RESELLER_INVITE_ARTIFACT_CAPABILITY` in
|
|
143
|
+
* `workers/storefront/src/reseller-invite-link.ts`: without it the worker
|
|
144
|
+
* leaves a reseller invitation in the URL for that build.
|
|
145
|
+
*/
|
|
146
|
+
declare const ACCOUNT_ARTIFACT_CAPABILITIES: readonly ["account-reseller-invite-cookie"];
|
|
147
|
+
|
|
148
|
+
declare const ACCOUNT_ROUTE_MESSAGE_KEYS: readonly ["account.checkingSession", "common.notLoaded", "common.genericError", "common.retry"];
|
|
149
|
+
type AccountRouteMessageKey = (typeof ACCOUNT_ROUTE_MESSAGE_KEYS)[number];
|
|
150
|
+
declare function accountRouteMessage(key: AccountRouteMessageKey, lang?: string | null): string;
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Type surface of the Shoppex Storefront SDK global (window.shoppex), loaded
|
|
154
|
+
* as a UMD script from cdn.shoppex.io in index.html.
|
|
155
|
+
*
|
|
156
|
+
* This declares the whole customer surface the SDK exports in addition to the
|
|
157
|
+
* storefront subset this theme consumes today: merchants fork this template,
|
|
158
|
+
* and a runtime method must not be hidden by its local types. The full API is
|
|
159
|
+
* documented at https://docs.shoppex.io/storefront-sdk. Keep field names in
|
|
160
|
+
* sync with the SDK's public response contracts (snake_case).
|
|
161
|
+
*/
|
|
162
|
+
/** One addon selection on a cart line. Part of the line's identity. */
|
|
163
|
+
interface SdkCartAddon {
|
|
164
|
+
id: string;
|
|
165
|
+
option_id?: string;
|
|
166
|
+
quantity?: number;
|
|
167
|
+
}
|
|
168
|
+
interface SdkCartItem {
|
|
169
|
+
line_id: string;
|
|
170
|
+
product_id: string;
|
|
171
|
+
variant_id: string;
|
|
172
|
+
quantity: number;
|
|
173
|
+
price_variant_id?: string | null;
|
|
174
|
+
addons?: SdkCartAddon[];
|
|
175
|
+
custom_fields?: Record<string, string>;
|
|
176
|
+
price_data?: {
|
|
177
|
+
unit_price: number;
|
|
178
|
+
};
|
|
179
|
+
pay_what_you_want_price?: number;
|
|
180
|
+
title?: string;
|
|
181
|
+
variant_title?: string;
|
|
182
|
+
image_url?: string;
|
|
183
|
+
addon_labels?: string[];
|
|
184
|
+
max_quantity?: number;
|
|
185
|
+
min_quantity?: number;
|
|
186
|
+
}
|
|
187
|
+
interface SdkCartAppliedDiscount {
|
|
188
|
+
source: 'PRODUCT_DISCOUNT' | 'VOLUME_DISCOUNT' | 'AUTOMATIC_PRODUCT_DEAL' | 'BUNDLE_DEAL' | 'STORE_PROMOTION' | 'COUPON' | 'AFFILIATE';
|
|
189
|
+
label: string;
|
|
190
|
+
amount: string;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* One cart line as the SERVER resolved it — the authoritative bounds and
|
|
194
|
+
* sellability for the option that line names.
|
|
195
|
+
*
|
|
196
|
+
* PAYMENT PATH, and a contract with rules that cannot be guessed from the
|
|
197
|
+
* shape. `min_quantity` is OMITTED when it is 1; `max_quantity` is OMITTED when
|
|
198
|
+
* nothing bounds the line (`-1` is never emitted, so a missing key means
|
|
199
|
+
* UNBOUNDED and must never be read as 0). When several cart lines name one
|
|
200
|
+
* product+option they draw from one shelf and the checkout sums them, so
|
|
201
|
+
* `max_quantity` is that line's SHARE of the option's ceiling. `sellable: false`
|
|
202
|
+
* means this cart cannot be ordered as it stands — the option is unorderable,
|
|
203
|
+
* or the cart's lines of it together exceed its ceiling — and it answers for the
|
|
204
|
+
* OPTION, so every line of one option carries the same value.
|
|
205
|
+
*
|
|
206
|
+
* The consumer duty is in `cart-quote-bounds.ts`: REPLACE the stored
|
|
207
|
+
* bounds rather than merging them, and refuse the hand-off while any line is
|
|
208
|
+
* unsellable.
|
|
209
|
+
*/
|
|
210
|
+
interface SdkCartQuoteLine {
|
|
211
|
+
product_id: string;
|
|
212
|
+
/** The resolved option id, or null for a product with no variants. */
|
|
213
|
+
variant_id: string | null;
|
|
214
|
+
/** The quantity this row was quoted AT, after per-line summing. */
|
|
215
|
+
quantity: number;
|
|
216
|
+
/** Absent when 1. */
|
|
217
|
+
min_quantity?: number;
|
|
218
|
+
/** Absent when unbounded. Never `-1`. */
|
|
219
|
+
max_quantity?: number;
|
|
220
|
+
sellable: boolean;
|
|
221
|
+
}
|
|
222
|
+
interface SdkEligibleCartDeal {
|
|
223
|
+
deal_uniqid: string;
|
|
224
|
+
product_id: string;
|
|
225
|
+
label: string;
|
|
226
|
+
missing_quantity: number;
|
|
227
|
+
required_quantity: number;
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* How the backend classified an accepted cart code. The buyer types ONE field;
|
|
231
|
+
* the server decides whether that string is a coupon or an affiliate referral
|
|
232
|
+
* (`CartQuoteService`), and the two are paid out completely differently — a
|
|
233
|
+
* referral submitted as a coupon credits nobody.
|
|
234
|
+
*/
|
|
235
|
+
type SdkCartCodeSource = 'COUPON' | 'AFFILIATE';
|
|
236
|
+
interface SdkCartQuote {
|
|
237
|
+
subtotal: string;
|
|
238
|
+
discount: string;
|
|
239
|
+
total: string;
|
|
240
|
+
currency: string;
|
|
241
|
+
coupon_applied: boolean;
|
|
242
|
+
/**
|
|
243
|
+
* Which kind of code the quote accepted; null when it accepted none. The
|
|
244
|
+
* theme must not classify a code itself — only the server knows which of the
|
|
245
|
+
* merchant's coupons and which affiliates exist.
|
|
246
|
+
*/
|
|
247
|
+
coupon_source: SdkCartCodeSource | null;
|
|
248
|
+
applied_discounts: SdkCartAppliedDiscount[];
|
|
249
|
+
/**
|
|
250
|
+
* Per-line effective bounds and sellability — see {@link SdkCartQuoteLine}.
|
|
251
|
+
* Absent on a backend older than the contract; a consumer must then keep the
|
|
252
|
+
* add-time snapshot rather than invent bounds of its own.
|
|
253
|
+
*/
|
|
254
|
+
lines?: SdkCartQuoteLine[];
|
|
255
|
+
/**
|
|
256
|
+
* TRUE when this cart's total already exceeds the shop's configured checkout
|
|
257
|
+
* maximum. OMITTED when the cart is within it.
|
|
258
|
+
*
|
|
259
|
+
* PAYMENT PATH, and advisory by design: the quote still prices and returns
|
|
260
|
+
* the cart so the storefront can warn the buyer while they are still looking
|
|
261
|
+
* at it, instead of letting them spend a click on a
|
|
262
|
+
* `cart_maximum_checkout_exceeded` rejection that explains nothing.
|
|
263
|
+
* `/from-cart` refuses authoritatively either way — this flag never
|
|
264
|
+
* AUTHORISES a checkout, it only ever stops one.
|
|
265
|
+
*/
|
|
266
|
+
cart_max_exceeded?: true;
|
|
267
|
+
/**
|
|
268
|
+
* TRUE when at least one line is a PHYSICAL product. `total` then EXCLUDES
|
|
269
|
+
* shipping: the hosted checkout prices it from the buyer's address. Absent on
|
|
270
|
+
* a backend without physical products, so read it as `=== true`.
|
|
271
|
+
*/
|
|
272
|
+
requires_shipping?: boolean;
|
|
273
|
+
/**
|
|
274
|
+
* The configured maximum, in this quote's currency. Present only alongside
|
|
275
|
+
* `cart_max_exceeded`, so it is read only when that flag is set.
|
|
276
|
+
*/
|
|
277
|
+
cart_maximum_checkout?: string;
|
|
278
|
+
eligible_deals: SdkEligibleCartDeal[];
|
|
279
|
+
eligible_bundle_deals: Array<{
|
|
280
|
+
bundle_uniqid: string;
|
|
281
|
+
label: string;
|
|
282
|
+
/**
|
|
283
|
+
* The members the cart is still short of. `price` and `default_variant_id`
|
|
284
|
+
* are what let the teaser's Add button cart them with a real price
|
|
285
|
+
* snapshot — the storefront payload carries no catalog entry for a product
|
|
286
|
+
* the buyer has not opened, so this is the only price available here.
|
|
287
|
+
*/
|
|
288
|
+
missing_products: Array<{
|
|
289
|
+
product_uniqid: string;
|
|
290
|
+
title: string;
|
|
291
|
+
quantity: number;
|
|
292
|
+
price: string;
|
|
293
|
+
default_variant_id: string;
|
|
294
|
+
}>;
|
|
295
|
+
}>;
|
|
296
|
+
}
|
|
297
|
+
interface SdkCartAddOptions {
|
|
298
|
+
price_variant_id?: string | null;
|
|
299
|
+
/** Identity fields — omitting one on a rebuild changes which line it is. */
|
|
300
|
+
addons?: SdkCartAddon[];
|
|
301
|
+
custom_fields?: Record<string, string>;
|
|
302
|
+
price_data?: {
|
|
303
|
+
unit_price: number;
|
|
304
|
+
};
|
|
305
|
+
pay_what_you_want_price?: number;
|
|
306
|
+
/** Display snapshot fields shown in the cart without a product lookup. */
|
|
307
|
+
title?: string;
|
|
308
|
+
variant_title?: string;
|
|
309
|
+
image_url?: string;
|
|
310
|
+
addon_labels?: string[];
|
|
311
|
+
max_quantity?: number;
|
|
312
|
+
min_quantity?: number;
|
|
313
|
+
replace_display_snapshot?: boolean;
|
|
314
|
+
}
|
|
315
|
+
interface SdkPage {
|
|
316
|
+
id: string;
|
|
317
|
+
slug: string;
|
|
318
|
+
name: string;
|
|
319
|
+
/**
|
|
320
|
+
* The merchant's body as MARKDOWN, not HTML — the dashboard's Pages editor
|
|
321
|
+
* stores it that way and every lane renders it through a markdown pass
|
|
322
|
+
* (`renderCmsPageMarkdownHtml` on the Easy lane). Null when the page has no
|
|
323
|
+
* body yet.
|
|
324
|
+
*/
|
|
325
|
+
content: string | null;
|
|
326
|
+
/** 'faq' switches the page to the accordion; anything else renders the body. */
|
|
327
|
+
template_type?: string;
|
|
328
|
+
is_system?: boolean;
|
|
329
|
+
/** Template-specific payload. `faq_items` lives here for `template_type: 'faq'`. */
|
|
330
|
+
settings?: Record<string, unknown> | null;
|
|
331
|
+
}
|
|
332
|
+
interface SdkMenuItem {
|
|
333
|
+
id: string;
|
|
334
|
+
title: string;
|
|
335
|
+
/** Absent on a PAGE item — `page_slug` carries the destination instead. */
|
|
336
|
+
url?: string | null;
|
|
337
|
+
/**
|
|
338
|
+
* Set when the item points at one of the merchant's CMS pages. It wins over
|
|
339
|
+
* `url`, and resolves to `/page/<slug>` in the same tab.
|
|
340
|
+
*/
|
|
341
|
+
page_slug?: string | null;
|
|
342
|
+
link_type?: string | null;
|
|
343
|
+
target?: string | null;
|
|
344
|
+
sort_order?: number;
|
|
345
|
+
}
|
|
346
|
+
interface SdkMenu {
|
|
347
|
+
id: string;
|
|
348
|
+
title: string;
|
|
349
|
+
/** 'header' | 'footer' | custom slot name. */
|
|
350
|
+
slot?: string | null;
|
|
351
|
+
item_count?: number | null;
|
|
352
|
+
items?: SdkMenuItem[];
|
|
353
|
+
}
|
|
354
|
+
interface SdkCheckoutResult {
|
|
355
|
+
success: boolean;
|
|
356
|
+
message?: string;
|
|
357
|
+
/**
|
|
358
|
+
* The backend's machine-readable refusal reason, present only on failures
|
|
359
|
+
* that name one. It is the server's FULL i18n key —
|
|
360
|
+
* `errors.checkout.affiliate_code_not_applicable`,
|
|
361
|
+
* `errors.checkout.coupon_no_longer_valid`,
|
|
362
|
+
* `errors.checkout.price_increased_since_quote`, … — because `from-cart`
|
|
363
|
+
* sends exactly that as `error_code` and the SDK copies it verbatim. The
|
|
364
|
+
* short reason names in the server's own code never cross the wire.
|
|
365
|
+
*
|
|
366
|
+
* The quote-proof family — `errors.checkout.quote_token_invalid`,
|
|
367
|
+
* `…_expired`, `…_stale`, and `errors.checkout.price_increased_since_quote`
|
|
368
|
+
* — means the proof this hand-off submitted could not be honoured (not ours,
|
|
369
|
+
* past its window, priced for a cart that has since moved, or the ceiling
|
|
370
|
+
* refusing an invoice that would price ABOVE the approved total). They are
|
|
371
|
+
* all cleared the same way: re-quote and let the buyer approve the current
|
|
372
|
+
* total before retrying.
|
|
373
|
+
*
|
|
374
|
+
* `message` is already localized prose meant for the buyer; it is NOT a
|
|
375
|
+
* branchable value — matching on its text breaks the moment the copy or the
|
|
376
|
+
* locale changes. Branch on this instead, and only on this.
|
|
377
|
+
*/
|
|
378
|
+
code?: string;
|
|
379
|
+
redirectUrl?: string;
|
|
380
|
+
invoiceId?: string;
|
|
381
|
+
}
|
|
382
|
+
interface SdkRecentSaleEntry {
|
|
383
|
+
product_title: string;
|
|
384
|
+
seconds_ago: number;
|
|
385
|
+
}
|
|
386
|
+
interface SdkInitOptions {
|
|
387
|
+
shopId?: string;
|
|
388
|
+
apiBaseUrl?: string;
|
|
389
|
+
checkoutBaseUrl?: string;
|
|
390
|
+
currency?: string;
|
|
391
|
+
locale?: string;
|
|
392
|
+
}
|
|
393
|
+
/** The envelope every customer-account call answers with. */
|
|
394
|
+
interface SdkResponse<T> {
|
|
395
|
+
success: boolean;
|
|
396
|
+
data?: T;
|
|
397
|
+
/** Buyer-safe sentence from the server. Display copy, never a branch value. */
|
|
398
|
+
message?: string;
|
|
399
|
+
/**
|
|
400
|
+
* The TRANSPORT status, absent when the request never reached a server.
|
|
401
|
+
*
|
|
402
|
+
* This is the field that separates "the buyer is signed out" (401) from "the
|
|
403
|
+
* API is broken" (5xx) or "the request never left the browser" (undefined).
|
|
404
|
+
* Branch on it before deciding a buyer has no session: signing someone out
|
|
405
|
+
* because an endpoint returned 500 throws away a session that is still valid.
|
|
406
|
+
*/
|
|
407
|
+
status?: number;
|
|
408
|
+
/** Machine-readable refusal identifier, when the server emitted one. */
|
|
409
|
+
code?: string;
|
|
410
|
+
errorParams?: Record<string, unknown>;
|
|
411
|
+
}
|
|
412
|
+
/** One order row inside the dashboard payload. snake_case, unlike the list. */
|
|
413
|
+
interface SdkCustomerDashboardOrder {
|
|
414
|
+
uniqid: string;
|
|
415
|
+
status: string;
|
|
416
|
+
/**
|
|
417
|
+
* USD-normalized total. `currency` is the BUYER display currency, so this
|
|
418
|
+
* number and that currency are not a pair — see `total_display`.
|
|
419
|
+
*/
|
|
420
|
+
total: string;
|
|
421
|
+
/** The total in `currency`. Null when the invoice has no display amount. */
|
|
422
|
+
total_display: string | null;
|
|
423
|
+
currency: string;
|
|
424
|
+
created_at: string;
|
|
425
|
+
line_items: Array<{
|
|
426
|
+
product_title: string | null;
|
|
427
|
+
quantity: number;
|
|
428
|
+
}>;
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* One row of the buyer's purchased licenses.
|
|
432
|
+
*
|
|
433
|
+
* `expires_at` is EPOCH SECONDS, not milliseconds — the producer emits
|
|
434
|
+
* `EXTRACT(EPOCH FROM ...)::bigint` — and `null` means the license has no end
|
|
435
|
+
* date at all, which is a different thing from an expiry the client failed to
|
|
436
|
+
* read. `invoice_id` carries the ORDER's `uniqid`, so it addresses the same
|
|
437
|
+
* order-detail route as an invoice row.
|
|
438
|
+
*
|
|
439
|
+
* `renewable` is decided server-side: only the backend can see whether the
|
|
440
|
+
* product still exists, still sells and still licenses. A storefront must never
|
|
441
|
+
* derive it.
|
|
442
|
+
*/
|
|
443
|
+
interface SdkCustomerLicense {
|
|
444
|
+
uniqid: string;
|
|
445
|
+
invoice_id: string;
|
|
446
|
+
status: string;
|
|
447
|
+
hardware_id: string | null;
|
|
448
|
+
expires_at: number | null;
|
|
449
|
+
renewable: boolean;
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* One thing the shop has delivered: a license key, a file, or a code/link.
|
|
453
|
+
*
|
|
454
|
+
* A `license` deliverable's `id` is `license:<license uniqid>` and that is the
|
|
455
|
+
* only join between this list and {@link SdkCustomerLicense} — the license row
|
|
456
|
+
* carries the expiry, the hardware lock and the renewal flag, the deliverable
|
|
457
|
+
* carries the key itself.
|
|
458
|
+
*
|
|
459
|
+
* `href` on a `file` is a same-origin `/api/customer/invoice/:uniqid/download`
|
|
460
|
+
* path. The session cookie authorizes it, so it is followed as a plain link;
|
|
461
|
+
* nothing here attaches a token.
|
|
462
|
+
*/
|
|
463
|
+
interface SdkCustomerDeliverable {
|
|
464
|
+
id: string;
|
|
465
|
+
invoice_uniqid: string;
|
|
466
|
+
product_title: string;
|
|
467
|
+
kind: 'file' | 'serial' | 'license';
|
|
468
|
+
label: string;
|
|
469
|
+
value: string;
|
|
470
|
+
href: string | null;
|
|
471
|
+
created_at: string;
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* One subscription, with the actions the SERVER says are possible on it.
|
|
475
|
+
*
|
|
476
|
+
* `actions` is not advice: pause and resume are Stripe-only and status-bound,
|
|
477
|
+
* and a storefront that renders a control the flag denies is offering a button
|
|
478
|
+
* the backend will refuse. There is deliberately no payment-method action here
|
|
479
|
+
* — that endpoint hands out a signed redirect into the provider's portal and is
|
|
480
|
+
* not open to storefront code (ADR-0064).
|
|
481
|
+
*/
|
|
482
|
+
interface SdkCustomerSubscription {
|
|
483
|
+
uniqid: string;
|
|
484
|
+
status: string;
|
|
485
|
+
gateway: string;
|
|
486
|
+
current_period_end: string;
|
|
487
|
+
trial_end: string | null;
|
|
488
|
+
cancel_at_period_end: boolean;
|
|
489
|
+
canceled_at: string | null;
|
|
490
|
+
created_at: string;
|
|
491
|
+
plan: {
|
|
492
|
+
name: string;
|
|
493
|
+
price: string;
|
|
494
|
+
currency: string;
|
|
495
|
+
/** 'day' | 'week' | 'month' | 'year' as the gateway spells it. */
|
|
496
|
+
interval: string;
|
|
497
|
+
interval_count: number;
|
|
498
|
+
} | null;
|
|
499
|
+
payment_method: {
|
|
500
|
+
type: string;
|
|
501
|
+
card_brand: string | null;
|
|
502
|
+
card_last4: string | null;
|
|
503
|
+
card_exp_month: number | null;
|
|
504
|
+
card_exp_year: number | null;
|
|
505
|
+
} | null;
|
|
506
|
+
actions: {
|
|
507
|
+
can_pause: boolean;
|
|
508
|
+
can_resume: boolean;
|
|
509
|
+
can_cancel: boolean;
|
|
510
|
+
can_manage_payment_method: boolean;
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
/** One earned reward. `created_at` and `expires_at` are epoch SECONDS. */
|
|
514
|
+
interface SdkCustomerRewardActivity {
|
|
515
|
+
id: string;
|
|
516
|
+
type: string;
|
|
517
|
+
status: string;
|
|
518
|
+
amount: string | null;
|
|
519
|
+
currency: string | null;
|
|
520
|
+
coupon_code: string | null;
|
|
521
|
+
reason: string;
|
|
522
|
+
created_at: number;
|
|
523
|
+
expires_at: number | null;
|
|
524
|
+
}
|
|
525
|
+
interface SdkCustomerRewards {
|
|
526
|
+
summary: {
|
|
527
|
+
available: string;
|
|
528
|
+
pending: string;
|
|
529
|
+
lifetime_earned: string;
|
|
530
|
+
currency: string;
|
|
531
|
+
};
|
|
532
|
+
activity: SdkCustomerRewardActivity[];
|
|
533
|
+
}
|
|
534
|
+
interface SdkCustomerAffiliateLink {
|
|
535
|
+
id: string;
|
|
536
|
+
label: string | null;
|
|
537
|
+
code: string;
|
|
538
|
+
url: string | null;
|
|
539
|
+
status: 'pending' | 'active' | 'inactive' | 'rejected';
|
|
540
|
+
is_primary: boolean;
|
|
541
|
+
total_clicks: number;
|
|
542
|
+
total_sales: number;
|
|
543
|
+
total_revenue: string;
|
|
544
|
+
effective_commission_percent: number;
|
|
545
|
+
effective_customer_discount_percent: number;
|
|
546
|
+
}
|
|
547
|
+
/**
|
|
548
|
+
* The buyer's standing in the shop's referral programme.
|
|
549
|
+
*
|
|
550
|
+
* Read-only on this lane. Converting a balance and requesting a payout move
|
|
551
|
+
* money toward the buyer and are not open to storefront code (ADR-0064, "money
|
|
552
|
+
* paths do not belong on the account API"), so this template renders what the
|
|
553
|
+
* buyer has earned and says where the payout lives.
|
|
554
|
+
*/
|
|
555
|
+
interface SdkCustomerAffiliateSummary {
|
|
556
|
+
/** `not_enrolled` is the availability gate for the whole referrals panel. */
|
|
557
|
+
state: string;
|
|
558
|
+
affiliate_codes_accepted_at_checkout: boolean;
|
|
559
|
+
balance_currency: string;
|
|
560
|
+
balances: {
|
|
561
|
+
pending: string;
|
|
562
|
+
available: string;
|
|
563
|
+
requested: string;
|
|
564
|
+
converted: string;
|
|
565
|
+
paid: string;
|
|
566
|
+
};
|
|
567
|
+
links: SdkCustomerAffiliateLink[];
|
|
568
|
+
commissions: Array<{
|
|
569
|
+
id: string;
|
|
570
|
+
invoice_id: string;
|
|
571
|
+
commission_amount: string;
|
|
572
|
+
status: string;
|
|
573
|
+
created_at: string | null;
|
|
574
|
+
}>;
|
|
575
|
+
claims: Array<{
|
|
576
|
+
id: string;
|
|
577
|
+
type: string;
|
|
578
|
+
status: string;
|
|
579
|
+
amount: string;
|
|
580
|
+
currency: string;
|
|
581
|
+
created_at: string | null;
|
|
582
|
+
}>;
|
|
583
|
+
}
|
|
584
|
+
/** One support ticket as the dashboard lists it. The thread is a second call. */
|
|
585
|
+
interface SdkCustomerTicketSummary {
|
|
586
|
+
uniqid: string;
|
|
587
|
+
title: string;
|
|
588
|
+
status: string;
|
|
589
|
+
updated_at: string;
|
|
590
|
+
created_at: string | null;
|
|
591
|
+
}
|
|
592
|
+
type SdkCustomerLoyaltyTransactionType = 'EARN_ORDER' | 'EARN_REVIEW' | 'REDEEM' | 'ADJUST';
|
|
593
|
+
interface SdkCustomerLoyalty {
|
|
594
|
+
enabled: boolean;
|
|
595
|
+
account: {
|
|
596
|
+
points_balance: number;
|
|
597
|
+
lifetime_points: number;
|
|
598
|
+
} | null;
|
|
599
|
+
settings: {
|
|
600
|
+
earn_rate_per_usd: number;
|
|
601
|
+
redeem_value_per_point: number;
|
|
602
|
+
min_redeem_points: number;
|
|
603
|
+
} | null;
|
|
604
|
+
transactions: Array<{
|
|
605
|
+
type: SdkCustomerLoyaltyTransactionType;
|
|
606
|
+
points: number;
|
|
607
|
+
balance_after: number;
|
|
608
|
+
description: string | null;
|
|
609
|
+
created_at: string;
|
|
610
|
+
}>;
|
|
611
|
+
}
|
|
612
|
+
interface SdkCustomerLoyaltyRedeem {
|
|
613
|
+
redeemed_points: number;
|
|
614
|
+
wallet_amount: string;
|
|
615
|
+
currency: string;
|
|
616
|
+
points_balance: number;
|
|
617
|
+
replayed: boolean;
|
|
618
|
+
}
|
|
619
|
+
type SdkCustomerWarrantyStatus = 'ACTIVE' | 'EXPIRED' | 'VOIDED';
|
|
620
|
+
type SdkCustomerWarrantyClaimStatus = 'REQUESTED' | 'APPROVED' | 'DENIED';
|
|
621
|
+
interface SdkCustomerWarranty {
|
|
622
|
+
uniqid: string;
|
|
623
|
+
invoice_uniqid: string;
|
|
624
|
+
product_title: string;
|
|
625
|
+
period_days: number;
|
|
626
|
+
starts_at: string;
|
|
627
|
+
expires_at: string;
|
|
628
|
+
status: SdkCustomerWarrantyStatus;
|
|
629
|
+
claim_status: SdkCustomerWarrantyClaimStatus | null;
|
|
630
|
+
created_at: string;
|
|
631
|
+
}
|
|
632
|
+
interface SdkCustomerWarrantyList {
|
|
633
|
+
warranties: SdkCustomerWarranty[];
|
|
634
|
+
}
|
|
635
|
+
interface SdkCustomerWarrantyClaim {
|
|
636
|
+
warranty: Omit<SdkCustomerWarranty, 'claim_status'> & {
|
|
637
|
+
claim_status: 'REQUESTED';
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
interface SdkCustomerDashboard {
|
|
641
|
+
shop: {
|
|
642
|
+
name: string;
|
|
643
|
+
currency: string;
|
|
644
|
+
};
|
|
645
|
+
customer: {
|
|
646
|
+
email: string;
|
|
647
|
+
name: string;
|
|
648
|
+
};
|
|
649
|
+
stats: {
|
|
650
|
+
invoices_count: number;
|
|
651
|
+
licenses_count: number;
|
|
652
|
+
deliverables_count: number;
|
|
653
|
+
tickets_count: number;
|
|
654
|
+
};
|
|
655
|
+
/**
|
|
656
|
+
* Null when the shop runs no wallet. That null IS the availability gate: a
|
|
657
|
+
* storefront must hide the wallet section rather than render a zero balance.
|
|
658
|
+
*/
|
|
659
|
+
wallet: {
|
|
660
|
+
available: string;
|
|
661
|
+
total: string;
|
|
662
|
+
hold: string;
|
|
663
|
+
currency: string;
|
|
664
|
+
enabled: boolean;
|
|
665
|
+
} | null;
|
|
666
|
+
invoices: SdkCustomerDashboardOrder[];
|
|
667
|
+
rewards: SdkCustomerRewards;
|
|
668
|
+
affiliate: SdkCustomerAffiliateSummary;
|
|
669
|
+
subscriptions: SdkCustomerSubscription[];
|
|
670
|
+
licenses: SdkCustomerLicense[];
|
|
671
|
+
deliverables: SdkCustomerDeliverable[];
|
|
672
|
+
tickets: SdkCustomerTicketSummary[];
|
|
673
|
+
}
|
|
674
|
+
/** One order row in the paginated list. camelCase, unlike the dashboard. */
|
|
675
|
+
interface SdkCustomerOrderSummary {
|
|
676
|
+
uniqid: string;
|
|
677
|
+
status: string;
|
|
678
|
+
/** USD-normalized, like the dashboard's `total`. Render `totalDisplay` first. */
|
|
679
|
+
total: string;
|
|
680
|
+
totalDisplay: string | null;
|
|
681
|
+
currency: string;
|
|
682
|
+
createdAt: string;
|
|
683
|
+
lineItems: Array<{
|
|
684
|
+
productTitle: string | null;
|
|
685
|
+
quantity: number;
|
|
686
|
+
}>;
|
|
687
|
+
}
|
|
688
|
+
/**
|
|
689
|
+
* Page-based, not cursor-based. The producer reads `page` and `limit` and
|
|
690
|
+
* answers with `has_more`; a cursor parameter is ignored upstream and returns
|
|
691
|
+
* page one forever.
|
|
692
|
+
*/
|
|
693
|
+
interface SdkCustomerOrderPage {
|
|
694
|
+
invoices: SdkCustomerOrderSummary[];
|
|
695
|
+
pagination: {
|
|
696
|
+
page: number;
|
|
697
|
+
limit: number;
|
|
698
|
+
total: number;
|
|
699
|
+
has_more: boolean;
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
/** Per-line fulfillment state, decided server-side and independent of payment. */
|
|
703
|
+
type SdkCustomerDeliveryStatus = 'PENDING' | 'AWAITING_FULFILLMENT' | 'DELIVERED' | 'FAILED';
|
|
704
|
+
/** What the shop delivered for one line. Null while nothing was delivered. */
|
|
705
|
+
interface SdkCustomerDeliverySummary {
|
|
706
|
+
codes: string[];
|
|
707
|
+
links: Array<{
|
|
708
|
+
label: string;
|
|
709
|
+
value: string;
|
|
710
|
+
href?: string | null;
|
|
711
|
+
}>;
|
|
712
|
+
notes: string[];
|
|
713
|
+
serials: Array<{
|
|
714
|
+
value: string;
|
|
715
|
+
is_replacement: boolean;
|
|
716
|
+
is_replaced: boolean;
|
|
717
|
+
}>;
|
|
718
|
+
}
|
|
719
|
+
interface SdkCustomerOrderDetail {
|
|
720
|
+
uniqid: string;
|
|
721
|
+
status: string;
|
|
722
|
+
statusDetails: string | null;
|
|
723
|
+
total: string;
|
|
724
|
+
totalDisplay: string | null;
|
|
725
|
+
subtotal: string;
|
|
726
|
+
discount: string;
|
|
727
|
+
discountDisplay: string | null;
|
|
728
|
+
tax: string;
|
|
729
|
+
taxPercentage: string;
|
|
730
|
+
currency: string;
|
|
731
|
+
createdAt: string;
|
|
732
|
+
lineItems: Array<{
|
|
733
|
+
id: string;
|
|
734
|
+
productTitle: string;
|
|
735
|
+
variantTitle: string | null;
|
|
736
|
+
unitPrice: string;
|
|
737
|
+
quantity: number;
|
|
738
|
+
total: string;
|
|
739
|
+
deliveryText: string | null;
|
|
740
|
+
deliveryStatus: SdkCustomerDeliveryStatus;
|
|
741
|
+
deliverySummary: SdkCustomerDeliverySummary | null;
|
|
742
|
+
}>;
|
|
743
|
+
currentPayment: {
|
|
744
|
+
/** Internal rail — NATIVE_CRYPTO, NOWPAYMENTS, STRIPE… Not buyer-facing. */
|
|
745
|
+
gateway: string;
|
|
746
|
+
displayName: string | null;
|
|
747
|
+
/** Concrete coin, e.g. USDT_TRC20. Set for crypto rails. */
|
|
748
|
+
cryptoGateway: string | null;
|
|
749
|
+
blockchain: string | null;
|
|
750
|
+
/** Alternative method the buyer chose, e.g. 'Card', 'iDEAL'. */
|
|
751
|
+
apmMethod: string | null;
|
|
752
|
+
} | null;
|
|
753
|
+
}
|
|
754
|
+
interface ShoppexSdk {
|
|
755
|
+
init(storeSlug: string, options?: SdkInitOptions): unknown;
|
|
756
|
+
isInitialized(): boolean;
|
|
757
|
+
getConfig(): {
|
|
758
|
+
storeSlug: string;
|
|
759
|
+
};
|
|
760
|
+
getCart(): SdkCartItem[];
|
|
761
|
+
getCartItemCount(): number;
|
|
762
|
+
addToCart(productId: string, variantId: string, quantity?: number, options?: SdkCartAddOptions): void;
|
|
763
|
+
/**
|
|
764
|
+
* Patches a cart line. The second argument is an update object — passing a
|
|
765
|
+
* bare number silently does nothing, because the SDK reads named keys off it.
|
|
766
|
+
*
|
|
767
|
+
* `custom_fields` is part of the line's IDENTITY, so writing it makes the SDK
|
|
768
|
+
* recompute `line_id` and merge the line into any existing line that now has
|
|
769
|
+
* the same identity. A caller holding a list of line ids has to tolerate one
|
|
770
|
+
* of them no longer existing after a write — and, because the merge SUMS the
|
|
771
|
+
* two lines' quantities, has to treat it as a repricing: quantities are bound
|
|
772
|
+
* into the signed quote proof, `custom_fields` is not. See
|
|
773
|
+
* `checkoutFieldWriteRepriced` in `checkout-fields.ts`.
|
|
774
|
+
*
|
|
775
|
+
* `min_quantity` and `max_quantity` are TRI-STATE: an absent key leaves the
|
|
776
|
+
* stored bound alone, a number sets it, and `null` DELETES it. The null
|
|
777
|
+
* spelling is what a healed line needs — an `undefined` reads as "not
|
|
778
|
+
* mentioned", so a ceiling the merchant removed would survive every heal that
|
|
779
|
+
* tried to remove it. The SDK re-clamps the line's quantity after the write.
|
|
780
|
+
*/
|
|
781
|
+
updateCartItem(lineId: string, updates: {
|
|
782
|
+
quantity?: number;
|
|
783
|
+
custom_fields?: Record<string, string>;
|
|
784
|
+
min_quantity?: number | null;
|
|
785
|
+
max_quantity?: number | null;
|
|
786
|
+
}): void;
|
|
787
|
+
removeFromCart(lineId: string): void;
|
|
788
|
+
clearCart(): void;
|
|
789
|
+
/**
|
|
790
|
+
* `currency` is not optional in practice: omitted, the SDK reads `?currency=`
|
|
791
|
+
* off window.location, which a client-side navigation has already dropped.
|
|
792
|
+
*/
|
|
793
|
+
quoteCart(coupon?: string, currency?: string): Promise<{
|
|
794
|
+
success: boolean;
|
|
795
|
+
data?: SdkCartQuote;
|
|
796
|
+
message?: string;
|
|
797
|
+
}>;
|
|
798
|
+
getCartCoupon(): string | null;
|
|
799
|
+
/** The stored code's classification, or null when nothing is stored. */
|
|
800
|
+
getCartCouponSource(): SdkCartCodeSource | null;
|
|
801
|
+
/**
|
|
802
|
+
* Stores a code AND what kind of code it is. The source defaults to `'COUPON'`
|
|
803
|
+
* in the SDK, so an affiliate referral saved without it is later submitted as
|
|
804
|
+
* a coupon and the affiliate is never credited — pass the source the QUOTE
|
|
805
|
+
* reported, never a guess.
|
|
806
|
+
*/
|
|
807
|
+
setCartCoupon(coupon: string, source?: SdkCartCodeSource): void;
|
|
808
|
+
clearCartCoupon(): void;
|
|
809
|
+
/**
|
|
810
|
+
* Reads `?ref=<code>` from the current URL, validates it and stores it for 30
|
|
811
|
+
* days (last-click). `checkout()` then sends the stored code on its own, so
|
|
812
|
+
* calling this once at boot is all a storefront has to do for referrals to
|
|
813
|
+
* pay out.
|
|
814
|
+
*/
|
|
815
|
+
captureAffiliateFromUrl(param?: string): Promise<string | null>;
|
|
816
|
+
getAffiliateCode(): string | null;
|
|
817
|
+
/**
|
|
818
|
+
* Forgets the stored ambient referral. The storage key belongs to the SDK —
|
|
819
|
+
* this is the only supported way to drop the attribution, and the exit a
|
|
820
|
+
* storefront needs when the backend refuses the code the buyer is carrying.
|
|
821
|
+
*/
|
|
822
|
+
clearAffiliateCode(): void;
|
|
823
|
+
/** Merchant-authored custom pages (About, Delivery info, …). */
|
|
824
|
+
getPages(): Promise<{
|
|
825
|
+
success: boolean;
|
|
826
|
+
data?: SdkPage[];
|
|
827
|
+
message?: string;
|
|
828
|
+
}>;
|
|
829
|
+
getPage(slug: string): Promise<{
|
|
830
|
+
success: boolean;
|
|
831
|
+
data?: SdkPage;
|
|
832
|
+
message?: string;
|
|
833
|
+
}>;
|
|
834
|
+
/** Merchant-configured navigation menus, keyed by slot (header/footer/…). */
|
|
835
|
+
getMenus(): Promise<{
|
|
836
|
+
success: boolean;
|
|
837
|
+
data?: SdkMenu[];
|
|
838
|
+
message?: string;
|
|
839
|
+
}>;
|
|
840
|
+
checkout(options?: {
|
|
841
|
+
email?: string;
|
|
842
|
+
/**
|
|
843
|
+
* Submitted as `coupon`. An OMITTED value is not "no coupon": the SDK then
|
|
844
|
+
* falls back to whatever is in cart storage (`resolveCheckoutCodes`), so
|
|
845
|
+
* suppressing a stored code takes an explicit empty string.
|
|
846
|
+
*/
|
|
847
|
+
coupon?: string;
|
|
848
|
+
/**
|
|
849
|
+
* Submitted as `affiliate_code` — the field the payout is attributed on.
|
|
850
|
+
* A referral sent under `coupon` instead credits nobody.
|
|
851
|
+
*
|
|
852
|
+
* A tri-state, exactly like `coupon` (`normalizeAffiliateCode`):
|
|
853
|
+
* - OMITTED — the SDK reads the ambient `?ref=` code out of its own storage
|
|
854
|
+
* at submit time, a SECOND read of storage the caller already looked at.
|
|
855
|
+
* - `''` — explicitly NONE: no referral is submitted and the storage read
|
|
856
|
+
* is suppressed. This is how a caller pins "quoted without a referral".
|
|
857
|
+
* - non-empty — pinned: submitted verbatim.
|
|
858
|
+
*
|
|
859
|
+
* So the quote and the invoice only agree if the code is NAMED here, in
|
|
860
|
+
* every lane including the empty one.
|
|
861
|
+
*/
|
|
862
|
+
affiliateCode?: string;
|
|
863
|
+
autoRedirect?: boolean;
|
|
864
|
+
/** Same as quoteCart: pass it, or the cart and the page disagree. */
|
|
865
|
+
currency?: string;
|
|
866
|
+
}): Promise<SdkCheckoutResult>;
|
|
867
|
+
getStorefrontRecentSales(): Promise<{
|
|
868
|
+
success: boolean;
|
|
869
|
+
data?: {
|
|
870
|
+
items: SdkRecentSaleEntry[];
|
|
871
|
+
window_hours: number;
|
|
872
|
+
};
|
|
873
|
+
}>;
|
|
874
|
+
/**
|
|
875
|
+
* Visitors active on this storefront in the last 10 minutes. A real count of
|
|
876
|
+
* real sessions — the header pill hides itself at 0 rather than inventing a
|
|
877
|
+
* livelier shop than the merchant has.
|
|
878
|
+
*/
|
|
879
|
+
getStorefrontOnlineUsers(): Promise<{
|
|
880
|
+
success: boolean;
|
|
881
|
+
data?: {
|
|
882
|
+
count: number;
|
|
883
|
+
window_seconds: number;
|
|
884
|
+
};
|
|
885
|
+
}>;
|
|
886
|
+
/**
|
|
887
|
+
* Shop-level feedback/reviews (Shoppex has shop feedback, not per-product
|
|
888
|
+
* reviews). Returns [] when the shop has none — the caller must hide the
|
|
889
|
+
* reviews UI rather than render an empty block or fake entries.
|
|
890
|
+
*/
|
|
891
|
+
getShopReviews(): Promise<{
|
|
892
|
+
success: boolean;
|
|
893
|
+
data?: SdkShopReview[];
|
|
894
|
+
}>;
|
|
895
|
+
/** Starts an in-page sign-in. Sends the email and nothing else. */
|
|
896
|
+
requestOtp(email: string): Promise<SdkResponse<unknown>>;
|
|
897
|
+
/**
|
|
898
|
+
* Finishes sign-in. The second argument is submitted as `otp`; the producer
|
|
899
|
+
* rejects a body that names it `code` with a 400.
|
|
900
|
+
*
|
|
901
|
+
* On success the WORKER sets the session cookie from the response it saw.
|
|
902
|
+
* Nothing here returns a token, and no storefront code may store one.
|
|
903
|
+
*/
|
|
904
|
+
verifyOtp(email: string, otp: string): Promise<SdkResponse<unknown>>;
|
|
905
|
+
/** Ends the session. The worker clears the cookie on the way back. */
|
|
906
|
+
logout(): Promise<SdkResponse<unknown>>;
|
|
907
|
+
/**
|
|
908
|
+
* The signed-in customer, or a failure when there is no session.
|
|
909
|
+
*
|
|
910
|
+
* Only a **401** means "not signed in". Any other failure — 403, 5xx, a
|
|
911
|
+
* contract mismatch, a network error with no `status` at all — means the
|
|
912
|
+
* answer is unknown, and a storefront that renders the sign-in form there
|
|
913
|
+
* tells a buyer with a live session that they are signed out. Show an error
|
|
914
|
+
* and let them retry instead.
|
|
915
|
+
*
|
|
916
|
+
* Never treat a failure as a signed-in customer either: that renders an
|
|
917
|
+
* account shell around data nobody fetched.
|
|
918
|
+
*/
|
|
919
|
+
me(): Promise<SdkResponse<unknown>>;
|
|
920
|
+
/**
|
|
921
|
+
* The overview payload — and the collection route for five other panels.
|
|
922
|
+
*
|
|
923
|
+
* Shop, customer, counters, the wallet gate, recent orders, and the lists
|
|
924
|
+
* that have no route of their own: licenses, deliverables, subscriptions,
|
|
925
|
+
* tickets, rewards and the affiliate summary.
|
|
926
|
+
*/
|
|
927
|
+
dashboard(): Promise<SdkResponse<SdkCustomerDashboard>>;
|
|
928
|
+
/** Order history, page-based. */
|
|
929
|
+
/**
|
|
930
|
+
* `status` and `search` filter SERVER-side. The list is paged, so a filter
|
|
931
|
+
* applied to the page in hand would filter only the rows it happens to hold.
|
|
932
|
+
*/
|
|
933
|
+
orders(options?: {
|
|
934
|
+
page?: number;
|
|
935
|
+
limit?: number;
|
|
936
|
+
status?: string;
|
|
937
|
+
search?: string;
|
|
938
|
+
}): Promise<SdkResponse<SdkCustomerOrderPage>>;
|
|
939
|
+
/** One order, by the `uniqid` the list rows carry. */
|
|
940
|
+
order(id: string): Promise<SdkResponse<SdkCustomerOrderDetail>>;
|
|
941
|
+
/** Loyalty settings, balance and the latest points activity. */
|
|
942
|
+
loyalty(): Promise<SdkResponse<SdkCustomerLoyalty>>;
|
|
943
|
+
/** Exchanges points for store credit. Keep one idempotency key per retryable submission. */
|
|
944
|
+
redeemLoyaltyPoints(input: {
|
|
945
|
+
points: number;
|
|
946
|
+
idempotencyKey: string;
|
|
947
|
+
}): Promise<SdkResponse<SdkCustomerLoyaltyRedeem>>;
|
|
948
|
+
/**
|
|
949
|
+
* The warranties this buyer holds. Given an order's `uniqid`, only that
|
|
950
|
+
* order's — the route filters on `invoices.uniqid`, which is the same id.
|
|
951
|
+
*/
|
|
952
|
+
warranties(invoiceUniqid?: string): Promise<SdkResponse<SdkCustomerWarrantyList>>;
|
|
953
|
+
claimWarranty(uniqid: string, message?: string): Promise<SdkResponse<SdkCustomerWarrantyClaim>>;
|
|
954
|
+
/** Clears a license's hardware lock so the next activation claims it. */
|
|
955
|
+
resetLicenseHwid(uniqid: string): Promise<SdkResponse<unknown>>;
|
|
956
|
+
/** Renewal invoices for one subscription. */
|
|
957
|
+
subscriptionBillingHistory(uniqid: string): Promise<SdkResponse<unknown>>;
|
|
958
|
+
/**
|
|
959
|
+
* Ends a subscription. `cancel_at_period_end` decides whether the buyer keeps
|
|
960
|
+
* the time they already paid for — PayPal cannot, every other gateway can.
|
|
961
|
+
*/
|
|
962
|
+
cancelSubscription(uniqid: string, options?: {
|
|
963
|
+
cancel_at_period_end?: boolean;
|
|
964
|
+
reason?: string | null;
|
|
965
|
+
}): Promise<SdkResponse<unknown>>;
|
|
966
|
+
pauseSubscription(uniqid: string): Promise<SdkResponse<unknown>>;
|
|
967
|
+
resumeSubscription(uniqid: string): Promise<SdkResponse<unknown>>;
|
|
968
|
+
/** The buyer's saved products. */
|
|
969
|
+
favorites(): Promise<SdkResponse<unknown>>;
|
|
970
|
+
/** Idempotent upstream, and a PUT — `customer-favorites.ts` has no POST route. */
|
|
971
|
+
addFavorite(productUniqid: string): Promise<SdkResponse<unknown>>;
|
|
972
|
+
removeFavorite(productUniqid: string): Promise<SdkResponse<unknown>>;
|
|
973
|
+
/**
|
|
974
|
+
* The affiliate summary as its own read. The Referrals panel takes it from
|
|
975
|
+
* `dashboard()` instead, but a merchant extending this template should not
|
|
976
|
+
* have to discover that the runtime has a method the types deny.
|
|
977
|
+
*/
|
|
978
|
+
affiliate(): Promise<SdkResponse<unknown>>;
|
|
979
|
+
/** Opens a ticket. `title` is optional; the producer names it "Support Request". */
|
|
980
|
+
createTicket(payload: {
|
|
981
|
+
title?: string;
|
|
982
|
+
message: string;
|
|
983
|
+
invoice_id?: string;
|
|
984
|
+
}): Promise<SdkResponse<unknown>>;
|
|
985
|
+
/** One ticket with its whole message thread. */
|
|
986
|
+
ticket(uniqid: string): Promise<SdkResponse<unknown>>;
|
|
987
|
+
replyToTicket(uniqid: string, message: string): Promise<SdkResponse<unknown>>;
|
|
988
|
+
/**
|
|
989
|
+
* Renames the buyer. The email is NOT editable here: changing it is gated
|
|
990
|
+
* behind a fresh code in the backend and has no allowlisted path.
|
|
991
|
+
*/
|
|
992
|
+
updateProfile(patch: {
|
|
993
|
+
name: string;
|
|
994
|
+
}): Promise<SdkResponse<unknown>>;
|
|
995
|
+
updateAvatar(file: File): Promise<SdkResponse<unknown>>;
|
|
996
|
+
removeAvatar(): Promise<SdkResponse<unknown>>;
|
|
997
|
+
/** Marketing consent: the shop's lists plus the global unsubscribe. */
|
|
998
|
+
emailPreferences(): Promise<SdkResponse<unknown>>;
|
|
999
|
+
updateEmailPreferences(patch: {
|
|
1000
|
+
global_unsubscribed?: boolean;
|
|
1001
|
+
list_subscriptions?: Array<{
|
|
1002
|
+
list_id: string;
|
|
1003
|
+
subscribed: boolean;
|
|
1004
|
+
}>;
|
|
1005
|
+
}): Promise<SdkResponse<unknown>>;
|
|
1006
|
+
/** Signed-in devices. Never returns a token value, only a description. */
|
|
1007
|
+
sessions(): Promise<SdkResponse<unknown>>;
|
|
1008
|
+
revokeSession(id: string): Promise<SdkResponse<unknown>>;
|
|
1009
|
+
/** Ends every session except the one making the call. */
|
|
1010
|
+
revokeAllSessions(): Promise<SdkResponse<unknown>>;
|
|
1011
|
+
/** Program configuration and this buyer's reseller relationship, if any. */
|
|
1012
|
+
reseller(): Promise<SdkResponse<unknown>>;
|
|
1013
|
+
/** APPLICATION mode: submits the producer's optional/required note. */
|
|
1014
|
+
applyForReseller(note?: string): Promise<SdkResponse<unknown>>;
|
|
1015
|
+
/** OPEN mode: enrolls the signed-in buyer immediately. */
|
|
1016
|
+
enrollAsReseller(): Promise<SdkResponse<unknown>>;
|
|
1017
|
+
/** MANUAL mode: redeems the merchant-issued invitation token. */
|
|
1018
|
+
acceptResellerInvite(token: string): Promise<SdkResponse<unknown>>;
|
|
1019
|
+
/** Wholesale catalog at this buyer's tier prices. */
|
|
1020
|
+
resellerCatalog(options?: {
|
|
1021
|
+
search?: string;
|
|
1022
|
+
page?: number;
|
|
1023
|
+
per_page?: number;
|
|
1024
|
+
}): Promise<SdkResponse<unknown>>;
|
|
1025
|
+
/** Server-prices a basket, including volume discounts, without writing. */
|
|
1026
|
+
quoteResellerOrder(items: Array<{
|
|
1027
|
+
product_id: string;
|
|
1028
|
+
variant_id?: string;
|
|
1029
|
+
quantity: number;
|
|
1030
|
+
}>): Promise<SdkResponse<unknown>>;
|
|
1031
|
+
/** Page-based wholesale order history. */
|
|
1032
|
+
resellerOrders(options?: {
|
|
1033
|
+
page?: number;
|
|
1034
|
+
per_page?: number;
|
|
1035
|
+
}): Promise<SdkResponse<unknown>>;
|
|
1036
|
+
/** One wholesale order, including delivery state and serials. */
|
|
1037
|
+
resellerOrder(uniqid: string): Promise<SdkResponse<unknown>>;
|
|
1038
|
+
/** Wholesale prepaid balance, separate from the retail wallet panel. */
|
|
1039
|
+
resellerWallet(): Promise<SdkResponse<unknown>>;
|
|
1040
|
+
/** Lists the buyer's reseller API keys without plaintext secrets. */
|
|
1041
|
+
resellerApiKeys(): Promise<SdkResponse<unknown>>;
|
|
1042
|
+
}
|
|
1043
|
+
interface SdkShopReview {
|
|
1044
|
+
id: string;
|
|
1045
|
+
/** 1-5, normalized by the SDK from the shop-feedback score (5/3/1). */
|
|
1046
|
+
rating: number;
|
|
1047
|
+
/** Buyer names are anonymized by the API to "Customer Hidden". */
|
|
1048
|
+
author?: string;
|
|
1049
|
+
customer_name?: string;
|
|
1050
|
+
comment?: string;
|
|
1051
|
+
message?: string | null;
|
|
1052
|
+
product_title?: string | null;
|
|
1053
|
+
/** True when the review is tied to a real completed purchase. */
|
|
1054
|
+
verified?: boolean;
|
|
1055
|
+
is_automated?: boolean;
|
|
1056
|
+
created_at: string;
|
|
1057
|
+
}
|
|
1058
|
+
type AddonBarPreset = 'brand_blue' | 'emerald' | 'sunset' | 'rose' | 'charcoal' | 'custom';
|
|
1059
|
+
type StorefrontAddon = {
|
|
1060
|
+
id: string;
|
|
1061
|
+
type: 'announcement_bar';
|
|
1062
|
+
slot: string;
|
|
1063
|
+
component: 'announcement_bar';
|
|
1064
|
+
sort_order: number;
|
|
1065
|
+
props: {
|
|
1066
|
+
addon_id: string;
|
|
1067
|
+
text: string;
|
|
1068
|
+
link_label?: string | null;
|
|
1069
|
+
link_url?: string | null;
|
|
1070
|
+
dismissible: boolean;
|
|
1071
|
+
display_mode: 'static' | 'marquee';
|
|
1072
|
+
theme_preset: AddonBarPreset;
|
|
1073
|
+
animation_speed_seconds: number;
|
|
1074
|
+
background_color: string;
|
|
1075
|
+
text_color: string;
|
|
1076
|
+
};
|
|
1077
|
+
} | {
|
|
1078
|
+
id: string;
|
|
1079
|
+
type: 'countdown_bar';
|
|
1080
|
+
slot: string;
|
|
1081
|
+
component: 'countdown_bar';
|
|
1082
|
+
sort_order: number;
|
|
1083
|
+
props: {
|
|
1084
|
+
addon_id: string;
|
|
1085
|
+
text: string;
|
|
1086
|
+
end_at: string;
|
|
1087
|
+
link_label?: string | null;
|
|
1088
|
+
link_url?: string | null;
|
|
1089
|
+
dismissible: boolean;
|
|
1090
|
+
theme_preset: AddonBarPreset;
|
|
1091
|
+
expired_behavior: 'hide' | 'message';
|
|
1092
|
+
expired_message?: string | null;
|
|
1093
|
+
density: 'compact' | 'comfortable';
|
|
1094
|
+
cta_style: 'subtle' | 'outline' | 'solid';
|
|
1095
|
+
timer_style: 'minimal' | 'boxed';
|
|
1096
|
+
background_color: string;
|
|
1097
|
+
text_color: string;
|
|
1098
|
+
};
|
|
1099
|
+
} | {
|
|
1100
|
+
id: string;
|
|
1101
|
+
type: 'promo_info_card';
|
|
1102
|
+
slot: string;
|
|
1103
|
+
component: 'promo_info_card';
|
|
1104
|
+
sort_order: number;
|
|
1105
|
+
props: {
|
|
1106
|
+
addon_id: string;
|
|
1107
|
+
eyebrow?: string | null;
|
|
1108
|
+
title: string;
|
|
1109
|
+
body: string;
|
|
1110
|
+
link_label?: string | null;
|
|
1111
|
+
link_url?: string | null;
|
|
1112
|
+
theme_preset: 'indigo' | 'emerald' | 'amber' | 'rose' | 'slate' | 'custom';
|
|
1113
|
+
layout_style: 'compact' | 'feature' | 'alert';
|
|
1114
|
+
density: 'compact' | 'comfortable';
|
|
1115
|
+
cta_style: 'subtle' | 'outline' | 'solid';
|
|
1116
|
+
icon: 'sparkles' | 'megaphone' | 'gift' | 'truck' | 'shield' | 'support' | 'none';
|
|
1117
|
+
icon_visibility: 'show' | 'hide';
|
|
1118
|
+
background_color: string;
|
|
1119
|
+
text_color: string;
|
|
1120
|
+
accent_color: string;
|
|
1121
|
+
};
|
|
1122
|
+
} | {
|
|
1123
|
+
id: string;
|
|
1124
|
+
type: 'recent_purchase_popup';
|
|
1125
|
+
slot: string;
|
|
1126
|
+
component: 'recent_purchase_popup';
|
|
1127
|
+
sort_order: number;
|
|
1128
|
+
props: {
|
|
1129
|
+
addon_id: string;
|
|
1130
|
+
title: string;
|
|
1131
|
+
cooldown_seconds: number;
|
|
1132
|
+
items: Array<{
|
|
1133
|
+
customer_label: string;
|
|
1134
|
+
product_title: string;
|
|
1135
|
+
created_at: string;
|
|
1136
|
+
}>;
|
|
1137
|
+
};
|
|
1138
|
+
} | {
|
|
1139
|
+
id: string;
|
|
1140
|
+
type: 'coupon_popup_modal';
|
|
1141
|
+
slot: string;
|
|
1142
|
+
component: 'coupon_popup_modal';
|
|
1143
|
+
sort_order: number;
|
|
1144
|
+
props: {
|
|
1145
|
+
addon_id: string;
|
|
1146
|
+
eyebrow?: string | null;
|
|
1147
|
+
title: string;
|
|
1148
|
+
body: string;
|
|
1149
|
+
coupon_code: string;
|
|
1150
|
+
primary_button_label: string;
|
|
1151
|
+
secondary_button_label: string;
|
|
1152
|
+
disclaimer?: string | null;
|
|
1153
|
+
theme_preset: 'midnight' | 'ocean' | 'ember' | 'forest' | 'custom';
|
|
1154
|
+
trigger: 'delay' | 'exit_intent';
|
|
1155
|
+
delay_seconds: number;
|
|
1156
|
+
show_once_per_session: boolean;
|
|
1157
|
+
reminder_hours: number;
|
|
1158
|
+
background_color: string;
|
|
1159
|
+
text_color: string;
|
|
1160
|
+
accent_color: string;
|
|
1161
|
+
hero_image_url?: string | null;
|
|
1162
|
+
discount_display?: string | null;
|
|
1163
|
+
expires_at?: string | null;
|
|
1164
|
+
};
|
|
1165
|
+
};
|
|
1166
|
+
declare global {
|
|
1167
|
+
interface Window {
|
|
1168
|
+
shoppex?: ShoppexSdk;
|
|
1169
|
+
/** SDK bootstrap globals injected by the Shoppex storefront/preview worker. */
|
|
1170
|
+
shopSlug?: string;
|
|
1171
|
+
shopId?: string;
|
|
1172
|
+
apiBaseUrl?: string;
|
|
1173
|
+
checkoutBaseUrl?: string;
|
|
1174
|
+
defaultCurrency?: string;
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
/**
|
|
1179
|
+
* Merchant-authored product badges ("New!", "Limited", "Best value").
|
|
1180
|
+
*
|
|
1181
|
+
* Purely decorative merchandising the merchant attaches in the dashboard. They
|
|
1182
|
+
* never affect price, stock or fulfilment, and they never speak for the
|
|
1183
|
+
* storefront: a badge cannot say a product is available, on sale, or in stock —
|
|
1184
|
+
* those remain the theme's own signals, derived from real fields.
|
|
1185
|
+
*
|
|
1186
|
+
* Ported rather than imported, for the same reason `video-embed.ts` is: this
|
|
1187
|
+
* theme builds standalone from `templates/code-storefronts/`, in an offline
|
|
1188
|
+
* sandbox, with no workspace dependency on `@shoppex/builder-contracts`. The
|
|
1189
|
+
* canonical contract is `packages/builder-contracts/src/product-badges.ts` —
|
|
1190
|
+
* keep the wire field names, the caps and the contrast rule in step with it.
|
|
1191
|
+
*
|
|
1192
|
+
* Everything below reads UNTRUSTED JSON: the payload arrives over the network
|
|
1193
|
+
* in `window.__SHOPPEX_INITIAL__`. Entries that do not parse are dropped rather
|
|
1194
|
+
* than repaired — a badge with no usable label has no meaning, and inventing
|
|
1195
|
+
* one would be a fabricated value on a merchandising surface.
|
|
1196
|
+
*/
|
|
1197
|
+
/**
|
|
1198
|
+
* The icon catalog the dashboard offers. A name outside this list is NOT an
|
|
1199
|
+
* error — the catalog is append-only upstream, so a storefront artifact built
|
|
1200
|
+
* before an icon was added must degrade to a text-only badge rather than throw
|
|
1201
|
+
* on a name it has never heard of.
|
|
1202
|
+
*/
|
|
1203
|
+
declare const PRODUCT_BADGE_ICON_NAMES: readonly ["sparkles", "flame", "star", "zap", "tag", "percent", "trending-up", "megaphone", "party-popper", "crown", "gem", "trophy", "award", "medal", "badge-check", "thumbs-up", "heart", "shield", "shield-check", "lock", "handshake", "rocket", "truck", "clock", "timer", "infinity", "refresh-ccw", "download", "package", "send", "headphones", "message-circle", "life-buoy", "users", "gamepad-2", "monitor", "smartphone", "key", "file-text", "book-open", "music", "video", "image", "palette", "code", "wrench", "wallet", "credit-card", "globe", "gift", "bot"];
|
|
1204
|
+
type ProductBadgeIconName = (typeof PRODUCT_BADGE_ICON_NAMES)[number];
|
|
1205
|
+
/** One badge, in this theme's camelCase model. */
|
|
1206
|
+
interface ProductBadge {
|
|
1207
|
+
label: string;
|
|
1208
|
+
/** null for a text-only badge, and for any icon name this build cannot draw. */
|
|
1209
|
+
icon: ProductBadgeIconName | null;
|
|
1210
|
+
/** `#rrggbb` lowercase, or null when the theme owns the colour. */
|
|
1211
|
+
backgroundColor: string | null;
|
|
1212
|
+
/**
|
|
1213
|
+
* The readable foreground for `backgroundColor`, decided by WCAG contrast.
|
|
1214
|
+
* null exactly when `backgroundColor` is null — the theme then owns both.
|
|
1215
|
+
*/
|
|
1216
|
+
foregroundColor: string | null;
|
|
1217
|
+
showOnProductCard: boolean;
|
|
1218
|
+
showOnProductPage: boolean;
|
|
1219
|
+
}
|
|
1220
|
+
/**
|
|
1221
|
+
* The readable foreground for a badge background.
|
|
1222
|
+
*
|
|
1223
|
+
* The merchant picks the fill from a colour well with no contrast check of its
|
|
1224
|
+
* own, so the label colour cannot also be theirs: a mid-yellow badge with white
|
|
1225
|
+
* text is unreadable and a navy badge with black text is worse. Both candidates
|
|
1226
|
+
* are scored by WCAG contrast ratio against the chosen fill and the higher one
|
|
1227
|
+
* wins, which is the same decision the other lane makes for the same badge.
|
|
1228
|
+
*
|
|
1229
|
+
* Returns null when there is no explicit background — the theme then owns both
|
|
1230
|
+
* colours, and substituting one here would override its palette.
|
|
1231
|
+
*/
|
|
1232
|
+
declare function resolveBadgeForeground(backgroundColor: string | null): string | null;
|
|
1233
|
+
/**
|
|
1234
|
+
* The `badges` array off the product wire. Missing, malformed or empty all
|
|
1235
|
+
* produce `[]` — a product without badges is the overwhelmingly common case and
|
|
1236
|
+
* is not an error.
|
|
1237
|
+
*/
|
|
1238
|
+
declare function parseProductBadges(value: unknown): ProductBadge[];
|
|
1239
|
+
/** Badges the catalog card shows, in the merchant's order. */
|
|
1240
|
+
declare function badgesForCard(badges: readonly ProductBadge[]): ProductBadge[];
|
|
1241
|
+
/** Badges the product page shows, in the merchant's order. */
|
|
1242
|
+
declare function badgesForProductPage(badges: readonly ProductBadge[]): ProductBadge[];
|
|
1243
|
+
|
|
1244
|
+
/**
|
|
1245
|
+
* The `window.__SHOPPEX_INITIAL__` contract: catalog, store switches and addon
|
|
1246
|
+
* slots, injected by the Shoppex storefront (or preview) worker immediately
|
|
1247
|
+
* before `</head>` per
|
|
1248
|
+
* `packages/builder-contracts/src/storefront-initial-data-html.ts`.
|
|
1249
|
+
*
|
|
1250
|
+
* This is PLATFORM data, not merchant content — the theme never authors it and
|
|
1251
|
+
* must never invent it. Merchant-authored content lives in
|
|
1252
|
+
* `src/config/site-config.ts` and is read from the same object under
|
|
1253
|
+
* `store.builder_settings.theme.content`.
|
|
1254
|
+
*/
|
|
1255
|
+
|
|
1256
|
+
interface StorefrontPriceVariant {
|
|
1257
|
+
id: string;
|
|
1258
|
+
label: string;
|
|
1259
|
+
/** What this option states about its term; `inherit` defers to the product. */
|
|
1260
|
+
license: LicenseStatement;
|
|
1261
|
+
/** Display unit price with the product percent discount already applied. */
|
|
1262
|
+
price: number;
|
|
1263
|
+
/**
|
|
1264
|
+
* Undiscounted display-currency unit price — the input for line-level
|
|
1265
|
+
* pricing, where the percent is applied to the whole line like the quote
|
|
1266
|
+
* does, and the floor the server holds pay-what-you-want amounts to.
|
|
1267
|
+
*/
|
|
1268
|
+
baseUnitPrice: number;
|
|
1269
|
+
stock: number | null;
|
|
1270
|
+
quantityMin: number | null;
|
|
1271
|
+
quantityMax: number | null;
|
|
1272
|
+
imageUrl: string | null;
|
|
1273
|
+
isFeatured: boolean;
|
|
1274
|
+
}
|
|
1275
|
+
interface VolumeDiscountRule {
|
|
1276
|
+
quantity: number;
|
|
1277
|
+
type: 'PERCENTAGE' | 'FIXED';
|
|
1278
|
+
value: number;
|
|
1279
|
+
}
|
|
1280
|
+
type ProductBundleItem = {
|
|
1281
|
+
/** Cart id (uniqid) — what addToCart expects. */
|
|
1282
|
+
id: string;
|
|
1283
|
+
title: string;
|
|
1284
|
+
variantId: string;
|
|
1285
|
+
unitPrice: number;
|
|
1286
|
+
imageUrl: string | null;
|
|
1287
|
+
variantTitle: string | null;
|
|
1288
|
+
};
|
|
1289
|
+
/**
|
|
1290
|
+
* A "buy these together" offer the merchant configured. The discount is applied
|
|
1291
|
+
* by the backend when the cart is quoted — the theme only advertises it, it
|
|
1292
|
+
* never computes the charged price itself.
|
|
1293
|
+
*/
|
|
1294
|
+
type ProductBundle = {
|
|
1295
|
+
id: string;
|
|
1296
|
+
title: string;
|
|
1297
|
+
discountType: 'PERCENTAGE' | 'FIXED';
|
|
1298
|
+
discountAmount: number;
|
|
1299
|
+
items: ProductBundleItem[];
|
|
1300
|
+
};
|
|
1301
|
+
type ProductContentTab = {
|
|
1302
|
+
title: string;
|
|
1303
|
+
html: string;
|
|
1304
|
+
};
|
|
1305
|
+
/**
|
|
1306
|
+
* A paid extra the buyer may add to the line. The price is a per-unit surcharge
|
|
1307
|
+
* on top of the resolved variant price, and `required` options are locked on —
|
|
1308
|
+
* the buy box shows them checked and refuses to clear them, because the backend
|
|
1309
|
+
* charges them either way.
|
|
1310
|
+
*/
|
|
1311
|
+
type ProductAddon = {
|
|
1312
|
+
id: string;
|
|
1313
|
+
title: string;
|
|
1314
|
+
price: number;
|
|
1315
|
+
required: boolean;
|
|
1316
|
+
};
|
|
1317
|
+
type ProductFaq = {
|
|
1318
|
+
question: string;
|
|
1319
|
+
answer: string;
|
|
1320
|
+
};
|
|
1321
|
+
/**
|
|
1322
|
+
* One ACTIVE "buy N, get M" deal on this product.
|
|
1323
|
+
*
|
|
1324
|
+
* The server filters the list to the currently live window before it emits it,
|
|
1325
|
+
* so the storefront advertises what `CartQuoteService` would actually apply —
|
|
1326
|
+
* it never computes the discount itself. The discounted units sit WITHIN the
|
|
1327
|
+
* required quantity (they are not extra items on top), and
|
|
1328
|
+
* `maxApplicationsPerCart` caps how often the reward repeats in one cart; both
|
|
1329
|
+
* are `applyAutomaticProductDeals` semantics and the wording depends on them.
|
|
1330
|
+
*
|
|
1331
|
+
* `endsAt` is what makes a row time-limited. Once it passes the backend stops
|
|
1332
|
+
* applying the deal, so the WHOLE row has to disappear — not just its
|
|
1333
|
+
* countdown — which is why the panel drops expired rows rather than freezing
|
|
1334
|
+
* them at 00:00:00.
|
|
1335
|
+
*/
|
|
1336
|
+
type ProductAutomaticDeal = {
|
|
1337
|
+
id: string;
|
|
1338
|
+
/** Merchant title. Empty when they named the rule nothing. */
|
|
1339
|
+
title: string;
|
|
1340
|
+
requiredQuantity: number;
|
|
1341
|
+
discountedQuantity: number;
|
|
1342
|
+
/** FREE gives the units away; PERCENTAGE takes `discountValue` percent off. */
|
|
1343
|
+
discountType: 'FREE' | 'PERCENTAGE';
|
|
1344
|
+
/** Percent off the discounted units. 0 for a FREE deal. */
|
|
1345
|
+
discountValue: number;
|
|
1346
|
+
/** How often the reward repeats per cart; null when uncapped. */
|
|
1347
|
+
maxApplicationsPerCart: number | null;
|
|
1348
|
+
/** ISO timestamp the deal stops applying, or null when it does not expire. */
|
|
1349
|
+
endsAt: string | null;
|
|
1350
|
+
};
|
|
1351
|
+
/**
|
|
1352
|
+
* One detail the merchant requires from the buyer for this product — a Steam
|
|
1353
|
+
* id, a server name, a licence holder.
|
|
1354
|
+
*
|
|
1355
|
+
* PAYMENT PATH. These are not decoration: the backend validates them when the
|
|
1356
|
+
* invoice is built (`custom_fields_validation_failed`), so a product with a
|
|
1357
|
+
* REQUIRED field cannot be sold at all until its value reaches the cart line.
|
|
1358
|
+
* The values ride on the cart line's `custom_fields`, which is also part of the
|
|
1359
|
+
* SDK's line identity — see `checkout-fields.ts`.
|
|
1360
|
+
*
|
|
1361
|
+
* The shape mirrors the SDK's own `StorefrontCustomField`
|
|
1362
|
+
* (`packages/sdk/src/utils/storefront-custom-fields.ts`), because that is what
|
|
1363
|
+
* the Easy lane validates against and the two lanes must agree about which
|
|
1364
|
+
* value a merchant's definition accepts.
|
|
1365
|
+
*/
|
|
1366
|
+
type ProductCustomField = {
|
|
1367
|
+
name: string;
|
|
1368
|
+
/** 'text' | 'textarea' | 'checkbox' | … — anything else renders as text. */
|
|
1369
|
+
type: string;
|
|
1370
|
+
required: boolean;
|
|
1371
|
+
defaultValue: string;
|
|
1372
|
+
placeholder: string;
|
|
1373
|
+
/** Merchant-authored validation pattern. Absent when they set none. */
|
|
1374
|
+
regex: string | null;
|
|
1375
|
+
};
|
|
1376
|
+
/** A category a product belongs to. The id is what the filter chips match on. */
|
|
1377
|
+
type ProductCategoryRef = {
|
|
1378
|
+
id: string;
|
|
1379
|
+
title: string;
|
|
1380
|
+
};
|
|
1381
|
+
/**
|
|
1382
|
+
* Product kinds the catalog renders differently. `SUBSCRIPTION` earns a badge,
|
|
1383
|
+
* `INFO_CARD` is not buyable at all, `SERIALS` reads its stock straight off
|
|
1384
|
+
* the product row instead of aggregating variants, and `PHYSICAL` ships to an
|
|
1385
|
+
* address the hosted checkout collects, so it is never "instant".
|
|
1386
|
+
*/
|
|
1387
|
+
type ProductType = 'PRODUCT' | 'SUBSCRIPTION' | 'INFO_CARD' | 'SERIALS' | 'PHYSICAL';
|
|
1388
|
+
/**
|
|
1389
|
+
* What a product promises about delivery.
|
|
1390
|
+
*
|
|
1391
|
+
* - `shipped`: a PHYSICAL product. Its digital `delivery_time` is null, which
|
|
1392
|
+
* would otherwise read as instant, so the type decides first.
|
|
1393
|
+
* - `timed`: the merchant set a delivery time.
|
|
1394
|
+
* - `instant`: digital with no delivery time.
|
|
1395
|
+
*
|
|
1396
|
+
* Parity with the hosted checkout's `apps/checkout/lib/delivery-promise.ts`.
|
|
1397
|
+
*/
|
|
1398
|
+
type DeliveryPromise = {
|
|
1399
|
+
kind: 'shipped';
|
|
1400
|
+
} | {
|
|
1401
|
+
kind: 'timed';
|
|
1402
|
+
count: number;
|
|
1403
|
+
unit: DeliveryTimeUnit;
|
|
1404
|
+
} | {
|
|
1405
|
+
kind: 'instant';
|
|
1406
|
+
};
|
|
1407
|
+
declare function resolveDeliveryPromise(product: Pick<StorefrontProduct, 'productType' | 'deliveryTimeCount' | 'deliveryTimeUnit'>): DeliveryPromise;
|
|
1408
|
+
/**
|
|
1409
|
+
* How often a subscription product is charged, and how long it is free first.
|
|
1410
|
+
*
|
|
1411
|
+
* PARITY CONTRACT. The Easy lane derives the same three facts from the same
|
|
1412
|
+
* flat wire fields in
|
|
1413
|
+
* `packages/storefront-render-core/src/subscription-billing.ts`, and the
|
|
1414
|
+
* wordings below are that module's wordings. A change on either side without
|
|
1415
|
+
* the other makes the two lanes describe one product differently, which is the
|
|
1416
|
+
* one thing this template may not do.
|
|
1417
|
+
*/
|
|
1418
|
+
type SubscriptionIntervalUnit = 'DAY' | 'WEEK' | 'MONTH' | 'YEAR';
|
|
1419
|
+
interface ProductSubscription {
|
|
1420
|
+
/**
|
|
1421
|
+
* The billing interval, or null when the merchant configured none. The
|
|
1422
|
+
* product contract allows a SUBSCRIPTION product without an interval, so the
|
|
1423
|
+
* buy box then prints no cadence rather than inventing one.
|
|
1424
|
+
*/
|
|
1425
|
+
intervalUnit: SubscriptionIntervalUnit | null;
|
|
1426
|
+
/** Intervals per charge, at least 1. */
|
|
1427
|
+
intervalCount: number;
|
|
1428
|
+
/** Free-trial length in days; 0 when the merchant set none. */
|
|
1429
|
+
trialDays: number;
|
|
1430
|
+
}
|
|
1431
|
+
/** The cadence as it reads next to a price: "per month", "every 3 months". */
|
|
1432
|
+
declare function formatSubscriptionCadence(subscription: ProductSubscription): string;
|
|
1433
|
+
/** "7-day free trial" — the plan charges nothing for those days. */
|
|
1434
|
+
declare function formatSubscriptionTrial(subscription: ProductSubscription): string;
|
|
1435
|
+
interface StorefrontProduct {
|
|
1436
|
+
/** uniqid — the identifier the cart/SDK use for this product. */
|
|
1437
|
+
id: string;
|
|
1438
|
+
/** Drives the catalog badge, the buy controls and the stock aggregation. */
|
|
1439
|
+
productType: ProductType;
|
|
1440
|
+
/**
|
|
1441
|
+
* The product's real database id. Distinct from `id` (uniqid); shop feedback
|
|
1442
|
+
* references products by this id, so review matching must use it.
|
|
1443
|
+
*/
|
|
1444
|
+
productId: string;
|
|
1445
|
+
name: string;
|
|
1446
|
+
price: number;
|
|
1447
|
+
currency: string;
|
|
1448
|
+
description: string | null;
|
|
1449
|
+
slug: string | null;
|
|
1450
|
+
imageUrl: string | null;
|
|
1451
|
+
/**
|
|
1452
|
+
* The product's own image attachments, in the merchant's order — the product
|
|
1453
|
+
* page gallery. Empty when the product has none; the gallery then falls back
|
|
1454
|
+
* to `imageUrl`, the same resolution the Easy lane's gallery producer applies.
|
|
1455
|
+
* The wire carries no per-image alt text, so the alt is the product title.
|
|
1456
|
+
*/
|
|
1457
|
+
imageUrls: string[];
|
|
1458
|
+
stock: number | null;
|
|
1459
|
+
/**
|
|
1460
|
+
* Merchant-set "Low Stock Warning" threshold (`quantity_warning` on the
|
|
1461
|
+
* wire), null when disabled. NOT `stock_delimiter` — that field is the text
|
|
1462
|
+
* delimiter splitting the serial-key blob, never a number.
|
|
1463
|
+
*/
|
|
1464
|
+
lowStockThreshold: number | null;
|
|
1465
|
+
/** Per-product stock-counter override; null inherits the shop-wide setting. */
|
|
1466
|
+
hideStockCounter: boolean | null;
|
|
1467
|
+
/**
|
|
1468
|
+
* Quantity-tier discounts, sorted ascending by quantity. The backend applies
|
|
1469
|
+
* them at charge time, so the displayed line total must apply them too or
|
|
1470
|
+
* the buyer sees a higher number than they pay.
|
|
1471
|
+
*/
|
|
1472
|
+
volumeDiscounts: VolumeDiscountRule[];
|
|
1473
|
+
/**
|
|
1474
|
+
* Buyers may choose their own unit price at or above the resolved
|
|
1475
|
+
* product/variant price. Without an amount input the checkout silently
|
|
1476
|
+
* charges the minimum, so the buy box must render one when this is set.
|
|
1477
|
+
*/
|
|
1478
|
+
payWhatYouWant: boolean;
|
|
1479
|
+
/** Normalized product percent discount (0 when none, and always 0 for PWYW). */
|
|
1480
|
+
discountPercent: number;
|
|
1481
|
+
/**
|
|
1482
|
+
* Undiscounted starting unit price (min variant, or the base for
|
|
1483
|
+
* variant-less products) — the line-pricing input for the no-variant case.
|
|
1484
|
+
*/
|
|
1485
|
+
baseUnitPrice: number;
|
|
1486
|
+
/**
|
|
1487
|
+
* The product ROW's own undiscounted price, before variants are considered.
|
|
1488
|
+
* Distinct from `baseUnitPrice`, which already falls back to the cheapest
|
|
1489
|
+
* variant: a variant-led product carries 0 here, and that zero is what marks
|
|
1490
|
+
* its catalog price as a range ("$7.00 - $50.00") rather than a single
|
|
1491
|
+
* amount. Collapsing the two would price every variant product from its
|
|
1492
|
+
* minimum and silently drop the range.
|
|
1493
|
+
*/
|
|
1494
|
+
basePrice: number;
|
|
1495
|
+
priceVariants: StorefrontPriceVariant[];
|
|
1496
|
+
quantityMin: number;
|
|
1497
|
+
quantityMax: number | null;
|
|
1498
|
+
/** Units sold, for social proof. 0 when the shop has no sales for it. */
|
|
1499
|
+
soldCount: number;
|
|
1500
|
+
/** Merchant flagged this product as a bestseller. */
|
|
1501
|
+
bestseller: boolean;
|
|
1502
|
+
/**
|
|
1503
|
+
* Merchant-authored decorative badges, in their order, at most four. Empty
|
|
1504
|
+
* on almost every product. Purely merchandising — see `product-badges.ts`:
|
|
1505
|
+
* they never speak for availability, price or fulfilment, so they are read
|
|
1506
|
+
* alongside the theme's own signals rather than instead of them.
|
|
1507
|
+
*/
|
|
1508
|
+
badges: ProductBadge[];
|
|
1509
|
+
/** Merchant-assigned listing order; lower sorts first. */
|
|
1510
|
+
sortPriority: number;
|
|
1511
|
+
/** ISO timestamp the product was created, or null when the payload omits it. */
|
|
1512
|
+
createdAt: string | null;
|
|
1513
|
+
/** Categories this product belongs to. Empty when uncategorised. */
|
|
1514
|
+
categories: ProductCategoryRef[];
|
|
1515
|
+
/** Bundle offers this product takes part in. Empty when it is in none. */
|
|
1516
|
+
bundles: ProductBundle[];
|
|
1517
|
+
/**
|
|
1518
|
+
* Active quantity deals on this product, in the merchant's order. Empty when
|
|
1519
|
+
* the product has none or every rule is malformed — see `ProductAutomaticDeal`.
|
|
1520
|
+
*/
|
|
1521
|
+
automaticDeals: ProductAutomaticDeal[];
|
|
1522
|
+
/**
|
|
1523
|
+
* The merchant's product video as an embeddable, allowlisted `src`. null when
|
|
1524
|
+
* they set none OR when the host is not one the storefront frames — see
|
|
1525
|
+
* `video-embed.ts`.
|
|
1526
|
+
*/
|
|
1527
|
+
videoEmbedSrc: string | null;
|
|
1528
|
+
/** Paid extras offered alongside this product. Empty when it has none. */
|
|
1529
|
+
addons: ProductAddon[];
|
|
1530
|
+
/**
|
|
1531
|
+
* Details the merchant collects from the buyer for this product. Empty when
|
|
1532
|
+
* they configured none. A REQUIRED entry makes the product unsellable until
|
|
1533
|
+
* its value is on the cart line — see `ProductCustomField`.
|
|
1534
|
+
*/
|
|
1535
|
+
customFields: ProductCustomField[];
|
|
1536
|
+
/**
|
|
1537
|
+
* The merchant PAUSED this product ("Coming soon"). It is not a supply state
|
|
1538
|
+
* and has nothing to do with stock or `orderable`: the backend refuses to
|
|
1539
|
+
* sell it at any inventory level, so the storefront must not offer it. Same
|
|
1540
|
+
* precedence the document lane applies (`isProductOutOfStock` checks it
|
|
1541
|
+
* before it looks at stock at all).
|
|
1542
|
+
*/
|
|
1543
|
+
onHold: boolean;
|
|
1544
|
+
/**
|
|
1545
|
+
* Original price when a discount applies (price is the discounted one).
|
|
1546
|
+
* null when there is no discount — never a fabricated "was" price.
|
|
1547
|
+
*/
|
|
1548
|
+
compareAtPrice: number | null;
|
|
1549
|
+
/**
|
|
1550
|
+
* Average star rating (1-5) derived from REAL feedback only. null when the
|
|
1551
|
+
* product has zero feedback — the UI then renders no stars rather than a
|
|
1552
|
+
* fake score.
|
|
1553
|
+
*/
|
|
1554
|
+
rating: number | null;
|
|
1555
|
+
/** Number of feedback entries the rating is based on (0 when none). */
|
|
1556
|
+
ratingCount: number;
|
|
1557
|
+
/**
|
|
1558
|
+
* The feedback tally as the payload states it: how many buyers were
|
|
1559
|
+
* positive, neutral or negative. null exactly when `rating` is null.
|
|
1560
|
+
*/
|
|
1561
|
+
ratingTally: {
|
|
1562
|
+
positive: number;
|
|
1563
|
+
neutral: number;
|
|
1564
|
+
negative: number;
|
|
1565
|
+
} | null;
|
|
1566
|
+
/** Bulleted selling points shown in the buy box. */
|
|
1567
|
+
highlights: string[];
|
|
1568
|
+
/**
|
|
1569
|
+
* Merchant-defined key/value pairs, set per product in the dashboard under
|
|
1570
|
+
* Integrations -> Metadata. Always present ({} when the merchant set none)
|
|
1571
|
+
* and every value is a string, so `product.metadata.status` is safe to read
|
|
1572
|
+
* without a guard.
|
|
1573
|
+
*
|
|
1574
|
+
* This is your extension point: values you need that Shoppex has no field for
|
|
1575
|
+
* — a release status, an accent colour, a grouping tag. The merchant edits
|
|
1576
|
+
* them in the dashboard, so changing one does NOT require rebuilding this
|
|
1577
|
+
* storefront. Nothing here is rendered for you; read the keys your shop uses
|
|
1578
|
+
* and render them yourself.
|
|
1579
|
+
*
|
|
1580
|
+
* Public: everything in here is readable by anyone who opens the store.
|
|
1581
|
+
*/
|
|
1582
|
+
metadata: Record<string, string>;
|
|
1583
|
+
/** Extra merchant-authored content tabs ({title, html}). */
|
|
1584
|
+
descriptionTabs: ProductContentTab[];
|
|
1585
|
+
/** Per-product FAQ entries for the details accordion. */
|
|
1586
|
+
faqs: ProductFaq[];
|
|
1587
|
+
/** What this product states about its licence term. Resolve with resolveLicenseTerm. */
|
|
1588
|
+
license: LicenseStatement;
|
|
1589
|
+
/** Whether licensing is on at all; a stale term must not be advertised when it is off. */
|
|
1590
|
+
licensingEnabled: boolean;
|
|
1591
|
+
/** Estimated delivery time as a bare count (null when unset/instant). */
|
|
1592
|
+
deliveryTimeCount: number | null;
|
|
1593
|
+
/** Unit `deliveryTimeCount` is counted in: 'minutes' | 'hours' | 'days'. */
|
|
1594
|
+
deliveryTimeUnit: DeliveryTimeUnit;
|
|
1595
|
+
/**
|
|
1596
|
+
* Recurring billing, present only on a product the merchant sells as a
|
|
1597
|
+
* subscription. null on every other product.
|
|
1598
|
+
*/
|
|
1599
|
+
subscription: ProductSubscription | null;
|
|
1600
|
+
}
|
|
1601
|
+
interface StorefrontSettings {
|
|
1602
|
+
cartEnabled: boolean;
|
|
1603
|
+
hideOutOfStock: boolean;
|
|
1604
|
+
hideStockCounter: boolean;
|
|
1605
|
+
/**
|
|
1606
|
+
* Buyer-facing switches the MERCHANT owns, not the theme.
|
|
1607
|
+
*
|
|
1608
|
+
* A storefront that moves to the Advanced lane must not quietly re-enable
|
|
1609
|
+
* what its owner turned off, or offer a contact form the backend will refuse.
|
|
1610
|
+
* The document lane has honoured all three from the start.
|
|
1611
|
+
*/
|
|
1612
|
+
searchEnabled: boolean;
|
|
1613
|
+
contactFormEnabled: boolean;
|
|
1614
|
+
requireInvoiceForContact: boolean;
|
|
1615
|
+
/**
|
|
1616
|
+
* The merchant centres bundle cards. A shop-level presentation switch, not a
|
|
1617
|
+
* theme setting: the Easy lane reads `ctx.shop.center_group_titles` and
|
|
1618
|
+
* re-lays the GROUP card out — product cards are untouched.
|
|
1619
|
+
*/
|
|
1620
|
+
centerGroupTitles: boolean;
|
|
1621
|
+
/**
|
|
1622
|
+
* The merchant does not want the product's own picture used as its gallery.
|
|
1623
|
+
* The Easy lane's gallery producer empties the image list outright when this
|
|
1624
|
+
* is on, so the product page shows its placeholder frame instead.
|
|
1625
|
+
*/
|
|
1626
|
+
descriptionSkipDefaultImage: boolean;
|
|
1627
|
+
/**
|
|
1628
|
+
* Whether pictures INSIDE a product description are shown. Off strips every
|
|
1629
|
+
* `<img>` from the description and from each content tab, the same rewrite
|
|
1630
|
+
* `stripDescriptionImages` performs on the document lane. It does not touch
|
|
1631
|
+
* the gallery, which is `descriptionSkipDefaultImage`'s job.
|
|
1632
|
+
*/
|
|
1633
|
+
descriptionImage: boolean;
|
|
1634
|
+
/**
|
|
1635
|
+
* The merchant let buyers re-sort the catalog. Off hides the control on every
|
|
1636
|
+
* listing surface and pins the order to `defaultSort` — the same AND the Easy
|
|
1637
|
+
* lane applies (`enrichPageAllProductsBlockSettings`: the block setting and
|
|
1638
|
+
* `shop.sort_enabled` both have to be on).
|
|
1639
|
+
*/
|
|
1640
|
+
sortEnabled: boolean;
|
|
1641
|
+
/**
|
|
1642
|
+
* The order the merchant wants their catalog listed in, normalized to the
|
|
1643
|
+
* four keys every lane sorts by. It is the INITIAL sort of a listing and the
|
|
1644
|
+
* only sort when `sortEnabled` is false.
|
|
1645
|
+
*/
|
|
1646
|
+
defaultSort: StorefrontSortKey;
|
|
1647
|
+
/**
|
|
1648
|
+
* Every currency this shop sells in, the shop's own default first, uppercased
|
|
1649
|
+
* and de-duplicated. One entry (or none) means the shop has nothing to switch
|
|
1650
|
+
* between and the header renders no control.
|
|
1651
|
+
*/
|
|
1652
|
+
availableCurrencies: string[];
|
|
1653
|
+
/** The currency the merchant configured — the one served without a choice. */
|
|
1654
|
+
defaultCurrency: string;
|
|
1655
|
+
/**
|
|
1656
|
+
* The currency THIS page was priced in. It is the buyer's choice, their
|
|
1657
|
+
* country's match, or the shop default, resolved at the edge before the
|
|
1658
|
+
* catalog was fetched — so it is the only currency the amounts on screen are
|
|
1659
|
+
* actually in.
|
|
1660
|
+
*/
|
|
1661
|
+
activeCurrency: string;
|
|
1662
|
+
/**
|
|
1663
|
+
* Payment gateway codes the shop has connected. The cart turns them into the
|
|
1664
|
+
* brand marks under its checkout button — merchant fact, so an empty list
|
|
1665
|
+
* renders no row rather than a reassuring default.
|
|
1666
|
+
*/
|
|
1667
|
+
availableGateways: string[];
|
|
1668
|
+
}
|
|
1669
|
+
/**
|
|
1670
|
+
* The shop itself: the identity and the aggregate metrics the storefront
|
|
1671
|
+
* chrome renders. Merchant-owned facts, never theme content — a theme that
|
|
1672
|
+
* authored its own store name or sales count would be publishing a claim the
|
|
1673
|
+
* merchant never made.
|
|
1674
|
+
*/
|
|
1675
|
+
interface StorefrontShop {
|
|
1676
|
+
/** The shop's public display name. */
|
|
1677
|
+
name: string;
|
|
1678
|
+
/** Branding/domain logo, falling back to the profile picture for logo surfaces. */
|
|
1679
|
+
logoUrl: string | null;
|
|
1680
|
+
/** Shop profile picture, falling back to the logo only when no avatar exists. */
|
|
1681
|
+
avatarUrl: string | null;
|
|
1682
|
+
/** Merchant-configured social profiles, in the platform's presentation order. */
|
|
1683
|
+
socialLinks: SocialLink[];
|
|
1684
|
+
/**
|
|
1685
|
+
* The merchant's published terms of service, as authored HTML — null when
|
|
1686
|
+
* the shop has no terms or turned them off. `/terms` renders it through
|
|
1687
|
+
* `sanitizeMerchantHtml`, never raw.
|
|
1688
|
+
*/
|
|
1689
|
+
termsHtml: string | null;
|
|
1690
|
+
/** True when the merchant published terms and the `/terms` route has content. */
|
|
1691
|
+
hasPublishedTerms: boolean;
|
|
1692
|
+
/**
|
|
1693
|
+
* The shop's reward program, or null when it is off / has no perks. Merchant
|
|
1694
|
+
* fact: the headline and the perk labels are the merchant's wording, and a
|
|
1695
|
+
* theme that supplied its own would advertise a program nobody configured.
|
|
1696
|
+
*/
|
|
1697
|
+
rewardProgram: RewardProgramSummary | null;
|
|
1698
|
+
/**
|
|
1699
|
+
* The shop's wholesale reseller program, or null when the shop runs none.
|
|
1700
|
+
* Merchant fact: only a shop that actually accepts resellers may advertise
|
|
1701
|
+
* one, so the footer's "Become a reseller" link is gated on this and not on
|
|
1702
|
+
* theme content.
|
|
1703
|
+
*/
|
|
1704
|
+
resellerProgram: ResellerProgramSummary | null;
|
|
1705
|
+
stats: StorefrontShopStats;
|
|
1706
|
+
}
|
|
1707
|
+
/**
|
|
1708
|
+
* The reseller program off the wire, gated the way the Easy lane's footer link
|
|
1709
|
+
* gates it (`blocks/reseller-program-link.ts`): a summary is usable only when
|
|
1710
|
+
* it names an enrolment mode the portal can serve. Both modes render the SAME
|
|
1711
|
+
* link — the portal decides between one-click enrolment and the application
|
|
1712
|
+
* form.
|
|
1713
|
+
*/
|
|
1714
|
+
type ResellerProgramSummary = {
|
|
1715
|
+
enrollmentMode: 'OPEN' | 'APPLICATION';
|
|
1716
|
+
};
|
|
1717
|
+
/**
|
|
1718
|
+
* The reward-program teaser's data, gated exactly as the Easy lane gates it
|
|
1719
|
+
* (`blocks/reward-program-teaser.ts` + `mapRewardProgramSummary`): the program
|
|
1720
|
+
* must be enabled, it must carry a headline, and it must have at least one
|
|
1721
|
+
* usable perk. Anything short of that is not a program to advertise, so the
|
|
1722
|
+
* whole summary is null and every teaser renders nothing.
|
|
1723
|
+
*/
|
|
1724
|
+
type RewardProgramSummary = {
|
|
1725
|
+
headline: string;
|
|
1726
|
+
items: RewardProgramItem[];
|
|
1727
|
+
};
|
|
1728
|
+
/**
|
|
1729
|
+
* One perk. `triggerType` and `actionType` are required by the platform's own
|
|
1730
|
+
* mapper — a row missing either is a half-configured rule, not a perk — even
|
|
1731
|
+
* though only the label is rendered.
|
|
1732
|
+
*/
|
|
1733
|
+
type RewardProgramItem = {
|
|
1734
|
+
triggerType: string;
|
|
1735
|
+
actionType: string;
|
|
1736
|
+
label: string;
|
|
1737
|
+
};
|
|
1738
|
+
type SocialNetwork = 'discord' | 'telegram' | 'youtube' | 'twitter' | 'instagram' | 'tiktok' | 'facebook' | 'reddit' | 'email';
|
|
1739
|
+
type SocialLink = {
|
|
1740
|
+
network: SocialNetwork;
|
|
1741
|
+
href: string;
|
|
1742
|
+
};
|
|
1743
|
+
/**
|
|
1744
|
+
* Aggregate shop metrics for the store-stats strip.
|
|
1745
|
+
*
|
|
1746
|
+
* Counts are raw numbers, not display strings: formatting belongs to the
|
|
1747
|
+
* component. `averageRating` and `reviewCount` are independent on purpose —
|
|
1748
|
+
* a shop can carry a rating from transferred feedback without a countable
|
|
1749
|
+
* review, and rounding that to "no reviews" would drop a real number.
|
|
1750
|
+
*/
|
|
1751
|
+
interface StorefrontShopStats {
|
|
1752
|
+
productsSold: number;
|
|
1753
|
+
customers: number;
|
|
1754
|
+
reviewCount: number;
|
|
1755
|
+
/** 1-5, or null when the shop has no rating at all. */
|
|
1756
|
+
averageRating: number | null;
|
|
1757
|
+
/** Products the storefront lists — the theme counts them, the API does not. */
|
|
1758
|
+
productCount: number;
|
|
1759
|
+
}
|
|
1760
|
+
/**
|
|
1761
|
+
* A merchant-curated bundle of products. The wire payload references its
|
|
1762
|
+
* members by uniqid; the members themselves are ordinary catalog products, so
|
|
1763
|
+
* they arrive in `products` and are resolved from there.
|
|
1764
|
+
*/
|
|
1765
|
+
interface StorefrontGroup {
|
|
1766
|
+
id: string;
|
|
1767
|
+
title: string;
|
|
1768
|
+
description: string | null;
|
|
1769
|
+
imageUrl: string | null;
|
|
1770
|
+
/** Category the merchant filed the bundle under, if any. */
|
|
1771
|
+
categoryId: string | null;
|
|
1772
|
+
/** Member product uniqids, in the merchant's order. */
|
|
1773
|
+
productIds: string[];
|
|
1774
|
+
/** Member count as the API counts it — may exceed the resolvable members. */
|
|
1775
|
+
productsCount: number;
|
|
1776
|
+
sortPriority: number;
|
|
1777
|
+
}
|
|
1778
|
+
interface StorefrontCategory {
|
|
1779
|
+
id: string;
|
|
1780
|
+
slug: string;
|
|
1781
|
+
title: string;
|
|
1782
|
+
/** The merchant's category picture. Null leaves the filter chip text-only. */
|
|
1783
|
+
imageUrl: string | null;
|
|
1784
|
+
productsCount: number;
|
|
1785
|
+
groupsCount: number;
|
|
1786
|
+
}
|
|
1787
|
+
/**
|
|
1788
|
+
* One entry of the storefront's listing order.
|
|
1789
|
+
*
|
|
1790
|
+
* `items` is a pure display-order reference list: it decides WHICH products and
|
|
1791
|
+
* groups the storefront lists and in what order, and the referenced records are
|
|
1792
|
+
* delivered exactly once in `products` / `groups`. A storefront that listed
|
|
1793
|
+
* `products` directly would show unlisted rows (bundle members, hidden
|
|
1794
|
+
* products) the merchant never put on the shelf.
|
|
1795
|
+
*/
|
|
1796
|
+
interface StorefrontListingItem {
|
|
1797
|
+
type: 'product' | 'group';
|
|
1798
|
+
id: string;
|
|
1799
|
+
sortPriority: number;
|
|
1800
|
+
}
|
|
1801
|
+
interface ShoppexInitialData {
|
|
1802
|
+
store?: Record<string, unknown> & {
|
|
1803
|
+
builder_settings?: {
|
|
1804
|
+
theme?: {
|
|
1805
|
+
content?: unknown;
|
|
1806
|
+
};
|
|
1807
|
+
};
|
|
1808
|
+
};
|
|
1809
|
+
products?: unknown[];
|
|
1810
|
+
groups?: unknown[];
|
|
1811
|
+
items?: unknown[];
|
|
1812
|
+
categories?: unknown[];
|
|
1813
|
+
addons?: {
|
|
1814
|
+
items?: StorefrontAddon[];
|
|
1815
|
+
};
|
|
1816
|
+
}
|
|
1817
|
+
declare global {
|
|
1818
|
+
interface Window {
|
|
1819
|
+
__SHOPPEX_INITIAL__?: ShoppexInitialData;
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
/**
|
|
1823
|
+
* Whether the page carries an injected payload AT ALL.
|
|
1824
|
+
*
|
|
1825
|
+
* Not a validity check — an empty object passes. It separates "nothing injected
|
|
1826
|
+
* this document" from "something was injected and is wrong", which are two
|
|
1827
|
+
* different faults with two different fixes. See `boot` in `App.tsx`.
|
|
1828
|
+
*/
|
|
1829
|
+
declare function hasInjectedStorefrontData(): boolean;
|
|
1830
|
+
/** Merchant-authored content, or undefined when the store has none yet. */
|
|
1831
|
+
declare function getAuthoredContent(): unknown;
|
|
1832
|
+
declare function getProducts(): StorefrontProduct[];
|
|
1833
|
+
declare function getProductGroups(): StorefrontGroup[];
|
|
1834
|
+
declare function getStorefrontCategories(): StorefrontCategory[];
|
|
1835
|
+
declare function getStorefrontListing(): StorefrontListingItem[];
|
|
1836
|
+
declare function getStorefrontShop(productCount: number): StorefrontShop;
|
|
1837
|
+
/** The four orders every Shoppex lane sorts a catalog listing by. */
|
|
1838
|
+
declare const STOREFRONT_SORT_KEYS: readonly ["featured", "newest", "price-asc", "price-desc"];
|
|
1839
|
+
type StorefrontSortKey = (typeof STOREFRONT_SORT_KEYS)[number];
|
|
1840
|
+
/**
|
|
1841
|
+
* `store.default_sort` off the wire, normalized to one of the four keys.
|
|
1842
|
+
*
|
|
1843
|
+
* Field-for-field port of `normalizeStorefrontSortKey`
|
|
1844
|
+
* (`packages/storefront-render-core/src/normalize-storefront-sort-key.ts`),
|
|
1845
|
+
* because the column does NOT hold those keys: the response contract emits the
|
|
1846
|
+
* dashboard's enum (`DEFAULT`, `NEWEST`, `PRICE`, `PRICE_DESC`, `BESTSELLER`,
|
|
1847
|
+
* `NAME`, …). Reading it literally would leave every shop on 'featured' and the
|
|
1848
|
+
* merchant's chosen order would never reach either lane. An order this theme
|
|
1849
|
+
* cannot express — name, stock, type — degrades to the merchant's own shelf
|
|
1850
|
+
* order rather than to a different one.
|
|
1851
|
+
*/
|
|
1852
|
+
declare function parseStorefrontSortKey(value: unknown): StorefrontSortKey;
|
|
1853
|
+
declare function getStorefrontSettings(): StorefrontSettings;
|
|
1854
|
+
declare function getStorefrontAddons(): StorefrontAddon[];
|
|
1855
|
+
/**
|
|
1856
|
+
* The stock a buyer can actually reach, in ONE number: null is unlimited.
|
|
1857
|
+
*
|
|
1858
|
+
* Variants are the trap. A variant product's own `stock` column is not the
|
|
1859
|
+
* inventory anyone buys — the variants hold it — so a product with two
|
|
1860
|
+
* unbounded variants and a `stock` of 0 is not sold out. Serial products are
|
|
1861
|
+
* the exception: their stock IS the key pool on the product row.
|
|
1862
|
+
*/
|
|
1863
|
+
declare function resolveDisplayStock(product: StorefrontProduct): number | null;
|
|
1864
|
+
declare function isProductSoldOut(product: StorefrontProduct): boolean;
|
|
1865
|
+
/**
|
|
1866
|
+
* The stock behind ONE selected option — null is unlimited.
|
|
1867
|
+
*
|
|
1868
|
+
* A variant without its own counter inherits the product's, and the whole
|
|
1869
|
+
* product falls back to the product row when EVERY variant reports zero while
|
|
1870
|
+
* the product itself has units — that combination is a catalog that never
|
|
1871
|
+
* populated per-variant stock, not a sold-out product.
|
|
1872
|
+
*/
|
|
1873
|
+
declare function resolveVariantStock(product: StorefrontProduct, variant: StorefrontPriceVariant): number | null;
|
|
1874
|
+
/**
|
|
1875
|
+
* The one sold-out predicate every add path passes.
|
|
1876
|
+
*
|
|
1877
|
+
* PAYMENT PATH. A product-level read is not enough: an option of an otherwise
|
|
1878
|
+
* stocked product can be sold out on its own, and the backend refuses that line
|
|
1879
|
+
* at invoice creation (`errors.checkout.insufficient_stock`). Adding it anyway
|
|
1880
|
+
* only moves the dead end to checkout.
|
|
1881
|
+
*/
|
|
1882
|
+
declare function isSelectionSoldOut(product: StorefrontProduct, variant: StorefrontPriceVariant | null): boolean;
|
|
1883
|
+
/**
|
|
1884
|
+
* The stock counter resolves per product: a product's own hide_stock_counter
|
|
1885
|
+
* wins, null inherits the shop-wide setting — same rule the document lane's
|
|
1886
|
+
* readHideStockCounterForProduct applies.
|
|
1887
|
+
*/
|
|
1888
|
+
declare function resolveHideStockCounter(product: StorefrontProduct, settings: StorefrontSettings): boolean;
|
|
1889
|
+
/**
|
|
1890
|
+
* How many units the buyer may put on ONE line, for the selected option.
|
|
1891
|
+
*
|
|
1892
|
+
* A variant overrides the product's own bounds, and the stock caps whatever the
|
|
1893
|
+
* merchant configured: offering a stepper that runs past the inventory produces
|
|
1894
|
+
* a cart the quote then rejects. `max: null` is unlimited, never zero.
|
|
1895
|
+
*/
|
|
1896
|
+
declare function resolveQuantityBounds(product: StorefrontProduct, variant: StorefrontPriceVariant | null): {
|
|
1897
|
+
min: number;
|
|
1898
|
+
max: number | null;
|
|
1899
|
+
};
|
|
1900
|
+
type LicenseTerm = {
|
|
1901
|
+
kind: 'lifetime';
|
|
1902
|
+
} | {
|
|
1903
|
+
kind: 'period';
|
|
1904
|
+
days: number;
|
|
1905
|
+
};
|
|
1906
|
+
/**
|
|
1907
|
+
* What ONE row — a product or one of its options — states about its own term.
|
|
1908
|
+
*
|
|
1909
|
+
* `inherit` is an option saying the product answers for it; only an option may say it, because the
|
|
1910
|
+
* product is the root of the term. `unresolved` is a row stating a term nobody can read: an unknown
|
|
1911
|
+
* mode, a PERIOD carrying no days, a legacy `'90d'` that was never validated.
|
|
1912
|
+
*
|
|
1913
|
+
* These three are separate states on purpose. They used to share one nullable column — `NULL` meant
|
|
1914
|
+
* "inherit" on an option and "nothing" on a product — and on 2026-08-18 the unnamed state was
|
|
1915
|
+
* rendered as "Lifetime": 5,333 licences were sold as perpetual and issued with a 30-day expiry.
|
|
1916
|
+
* Neither `inherit` nor `unresolved` may become a term here.
|
|
1917
|
+
*/
|
|
1918
|
+
type LicenseStatement = LicenseTerm | {
|
|
1919
|
+
kind: 'inherit';
|
|
1920
|
+
} | {
|
|
1921
|
+
kind: 'unresolved';
|
|
1922
|
+
};
|
|
1923
|
+
/**
|
|
1924
|
+
* The term the buyer is being sold for the option they have selected, or `null` when nothing about
|
|
1925
|
+
* it is resolvable — licensing off, a mode nobody can read, a PERIOD with no days, an option
|
|
1926
|
+
* inheriting from a product that states nothing itself.
|
|
1927
|
+
*
|
|
1928
|
+
* `null` means the page renders NO licence row. It never means "Lifetime".
|
|
1929
|
+
*/
|
|
1930
|
+
declare function resolveLicenseTerm(product: StorefrontProduct, variant: StorefrontPriceVariant | null): LicenseTerm | null;
|
|
1931
|
+
/** "Lifetime" / "30 days" — the same wording the Easy lane prints for the same product. */
|
|
1932
|
+
declare function formatLicenseTerm(term: LicenseTerm): string;
|
|
1933
|
+
/** Half-up rounding to cents. Every displayed money amount rounds through here. */
|
|
1934
|
+
declare function roundCurrency(value: number): number;
|
|
1935
|
+
/**
|
|
1936
|
+
* Line total the way the quote computes it: undiscounted subtotal, percent
|
|
1937
|
+
* discount applied at LINE level (not per rounded unit — $9.99 at 15% × 10 is
|
|
1938
|
+
* $84.92, not 10 × $8.49), then the volume tier. Intermediate values stay
|
|
1939
|
+
* unrounded like the quote's Decimal chain; only the final amount rounds —
|
|
1940
|
+
* rounding between the two percent stages shifts the cent ($76.43 vs the
|
|
1941
|
+
* quoted $76.42 for 15% + 10% on a $99.90 line). Every displayed quantity
|
|
1942
|
+
* price must come from here or it disagrees with checkout.
|
|
1943
|
+
*/
|
|
1944
|
+
declare function calculateDisplayLineTotal(baseUnitPrice: number, quantity: number, discountPercent: number, rules: VolumeDiscountRule[]): number;
|
|
1945
|
+
/**
|
|
1946
|
+
* Delivery units, mirroring the `products.delivery_time_unit` CHECK constraint.
|
|
1947
|
+
* The column is NOT NULL DEFAULT 'days', so an absent value only comes from a
|
|
1948
|
+
* payload minted before the unit existed — and days is what it meant.
|
|
1949
|
+
*/
|
|
1950
|
+
declare const DELIVERY_TIME_UNITS: readonly ["minutes", "hours", "days"];
|
|
1951
|
+
type DeliveryTimeUnit = (typeof DELIVERY_TIME_UNITS)[number];
|
|
1952
|
+
/** "1 hour" / "3 days" — the count with its unit, singular when the count is one. */
|
|
1953
|
+
declare function formatDeliveryTime(count: number, unit: DeliveryTimeUnit): string;
|
|
1954
|
+
|
|
1955
|
+
/**
|
|
1956
|
+
* The per-product details a buyer has to supply before an invoice can exist.
|
|
1957
|
+
*
|
|
1958
|
+
* PAYMENT PATH, and the reason this module exists at all: a merchant who marks
|
|
1959
|
+
* one product custom field REQUIRED cannot sell that product until its value is
|
|
1960
|
+
* on the cart line. `apps/backend/src/services/storefront/invoices/from-cart.ts`
|
|
1961
|
+
* validates every line's `custom_fields` and rejects the whole request with
|
|
1962
|
+
* `custom_fields_validation_failed` — no invoice is created, so the buyer gets a
|
|
1963
|
+
* dead checkout button and the merchant gets no order.
|
|
1964
|
+
*
|
|
1965
|
+
* WHERE THE VALUES GO. Not into the checkout call: the SDK's `checkout()` has no
|
|
1966
|
+
* custom-field option. `mapCartItemsForApi` (`packages/sdk/src/modules/checkout.ts`)
|
|
1967
|
+
* reads `custom_fields` off each CART LINE, so the values are written onto the
|
|
1968
|
+
* lines with `updateCartItem(lineId, { custom_fields })` BEFORE the hand-off.
|
|
1969
|
+
* That is exactly what the Easy lane does — see
|
|
1970
|
+
* `packages/storefront-commerce/src/checkout-custom-fields.ts`, whose
|
|
1971
|
+
* `writeCustomFieldsToCart` runs on the same contract.
|
|
1972
|
+
*
|
|
1973
|
+
* WHERE THE DEFINITIONS COME FROM. The Easy lane fetches each cart product
|
|
1974
|
+
* (`getProduct(uniqid)`) because it has no catalog in the page. This lane does:
|
|
1975
|
+
* `window.__SHOPPEX_INITIAL__` carries every product, listed or not, so the
|
|
1976
|
+
* definitions are read straight out of it. There is no request to fail — but
|
|
1977
|
+
* there is still a line whose product is not in the payload, and that case
|
|
1978
|
+
* FAILS CLOSED for the same reason the Easy lane's failed fetch does: an
|
|
1979
|
+
* unresolvable product may carry required fields, and treating "unknown" as
|
|
1980
|
+
* "none" is how a checkout submits without them.
|
|
1981
|
+
*
|
|
1982
|
+
* VALUES ARE PER PRODUCT, not per cart line. Two lines of the same product (a
|
|
1983
|
+
* different option, a different extra) describe the same thing being bought
|
|
1984
|
+
* twice, and the merchant asked their question once — so both lines receive the
|
|
1985
|
+
* same answers, which is also what keeps them from splitting into two identities
|
|
1986
|
+
* the buyer never chose. It differs from the Easy lane, which dedupes by field
|
|
1987
|
+
* NAME across the whole cart: two products that both ask "Steam ID" are two
|
|
1988
|
+
* questions here, and one there.
|
|
1989
|
+
*/
|
|
1990
|
+
|
|
1991
|
+
/** One product's questions, and the product they belong to. */
|
|
1992
|
+
interface CheckoutFieldGroup {
|
|
1993
|
+
productId: string;
|
|
1994
|
+
/** The product's own title — the section heading the buyer reads. */
|
|
1995
|
+
productName: string;
|
|
1996
|
+
fields: ProductCustomField[];
|
|
1997
|
+
}
|
|
1998
|
+
/**
|
|
1999
|
+
* What the cart needs before it can be checked out.
|
|
2000
|
+
*
|
|
2001
|
+
* `resolved: false` is the fail-closed state: at least one line names a product
|
|
2002
|
+
* the payload does not carry, so nothing can be said about what it requires.
|
|
2003
|
+
*/
|
|
2004
|
+
type CheckoutFields = {
|
|
2005
|
+
resolved: true;
|
|
2006
|
+
groups: CheckoutFieldGroup[];
|
|
2007
|
+
hasRequired: boolean;
|
|
2008
|
+
} | {
|
|
2009
|
+
resolved: false;
|
|
2010
|
+
unresolvedProductIds: string[];
|
|
2011
|
+
};
|
|
2012
|
+
/** Answers, keyed product id → field name → value. */
|
|
2013
|
+
type CheckoutFieldValues = Record<string, Record<string, string>>;
|
|
2014
|
+
/**
|
|
2015
|
+
* The questions the current cart raises, grouped by product and in cart order.
|
|
2016
|
+
*
|
|
2017
|
+
* Products with no custom fields produce no group — they are resolved, they
|
|
2018
|
+
* simply ask nothing.
|
|
2019
|
+
*/
|
|
2020
|
+
declare function resolveCheckoutFields(lines: readonly SdkCartItem[]): CheckoutFields;
|
|
2021
|
+
/**
|
|
2022
|
+
* Whether the checkout CTA has to route through the details form instead of
|
|
2023
|
+
* handing off straight away.
|
|
2024
|
+
*
|
|
2025
|
+
* True for the fail-closed state as well: a cart whose products cannot be
|
|
2026
|
+
* resolved must not be handed to `checkout()` on the assumption that it asks
|
|
2027
|
+
* nothing. The form is where that is said out loud.
|
|
2028
|
+
*
|
|
2029
|
+
* Optional-only fields do NOT gate the hand-off — nothing about the invoice
|
|
2030
|
+
* depends on them, and stopping a buyer for a question they may leave blank
|
|
2031
|
+
* would be this theme inventing a step. They are still collected whenever the
|
|
2032
|
+
* form does open, so a cart that has to stop for one product's required field
|
|
2033
|
+
* gathers every other answer in the same pass.
|
|
2034
|
+
*/
|
|
2035
|
+
declare function checkoutNeedsDetails(fields: CheckoutFields): boolean;
|
|
2036
|
+
/**
|
|
2037
|
+
* The form's starting values: whatever the cart lines already carry, else the
|
|
2038
|
+
* merchant's own default for the field.
|
|
2039
|
+
*
|
|
2040
|
+
* Pre-filling from the line is what makes the form survive a buyer going back to
|
|
2041
|
+
* the cart and returning — the answers are stored on the line, not in React.
|
|
2042
|
+
*/
|
|
2043
|
+
declare function buildInitialCheckoutFieldValues(groups: readonly CheckoutFieldGroup[], lines: readonly SdkCartItem[]): CheckoutFieldValues;
|
|
2044
|
+
/**
|
|
2045
|
+
* Whether a checkbox answer counts as ticked.
|
|
2046
|
+
*
|
|
2047
|
+
* The stored value is a string, and the four spellings below are the ones the
|
|
2048
|
+
* SDK's `isStorefrontCheckboxCustomFieldValueChecked` accepts — a checkbox the
|
|
2049
|
+
* two lanes read differently is a required tick one lane honours and the other
|
|
2050
|
+
* silently drops.
|
|
2051
|
+
*/
|
|
2052
|
+
declare function isCheckboxValueChecked(value: string | undefined): boolean;
|
|
2053
|
+
/**
|
|
2054
|
+
* The buyer-facing problem with one answer, or null when it is acceptable.
|
|
2055
|
+
*
|
|
2056
|
+
* A port of the SDK's `validateStorefrontCustomFieldValue`, down to the wording:
|
|
2057
|
+
* the Easy lane refuses the same values with the same sentences, and the backend
|
|
2058
|
+
* refuses them again when the invoice is built. A malformed merchant regex
|
|
2059
|
+
* validates NOTHING rather than blocking every buyer on a pattern that cannot
|
|
2060
|
+
* compile — the merchant's mistake must not become the buyer's dead end, and the
|
|
2061
|
+
* backend still holds the line.
|
|
2062
|
+
*/
|
|
2063
|
+
declare function validateCheckoutFieldValue(field: ProductCustomField, value: string): string | null;
|
|
2064
|
+
/**
|
|
2065
|
+
* Every problem the form has to show, keyed `<productId>.<fieldName>` so two
|
|
2066
|
+
* products asking the same question keep their own message.
|
|
2067
|
+
*/
|
|
2068
|
+
declare function validateCheckoutFields(groups: readonly CheckoutFieldGroup[], values: CheckoutFieldValues): Record<string, string>;
|
|
2069
|
+
declare function checkoutFieldKey(productId: string, fieldName: string): string;
|
|
2070
|
+
/**
|
|
2071
|
+
* One product's answers in the shape a cart line stores.
|
|
2072
|
+
*
|
|
2073
|
+
* A port of the SDK's `buildStorefrontCustomFieldPayload`: an unticked checkbox
|
|
2074
|
+
* and an empty answer are OMITTED rather than stored blank, which is what keeps
|
|
2075
|
+
* an optional field the buyer skipped from becoming an empty string the backend
|
|
2076
|
+
* has to interpret. A ticked checkbox is always the literal `'true'`.
|
|
2077
|
+
*/
|
|
2078
|
+
declare function buildCustomFieldPayload(fields: readonly ProductCustomField[], values: Record<string, string> | undefined): Record<string, string>;
|
|
2079
|
+
/**
|
|
2080
|
+
* The required answers that are NOT on the cart, read back out of the SDK.
|
|
2081
|
+
*
|
|
2082
|
+
* PAYMENT PATH. The write-back is `updateCartItem`, which the SDK can drop the
|
|
2083
|
+
* same way it drops an add — its storage wrapper catches a quota or blocked
|
|
2084
|
+
* write and only logs it. A hand-off that trusted the write would create no
|
|
2085
|
+
* invoice at all (the backend rejects the cart) after telling the buyer their
|
|
2086
|
+
* details were taken. So the cart is read back and every required field is
|
|
2087
|
+
* checked against the value that actually landed, with the same validator the
|
|
2088
|
+
* form used.
|
|
2089
|
+
*
|
|
2090
|
+
* Returns the buyer-facing messages, empty when the cart is ready to submit.
|
|
2091
|
+
*/
|
|
2092
|
+
declare function missingRequiredCheckoutValues(groups: readonly CheckoutFieldGroup[], lines: readonly SdkCartItem[]): string[];
|
|
2093
|
+
/**
|
|
2094
|
+
* Whether a details write changed what the cart PRICES.
|
|
2095
|
+
*
|
|
2096
|
+
* PAYMENT PATH, and the one consequence of the identity merge that is not
|
|
2097
|
+
* harmless. Two lines of the same product and option can exist while their
|
|
2098
|
+
* `custom_fields` differ — the only way this theme's `add` produces one is a
|
|
2099
|
+
* buyer who answered once, came back from a checkout that did not complete, and
|
|
2100
|
+
* added the product again from the PDP or through Buy Now, because `add` sends
|
|
2101
|
+
* no answers at all. Giving both the SAME answers collapses them into one line
|
|
2102
|
+
* whose quantity is the SUM, and the quote proof the SDK is holding is bound to
|
|
2103
|
+
* a hash of the cart's products, options and QUANTITIES (`custom_fields` and
|
|
2104
|
+
* `delivery_instructions` are deliberately NOT hashed — see
|
|
2105
|
+
* `apps/backend/src/services/storefront/cart/cartQuoteToken.ts`). So the answers
|
|
2106
|
+
* alone never stale a proof, and a merge always does.
|
|
2107
|
+
*
|
|
2108
|
+
* Handing off anyway is refused by `/from-cart` as `quote_token_stale`. The
|
|
2109
|
+
* hand-off already recovers from that refusal by re-quoting, so nothing is
|
|
2110
|
+
* mispriced — but the buyer spends a click and a server round trip on a message
|
|
2111
|
+
* about a cart change they did not make. Answering it here instead re-quotes
|
|
2112
|
+
* straight away and names what actually happened.
|
|
2113
|
+
*
|
|
2114
|
+
* Compared on the priced shape rather than on `line_id`s, because every id in
|
|
2115
|
+
* the cart changes when the answers are written — that is the identity rehash
|
|
2116
|
+
* doing its job, not a repricing. A merge is the only thing this write can do
|
|
2117
|
+
* to that shape, and it always removes a line, so the two multisets can never
|
|
2118
|
+
* coincide.
|
|
2119
|
+
*/
|
|
2120
|
+
declare function checkoutFieldWriteRepriced(before: readonly SdkCartItem[], after: readonly SdkCartItem[]): boolean;
|
|
2121
|
+
|
|
2122
|
+
type CartContextValue = {
|
|
2123
|
+
lines: SdkCartItem[];
|
|
2124
|
+
count: number;
|
|
2125
|
+
quote: SdkCartQuote | null;
|
|
2126
|
+
/** The server could not price the cart. Written by the refresh, only. */
|
|
2127
|
+
quoteError: string | null;
|
|
2128
|
+
/**
|
|
2129
|
+
* A cart WRITE failed — an add the SDK refused or dropped, a bundle that had
|
|
2130
|
+
* to be rolled back. Held apart from `quoteError` on purpose: a refresh must
|
|
2131
|
+
* never be able to clear it. See `mutate`.
|
|
2132
|
+
*/
|
|
2133
|
+
mutationError: string | null;
|
|
2134
|
+
/**
|
|
2135
|
+
* The last code the buyer applied was refused by the quote.
|
|
2136
|
+
*
|
|
2137
|
+
* Written only by `applyCoupon` and cleared only by the buyer's next cart
|
|
2138
|
+
* action (see `mutate`). It cannot be derived from the quote any more,
|
|
2139
|
+
* because a refused code is REMOVED from storage the moment it is refused —
|
|
2140
|
+
* the very next quote therefore carries no code and would erase the message
|
|
2141
|
+
* that explains why.
|
|
2142
|
+
*/
|
|
2143
|
+
couponRejected: boolean;
|
|
2144
|
+
/**
|
|
2145
|
+
* True when the code the CURRENT quote was priced with is the ambient
|
|
2146
|
+
* affiliate referral rather than one the buyer applied.
|
|
2147
|
+
*
|
|
2148
|
+
* The promo field decides between its entry row and its "applied" chip from
|
|
2149
|
+
* `quote.coupon_applied`, which was a complete answer while the only code
|
|
2150
|
+
* that could ever reach a quote was one the buyer typed. It is not one any
|
|
2151
|
+
* more: an accepted ambient referral sets that same flag, and the chip would
|
|
2152
|
+
* then congratulate the buyer on a code they never entered, print it blank
|
|
2153
|
+
* (the field is empty, because nothing was applied), and offer a Remove
|
|
2154
|
+
* button wired to cart-coupon storage the referral does not live in — so it
|
|
2155
|
+
* would reappear on the next quote. Kept next to the quote it describes and
|
|
2156
|
+
* written by the same statements, so the two can never disagree.
|
|
2157
|
+
*/
|
|
2158
|
+
quotedAmbientAffiliate: boolean;
|
|
2159
|
+
/**
|
|
2160
|
+
* The shop's checkout maximum, stated once, while the CURRENT quote says this
|
|
2161
|
+
* cart is over it. Null while it is within the limit or while no quote has
|
|
2162
|
+
* settled.
|
|
2163
|
+
*
|
|
2164
|
+
* PAYMENT PATH. The summary prints it beside the total — an over-limit cart
|
|
2165
|
+
* is a cart the checkout refuses, and the buyer cannot act on a limit nobody
|
|
2166
|
+
* names — and `checkout` refuses the hand-off on the same fact, exactly as it
|
|
2167
|
+
* refuses an unsellable line. See `resolveCartMaximumWarning`.
|
|
2168
|
+
*/
|
|
2169
|
+
cartMaxWarning: string | null;
|
|
2170
|
+
/**
|
|
2171
|
+
* The settled quote contains a PHYSICAL line, so its total excludes shipping,
|
|
2172
|
+
* which the hosted checkout prices from the buyer's address. False while no
|
|
2173
|
+
* quote has settled. The summary says so under the total.
|
|
2174
|
+
*/
|
|
2175
|
+
requiresShipping: boolean;
|
|
2176
|
+
checkoutError: string | null;
|
|
2177
|
+
/**
|
|
2178
|
+
* True when the LAST hand-off was refused because the referral on this cart
|
|
2179
|
+
* is the buyer's own (`affiliate_code_not_applicable`).
|
|
2180
|
+
*
|
|
2181
|
+
* It needs its own flag rather than being read out of `checkoutError`,
|
|
2182
|
+
* because it is the one refusal the buyer cannot resolve by re-reading the
|
|
2183
|
+
* message: the code is ambient — nobody typed it, no field shows it, and no
|
|
2184
|
+
* control removes it — so without an explicit exit the cart refuses every
|
|
2185
|
+
* retry forever. The surface renders `clearBlockedReferral` next to the
|
|
2186
|
+
* message; the flag is what tells it to.
|
|
2187
|
+
*/
|
|
2188
|
+
selfReferralBlocked: boolean;
|
|
2189
|
+
/**
|
|
2190
|
+
* The exit. Drops the referral attribution the SDK is carrying, re-quotes
|
|
2191
|
+
* without it, and lets the buyer check out again. See the implementation.
|
|
2192
|
+
*/
|
|
2193
|
+
clearBlockedReferral: () => Promise<void>;
|
|
2194
|
+
busy: boolean;
|
|
2195
|
+
couponInput: string;
|
|
2196
|
+
setCouponInput: (value: string) => void;
|
|
2197
|
+
drawerOpen: boolean;
|
|
2198
|
+
setDrawerOpen: (open: boolean) => void;
|
|
2199
|
+
add: (product: StorefrontProduct, variant: StorefrontPriceVariant | null, quantity: number, options?: {
|
|
2200
|
+
/**
|
|
2201
|
+
* Buyer-chosen unit price for a pay-what-you-want product. Part of the
|
|
2202
|
+
* cart line identity — the server validates it against the minimum.
|
|
2203
|
+
*/
|
|
2204
|
+
payWhatYouWantPrice?: number;
|
|
2205
|
+
/** Extras the buyer ticked in the buy box. Also part of line identity. */
|
|
2206
|
+
addons?: ProductAddon[];
|
|
2207
|
+
}) => Promise<void>;
|
|
2208
|
+
/**
|
|
2209
|
+
* Adds EVERY member of a bundle offer, as one operation.
|
|
2210
|
+
*
|
|
2211
|
+
* Bundle members arrive as a compact summary rather than a full product, so
|
|
2212
|
+
* they cannot go through `add` — their quantity bounds are resolved from the
|
|
2213
|
+
* member's own catalog product instead, and a member that cannot be resolved
|
|
2214
|
+
* is rejected rather than added. The set is all-or-nothing: see the
|
|
2215
|
+
* implementation for why a half-added bundle is worse than none of it.
|
|
2216
|
+
*/
|
|
2217
|
+
addBundle: (items: readonly ProductBundleItem[]) => Promise<void>;
|
|
2218
|
+
update: (lineId: string, quantity: number) => Promise<void>;
|
|
2219
|
+
remove: (lineId: string) => Promise<void>;
|
|
2220
|
+
clear: () => Promise<void>;
|
|
2221
|
+
applyCoupon: () => Promise<void>;
|
|
2222
|
+
removeCoupon: () => Promise<void>;
|
|
2223
|
+
/**
|
|
2224
|
+
* The per-product details this cart's products require, resolved from the
|
|
2225
|
+
* injected catalog. `/checkout` renders the form from it.
|
|
2226
|
+
*/
|
|
2227
|
+
checkoutFields: CheckoutFields;
|
|
2228
|
+
/**
|
|
2229
|
+
* True when the hand-off must go through `/checkout` to collect those details
|
|
2230
|
+
* first. Read it on CLICK rather than from a render closure after a cart
|
|
2231
|
+
* write — `readCheckoutNeedsDetails` exists for exactly that.
|
|
2232
|
+
*/
|
|
2233
|
+
checkoutNeedsDetails: boolean;
|
|
2234
|
+
/**
|
|
2235
|
+
* Hands over to Hosted Checkout. `customFieldValues` writes the buyer's
|
|
2236
|
+
* answers onto the cart lines first and is what `/checkout` submits; every
|
|
2237
|
+
* other surface hands off with no values, and may only do so when
|
|
2238
|
+
* `checkoutNeedsDetails` is false.
|
|
2239
|
+
*/
|
|
2240
|
+
checkout: (customFieldValues?: CheckoutFieldValues) => Promise<void>;
|
|
2241
|
+
};
|
|
2242
|
+
/**
|
|
2243
|
+
* Whether the checkout CTA has to stop for details, read from the SDK's cart
|
|
2244
|
+
* RIGHT NOW rather than from React state.
|
|
2245
|
+
*
|
|
2246
|
+
* PAYMENT PATH. Buy Now adds a line and then hands off in the same click. The
|
|
2247
|
+
* `checkoutNeedsDetails` on the context describes the cart as it was when the
|
|
2248
|
+
* component last rendered, which is the cart BEFORE that add — a product whose
|
|
2249
|
+
* required fields the add has just brought into the cart would be handed
|
|
2250
|
+
* straight to a checkout that cannot be created.
|
|
2251
|
+
*/
|
|
2252
|
+
declare function readCheckoutNeedsDetails(): boolean;
|
|
2253
|
+
declare function CartProvider({ children }: {
|
|
2254
|
+
children: ReactNode;
|
|
2255
|
+
}): react_jsx_runtime.JSX.Element;
|
|
2256
|
+
declare function useCart(): CartContextValue;
|
|
2257
|
+
/**
|
|
2258
|
+
* Starts a cart mutation from a control that has nothing to chain onto it.
|
|
2259
|
+
*
|
|
2260
|
+
* Every mutation now REJECTS when the write fails, which is what lets Buy Now
|
|
2261
|
+
* stop before checkout. A stepper, a remove button or a bundle offer has no next
|
|
2262
|
+
* step to stop: its failure is already rendered from `mutationError` on the
|
|
2263
|
+
* drawer and `/cart`, the surfaces those controls live on. This states that at
|
|
2264
|
+
* the call site rather than leaving the rejection unhandled — the error is
|
|
2265
|
+
* displayed, not swallowed.
|
|
2266
|
+
*/
|
|
2267
|
+
declare function startCartMutation(mutation: Promise<void>): void;
|
|
2268
|
+
|
|
2269
|
+
/**
|
|
2270
|
+
* Reading THE CART BOUNDS CONTRACT off a settled quote.
|
|
2271
|
+
*
|
|
2272
|
+
* PAYMENT PATH. Its own module, free of React and of the SDK singleton, so the
|
|
2273
|
+
* rules can be read and exercised on their own: the cart provider hands them the
|
|
2274
|
+
* quote's `lines` and the cart it holds, and they answer what has to change.
|
|
2275
|
+
*
|
|
2276
|
+
* The producer is the backend's `CartQuoteService` (`CartQuoteLine`), and its
|
|
2277
|
+
* field rules — which this module exists to honour exactly — are:
|
|
2278
|
+
*
|
|
2279
|
+
* `variant_id` the RESOLVED option id, or `null` for a product with no
|
|
2280
|
+
* variants, which the cart stores under the `'default'` marker.
|
|
2281
|
+
* The match has to bridge the two spellings.
|
|
2282
|
+
* `min_quantity` OMITTED when it is 1.
|
|
2283
|
+
* `max_quantity` OMITTED when nothing bounds the line. `-1` is NEVER emitted;
|
|
2284
|
+
* omission IS the unbounded sentinel, and a missing key must
|
|
2285
|
+
* never be read as 0 — that says "sold out". SHARED when
|
|
2286
|
+
* several cart lines name one product+option: they draw from
|
|
2287
|
+
* one shelf and the checkout sums them, so the value is that
|
|
2288
|
+
* line's SHARE of the option's ceiling.
|
|
2289
|
+
* `sellable` false = this line cannot be ordered as the cart stands:
|
|
2290
|
+
* either no quantity of the option can be ordered at all, or
|
|
2291
|
+
* the cart's lines of this option together ask for more units
|
|
2292
|
+
* than the option's ceiling, which the checkout refuses on the
|
|
2293
|
+
* SUM. The line is still priced and still counted — it is one
|
|
2294
|
+
* the buyer must reduce or remove. It answers for the OPTION
|
|
2295
|
+
* and for the whole cart's demand on it, so every line of one
|
|
2296
|
+
* option carries the same value.
|
|
2297
|
+
*
|
|
2298
|
+
* CONSUMER DUTY, from the same contract, and the reason this module exists in
|
|
2299
|
+
* the Advanced lane at all: cart lines hold an ADD-TIME SNAPSHOT of their
|
|
2300
|
+
* bounds, so a line goes stale the moment stock moves or the merchant edits the
|
|
2301
|
+
* limits — and the stepper is driven off those stored values. Heal them by
|
|
2302
|
+
* REPLACING, never merging: an absent `max_quantity` means the ceiling was
|
|
2303
|
+
* REMOVED, and a merge would keep a stale one forever. And refuse the checkout
|
|
2304
|
+
* hand-off while any line is unsellable, because that cart is exactly the one
|
|
2305
|
+
* `/from-cart` rejects.
|
|
2306
|
+
*/
|
|
2307
|
+
|
|
2308
|
+
type CartQuoteLineBounds = {
|
|
2309
|
+
productId: string;
|
|
2310
|
+
/** `'default'` for the contract's `null`, so it compares against a cart line. */
|
|
2311
|
+
variantId: string;
|
|
2312
|
+
/** The quantity this row was quoted AT, after per-line summing. */
|
|
2313
|
+
quantity: number;
|
|
2314
|
+
/** `null` = unstated, which is the SDK's own default of 1. */
|
|
2315
|
+
minQuantity: number | null;
|
|
2316
|
+
/** `null` = unbounded. Never a negative sentinel. */
|
|
2317
|
+
maxQuantity: number | null;
|
|
2318
|
+
sellable: boolean;
|
|
2319
|
+
};
|
|
2320
|
+
/** One healing write: the line to patch, and the bounds to REPLACE it with. */
|
|
2321
|
+
type CartBoundsHeal = {
|
|
2322
|
+
lineId: string;
|
|
2323
|
+
/**
|
|
2324
|
+
* `null` DELETES the stored bound — the contract's "no bound" — in the SDK's
|
|
2325
|
+
* own tri-state (absent = leave alone, null = clear, number = set). It is not
|
|
2326
|
+
* `undefined`, because `undefined` is exactly what the SDK reads as "this
|
|
2327
|
+
* update did not mention the field", so a removed ceiling would stick to the
|
|
2328
|
+
* line forever.
|
|
2329
|
+
*/
|
|
2330
|
+
minQuantity: number | null;
|
|
2331
|
+
maxQuantity: number | null;
|
|
2332
|
+
};
|
|
2333
|
+
/** Parse `lines` off a settled quote. An absent or malformed list yields none. */
|
|
2334
|
+
declare function parseCartQuoteLines(raw: SdkCartQuoteLine[] | undefined): CartQuoteLineBounds[];
|
|
2335
|
+
/**
|
|
2336
|
+
* The writes needed to bring stored line bounds back in line with the quote.
|
|
2337
|
+
*
|
|
2338
|
+
* Only lines whose stored bounds actually DIFFER are returned, which is what
|
|
2339
|
+
* keeps this from looping: writing the same values back would emit another
|
|
2340
|
+
* cart-changed, re-quote, and arrive here again. Cart lines the quote did not
|
|
2341
|
+
* mention are left untouched — the quote answers for the cart it was given, and
|
|
2342
|
+
* a line added after it was sent has no answer in it yet.
|
|
2343
|
+
*/
|
|
2344
|
+
declare function resolveCartBoundsHeals(lines: readonly SdkCartItem[], quoted: readonly CartQuoteLineBounds[]): CartBoundsHeal[];
|
|
2345
|
+
/** The stored `line_id`s the quote says cannot be ordered right now. */
|
|
2346
|
+
declare function resolveUnsellableLineIds(lines: readonly SdkCartItem[], quoted: readonly CartQuoteLineBounds[]): string[];
|
|
2347
|
+
|
|
2348
|
+
/**
|
|
2349
|
+
* View models for the cart drawer and the `/cart` page.
|
|
2350
|
+
*
|
|
2351
|
+
* The cart lives in the SDK as a flat list of lines plus a server quote. This
|
|
2352
|
+
* module turns those two into the rows a buyer reads, and it is the only place
|
|
2353
|
+
* that decides what a cart line SAYS.
|
|
2354
|
+
*
|
|
2355
|
+
* PRICING RULE, and it is the whole reason this file exists: a line total is
|
|
2356
|
+
* the stored price snapshot times the quantity, and nothing else. The totals
|
|
2357
|
+
* block is the SERVER's quote, verbatim — subtotal, discounts and total all
|
|
2358
|
+
* from the SAME quote, on both the drawer and `/cart`.
|
|
2359
|
+
*
|
|
2360
|
+
* The two are never mixed. The quote's subtotal is pre-discount while the line
|
|
2361
|
+
* snapshots already carry the product and volume discounts, so printing the
|
|
2362
|
+
* client's sum above the quote's discount rows produces a summary whose
|
|
2363
|
+
* arithmetic does not work (Subtotal 9, Discount −1, Total 9) — which is
|
|
2364
|
+
* exactly what the drawer used to show. `CartView.subtotal` is therefore the
|
|
2365
|
+
* client's own number for ONE purpose: standing in for the whole block while no
|
|
2366
|
+
* quote exists, where every row is the client's and the block still adds up.
|
|
2367
|
+
*
|
|
2368
|
+
* When a line has no snapshot its total is zero and the whole cart is flagged
|
|
2369
|
+
* as an estimate rather than presenting a confident wrong number.
|
|
2370
|
+
*
|
|
2371
|
+
* Cents are settled by the SHARED `roundCurrency`, the same half-up-on-the-
|
|
2372
|
+
* decimal-text rule the buy box and the line-pricing maths use. This file used
|
|
2373
|
+
* to carry a naive `Math.round(v * 100) / 100` of its own, which is a different
|
|
2374
|
+
* function: binary noise puts 10.03 × 50% at 5.014999…, which that version
|
|
2375
|
+
* floors to 5.01 while the quote's ROUND_HALF_UP gives 5.02. One rounding rule,
|
|
2376
|
+
* or two surfaces disagree by a cent about the same cart.
|
|
2377
|
+
*/
|
|
2378
|
+
|
|
2379
|
+
interface CartViewLine {
|
|
2380
|
+
lineId: string;
|
|
2381
|
+
title: string;
|
|
2382
|
+
/** The option, and any extras the buyer ticked, as one caption. Blank hides it. */
|
|
2383
|
+
subtitle: string;
|
|
2384
|
+
imageUrl: string | null;
|
|
2385
|
+
quantity: number;
|
|
2386
|
+
lineTotal: number;
|
|
2387
|
+
/** False at the line's floor — the stepper removes the line instead. */
|
|
2388
|
+
canDecrease: boolean;
|
|
2389
|
+
/** False at the stock or per-order ceiling recorded when the line was added. */
|
|
2390
|
+
canIncrease: boolean;
|
|
2391
|
+
/** The quantity the stepper drops to; below it the line is removed. */
|
|
2392
|
+
minimumQuantity: number;
|
|
2393
|
+
}
|
|
2394
|
+
interface CartView {
|
|
2395
|
+
lines: CartViewLine[];
|
|
2396
|
+
/** Sum of the line totals — the client's own number, not the quote's. */
|
|
2397
|
+
subtotal: number;
|
|
2398
|
+
/**
|
|
2399
|
+
* True when a line carries no price snapshot, so `subtotal` understates the
|
|
2400
|
+
* cart. The summary says "estimated" instead of pretending otherwise.
|
|
2401
|
+
*/
|
|
2402
|
+
subtotalIsEstimate: boolean;
|
|
2403
|
+
isEmpty: boolean;
|
|
2404
|
+
}
|
|
2405
|
+
declare function buildCartView(lines: SdkCartItem[]): CartView;
|
|
2406
|
+
interface CartTotals {
|
|
2407
|
+
/**
|
|
2408
|
+
* The subtotal to print. From the quote when one exists (pre-discount, the
|
|
2409
|
+
* same basis its discount rows are stated against), otherwise the client's
|
|
2410
|
+
* own sum.
|
|
2411
|
+
*/
|
|
2412
|
+
subtotal: number;
|
|
2413
|
+
/** Itemised discounts, exactly as the quote named them. */
|
|
2414
|
+
discounts: Array<{
|
|
2415
|
+
label: string;
|
|
2416
|
+
amount: string;
|
|
2417
|
+
}>;
|
|
2418
|
+
/**
|
|
2419
|
+
* Aggregate discount, shown only when the quote reports a reduction it did
|
|
2420
|
+
* not itemise. Never rendered alongside `discounts`.
|
|
2421
|
+
*/
|
|
2422
|
+
aggregateDiscount: number | null;
|
|
2423
|
+
total: number;
|
|
2424
|
+
currency: string;
|
|
2425
|
+
/**
|
|
2426
|
+
* True while the printed numbers are not a server quote — a quote in flight,
|
|
2427
|
+
* a quote that failed, or a line without a price snapshot.
|
|
2428
|
+
*/
|
|
2429
|
+
isEstimate: boolean;
|
|
2430
|
+
}
|
|
2431
|
+
declare function buildCartTotals(view: CartView, quote: SdkCartQuote | null, fallbackCurrency: string): CartTotals;
|
|
2432
|
+
|
|
2433
|
+
/**
|
|
2434
|
+
* Catalog view models: the step between the injected wire payload and the
|
|
2435
|
+
* cards.
|
|
2436
|
+
*
|
|
2437
|
+
* Everything a card shows — the price label, the badge, the stock chip, the
|
|
2438
|
+
* buy control it gets — is decided here once, so the card components stay
|
|
2439
|
+
* markup. Nothing in this module invents a value: a product without a rating
|
|
2440
|
+
* has no rating chip, a product without a description has no description line.
|
|
2441
|
+
*/
|
|
2442
|
+
|
|
2443
|
+
/**
|
|
2444
|
+
* Money as the storefront shows it: `$49`, `$49.50`, `€9.99`, `¥1,200`.
|
|
2445
|
+
*
|
|
2446
|
+
* A whole amount drops its zero cents (`$49`, not `$49.00`); any other amount
|
|
2447
|
+
* keeps the currency's own number of decimals, so `$49.50` never turns into
|
|
2448
|
+
* `$49.5` and a three-decimal currency keeps all three. "Whole" is judged on
|
|
2449
|
+
* the amount rounded to those decimals, so float noise like `48.99999999`
|
|
2450
|
+
* still reads as `$49`.
|
|
2451
|
+
*/
|
|
2452
|
+
declare function formatMoney(amount: number, currency: string): string;
|
|
2453
|
+
/** Compact counts for the store-stats strip: 12450 → `12k`, 8200 → `8.2k`. */
|
|
2454
|
+
declare function formatCompactCount(value: number): string;
|
|
2455
|
+
type StockTone = 'in_stock' | 'low_stock' | 'sold_out';
|
|
2456
|
+
type StockChip = {
|
|
2457
|
+
tone: StockTone;
|
|
2458
|
+
label: string;
|
|
2459
|
+
};
|
|
2460
|
+
/**
|
|
2461
|
+
* The units at or below which stock reads as LOW when the merchant configured
|
|
2462
|
+
* no threshold of their own. Ten is what every Easy-lane stock surface hardcodes
|
|
2463
|
+
* (`resolveCatalogStockPresentation`, `resolveStockTone`), so a merchant who
|
|
2464
|
+
* never set "Low Stock Warning" sees the same wording on both lanes.
|
|
2465
|
+
*/
|
|
2466
|
+
declare const DEFAULT_LOW_STOCK_THRESHOLD = 10;
|
|
2467
|
+
interface ProductCardModel {
|
|
2468
|
+
kind: 'product';
|
|
2469
|
+
key: string;
|
|
2470
|
+
product: StorefrontProduct;
|
|
2471
|
+
href: string;
|
|
2472
|
+
priceLabel: string;
|
|
2473
|
+
badgeLabel: string | null;
|
|
2474
|
+
/**
|
|
2475
|
+
* The merchant's own badges for this surface, already filtered by
|
|
2476
|
+
* `show_on_product_card`. Separate from `badgeLabel`, which is the theme's
|
|
2477
|
+
* product-TYPE chip: one is authored, the other is derived, and they occupy
|
|
2478
|
+
* different corners of the media so neither can hide the other.
|
|
2479
|
+
*/
|
|
2480
|
+
badges: ProductBadge[];
|
|
2481
|
+
/** Up to three merchant bullets; the description line renders only when empty. */
|
|
2482
|
+
highlights: string[];
|
|
2483
|
+
description: string | null;
|
|
2484
|
+
rating: number | null;
|
|
2485
|
+
/**
|
|
2486
|
+
* Absent for info cards, sold-out products and paused ones — each has its own
|
|
2487
|
+
* chip or its own action row instead.
|
|
2488
|
+
*/
|
|
2489
|
+
stockChip: StockChip | null;
|
|
2490
|
+
soldOut: boolean;
|
|
2491
|
+
/**
|
|
2492
|
+
* The merchant PAUSED this product. Not a stock state: it outranks stock
|
|
2493
|
+
* entirely, because the backend refuses to sell it however many units exist.
|
|
2494
|
+
*/
|
|
2495
|
+
onHold: boolean;
|
|
2496
|
+
categoryIds: string[];
|
|
2497
|
+
sortPriority: number;
|
|
2498
|
+
createdAt: string | null;
|
|
2499
|
+
price: number;
|
|
2500
|
+
}
|
|
2501
|
+
interface GroupCardModel {
|
|
2502
|
+
kind: 'group';
|
|
2503
|
+
key: string;
|
|
2504
|
+
group: StorefrontGroup;
|
|
2505
|
+
priceLabel: string;
|
|
2506
|
+
/** Resolvable members, in the merchant's order. */
|
|
2507
|
+
members: StorefrontProduct[];
|
|
2508
|
+
memberCountLabel: string;
|
|
2509
|
+
categoryIds: string[];
|
|
2510
|
+
sortPriority: number;
|
|
2511
|
+
price: number;
|
|
2512
|
+
}
|
|
2513
|
+
type CatalogCardModel = ProductCardModel | GroupCardModel;
|
|
2514
|
+
interface StorefrontCatalog {
|
|
2515
|
+
cards: CatalogCardModel[];
|
|
2516
|
+
categories: StorefrontCategory[];
|
|
2517
|
+
/**
|
|
2518
|
+
* Every product the payload carries, including the ones the shelf does not
|
|
2519
|
+
* list. A bundle member has a real product page even though it has no card,
|
|
2520
|
+
* so product-page lookups read this and not `cards`.
|
|
2521
|
+
*/
|
|
2522
|
+
products: StorefrontProduct[];
|
|
2523
|
+
/** Products the storefront lists — the store-stats "products" cell counts these. */
|
|
2524
|
+
listedProductCount: number;
|
|
2525
|
+
}
|
|
2526
|
+
/**
|
|
2527
|
+
* The product a `/product/:slug` route points at.
|
|
2528
|
+
*
|
|
2529
|
+
* The id is accepted alongside the slug because a product without a slug is
|
|
2530
|
+
* linked by uniqid, and a bookmarked uniqid must keep working after the
|
|
2531
|
+
* merchant adds one.
|
|
2532
|
+
*/
|
|
2533
|
+
declare function findStorefrontProduct(catalog: StorefrontCatalog, slugOrId: string): StorefrontProduct | null;
|
|
2534
|
+
/**
|
|
2535
|
+
* The storefront's catalog, in the merchant's shelf order.
|
|
2536
|
+
*
|
|
2537
|
+
* `items` decides what is on the shelf. Reading `products` instead would put
|
|
2538
|
+
* every bundle member on the home page as a standalone card — they are real
|
|
2539
|
+
* catalog products, they are simply not listed on their own.
|
|
2540
|
+
*/
|
|
2541
|
+
declare function buildStorefrontCatalog(settings: StorefrontSettings): StorefrontCatalog;
|
|
2542
|
+
declare const CATALOG_SORTS: readonly ["featured", "newest", "price-asc", "price-desc"];
|
|
2543
|
+
type CatalogSort = StorefrontSortKey;
|
|
2544
|
+
declare const CATALOG_SORT_LABELS: Record<CatalogSort, string>;
|
|
2545
|
+
/**
|
|
2546
|
+
* Sorting never mutates the shelf order: `featured` IS that order, and the
|
|
2547
|
+
* others sort a copy of it so switching back is exact.
|
|
2548
|
+
*/
|
|
2549
|
+
declare function sortCatalogCards(cards: CatalogCardModel[], sort: CatalogSort): CatalogCardModel[];
|
|
2550
|
+
/**
|
|
2551
|
+
* The category a `?category=` value names, as the CARD models key on it.
|
|
2552
|
+
*
|
|
2553
|
+
* A shared or bookmarked listing URL may carry the merchant's slug, the
|
|
2554
|
+
* category title, or the uniqid — the Easy lane accepts all of them
|
|
2555
|
+
* (`resolveAllProductsCategorySelection`) and answers with the uniqid the cards
|
|
2556
|
+
* are filtered by. Mirrored here so one storefront's filtered link keeps working
|
|
2557
|
+
* after a lane switch. An unknown value selects nothing rather than emptying the
|
|
2558
|
+
* grid against a category that does not exist.
|
|
2559
|
+
*/
|
|
2560
|
+
declare function resolveCategorySelection(value: string | null, categories: readonly StorefrontCategory[]): string | null;
|
|
2561
|
+
declare function filterCatalogCards(cards: CatalogCardModel[], categoryId: string | null): CatalogCardModel[];
|
|
2562
|
+
/** Products per page in the full listing. Bundles do not count against it. */
|
|
2563
|
+
declare const CATALOG_PAGE_SIZE = 24;
|
|
2564
|
+
interface CatalogPage {
|
|
2565
|
+
cards: CatalogCardModel[];
|
|
2566
|
+
/** The page actually shown — an out-of-range request lands on the last one. */
|
|
2567
|
+
page: number;
|
|
2568
|
+
totalPages: number;
|
|
2569
|
+
}
|
|
2570
|
+
/**
|
|
2571
|
+
* One page of the full listing.
|
|
2572
|
+
*
|
|
2573
|
+
* Bundles all sit on page one and never count against the page size: a shop
|
|
2574
|
+
* with three bundles and twenty-four products is one page, not two, because a
|
|
2575
|
+
* bundle is an entry point into the catalog rather than an item in it. The
|
|
2576
|
+
* input must already be filtered and sorted — paging re-orders nothing.
|
|
2577
|
+
*/
|
|
2578
|
+
declare function paginateCatalogCards(cards: CatalogCardModel[], page: number): CatalogPage;
|
|
2579
|
+
|
|
2580
|
+
declare const QUOTE_UNAVAILABLE_CHECKOUT_ERROR = "Cart pricing is unavailable, so this order cannot be priced yet. Try again in a moment.";
|
|
2581
|
+
/** Retires only the checkout refusal that a newly settled quote resolved. */
|
|
2582
|
+
declare function clearRecoveredQuoteCheckoutError(error: string | null): string | null;
|
|
2583
|
+
|
|
2584
|
+
/**
|
|
2585
|
+
* The contact form's submit path and the community channels beside it.
|
|
2586
|
+
*
|
|
2587
|
+
* The Storefront SDK has no contact method, so this posts the storefront
|
|
2588
|
+
* ticket endpoint directly — the same request, body and validation the
|
|
2589
|
+
* document lane's `sx-contact-form` runs through `submitStorefrontContactTicket`
|
|
2590
|
+
* (`packages/sdk/src/utils/storefront-contact.ts`). Keeping the payload shape
|
|
2591
|
+
* identical is what makes a message sent from a code storefront indistinguishable
|
|
2592
|
+
* from one sent from the Builder.
|
|
2593
|
+
*/
|
|
2594
|
+
|
|
2595
|
+
interface ContactFormValues {
|
|
2596
|
+
name: string;
|
|
2597
|
+
email: string;
|
|
2598
|
+
subject: string;
|
|
2599
|
+
message: string;
|
|
2600
|
+
invoiceId: string;
|
|
2601
|
+
}
|
|
2602
|
+
interface ContactSubmissionContext {
|
|
2603
|
+
contactEnabled: boolean;
|
|
2604
|
+
requireInvoice: boolean;
|
|
2605
|
+
}
|
|
2606
|
+
/**
|
|
2607
|
+
* Client-side validation, mirroring `validateStorefrontContactSubmission`.
|
|
2608
|
+
*
|
|
2609
|
+
* It is a courtesy, not a gate: the backend enforces the same rules, and a
|
|
2610
|
+
* form that only *looked* validated would let a disabled shop's visitor watch
|
|
2611
|
+
* a request fail with no explanation.
|
|
2612
|
+
*/
|
|
2613
|
+
declare function validateContactSubmission(values: ContactFormValues, context: ContactSubmissionContext): string | null;
|
|
2614
|
+
declare function submitContactTicket(values: ContactFormValues, context: ContactSubmissionContext): Promise<void>;
|
|
2615
|
+
interface SupportChannel {
|
|
2616
|
+
label: string;
|
|
2617
|
+
href: string;
|
|
2618
|
+
/** Mail links open the visitor's client rather than a new tab. */
|
|
2619
|
+
isMail: boolean;
|
|
2620
|
+
}
|
|
2621
|
+
/**
|
|
2622
|
+
* The channels a buyer can reach the shop on before they buy.
|
|
2623
|
+
*
|
|
2624
|
+
* Derived from the shop's own social profiles, not from theme content: those
|
|
2625
|
+
* handles belong to the merchant and are edited once in the dashboard. The
|
|
2626
|
+
* three networks and their order are the platform's
|
|
2627
|
+
* (`withShopContactFallbacks` in the document renderer), so the two lanes list
|
|
2628
|
+
* the same links in the same order.
|
|
2629
|
+
*/
|
|
2630
|
+
declare function resolveSupportChannels(links: readonly SocialLink[]): SupportChannel[];
|
|
2631
|
+
|
|
2632
|
+
/**
|
|
2633
|
+
* The merchant's navigation menus, and where they sit against theme content.
|
|
2634
|
+
*
|
|
2635
|
+
* A shop configures menus once in the dashboard and expects every theme to use
|
|
2636
|
+
* them. The Easy lane resolves a slot in this order
|
|
2637
|
+
* (`build-storefront-navigation.ts` + `resolveDocumentNavigationLinks`):
|
|
2638
|
+
*
|
|
2639
|
+
* 1. a link list the merchant authored IN THE THEME wins — including an
|
|
2640
|
+
* empty one, which means "no links here", not "fall back";
|
|
2641
|
+
* 2. otherwise the merchant's menu for that slot;
|
|
2642
|
+
* 3. otherwise the theme's own static links.
|
|
2643
|
+
*
|
|
2644
|
+
* Mirrored exactly. `hasAuthoredLinks` is what separates (1) from (3): the
|
|
2645
|
+
* template's shipped `nav.links` are step 3, not an override, so a shop with a
|
|
2646
|
+
* header menu and no theme edit gets its menu — which is the whole gap this
|
|
2647
|
+
* closes.
|
|
2648
|
+
*
|
|
2649
|
+
* The menus are fetched once per page load and shared by both slots. While the
|
|
2650
|
+
* request is in flight the theme's own links render: that is the same list step
|
|
2651
|
+
* 3 would settle on, so the header is never empty and never shows a spinner
|
|
2652
|
+
* where its navigation belongs. A shop WITH a menu therefore swaps its links in
|
|
2653
|
+
* once, on first resolve.
|
|
2654
|
+
*/
|
|
2655
|
+
|
|
2656
|
+
type MenuSlot = 'header' | 'footer';
|
|
2657
|
+
/** One navigation link as a theme authors it: a label and where it goes. */
|
|
2658
|
+
type NavLink = {
|
|
2659
|
+
label: string;
|
|
2660
|
+
href: string;
|
|
2661
|
+
};
|
|
2662
|
+
type MenuNavLink = NavLink & {
|
|
2663
|
+
target: string | null;
|
|
2664
|
+
};
|
|
2665
|
+
declare function resolveMenuLinksForSlot(menus: readonly SdkMenu[], slot: MenuSlot): MenuNavLink[];
|
|
2666
|
+
interface NavigationLinks {
|
|
2667
|
+
links: readonly MenuNavLink[];
|
|
2668
|
+
/**
|
|
2669
|
+
* True when the rendered list is the THEME's content, which is what decides
|
|
2670
|
+
* whether each entry carries a `data-sx-config` path — a merchant's menu item
|
|
2671
|
+
* is edited in the dashboard's navigation screen, not in the design
|
|
2672
|
+
* inspector, and pointing the inspector at it would offer an edit that goes
|
|
2673
|
+
* nowhere.
|
|
2674
|
+
*/
|
|
2675
|
+
fromTheme: boolean;
|
|
2676
|
+
}
|
|
2677
|
+
/**
|
|
2678
|
+
* The links one slot should render, with the precedence above already applied.
|
|
2679
|
+
*
|
|
2680
|
+
* `themeLinks` is the template's own list. It is returned unchanged when the
|
|
2681
|
+
* merchant authored it (step 1) and while the menus are still loading; a
|
|
2682
|
+
* non-empty menu replaces it once resolved (step 2); an empty menu leaves it in
|
|
2683
|
+
* place (step 3).
|
|
2684
|
+
*/
|
|
2685
|
+
declare function useNavigationLinks(slot: MenuSlot, themeLinks: readonly NavLink[],
|
|
2686
|
+
/** True when the merchant edited this slot's links in the theme content. */
|
|
2687
|
+
authored: boolean): NavigationLinks;
|
|
2688
|
+
|
|
2689
|
+
/**
|
|
2690
|
+
* Strict parsing helpers shared by the content contract (`src/config`) and the
|
|
2691
|
+
* storefront data contract (`src/commerce`).
|
|
2692
|
+
*
|
|
2693
|
+
* The rule both contracts follow: an ABSENT value may fall back to a documented
|
|
2694
|
+
* default, a PRESENT but malformed one throws. Silently repairing bad input
|
|
2695
|
+
* would hide a broken content draft or a changed wire contract behind a
|
|
2696
|
+
* storefront that merely looks slightly wrong.
|
|
2697
|
+
*/
|
|
2698
|
+
declare function isRecord(value: unknown): value is Record<string, unknown>;
|
|
2699
|
+
declare function requireUnknownRecord(value: unknown, context: string): Record<string, unknown>;
|
|
2700
|
+
declare function requireRecord(record: Record<string, unknown>, key: string): Record<string, unknown>;
|
|
2701
|
+
declare function requireArray(record: Record<string, unknown>, key: string): unknown[];
|
|
2702
|
+
declare function requireString(record: Record<string, unknown>, key: string, context: string): string;
|
|
2703
|
+
declare function requireStringFromKeys(record: Record<string, unknown>, keys: string[], context: string): string;
|
|
2704
|
+
declare function requireNumber(record: Record<string, unknown>, key: string, context: string): number;
|
|
2705
|
+
/** A non-empty trimmed string, or null for absent/blank/non-string input. */
|
|
2706
|
+
declare function optionalString(value: unknown): string | null;
|
|
2707
|
+
/** The first usable string among several candidate keys. */
|
|
2708
|
+
declare function firstOptionalString(record: Record<string, unknown>, keys: string[]): string | null;
|
|
2709
|
+
/**
|
|
2710
|
+
* A visible string that may be absent from an older content draft. Absent or
|
|
2711
|
+
* blank renders the shipped wording; a present non-string is still an error.
|
|
2712
|
+
*/
|
|
2713
|
+
declare function optionalStringWithDefault(value: unknown, fallback: string, context: string): string;
|
|
2714
|
+
/**
|
|
2715
|
+
* A boolean the serializer ALWAYS emits, so its absence is a broken payload.
|
|
2716
|
+
*
|
|
2717
|
+
* Defaulting one of these is worse than throwing: a merchant switch that never
|
|
2718
|
+
* arrives silently reads as whatever the theme guessed, and the storefront then
|
|
2719
|
+
* offers something the backend refuses. `0`/`1` are accepted because the wire
|
|
2720
|
+
* carries some flags that way; anything else is an error.
|
|
2721
|
+
*/
|
|
2722
|
+
declare function requireBoolean(value: unknown, context: string): boolean;
|
|
2723
|
+
declare function optionalBoolean(value: unknown, fallback: boolean, context: string): boolean;
|
|
2724
|
+
declare function parseFiniteNumber(value: unknown, context: string): number;
|
|
2725
|
+
/**
|
|
2726
|
+
* One of a fixed option set. Absent falls back to the theme default; anything
|
|
2727
|
+
* else throws, because a typo in an enum is a broken content draft, not a
|
|
2728
|
+
* missing field — and the wrong branch of a layout switch is worse than a
|
|
2729
|
+
* visible error.
|
|
2730
|
+
*/
|
|
2731
|
+
declare function parseEnum<T extends string>(value: unknown, allowed: readonly T[], fallback: T, context: string): T;
|
|
2732
|
+
|
|
2733
|
+
/**
|
|
2734
|
+
* Product-page view models: the step between one parsed product and the
|
|
2735
|
+
* gallery, buy box, details tabs and related strip.
|
|
2736
|
+
*
|
|
2737
|
+
* Same division of labour as `catalog.ts` — every decision a product page makes
|
|
2738
|
+
* about WHAT to show is made here, so the components stay markup. The two files
|
|
2739
|
+
* deliberately differ on stock wording: a catalog card shouts a short chip
|
|
2740
|
+
* ("999 IN STOCK"), the buy box states the option's availability in a sentence
|
|
2741
|
+
* ("999 in stock", "Only 3 left"). Same source, two audiences.
|
|
2742
|
+
*/
|
|
2743
|
+
|
|
2744
|
+
/** The five star slots under the product title; `true` is a filled star. */
|
|
2745
|
+
type RatingStars = readonly [boolean, boolean, boolean, boolean, boolean];
|
|
2746
|
+
interface BuyBoxVariantModel {
|
|
2747
|
+
variant: StorefrontPriceVariant;
|
|
2748
|
+
/** Unit price with the product discount applied — what one unit costs today. */
|
|
2749
|
+
price: number;
|
|
2750
|
+
/** Undiscounted unit price, shown struck through. null when nothing is off. */
|
|
2751
|
+
compareAtPrice: number | null;
|
|
2752
|
+
stockTone: StockTone;
|
|
2753
|
+
stockLabel: string;
|
|
2754
|
+
soldOut: boolean;
|
|
2755
|
+
}
|
|
2756
|
+
interface ProductRatingModel {
|
|
2757
|
+
stars: RatingStars;
|
|
2758
|
+
/** Reviews the tally counted — the label pluralises off this. */
|
|
2759
|
+
count: number;
|
|
2760
|
+
/** 1-5, rounded to one decimal. */
|
|
2761
|
+
average: number;
|
|
2762
|
+
/**
|
|
2763
|
+
* How the tally splits. The bootstrap payload carries these three buckets and
|
|
2764
|
+
* no review bodies, so this is the finest grain the code lane can honestly
|
|
2765
|
+
* report.
|
|
2766
|
+
*/
|
|
2767
|
+
positive: number;
|
|
2768
|
+
neutral: number;
|
|
2769
|
+
negative: number;
|
|
2770
|
+
}
|
|
2771
|
+
interface ProductDetailModel {
|
|
2772
|
+
product: StorefrontProduct;
|
|
2773
|
+
/** Gallery images, primary first. Empty when the product has no art at all. */
|
|
2774
|
+
images: string[];
|
|
2775
|
+
/** Rating derived from real feedback; null when the product has none. */
|
|
2776
|
+
rating: ProductRatingModel | null;
|
|
2777
|
+
variants: BuyBoxVariantModel[];
|
|
2778
|
+
addons: ProductAddon[];
|
|
2779
|
+
soldOut: boolean;
|
|
2780
|
+
/** Cards for the "You might also like" strip, in the merchant's shelf order. */
|
|
2781
|
+
related: ProductCardModel[];
|
|
2782
|
+
}
|
|
2783
|
+
declare function buildProductDetail(product: StorefrontProduct, settings: StorefrontSettings, cards: CatalogCardModel[]): ProductDetailModel;
|
|
2784
|
+
interface LinePricing {
|
|
2785
|
+
/** What the cart will charge for the whole line. */
|
|
2786
|
+
total: number;
|
|
2787
|
+
/** The same line before any discount. Equal to `total` when none applies. */
|
|
2788
|
+
originalSubtotal: number;
|
|
2789
|
+
/** Per-unit price after discounts, for the "… each" line. */
|
|
2790
|
+
effectiveUnitPrice: number;
|
|
2791
|
+
discountAmount: number;
|
|
2792
|
+
hasDiscount: boolean;
|
|
2793
|
+
}
|
|
2794
|
+
/**
|
|
2795
|
+
* What the buy box headline shows.
|
|
2796
|
+
*
|
|
2797
|
+
* The unit price folds the selected variant AND the selected addons before the
|
|
2798
|
+
* discount stages run, exactly as the platform's product-form controller does —
|
|
2799
|
+
* a storefront that discounted the variant and then added the extras would
|
|
2800
|
+
* advertise a total the checkout does not honour.
|
|
2801
|
+
*/
|
|
2802
|
+
declare function calculateLinePricing(product: StorefrontProduct, variant: StorefrontPriceVariant | null, addons: ProductAddon[], quantity: number,
|
|
2803
|
+
/**
|
|
2804
|
+
* The buyer's pay-what-you-want amount: PER UNIT, and for the product alone.
|
|
2805
|
+
* The extras are added on top of it here exactly once, which is the same
|
|
2806
|
+
* split `pay_what_you_want_price` + `price_data.unit_price` the cart sends
|
|
2807
|
+
* and the backend charges.
|
|
2808
|
+
*/
|
|
2809
|
+
payWhatYouWantPrice?: number): LinePricing;
|
|
2810
|
+
/**
|
|
2811
|
+
* The floor a pay-what-you-want buyer may name: PER UNIT, extras excluded.
|
|
2812
|
+
*
|
|
2813
|
+
* It is the selected option's UNDISCOUNTED price — the same number the backend
|
|
2814
|
+
* validates the buyer's amount against (`resolvePayWhatYouWantUnitPrice`) and
|
|
2815
|
+
* the same one the document lane offers as the default. Reading a discounted
|
|
2816
|
+
* price here would pre-fill an amount the checkout then refuses as too low.
|
|
2817
|
+
*/
|
|
2818
|
+
declare function resolvePayWhatYouWantMinimum(product: StorefrontProduct, variant: StorefrontPriceVariant | null): number;
|
|
2819
|
+
/**
|
|
2820
|
+
* The backend's ceiling for a named price (`MAX_PAY_WHAT_YOU_WANT_PRICE`).
|
|
2821
|
+
* An amount above it fails when the invoice is built, so the buy box refuses it
|
|
2822
|
+
* first instead of handing the buyer to a checkout that cannot be created.
|
|
2823
|
+
*/
|
|
2824
|
+
declare const MAX_PAY_WHAT_YOU_WANT_PRICE = 1000000000;
|
|
2825
|
+
/** Clamps a buyer-entered quantity into the bounds the line actually allows. */
|
|
2826
|
+
declare function clampQuantity(product: StorefrontProduct, variant: StorefrontPriceVariant | null, quantity: number): number;
|
|
2827
|
+
|
|
2828
|
+
/**
|
|
2829
|
+
* Shop feedback for the `/reviews` page.
|
|
2830
|
+
*
|
|
2831
|
+
* Shoppex feedback is SHOP-level, not per product, and it is not part of the
|
|
2832
|
+
* injected catalog payload — the bootstrap object carries only per-product
|
|
2833
|
+
* tallies. The bodies come from a runtime call, so this is the one surface in
|
|
2834
|
+
* the theme that renders a list it has to wait for.
|
|
2835
|
+
*
|
|
2836
|
+
* Nothing here invents a review. A shop with no feedback resolves to an empty
|
|
2837
|
+
* list and the page says so; a failed request resolves to an error state and
|
|
2838
|
+
* the page says that instead. Neither is padded with sample data.
|
|
2839
|
+
*/
|
|
2840
|
+
|
|
2841
|
+
interface ShopReview {
|
|
2842
|
+
id: string;
|
|
2843
|
+
/** Filled-star count, the rating clamped to 0-5 and rounded. */
|
|
2844
|
+
starCount: number;
|
|
2845
|
+
rating: number;
|
|
2846
|
+
comment: string;
|
|
2847
|
+
author: string;
|
|
2848
|
+
/** Localized short date, e.g. "Jan 12, 2026". Blank when the API sent none. */
|
|
2849
|
+
dateLabel: string;
|
|
2850
|
+
/** Sort key; 0 when the entry carries no usable timestamp. */
|
|
2851
|
+
timestamp: number;
|
|
2852
|
+
/**
|
|
2853
|
+
* The reviewed product's CURRENT title, or null when the review names none.
|
|
2854
|
+
* The public feed withholds product ids, so the title is the only link to a
|
|
2855
|
+
* product there is; see `reviewsForProduct`.
|
|
2856
|
+
*/
|
|
2857
|
+
productTitle: string | null;
|
|
2858
|
+
/** True when the review is tied to a real completed purchase. */
|
|
2859
|
+
verified: boolean;
|
|
2860
|
+
}
|
|
2861
|
+
type ShopReviewsState = {
|
|
2862
|
+
status: 'loading';
|
|
2863
|
+
} | {
|
|
2864
|
+
status: 'error';
|
|
2865
|
+
} | {
|
|
2866
|
+
status: 'ready';
|
|
2867
|
+
reviews: ShopReview[];
|
|
2868
|
+
average: number | null;
|
|
2869
|
+
};
|
|
2870
|
+
/**
|
|
2871
|
+
* How many cards the page shows. The reference storefront pages its list at
|
|
2872
|
+
* twelve; a shop with hundreds of reviews should not ship all of them into one
|
|
2873
|
+
* screenshot-tall grid.
|
|
2874
|
+
*/
|
|
2875
|
+
declare const SHOP_REVIEW_PAGE_SIZE = 12;
|
|
2876
|
+
interface ShopReviewLabels {
|
|
2877
|
+
anonymousAuthor: string;
|
|
2878
|
+
emptyComment: string;
|
|
2879
|
+
}
|
|
2880
|
+
declare function toStarCount(rating: number): number;
|
|
2881
|
+
/**
|
|
2882
|
+
* Newest first, and only entries that carry a real rating: a zero-star row is
|
|
2883
|
+
* an unrated record, not a one-star verdict, and averaging it in would drag
|
|
2884
|
+
* every shop's score down.
|
|
2885
|
+
*/
|
|
2886
|
+
declare function toShopReviews(rows: readonly SdkShopReview[], labels: ShopReviewLabels): ShopReview[];
|
|
2887
|
+
/**
|
|
2888
|
+
* The written reviews that name this product, newest first.
|
|
2889
|
+
*
|
|
2890
|
+
* Matched on the product's title because the public feed carries no product
|
|
2891
|
+
* ids. That makes it a sample, not a count: the feed leaves out appealed and
|
|
2892
|
+
* filtered feedback, and a renamed product loses its older reviews. Surfaces
|
|
2893
|
+
* show these as "latest reviews" beside the payload's own tally and never
|
|
2894
|
+
* derive a number from them.
|
|
2895
|
+
*/
|
|
2896
|
+
declare function reviewsForProduct(reviews: readonly ShopReview[], productTitle: string): ShopReview[];
|
|
2897
|
+
declare function averageRating(reviews: readonly ShopReview[]): number | null;
|
|
2898
|
+
declare function useShopReviews(labels: ShopReviewLabels): ShopReviewsState;
|
|
2899
|
+
|
|
2900
|
+
/**
|
|
2901
|
+
* Merchant-authored HTML, made safe to inject.
|
|
2902
|
+
*
|
|
2903
|
+
* The storefront payload carries product descriptions and content tabs exactly
|
|
2904
|
+
* as the merchant stored them; nothing sanitizes them on the way out. The Easy
|
|
2905
|
+
* lane sanitizes at RENDER time (`sanitizeStorefrontRichTextHtml`), so a code
|
|
2906
|
+
* storefront that skipped this step would make the same shop's content safe on
|
|
2907
|
+
* one lane and executable on the other.
|
|
2908
|
+
*
|
|
2909
|
+
* "The merchant wrote it, on their own storefront" is not a safety argument:
|
|
2910
|
+
* imported shops carry HTML nobody at the shop authored, and dashboard access is
|
|
2911
|
+
* not always the owner's. The allowlist mirrors the Easy lane's — anything
|
|
2912
|
+
* outside it is DROPPED rather than escaped, so a stray `onerror` disappears
|
|
2913
|
+
* instead of showing up as text.
|
|
2914
|
+
*/
|
|
2915
|
+
declare function sanitizeMerchantHtml(value: unknown): string;
|
|
2916
|
+
/**
|
|
2917
|
+
* Merchant HTML reduced to plain text, safely.
|
|
2918
|
+
*
|
|
2919
|
+
* Product descriptions arrive as rich text and several surfaces render them as
|
|
2920
|
+
* TEXT — a catalog card's two-line summary, the search haystack. Passing the
|
|
2921
|
+
* raw string there printed the merchant's own `<p>` tags at the buyer, and
|
|
2922
|
+
* passing `textContent` of the parsed markup ran the block boundaries together.
|
|
2923
|
+
*
|
|
2924
|
+
* So the value is sanitized first (same allowlist as everything else — the
|
|
2925
|
+
* string is never parsed as live markup before DOMPurify has seen it) and then
|
|
2926
|
+
* flattened with block boundaries counting as whitespace. The result is
|
|
2927
|
+
* collapsed to single spaces because CSS line-clamping cannot do it for text.
|
|
2928
|
+
*/
|
|
2929
|
+
declare function flattenMerchantHtmlToText(value: unknown): string;
|
|
2930
|
+
/**
|
|
2931
|
+
* True when the sanitized markup carries no renderable content.
|
|
2932
|
+
*
|
|
2933
|
+
* A description of `<p></p>` is not a description: the Easy lane's
|
|
2934
|
+
* `hasDescription` gate tests the SOURCE text, so a merchant who cleared their
|
|
2935
|
+
* editor without clearing its wrapper tags gets the empty state rather than a
|
|
2936
|
+
* blank panel.
|
|
2937
|
+
*/
|
|
2938
|
+
/**
|
|
2939
|
+
* Drops every picture from merchant HTML.
|
|
2940
|
+
*
|
|
2941
|
+
* The merchant's "show images in descriptions" switch, applied at the same
|
|
2942
|
+
* place the document lane applies it (`stripDescriptionImages`): the shop asked
|
|
2943
|
+
* for text, so the description and every content tab arrive without their
|
|
2944
|
+
* `<img>` elements. Run BEFORE the sanitizer's output is injected, on the
|
|
2945
|
+
* sanitized string, so a tag the allowlist would have dropped anyway cannot
|
|
2946
|
+
* survive by looking like an image to this pass.
|
|
2947
|
+
*/
|
|
2948
|
+
declare function stripMerchantHtmlImages(html: string): string;
|
|
2949
|
+
declare function isBlankMerchantHtml(html: string): boolean;
|
|
2950
|
+
|
|
2951
|
+
/**
|
|
2952
|
+
* Merchant CMS pages (dashboard → Pages), fetched by slug for `/page/:slug`.
|
|
2953
|
+
*
|
|
2954
|
+
* These are NOT in the injected catalog: the edge injects the storefront
|
|
2955
|
+
* payload, and the merchant's pages come from a separate service the SDK reads
|
|
2956
|
+
* (`getPage` / `getPages`). So the route loads them, and every state it can be
|
|
2957
|
+
* in is explicit — a slug that does not resolve renders the theme's not-found
|
|
2958
|
+
* copy rather than an empty article.
|
|
2959
|
+
*
|
|
2960
|
+
* The rendering rules are the Easy lane's, from `enrichCmsPageBlockSettings`
|
|
2961
|
+
* plus `blocks/page-tool-page.ts` → `renderCmsPage`:
|
|
2962
|
+
*
|
|
2963
|
+
* 1. `template_type === 'faq'` and `settings.faq_items` non-empty → the
|
|
2964
|
+
* accordion. A row needs BOTH a question and an answer to count, exactly
|
|
2965
|
+
* as `normalizeCmsFaqItems` requires.
|
|
2966
|
+
* 2. otherwise the body, rendered from MARKDOWN (gfm + breaks, the same
|
|
2967
|
+
* `marked` configuration `renderCmsPageMarkdownHtml` uses).
|
|
2968
|
+
* 3. otherwise the empty state.
|
|
2969
|
+
*
|
|
2970
|
+
* What this module does NOT do is sanitize. The markdown pass emits merchant
|
|
2971
|
+
* HTML — markdown may contain raw tags — and it is merchant HTML of exactly the
|
|
2972
|
+
* same status as `shop.termsHtml` or a product description, so it is sanitized
|
|
2973
|
+
* where it is injected, by the component that injects it. That is the rule the
|
|
2974
|
+
* whole template follows and the one its HTML-sink guard checks.
|
|
2975
|
+
*/
|
|
2976
|
+
type ShopPageFaqItem = {
|
|
2977
|
+
question: string;
|
|
2978
|
+
answer: string;
|
|
2979
|
+
};
|
|
2980
|
+
/** One resolved CMS page, reduced to what the route renders. */
|
|
2981
|
+
interface ShopPageModel {
|
|
2982
|
+
title: string;
|
|
2983
|
+
/**
|
|
2984
|
+
* The markdown body as HTML — merchant markup, NOT yet sanitized. The route
|
|
2985
|
+
* sanitizes it at the sink; see the module docblock.
|
|
2986
|
+
*/
|
|
2987
|
+
bodyMarkdownHtml: string;
|
|
2988
|
+
/** Non-empty only for a `faq` page with usable rows. */
|
|
2989
|
+
faqItems: ShopPageFaqItem[];
|
|
2990
|
+
}
|
|
2991
|
+
type ShopPageState = {
|
|
2992
|
+
status: 'loading';
|
|
2993
|
+
} | {
|
|
2994
|
+
status: 'ready';
|
|
2995
|
+
page: ShopPageModel;
|
|
2996
|
+
}
|
|
2997
|
+
/** The slug resolved to nothing, or the request failed. Same screen either way. */
|
|
2998
|
+
| {
|
|
2999
|
+
status: 'missing';
|
|
3000
|
+
};
|
|
3001
|
+
/**
|
|
3002
|
+
* Loads one CMS page by slug.
|
|
3003
|
+
*
|
|
3004
|
+
* A slug the API does not know, a page without a name, and a failed request all
|
|
3005
|
+
* end in `missing`: there is no page to show, and inventing a title from the
|
|
3006
|
+
* slug would publish a heading the merchant never wrote.
|
|
3007
|
+
*/
|
|
3008
|
+
declare function loadShopPage(slug: string): Promise<ShopPageState>;
|
|
3009
|
+
|
|
3010
|
+
/**
|
|
3011
|
+
* The sentence that names the CAUSE rather than the symptom.
|
|
3012
|
+
*
|
|
3013
|
+
* A missing bootstrap global and a missing `__SHOPPEX_INITIAL__` payload are
|
|
3014
|
+
* the same fault seen from two sides: the page is not being served by the
|
|
3015
|
+
* worker that injects both. `App.tsx`'s configuration screen shows this to the
|
|
3016
|
+
* merchant, so it lives here once instead of being written twice.
|
|
3017
|
+
*/
|
|
3018
|
+
declare const SERVING_CONTEXT_HINT = "This storefront must be served by the Shoppex storefront (or preview) worker.";
|
|
3019
|
+
declare function getShoppex(): ShoppexSdk;
|
|
3020
|
+
/**
|
|
3021
|
+
* The currency the prices on this page are in.
|
|
3022
|
+
*
|
|
3023
|
+
* The edge injected it alongside the catalog, from the same payload the prices
|
|
3024
|
+
* came from, so it is right whether the buyer asked for a currency, whether
|
|
3025
|
+
* their country selected one, or neither.
|
|
3026
|
+
*
|
|
3027
|
+
* Pass it to every quote and checkout. Left out, the SDK falls back to reading
|
|
3028
|
+
* `?currency=` off `window.location` — which survives the first page load and
|
|
3029
|
+
* nothing after it, because client-side navigation drops the query. The cart
|
|
3030
|
+
* then quoted in the shop's geo-resolved currency while the page went on
|
|
3031
|
+
* showing the one the buyer asked for.
|
|
3032
|
+
*/
|
|
3033
|
+
declare function getBuyerCurrency(): string;
|
|
3034
|
+
|
|
3035
|
+
interface StorefrontData {
|
|
3036
|
+
settings: StorefrontSettings;
|
|
3037
|
+
shop: StorefrontShop;
|
|
3038
|
+
catalog: StorefrontCatalog;
|
|
3039
|
+
}
|
|
3040
|
+
declare function readStorefrontData(): StorefrontData;
|
|
3041
|
+
declare function StorefrontProvider({ data, children }: {
|
|
3042
|
+
data: StorefrontData;
|
|
3043
|
+
children: ReactNode;
|
|
3044
|
+
}): react_jsx_runtime.JSX.Element;
|
|
3045
|
+
declare function useStorefront(): StorefrontData;
|
|
3046
|
+
/**
|
|
3047
|
+
* Live visitor count for the header pill.
|
|
3048
|
+
*
|
|
3049
|
+
* Starts at null — "not answered yet" — and the pill renders nothing until a
|
|
3050
|
+
* real number arrives. A placeholder zero would flash "0 online" on every page
|
|
3051
|
+
* load, and a placeholder guess would be a fabricated claim. A failed request
|
|
3052
|
+
* leaves it null and the pill simply stays away.
|
|
3053
|
+
*/
|
|
3054
|
+
declare function useOnlineUserCount(): number | null;
|
|
3055
|
+
|
|
3056
|
+
/**
|
|
3057
|
+
* The merchant's product video, turned into an embeddable `src` — and gated.
|
|
3058
|
+
*
|
|
3059
|
+
* `products.video_link` is a raw string the merchant typed: a YouTube watch URL,
|
|
3060
|
+
* a bare video id, a Streamable short link, a Vimeo page. The Easy lane resolves
|
|
3061
|
+
* it through `resolveVideoEmbedSrc`
|
|
3062
|
+
* (`packages/builder-runtime/src/video-embed.ts`, called by the storefront
|
|
3063
|
+
* render context) and then renders the iframe ONLY when the resolved host is on
|
|
3064
|
+
* the gallery block's allowlist (`blocks/gallery.ts`, ADR-0045).
|
|
3065
|
+
*
|
|
3066
|
+
* Both halves are ported here, in that order, because they answer different
|
|
3067
|
+
* questions. The parser decides which provider a merchant meant; the allowlist
|
|
3068
|
+
* decides whether the storefront is willing to frame it at all. A host that is
|
|
3069
|
+
* not on the list renders NOTHING — never the raw string in an iframe, which is
|
|
3070
|
+
* what turns a merchant-editable text column into an arbitrary embed.
|
|
3071
|
+
*
|
|
3072
|
+
* Ported rather than imported: the theme builds standalone from
|
|
3073
|
+
* `templates/code-storefronts/`, with no workspace dependency on the renderer.
|
|
3074
|
+
* Keep the allowlist and the provider rules in step with the two files above.
|
|
3075
|
+
*/
|
|
3076
|
+
/**
|
|
3077
|
+
* The iframe `src` for a merchant's `video_link`, or null when there is nothing
|
|
3078
|
+
* this storefront is willing to frame.
|
|
3079
|
+
*
|
|
3080
|
+
* Privacy-enhanced YouTube (`youtube-nocookie.com`) because that is what the
|
|
3081
|
+
* render context asks for (`resolveVideoEmbedSrc(..., { privacyEnhanced: true })`)
|
|
3082
|
+
* — a code storefront must not set more cookies on a buyer than the Easy lane
|
|
3083
|
+
* for the same merchant's video.
|
|
3084
|
+
*/
|
|
3085
|
+
declare function resolveProductVideoEmbedSrc(value: unknown): string | null;
|
|
3086
|
+
|
|
3087
|
+
export { ACCOUNT_ARTIFACT_CAPABILITIES, ACCOUNT_PATH, ACCOUNT_PATH_ALIAS, ACCOUNT_ROUTE_MESSAGE_KEYS, ACCOUNT_STOREFRONT_ROUTES, ACCOUNT_TABS, type AccountRouteMessageKey, type AccountRouteParts, type AccountTab, type AccountView, type BuyBoxVariantModel, CATALOG_PAGE_SIZE, CATALOG_SORTS, CATALOG_SORT_LABELS, type CartBoundsHeal, CartProvider, type CartQuoteLineBounds, type CartTotals, type CartView, type CartViewLine, type CatalogCardModel, type CatalogPage, type CatalogSort, type CheckoutFieldGroup, type CheckoutFieldValues, type CheckoutFields, type ContactFormValues, type ContactSubmissionContext, DEFAULT_ACCOUNT_TAB, DEFAULT_LOW_STOCK_THRESHOLD, DELIVERY_TIME_UNITS, type DeliveryPromise, type DeliveryTimeUnit, type GroupCardModel, IMPLEMENTED_ACCOUNT_TABS, type ImplementedAccountTab, type LicenseStatement, type LicenseTerm, type LinePricing, MAX_PAY_WHAT_YOU_WANT_PRICE, type MenuNavLink, type MenuSlot, type NavLink, type NavigationLinks, PRODUCT_BADGE_ICON_NAMES, type ProductAddon, type ProductAutomaticDeal, type ProductBadge, type ProductBadgeIconName, type ProductBundle, type ProductBundleItem, type ProductCardModel, type ProductCategoryRef, type ProductContentTab, type ProductCustomField, type ProductDetailModel, type ProductFaq, type ProductRatingModel, type ProductSubscription, type ProductType, QUOTE_UNAVAILABLE_CHECKOUT_ERROR, type RatingStars, type ResellerProgramSummary, type RewardProgramItem, type RewardProgramSummary, SERVING_CONTEXT_HINT, SHOP_REVIEW_PAGE_SIZE, STOREFRONT_SORT_KEYS, type SdkCartAddOptions, type SdkCartAddon, type SdkCartAppliedDiscount, type SdkCartCodeSource, type SdkCartItem, type SdkCartQuote, type SdkCartQuoteLine, type SdkCheckoutResult, type SdkCustomerAffiliateLink, type SdkCustomerAffiliateSummary, type SdkCustomerDashboard, type SdkCustomerDashboardOrder, type SdkCustomerDeliverable, type SdkCustomerDeliveryStatus, type SdkCustomerDeliverySummary, type SdkCustomerLicense, type SdkCustomerLoyalty, type SdkCustomerLoyaltyRedeem, type SdkCustomerLoyaltyTransactionType, type SdkCustomerOrderDetail, type SdkCustomerOrderPage, type SdkCustomerOrderSummary, type SdkCustomerRewardActivity, type SdkCustomerRewards, type SdkCustomerSubscription, type SdkCustomerTicketSummary, type SdkCustomerWarranty, type SdkCustomerWarrantyClaim, type SdkCustomerWarrantyClaimStatus, type SdkCustomerWarrantyList, type SdkCustomerWarrantyStatus, type SdkEligibleCartDeal, type SdkInitOptions, type SdkMenu, type SdkMenuItem, type SdkPage, type SdkRecentSaleEntry, type SdkResponse, type SdkShopReview, type ShopPageFaqItem, type ShopPageModel, type ShopPageState, type ShopReview, type ShopReviewLabels, type ShopReviewsState, type ShoppexSdk, type SocialLink, type SocialNetwork, type StockChip, type StockTone, type StorefrontAddon, type StorefrontCatalog, type StorefrontCategory, type StorefrontData, type StorefrontGroup, type StorefrontListingItem, type StorefrontPriceVariant, type StorefrontProduct, StorefrontProvider, type StorefrontSettings, type StorefrontShop, type StorefrontShopStats, type StorefrontSortKey, type SubscriptionIntervalUnit, type SupportChannel, type VolumeDiscountRule, accountOrderHref, accountRouteMessage, accountTabHref, accountTabLabelKey, averageRating, badgesForCard, badgesForProductPage, buildCartTotals, buildCartView, buildCustomFieldPayload, buildInitialCheckoutFieldValues, buildProductDetail, buildStorefrontCatalog, calculateDisplayLineTotal, calculateLinePricing, canonicalAccountPath, checkoutFieldKey, checkoutFieldWriteRepriced, checkoutNeedsDetails, clampQuantity, clearRecoveredQuoteCheckoutError, filterCatalogCards, findStorefrontProduct, firstOptionalString, flattenMerchantHtmlToText, formatCompactCount, formatDeliveryTime, formatLicenseTerm, formatMoney, formatSubscriptionCadence, formatSubscriptionTrial, getAuthoredContent, getBuyerCurrency, getProductGroups, getProducts, getShoppex, getStorefrontAddons, getStorefrontCategories, getStorefrontListing, getStorefrontSettings, getStorefrontShop, hasInjectedStorefrontData, isAccountTab, isBlankMerchantHtml, isCheckboxValueChecked, isImplementedAccountTab, isProductSoldOut, isRecord, isSelectionSoldOut, loadShopPage, missingRequiredCheckoutValues, optionalBoolean, optionalString, optionalStringWithDefault, paginateCatalogCards, parseAccountPath, parseCartQuoteLines, parseEnum, parseFiniteNumber, parseProductBadges, parseStorefrontSortKey, readCheckoutNeedsDetails, readStorefrontData, requireArray, requireBoolean, requireNumber, requireRecord, requireString, requireStringFromKeys, requireUnknownRecord, resolveAccountView, resolveBadgeForeground, resolveCartBoundsHeals, resolveCategorySelection, resolveCheckoutFields, resolveDeliveryPromise, resolveDisplayStock, resolveHideStockCounter, resolveLicenseTerm, resolveMenuLinksForSlot, resolvePayWhatYouWantMinimum, resolveProductVideoEmbedSrc, resolveQuantityBounds, resolveSupportChannels, resolveUnsellableLineIds, resolveVariantStock, reviewsForProduct, roundCurrency, sanitizeMerchantHtml, sortCatalogCards, startCartMutation, stripMerchantHtmlImages, submitContactTicket, toShopReviews, toStarCount, useCart, useNavigationLinks, useOnlineUserCount, useShopReviews, useStorefront, validateCheckoutFieldValue, validateCheckoutFields, validateContactSubmission };
|