@shoppexio/storefront-react 0.1.5 → 0.2.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.
@@ -158,6 +158,147 @@ function getBuyerCurrency() {
158
158
  return requiredGlobal("defaultCurrency");
159
159
  }
160
160
 
161
+ // src/favorites.tsx
162
+ import { createContext, useContext, useEffect, useMemo, useSyncExternalStore } from "react";
163
+
164
+ // src/account/account-favorites.ts
165
+ function isRecord(value) {
166
+ return typeof value === "object" && value !== null && !Array.isArray(value);
167
+ }
168
+ function readNullableString(record, key) {
169
+ const value = record[key];
170
+ if (value === null || typeof value === "string") return value;
171
+ return void 0;
172
+ }
173
+ function narrowFavorites(payload) {
174
+ if (!isRecord(payload) || !Array.isArray(payload.favorites)) return { ok: false };
175
+ const favorites = [];
176
+ for (const entry of payload.favorites) {
177
+ if (!isRecord(entry)) return { ok: false };
178
+ const productUniqid = entry.product_uniqid;
179
+ const title = entry.title;
180
+ const slug = readNullableString(entry, "slug");
181
+ const imageUrl = readNullableString(entry, "image_url");
182
+ const priceDisplay = entry.price_display;
183
+ const unavailable = entry.unavailable;
184
+ const createdAt = entry.created_at;
185
+ if (typeof productUniqid !== "string" || typeof title !== "string" || slug === void 0 || imageUrl === void 0 || typeof priceDisplay !== "number" || !Number.isFinite(priceDisplay) || typeof unavailable !== "boolean" || typeof createdAt !== "string") {
186
+ return { ok: false };
187
+ }
188
+ favorites.push({
189
+ product_uniqid: productUniqid,
190
+ title,
191
+ slug,
192
+ image_url: imageUrl,
193
+ price_display: priceDisplay,
194
+ unavailable,
195
+ created_at: createdAt
196
+ });
197
+ }
198
+ return { ok: true, favorites };
199
+ }
200
+ function favoriteProductHref(favorite) {
201
+ if (favorite.unavailable || !favorite.slug) return null;
202
+ return `/product/${encodeURIComponent(favorite.slug)}`;
203
+ }
204
+ function withoutFavorite(favorites, productUniqid) {
205
+ return favorites.filter((favorite) => favorite.product_uniqid !== productUniqid);
206
+ }
207
+
208
+ // src/favorites.tsx
209
+ import { jsx } from "react/jsx-runtime";
210
+ var FAVORITES_CHANGED_EVENT = "sx:favorites-changed";
211
+ function notifyFavoritesChanged() {
212
+ if (typeof window !== "undefined") window.dispatchEvent(new Event(FAVORITES_CHANGED_EVENT));
213
+ }
214
+ async function callSdk(operation) {
215
+ try {
216
+ return await operation(getShoppex());
217
+ } catch {
218
+ return { success: false };
219
+ }
220
+ }
221
+ var sdkFavoritesTransport = {
222
+ list: () => callSdk((sdk) => sdk.favorites()),
223
+ add: (productUniqid) => callSdk((sdk) => sdk.addFavorite(productUniqid)),
224
+ remove: (productUniqid) => callSdk((sdk) => sdk.removeFavorite(productUniqid))
225
+ };
226
+ function createFavoritesStore(transport) {
227
+ let snapshot = { status: "loading", saved: /* @__PURE__ */ new Set(), pending: /* @__PURE__ */ new Set() };
228
+ const listeners = /* @__PURE__ */ new Set();
229
+ const update = (next) => {
230
+ snapshot = { ...snapshot, ...next };
231
+ for (const listener of listeners) listener();
232
+ };
233
+ return {
234
+ getSnapshot: () => snapshot,
235
+ subscribe(listener) {
236
+ listeners.add(listener);
237
+ return () => listeners.delete(listener);
238
+ },
239
+ async load() {
240
+ const result = await transport.list();
241
+ if (!result.success) {
242
+ update({ status: result.status === 401 ? "signed_out" : "unavailable", saved: /* @__PURE__ */ new Set() });
243
+ return;
244
+ }
245
+ const narrowed = narrowFavorites(result.data);
246
+ if (!narrowed.ok) {
247
+ update({ status: "unavailable", saved: /* @__PURE__ */ new Set() });
248
+ return;
249
+ }
250
+ update({ status: "ready", saved: new Set(narrowed.favorites.map((favorite) => favorite.product_uniqid)) });
251
+ },
252
+ async toggle(productUniqid) {
253
+ if (snapshot.status === "signed_out") return "signed_out";
254
+ if (snapshot.status !== "ready" || snapshot.pending.has(productUniqid)) return "failed";
255
+ const adding = !snapshot.saved.has(productUniqid);
256
+ update({ pending: new Set(snapshot.pending).add(productUniqid) });
257
+ const result = adding ? await transport.add(productUniqid) : await transport.remove(productUniqid);
258
+ const pending = new Set(snapshot.pending);
259
+ pending.delete(productUniqid);
260
+ if (!result.success) {
261
+ if (result.status === 401) {
262
+ update({ pending, status: "signed_out", saved: /* @__PURE__ */ new Set() });
263
+ return "signed_out";
264
+ }
265
+ update({ pending });
266
+ return "failed";
267
+ }
268
+ const saved = new Set(snapshot.saved);
269
+ if (adding) saved.add(productUniqid);
270
+ else saved.delete(productUniqid);
271
+ update({ pending, saved });
272
+ return adding ? "added" : "removed";
273
+ }
274
+ };
275
+ }
276
+ var FavoritesContext = createContext(null);
277
+ function FavoritesProvider({
278
+ children,
279
+ transport = sdkFavoritesTransport
280
+ }) {
281
+ const store = useMemo(() => createFavoritesStore(transport), [transport]);
282
+ useEffect(() => {
283
+ void store.load();
284
+ const onChanged = () => void store.load();
285
+ window.addEventListener(FAVORITES_CHANGED_EVENT, onChanged);
286
+ return () => window.removeEventListener(FAVORITES_CHANGED_EVENT, onChanged);
287
+ }, [store]);
288
+ return /* @__PURE__ */ jsx(FavoritesContext.Provider, { value: store, children });
289
+ }
290
+ function useFavorites() {
291
+ const store = useContext(FavoritesContext);
292
+ if (!store) throw new Error("useFavorites must be used inside FavoritesProvider.");
293
+ const snapshot = useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);
294
+ return {
295
+ status: snapshot.status,
296
+ isFavorite: (productUniqid) => snapshot.saved.has(productUniqid),
297
+ isPending: (productUniqid) => snapshot.pending.has(productUniqid),
298
+ toggle: store.toggle
299
+ };
300
+ }
301
+
161
302
  export {
162
303
  ACCOUNT_PATH,
163
304
  ACCOUNT_PATH_ALIAS,
@@ -176,6 +317,15 @@ export {
176
317
  ACCOUNT_ARTIFACT_CAPABILITIES,
177
318
  SERVING_CONTEXT_HINT,
178
319
  getShoppex,
179
- getBuyerCurrency
320
+ getBuyerCurrency,
321
+ narrowFavorites,
322
+ favoriteProductHref,
323
+ withoutFavorite,
324
+ FAVORITES_CHANGED_EVENT,
325
+ notifyFavoritesChanged,
326
+ sdkFavoritesTransport,
327
+ createFavoritesStore,
328
+ FavoritesProvider,
329
+ useFavorites
180
330
  };
181
- //# sourceMappingURL=chunk-D6XKDVAV.js.map
331
+ //# sourceMappingURL=chunk-ZEFZPUAW.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/account-route.ts","../src/shoppex.ts","../src/favorites.tsx","../src/account/account-favorites.ts"],"sourcesContent":["/**\n * Which account surface a URL names.\n *\n * The account lives under `/dashboard`, and the panel is a PATH SEGMENT:\n * `/dashboard/orders`, `/dashboard/orders/<uniqid>`, `/dashboard/settings`.\n * `/dashboard` itself is the overview. This is the platform's route shape and it\n * is identical on both storefront lanes (ADR-0064), so a theme that invents its\n * own account paths breaks links it does not own.\n *\n * The older query spelling — `?tab=<tab>` and `?order=<id>` — is still ACCEPTED,\n * because it sits in order emails already sent and in the platform's post-login\n * redirect. It is read as input and never written: a URL carrying it is\n * canonicalized to the path form, by replacement rather than a push, so the back\n * button does not bounce the buyer between the two spellings. The path always\n * wins when a URL carries both.\n *\n * `/customer-portal` is the oldest spelling of the same route and folds in with\n * its tail and query intact.\n *\n * A locale prefix (`/de/dashboard/orders`) is part of the served URL on a\n * localized storefront. The router's basename normally strips it before any of\n * this sees it, so the parser accepts a pathname with or without one and\n * canonicalization keeps whatever prefix it was given.\n *\n * Everything here is pure string work, so `tests/account-route.test.ts` can pin\n * the parts email links depend on.\n */\n\nexport const ACCOUNT_PATH = '/dashboard';\n\n/** The older spelling of the same route. Folded, never served separately. */\nexport const ACCOUNT_PATH_ALIAS = '/customer-portal';\n\nconst ACCOUNT_SEGMENT = 'dashboard';\nconst ACCOUNT_ALIAS_SEGMENT = 'customer-portal';\n\n/**\n * Every tab the Shoppex account surface defines, in navigation order.\n *\n * Listed in full so a link to a real platform tab can be told apart from a typo\n * and answered differently.\n */\nexport const ACCOUNT_TABS = [\n 'overview',\n 'orders',\n 'downloads',\n 'subscriptions',\n 'rewards',\n 'referrals',\n 'favorites',\n 'support',\n 'settings',\n 'reseller',\n] as const;\n\nexport type AccountTab = (typeof ACCOUNT_TABS)[number];\n\n/**\n * The catalog key a tab's label lives under.\n *\n * A key rather than a string: this file is imported by the router, the\n * navigation and the \"not part of this store\" notice, and each of them needs\n * the label in the locale the edge served — see `account/i18n.ts`.\n */\nexport function accountTabLabelKey(tab: AccountTab): string {\n return `account.tabs.${tab}`;\n}\n\n/**\n * The tabs the account renders.\n *\n * All ten platform tabs. Adding a panel means adding its tab here AND\n * rendering it, in the same change: a tab listed without a panel would navigate\n * to the \"not built\" notice, which is worse than not offering it.\n *\n * Reseller payloads are locally narrowed because shared contracts do not cover\n * them, but the worker now allowlists their SDK routes.\n */\nexport const IMPLEMENTED_ACCOUNT_TABS = [\n 'overview',\n 'orders',\n 'downloads',\n 'subscriptions',\n 'rewards',\n 'referrals',\n 'favorites',\n 'support',\n 'settings',\n 'reseller',\n] as const;\n\nexport type ImplementedAccountTab = (typeof IMPLEMENTED_ACCOUNT_TABS)[number];\n\nexport const DEFAULT_ACCOUNT_TAB: ImplementedAccountTab = 'overview';\n\n/**\n * What the account page should show.\n *\n * `not-built` and `unknown-tab` are separate on purpose. The first is a real\n * platform tab this storefront chose not to render, the second is a value that\n * names no tab at all. Collapsing them into \"show the overview instead\" would\n * hide a broken link behind a page that looks like it worked.\n */\nexport type AccountView =\n | { kind: 'panel'; tab: ImplementedAccountTab }\n | { kind: 'order'; orderId: string }\n | { kind: 'not-built'; tab: AccountTab }\n | { kind: 'unknown-tab' };\n\nexport function isAccountTab(value: string): value is AccountTab {\n return (ACCOUNT_TABS as readonly string[]).includes(value);\n}\n\nexport function isImplementedAccountTab(value: string): value is ImplementedAccountTab {\n return (IMPLEMENTED_ACCOUNT_TABS as readonly string[]).includes(value);\n}\n\n/** A two-letter locale segment, the only prefix the storefront edge serves. */\nconst LOCALE_SEGMENT = /^[a-z]{2}$/i;\n\nfunction isAccountSegment(segment: string | undefined): boolean {\n return segment === ACCOUNT_SEGMENT || segment === ACCOUNT_ALIAS_SEGMENT;\n}\n\nexport interface AccountRouteParts {\n /** What preceded `/dashboard`: '' or a locale prefix such as '/de'. */\n prefix: string;\n /** Whether the URL used the retired `/customer-portal` spelling. */\n alias: boolean;\n /** The segments after `/dashboard`, decoded. */\n tail: string[];\n}\n\n/**\n * Split an account URL into its prefix and the segments below `/dashboard`.\n *\n * Null when the path is not the account route at all. The account segment is\n * only recognised first or directly after a locale segment — searching for it\n * anywhere would make `/product/dashboard-pro` an account URL.\n */\nexport function parseAccountPath(pathname: string): AccountRouteParts | null {\n const segments = pathname.split('/').filter((segment) => segment.length > 0);\n\n let index = -1;\n if (isAccountSegment(segments[0])) index = 0;\n else if (segments[0] && LOCALE_SEGMENT.test(segments[0]) && isAccountSegment(segments[1])) index = 1;\n if (index === -1) return null;\n\n return {\n prefix: index === 0 ? '' : `/${segments[0]}`,\n alias: segments[index] === ACCOUNT_ALIAS_SEGMENT,\n tail: segments.slice(index + 1).map(decodeSegment),\n };\n}\n\nfunction decodeSegment(segment: string): string {\n try {\n return decodeURIComponent(segment);\n } catch {\n // A malformed escape is not a segment this route can name; keeping it raw\n // lets it fall through to `unknown-tab` instead of throwing on a link a\n // buyer merely mistyped.\n return segment;\n }\n}\n\n/**\n * What to render for a URL.\n *\n * The path is read first and the query only when the path names no panel, so a\n * canonical URL is never reinterpreted by a stale parameter riding along on it.\n */\nexport function resolveAccountView(pathname: string, search: URLSearchParams): AccountView {\n const parts = parseAccountPath(pathname);\n if (!parts) return { kind: 'unknown-tab' };\n\n if (parts.tail.length === 0) return resolveLegacyQueryView(search);\n\n const [first, second, ...rest] = parts.tail;\n if (rest.length > 0) return { kind: 'unknown-tab' };\n if (!first) return resolveLegacyQueryView(search);\n\n if (second !== undefined) {\n // The only two-segment surface: one order inside the orders panel.\n if (first !== 'orders' || second.length === 0) return { kind: 'unknown-tab' };\n return { kind: 'order', orderId: second };\n }\n\n return viewForTab(first);\n}\n\n/**\n * The retired spelling, read as input only.\n *\n * `?order=` belongs to the orders panel, so it is honoured when the query asks\n * for the orders tab or names no tab at all. An explicit different tab wins:\n * `?tab=settings&order=x` is a request for settings, and answering it with an\n * order would be the page deciding it knows better.\n */\nfunction resolveLegacyQueryView(search: URLSearchParams): AccountView {\n const rawTab = (search.get('tab') ?? '').trim();\n // `invoice` is the portal's spelling and it is already out in the world:\n // notification mails carry `?tab=orders&invoice=<uniqid>` and cannot be\n // edited after the fact. Reading only `order` would open the list and\n // canonicalize the invoice away.\n const orderId = (search.get('order') ?? search.get('invoice') ?? '').trim();\n\n if (rawTab && !isAccountTab(rawTab)) return { kind: 'unknown-tab' };\n if (orderId && (rawTab === '' || rawTab === 'orders')) return { kind: 'order', orderId };\n if (rawTab === '') return { kind: 'panel', tab: DEFAULT_ACCOUNT_TAB };\n\n return viewForTab(rawTab);\n}\n\nfunction viewForTab(value: string): AccountView {\n if (!isAccountTab(value)) return { kind: 'unknown-tab' };\n if (!isImplementedAccountTab(value)) return { kind: 'not-built', tab: value };\n return { kind: 'panel', tab: value };\n}\n\n/** `/dashboard/<tab>`. The overview is the bare path, not `/dashboard/overview`. */\nexport function accountTabHref(tab: AccountTab): string {\n if (tab === DEFAULT_ACCOUNT_TAB) return ACCOUNT_PATH;\n return `${ACCOUNT_PATH}/${tab}`;\n}\n\n/** `/dashboard/orders/<id>`, the one order-detail spelling. */\nexport function accountOrderHref(orderId: string): string {\n return `${ACCOUNT_PATH}/orders/${encodeURIComponent(orderId)}`;\n}\n\n/**\n * The canonical path for a URL that is not written the canonical way, or null\n * when it already is.\n *\n * Three URLs need rewriting: the `/customer-portal` alias, a `?tab=`/`?order=`\n * query, and the redundant `/dashboard/overview`. The caller replaces rather\n * than pushes — a push would put the retired spelling in the history and send\n * the back button straight back into it.\n *\n * A URL naming nothing real (`?tab=orderz`, `/dashboard/nope`) has no canonical\n * form and keeps the address it came with: rewriting it to the overview would\n * turn a broken link into one that looks like it worked.\n *\n * The prefix the URL came with is preserved, so a buyer on `/de/dashboard` is\n * not quietly moved out of their locale.\n */\nexport function canonicalAccountPath(pathname: string, search: URLSearchParams): string | null {\n const parts = parseAccountPath(pathname);\n if (!parts) return null;\n\n const view = resolveAccountView(pathname, search);\n const canonical = canonicalPathForView(view);\n if (canonical === null) return null;\n\n const current = `${parts.prefix}${ACCOUNT_PATH}${parts.tail.map((segment) => `/${encodeURIComponent(segment)}`).join('')}`;\n const target = `${parts.prefix}${canonical}`;\n\n // Already canonical AND carrying nothing to strip: leave the URL alone.\n if (!parts.alias && current === target && search.get('tab') === null && search.get('order') === null && search.get('invoice') === null) {\n return null;\n }\n\n return target;\n}\n\nfunction canonicalPathForView(view: AccountView): string | null {\n switch (view.kind) {\n case 'panel':\n return accountTabHref(view.tab);\n case 'order':\n return accountOrderHref(view.orderId);\n case 'not-built':\n return accountTabHref(view.tab);\n case 'unknown-tab':\n return null;\n }\n}\n\n\n/**\n * The storefront routes the account links to outside itself: the catalog\n * behind every \"browse the store\" action, and a product page per favorite.\n * React Router does not check a `to`, so a host that does not route one of\n * these answers the account's own buttons with its not-found page. A host\n * checks its router against this list (ForkMe: `tests/account-routes.test.ts`).\n */\nexport const ACCOUNT_STOREFRONT_ROUTES = ['/all-products', '/product/:slug'] as const;\n\n/**\n * The artifact capabilities the account relies on. A storefront that mounts\n * the account lists each of them in its `public/shoppex.capabilities.json`;\n * the storefront worker reads that file from the artifact and only then hands\n * the account what the capability promises. `account-reseller-invite-cookie`\n * is pinned to `RESELLER_INVITE_ARTIFACT_CAPABILITY` in\n * `workers/storefront/src/reseller-invite-link.ts`: without it the worker\n * leaves a reseller invitation in the URL for that build.\n */\nexport const ACCOUNT_ARTIFACT_CAPABILITIES = ['account-reseller-invite-cookie'] as const;\n","import type { ShoppexSdk } from './sdk';\n\n/**\n * Access to the Shoppex Storefront SDK global (loaded in index.html from\n * cdn.shoppex.io) plus its one-time initialization from the bootstrap globals\n * the Shoppex worker injects next to __SHOPPEX_INITIAL__:\n * shopSlug, shopId, apiBaseUrl, defaultCurrency (+ optional checkoutBaseUrl).\n *\n * Both the script and the globals are hard requirements — a storefront without\n * them cannot price, cart, or check out, so we fail fast instead of rendering\n * a broken shop.\n */\n\nlet initialized = false;\n\n/**\n * The sentence that names the CAUSE rather than the symptom.\n *\n * A missing bootstrap global and a missing `__SHOPPEX_INITIAL__` payload are\n * the same fault seen from two sides: the page is not being served by the\n * worker that injects both. `App.tsx`'s configuration screen shows this to the\n * merchant, so it lives here once instead of being written twice.\n */\nexport const SERVING_CONTEXT_HINT =\n 'This storefront must be served by the Shoppex storefront (or preview) worker.';\n\nfunction requiredGlobal(name: 'shopSlug' | 'shopId' | 'apiBaseUrl' | 'defaultCurrency'): string {\n const value = window[name];\n if (typeof value !== 'string' || !value.trim()) {\n throw new Error(`Shoppex bootstrap global \"${name}\" is missing. ${SERVING_CONTEXT_HINT}`);\n }\n return value.trim();\n}\n\nexport function getShoppex(): ShoppexSdk {\n const sdk = window.shoppex;\n if (!sdk) {\n throw new Error(\n 'The Shoppex SDK script did not load (window.shoppex is missing). Check the <script src=\"https://cdn.shoppex.io/sdk/...\"> tag in index.html.',\n );\n }\n if (!initialized) {\n const storeSlug = requiredGlobal('shopSlug');\n if (!sdk.isInitialized() || sdk.getConfig().storeSlug !== storeSlug) {\n const checkoutBaseUrl = typeof window.checkoutBaseUrl === 'string' && window.checkoutBaseUrl.trim()\n ? window.checkoutBaseUrl.trim()\n : undefined;\n sdk.init(storeSlug, {\n shopId: requiredGlobal('shopId'),\n apiBaseUrl: requiredGlobal('apiBaseUrl'),\n currency: requiredGlobal('defaultCurrency'),\n ...(checkoutBaseUrl ? { checkoutBaseUrl } : {}),\n });\n }\n // Last-click referral capture. checkout() reads this stored code by itself\n // whenever the cart does not carry an affiliate code of its own, so this\n // single call is what makes `?ref=<code>` links actually pay out. A code the\n // buyer TYPED and the quote classified as a referral takes precedence — see\n // `cart.tsx` → `checkout`.\n // A failed capture must never block the storefront from rendering.\n void sdk.captureAffiliateFromUrl().catch(() => undefined);\n initialized = true;\n }\n return sdk;\n}\n\n/**\n * The currency the prices on this page are in.\n *\n * The edge injected it alongside the catalog, from the same payload the prices\n * came from, so it is right whether the buyer asked for a currency, whether\n * their country selected one, or neither.\n *\n * Pass it to every quote and checkout. Left out, the SDK falls back to reading\n * `?currency=` off `window.location` — which survives the first page load and\n * nothing after it, because client-side navigation drops the query. The cart\n * then quoted in the shop's geo-resolved currency while the page went on\n * showing the one the buyer asked for.\n */\nexport function getBuyerCurrency(): string {\n return requiredGlobal('defaultCurrency');\n}\n","/**\n * Favorites outside the account: the heart on product cards and pages.\n *\n * Same backend as the account's Favorites panel (`/v1/customer/favorites`,\n * reached through the SDK on the shop's own origin, where the Shoppex worker\n * turns the HttpOnly session cookie into the buyer's session). There is no\n * guest wishlist: a signed-out visitor who presses a heart is sent to sign in,\n * and nothing is stored in the browser.\n *\n * The saved set is read once per page load. A heart only changes after the\n * server confirmed the write, like the account panel's remove: flipping it\n * first and flipping it back on failure makes the buyer watch their own action\n * undo itself.\n *\n * This module stays out of the account entry on purpose (no account messages,\n * no account UI), so a card grid does not load the account bundle.\n */\n\nimport { createContext, useContext, useEffect, useMemo, useSyncExternalStore, type ReactNode } from 'react';\nimport { narrowFavorites } from './account/account-favorites';\nimport { getShoppex } from './shoppex';\nimport type { SdkResponse, ShoppexSdk } from './sdk';\n\n/**\n * - `loading`: the saved set has not been read yet (render the heart idle).\n * - `signed_out`: the read answered 401; a press asks the buyer to sign in.\n * - `ready`: the saved set is known.\n * - `unavailable`: the read failed for another reason (5xx, no SDK support, a\n * changed contract, a buyer with no customer profile in this shop). The\n * feature cannot work on this page, so render no heart.\n */\nexport type FavoritesStatus = 'loading' | 'signed_out' | 'ready' | 'unavailable';\n\n/** What one press did. `signed_out` means: send the buyer to sign in. */\nexport type FavoriteToggleResult = 'added' | 'removed' | 'signed_out' | 'failed';\n\nexport interface FavoritesSnapshot {\n status: FavoritesStatus;\n saved: ReadonlySet<string>;\n pending: ReadonlySet<string>;\n}\n\nexport interface FavoritesApi {\n status: FavoritesStatus;\n isFavorite(productUniqid: string): boolean;\n /** A write for this product is in flight; the heart should say so and ignore presses. */\n isPending(productUniqid: string): boolean;\n toggle(productUniqid: string): Promise<FavoriteToggleResult>;\n}\n\n/** Fired by the account panel after it removed a favorite, so hearts on the page follow. */\nexport const FAVORITES_CHANGED_EVENT = 'sx:favorites-changed';\n\nexport function notifyFavoritesChanged(): void {\n if (typeof window !== 'undefined') window.dispatchEvent(new Event(FAVORITES_CHANGED_EVENT));\n}\n\n/** The three SDK calls the store needs; injectable so the logic is testable without a page. */\nexport interface FavoritesTransport {\n list(): Promise<SdkResponse<unknown>>;\n add(productUniqid: string): Promise<SdkResponse<unknown>>;\n remove(productUniqid: string): Promise<SdkResponse<unknown>>;\n}\n\nasync function callSdk<T>(operation: (sdk: ShoppexSdk) => Promise<SdkResponse<T>>): Promise<SdkResponse<T>> {\n try {\n return await operation(getShoppex());\n } catch {\n // No SDK, or an SDK older than the customer module: the feature is not available here.\n return { success: false };\n }\n}\n\nexport const sdkFavoritesTransport: FavoritesTransport = {\n list: () => callSdk((sdk) => sdk.favorites()),\n add: (productUniqid) => callSdk((sdk) => sdk.addFavorite(productUniqid)),\n remove: (productUniqid) => callSdk((sdk) => sdk.removeFavorite(productUniqid)),\n};\n\nexport interface FavoritesStore {\n getSnapshot(): FavoritesSnapshot;\n subscribe(listener: () => void): () => void;\n load(): Promise<void>;\n toggle(productUniqid: string): Promise<FavoriteToggleResult>;\n}\n\nexport function createFavoritesStore(transport: FavoritesTransport): FavoritesStore {\n let snapshot: FavoritesSnapshot = { status: 'loading', saved: new Set(), pending: new Set() };\n const listeners = new Set<() => void>();\n const update = (next: Partial<FavoritesSnapshot>) => {\n snapshot = { ...snapshot, ...next };\n for (const listener of listeners) listener();\n };\n\n return {\n getSnapshot: () => snapshot,\n subscribe(listener) {\n listeners.add(listener);\n return () => listeners.delete(listener);\n },\n async load() {\n const result = await transport.list();\n if (!result.success) {\n update({ status: result.status === 401 ? 'signed_out' : 'unavailable', saved: new Set() });\n return;\n }\n const narrowed = narrowFavorites(result.data);\n if (!narrowed.ok) {\n update({ status: 'unavailable', saved: new Set() });\n return;\n }\n update({ status: 'ready', saved: new Set(narrowed.favorites.map((favorite) => favorite.product_uniqid)) });\n },\n async toggle(productUniqid) {\n if (snapshot.status === 'signed_out') return 'signed_out';\n if (snapshot.status !== 'ready' || snapshot.pending.has(productUniqid)) return 'failed';\n\n const adding = !snapshot.saved.has(productUniqid);\n update({ pending: new Set(snapshot.pending).add(productUniqid) });\n const result = adding ? await transport.add(productUniqid) : await transport.remove(productUniqid);\n const pending = new Set(snapshot.pending);\n pending.delete(productUniqid);\n\n if (!result.success) {\n if (result.status === 401) {\n // The session ended since the page loaded.\n update({ pending, status: 'signed_out', saved: new Set() });\n return 'signed_out';\n }\n update({ pending });\n return 'failed';\n }\n const saved = new Set(snapshot.saved);\n if (adding) saved.add(productUniqid);\n else saved.delete(productUniqid);\n update({ pending, saved });\n return adding ? 'added' : 'removed';\n },\n };\n}\n\nconst FavoritesContext = createContext<FavoritesStore | null>(null);\n\nexport function FavoritesProvider({\n children,\n transport = sdkFavoritesTransport,\n}: {\n children: ReactNode;\n transport?: FavoritesTransport;\n}) {\n const store = useMemo(() => createFavoritesStore(transport), [transport]);\n\n useEffect(() => {\n void store.load();\n const onChanged = () => void store.load();\n window.addEventListener(FAVORITES_CHANGED_EVENT, onChanged);\n return () => window.removeEventListener(FAVORITES_CHANGED_EVENT, onChanged);\n }, [store]);\n\n return <FavoritesContext.Provider value={store}>{children}</FavoritesContext.Provider>;\n}\n\nexport function useFavorites(): FavoritesApi {\n const store = useContext(FavoritesContext);\n if (!store) throw new Error('useFavorites must be used inside FavoritesProvider.');\n const snapshot = useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);\n return {\n status: snapshot.status,\n isFavorite: (productUniqid) => snapshot.saved.has(productUniqid),\n isPending: (productUniqid) => snapshot.pending.has(productUniqid),\n toggle: store.toggle,\n };\n}\n","/**\n * The favorites panel's pure half.\n *\n * `GET /favorites` is one of the endpoints `@shoppex/contracts` does not\n * describe, so the SDK answers `unknown` and the narrowing is here, written\n * against what `customer-favorites.ts` actually selects. Every field it sends\n * is required: a saved product with no title and no price is not a favorite the\n * buyer can act on, and rendering it as an empty card would hide the fault.\n */\n\nexport interface CustomerFavorite {\n product_uniqid: string;\n title: string;\n /** Null for a product with no slug — such a product has no page to open. */\n slug: string | null;\n image_url: string | null;\n price_display: number;\n /** True once the merchant deletes the product. The row stays, the link goes. */\n unavailable: boolean;\n created_at: string;\n}\n\nexport type FavoritesResult =\n | { ok: true; favorites: CustomerFavorite[] }\n | { ok: false };\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction readNullableString(record: Record<string, unknown>, key: string): string | null | undefined {\n const value = record[key];\n if (value === null || typeof value === 'string') return value;\n return undefined;\n}\n\nexport function narrowFavorites(payload: unknown): FavoritesResult {\n if (!isRecord(payload) || !Array.isArray(payload.favorites)) return { ok: false };\n\n const favorites: CustomerFavorite[] = [];\n for (const entry of payload.favorites) {\n if (!isRecord(entry)) return { ok: false };\n\n const productUniqid = entry.product_uniqid;\n const title = entry.title;\n const slug = readNullableString(entry, 'slug');\n const imageUrl = readNullableString(entry, 'image_url');\n const priceDisplay = entry.price_display;\n const unavailable = entry.unavailable;\n const createdAt = entry.created_at;\n\n if (\n typeof productUniqid !== 'string'\n || typeof title !== 'string'\n || slug === undefined\n || imageUrl === undefined\n || typeof priceDisplay !== 'number'\n || !Number.isFinite(priceDisplay)\n || typeof unavailable !== 'boolean'\n || typeof createdAt !== 'string'\n ) {\n return { ok: false };\n }\n\n favorites.push({\n product_uniqid: productUniqid,\n title,\n slug,\n image_url: imageUrl,\n price_display: priceDisplay,\n unavailable,\n created_at: createdAt,\n });\n }\n\n return { ok: true, favorites };\n}\n\n/**\n * Where a saved product lives in this storefront, or null when it has nowhere\n * to go.\n *\n * A product the merchant deleted keeps its row so the buyer can clear it, but\n * it must not keep its link: `/product/<slug>` would answer with the storefront's\n * not-found page, which reads as a broken shop rather than a removed product.\n */\nexport function favoriteProductHref(favorite: CustomerFavorite): string | null {\n if (favorite.unavailable || !favorite.slug) return null;\n return `/product/${encodeURIComponent(favorite.slug)}`;\n}\n\n/** Drops one product from a list, used after the server confirms the removal. */\nexport function withoutFavorite(\n favorites: readonly CustomerFavorite[],\n productUniqid: string,\n): CustomerFavorite[] {\n return favorites.filter((favorite) => favorite.product_uniqid !== productUniqid);\n}\n\n"],"mappings":"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","names":[]}
package/dist/index.d.ts CHANGED
@@ -1280,6 +1280,42 @@ declare function badgesForCard(badges: readonly ProductBadge[]): ProductBadge[];
1280
1280
  /** Badges the product page shows, in the merchant's order. */
1281
1281
  declare function badgesForProductPage(badges: readonly ProductBadge[]): ProductBadge[];
1282
1282
 
1283
+ /**
1284
+ * The `/status` page model: the products the merchant gave an explicit service
1285
+ * status in the dashboard (`service_status` on the catalog wire).
1286
+ *
1287
+ * Nothing is derived here. A product without a status is not listed, and a
1288
+ * shop that set none gets `null`, so a theme can hide its status route and nav
1289
+ * link instead of showing an empty or invented board. The words for the four
1290
+ * states ("Undetected", "Updating", ...) belong to the theme.
1291
+ */
1292
+
1293
+ declare const SERVICE_STATES: readonly ["operational", "updating", "detected", "offline"];
1294
+ type ServiceState = (typeof SERVICE_STATES)[number];
1295
+ interface ProductServiceStatus {
1296
+ state: ServiceState;
1297
+ /** The merchant's note ("Patch 4.2 in progress"); null when none. */
1298
+ note: string | null;
1299
+ /** ISO timestamp of the merchant's last change. */
1300
+ updatedAt: string;
1301
+ }
1302
+ interface StatusPageEntry {
1303
+ product: StorefrontProduct;
1304
+ status: ProductServiceStatus;
1305
+ }
1306
+ interface StatusPageModel {
1307
+ /** Entries in board order: the most urgent state first, then the merchant's shelf order. */
1308
+ entries: StatusPageEntry[];
1309
+ /** How many products are in each state; every state is present (0 when none). */
1310
+ counts: Record<ServiceState, number>;
1311
+ /** The states that have at least one product, in board order, for filter chips. */
1312
+ presentStates: ServiceState[];
1313
+ /** The latest change across all entries (ISO). */
1314
+ lastUpdatedAt: string;
1315
+ }
1316
+ declare function parseServiceStatus(value: unknown, context: string): ProductServiceStatus | null;
1317
+ declare function buildStatusPageModel(products: readonly StorefrontProduct[]): StatusPageModel | null;
1318
+
1283
1319
  /**
1284
1320
  * The `window.__SHOPPEX_INITIAL__` contract: catalog, store switches and addon
1285
1321
  * slots, injected by the Shoppex storefront (or preview) worker immediately
@@ -1295,6 +1331,11 @@ declare function badgesForProductPage(badges: readonly ProductBadge[]): ProductB
1295
1331
  interface StorefrontPriceVariant {
1296
1332
  id: string;
1297
1333
  label: string;
1334
+ /**
1335
+ * The merchant's one-line note on this option ("1 seat", "5 seats"). null
1336
+ * when the option has none.
1337
+ */
1338
+ description: string | null;
1298
1339
  /** What this option states about its term; `inherit` defers to the product. */
1299
1340
  license: LicenseStatement;
1300
1341
  /** Display unit price with the product percent discount already applied. */
@@ -1429,10 +1470,16 @@ type ProductType = 'PRODUCT' | 'SUBSCRIPTION' | 'INFO_CARD' | 'SERIALS' | 'PHYSI
1429
1470
  *
1430
1471
  * - `shipped`: a PHYSICAL product. Its digital `delivery_time` is null, which
1431
1472
  * would otherwise read as instant, so the type decides first.
1432
- * - `timed`: the merchant set a delivery time.
1433
- * - `instant`: digital with no delivery time.
1473
+ * - `timed`: the merchant set a positive delivery time. Fractions round UP: a
1474
+ * partial wait is still a wait.
1475
+ * - `instant`: the payload STATES a delivery time and it is `null` or `0`,
1476
+ * the merchant's written "no wait".
1477
+ * - `unspecified`: the payload carries no delivery time at all. Nobody gave a
1478
+ * promise, so none is shown; reading absence as "instant" would invent the
1479
+ * merchant's answer.
1434
1480
  *
1435
- * Parity with the hosted checkout's `apps/checkout/lib/delivery-promise.ts`.
1481
+ * Parity with the hosted checkout's `apps/checkout/lib/delivery-promise.ts`,
1482
+ * which draws the same line between a written answer and no answer.
1436
1483
  */
1437
1484
  type DeliveryPromise = {
1438
1485
  kind: 'shipped';
@@ -1442,8 +1489,34 @@ type DeliveryPromise = {
1442
1489
  unit: DeliveryTimeUnit;
1443
1490
  } | {
1444
1491
  kind: 'instant';
1492
+ } | {
1493
+ kind: 'unspecified';
1445
1494
  };
1446
- declare function resolveDeliveryPromise(product: Pick<StorefrontProduct, 'productType' | 'deliveryTimeCount' | 'deliveryTimeUnit'>): DeliveryPromise;
1495
+ declare function resolveDeliveryPromise(product: Pick<StorefrontProduct, 'productType' | 'deliveryTimeStated' | 'deliveryTimeCount' | 'deliveryTimeUnit'>): DeliveryPromise;
1496
+ /**
1497
+ * What the buyer is handed, from the product's raw delivery type. null for a
1498
+ * type this package does not know yet.
1499
+ */
1500
+ type ProductDeliverable = 'serials' | 'file' | 'service' | 'dynamic' | 'info_card' | 'subscription' | 'physical';
1501
+ /**
1502
+ * The "You receive" statement, only where the delivery type says what arrives:
1503
+ *
1504
+ * - `serials`: delivered text units. `unitLabel` is what the merchant calls
1505
+ * one (`label_singular`, e.g. "License key"), null to let the theme use its
1506
+ * own neutral word.
1507
+ * - `download`: a FILE product, delivered as a download link.
1508
+ *
1509
+ * Every other type returns null: a service, a generated item or an info card
1510
+ * does not state its contents on the wire, so a theme shows nothing rather
1511
+ * than guessing.
1512
+ */
1513
+ type YouReceive = {
1514
+ kind: 'serials';
1515
+ unitLabel: string | null;
1516
+ } | {
1517
+ kind: 'download';
1518
+ };
1519
+ declare function resolveYouReceive(product: Pick<StorefrontProduct, 'deliverable' | 'unitLabelSingular'>): YouReceive | null;
1447
1520
  /**
1448
1521
  * How often a subscription product is charged, and how long it is free first.
1449
1522
  *
@@ -1485,6 +1558,24 @@ interface StorefrontProduct {
1485
1558
  price: number;
1486
1559
  currency: string;
1487
1560
  description: string | null;
1561
+ /**
1562
+ * One plain-text line the merchant wrote for under the title
1563
+ * (`short_description`). null when they wrote none; never derived from the
1564
+ * description.
1565
+ */
1566
+ shortDescription: string | null;
1567
+ /** What the buyer is handed, from the raw delivery type. See `resolveYouReceive`. */
1568
+ deliverable: ProductDeliverable | null;
1569
+ /** The merchant's word for one delivered unit ("License key"); null when unset. */
1570
+ unitLabelSingular: string | null;
1571
+ /** The merchant's word for several delivered units ("License keys"); null when unset. */
1572
+ unitLabelPlural: string | null;
1573
+ /**
1574
+ * The merchant's explicit `/status` state for this product, set in the
1575
+ * dashboard. null when they set none: the product is then not on the status
1576
+ * board. See `buildStatusPageModel`.
1577
+ */
1578
+ serviceStatus: ProductServiceStatus | null;
1488
1579
  slug: string | null;
1489
1580
  imageUrl: string | null;
1490
1581
  /**
@@ -1627,7 +1718,12 @@ interface StorefrontProduct {
1627
1718
  license: LicenseStatement;
1628
1719
  /** Whether licensing is on at all; a stale term must not be advertised when it is off. */
1629
1720
  licensingEnabled: boolean;
1630
- /** Estimated delivery time as a bare count (null when unset/instant). */
1721
+ /**
1722
+ * Whether the payload states a delivery time at all (`delivery_time` present,
1723
+ * `null` included). false makes the promise `unspecified`.
1724
+ */
1725
+ deliveryTimeStated: boolean;
1726
+ /** Estimated delivery time as a whole positive count, rounded up; null for no wait. */
1631
1727
  deliveryTimeCount: number | null;
1632
1728
  /** Unit `deliveryTimeCount` is counted in: 'minutes' | 'hours' | 'days'. */
1633
1729
  deliveryTimeUnit: DeliveryTimeUnit;
@@ -1651,6 +1747,11 @@ interface StorefrontSettings {
1651
1747
  searchEnabled: boolean;
1652
1748
  contactFormEnabled: boolean;
1653
1749
  requireInvoiceForContact: boolean;
1750
+ /**
1751
+ * Sold-out products may offer "Notify me" (back-in-stock emails). A merchant
1752
+ * switch the backend enforces; see `canRequestStockAlert`.
1753
+ */
1754
+ stockAlertsEnabled: boolean;
1654
1755
  /**
1655
1756
  * The merchant centres bundle cards. A shop-level presentation switch, not a
1656
1757
  * theme setting: the Easy lane reads `ctx.shop.center_group_titles` and
@@ -1699,11 +1800,23 @@ interface StorefrontSettings {
1699
1800
  */
1700
1801
  activeCurrency: string;
1701
1802
  /**
1702
- * Payment gateway codes the shop has connected. The cart turns them into the
1703
- * brand marks under its checkout button — merchant fact, so an empty list
1704
- * renders no row rather than a reassuring default.
1803
+ * The payment methods the shop's connected gateways take (`store.payment_methods`),
1804
+ * cards first, then wallets, then crypto. The backend derives them from the
1805
+ * gateway codes, so a theme renders marks without a mapping of its own.
1806
+ * Merchant fact: an empty list renders no row rather than a reassuring default.
1705
1807
  */
1706
- availableGateways: string[];
1808
+ paymentMethods: StorefrontPaymentMethod[];
1809
+ }
1810
+ type StorefrontPaymentMethodKind = 'card' | 'wallet' | 'crypto';
1811
+ /**
1812
+ * One payment method the checkout takes. `id` is a stable lower-case mark id
1813
+ * (`visa`, `paypal`, `bitcoin`, `usdt`, ...); `label` is its English name.
1814
+ * Unknown ids may appear as the platform adds gateways: render the label.
1815
+ */
1816
+ interface StorefrontPaymentMethod {
1817
+ id: string;
1818
+ label: string;
1819
+ kind: StorefrontPaymentMethodKind;
1707
1820
  }
1708
1821
  /**
1709
1822
  * The shop itself: the identity and the aggregate metrics the storefront
@@ -2685,6 +2798,52 @@ interface SupportChannel {
2685
2798
  */
2686
2799
  declare function resolveSupportChannels(links: readonly SocialLink[]): SupportChannel[];
2687
2800
 
2801
+ /**
2802
+ * - `loading`: the saved set has not been read yet (render the heart idle).
2803
+ * - `signed_out`: the read answered 401; a press asks the buyer to sign in.
2804
+ * - `ready`: the saved set is known.
2805
+ * - `unavailable`: the read failed for another reason (5xx, no SDK support, a
2806
+ * changed contract, a buyer with no customer profile in this shop). The
2807
+ * feature cannot work on this page, so render no heart.
2808
+ */
2809
+ type FavoritesStatus = 'loading' | 'signed_out' | 'ready' | 'unavailable';
2810
+ /** What one press did. `signed_out` means: send the buyer to sign in. */
2811
+ type FavoriteToggleResult = 'added' | 'removed' | 'signed_out' | 'failed';
2812
+ interface FavoritesSnapshot {
2813
+ status: FavoritesStatus;
2814
+ saved: ReadonlySet<string>;
2815
+ pending: ReadonlySet<string>;
2816
+ }
2817
+ interface FavoritesApi {
2818
+ status: FavoritesStatus;
2819
+ isFavorite(productUniqid: string): boolean;
2820
+ /** A write for this product is in flight; the heart should say so and ignore presses. */
2821
+ isPending(productUniqid: string): boolean;
2822
+ toggle(productUniqid: string): Promise<FavoriteToggleResult>;
2823
+ }
2824
+ /** Fired by the account panel after it removed a favorite, so hearts on the page follow. */
2825
+ declare const FAVORITES_CHANGED_EVENT = "sx:favorites-changed";
2826
+ declare function notifyFavoritesChanged(): void;
2827
+ /** The three SDK calls the store needs; injectable so the logic is testable without a page. */
2828
+ interface FavoritesTransport {
2829
+ list(): Promise<SdkResponse<unknown>>;
2830
+ add(productUniqid: string): Promise<SdkResponse<unknown>>;
2831
+ remove(productUniqid: string): Promise<SdkResponse<unknown>>;
2832
+ }
2833
+ declare const sdkFavoritesTransport: FavoritesTransport;
2834
+ interface FavoritesStore {
2835
+ getSnapshot(): FavoritesSnapshot;
2836
+ subscribe(listener: () => void): () => void;
2837
+ load(): Promise<void>;
2838
+ toggle(productUniqid: string): Promise<FavoriteToggleResult>;
2839
+ }
2840
+ declare function createFavoritesStore(transport: FavoritesTransport): FavoritesStore;
2841
+ declare function FavoritesProvider({ children, transport, }: {
2842
+ children: ReactNode;
2843
+ transport?: FavoritesTransport;
2844
+ }): react_jsx_runtime.JSX.Element;
2845
+ declare function useFavorites(): FavoritesApi;
2846
+
2688
2847
  /**
2689
2848
  * The merchant's navigation menus, and where they sit against theme content.
2690
2849
  *
@@ -3129,6 +3288,42 @@ type ShopPageState = {
3129
3288
  */
3130
3289
  declare function loadShopPage(slug: string): Promise<ShopPageState>;
3131
3290
 
3291
+ /**
3292
+ * "Notify me" on sold-out products: a buyer leaves an email and gets one email
3293
+ * when the product (or the chosen option) can be ordered again.
3294
+ *
3295
+ * The control appears only where it can work: the shop allows it
3296
+ * (`settings.stockAlertsEnabled`, a merchant switch enforced by the backend),
3297
+ * and the selection is sold out, not paused. A paused ("coming soon") product
3298
+ * is not a stock state, and an info card can never be bought, so neither
3299
+ * offers an alert.
3300
+ *
3301
+ * The endpoint answers the same for a new and a repeated request, so the UI
3302
+ * says "We'll email you" either way.
3303
+ */
3304
+
3305
+ declare function canRequestStockAlert(settings: Pick<StorefrontSettings, 'stockAlertsEnabled'>, product: Pick<StorefrontProduct, 'onHold' | 'productType'>, soldOut: boolean): boolean;
3306
+ interface StockAlertRequest {
3307
+ /** The product's uniqid (`StorefrontProduct.id`). */
3308
+ productId: string;
3309
+ /** The sold-out option, or null for the product as a whole. */
3310
+ variantId: StorefrontPriceVariant['id'] | null;
3311
+ email: string;
3312
+ /**
3313
+ * The hidden honeypot input's value. Pass it through untouched: people leave
3314
+ * it empty, form bots fill it, and the server quietly ignores a filled one.
3315
+ */
3316
+ honeypot?: string;
3317
+ }
3318
+ type StockAlertResult = {
3319
+ ok: true;
3320
+ } | {
3321
+ ok: false;
3322
+ status: number | null;
3323
+ message: string;
3324
+ };
3325
+ declare function requestStockAlert(request: StockAlertRequest): Promise<StockAlertResult>;
3326
+
3132
3327
  /**
3133
3328
  * The sentence that names the CAUSE rather than the symptom.
3134
3329
  *
@@ -3206,4 +3401,4 @@ declare function useOnlineUserCount(): number | null;
3206
3401
  */
3207
3402
  declare function resolveProductVideoEmbedSrc(value: unknown): string | null;
3208
3403
 
3209
- 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 PagedShopReviewsState, 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 SdkShopReviewsSummary, type ShopPageFaqItem, type ShopPageModel, type ShopPageState, type ShopReview, type ShopReviewLabels, type ShopReviewsPageResult, 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, isCardSoldOut, isCheckboxValueChecked, isImplementedAccountTab, isProductSoldOut, isRecord, isSelectionSoldOut, loadLatestWrittenReviews, loadShopPage, loadShopReviewsPage, 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, usePagedShopReviews, useShopReviews, useStorefront, validateCheckoutFieldValue, validateCheckoutFields, validateContactSubmission, withRequiredAddons };
3404
+ 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, FAVORITES_CHANGED_EVENT, type FavoriteToggleResult, type FavoritesApi, FavoritesProvider, type FavoritesSnapshot, type FavoritesStatus, type FavoritesStore, type FavoritesTransport, 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 PagedShopReviewsState, type ProductAddon, type ProductAutomaticDeal, type ProductBadge, type ProductBadgeIconName, type ProductBundle, type ProductBundleItem, type ProductCardModel, type ProductCategoryRef, type ProductContentTab, type ProductCustomField, type ProductDeliverable, type ProductDetailModel, type ProductFaq, type ProductRatingModel, type ProductServiceStatus, type ProductSubscription, type ProductType, QUOTE_UNAVAILABLE_CHECKOUT_ERROR, type RatingStars, type ResellerProgramSummary, type RewardProgramItem, type RewardProgramSummary, SERVICE_STATES, 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 SdkShopReviewsSummary, type ServiceState, type ShopPageFaqItem, type ShopPageModel, type ShopPageState, type ShopReview, type ShopReviewLabels, type ShopReviewsPageResult, type ShopReviewsState, type ShoppexSdk, type SocialLink, type SocialNetwork, type StatusPageEntry, type StatusPageModel, type StockAlertRequest, type StockAlertResult, type StockChip, type StockTone, type StorefrontAddon, type StorefrontCatalog, type StorefrontCategory, type StorefrontData, type StorefrontGroup, type StorefrontListingItem, type StorefrontPaymentMethod, type StorefrontPaymentMethodKind, type StorefrontPriceVariant, type StorefrontProduct, StorefrontProvider, type StorefrontSettings, type StorefrontShop, type StorefrontShopStats, type StorefrontSortKey, type SubscriptionIntervalUnit, type SupportChannel, type VolumeDiscountRule, type YouReceive, accountOrderHref, accountRouteMessage, accountTabHref, accountTabLabelKey, averageRating, badgesForCard, badgesForProductPage, buildCartTotals, buildCartView, buildCustomFieldPayload, buildInitialCheckoutFieldValues, buildProductDetail, buildStatusPageModel, buildStorefrontCatalog, calculateDisplayLineTotal, calculateLinePricing, canRequestStockAlert, canonicalAccountPath, checkoutFieldKey, checkoutFieldWriteRepriced, checkoutNeedsDetails, clampQuantity, clearRecoveredQuoteCheckoutError, createFavoritesStore, filterCatalogCards, findStorefrontProduct, firstOptionalString, flattenMerchantHtmlToText, formatCompactCount, formatDeliveryTime, formatLicenseTerm, formatMoney, formatSubscriptionCadence, formatSubscriptionTrial, getAuthoredContent, getBuyerCurrency, getProductGroups, getProducts, getShoppex, getStorefrontAddons, getStorefrontCategories, getStorefrontListing, getStorefrontSettings, getStorefrontShop, hasInjectedStorefrontData, isAccountTab, isBlankMerchantHtml, isCardSoldOut, isCheckboxValueChecked, isImplementedAccountTab, isProductSoldOut, isRecord, isSelectionSoldOut, loadLatestWrittenReviews, loadShopPage, loadShopReviewsPage, missingRequiredCheckoutValues, notifyFavoritesChanged, optionalBoolean, optionalString, optionalStringWithDefault, paginateCatalogCards, parseAccountPath, parseCartQuoteLines, parseEnum, parseFiniteNumber, parseProductBadges, parseServiceStatus, parseStorefrontSortKey, readCheckoutNeedsDetails, readStorefrontData, requestStockAlert, requireArray, requireBoolean, requireNumber, requireRecord, requireString, requireStringFromKeys, requireUnknownRecord, resolveAccountView, resolveBadgeForeground, resolveCartBoundsHeals, resolveCategorySelection, resolveCheckoutFields, resolveDeliveryPromise, resolveDisplayStock, resolveHideStockCounter, resolveLicenseTerm, resolveMenuLinksForSlot, resolvePayWhatYouWantMinimum, resolveProductVideoEmbedSrc, resolveQuantityBounds, resolveSupportChannels, resolveUnsellableLineIds, resolveVariantStock, resolveYouReceive, reviewsForProduct, roundCurrency, sanitizeMerchantHtml, sdkFavoritesTransport, sortCatalogCards, startCartMutation, stripMerchantHtmlImages, submitContactTicket, toShopReviews, toStarCount, useCart, useFavorites, useNavigationLinks, useOnlineUserCount, usePagedShopReviews, useShopReviews, useStorefront, validateCheckoutFieldValue, validateCheckoutFields, validateContactSubmission, withRequiredAddons };