@shoppexio/storefront 1.0.72 → 1.0.74

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/dist/index.js CHANGED
@@ -22,95 +22,10 @@ import {
22
22
  union,
23
23
  unknown,
24
24
  url
25
- } from "./chunk-UHEFHGZ3.js";
26
-
27
- // ../sdk/src/core/cache.ts
28
- var cache = /* @__PURE__ */ new Map();
29
- var pending = /* @__PURE__ */ new Map();
30
- var stats = {
31
- hits: 0,
32
- misses: 0
33
- };
34
- function isExpired(entry) {
35
- return Date.now() > entry.expiresAt;
36
- }
37
- function getCacheStats() {
38
- return {
39
- hits: stats.hits,
40
- misses: stats.misses,
41
- pendingRequests: pending.size,
42
- entries: cache.size
43
- };
44
- }
45
- function clearCache() {
46
- cache.clear();
47
- pending.clear();
48
- }
49
- function invalidateCache(prefixOrKey) {
50
- for (const key of cache.keys()) {
51
- if (key === prefixOrKey || key.startsWith(prefixOrKey)) {
52
- cache.delete(key);
53
- }
54
- }
55
- }
56
- function setCacheEntry(key, data, ttl) {
57
- const now = Date.now();
58
- cache.set(key, {
59
- data,
60
- ttl,
61
- updatedAt: now,
62
- expiresAt: now + ttl
63
- });
64
- }
65
- function getCacheEntry(key) {
66
- const entry = cache.get(key);
67
- if (!entry) return null;
68
- return entry;
69
- }
70
- async function getOrFetch(key, fetcher, options, shouldCache = () => true) {
71
- const entry = getCacheEntry(key);
72
- if (entry && !isExpired(entry)) {
73
- stats.hits += 1;
74
- return entry.data;
75
- }
76
- if (entry && options.staleWhileRevalidate) {
77
- stats.hits += 1;
78
- if (!pending.has(key)) {
79
- const refreshPromise = (async () => {
80
- try {
81
- const data = await fetcher();
82
- if (shouldCache(data)) {
83
- setCacheEntry(key, data, options.ttl);
84
- }
85
- return data;
86
- } finally {
87
- pending.delete(key);
88
- }
89
- })();
90
- pending.set(key, refreshPromise);
91
- }
92
- return entry.data;
93
- }
94
- if (pending.has(key)) {
95
- return pending.get(key);
96
- }
97
- stats.misses += 1;
98
- const promise = (async () => {
99
- try {
100
- const data = await fetcher();
101
- if (shouldCache(data)) {
102
- setCacheEntry(key, data, options.ttl);
103
- }
104
- return data;
105
- } finally {
106
- pending.delete(key);
107
- }
108
- })();
109
- pending.set(key, promise);
110
- return promise;
111
- }
25
+ } from "./chunk-ZVOFFURI.js";
112
26
 
113
27
  // ../sdk/src/core/errors.ts
28
+ var CURRENCY_UNAVAILABLE_ERROR_CODE = "errors.storefront.currency_unavailable";
114
29
  var ShoppexError = class _ShoppexError extends Error {
115
30
  constructor(message, code, statusCode) {
116
31
  super(message);
@@ -161,7 +76,7 @@ var CartError = class _CartError extends ShoppexError {
161
76
  }
162
77
  };
163
78
 
164
- // ../../../../../node_modules/.bun/openapi-fetch@0.17.0/node_modules/openapi-fetch/dist/index.mjs
79
+ // ../../node_modules/.bun/openapi-fetch@0.17.0/node_modules/openapi-fetch/dist/index.mjs
165
80
  var PATH_PARAM_RE = /\{[^{}]+\}/g;
166
81
  var supportsRequestInitExt = () => {
167
82
  return typeof process === "object" && Number.parseInt(process?.versions?.node?.substring(0, 2)) >= 18 && process.versions.undici;
@@ -1031,7 +946,7 @@ var PaymentGatewayStateSchema = object({
1031
946
 
1032
947
  // ../contracts/src/style-center.ts
1033
948
  var CHECKOUT_PLATFORM_BRAND_COLOR = "#7c3aed";
1034
- var CHECKOUT_PLATFORM_FONT_FAMILY = "Geist";
949
+ var CHECKOUT_PLATFORM_FONT_FAMILY = "Inter";
1035
950
  var checkoutStyleSurfaceValues = ["checkout", "payment_link", "embed"];
1036
951
  var checkoutStyleDensityValues = ["comfortable", "compact"];
1037
952
  var checkoutStyleModeValues = ["light", "dark", "system"];
@@ -1078,7 +993,7 @@ var checkoutStyleTokenDefinitions = [
1078
993
  // `var(--spx-checkout-border, …)` chains loose. The hosted checkout draws
1079
994
  // almost no hairlines any more — the slots that still do read this, the rest
1080
995
  // resolve their border to `transparent` in the system CSS.
1081
- { key: "color.border", cssVar: "--spx-checkout-border", group: "color", type: "color", default: "#2c2c2c" },
996
+ { key: "color.border", cssVar: "--spx-checkout-border", group: "color", type: "color", default: "#3a3a3a" },
1082
997
  { key: "color.focus", cssVar: "--spx-checkout-focus", group: "color", type: "color", default: CHECKOUT_PLATFORM_BRAND_COLOR, protected: true },
1083
998
  { key: "color.success", cssVar: "--spx-checkout-success", group: "color", type: "color", default: "#22c55e", protected: true },
1084
999
  { key: "color.warning", cssVar: "--spx-checkout-warning", group: "color", type: "color", default: "#f59e0b", protected: true },
@@ -1090,16 +1005,22 @@ var checkoutStyleTokenDefinitions = [
1090
1005
  // actually loaded.
1091
1006
  { key: "typography.googleFontFamily", cssVar: "--spx-checkout-google-font", group: "typography", type: "font", default: "" },
1092
1007
  { key: "typography.baseSize", cssVar: "--spx-checkout-font-size", group: "typography", type: "number", default: 14, min: 12, max: 18, step: 1, unit: "px" },
1093
- // 12px, not 8, for the same reason as the input below: the hosted pay CTA is
1094
- // `rounded-xl` and this token is materialised over every slotted button. At 8
1095
- // the store-credit and manual actions rendered one step tighter than the
1096
- // field directly above them and than the CTA they stand in for.
1097
- { key: "shape.buttonRadius", cssVar: "--spx-checkout-button-radius", group: "shape", type: "number", default: 10, min: 0, max: 24, step: 1, unit: "px" },
1098
- // 12px, not 8: the hosted `Input` primitive is `rounded-lg`, and this token is
1099
- // materialised over it. At 8 the system CSS rounded every field one step
1100
- // tighter than the component that drew it.
1101
- { key: "shape.inputRadius", cssVar: "--spx-checkout-input-radius", group: "shape", type: "number", default: 10, min: 0, max: 24, step: 1, unit: "px" },
1102
- { key: "shape.cardRadius", cssVar: "--spx-checkout-card-radius", group: "shape", type: "number", default: 10, min: 0, max: 28, step: 1, unit: "px" },
1008
+ // ALL THREE ARE 8, and they have to be this is one edge, stated in three
1009
+ // places, and the whole reason it is worth a comment is that the three places
1010
+ // are read by three different renderers:
1011
+ // 1. these defaults, materialised into the live checkout root (the `shape`
1012
+ // group is a foundation group, so an untouched shop still gets them);
1013
+ // 2. `--radius-sm` / `--radius-lg` in apps/checkout/app/globals.css, which
1014
+ // every component class paints from, both 0.5rem;
1015
+ // 3. the Stripe Appearance object (`buildStripeAppearanceFromCheckoutStyle`),
1016
+ // which cannot resolve `var()` and is handed these same values as plain
1017
+ // strings for `borderRadius` and the `.Block` / `.AccordionItem` rules.
1018
+ // A disagreement between any two of them is visible as a card inside the
1019
+ // provider iframe rounded differently from the card around it. Pinned by
1020
+ // `__tests__/checkout-radius-chain.test.ts` in apps/checkout.
1021
+ { key: "shape.buttonRadius", cssVar: "--spx-checkout-button-radius", group: "shape", type: "number", default: 8, min: 0, max: 24, step: 1, unit: "px" },
1022
+ { key: "shape.inputRadius", cssVar: "--spx-checkout-input-radius", group: "shape", type: "number", default: 8, min: 0, max: 24, step: 1, unit: "px" },
1023
+ { key: "shape.cardRadius", cssVar: "--spx-checkout-card-radius", group: "shape", type: "number", default: 8, min: 0, max: 28, step: 1, unit: "px" },
1103
1024
  {
1104
1025
  key: "spacing.density",
1105
1026
  cssVar: "--spx-checkout-density",
@@ -1112,7 +1033,7 @@ var checkoutStyleTokenDefinitions = [
1112
1033
  { key: "component.primaryButton.background", cssVar: "--spx-checkout-button-bg", group: "component", type: "color", default: "", protected: true },
1113
1034
  { key: "component.primaryButton.text", cssVar: "--spx-checkout-button-text", group: "component", type: "color", default: "#ffffff", protected: true },
1114
1035
  { key: "component.input.background", cssVar: "--spx-checkout-input-bg", group: "component", type: "color", default: "#292929" },
1115
- { key: "component.input.border", cssVar: "--spx-checkout-input-border", group: "component", type: "color", default: "#2c2c2c" },
1036
+ { key: "component.input.border", cssVar: "--spx-checkout-input-border", group: "component", type: "color", default: "#3a3a3a" },
1116
1037
  { key: "component.input.focusRing", cssVar: "--spx-checkout-input-focus-ring", group: "component", type: "color", default: "" },
1117
1038
  // Default '' is filtered out by the preview-iframe normaliser, so the
1118
1039
  // CSS fallback chain on [data-spx-slot="summary.panel"] resolves to
@@ -1126,9 +1047,28 @@ var checkoutStyleTokenDefinitions = [
1126
1047
  { key: "component.paymentMethod.selectedBackground", cssVar: "--spx-checkout-payment-method-selected-bg", group: "component", type: "color", default: "" },
1127
1048
  { key: "component.checkoutHeader.background", cssVar: "--spx-checkout-embed-header-bg", group: "component", type: "color", default: "" },
1128
1049
  { key: "component.checkoutHeader.text", cssVar: "--spx-checkout-embed-header-text", group: "component", type: "color", default: "" },
1129
- { key: "component.productCard.background", cssVar: "--spx-checkout-product-card-bg", group: "component", type: "color", default: "rgba(255,255,255,0.03)" },
1130
- { key: "component.productCard.border", cssVar: "--spx-checkout-product-card-border", group: "component", type: "color", default: "rgba(255,255,255,0.06)" },
1131
- { key: "component.productCard.shadow", cssVar: "--spx-checkout-product-card-shadow", group: "component", type: "shadow", default: "none" },
1050
+ // THE PRODUCT CARD IS GONE FROM THE HOSTED CHECKOUT, and these three tokens
1051
+ // are what is left of it.
1052
+ //
1053
+ // The 2026 rework replaced the boxed summary line item with a FLAT ROW
1054
+ // (`data-spx-slot="product.line"`, apps/checkout/components/checkout/
1055
+ // product-list.tsx). A flat row has no fill of its own, no edge and no cast —
1056
+ // that is the design, not an omission — so there is nothing on the hosted or
1057
+ // payment-link surface for a border or a shadow token to land on. They are
1058
+ // NOT re-pointed at `product.line`: handing a merchant a border control for a
1059
+ // row that must not have a border is a control that can only make the page
1060
+ // worse, and the slot layer would paint it with `!important` where no call
1061
+ // site could undo it.
1062
+ //
1063
+ // `background` is the exception and stays LIVE: the EMBED surface reads
1064
+ // `--spx-checkout-product-card-bg` in `applyCheckoutStyleToEmbedStyles`
1065
+ // (apps/checkout/lib/checkout-style.tsx), where it is the card fill AND the
1066
+ // input to the embed's light/dark inference. It is marked `surface: 'embed'`
1067
+ // so the Style Center offers it where it still does something, and it keeps
1068
+ // being emitted for every stored theme.
1069
+ { key: "component.productCard.background", cssVar: "--spx-checkout-product-card-bg", group: "component", type: "color", default: "rgba(255,255,255,0.03)", surface: "embed" },
1070
+ { key: "component.productCard.border", cssVar: "--spx-checkout-product-card-border", group: "component", type: "color", default: "rgba(255,255,255,0.06)", deprecated: true },
1071
+ { key: "component.productCard.shadow", cssVar: "--spx-checkout-product-card-shadow", group: "component", type: "shadow", default: "none", deprecated: true },
1132
1072
  { key: "component.productImage.background", cssVar: "--spx-checkout-product-image-bg", group: "component", type: "color", default: "rgba(255,255,255,0.06)" },
1133
1073
  { key: "component.productImage.border", cssVar: "--spx-checkout-product-image-border", group: "component", type: "color", default: "rgba(255,255,255,0.04)" },
1134
1074
  { key: "component.productImage.icon", cssVar: "--spx-checkout-product-image-icon", group: "component", type: "color", default: "rgba(250,250,250,0.4)" },
@@ -1208,28 +1148,91 @@ var checkoutStyleControlTokenDefinitions = [
1208
1148
  ];
1209
1149
  var checkoutStyleSlotValues = [
1210
1150
  "checkout.shell",
1151
+ "checkout.chrome",
1211
1152
  "checkout.panel",
1212
1153
  "checkout.header",
1154
+ "checkout.footer",
1155
+ "checkout.trust",
1156
+ "checkout.section.active",
1157
+ "checkout.section.collapsed",
1158
+ "checkout.mobile_pay_bar",
1159
+ // The spacer under the bar, carrying the working column's ground so the last
1160
+ // scroll does not step down onto the shell's.
1161
+ "checkout.mobile_pay_bar_floor",
1213
1162
  "brand.logo",
1163
+ "brand.hero",
1164
+ // RETIRED, AND DELIBERATELY STILL HERE (see the note above). No element
1165
+ // carries `product.card` any more (see `component.productCard.*` above), so a
1166
+ // rule aimed at it is inert, which is the harmless half of the trade.
1214
1167
  "product.card",
1168
+ // What replaced it: the flat summary row. A merchant can reach it for type
1169
+ // and spacing; it has no card material to override, by design.
1170
+ "product.line",
1215
1171
  "product.image",
1216
1172
  "product.title",
1217
1173
  "product.description",
1218
1174
  "product.price",
1219
1175
  "product.quantity",
1176
+ "product.quantity_menu",
1177
+ "product.remove",
1220
1178
  "product.addon",
1179
+ // The "Included" marker a SELECTED add-on carries in place of its "+$9.00".
1180
+ // Its own slot rather than `product.price`, because it is not a figure: the
1181
+ // amount is already inside the line total above it, and a merchant styling
1182
+ // their price column must not accidentally style a word.
1183
+ "product.addon_included",
1184
+ // The line of glyph-led metadata under the product's title in the embed's
1185
+ // order bar — the description trigger plus whichever of the facts below the
1186
+ // product has. Its own slot so a merchant can retune the whole row's rhythm
1187
+ // (or hide it) without reaching into each fact one at a time.
1188
+ "product.facts",
1189
+ // How many are left. It appears in TWO places and deliberately shares one
1190
+ // slot: on the facts row when the product has no variants, and on each option
1191
+ // tile when it does — because with options there is no single stock figure,
1192
+ // only one per option. A merchant styling scarcity means the same thing in
1193
+ // both, and a shop is unlikely to have configured only the one it can see.
1194
+ "product.stock",
1195
+ "product.delivery",
1196
+ // The billing period a recurring line states — "/month" beside the figure,
1197
+ // or the full phrase on the metadata line when the period is not one-per-unit.
1198
+ "product.recurrence",
1199
+ "product.warranty",
1221
1200
  "paymentLink.hero",
1222
1201
  "summary.panel",
1223
1202
  "summary.line",
1224
1203
  "summary.total",
1204
+ // The row that CLOSES the ledger, distinct from `summary.total` (the column's
1205
+ // headline figure) and from `summary.line` (one part of the sum): it is the
1206
+ // sum itself, and it is the only row in the table that draws a rule.
1207
+ "summary.total_line",
1208
+ // What the total does next month, stated directly under it.
1209
+ "summary.recurrence",
1210
+ "summary.total_note",
1211
+ "summary.order_number",
1212
+ "summary.tracking",
1213
+ "summary.tip",
1214
+ "summary.after_payment",
1215
+ // The mobile order summary: a persistent bar that expands into the panel.
1216
+ "summary.mobile_bar",
1217
+ "summary.disclosure",
1225
1218
  "form.field",
1226
1219
  "form.label",
1227
1220
  "form.help",
1221
+ "form.value",
1222
+ "form.error",
1223
+ "form.panel",
1224
+ "form.save",
1225
+ "form.saved",
1228
1226
  "coupon.input",
1227
+ "coupon.applied",
1229
1228
  "input.base",
1230
1229
  "input.error",
1230
+ // The reserved line under an input that holds its message. It is present
1231
+ // whether or not there is something to say, which is the point: no reflow.
1232
+ "input.message_slot",
1231
1233
  "button.primary",
1232
1234
  "button.secondary",
1235
+ "button.blocked",
1233
1236
  "payment.methods",
1234
1237
  "payment.method",
1235
1238
  "payment.method.icon",
@@ -1237,13 +1240,35 @@ var checkoutStyleSlotValues = [
1237
1240
  "payment.method.meta",
1238
1241
  "payment.method.fee",
1239
1242
  "payment.method.indicator",
1243
+ "payment.method.trailing",
1244
+ "payment.method.change",
1245
+ "payment.selected_method",
1240
1246
  "payment.provider_widget",
1247
+ "payment.express_checkout",
1248
+ "payment.paypal_shell",
1249
+ "payment.acknowledgement",
1250
+ "payment.free_completion",
1251
+ "payment.notice",
1252
+ "payment.trial_notice",
1253
+ "payment.blocker_hint",
1241
1254
  "payment.loading",
1242
1255
  "payment.error",
1243
1256
  "payment.warning",
1244
1257
  "legal.terms",
1245
1258
  "status.success",
1246
1259
  "status.processing",
1260
+ "status.details",
1261
+ "status.wait_hint",
1262
+ // The open order kept its gateway: the buyer is told which one, and why the
1263
+ // picker is gone.
1264
+ "status.method_locked",
1265
+ "status.payment_rescue",
1266
+ "status.recovery",
1267
+ "status.delivery_destination",
1268
+ "embed.overlay_root",
1269
+ // The rest of the embed group belongs to the standalone widget bundle, which
1270
+ // no longer carries these names on an element. Kept for the same reason
1271
+ // `product.card` is kept: a stored theme that targets one must still save.
1247
1272
  "embed.launcher",
1248
1273
  "embed.launcher.icon",
1249
1274
  "embed.productCard",
@@ -1266,6 +1291,18 @@ var checkoutStyleProtectedSlotValues = [
1266
1291
  "summary.total",
1267
1292
  "payment.method.label",
1268
1293
  "payment.provider_widget",
1294
+ // The pay control on mobile. It is the only one in the viewport once the
1295
+ // page scrolls, so hiding it hides checkout.
1296
+ "checkout.mobile_pay_bar",
1297
+ // Whole payment routes: express wallets, the PayPal button host, and the
1298
+ // completion control a zero-total order has instead of a gateway.
1299
+ "payment.express_checkout",
1300
+ "payment.paypal_shell",
1301
+ "payment.free_completion",
1302
+ // Disclosures. The acknowledgement is the buyer's consent to an offline
1303
+ // payment; the trial notice is what they will be charged and when.
1304
+ "payment.acknowledgement",
1305
+ "payment.trial_notice",
1269
1306
  "payment.loading",
1270
1307
  "payment.error",
1271
1308
  "payment.warning",
@@ -2000,6 +2037,7 @@ var embedPaymentSessionWireSchema = external_exports.object({
2000
2037
  confirmations_needed: external_exports.number().optional()
2001
2038
  }).passthrough();
2002
2039
  var publicInvoiceWireCustomFieldDefinitionSchema = external_exports.object({
2040
+ collect_after_payment: external_exports.boolean().optional(),
2003
2041
  default_value: external_exports.string().optional(),
2004
2042
  min_length: external_exports.number().optional(),
2005
2043
  name: external_exports.string(),
@@ -2008,6 +2046,17 @@ var publicInvoiceWireCustomFieldDefinitionSchema = external_exports.object({
2008
2046
  required: external_exports.boolean(),
2009
2047
  type: external_exports.string()
2010
2048
  });
2049
+ var pendingPostPaymentCustomFieldsSchema = external_exports.object({
2050
+ version: external_exports.literal(1),
2051
+ line_items: external_exports.array(external_exports.object({
2052
+ line_item_id: external_exports.string().uuid(),
2053
+ product_id: external_exports.string().uuid(),
2054
+ product_title: external_exports.string(),
2055
+ fields: external_exports.array(publicInvoiceWireCustomFieldDefinitionSchema.extend({
2056
+ collect_after_payment: external_exports.literal(true)
2057
+ }))
2058
+ }))
2059
+ });
2011
2060
  var publicInvoiceWireProductAddonSchema = external_exports.object({
2012
2061
  id: external_exports.string(),
2013
2062
  price: external_exports.number(),
@@ -2044,6 +2093,7 @@ var publicInvoiceWireProductSchema = external_exports.object({
2044
2093
  currency: external_exports.string(),
2045
2094
  custom_fields: unknownRecordSchema.nullable().optional(),
2046
2095
  custom_fields_config: external_exports.array(publicInvoiceWireCustomFieldDefinitionSchema).nullable().optional(),
2096
+ post_payment_custom_fields_config: external_exports.array(publicInvoiceWireCustomFieldDefinitionSchema).nullable().optional(),
2047
2097
  delivery_instruction: external_exports.unknown().optional(),
2048
2098
  delivery_instruction_config: external_exports.unknown().optional(),
2049
2099
  delivery_instruction_label: external_exports.unknown().optional(),
@@ -2352,6 +2402,20 @@ var publicInvoiceWireSchema = external_exports.object({
2352
2402
  buyer_identity: buyerIdentitySchema.nullable().optional(),
2353
2403
  checkout_tipping: checkoutTippingSchema.nullable().optional(),
2354
2404
  country_regulations: external_exports.string().nullable().optional(),
2405
+ /**
2406
+ * THAT a coupon is on the order — never WHICH one.
2407
+ *
2408
+ * The redeemed code has no key on this contract, and none is coming back to
2409
+ * it: this is the wire of the anonymous invoice read, whose only credential
2410
+ * is knowledge of the invoice URL. Coupon codes are merchant-authored and
2411
+ * routinely private (campaign, influencer, single-buyer codes), so naming one
2412
+ * to an unproven reader discloses the merchant's code. The producer withholds
2413
+ * it too — `coupon_code`, `coupon_id` and the raw `discount_breakdown` are all
2414
+ * outside `PUBLIC_INVOICE_FIELDS` — so the summary prints "Coupon −$9.80".
2415
+ *
2416
+ * A buyer-bound lane may name the coupon, but only from a source that proves
2417
+ * the reader: this schema is not it.
2418
+ */
2355
2419
  coupon_applied: external_exports.boolean().optional(),
2356
2420
  crypto_mode: external_exports.string().nullable().optional(),
2357
2421
  crypto_transactions: external_exports.array(cryptoTransactionSchema).optional(),
@@ -2380,6 +2444,7 @@ var publicInvoiceWireSchema = external_exports.object({
2380
2444
  paddle_transaction_id: external_exports.unknown().optional(),
2381
2445
  payment_link_id: external_exports.string().nullable().optional(),
2382
2446
  payment_method_overrides: external_exports.array(paymentMethodOverrideSchema).optional(),
2447
+ pending_custom_fields: pendingPostPaymentCustomFieldsSchema.nullable().optional(),
2383
2448
  payment_session_state: invoicePaymentSessionStateSchema.nullable().optional(),
2384
2449
  polling: pollingSchema,
2385
2450
  pricing_breakdown: pricingBreakdownSchema.nullable().optional(),
@@ -2483,7 +2548,18 @@ var publicInvoiceWireSchema = external_exports.object({
2483
2548
  telegram_stars_payment_note: external_exports.string().nullable().optional(),
2484
2549
  updated_at: external_exports.string().optional(),
2485
2550
  virtual_payments_id: external_exports.string().nullable().optional(),
2486
- void_details: external_exports.unknown().optional()
2551
+ void_details: external_exports.unknown().optional(),
2552
+ // EU right-of-withdrawal consent (§ 356 Abs. 5 BGB / CRD Art. 16(m)).
2553
+ // `withdrawal_consent_required` is the server's verdict for THIS invoice
2554
+ // (shop opt-in + buyer country + digital deliverables, renewals exempt) and is
2555
+ // REQUIRED on the wire: a missing verdict is a producer bug, not a licence for
2556
+ // the checkout to assume `false` and show a pay button the backend refuses.
2557
+ // The other two are the stored proof — always present, null until the buyer
2558
+ // confirms. The checkout gates on these three and never infers the
2559
+ // requirement itself.
2560
+ withdrawal_consent_at: external_exports.string().nullable(),
2561
+ withdrawal_consent_required: external_exports.boolean(),
2562
+ withdrawal_consent_text_version: external_exports.string().nullable()
2487
2563
  }).catchall(external_exports.unknown());
2488
2564
  var embedCheckoutProductWireSchema = publicInvoiceWireProductSchema.extend({
2489
2565
  available_addons: external_exports.array(publicInvoiceWireAvailableAddonSchema).optional(),
@@ -2508,6 +2584,15 @@ var embedCheckoutInvoiceWireSchema = publicInvoiceWireSchema.extend({
2508
2584
  customer_email: external_exports.string().nullable(),
2509
2585
  checkout_style: checkoutStyleStateSchema.nullable().optional(),
2510
2586
  coupon_id: external_exports.string().nullable().optional(),
2587
+ // Merchant toggle (shop_commerce_settings.coupon_field_always_visible):
2588
+ // render the coupon input expanded instead of behind the "Add coupon code"
2589
+ // trigger. Presentation only — coupon validation is unchanged.
2590
+ //
2591
+ // Required, not optional: the public producer (`InvoiceEnricher`) always
2592
+ // resolves this to a boolean, so an absent field means the producer broke,
2593
+ // not that the merchant left it unset. Failing here beats the checkout
2594
+ // quietly rendering the collapsed default for a shop that turned it on.
2595
+ coupon_field_always_visible: external_exports.boolean(),
2511
2596
  discount_breakdown: unknownRecordSchema.nullable().optional(),
2512
2597
  affiliate_code: external_exports.string().nullable().optional(),
2513
2598
  country: external_exports.string().nullable().optional(),
@@ -2589,28 +2674,67 @@ function resetTypedClient() {
2589
2674
  cachedBaseUrl = null;
2590
2675
  }
2591
2676
 
2677
+ // ../sdk/src/utils/requested-currency.ts
2678
+ function normalizeRequestedCurrency(value) {
2679
+ const normalized = value?.trim().toUpperCase();
2680
+ return normalized && /^[A-Z]{3}$/.test(normalized) ? normalized : null;
2681
+ }
2682
+ function resolveBuyerCurrency(configCurrency) {
2683
+ return getRequestedCurrencyFromLocation() ?? normalizeRequestedCurrency(configCurrency);
2684
+ }
2685
+ function getRequestedCurrencyFromLocation() {
2686
+ if (typeof window === "undefined" || !window.location) {
2687
+ return null;
2688
+ }
2689
+ const search = typeof window.location.search === "string" ? window.location.search : "";
2690
+ if (search) {
2691
+ return normalizeRequestedCurrency(new URLSearchParams(search).get("currency"));
2692
+ }
2693
+ const href = typeof window.location.href === "string" ? window.location.href : "";
2694
+ if (!href) {
2695
+ return null;
2696
+ }
2697
+ try {
2698
+ return normalizeRequestedCurrency(
2699
+ new URL(href, "https://storefront.shoppex.local").searchParams.get("currency")
2700
+ );
2701
+ } catch {
2702
+ return null;
2703
+ }
2704
+ }
2705
+
2592
2706
  // ../sdk/src/core/config.ts
2593
2707
  var DEFAULT_API_BASE_URL = "https://api.shoppex.io";
2594
2708
  var currentConfig = null;
2595
2709
  var cachedShopId = null;
2596
2710
  var DEFAULT_CHECKOUT_BASE_URL = "https://checkout.shoppex.io";
2597
2711
  function initConfig(storeSlug, options) {
2712
+ const currency = normalizeInitCurrency(options?.currency);
2598
2713
  const normalizedShopId = options?.shopId?.trim();
2599
2714
  cachedShopId = normalizedShopId ? normalizedShopId : null;
2600
- const previousLocale = currentConfig?.locale;
2601
2715
  currentConfig = {
2602
2716
  storeSlug,
2603
2717
  locale: options?.locale,
2604
- currency: options?.currency,
2718
+ currency,
2605
2719
  apiBaseUrl: options?.apiBaseUrl ?? DEFAULT_API_BASE_URL,
2606
2720
  checkoutBaseUrl: options?.checkoutBaseUrl ?? DEFAULT_CHECKOUT_BASE_URL
2607
2721
  };
2608
- if (previousLocale !== currentConfig.locale) {
2609
- clearCache();
2610
- }
2611
2722
  resetTypedClient();
2612
2723
  return currentConfig;
2613
2724
  }
2725
+ function normalizeInitCurrency(value) {
2726
+ if (value === void 0 || value.trim() === "") {
2727
+ return void 0;
2728
+ }
2729
+ const normalized = normalizeRequestedCurrency(value);
2730
+ if (!normalized) {
2731
+ throw new ValidationError(
2732
+ `Invalid currency "${value}": pass an ISO 4217 code such as "EUR".`,
2733
+ ["currency"]
2734
+ );
2735
+ }
2736
+ return normalized;
2737
+ }
2614
2738
  function getConfig() {
2615
2739
  if (!currentConfig) {
2616
2740
  throw new NotInitializedError();
@@ -3022,6 +3146,94 @@ function resolveStorefrontSocialLinks(store) {
3022
3146
  };
3023
3147
  }
3024
3148
 
3149
+ // ../sdk/src/core/cache.ts
3150
+ var cache = /* @__PURE__ */ new Map();
3151
+ var pending = /* @__PURE__ */ new Map();
3152
+ var stats = {
3153
+ hits: 0,
3154
+ misses: 0
3155
+ };
3156
+ function isExpired(entry) {
3157
+ return Date.now() > entry.expiresAt;
3158
+ }
3159
+ function getCacheStats() {
3160
+ return {
3161
+ hits: stats.hits,
3162
+ misses: stats.misses,
3163
+ pendingRequests: pending.size,
3164
+ entries: cache.size
3165
+ };
3166
+ }
3167
+ function clearCache() {
3168
+ cache.clear();
3169
+ pending.clear();
3170
+ }
3171
+ var AUDIENCE_PREFIX = /^[^|]*\|[^|]*\|/;
3172
+ function invalidateCache(prefixOrKey) {
3173
+ for (const key of cache.keys()) {
3174
+ const bareKey = key.replace(AUDIENCE_PREFIX, "");
3175
+ if (key === prefixOrKey || key.startsWith(prefixOrKey) || bareKey === prefixOrKey || bareKey.startsWith(prefixOrKey)) {
3176
+ cache.delete(key);
3177
+ }
3178
+ }
3179
+ }
3180
+ function setCacheEntry(key, data, ttl) {
3181
+ const now = Date.now();
3182
+ cache.set(key, {
3183
+ data,
3184
+ ttl,
3185
+ updatedAt: now,
3186
+ expiresAt: now + ttl
3187
+ });
3188
+ }
3189
+ function getCacheEntry(key) {
3190
+ const entry = cache.get(key);
3191
+ if (!entry) return null;
3192
+ return entry;
3193
+ }
3194
+ async function getOrFetch(key, fetcher, options, shouldCache = () => true) {
3195
+ const entry = getCacheEntry(key);
3196
+ if (entry && !isExpired(entry)) {
3197
+ stats.hits += 1;
3198
+ return entry.data;
3199
+ }
3200
+ if (entry && options.staleWhileRevalidate) {
3201
+ stats.hits += 1;
3202
+ if (!pending.has(key)) {
3203
+ const refreshPromise = (async () => {
3204
+ try {
3205
+ const data = await fetcher();
3206
+ if (shouldCache(data)) {
3207
+ setCacheEntry(key, data, options.ttl);
3208
+ }
3209
+ return data;
3210
+ } finally {
3211
+ pending.delete(key);
3212
+ }
3213
+ })();
3214
+ pending.set(key, refreshPromise);
3215
+ }
3216
+ return entry.data;
3217
+ }
3218
+ if (pending.has(key)) {
3219
+ return pending.get(key);
3220
+ }
3221
+ stats.misses += 1;
3222
+ const promise = (async () => {
3223
+ try {
3224
+ const data = await fetcher();
3225
+ if (shouldCache(data)) {
3226
+ setCacheEntry(key, data, options.ttl);
3227
+ }
3228
+ return data;
3229
+ } finally {
3230
+ pending.delete(key);
3231
+ }
3232
+ })();
3233
+ pending.set(key, promise);
3234
+ return promise;
3235
+ }
3236
+
3025
3237
  // ../sdk/src/core/endpoint.ts
3026
3238
  var PARAM_PATTERN = /:([A-Za-z0-9_]+)/g;
3027
3239
  function buildEndpoint(template, params) {
@@ -3155,9 +3367,13 @@ var MAX_RETRIES = 2;
3155
3367
  async function sleep(ms) {
3156
3368
  return new Promise((resolve) => setTimeout(resolve, ms));
3157
3369
  }
3370
+ function appendQueryParam(url2, key, value) {
3371
+ const separator = url2.includes("?") ? "&" : "?";
3372
+ return `${url2}${separator}${encodeURIComponent(key)}=${encodeURIComponent(value)}`;
3373
+ }
3158
3374
  async function request(endpoint, options = {}) {
3159
3375
  const config = options.baseUrl ? null : getConfig();
3160
- const localeConfig = config ?? (isInitialized() ? getConfig() : null);
3376
+ const audience = config ?? (isInitialized() ? getConfig() : null);
3161
3377
  const {
3162
3378
  method = "GET",
3163
3379
  body,
@@ -3165,18 +3381,21 @@ async function request(endpoint, options = {}) {
3165
3381
  retries,
3166
3382
  baseUrl,
3167
3383
  headers: requestHeaders,
3168
- cache: cache2
3384
+ cache: cache2,
3385
+ buyerCurrency: pricesInBuyerCurrency = false
3169
3386
  } = options;
3170
3387
  const retryCount = retries ?? (method === "GET" ? MAX_RETRIES : 0);
3171
3388
  const apiBaseUrl = baseUrl ?? config?.apiBaseUrl ?? "";
3172
- const url2 = `${apiBaseUrl}${endpoint}`;
3389
+ const buyerCurrency = pricesInBuyerCurrency ? resolveBuyerCurrency(audience?.currency) : null;
3390
+ const url2 = buyerCurrency ? appendQueryParam(`${apiBaseUrl}${endpoint}`, "currency", buyerCurrency) : `${apiBaseUrl}${endpoint}`;
3173
3391
  const headers = {
3174
3392
  "Content-Type": "application/json",
3175
3393
  Accept: "application/json",
3176
3394
  ...requestHeaders
3177
3395
  };
3178
- if (typeof localeConfig?.locale === "string" && localeConfig.locale.trim()) {
3179
- headers["x-shoppex-locale"] = localeConfig.locale.trim();
3396
+ const locale = typeof audience?.locale === "string" && audience.locale.trim() ? audience.locale.trim() : null;
3397
+ if (locale) {
3398
+ headers["x-shoppex-locale"] = locale;
3180
3399
  }
3181
3400
  let lastFailure = null;
3182
3401
  const executeRequest = async () => {
@@ -3259,7 +3478,7 @@ async function request(endpoint, options = {}) {
3259
3478
  };
3260
3479
  };
3261
3480
  const result = method === "GET" && cache2 && cache2.ttl > 0 ? await getOrFetch(
3262
- cache2.key ?? `GET:${url2}`,
3481
+ `${locale ?? ""}|${buyerCurrency ?? ""}|${cache2.key ?? `GET:${url2}`}`,
3263
3482
  executeRequest,
3264
3483
  { ttl: cache2.ttl, staleWhileRevalidate: cache2.staleWhileRevalidate },
3265
3484
  (value) => value.success
@@ -3369,6 +3588,7 @@ async function getStore() {
3369
3588
  storeSlug: config.storeSlug
3370
3589
  }),
3371
3590
  {
3591
+ buyerCurrency: true,
3372
3592
  cache: {
3373
3593
  key: `store:${config.storeSlug}`,
3374
3594
  ttl: STORE_CACHE_TTL,
@@ -3406,6 +3626,7 @@ async function resolveStoreByDomain(domain, apiBaseUrl) {
3406
3626
  }),
3407
3627
  {
3408
3628
  baseUrl,
3629
+ buyerCurrency: true,
3409
3630
  cache: {
3410
3631
  key: `store:domain:${cleanDomain}`,
3411
3632
  ttl: STORE_CACHE_TTL,
@@ -3442,6 +3663,7 @@ async function getStorefront(options) {
3442
3663
  storeSlug: config.storeSlug
3443
3664
  })}${querySuffix}`,
3444
3665
  {
3666
+ buyerCurrency: true,
3445
3667
  cache: {
3446
3668
  key: `storefront:${config.storeSlug}:${options?.productsLimit ?? "full"}:${options?.productsCursor ?? "start"}`,
3447
3669
  ttl: STORE_CACHE_TTL,
@@ -3539,6 +3761,7 @@ async function getProducts() {
3539
3761
  storeSlug: config.storeSlug
3540
3762
  }),
3541
3763
  {
3764
+ buyerCurrency: true,
3542
3765
  cache: {
3543
3766
  key: `products:${config.storeSlug}`,
3544
3767
  ttl: PRODUCTS_CACHE_TTL,
@@ -3582,6 +3805,7 @@ async function getStorefrontProductsPage(options) {
3582
3805
  storeSlug: config.storeSlug
3583
3806
  })}${querySuffix}`,
3584
3807
  {
3808
+ buyerCurrency: true,
3585
3809
  cache: {
3586
3810
  key: `products:page:${config.storeSlug}:${options?.limit ?? "default"}:${options?.cursor ?? "start"}:${options?.sort ?? "featured"}:${getStorefrontProductsPageCategoryCacheKey(options?.category)}:${options?.hideOutOfStock === true ? "in-stock" : "all-stock"}`,
3587
3811
  ttl: PRODUCTS_CACHE_TTL,
@@ -3613,6 +3837,7 @@ async function getProduct(idOrSlug) {
3613
3837
  const response = await get(
3614
3838
  `${buildEndpoint("/v1/storefront/products/unique/:idOrSlug", { idOrSlug })}${queryParams}`,
3615
3839
  {
3840
+ buyerCurrency: true,
3616
3841
  cache: {
3617
3842
  key: `product:${idOrSlug}:${shopId ?? "no-shop"}`,
3618
3843
  ttl: PRODUCTS_CACHE_TTL,
@@ -3908,32 +4133,6 @@ function ensureCartLineId(item) {
3908
4133
  return { ...item, line_id: computeCartLineId(item) };
3909
4134
  }
3910
4135
 
3911
- // ../sdk/src/utils/requested-currency.ts
3912
- function normalizeRequestedCurrency(value) {
3913
- const normalized = value?.trim().toUpperCase();
3914
- return normalized && /^[A-Z]{3}$/.test(normalized) ? normalized : null;
3915
- }
3916
- function getRequestedCurrencyFromLocation() {
3917
- if (typeof window === "undefined" || !window.location) {
3918
- return null;
3919
- }
3920
- const search = typeof window.location.search === "string" ? window.location.search : "";
3921
- if (search) {
3922
- return normalizeRequestedCurrency(new URLSearchParams(search).get("currency"));
3923
- }
3924
- const href = typeof window.location.href === "string" ? window.location.href : "";
3925
- if (!href) {
3926
- return null;
3927
- }
3928
- try {
3929
- return normalizeRequestedCurrency(
3930
- new URL(href, "https://storefront.shoppex.local").searchParams.get("currency")
3931
- );
3932
- } catch {
3933
- return null;
3934
- }
3935
- }
3936
-
3937
4136
  // ../sdk/src/modules/cart.ts
3938
4137
  var STORAGE_KEYS = {
3939
4138
  cart: "cart",
@@ -4500,7 +4699,7 @@ function clearLatestQuoteToken() {
4500
4699
  async function quoteCart(coupon, currency) {
4501
4700
  const sequence = quoteSequence += 1;
4502
4701
  const payload = getCartPayload(coupon);
4503
- const normalizedCurrency = normalizeRequestedCurrency(currency) ?? getRequestedCurrencyFromLocation() ?? normalizeRequestedCurrency(getConfig().currency);
4702
+ const normalizedCurrency = normalizeRequestedCurrency(currency) ?? resolveBuyerCurrency(getConfig().currency);
4504
4703
  const response = await post("/v1/storefront/cart/quote", {
4505
4704
  shop_slug: payload.store_slug,
4506
4705
  cart: payload.items,
@@ -4587,10 +4786,10 @@ function resolveRequestedCheckoutCurrency(options) {
4587
4786
  if (options.currency !== void 0) {
4588
4787
  return normalizeRequestedCurrency(options.currency);
4589
4788
  }
4590
- return getRequestedCurrencyFromLocation() ?? normalizeRequestedCurrency(getConfig().currency);
4789
+ return resolveBuyerCurrency(getConfig().currency);
4591
4790
  }
4592
4791
  function getRequestedCheckoutCurrency() {
4593
- return getRequestedCurrencyFromLocation() ?? normalizeRequestedCurrency(getConfig().currency);
4792
+ return resolveBuyerCurrency(getConfig().currency);
4594
4793
  }
4595
4794
  function normalizeCheckoutFailureMessage(rawMessage) {
4596
4795
  const message = rawMessage?.trim() ?? "";
@@ -6074,6 +6273,7 @@ export {
6074
6273
  ApiError,
6075
6274
  CATALOG_UNIT_PRICE_DECIMAL_PLACES,
6076
6275
  CATALOG_UNIT_PRICE_FORMAT_OPTIONS,
6276
+ CURRENCY_UNAVAILABLE_ERROR_CODE,
6077
6277
  CartError,
6078
6278
  CheckoutCreateError,
6079
6279
  NetworkError,