@shoppexio/storefront 1.0.63 → 1.0.64
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 +4 -0
- package/README.md +40 -0
- package/dist/index.cjs +169 -74
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -8
- package/dist/index.d.ts +2 -8
- package/dist/index.js +169 -74
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.64
|
|
4
|
+
|
|
5
|
+
- Resolve checkout verification challenges automatically: `checkout()` and `buildCheckoutUrl()` mount the hosted broker themselves on a challenge refusal and replay the create once with the delivered proof — no merchant wiring, no second click. `mountCheckoutChallenge()` gains an `onVisibilityChange` callback.
|
|
6
|
+
|
|
3
7
|
## 1.0.63
|
|
4
8
|
|
|
5
9
|
- Keep managed checkout verification collapsed until Cloudflare requires buyer interaction.
|
package/README.md
CHANGED
|
@@ -66,6 +66,46 @@ currency-bound; Shoppex rejects a mismatch as
|
|
|
66
66
|
`errors.checkout.quote_token_stale` instead of creating an invoice for a total
|
|
67
67
|
the buyer did not approve.
|
|
68
68
|
|
|
69
|
+
## Checkout verification on custom domains
|
|
70
|
+
|
|
71
|
+
Shoppex can require human verification before it creates an invoice. Use the
|
|
72
|
+
hosted verification broker from the SDK. Do not render the returned Turnstile
|
|
73
|
+
site key directly on your custom domain.
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
import shoppex, { mountCheckoutChallenge } from '@shoppexio/storefront';
|
|
77
|
+
|
|
78
|
+
const container = document.getElementById('checkout-verification');
|
|
79
|
+
let frame: ReturnType<typeof mountCheckoutChallenge> | undefined;
|
|
80
|
+
|
|
81
|
+
if (!container) throw new Error('Checkout verification container is missing.');
|
|
82
|
+
|
|
83
|
+
async function startCheckout(turnstileToken?: string) {
|
|
84
|
+
const result = await shoppex.checkout({ turnstileToken });
|
|
85
|
+
|
|
86
|
+
if (result.success) return;
|
|
87
|
+
if (!result.challenge) throw new Error(result.message ?? 'Checkout could not be started.');
|
|
88
|
+
|
|
89
|
+
frame?.dispose();
|
|
90
|
+
frame = mountCheckoutChallenge(container, result.challenge, {
|
|
91
|
+
onSuccess(token) {
|
|
92
|
+
frame?.dispose();
|
|
93
|
+
frame = undefined;
|
|
94
|
+
void startCheckout(token);
|
|
95
|
+
},
|
|
96
|
+
onUnavailable() {
|
|
97
|
+
frame?.dispose();
|
|
98
|
+
frame = undefined;
|
|
99
|
+
console.error('Checkout verification could not load.');
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
If your site sends a Content Security Policy, add
|
|
106
|
+
`https://checkout.shoppex.io` to `frame-src`. Call `dispose()` when the checkout
|
|
107
|
+
component unmounts or before you mount another challenge.
|
|
108
|
+
|
|
69
109
|
## Custom customer account
|
|
70
110
|
|
|
71
111
|
Build your own OTP login, order history, and download UI without adding a
|
package/dist/index.cjs
CHANGED
|
@@ -19934,6 +19934,152 @@ async function quoteCart(coupon, currency) {
|
|
|
19934
19934
|
return response;
|
|
19935
19935
|
}
|
|
19936
19936
|
|
|
19937
|
+
// ../sdk/src/modules/checkout-challenge.ts
|
|
19938
|
+
var TURNSTILE_FRAME_MESSAGE_SOURCE = "shoppex-turnstile";
|
|
19939
|
+
var TURNSTILE_FRAME_MESSAGE_VERSION = 1;
|
|
19940
|
+
var TURNSTILE_FRAME_READY_TIMEOUT_MS = 1e4;
|
|
19941
|
+
function readFrameMessage(value, nonce) {
|
|
19942
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
19943
|
+
const record2 = value;
|
|
19944
|
+
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;
|
|
19945
|
+
if (record2.type === "success" && (typeof record2.token !== "string" || !record2.token.trim())) {
|
|
19946
|
+
return null;
|
|
19947
|
+
}
|
|
19948
|
+
return {
|
|
19949
|
+
type: record2.type,
|
|
19950
|
+
...typeof record2.token === "string" ? { token: record2.token.trim() } : {}
|
|
19951
|
+
};
|
|
19952
|
+
}
|
|
19953
|
+
var AUTO_CHALLENGE_INVISIBLE_TIMEOUT_MS = 3e4;
|
|
19954
|
+
async function resolveCheckoutChallengeProof(challenge) {
|
|
19955
|
+
if (typeof document === "undefined" || challenge.provider !== "turnstile" || !challenge.siteKey.trim()) {
|
|
19956
|
+
return null;
|
|
19957
|
+
}
|
|
19958
|
+
const host = document.createElement("div");
|
|
19959
|
+
host.setAttribute("data-shoppex-checkout-challenge", "");
|
|
19960
|
+
host.style.position = "fixed";
|
|
19961
|
+
host.style.inset = "0";
|
|
19962
|
+
host.style.display = "none";
|
|
19963
|
+
host.style.alignItems = "center";
|
|
19964
|
+
host.style.justifyContent = "center";
|
|
19965
|
+
host.style.background = "rgba(0, 0, 0, 0.55)";
|
|
19966
|
+
host.style.zIndex = "2147483646";
|
|
19967
|
+
const card = document.createElement("div");
|
|
19968
|
+
card.style.width = "min(340px, 90vw)";
|
|
19969
|
+
card.style.background = "#ffffff";
|
|
19970
|
+
card.style.borderRadius = "12px";
|
|
19971
|
+
card.style.padding = "16px";
|
|
19972
|
+
card.style.boxShadow = "0 12px 40px rgba(0, 0, 0, 0.35)";
|
|
19973
|
+
host.appendChild(card);
|
|
19974
|
+
document.body.appendChild(host);
|
|
19975
|
+
return new Promise((resolve) => {
|
|
19976
|
+
let settled = false;
|
|
19977
|
+
let timeoutId = null;
|
|
19978
|
+
let frame = null;
|
|
19979
|
+
const finish = (token) => {
|
|
19980
|
+
if (settled) return;
|
|
19981
|
+
settled = true;
|
|
19982
|
+
if (timeoutId !== null) window.clearTimeout(timeoutId);
|
|
19983
|
+
frame?.dispose();
|
|
19984
|
+
host.remove();
|
|
19985
|
+
resolve(token);
|
|
19986
|
+
};
|
|
19987
|
+
const armTimeout = () => {
|
|
19988
|
+
timeoutId = window.setTimeout(() => finish(null), AUTO_CHALLENGE_INVISIBLE_TIMEOUT_MS);
|
|
19989
|
+
};
|
|
19990
|
+
try {
|
|
19991
|
+
frame = mountCheckoutChallenge(card, challenge, {
|
|
19992
|
+
onSuccess: (token) => finish(token),
|
|
19993
|
+
// `refresh-expired: auto` renews expired runs on its own; the
|
|
19994
|
+
// invisible-run timeout stays the bound.
|
|
19995
|
+
onExpired: () => {
|
|
19996
|
+
},
|
|
19997
|
+
onUnavailable: () => finish(null),
|
|
19998
|
+
onVisibilityChange: (visible) => {
|
|
19999
|
+
host.style.display = visible ? "flex" : "none";
|
|
20000
|
+
if (visible) {
|
|
20001
|
+
if (timeoutId !== null) {
|
|
20002
|
+
window.clearTimeout(timeoutId);
|
|
20003
|
+
timeoutId = null;
|
|
20004
|
+
}
|
|
20005
|
+
} else if (timeoutId === null && !settled) {
|
|
20006
|
+
armTimeout();
|
|
20007
|
+
}
|
|
20008
|
+
}
|
|
20009
|
+
});
|
|
20010
|
+
} catch {
|
|
20011
|
+
host.remove();
|
|
20012
|
+
resolve(null);
|
|
20013
|
+
return;
|
|
20014
|
+
}
|
|
20015
|
+
armTimeout();
|
|
20016
|
+
});
|
|
20017
|
+
}
|
|
20018
|
+
function mountCheckoutChallenge(container, challenge, callbacks) {
|
|
20019
|
+
if (challenge.provider !== "turnstile" || !challenge.siteKey.trim()) {
|
|
20020
|
+
throw new Error("Checkout challenge is invalid.");
|
|
20021
|
+
}
|
|
20022
|
+
const win = container.ownerDocument.defaultView;
|
|
20023
|
+
if (!win) throw new Error("Checkout challenge requires a browser document.");
|
|
20024
|
+
const checkoutBaseUrl = getConfig().checkoutBaseUrl;
|
|
20025
|
+
const frameUrl = new URL("/turnstile", checkoutBaseUrl);
|
|
20026
|
+
if (frameUrl.protocol !== "https:" && frameUrl.protocol !== "http:") {
|
|
20027
|
+
throw new Error("Checkout base URL must use http or https.");
|
|
20028
|
+
}
|
|
20029
|
+
const nonce = win.crypto.randomUUID();
|
|
20030
|
+
frameUrl.searchParams.set("site_key", challenge.siteKey.trim());
|
|
20031
|
+
frameUrl.searchParams.set("nonce", nonce);
|
|
20032
|
+
const frame = container.ownerDocument.createElement("iframe");
|
|
20033
|
+
frame.src = frameUrl.toString();
|
|
20034
|
+
frame.title = "Checkout verification";
|
|
20035
|
+
frame.referrerPolicy = "no-referrer";
|
|
20036
|
+
frame.style.border = "0";
|
|
20037
|
+
frame.style.width = "100%";
|
|
20038
|
+
frame.style.height = "0";
|
|
20039
|
+
let disposed = false;
|
|
20040
|
+
let ready = false;
|
|
20041
|
+
const readyTimeout = win.setTimeout(() => {
|
|
20042
|
+
if (!disposed && !ready) callbacks.onUnavailable?.();
|
|
20043
|
+
}, TURNSTILE_FRAME_READY_TIMEOUT_MS);
|
|
20044
|
+
const onMessage = (event) => {
|
|
20045
|
+
if (disposed || event.origin !== frameUrl.origin || event.source !== frame.contentWindow) return;
|
|
20046
|
+
const message = readFrameMessage(event.data, nonce);
|
|
20047
|
+
if (!message) return;
|
|
20048
|
+
ready = true;
|
|
20049
|
+
win.clearTimeout(readyTimeout);
|
|
20050
|
+
if (message.type === "visible") {
|
|
20051
|
+
frame.style.height = "72px";
|
|
20052
|
+
callbacks.onVisibilityChange?.(true);
|
|
20053
|
+
}
|
|
20054
|
+
if (message.type === "hidden") {
|
|
20055
|
+
frame.style.height = "0";
|
|
20056
|
+
callbacks.onVisibilityChange?.(false);
|
|
20057
|
+
}
|
|
20058
|
+
if (message.type === "success") {
|
|
20059
|
+
frame.style.height = "0";
|
|
20060
|
+
callbacks.onVisibilityChange?.(false);
|
|
20061
|
+
callbacks.onSuccess(message.token);
|
|
20062
|
+
}
|
|
20063
|
+
if (message.type === "expired" || message.type === "timeout") callbacks.onExpired?.();
|
|
20064
|
+
if (message.type === "error") callbacks.onUnavailable?.();
|
|
20065
|
+
};
|
|
20066
|
+
const onFrameError = () => callbacks.onUnavailable?.();
|
|
20067
|
+
win.addEventListener("message", onMessage);
|
|
20068
|
+
frame.addEventListener("error", onFrameError, { once: true });
|
|
20069
|
+
container.appendChild(frame);
|
|
20070
|
+
return {
|
|
20071
|
+
element: frame,
|
|
20072
|
+
dispose() {
|
|
20073
|
+
if (disposed) return;
|
|
20074
|
+
disposed = true;
|
|
20075
|
+
win.clearTimeout(readyTimeout);
|
|
20076
|
+
win.removeEventListener("message", onMessage);
|
|
20077
|
+
frame.removeEventListener("error", onFrameError);
|
|
20078
|
+
frame.remove();
|
|
20079
|
+
}
|
|
20080
|
+
};
|
|
20081
|
+
}
|
|
20082
|
+
|
|
19937
20083
|
// ../sdk/src/modules/checkout.ts
|
|
19938
20084
|
var CheckoutCreateError = class _CheckoutCreateError extends Error {
|
|
19939
20085
|
constructor(message, options = {}) {
|
|
@@ -20176,6 +20322,16 @@ function mapCartItemsForApi(items) {
|
|
|
20176
20322
|
}
|
|
20177
20323
|
async function checkout(couponOrOptions, options) {
|
|
20178
20324
|
const resolvedOptions = resolveCheckoutOptions(couponOrOptions, options);
|
|
20325
|
+
const firstAttempt = await performCheckout(resolvedOptions);
|
|
20326
|
+
if (!firstAttempt.success && firstAttempt.challenge?.provider === "turnstile" && !resolvedOptions.turnstileToken && typeof document !== "undefined") {
|
|
20327
|
+
const proof = await resolveCheckoutChallengeProof(firstAttempt.challenge);
|
|
20328
|
+
if (proof) {
|
|
20329
|
+
return performCheckout({ ...resolvedOptions, turnstileToken: proof });
|
|
20330
|
+
}
|
|
20331
|
+
}
|
|
20332
|
+
return firstAttempt;
|
|
20333
|
+
}
|
|
20334
|
+
async function performCheckout(resolvedOptions) {
|
|
20179
20335
|
const { autoRedirect = true, email: email3 } = resolvedOptions;
|
|
20180
20336
|
const checkoutCodes = resolveCheckoutCodes(resolvedOptions);
|
|
20181
20337
|
const normalizedCoupon = checkoutCodes.coupon;
|
|
@@ -20293,6 +20449,19 @@ async function checkout(couponOrOptions, options) {
|
|
|
20293
20449
|
}
|
|
20294
20450
|
async function buildCheckoutUrl(couponOrOptions, options) {
|
|
20295
20451
|
const resolvedOptions = resolveCheckoutOptions(couponOrOptions, options);
|
|
20452
|
+
try {
|
|
20453
|
+
return await performBuildCheckoutUrl(resolvedOptions);
|
|
20454
|
+
} catch (error51) {
|
|
20455
|
+
if (error51 instanceof CheckoutCreateError && error51.challenge?.provider === "turnstile" && !resolvedOptions.turnstileToken && typeof document !== "undefined") {
|
|
20456
|
+
const proof = await resolveCheckoutChallengeProof(error51.challenge);
|
|
20457
|
+
if (proof) {
|
|
20458
|
+
return performBuildCheckoutUrl({ ...resolvedOptions, turnstileToken: proof });
|
|
20459
|
+
}
|
|
20460
|
+
}
|
|
20461
|
+
throw error51;
|
|
20462
|
+
}
|
|
20463
|
+
}
|
|
20464
|
+
async function performBuildCheckoutUrl(resolvedOptions) {
|
|
20296
20465
|
const { email: email3 } = resolvedOptions;
|
|
20297
20466
|
const checkoutCodes = resolveCheckoutCodes(resolvedOptions);
|
|
20298
20467
|
const normalizedCoupon = checkoutCodes.coupon;
|
|
@@ -20381,80 +20550,6 @@ function buildCheckoutUrlSync() {
|
|
|
20381
20550
|
throw new Error("buildCheckoutUrlSync is deprecated. Use buildCheckoutUrl (async) instead.");
|
|
20382
20551
|
}
|
|
20383
20552
|
|
|
20384
|
-
// ../sdk/src/modules/checkout-challenge.ts
|
|
20385
|
-
var TURNSTILE_FRAME_MESSAGE_SOURCE = "shoppex-turnstile";
|
|
20386
|
-
var TURNSTILE_FRAME_MESSAGE_VERSION = 1;
|
|
20387
|
-
var TURNSTILE_FRAME_READY_TIMEOUT_MS = 1e4;
|
|
20388
|
-
function readFrameMessage(value, nonce) {
|
|
20389
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
20390
|
-
const record2 = value;
|
|
20391
|
-
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;
|
|
20392
|
-
if (record2.type === "success" && (typeof record2.token !== "string" || !record2.token.trim())) {
|
|
20393
|
-
return null;
|
|
20394
|
-
}
|
|
20395
|
-
return {
|
|
20396
|
-
type: record2.type,
|
|
20397
|
-
...typeof record2.token === "string" ? { token: record2.token.trim() } : {}
|
|
20398
|
-
};
|
|
20399
|
-
}
|
|
20400
|
-
function mountCheckoutChallenge(container, challenge, callbacks) {
|
|
20401
|
-
if (challenge.provider !== "turnstile" || !challenge.siteKey.trim()) {
|
|
20402
|
-
throw new Error("Checkout challenge is invalid.");
|
|
20403
|
-
}
|
|
20404
|
-
const win = container.ownerDocument.defaultView;
|
|
20405
|
-
if (!win) throw new Error("Checkout challenge requires a browser document.");
|
|
20406
|
-
const checkoutBaseUrl = getConfig().checkoutBaseUrl;
|
|
20407
|
-
const frameUrl = new URL("/turnstile", checkoutBaseUrl);
|
|
20408
|
-
if (frameUrl.protocol !== "https:" && frameUrl.protocol !== "http:") {
|
|
20409
|
-
throw new Error("Checkout base URL must use http or https.");
|
|
20410
|
-
}
|
|
20411
|
-
const nonce = win.crypto.randomUUID();
|
|
20412
|
-
frameUrl.searchParams.set("site_key", challenge.siteKey.trim());
|
|
20413
|
-
frameUrl.searchParams.set("nonce", nonce);
|
|
20414
|
-
const frame = container.ownerDocument.createElement("iframe");
|
|
20415
|
-
frame.src = frameUrl.toString();
|
|
20416
|
-
frame.title = "Checkout verification";
|
|
20417
|
-
frame.referrerPolicy = "no-referrer";
|
|
20418
|
-
frame.style.border = "0";
|
|
20419
|
-
frame.style.width = "100%";
|
|
20420
|
-
frame.style.height = "0";
|
|
20421
|
-
let disposed = false;
|
|
20422
|
-
let ready = false;
|
|
20423
|
-
const readyTimeout = win.setTimeout(() => {
|
|
20424
|
-
if (!disposed && !ready) callbacks.onUnavailable?.();
|
|
20425
|
-
}, TURNSTILE_FRAME_READY_TIMEOUT_MS);
|
|
20426
|
-
const onMessage = (event) => {
|
|
20427
|
-
if (disposed || event.origin !== frameUrl.origin || event.source !== frame.contentWindow) return;
|
|
20428
|
-
const message = readFrameMessage(event.data, nonce);
|
|
20429
|
-
if (!message) return;
|
|
20430
|
-
ready = true;
|
|
20431
|
-
win.clearTimeout(readyTimeout);
|
|
20432
|
-
if (message.type === "visible") frame.style.height = "72px";
|
|
20433
|
-
if (message.type === "hidden") frame.style.height = "0";
|
|
20434
|
-
if (message.type === "success") {
|
|
20435
|
-
frame.style.height = "0";
|
|
20436
|
-
callbacks.onSuccess(message.token);
|
|
20437
|
-
}
|
|
20438
|
-
if (message.type === "expired" || message.type === "timeout") callbacks.onExpired?.();
|
|
20439
|
-
if (message.type === "error") callbacks.onUnavailable?.();
|
|
20440
|
-
};
|
|
20441
|
-
const onFrameError = () => callbacks.onUnavailable?.();
|
|
20442
|
-
win.addEventListener("message", onMessage);
|
|
20443
|
-
frame.addEventListener("error", onFrameError, { once: true });
|
|
20444
|
-
container.appendChild(frame);
|
|
20445
|
-
return {
|
|
20446
|
-
element: frame,
|
|
20447
|
-
dispose() {
|
|
20448
|
-
if (disposed) return;
|
|
20449
|
-
disposed = true;
|
|
20450
|
-
win.clearTimeout(readyTimeout);
|
|
20451
|
-
win.removeEventListener("message", onMessage);
|
|
20452
|
-
frame.removeEventListener("error", onFrameError);
|
|
20453
|
-
frame.remove();
|
|
20454
|
-
}
|
|
20455
|
-
};
|
|
20456
|
-
}
|
|
20457
|
-
|
|
20458
20553
|
// ../sdk/src/modules/coupons.ts
|
|
20459
20554
|
async function resolveShopId() {
|
|
20460
20555
|
const cachedShopId2 = getShopId();
|