@shoppexio/storefront 1.0.71 → 1.0.73
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/README.md +11 -40
- package/dist/index.cjs +353 -351
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +43 -38
- package/dist/index.d.ts +43 -38
- package/dist/index.js +352 -350
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -24,93 +24,8 @@ import {
|
|
|
24
24
|
url
|
|
25
25
|
} from "./chunk-ZVOFFURI.js";
|
|
26
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
|
-
}
|
|
112
|
-
|
|
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);
|
|
@@ -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 = "
|
|
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: "#
|
|
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
|
-
//
|
|
1094
|
-
//
|
|
1095
|
-
//
|
|
1096
|
-
//
|
|
1097
|
-
|
|
1098
|
-
//
|
|
1099
|
-
//
|
|
1100
|
-
//
|
|
1101
|
-
|
|
1102
|
-
|
|
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: "#
|
|
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
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
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,6 +2674,32 @@ 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 getRequestedCurrencyFromLocation() {
|
|
2683
|
+
if (typeof window === "undefined" || !window.location) {
|
|
2684
|
+
return null;
|
|
2685
|
+
}
|
|
2686
|
+
const search = typeof window.location.search === "string" ? window.location.search : "";
|
|
2687
|
+
if (search) {
|
|
2688
|
+
return normalizeRequestedCurrency(new URLSearchParams(search).get("currency"));
|
|
2689
|
+
}
|
|
2690
|
+
const href = typeof window.location.href === "string" ? window.location.href : "";
|
|
2691
|
+
if (!href) {
|
|
2692
|
+
return null;
|
|
2693
|
+
}
|
|
2694
|
+
try {
|
|
2695
|
+
return normalizeRequestedCurrency(
|
|
2696
|
+
new URL(href, "https://storefront.shoppex.local").searchParams.get("currency")
|
|
2697
|
+
);
|
|
2698
|
+
} catch {
|
|
2699
|
+
return null;
|
|
2700
|
+
}
|
|
2701
|
+
}
|
|
2702
|
+
|
|
2592
2703
|
// ../sdk/src/core/config.ts
|
|
2593
2704
|
var DEFAULT_API_BASE_URL = "https://api.shoppex.io";
|
|
2594
2705
|
var currentConfig = null;
|
|
@@ -2597,20 +2708,29 @@ var DEFAULT_CHECKOUT_BASE_URL = "https://checkout.shoppex.io";
|
|
|
2597
2708
|
function initConfig(storeSlug, options) {
|
|
2598
2709
|
const normalizedShopId = options?.shopId?.trim();
|
|
2599
2710
|
cachedShopId = normalizedShopId ? normalizedShopId : null;
|
|
2600
|
-
const previousLocale = currentConfig?.locale;
|
|
2601
2711
|
currentConfig = {
|
|
2602
2712
|
storeSlug,
|
|
2603
2713
|
locale: options?.locale,
|
|
2604
|
-
currency: options?.currency,
|
|
2714
|
+
currency: normalizeInitCurrency(options?.currency),
|
|
2605
2715
|
apiBaseUrl: options?.apiBaseUrl ?? DEFAULT_API_BASE_URL,
|
|
2606
2716
|
checkoutBaseUrl: options?.checkoutBaseUrl ?? DEFAULT_CHECKOUT_BASE_URL
|
|
2607
2717
|
};
|
|
2608
|
-
if (previousLocale !== currentConfig.locale) {
|
|
2609
|
-
clearCache();
|
|
2610
|
-
}
|
|
2611
2718
|
resetTypedClient();
|
|
2612
2719
|
return currentConfig;
|
|
2613
2720
|
}
|
|
2721
|
+
function normalizeInitCurrency(value) {
|
|
2722
|
+
if (value === void 0 || value.trim() === "") {
|
|
2723
|
+
return void 0;
|
|
2724
|
+
}
|
|
2725
|
+
const normalized = normalizeRequestedCurrency(value);
|
|
2726
|
+
if (!normalized) {
|
|
2727
|
+
throw new ValidationError(
|
|
2728
|
+
`Invalid currency "${value}": pass an ISO 4217 code such as "EUR".`,
|
|
2729
|
+
["currency"]
|
|
2730
|
+
);
|
|
2731
|
+
}
|
|
2732
|
+
return normalized;
|
|
2733
|
+
}
|
|
2614
2734
|
function getConfig() {
|
|
2615
2735
|
if (!currentConfig) {
|
|
2616
2736
|
throw new NotInitializedError();
|
|
@@ -3022,6 +3142,92 @@ function resolveStorefrontSocialLinks(store) {
|
|
|
3022
3142
|
};
|
|
3023
3143
|
}
|
|
3024
3144
|
|
|
3145
|
+
// ../sdk/src/core/cache.ts
|
|
3146
|
+
var cache = /* @__PURE__ */ new Map();
|
|
3147
|
+
var pending = /* @__PURE__ */ new Map();
|
|
3148
|
+
var stats = {
|
|
3149
|
+
hits: 0,
|
|
3150
|
+
misses: 0
|
|
3151
|
+
};
|
|
3152
|
+
function isExpired(entry) {
|
|
3153
|
+
return Date.now() > entry.expiresAt;
|
|
3154
|
+
}
|
|
3155
|
+
function getCacheStats() {
|
|
3156
|
+
return {
|
|
3157
|
+
hits: stats.hits,
|
|
3158
|
+
misses: stats.misses,
|
|
3159
|
+
pendingRequests: pending.size,
|
|
3160
|
+
entries: cache.size
|
|
3161
|
+
};
|
|
3162
|
+
}
|
|
3163
|
+
function clearCache() {
|
|
3164
|
+
cache.clear();
|
|
3165
|
+
pending.clear();
|
|
3166
|
+
}
|
|
3167
|
+
function invalidateCache(prefixOrKey) {
|
|
3168
|
+
for (const key of cache.keys()) {
|
|
3169
|
+
if (key === prefixOrKey || key.startsWith(prefixOrKey)) {
|
|
3170
|
+
cache.delete(key);
|
|
3171
|
+
}
|
|
3172
|
+
}
|
|
3173
|
+
}
|
|
3174
|
+
function setCacheEntry(key, data, ttl) {
|
|
3175
|
+
const now = Date.now();
|
|
3176
|
+
cache.set(key, {
|
|
3177
|
+
data,
|
|
3178
|
+
ttl,
|
|
3179
|
+
updatedAt: now,
|
|
3180
|
+
expiresAt: now + ttl
|
|
3181
|
+
});
|
|
3182
|
+
}
|
|
3183
|
+
function getCacheEntry(key) {
|
|
3184
|
+
const entry = cache.get(key);
|
|
3185
|
+
if (!entry) return null;
|
|
3186
|
+
return entry;
|
|
3187
|
+
}
|
|
3188
|
+
async function getOrFetch(key, fetcher, options, shouldCache = () => true) {
|
|
3189
|
+
const entry = getCacheEntry(key);
|
|
3190
|
+
if (entry && !isExpired(entry)) {
|
|
3191
|
+
stats.hits += 1;
|
|
3192
|
+
return entry.data;
|
|
3193
|
+
}
|
|
3194
|
+
if (entry && options.staleWhileRevalidate) {
|
|
3195
|
+
stats.hits += 1;
|
|
3196
|
+
if (!pending.has(key)) {
|
|
3197
|
+
const refreshPromise = (async () => {
|
|
3198
|
+
try {
|
|
3199
|
+
const data = await fetcher();
|
|
3200
|
+
if (shouldCache(data)) {
|
|
3201
|
+
setCacheEntry(key, data, options.ttl);
|
|
3202
|
+
}
|
|
3203
|
+
return data;
|
|
3204
|
+
} finally {
|
|
3205
|
+
pending.delete(key);
|
|
3206
|
+
}
|
|
3207
|
+
})();
|
|
3208
|
+
pending.set(key, refreshPromise);
|
|
3209
|
+
}
|
|
3210
|
+
return entry.data;
|
|
3211
|
+
}
|
|
3212
|
+
if (pending.has(key)) {
|
|
3213
|
+
return pending.get(key);
|
|
3214
|
+
}
|
|
3215
|
+
stats.misses += 1;
|
|
3216
|
+
const promise = (async () => {
|
|
3217
|
+
try {
|
|
3218
|
+
const data = await fetcher();
|
|
3219
|
+
if (shouldCache(data)) {
|
|
3220
|
+
setCacheEntry(key, data, options.ttl);
|
|
3221
|
+
}
|
|
3222
|
+
return data;
|
|
3223
|
+
} finally {
|
|
3224
|
+
pending.delete(key);
|
|
3225
|
+
}
|
|
3226
|
+
})();
|
|
3227
|
+
pending.set(key, promise);
|
|
3228
|
+
return promise;
|
|
3229
|
+
}
|
|
3230
|
+
|
|
3025
3231
|
// ../sdk/src/core/endpoint.ts
|
|
3026
3232
|
var PARAM_PATTERN = /:([A-Za-z0-9_]+)/g;
|
|
3027
3233
|
function buildEndpoint(template, params) {
|
|
@@ -3155,9 +3361,13 @@ var MAX_RETRIES = 2;
|
|
|
3155
3361
|
async function sleep(ms) {
|
|
3156
3362
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
3157
3363
|
}
|
|
3364
|
+
function appendQueryParam(url2, key, value) {
|
|
3365
|
+
const separator = url2.includes("?") ? "&" : "?";
|
|
3366
|
+
return `${url2}${separator}${encodeURIComponent(key)}=${encodeURIComponent(value)}`;
|
|
3367
|
+
}
|
|
3158
3368
|
async function request(endpoint, options = {}) {
|
|
3159
3369
|
const config = options.baseUrl ? null : getConfig();
|
|
3160
|
-
const
|
|
3370
|
+
const audience = config ?? (isInitialized() ? getConfig() : null);
|
|
3161
3371
|
const {
|
|
3162
3372
|
method = "GET",
|
|
3163
3373
|
body,
|
|
@@ -3165,25 +3375,27 @@ async function request(endpoint, options = {}) {
|
|
|
3165
3375
|
retries,
|
|
3166
3376
|
baseUrl,
|
|
3167
3377
|
headers: requestHeaders,
|
|
3168
|
-
cache: cache2
|
|
3378
|
+
cache: cache2,
|
|
3379
|
+
buyerCurrency: pricesInBuyerCurrency = false
|
|
3169
3380
|
} = options;
|
|
3170
3381
|
const retryCount = retries ?? (method === "GET" ? MAX_RETRIES : 0);
|
|
3171
3382
|
const apiBaseUrl = baseUrl ?? config?.apiBaseUrl ?? "";
|
|
3172
|
-
const
|
|
3383
|
+
const buyerCurrency = pricesInBuyerCurrency && audience?.currency ? audience.currency : null;
|
|
3384
|
+
const url2 = buyerCurrency ? appendQueryParam(`${apiBaseUrl}${endpoint}`, "currency", buyerCurrency) : `${apiBaseUrl}${endpoint}`;
|
|
3173
3385
|
const headers = {
|
|
3174
3386
|
"Content-Type": "application/json",
|
|
3175
3387
|
Accept: "application/json",
|
|
3176
3388
|
...requestHeaders
|
|
3177
3389
|
};
|
|
3178
|
-
|
|
3179
|
-
|
|
3390
|
+
const locale = typeof audience?.locale === "string" && audience.locale.trim() ? audience.locale.trim() : null;
|
|
3391
|
+
if (locale) {
|
|
3392
|
+
headers["x-shoppex-locale"] = locale;
|
|
3180
3393
|
}
|
|
3181
3394
|
let lastFailure = null;
|
|
3182
3395
|
const executeRequest = async () => {
|
|
3183
3396
|
for (let attempt = 0; attempt <= retryCount; attempt++) {
|
|
3184
3397
|
let responseReceived = false;
|
|
3185
3398
|
let responseDefinitive = false;
|
|
3186
|
-
let responseChallenge;
|
|
3187
3399
|
try {
|
|
3188
3400
|
const controller = new AbortController();
|
|
3189
3401
|
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
@@ -3196,7 +3408,6 @@ async function request(endpoint, options = {}) {
|
|
|
3196
3408
|
responseReceived = true;
|
|
3197
3409
|
clearTimeout(timeoutId);
|
|
3198
3410
|
const payload = await parseResponsePayload(response);
|
|
3199
|
-
responseChallenge = readResponseChallenge(payload.data);
|
|
3200
3411
|
if (!response.ok) {
|
|
3201
3412
|
responseDefinitive = isDefinitiveHttpRefusal(payload.data) && response.status >= 400 && response.status < 500 && response.status !== 408;
|
|
3202
3413
|
const fallbackHttpMessage = response.statusText ? `HTTP ${response.status}: ${response.statusText}` : `HTTP ${response.status}`;
|
|
@@ -3221,8 +3432,7 @@ async function request(endpoint, options = {}) {
|
|
|
3221
3432
|
...mapped,
|
|
3222
3433
|
responseReceived: true,
|
|
3223
3434
|
responseDefinitive: data.status >= 400 && data.status < 500 && data.status !== 408,
|
|
3224
|
-
status: response.status
|
|
3225
|
-
...responseChallenge ? { challenge: responseChallenge } : {}
|
|
3435
|
+
status: response.status
|
|
3226
3436
|
};
|
|
3227
3437
|
} catch (error) {
|
|
3228
3438
|
let normalizedError = error instanceof Error ? error : new Error(String(error));
|
|
@@ -3236,7 +3446,6 @@ async function request(endpoint, options = {}) {
|
|
|
3236
3446
|
isTransport: statusCode === void 0 || statusCode === 408,
|
|
3237
3447
|
responseReceived,
|
|
3238
3448
|
responseDefinitive,
|
|
3239
|
-
...responseChallenge ? { challenge: responseChallenge } : {},
|
|
3240
3449
|
...normalizedError instanceof ApiError ? {
|
|
3241
3450
|
code: normalizedError.code,
|
|
3242
3451
|
...normalizedError.errorParams ? { errorParams: normalizedError.errorParams } : {}
|
|
@@ -3258,13 +3467,12 @@ async function request(endpoint, options = {}) {
|
|
|
3258
3467
|
...lastFailure ? { responseReceived: lastFailure.responseReceived } : {},
|
|
3259
3468
|
...lastFailure?.responseDefinitive ? { responseDefinitive: true } : {},
|
|
3260
3469
|
...lastFailure?.responseReceived && lastFailure.statusCode !== void 0 ? { status: lastFailure.statusCode } : {},
|
|
3261
|
-
...lastFailure?.challenge ? { challenge: lastFailure.challenge } : {},
|
|
3262
3470
|
...lastFailure?.code ? { code: lastFailure.code } : {},
|
|
3263
3471
|
...lastFailure?.errorParams ? { errorParams: lastFailure.errorParams } : {}
|
|
3264
3472
|
};
|
|
3265
3473
|
};
|
|
3266
3474
|
const result = method === "GET" && cache2 && cache2.ttl > 0 ? await getOrFetch(
|
|
3267
|
-
cache2.key ?? `GET:${url2}`,
|
|
3475
|
+
`${locale ?? ""}|${buyerCurrency ?? ""}|${cache2.key ?? `GET:${url2}`}`,
|
|
3268
3476
|
executeRequest,
|
|
3269
3477
|
{ ttl: cache2.ttl, staleWhileRevalidate: cache2.staleWhileRevalidate },
|
|
3270
3478
|
(value) => value.success
|
|
@@ -3319,25 +3527,6 @@ async function parseResponsePayload(response) {
|
|
|
3319
3527
|
return { data: null, rawText: null };
|
|
3320
3528
|
}
|
|
3321
3529
|
}
|
|
3322
|
-
function readResponseChallenge(payload) {
|
|
3323
|
-
if (!payload || typeof payload !== "object") {
|
|
3324
|
-
return void 0;
|
|
3325
|
-
}
|
|
3326
|
-
const data = payload.data;
|
|
3327
|
-
if (!data || typeof data !== "object") {
|
|
3328
|
-
return void 0;
|
|
3329
|
-
}
|
|
3330
|
-
const challenge = data.challenge;
|
|
3331
|
-
if (!challenge || typeof challenge !== "object") {
|
|
3332
|
-
return void 0;
|
|
3333
|
-
}
|
|
3334
|
-
const provider = challenge.provider;
|
|
3335
|
-
const siteKey = challenge.site_key;
|
|
3336
|
-
if (provider !== "turnstile" || typeof siteKey !== "string" || !siteKey.trim()) {
|
|
3337
|
-
return void 0;
|
|
3338
|
-
}
|
|
3339
|
-
return { provider, siteKey: siteKey.trim() };
|
|
3340
|
-
}
|
|
3341
3530
|
function isDefinitiveHttpRefusal(payload) {
|
|
3342
3531
|
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
|
|
3343
3532
|
return false;
|
|
@@ -3393,6 +3582,7 @@ async function getStore() {
|
|
|
3393
3582
|
storeSlug: config.storeSlug
|
|
3394
3583
|
}),
|
|
3395
3584
|
{
|
|
3585
|
+
buyerCurrency: true,
|
|
3396
3586
|
cache: {
|
|
3397
3587
|
key: `store:${config.storeSlug}`,
|
|
3398
3588
|
ttl: STORE_CACHE_TTL,
|
|
@@ -3430,6 +3620,7 @@ async function resolveStoreByDomain(domain, apiBaseUrl) {
|
|
|
3430
3620
|
}),
|
|
3431
3621
|
{
|
|
3432
3622
|
baseUrl,
|
|
3623
|
+
buyerCurrency: true,
|
|
3433
3624
|
cache: {
|
|
3434
3625
|
key: `store:domain:${cleanDomain}`,
|
|
3435
3626
|
ttl: STORE_CACHE_TTL,
|
|
@@ -3466,6 +3657,7 @@ async function getStorefront(options) {
|
|
|
3466
3657
|
storeSlug: config.storeSlug
|
|
3467
3658
|
})}${querySuffix}`,
|
|
3468
3659
|
{
|
|
3660
|
+
buyerCurrency: true,
|
|
3469
3661
|
cache: {
|
|
3470
3662
|
key: `storefront:${config.storeSlug}:${options?.productsLimit ?? "full"}:${options?.productsCursor ?? "start"}`,
|
|
3471
3663
|
ttl: STORE_CACHE_TTL,
|
|
@@ -3563,6 +3755,7 @@ async function getProducts() {
|
|
|
3563
3755
|
storeSlug: config.storeSlug
|
|
3564
3756
|
}),
|
|
3565
3757
|
{
|
|
3758
|
+
buyerCurrency: true,
|
|
3566
3759
|
cache: {
|
|
3567
3760
|
key: `products:${config.storeSlug}`,
|
|
3568
3761
|
ttl: PRODUCTS_CACHE_TTL,
|
|
@@ -3606,6 +3799,7 @@ async function getStorefrontProductsPage(options) {
|
|
|
3606
3799
|
storeSlug: config.storeSlug
|
|
3607
3800
|
})}${querySuffix}`,
|
|
3608
3801
|
{
|
|
3802
|
+
buyerCurrency: true,
|
|
3609
3803
|
cache: {
|
|
3610
3804
|
key: `products:page:${config.storeSlug}:${options?.limit ?? "default"}:${options?.cursor ?? "start"}:${options?.sort ?? "featured"}:${getStorefrontProductsPageCategoryCacheKey(options?.category)}:${options?.hideOutOfStock === true ? "in-stock" : "all-stock"}`,
|
|
3611
3805
|
ttl: PRODUCTS_CACHE_TTL,
|
|
@@ -3637,6 +3831,7 @@ async function getProduct(idOrSlug) {
|
|
|
3637
3831
|
const response = await get(
|
|
3638
3832
|
`${buildEndpoint("/v1/storefront/products/unique/:idOrSlug", { idOrSlug })}${queryParams}`,
|
|
3639
3833
|
{
|
|
3834
|
+
buyerCurrency: true,
|
|
3640
3835
|
cache: {
|
|
3641
3836
|
key: `product:${idOrSlug}:${shopId ?? "no-shop"}`,
|
|
3642
3837
|
ttl: PRODUCTS_CACHE_TTL,
|
|
@@ -3932,32 +4127,6 @@ function ensureCartLineId(item) {
|
|
|
3932
4127
|
return { ...item, line_id: computeCartLineId(item) };
|
|
3933
4128
|
}
|
|
3934
4129
|
|
|
3935
|
-
// ../sdk/src/utils/requested-currency.ts
|
|
3936
|
-
function normalizeRequestedCurrency(value) {
|
|
3937
|
-
const normalized = value?.trim().toUpperCase();
|
|
3938
|
-
return normalized && /^[A-Z]{3}$/.test(normalized) ? normalized : null;
|
|
3939
|
-
}
|
|
3940
|
-
function getRequestedCurrencyFromLocation() {
|
|
3941
|
-
if (typeof window === "undefined" || !window.location) {
|
|
3942
|
-
return null;
|
|
3943
|
-
}
|
|
3944
|
-
const search = typeof window.location.search === "string" ? window.location.search : "";
|
|
3945
|
-
if (search) {
|
|
3946
|
-
return normalizeRequestedCurrency(new URLSearchParams(search).get("currency"));
|
|
3947
|
-
}
|
|
3948
|
-
const href = typeof window.location.href === "string" ? window.location.href : "";
|
|
3949
|
-
if (!href) {
|
|
3950
|
-
return null;
|
|
3951
|
-
}
|
|
3952
|
-
try {
|
|
3953
|
-
return normalizeRequestedCurrency(
|
|
3954
|
-
new URL(href, "https://storefront.shoppex.local").searchParams.get("currency")
|
|
3955
|
-
);
|
|
3956
|
-
} catch {
|
|
3957
|
-
return null;
|
|
3958
|
-
}
|
|
3959
|
-
}
|
|
3960
|
-
|
|
3961
4130
|
// ../sdk/src/modules/cart.ts
|
|
3962
4131
|
var STORAGE_KEYS = {
|
|
3963
4132
|
cart: "cart",
|
|
@@ -4539,158 +4708,11 @@ async function quoteCart(coupon, currency) {
|
|
|
4539
4708
|
return response;
|
|
4540
4709
|
}
|
|
4541
4710
|
|
|
4542
|
-
// ../sdk/src/modules/checkout-challenge.ts
|
|
4543
|
-
var TURNSTILE_FRAME_MESSAGE_SOURCE = "shoppex-turnstile";
|
|
4544
|
-
var TURNSTILE_FRAME_MESSAGE_VERSION = 1;
|
|
4545
|
-
var TURNSTILE_FRAME_READY_TIMEOUT_MS = 1e4;
|
|
4546
|
-
function readFrameMessage(value, nonce) {
|
|
4547
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
4548
|
-
const record2 = value;
|
|
4549
|
-
if (record2.source !== TURNSTILE_FRAME_MESSAGE_SOURCE || record2.version !== TURNSTILE_FRAME_MESSAGE_VERSION || record2.nonce !== nonce || !["ready", "visible", "hidden", "success", "expired", "timeout", "error"].includes(String(record2.type))) return null;
|
|
4550
|
-
if (record2.type === "success" && (typeof record2.token !== "string" || !record2.token.trim())) {
|
|
4551
|
-
return null;
|
|
4552
|
-
}
|
|
4553
|
-
return {
|
|
4554
|
-
type: record2.type,
|
|
4555
|
-
...typeof record2.token === "string" ? { token: record2.token.trim() } : {}
|
|
4556
|
-
};
|
|
4557
|
-
}
|
|
4558
|
-
var AUTO_CHALLENGE_INVISIBLE_TIMEOUT_MS = 3e4;
|
|
4559
|
-
async function resolveCheckoutChallengeProof(challenge) {
|
|
4560
|
-
if (typeof document === "undefined" || challenge.provider !== "turnstile" || !challenge.siteKey.trim()) {
|
|
4561
|
-
return null;
|
|
4562
|
-
}
|
|
4563
|
-
const host = document.createElement("div");
|
|
4564
|
-
host.setAttribute("data-shoppex-checkout-challenge", "");
|
|
4565
|
-
host.style.position = "fixed";
|
|
4566
|
-
host.style.inset = "0";
|
|
4567
|
-
host.style.display = "none";
|
|
4568
|
-
host.style.alignItems = "center";
|
|
4569
|
-
host.style.justifyContent = "center";
|
|
4570
|
-
host.style.background = "rgba(0, 0, 0, 0.55)";
|
|
4571
|
-
host.style.zIndex = "2147483646";
|
|
4572
|
-
const card = document.createElement("div");
|
|
4573
|
-
card.style.width = "min(340px, 90vw)";
|
|
4574
|
-
card.style.background = "#ffffff";
|
|
4575
|
-
card.style.borderRadius = "12px";
|
|
4576
|
-
card.style.padding = "16px";
|
|
4577
|
-
card.style.boxShadow = "0 12px 40px rgba(0, 0, 0, 0.35)";
|
|
4578
|
-
host.appendChild(card);
|
|
4579
|
-
document.body.appendChild(host);
|
|
4580
|
-
return new Promise((resolve) => {
|
|
4581
|
-
let settled = false;
|
|
4582
|
-
let timeoutId = null;
|
|
4583
|
-
let frame = null;
|
|
4584
|
-
const finish = (token) => {
|
|
4585
|
-
if (settled) return;
|
|
4586
|
-
settled = true;
|
|
4587
|
-
if (timeoutId !== null) window.clearTimeout(timeoutId);
|
|
4588
|
-
frame?.dispose();
|
|
4589
|
-
host.remove();
|
|
4590
|
-
resolve(token);
|
|
4591
|
-
};
|
|
4592
|
-
const armTimeout = () => {
|
|
4593
|
-
timeoutId = window.setTimeout(() => finish(null), AUTO_CHALLENGE_INVISIBLE_TIMEOUT_MS);
|
|
4594
|
-
};
|
|
4595
|
-
try {
|
|
4596
|
-
frame = mountCheckoutChallenge(card, challenge, {
|
|
4597
|
-
onSuccess: (token) => finish(token),
|
|
4598
|
-
// `refresh-expired: auto` renews expired runs on its own; the
|
|
4599
|
-
// invisible-run timeout stays the bound.
|
|
4600
|
-
onExpired: () => {
|
|
4601
|
-
},
|
|
4602
|
-
onUnavailable: () => finish(null),
|
|
4603
|
-
onVisibilityChange: (visible) => {
|
|
4604
|
-
host.style.display = visible ? "flex" : "none";
|
|
4605
|
-
if (visible) {
|
|
4606
|
-
if (timeoutId !== null) {
|
|
4607
|
-
window.clearTimeout(timeoutId);
|
|
4608
|
-
timeoutId = null;
|
|
4609
|
-
}
|
|
4610
|
-
} else if (timeoutId === null && !settled) {
|
|
4611
|
-
armTimeout();
|
|
4612
|
-
}
|
|
4613
|
-
}
|
|
4614
|
-
});
|
|
4615
|
-
} catch {
|
|
4616
|
-
host.remove();
|
|
4617
|
-
resolve(null);
|
|
4618
|
-
return;
|
|
4619
|
-
}
|
|
4620
|
-
armTimeout();
|
|
4621
|
-
});
|
|
4622
|
-
}
|
|
4623
|
-
function mountCheckoutChallenge(container, challenge, callbacks) {
|
|
4624
|
-
if (challenge.provider !== "turnstile" || !challenge.siteKey.trim()) {
|
|
4625
|
-
throw new Error("Checkout challenge is invalid.");
|
|
4626
|
-
}
|
|
4627
|
-
const win = container.ownerDocument.defaultView;
|
|
4628
|
-
if (!win) throw new Error("Checkout challenge requires a browser document.");
|
|
4629
|
-
const checkoutBaseUrl = getConfig().checkoutBaseUrl;
|
|
4630
|
-
const frameUrl = new URL("/turnstile", checkoutBaseUrl);
|
|
4631
|
-
if (frameUrl.protocol !== "https:" && frameUrl.protocol !== "http:") {
|
|
4632
|
-
throw new Error("Checkout base URL must use http or https.");
|
|
4633
|
-
}
|
|
4634
|
-
const nonce = win.crypto.randomUUID();
|
|
4635
|
-
frameUrl.searchParams.set("site_key", challenge.siteKey.trim());
|
|
4636
|
-
frameUrl.searchParams.set("nonce", nonce);
|
|
4637
|
-
const frame = container.ownerDocument.createElement("iframe");
|
|
4638
|
-
frame.src = frameUrl.toString();
|
|
4639
|
-
frame.title = "Checkout verification";
|
|
4640
|
-
frame.referrerPolicy = "no-referrer";
|
|
4641
|
-
frame.style.border = "0";
|
|
4642
|
-
frame.style.width = "100%";
|
|
4643
|
-
frame.style.height = "0";
|
|
4644
|
-
let disposed = false;
|
|
4645
|
-
let ready = false;
|
|
4646
|
-
const readyTimeout = win.setTimeout(() => {
|
|
4647
|
-
if (!disposed && !ready) callbacks.onUnavailable?.();
|
|
4648
|
-
}, TURNSTILE_FRAME_READY_TIMEOUT_MS);
|
|
4649
|
-
const onMessage = (event) => {
|
|
4650
|
-
if (disposed || event.origin !== frameUrl.origin || event.source !== frame.contentWindow) return;
|
|
4651
|
-
const message = readFrameMessage(event.data, nonce);
|
|
4652
|
-
if (!message) return;
|
|
4653
|
-
ready = true;
|
|
4654
|
-
win.clearTimeout(readyTimeout);
|
|
4655
|
-
if (message.type === "visible") {
|
|
4656
|
-
frame.style.height = "72px";
|
|
4657
|
-
callbacks.onVisibilityChange?.(true);
|
|
4658
|
-
}
|
|
4659
|
-
if (message.type === "hidden") {
|
|
4660
|
-
frame.style.height = "0";
|
|
4661
|
-
callbacks.onVisibilityChange?.(false);
|
|
4662
|
-
}
|
|
4663
|
-
if (message.type === "success") {
|
|
4664
|
-
frame.style.height = "0";
|
|
4665
|
-
callbacks.onVisibilityChange?.(false);
|
|
4666
|
-
callbacks.onSuccess(message.token);
|
|
4667
|
-
}
|
|
4668
|
-
if (message.type === "expired" || message.type === "timeout") callbacks.onExpired?.();
|
|
4669
|
-
if (message.type === "error") callbacks.onUnavailable?.();
|
|
4670
|
-
};
|
|
4671
|
-
const onFrameError = () => callbacks.onUnavailable?.();
|
|
4672
|
-
win.addEventListener("message", onMessage);
|
|
4673
|
-
frame.addEventListener("error", onFrameError, { once: true });
|
|
4674
|
-
container.appendChild(frame);
|
|
4675
|
-
return {
|
|
4676
|
-
element: frame,
|
|
4677
|
-
dispose() {
|
|
4678
|
-
if (disposed) return;
|
|
4679
|
-
disposed = true;
|
|
4680
|
-
win.clearTimeout(readyTimeout);
|
|
4681
|
-
win.removeEventListener("message", onMessage);
|
|
4682
|
-
frame.removeEventListener("error", onFrameError);
|
|
4683
|
-
frame.remove();
|
|
4684
|
-
}
|
|
4685
|
-
};
|
|
4686
|
-
}
|
|
4687
|
-
|
|
4688
4711
|
// ../sdk/src/modules/checkout.ts
|
|
4689
4712
|
var CheckoutCreateError = class _CheckoutCreateError extends Error {
|
|
4690
4713
|
constructor(message, options = {}) {
|
|
4691
4714
|
super(message);
|
|
4692
4715
|
this.name = "CheckoutCreateError";
|
|
4693
|
-
this.challenge = options.challenge;
|
|
4694
4716
|
this.code = options.code;
|
|
4695
4717
|
this.status = options.status;
|
|
4696
4718
|
Object.setPrototypeOf(this, _CheckoutCreateError.prototype);
|
|
@@ -4741,7 +4763,7 @@ function acquireCheckoutCreateIdempotency(requestTarget, createIntent) {
|
|
|
4741
4763
|
return attempt;
|
|
4742
4764
|
}
|
|
4743
4765
|
function releaseCheckoutCreateIdempotency(attempt, outcomeDefinitive) {
|
|
4744
|
-
if (outcomeDefinitive && pendingCheckoutCreates.get(attempt.fingerprint) === attempt) {
|
|
4766
|
+
if (outcomeDefinitive && pendingCheckoutCreates.get(attempt.fingerprint)?.key === attempt.key) {
|
|
4745
4767
|
pendingCheckoutCreates.delete(attempt.fingerprint);
|
|
4746
4768
|
}
|
|
4747
4769
|
}
|
|
@@ -4755,7 +4777,13 @@ function resolveCustomerCheckoutRequestTarget(options) {
|
|
|
4755
4777
|
return { endpoint: "/v1/storefront/invoices/from-cart" };
|
|
4756
4778
|
}
|
|
4757
4779
|
function resolveRequestedCheckoutCurrency(options) {
|
|
4758
|
-
|
|
4780
|
+
if (options.currency !== void 0) {
|
|
4781
|
+
return normalizeRequestedCurrency(options.currency);
|
|
4782
|
+
}
|
|
4783
|
+
return getRequestedCurrencyFromLocation() ?? normalizeRequestedCurrency(getConfig().currency);
|
|
4784
|
+
}
|
|
4785
|
+
function getRequestedCheckoutCurrency() {
|
|
4786
|
+
return getRequestedCurrencyFromLocation() ?? normalizeRequestedCurrency(getConfig().currency);
|
|
4759
4787
|
}
|
|
4760
4788
|
function normalizeCheckoutFailureMessage(rawMessage) {
|
|
4761
4789
|
const message = rawMessage?.trim() ?? "";
|
|
@@ -4927,14 +4955,7 @@ function mapCartItemsForApi(items) {
|
|
|
4927
4955
|
}
|
|
4928
4956
|
async function checkout(couponOrOptions, options) {
|
|
4929
4957
|
const resolvedOptions = resolveCheckoutOptions(couponOrOptions, options);
|
|
4930
|
-
|
|
4931
|
-
if (!firstAttempt.success && firstAttempt.challenge?.provider === "turnstile" && !resolvedOptions.turnstileToken && typeof document !== "undefined") {
|
|
4932
|
-
const proof = await resolveCheckoutChallengeProof(firstAttempt.challenge);
|
|
4933
|
-
if (proof) {
|
|
4934
|
-
return performCheckout({ ...resolvedOptions, turnstileToken: proof });
|
|
4935
|
-
}
|
|
4936
|
-
}
|
|
4937
|
-
return firstAttempt;
|
|
4958
|
+
return performCheckout(resolvedOptions);
|
|
4938
4959
|
}
|
|
4939
4960
|
async function performCheckout(resolvedOptions) {
|
|
4940
4961
|
const { autoRedirect = true, email: email2 } = resolvedOptions;
|
|
@@ -4978,16 +4999,12 @@ async function performCheckout(resolvedOptions) {
|
|
|
4978
4999
|
// automatically so the server can refuse an invoice priced above it.
|
|
4979
5000
|
// Undefined when nothing has been quoted this session — the endpoint
|
|
4980
5001
|
// treats that exactly as an older SDK.
|
|
4981
|
-
quote_token: getLatestQuoteToken() ?? void 0
|
|
4982
|
-
};
|
|
4983
|
-
const createCommand = {
|
|
4984
|
-
...createIntent,
|
|
4985
|
-
turnstile_token: resolvedOptions.turnstileToken?.trim() || void 0
|
|
5002
|
+
quote_token: resolvedOptions.quoteToken !== void 0 ? resolvedOptions.quoteToken ?? void 0 : getLatestQuoteToken() ?? void 0
|
|
4986
5003
|
};
|
|
4987
5004
|
const createAttempt = acquireCheckoutCreateIdempotency(checkoutRequestTarget, createIntent);
|
|
4988
5005
|
const response = await post(
|
|
4989
5006
|
checkoutRequestTarget.endpoint,
|
|
4990
|
-
|
|
5007
|
+
createIntent,
|
|
4991
5008
|
{
|
|
4992
5009
|
retries: 0,
|
|
4993
5010
|
baseUrl: checkoutRequestTarget.baseUrl,
|
|
@@ -5007,8 +5024,7 @@ async function performCheckout(resolvedOptions) {
|
|
|
5007
5024
|
// The server's machine-readable refusal, preserved so callers can react
|
|
5008
5025
|
// to e.g. `errors.checkout.price_increased_since_quote` without matching
|
|
5009
5026
|
// localized copy.
|
|
5010
|
-
...response.code ? { code: response.code } : {}
|
|
5011
|
-
...response.challenge ? { challenge: response.challenge } : {}
|
|
5027
|
+
...response.code ? { code: response.code } : {}
|
|
5012
5028
|
};
|
|
5013
5029
|
}
|
|
5014
5030
|
const checkoutData = normalizeCheckoutResponse(response.data, config.checkoutBaseUrl);
|
|
@@ -5054,17 +5070,7 @@ async function performCheckout(resolvedOptions) {
|
|
|
5054
5070
|
}
|
|
5055
5071
|
async function buildCheckoutUrl(couponOrOptions, options) {
|
|
5056
5072
|
const resolvedOptions = resolveCheckoutOptions(couponOrOptions, options);
|
|
5057
|
-
|
|
5058
|
-
return await performBuildCheckoutUrl(resolvedOptions);
|
|
5059
|
-
} catch (error) {
|
|
5060
|
-
if (error instanceof CheckoutCreateError && error.challenge?.provider === "turnstile" && !resolvedOptions.turnstileToken && typeof document !== "undefined") {
|
|
5061
|
-
const proof = await resolveCheckoutChallengeProof(error.challenge);
|
|
5062
|
-
if (proof) {
|
|
5063
|
-
return performBuildCheckoutUrl({ ...resolvedOptions, turnstileToken: proof });
|
|
5064
|
-
}
|
|
5065
|
-
}
|
|
5066
|
-
throw error;
|
|
5067
|
-
}
|
|
5073
|
+
return performBuildCheckoutUrl(resolvedOptions);
|
|
5068
5074
|
}
|
|
5069
5075
|
async function performBuildCheckoutUrl(resolvedOptions) {
|
|
5070
5076
|
const { email: email2 } = resolvedOptions;
|
|
@@ -5100,16 +5106,12 @@ async function performBuildCheckoutUrl(resolvedOptions) {
|
|
|
5100
5106
|
// token here left a public path on which the server had nothing to check
|
|
5101
5107
|
// the invoice against. Same optional semantics: undefined when nothing
|
|
5102
5108
|
// was quoted this session.
|
|
5103
|
-
quote_token: getLatestQuoteToken() ?? void 0
|
|
5104
|
-
};
|
|
5105
|
-
const createCommand = {
|
|
5106
|
-
...createIntent,
|
|
5107
|
-
turnstile_token: resolvedOptions.turnstileToken?.trim() || void 0
|
|
5109
|
+
quote_token: resolvedOptions.quoteToken !== void 0 ? resolvedOptions.quoteToken ?? void 0 : getLatestQuoteToken() ?? void 0
|
|
5108
5110
|
};
|
|
5109
5111
|
const createAttempt = acquireCheckoutCreateIdempotency(checkoutRequestTarget, createIntent);
|
|
5110
5112
|
const response = await post(
|
|
5111
5113
|
checkoutRequestTarget.endpoint,
|
|
5112
|
-
|
|
5114
|
+
createIntent,
|
|
5113
5115
|
{
|
|
5114
5116
|
retries: 0,
|
|
5115
5117
|
baseUrl: checkoutRequestTarget.baseUrl,
|
|
@@ -5124,7 +5126,6 @@ async function performBuildCheckoutUrl(resolvedOptions) {
|
|
|
5124
5126
|
clearCart();
|
|
5125
5127
|
}
|
|
5126
5128
|
throw new CheckoutCreateError(normalizeCheckoutFailureMessage(response.message), {
|
|
5127
|
-
...response.challenge ? { challenge: response.challenge } : {},
|
|
5128
5129
|
...response.code ? { code: response.code } : {},
|
|
5129
5130
|
...response.status !== void 0 ? { status: response.status } : {}
|
|
5130
5131
|
});
|
|
@@ -6159,12 +6160,13 @@ var shoppex = {
|
|
|
6159
6160
|
getCartStats,
|
|
6160
6161
|
validateCartIntegrity,
|
|
6161
6162
|
quoteCart,
|
|
6163
|
+
getLatestQuoteToken,
|
|
6162
6164
|
resolveCartLineId,
|
|
6163
6165
|
// Checkout
|
|
6164
6166
|
checkout,
|
|
6165
6167
|
buildCheckoutUrl,
|
|
6166
6168
|
buildCheckoutUrlSync,
|
|
6167
|
-
|
|
6169
|
+
getRequestedCheckoutCurrency,
|
|
6168
6170
|
// Affiliates
|
|
6169
6171
|
captureAffiliateFromUrl,
|
|
6170
6172
|
validateAffiliateCode,
|
|
@@ -6265,6 +6267,7 @@ export {
|
|
|
6265
6267
|
ApiError,
|
|
6266
6268
|
CATALOG_UNIT_PRICE_DECIMAL_PLACES,
|
|
6267
6269
|
CATALOG_UNIT_PRICE_FORMAT_OPTIONS,
|
|
6270
|
+
CURRENCY_UNAVAILABLE_ERROR_CODE,
|
|
6268
6271
|
CartError,
|
|
6269
6272
|
CheckoutCreateError,
|
|
6270
6273
|
NetworkError,
|
|
@@ -6313,7 +6316,6 @@ export {
|
|
|
6313
6316
|
loyalty,
|
|
6314
6317
|
me,
|
|
6315
6318
|
mergeSettings,
|
|
6316
|
-
mountCheckoutChallenge,
|
|
6317
6319
|
normalizeSearchQuery,
|
|
6318
6320
|
normalizeStorefrontCustomFields,
|
|
6319
6321
|
order,
|