@tokenoftrust/storefront-runner 4.6.2 → 4.6.4
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/agency-kit/tools/lib/catalog-model/sku.d.mts +1 -1
- package/agency-kit/tools/lib/catalog-model/sku.mjs +7 -2
- package/apps/storefront/src/lib/checkout/shared/lazyCheckoutLoader.ts +24 -6
- package/package.json +1 -1
- package/packages/public-runtime/src/integration/contact-sales-action-source.ts +5 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export const SKU_MAX_LENGTH: number;
|
|
2
2
|
export const CHECKOUT_CODE_MAX_LENGTH: number;
|
|
3
3
|
export const SKU_TOKEN_MAX_LENGTH: number;
|
|
4
|
-
export type SkuRefusal = "not-string" | "empty" | "control-character" | "too-long";
|
|
4
|
+
export type SkuRefusal = "not-string" | "empty" | "control-character" | "ill-formed" | "too-long";
|
|
5
5
|
export function parseSku(value: unknown): { ok: true; sku: string } | { ok: false; reason: SkuRefusal };
|
|
6
6
|
export function isStoredSku(value: unknown): value is string;
|
|
7
7
|
export function skuKey(sku: string): string;
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* A SKU is merchant data, kept exactly as the merchant wrote it. The only rewrite is trimming
|
|
5
5
|
* leading and trailing whitespace on the way in. A SKU is refused only when it is empty, carries a
|
|
6
|
-
* control character,
|
|
6
|
+
* control character, is not well-formed Unicode (a lone UTF-16 surrogate, which has no UTF-8 form
|
|
7
|
+
* and so no token), or is longer than {@link SKU_MAX_LENGTH} characters. Anything else, a space
|
|
7
8
|
* included, is a valid SKU; a space is a lint warning, never an error.
|
|
8
9
|
*
|
|
9
10
|
* Two SKUs are the same SKU when their {@link skuKey} is equal: trimmed, NBSP read as a space, and
|
|
@@ -25,6 +26,8 @@ export const CHECKOUT_CODE_MAX_LENGTH = 200;
|
|
|
25
26
|
export const SKU_TOKEN_MAX_LENGTH = SKU_MAX_LENGTH * 12;
|
|
26
27
|
|
|
27
28
|
const CONTROL = /\p{Cc}/u;
|
|
29
|
+
/** A lone surrogate: in `u` mode a paired surrogate is one code point and never matches. */
|
|
30
|
+
const LONE_SURROGATE = /\p{Cs}/u;
|
|
28
31
|
const WHITESPACE = /\s/u;
|
|
29
32
|
const TOKEN_SAFE = /^[A-Za-z0-9._-]$/;
|
|
30
33
|
const TOKEN = /^(?:[A-Za-z0-9._-]|%[0-9A-F]{2})+$/;
|
|
@@ -33,13 +36,14 @@ const TOKEN = /^(?:[A-Za-z0-9._-]|%[0-9A-F]{2})+$/;
|
|
|
33
36
|
* Read a SKU as it arrives from a merchant, a file or an import.
|
|
34
37
|
*
|
|
35
38
|
* @param {unknown} value
|
|
36
|
-
* @returns {{ ok: true, sku: string } | { ok: false, reason: "not-string" | "empty" | "control-character" | "too-long" }}
|
|
39
|
+
* @returns {{ ok: true, sku: string } | { ok: false, reason: "not-string" | "empty" | "control-character" | "ill-formed" | "too-long" }}
|
|
37
40
|
*/
|
|
38
41
|
export function parseSku(value) {
|
|
39
42
|
if (typeof value !== "string") return { ok: false, reason: "not-string" };
|
|
40
43
|
const sku = value.trim();
|
|
41
44
|
if (sku === "") return { ok: false, reason: "empty" };
|
|
42
45
|
if (CONTROL.test(sku)) return { ok: false, reason: "control-character" };
|
|
46
|
+
if (LONE_SURROGATE.test(sku)) return { ok: false, reason: "ill-formed" };
|
|
43
47
|
if ([...sku].length > SKU_MAX_LENGTH) return { ok: false, reason: "too-long" };
|
|
44
48
|
return { ok: true, sku };
|
|
45
49
|
}
|
|
@@ -92,6 +96,7 @@ const REASONS = {
|
|
|
92
96
|
"not-string": "a SKU must be text",
|
|
93
97
|
empty: "a SKU cannot be empty",
|
|
94
98
|
"control-character": "a SKU cannot contain a control character (tab, newline, ...)",
|
|
99
|
+
"ill-formed": "a SKU must be well-formed Unicode text (it contains a broken character)",
|
|
95
100
|
"too-long": `a SKU is at most ${SKU_MAX_LENGTH} characters`,
|
|
96
101
|
};
|
|
97
102
|
|
|
@@ -165,7 +165,13 @@ export function lazyCheckoutLoaderJs(options: LazyCheckoutLoaderOptions): string
|
|
|
165
165
|
document.addEventListener("visibilitychange", function () {
|
|
166
166
|
if (document.visibilityState === "hidden") endCartNavigation();
|
|
167
167
|
});
|
|
168
|
+
// A shopper action belongs to the page lifecycle it was taken in. Leaving the page
|
|
169
|
+
// starts a new one, so an action whose async work (session, gate, drawer) settles
|
|
170
|
+
// after a back-forward-cache restore is abandoned rather than replayed.
|
|
171
|
+
var lifecycle = 0;
|
|
172
|
+
function current(gen) { return gen === lifecycle; }
|
|
168
173
|
window.addEventListener("pagehide", function () {
|
|
174
|
+
lifecycle++;
|
|
169
175
|
endCartNavigation();
|
|
170
176
|
dropHeld();
|
|
171
177
|
});
|
|
@@ -389,7 +395,9 @@ export function lazyCheckoutLoaderJs(options: LazyCheckoutLoaderOptions): string
|
|
|
389
395
|
}
|
|
390
396
|
if (event.defaultPrevented) return;
|
|
391
397
|
event.preventDefault();
|
|
398
|
+
var gen = lifecycle;
|
|
392
399
|
whenReady(anchor, function () {
|
|
400
|
+
if (!current(gen)) return;
|
|
393
401
|
stampAnchor(anchor);
|
|
394
402
|
replay(anchor, function () { anchor.click(); });
|
|
395
403
|
});
|
|
@@ -427,8 +435,9 @@ export function lazyCheckoutLoaderJs(options: LazyCheckoutLoaderOptions): string
|
|
|
427
435
|
note.textContent = ${literal(CART_UNAVAILABLE_MESSAGE)};
|
|
428
436
|
form.insertAdjacentElement('afterend', note);
|
|
429
437
|
}
|
|
430
|
-
function openSessionThen(form, fn) {
|
|
438
|
+
function openSessionThen(form, gen, fn) {
|
|
431
439
|
ensureSession().then(function (id) {
|
|
440
|
+
if (!current(gen)) return;
|
|
432
441
|
if (!id) { showCartUnavailable(form); return; }
|
|
433
442
|
form.dataset.cartSession = '1';
|
|
434
443
|
fn();
|
|
@@ -441,6 +450,10 @@ export function lazyCheckoutLoaderJs(options: LazyCheckoutLoaderOptions): string
|
|
|
441
450
|
var managed = form.matches(${managedCart});
|
|
442
451
|
var cartForm = targetsCart(form.getAttribute('action'));
|
|
443
452
|
if (!managed && !cartForm) return;
|
|
453
|
+
var gen = lifecycle;
|
|
454
|
+
function resubmitLive() {
|
|
455
|
+
if (current(gen)) replay(form, function () { resubmit(form); });
|
|
456
|
+
}
|
|
444
457
|
if (form.dataset.cartSession === '1') {
|
|
445
458
|
if (!cartForm || settled()) {
|
|
446
459
|
if (!event.defaultPrevented && settled()) {
|
|
@@ -455,18 +468,23 @@ export function lazyCheckoutLoaderJs(options: LazyCheckoutLoaderOptions): string
|
|
|
455
468
|
return;
|
|
456
469
|
}
|
|
457
470
|
event.preventDefault();
|
|
458
|
-
whenReady(form, function () {
|
|
471
|
+
whenReady(form, function () {
|
|
472
|
+
if (!current(gen)) return;
|
|
473
|
+
stampForm(form);
|
|
474
|
+
resubmitLive();
|
|
475
|
+
});
|
|
459
476
|
return;
|
|
460
477
|
}
|
|
461
478
|
event.preventDefault();
|
|
462
479
|
function afterGate() {
|
|
463
|
-
|
|
480
|
+
if (!current(gen)) return;
|
|
481
|
+
openSessionThen(form, gen, function () {
|
|
464
482
|
if (cartForm) {
|
|
465
483
|
stampForm(form);
|
|
466
|
-
whenReady(form,
|
|
484
|
+
whenReady(form, resubmitLive);
|
|
467
485
|
return;
|
|
468
486
|
}
|
|
469
|
-
resubmit(form);
|
|
487
|
+
if (current(gen)) resubmit(form);
|
|
470
488
|
});
|
|
471
489
|
}
|
|
472
490
|
if (storeNotPublic && managed && form.dataset.checkoutReachable !== '1') {
|
|
@@ -481,7 +499,7 @@ export function lazyCheckoutLoaderJs(options: LazyCheckoutLoaderOptions): string
|
|
|
481
499
|
}).then(function (res) {
|
|
482
500
|
if (res.status === 401 && res.headers.get('x-tot-gate') === 'store-not-public') {
|
|
483
501
|
return res.json().then(function (body) { return body && body.signIn; }, function () { return null; })
|
|
484
|
-
.then(function (signIn) { showUnavailable(form, signIn); });
|
|
502
|
+
.then(function (signIn) { if (current(gen)) showUnavailable(form, signIn); });
|
|
485
503
|
}
|
|
486
504
|
form.dataset.checkoutReachable = '1';
|
|
487
505
|
afterGate();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tokenoftrust/storefront-runner",
|
|
3
|
-
"version": "4.6.
|
|
3
|
+
"version": "4.6.4",
|
|
4
4
|
"license": "SEE LICENSE IN LICENSE",
|
|
5
5
|
"description": "World-shareable storefront runner: multi-tenant renderer on Astro/Cloudflare. No control plane.",
|
|
6
6
|
"packageManager": "pnpm@11.9.0",
|
|
@@ -86,6 +86,10 @@ export function createContactSalesActionSource(
|
|
|
86
86
|
subscribe(listener) {
|
|
87
87
|
if (!document || typeof document.querySelectorAll !== "function") return () => {};
|
|
88
88
|
const removals: Array<() => void> = [];
|
|
89
|
+
// A deferred navigation belongs to this subscription. Teardown (pagehide) abandons
|
|
90
|
+
// it, so a back-forward-cache restore never resumes a navigation the shopper left.
|
|
91
|
+
let live = true;
|
|
92
|
+
removals.push(() => { live = false; });
|
|
89
93
|
const bound = new Set<ContactSalesActionElement>();
|
|
90
94
|
for (const source of CONTACT_SALES_ACTION_SOURCES) {
|
|
91
95
|
const nodes = document.querySelectorAll(source.selector);
|
|
@@ -110,7 +114,7 @@ export function createContactSalesActionSource(
|
|
|
110
114
|
// A full-document navigation destroys the isolated GA4 iframe. Give its
|
|
111
115
|
// MessagePort one bounded turn to receive the reviewed action first.
|
|
112
116
|
if (navigationHref) {
|
|
113
|
-
schedule(() => options.navigate?.(navigationHref), CONTACT_SALES_NAVIGATION_DEFER_MS);
|
|
117
|
+
schedule(() => { if (live) options.navigate?.(navigationHref); }, CONTACT_SALES_NAVIGATION_DEFER_MS);
|
|
114
118
|
}
|
|
115
119
|
};
|
|
116
120
|
node.addEventListener("click", emit);
|