@patientos/website-kit 0.2.5 → 0.2.6
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/README.md +34 -0
- package/dist/{checkout-block-Cog4H4I4.d.ts → checkout-block-BdpPIuQ5.d.ts} +23 -9
- package/dist/{chunk-75FJ5AJB.js → chunk-7ZOQ6UKG.js} +1790 -235
- package/dist/{chunk-FSZ726DM.js → chunk-ZYX4TXBN.js} +2 -0
- package/dist/{chunk-7UPUEMXX.js → chunk-ZZFBTLR4.js} +1 -1
- package/dist/index.d.ts +31 -4
- package/dist/index.js +39 -14
- package/dist/islands-impl.d.ts +4 -4
- package/dist/islands-impl.js +2 -2
- package/dist/islands-registry.js +3 -3
- package/dist/portal-booking-client.js +1 -1
- package/dist/website-kit.css +3 -1
- package/package.json +1 -1
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
createPortalClient,
|
|
28
28
|
describedBy,
|
|
29
29
|
effectiveFee,
|
|
30
|
+
encodePortalReturnTarget,
|
|
30
31
|
errorCode,
|
|
31
32
|
errorMessage,
|
|
32
33
|
feeNote,
|
|
@@ -48,6 +49,7 @@ import {
|
|
|
48
49
|
isSessionExpired,
|
|
49
50
|
isSlotTakenError,
|
|
50
51
|
loadFlowRecord,
|
|
52
|
+
normalizePortalApiOrigin,
|
|
51
53
|
observeSessionExpiry,
|
|
52
54
|
once,
|
|
53
55
|
patientos,
|
|
@@ -65,7 +67,7 @@ import {
|
|
|
65
67
|
signOutOfPortal,
|
|
66
68
|
writePortalSignedInHint,
|
|
67
69
|
zoneAbbr
|
|
68
|
-
} from "./chunk-
|
|
70
|
+
} from "./chunk-ZYX4TXBN.js";
|
|
69
71
|
import {
|
|
70
72
|
__export
|
|
71
73
|
} from "./chunk-MLKGABMK.js";
|
|
@@ -12067,6 +12069,25 @@ var EMPTY_CART = Object.freeze({
|
|
|
12067
12069
|
v: 1,
|
|
12068
12070
|
lines: Object.freeze([])
|
|
12069
12071
|
});
|
|
12072
|
+
function cartLineId(value) {
|
|
12073
|
+
if (typeof value !== "string") return null;
|
|
12074
|
+
const normalized = value.trim();
|
|
12075
|
+
return normalized.length > 0 && normalized.length <= 128 ? normalized : null;
|
|
12076
|
+
}
|
|
12077
|
+
function legacyCartLineId(kind, variantId) {
|
|
12078
|
+
const input = `${kind}\0${variantId}`;
|
|
12079
|
+
let first = 2166136261;
|
|
12080
|
+
let second = 2246822507;
|
|
12081
|
+
for (let index = 0; index < input.length; index += 1) {
|
|
12082
|
+
const code2 = input.charCodeAt(index);
|
|
12083
|
+
first = Math.imul(first ^ code2, 16777619);
|
|
12084
|
+
second = Math.imul(second ^ code2, 3266489909);
|
|
12085
|
+
}
|
|
12086
|
+
return `legacy-${(first >>> 0).toString(16).padStart(8, "0")}${(second >>> 0).toString(16).padStart(8, "0")}`;
|
|
12087
|
+
}
|
|
12088
|
+
function newCartLineId() {
|
|
12089
|
+
return crypto.randomUUID();
|
|
12090
|
+
}
|
|
12070
12091
|
function storage() {
|
|
12071
12092
|
try {
|
|
12072
12093
|
if (typeof window === "undefined" || !window.localStorage) return null;
|
|
@@ -12084,7 +12105,39 @@ function parseLine(raw) {
|
|
|
12084
12105
|
const n = typeof line.quantity === "number" ? line.quantity : Number(line.quantity);
|
|
12085
12106
|
if (!Number.isFinite(n)) return null;
|
|
12086
12107
|
const quantity = Math.min(Math.max(Math.floor(n), 1), MAX_QUANTITY);
|
|
12087
|
-
|
|
12108
|
+
const lineId = cartLineId(line.lineId) ?? legacyCartLineId(kind, variantId);
|
|
12109
|
+
return { lineId, kind, variantId, quantity };
|
|
12110
|
+
}
|
|
12111
|
+
function normalizeCartLines(entries) {
|
|
12112
|
+
const lines = [];
|
|
12113
|
+
const byIdentity = /* @__PURE__ */ new Map();
|
|
12114
|
+
for (const entry of entries) {
|
|
12115
|
+
const line = parseLine(entry);
|
|
12116
|
+
if (!line) continue;
|
|
12117
|
+
const identity = `${line.kind}\0${line.variantId}`;
|
|
12118
|
+
const existing = byIdentity.get(identity);
|
|
12119
|
+
if (existing) {
|
|
12120
|
+
existing.quantity = Math.min(
|
|
12121
|
+
existing.quantity + line.quantity,
|
|
12122
|
+
MAX_QUANTITY
|
|
12123
|
+
);
|
|
12124
|
+
continue;
|
|
12125
|
+
}
|
|
12126
|
+
if (lines.length >= MAX_LINES) continue;
|
|
12127
|
+
lines.push(line);
|
|
12128
|
+
byIdentity.set(identity, line);
|
|
12129
|
+
}
|
|
12130
|
+
return lines;
|
|
12131
|
+
}
|
|
12132
|
+
function normalizeSettledAttemptTokens(entries) {
|
|
12133
|
+
if (!Array.isArray(entries)) return [];
|
|
12134
|
+
return Array.from(
|
|
12135
|
+
new Set(
|
|
12136
|
+
entries.filter(
|
|
12137
|
+
(entry) => typeof entry === "string" && entry.length > 0 && entry.length <= 128
|
|
12138
|
+
)
|
|
12139
|
+
)
|
|
12140
|
+
).slice(-32);
|
|
12088
12141
|
}
|
|
12089
12142
|
function readCart() {
|
|
12090
12143
|
const store = storage();
|
|
@@ -12098,75 +12151,216 @@ function readCart() {
|
|
|
12098
12151
|
if (!raw) return emptyCart();
|
|
12099
12152
|
try {
|
|
12100
12153
|
const parsed = JSON.parse(raw);
|
|
12101
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
12154
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
12155
|
+
return emptyCart();
|
|
12102
12156
|
const bag = parsed;
|
|
12103
12157
|
if (bag.v !== 1 || !Array.isArray(bag.lines)) return emptyCart();
|
|
12104
|
-
const
|
|
12105
|
-
|
|
12106
|
-
|
|
12107
|
-
|
|
12108
|
-
|
|
12109
|
-
|
|
12158
|
+
const settledAttemptTokens = normalizeSettledAttemptTokens(
|
|
12159
|
+
bag.settledAttemptTokens
|
|
12160
|
+
);
|
|
12161
|
+
return {
|
|
12162
|
+
v: 1,
|
|
12163
|
+
lines: normalizeCartLines(bag.lines),
|
|
12164
|
+
...settledAttemptTokens.length > 0 ? { settledAttemptTokens } : {}
|
|
12165
|
+
};
|
|
12110
12166
|
} catch {
|
|
12111
12167
|
return emptyCart();
|
|
12112
12168
|
}
|
|
12113
12169
|
}
|
|
12114
|
-
|
|
12115
|
-
|
|
12170
|
+
var CART_MUTATION_LOCK = "patientos-cart-mutation-v1";
|
|
12171
|
+
var CART_MUTATION_VALIDATION_TIMEOUT_MS = 5e3;
|
|
12172
|
+
function persistCart(cart) {
|
|
12173
|
+
const settledAttemptTokens = normalizeSettledAttemptTokens(
|
|
12174
|
+
cart.settledAttemptTokens
|
|
12175
|
+
);
|
|
12176
|
+
const normalised = {
|
|
12177
|
+
v: 1,
|
|
12178
|
+
lines: normalizeCartLines(cart.lines),
|
|
12179
|
+
...settledAttemptTokens.length > 0 ? { settledAttemptTokens } : {}
|
|
12180
|
+
};
|
|
12116
12181
|
const store = storage();
|
|
12117
|
-
if (store)
|
|
12118
|
-
|
|
12119
|
-
|
|
12120
|
-
|
|
12121
|
-
|
|
12182
|
+
if (!store)
|
|
12183
|
+
return {
|
|
12184
|
+
cart: readCart(),
|
|
12185
|
+
persisted: false,
|
|
12186
|
+
status: "cart_storage_unavailable"
|
|
12187
|
+
};
|
|
12188
|
+
try {
|
|
12189
|
+
store.setItem(CART_STORAGE_KEY, JSON.stringify(normalised));
|
|
12190
|
+
} catch {
|
|
12191
|
+
return {
|
|
12192
|
+
cart: readCart(),
|
|
12193
|
+
persisted: false,
|
|
12194
|
+
status: "cart_storage_unavailable"
|
|
12195
|
+
};
|
|
12122
12196
|
}
|
|
12123
12197
|
notify(normalised);
|
|
12124
|
-
return normalised;
|
|
12125
|
-
}
|
|
12126
|
-
function
|
|
12127
|
-
|
|
12128
|
-
|
|
12129
|
-
|
|
12130
|
-
|
|
12131
|
-
|
|
12132
|
-
|
|
12133
|
-
|
|
12134
|
-
|
|
12198
|
+
return { cart: normalised, persisted: true, status: "updated" };
|
|
12199
|
+
}
|
|
12200
|
+
function unavailableMutation() {
|
|
12201
|
+
return {
|
|
12202
|
+
cart: readCart(),
|
|
12203
|
+
persisted: false,
|
|
12204
|
+
status: "cart_storage_unavailable"
|
|
12205
|
+
};
|
|
12206
|
+
}
|
|
12207
|
+
async function mutateCart(mutation, beforePersist) {
|
|
12208
|
+
const locks = typeof navigator === "undefined" ? void 0 : navigator.locks;
|
|
12209
|
+
if (!storage() || !locks) return unavailableMutation();
|
|
12210
|
+
try {
|
|
12211
|
+
return await locks.request(CART_MUTATION_LOCK, async () => {
|
|
12212
|
+
const latest = readCart();
|
|
12213
|
+
const mutated = mutation(latest);
|
|
12214
|
+
const proposed = {
|
|
12215
|
+
...mutated,
|
|
12216
|
+
lines: normalizeCartLines(mutated.lines),
|
|
12217
|
+
...mutated.settledAttemptTokens === void 0 && latest.settledAttemptTokens ? { settledAttemptTokens: latest.settledAttemptTokens } : {}
|
|
12218
|
+
};
|
|
12219
|
+
if (beforePersist) {
|
|
12220
|
+
const controller = new AbortController();
|
|
12221
|
+
let timer;
|
|
12222
|
+
const accepted = await Promise.race([
|
|
12223
|
+
beforePersist(proposed, controller.signal).catch(() => false),
|
|
12224
|
+
new Promise((resolve) => {
|
|
12225
|
+
timer = setTimeout(() => {
|
|
12226
|
+
controller.abort();
|
|
12227
|
+
resolve(false);
|
|
12228
|
+
}, CART_MUTATION_VALIDATION_TIMEOUT_MS);
|
|
12229
|
+
})
|
|
12230
|
+
]);
|
|
12231
|
+
if (timer) clearTimeout(timer);
|
|
12232
|
+
if (!accepted) {
|
|
12233
|
+
return {
|
|
12234
|
+
cart: readCart(),
|
|
12235
|
+
persisted: false,
|
|
12236
|
+
status: "not_committed"
|
|
12237
|
+
};
|
|
12238
|
+
}
|
|
12239
|
+
}
|
|
12240
|
+
return persistCart(proposed);
|
|
12241
|
+
});
|
|
12242
|
+
} catch {
|
|
12243
|
+
return unavailableMutation();
|
|
12135
12244
|
}
|
|
12136
|
-
return writeCart(cart);
|
|
12137
12245
|
}
|
|
12138
|
-
function
|
|
12139
|
-
const
|
|
12140
|
-
|
|
12141
|
-
|
|
12142
|
-
|
|
12143
|
-
|
|
12144
|
-
|
|
12145
|
-
|
|
12146
|
-
|
|
12246
|
+
async function withCartLock(operation) {
|
|
12247
|
+
const locks = typeof navigator === "undefined" ? void 0 : navigator.locks;
|
|
12248
|
+
if (!storage() || !locks)
|
|
12249
|
+
return { status: "cart_storage_unavailable", cart: readCart() };
|
|
12250
|
+
try {
|
|
12251
|
+
return await locks.request(CART_MUTATION_LOCK, async () => {
|
|
12252
|
+
const latest = readCart();
|
|
12253
|
+
const controller = new AbortController();
|
|
12254
|
+
let timer;
|
|
12255
|
+
const outcome = await Promise.race([
|
|
12256
|
+
operation(latest, controller.signal).then((result) => ({
|
|
12257
|
+
completed: true,
|
|
12258
|
+
result
|
|
12259
|
+
})),
|
|
12260
|
+
new Promise((resolve) => {
|
|
12261
|
+
timer = setTimeout(() => {
|
|
12262
|
+
controller.abort();
|
|
12263
|
+
resolve({ completed: false });
|
|
12264
|
+
}, CART_MUTATION_VALIDATION_TIMEOUT_MS);
|
|
12265
|
+
})
|
|
12266
|
+
]);
|
|
12267
|
+
if (timer) clearTimeout(timer);
|
|
12268
|
+
return outcome.completed ? { status: "completed", cart: latest, result: outcome.result } : { status: "not_completed", cart: latest };
|
|
12269
|
+
});
|
|
12270
|
+
} catch {
|
|
12271
|
+
return { status: "not_completed", cart: readCart() };
|
|
12272
|
+
}
|
|
12273
|
+
}
|
|
12274
|
+
async function addToCart(variantId, quantity = 1) {
|
|
12275
|
+
let status = "added";
|
|
12276
|
+
const result = await mutateCart((cart) => {
|
|
12277
|
+
const add2 = Math.min(Math.max(Math.floor(quantity), 1), MAX_QUANTITY);
|
|
12278
|
+
const existing = cart.lines.find(
|
|
12279
|
+
(line) => line.kind === "retail" && line.variantId === variantId
|
|
12280
|
+
);
|
|
12281
|
+
if (existing) {
|
|
12282
|
+
if (existing.quantity >= MAX_QUANTITY) {
|
|
12283
|
+
status = "quantity_limit";
|
|
12284
|
+
return cart;
|
|
12285
|
+
}
|
|
12286
|
+
existing.quantity = Math.min(existing.quantity + add2, MAX_QUANTITY);
|
|
12287
|
+
} else {
|
|
12288
|
+
if (cart.lines.length >= MAX_LINES) {
|
|
12289
|
+
status = "line_limit";
|
|
12290
|
+
return cart;
|
|
12291
|
+
}
|
|
12292
|
+
cart.lines.push({
|
|
12293
|
+
lineId: newCartLineId(),
|
|
12294
|
+
kind: "retail",
|
|
12295
|
+
variantId,
|
|
12296
|
+
quantity: add2
|
|
12297
|
+
});
|
|
12298
|
+
}
|
|
12299
|
+
return cart;
|
|
12147
12300
|
});
|
|
12301
|
+
return {
|
|
12302
|
+
cart: result.cart,
|
|
12303
|
+
status: result.status === "cart_storage_unavailable" ? result.status : status
|
|
12304
|
+
};
|
|
12148
12305
|
}
|
|
12149
|
-
function
|
|
12150
|
-
const
|
|
12151
|
-
|
|
12306
|
+
async function adjustCartQuantity(kind, variantId, delta, maximumBaseline) {
|
|
12307
|
+
const result = await mutateCart((cart) => ({
|
|
12308
|
+
v: 1,
|
|
12309
|
+
lines: cart.lines.flatMap((line) => {
|
|
12310
|
+
if (line.kind !== kind || line.variantId !== variantId) return [line];
|
|
12311
|
+
const baseline = maximumBaseline === void 0 ? line.quantity : Math.min(line.quantity, Math.max(0, Math.floor(maximumBaseline)));
|
|
12312
|
+
const quantity = Math.min(baseline + Math.floor(delta), MAX_QUANTITY);
|
|
12313
|
+
return quantity > 0 ? [{ ...line, quantity }] : [];
|
|
12314
|
+
})
|
|
12315
|
+
}));
|
|
12316
|
+
return result;
|
|
12152
12317
|
}
|
|
12153
|
-
function
|
|
12154
|
-
const
|
|
12155
|
-
|
|
12156
|
-
|
|
12157
|
-
|
|
12158
|
-
|
|
12159
|
-
|
|
12160
|
-
|
|
12161
|
-
|
|
12162
|
-
|
|
12163
|
-
|
|
12318
|
+
async function removeFromCart(variantId, kind) {
|
|
12319
|
+
const result = await mutateCart((cart) => ({
|
|
12320
|
+
v: 1,
|
|
12321
|
+
lines: cart.lines.filter(
|
|
12322
|
+
(line) => line.variantId !== variantId || kind && line.kind !== kind
|
|
12323
|
+
)
|
|
12324
|
+
}));
|
|
12325
|
+
return result;
|
|
12326
|
+
}
|
|
12327
|
+
async function subtractPurchasedCartLines(purchasedLines, attemptToken) {
|
|
12328
|
+
const purchased = new Map(
|
|
12329
|
+
normalizeCartLines(
|
|
12330
|
+
purchasedLines.filter(
|
|
12331
|
+
(line) => Boolean(line) && typeof line === "object" && !Array.isArray(line) && cartLineId(line.lineId) !== null
|
|
12332
|
+
)
|
|
12333
|
+
).map((line) => [line.lineId, line])
|
|
12334
|
+
);
|
|
12335
|
+
return mutateCart((cart) => {
|
|
12336
|
+
if (attemptToken && cart.settledAttemptTokens?.includes(attemptToken))
|
|
12337
|
+
return cart;
|
|
12338
|
+
return {
|
|
12339
|
+
v: 1,
|
|
12340
|
+
lines: cart.lines.flatMap((line) => {
|
|
12341
|
+
const purchasedLine = purchased.get(line.lineId);
|
|
12342
|
+
const purchasedQuantity = purchasedLine?.kind === line.kind && purchasedLine.variantId === line.variantId ? purchasedLine.quantity : 0;
|
|
12343
|
+
const quantity = line.quantity - purchasedQuantity;
|
|
12344
|
+
return quantity > 0 ? [{ ...line, quantity }] : [];
|
|
12345
|
+
}),
|
|
12346
|
+
...attemptToken ? {
|
|
12347
|
+
settledAttemptTokens: [
|
|
12348
|
+
...cart.settledAttemptTokens ?? [],
|
|
12349
|
+
attemptToken
|
|
12350
|
+
]
|
|
12351
|
+
} : {}
|
|
12352
|
+
};
|
|
12353
|
+
});
|
|
12164
12354
|
}
|
|
12165
12355
|
function cartItemCount(cart = readCart()) {
|
|
12166
12356
|
return cart.lines.reduce((sum, l) => sum + l.quantity, 0);
|
|
12167
12357
|
}
|
|
12168
12358
|
function cartToWireLines(cart) {
|
|
12169
|
-
return cart.lines.map((l) => ({
|
|
12359
|
+
return cart.lines.map((l) => ({
|
|
12360
|
+
kind: l.kind,
|
|
12361
|
+
variantId: l.variantId,
|
|
12362
|
+
quantity: l.quantity
|
|
12363
|
+
}));
|
|
12170
12364
|
}
|
|
12171
12365
|
var listeners = /* @__PURE__ */ new Set();
|
|
12172
12366
|
function notify(cart) {
|
|
@@ -12183,10 +12377,76 @@ function subscribeToCart(fn) {
|
|
|
12183
12377
|
if (e.key !== null && e.key !== CART_STORAGE_KEY) return;
|
|
12184
12378
|
fn(readCart());
|
|
12185
12379
|
};
|
|
12186
|
-
if (typeof window !== "undefined")
|
|
12380
|
+
if (typeof window !== "undefined")
|
|
12381
|
+
window.addEventListener("storage", onStorage);
|
|
12187
12382
|
return () => {
|
|
12188
12383
|
listeners.delete(fn);
|
|
12189
|
-
if (typeof window !== "undefined")
|
|
12384
|
+
if (typeof window !== "undefined")
|
|
12385
|
+
window.removeEventListener("storage", onStorage);
|
|
12386
|
+
};
|
|
12387
|
+
}
|
|
12388
|
+
|
|
12389
|
+
// src/store-client.ts
|
|
12390
|
+
var STORE_REQUEST_INIT = {
|
|
12391
|
+
credentials: "include",
|
|
12392
|
+
cache: "no-store"
|
|
12393
|
+
};
|
|
12394
|
+
async function readResult(response) {
|
|
12395
|
+
let body;
|
|
12396
|
+
try {
|
|
12397
|
+
body = await response.json();
|
|
12398
|
+
} catch {
|
|
12399
|
+
return response.ok ? { ok: false, status: null, error: null, message: null } : { ok: false, status: response.status, error: null, message: null };
|
|
12400
|
+
}
|
|
12401
|
+
if (response.ok) return { ok: true, data: body };
|
|
12402
|
+
const failure = body;
|
|
12403
|
+
return {
|
|
12404
|
+
ok: false,
|
|
12405
|
+
status: response.status,
|
|
12406
|
+
error: typeof failure?.error === "string" ? failure.error : null,
|
|
12407
|
+
message: typeof failure?.message === "string" ? failure.message : null
|
|
12408
|
+
};
|
|
12409
|
+
}
|
|
12410
|
+
function createStoreClient(config2 = {}) {
|
|
12411
|
+
const rawOrigin = config2.apiOrigin?.trim() ?? "";
|
|
12412
|
+
const normalizedOrigin = rawOrigin ? normalizePortalApiOrigin(rawOrigin) : "";
|
|
12413
|
+
if (rawOrigin && !normalizedOrigin) {
|
|
12414
|
+
console.error("[store] ignoring a malformed store API origin", rawOrigin);
|
|
12415
|
+
}
|
|
12416
|
+
const apiOrigin = normalizedOrigin ?? "";
|
|
12417
|
+
const fetchImpl = config2.fetchImpl ?? ((input, init) => globalThis.fetch(input, init));
|
|
12418
|
+
const url = (path) => apiOrigin && path.startsWith("/") ? apiOrigin + path : path;
|
|
12419
|
+
async function request(path, init) {
|
|
12420
|
+
try {
|
|
12421
|
+
return await readResult(
|
|
12422
|
+
await fetchImpl(url(path), {
|
|
12423
|
+
...STORE_REQUEST_INIT,
|
|
12424
|
+
...init
|
|
12425
|
+
})
|
|
12426
|
+
);
|
|
12427
|
+
} catch {
|
|
12428
|
+
return { ok: false, status: null, error: null, message: null };
|
|
12429
|
+
}
|
|
12430
|
+
}
|
|
12431
|
+
return {
|
|
12432
|
+
apiOrigin,
|
|
12433
|
+
url,
|
|
12434
|
+
get: (path) => request(path, { headers: { accept: "application/json" } }),
|
|
12435
|
+
post: (path, body, options) => request(path, {
|
|
12436
|
+
signal: options?.signal,
|
|
12437
|
+
method: "POST",
|
|
12438
|
+
headers: {
|
|
12439
|
+
accept: "application/json",
|
|
12440
|
+
"content-type": "application/json"
|
|
12441
|
+
},
|
|
12442
|
+
body: JSON.stringify(body ?? {})
|
|
12443
|
+
}),
|
|
12444
|
+
signInHref(returnPath = "/checkout") {
|
|
12445
|
+
if (typeof window === "undefined") return `${apiOrigin}/portal/sign-in`;
|
|
12446
|
+
const target = new URL(returnPath, window.location.origin).toString();
|
|
12447
|
+
const redirect = `/portal/return?to=${encodePortalReturnTarget(target)}`;
|
|
12448
|
+
return `${apiOrigin}/portal/sign-in?redirect=${encodeURIComponent(redirect)}`;
|
|
12449
|
+
}
|
|
12190
12450
|
};
|
|
12191
12451
|
}
|
|
12192
12452
|
|
|
@@ -12197,7 +12457,17 @@ function formatMoney(value) {
|
|
|
12197
12457
|
if (!Number.isFinite(n)) return value;
|
|
12198
12458
|
return `$${n.toFixed(2)}`;
|
|
12199
12459
|
}
|
|
12200
|
-
function StoreClient({
|
|
12460
|
+
function StoreClient({
|
|
12461
|
+
categoryHandle,
|
|
12462
|
+
productHandle,
|
|
12463
|
+
storeApiOrigin,
|
|
12464
|
+
title,
|
|
12465
|
+
columns
|
|
12466
|
+
}) {
|
|
12467
|
+
const client = React22.useMemo(
|
|
12468
|
+
() => createStoreClient({ apiOrigin: storeApiOrigin }),
|
|
12469
|
+
[storeApiOrigin]
|
|
12470
|
+
);
|
|
12201
12471
|
const [catalog, setCatalog] = React22.useState(null);
|
|
12202
12472
|
const [failed, setFailed] = React22.useState(false);
|
|
12203
12473
|
const [count, setCount] = React22.useState(0);
|
|
@@ -12207,22 +12477,32 @@ function StoreClient({ categoryHandle, title, columns }) {
|
|
|
12207
12477
|
}, []);
|
|
12208
12478
|
React22.useEffect(() => {
|
|
12209
12479
|
let alive = true;
|
|
12210
|
-
|
|
12211
|
-
|
|
12212
|
-
|
|
12213
|
-
if (
|
|
12214
|
-
|
|
12215
|
-
|
|
12480
|
+
const path = productHandle ? `/api/store/catalog/${encodeURIComponent(productHandle)}` : "/api/store/catalog";
|
|
12481
|
+
void client.get(path).then((result) => {
|
|
12482
|
+
if (!alive) return;
|
|
12483
|
+
if (!result.ok) {
|
|
12484
|
+
setFailed(true);
|
|
12485
|
+
return;
|
|
12486
|
+
}
|
|
12487
|
+
setCatalog(
|
|
12488
|
+
"product" in result.data ? {
|
|
12489
|
+
products: [result.data.product],
|
|
12490
|
+
currency: result.data.currency
|
|
12491
|
+
} : result.data
|
|
12492
|
+
);
|
|
12493
|
+
setFailed(false);
|
|
12216
12494
|
});
|
|
12217
12495
|
return () => {
|
|
12218
12496
|
alive = false;
|
|
12219
12497
|
};
|
|
12220
|
-
}, []);
|
|
12498
|
+
}, [client, productHandle]);
|
|
12221
12499
|
const products = React22.useMemo(() => {
|
|
12222
12500
|
if (!catalog) return [];
|
|
12223
|
-
if (!categoryHandle) return catalog.products;
|
|
12224
|
-
return catalog.products.filter(
|
|
12225
|
-
|
|
12501
|
+
if (productHandle || !categoryHandle) return catalog.products;
|
|
12502
|
+
return catalog.products.filter(
|
|
12503
|
+
(product) => product.categories.some((category) => category.handle === categoryHandle)
|
|
12504
|
+
);
|
|
12505
|
+
}, [catalog, categoryHandle, productHandle]);
|
|
12226
12506
|
if (failed) {
|
|
12227
12507
|
return /* @__PURE__ */ jsxs25(Fragment12, { children: [
|
|
12228
12508
|
/* @__PURE__ */ jsx28("h2", { className: "sk-store__heading", children: title ?? "Shop" }),
|
|
@@ -12251,17 +12531,30 @@ function StoreCard({ product }) {
|
|
|
12251
12531
|
const [variantId, setVariantId] = React22.useState(
|
|
12252
12532
|
() => (product.variants.find((v) => v.inStock) ?? product.variants[0]).id
|
|
12253
12533
|
);
|
|
12254
|
-
const [
|
|
12534
|
+
const [addFeedback, setAddFeedback] = React22.useState(null);
|
|
12255
12535
|
const variant = product.variants.find((v) => v.id === variantId) ?? product.variants[0];
|
|
12256
12536
|
const image2 = product.thumbnailUrl ?? product.images[0]?.url ?? null;
|
|
12257
|
-
function onAdd() {
|
|
12258
|
-
addToCart(variant.id, 1);
|
|
12259
|
-
|
|
12260
|
-
window.setTimeout(() =>
|
|
12537
|
+
async function onAdd() {
|
|
12538
|
+
const result = await addToCart(variant.id, 1);
|
|
12539
|
+
setAddFeedback(result.status);
|
|
12540
|
+
window.setTimeout(() => setAddFeedback(null), 1600);
|
|
12261
12541
|
}
|
|
12262
12542
|
return /* @__PURE__ */ jsxs25("li", { className: "sk-store__card", children: [
|
|
12263
12543
|
/* @__PURE__ */ jsxs25("a", { className: "sk-store__card-link", href: `/store/${product.handle}`, children: [
|
|
12264
|
-
image2 ? /* @__PURE__ */ jsx28(
|
|
12544
|
+
image2 ? /* @__PURE__ */ jsx28(
|
|
12545
|
+
"img",
|
|
12546
|
+
{
|
|
12547
|
+
className: "sk-store__image",
|
|
12548
|
+
src: image2,
|
|
12549
|
+
alt: product.images[0]?.alt ?? ""
|
|
12550
|
+
}
|
|
12551
|
+
) : /* @__PURE__ */ jsx28(
|
|
12552
|
+
"span",
|
|
12553
|
+
{
|
|
12554
|
+
className: "sk-store__image sk-store__image--empty",
|
|
12555
|
+
"aria-hidden": true
|
|
12556
|
+
}
|
|
12557
|
+
),
|
|
12265
12558
|
/* @__PURE__ */ jsx28("span", { className: "sk-store__title", children: product.title })
|
|
12266
12559
|
] }),
|
|
12267
12560
|
/* @__PURE__ */ jsxs25("span", { className: "sk-store__price", children: [
|
|
@@ -12270,16 +12563,23 @@ function StoreCard({ product }) {
|
|
|
12270
12563
|
] }),
|
|
12271
12564
|
product.variants.length > 1 ? /* @__PURE__ */ jsxs25("label", { className: "sk-store__variant", children: [
|
|
12272
12565
|
/* @__PURE__ */ jsx28("span", { className: "sk-store__variant-label", children: "Option" }),
|
|
12273
|
-
/* @__PURE__ */ jsx28(
|
|
12566
|
+
/* @__PURE__ */ jsx28(
|
|
12567
|
+
"select",
|
|
12568
|
+
{
|
|
12569
|
+
value: variantId,
|
|
12570
|
+
onChange: (e) => setVariantId(e.target.value),
|
|
12571
|
+
children: product.variants.map((v) => /* @__PURE__ */ jsx28("option", { value: v.id, disabled: !v.inStock, children: (v.options.length ? v.options.join(" / ") : v.title) + (v.inStock ? "" : " \u2014 sold out") }, v.id))
|
|
12572
|
+
}
|
|
12573
|
+
)
|
|
12274
12574
|
] }) : null,
|
|
12275
12575
|
/* @__PURE__ */ jsx28(
|
|
12276
12576
|
"button",
|
|
12277
12577
|
{
|
|
12278
12578
|
type: "button",
|
|
12279
12579
|
className: "sk-store__add",
|
|
12280
|
-
onClick: onAdd,
|
|
12580
|
+
onClick: () => void onAdd(),
|
|
12281
12581
|
disabled: !variant.inStock,
|
|
12282
|
-
children: !variant.inStock ? "Sold out" : added ? "Added \u2713" : "Add to cart"
|
|
12582
|
+
children: !variant.inStock ? "Sold out" : addFeedback === "added" ? "Added \u2713" : addFeedback === "line_limit" ? "Cart full \u2014 50 products max" : addFeedback === "quantity_limit" ? "Maximum 99 in cart" : addFeedback === "cart_storage_unavailable" ? "This browser can\u2019t safely update the cart" : "Add to cart"
|
|
12283
12583
|
}
|
|
12284
12584
|
)
|
|
12285
12585
|
] });
|
|
@@ -12288,35 +12588,48 @@ function StoreCard({ product }) {
|
|
|
12288
12588
|
// src/cart-block.client.tsx
|
|
12289
12589
|
import * as React23 from "react";
|
|
12290
12590
|
import { Fragment as Fragment13, jsx as jsx29, jsxs as jsxs26 } from "react/jsx-runtime";
|
|
12291
|
-
function CartClient(
|
|
12591
|
+
function CartClient({ storeApiOrigin }) {
|
|
12592
|
+
const client = React23.useMemo(
|
|
12593
|
+
() => createStoreClient({ apiOrigin: storeApiOrigin }),
|
|
12594
|
+
[storeApiOrigin]
|
|
12595
|
+
);
|
|
12292
12596
|
const [cart, setCart] = React23.useState(() => readCart());
|
|
12293
12597
|
const [quote, setQuote] = React23.useState(null);
|
|
12598
|
+
const [quoteSourceCart, setQuoteSourceCart] = React23.useState(
|
|
12599
|
+
null
|
|
12600
|
+
);
|
|
12294
12601
|
const [failed, setFailed] = React23.useState(false);
|
|
12602
|
+
const [mutationUnavailable, setMutationUnavailable] = React23.useState(false);
|
|
12603
|
+
async function runMutation(mutation) {
|
|
12604
|
+
const result = await mutation;
|
|
12605
|
+
setMutationUnavailable(result.status === "cart_storage_unavailable");
|
|
12606
|
+
}
|
|
12295
12607
|
React23.useEffect(() => subscribeToCart(setCart), []);
|
|
12296
12608
|
React23.useEffect(() => {
|
|
12297
12609
|
if (cart.lines.length === 0) {
|
|
12298
12610
|
setQuote(null);
|
|
12611
|
+
setQuoteSourceCart(null);
|
|
12299
12612
|
setFailed(false);
|
|
12300
12613
|
return;
|
|
12301
12614
|
}
|
|
12302
12615
|
let alive = true;
|
|
12303
|
-
|
|
12304
|
-
|
|
12305
|
-
|
|
12306
|
-
|
|
12307
|
-
}).then(
|
|
12308
|
-
(res) => res.ok ? res.json() : Promise.reject(new Error(String(res.status)))
|
|
12309
|
-
).then((data) => {
|
|
12616
|
+
setQuote(null);
|
|
12617
|
+
setQuoteSourceCart(null);
|
|
12618
|
+
setFailed(false);
|
|
12619
|
+
void client.post("/api/store/quote", { lines: cartToWireLines(cart) }).then((result) => {
|
|
12310
12620
|
if (!alive) return;
|
|
12311
|
-
|
|
12621
|
+
if (!result.ok) {
|
|
12622
|
+
setFailed(true);
|
|
12623
|
+
return;
|
|
12624
|
+
}
|
|
12625
|
+
setQuote(result.data);
|
|
12626
|
+
setQuoteSourceCart(cart);
|
|
12312
12627
|
setFailed(false);
|
|
12313
|
-
}).catch(() => {
|
|
12314
|
-
if (alive) setFailed(true);
|
|
12315
12628
|
});
|
|
12316
12629
|
return () => {
|
|
12317
12630
|
alive = false;
|
|
12318
12631
|
};
|
|
12319
|
-
}, [cart]);
|
|
12632
|
+
}, [cart, client]);
|
|
12320
12633
|
if (cart.lines.length === 0) {
|
|
12321
12634
|
return /* @__PURE__ */ jsxs26(Fragment13, { children: [
|
|
12322
12635
|
/* @__PURE__ */ jsx29("h2", { className: "sk-cart__heading", children: "Your cart" }),
|
|
@@ -12324,13 +12637,54 @@ function CartClient(_props) {
|
|
|
12324
12637
|
/* @__PURE__ */ jsx29("a", { className: "sk-cart__continue", href: "/store", children: "Browse the shop" })
|
|
12325
12638
|
] });
|
|
12326
12639
|
}
|
|
12640
|
+
const quotedVariantIds = new Set(
|
|
12641
|
+
quote?.lines.map((line) => line.variantId) ?? []
|
|
12642
|
+
);
|
|
12643
|
+
const quoteSourceQuantities = new Map(
|
|
12644
|
+
quoteSourceCart?.lines.filter((line) => line.kind === "retail").map((line) => [line.variantId, line.quantity]) ?? []
|
|
12645
|
+
);
|
|
12646
|
+
const unsupportedLines = cart.lines.filter(
|
|
12647
|
+
(line) => line.kind !== "retail" || quote !== null && !quotedVariantIds.has(line.variantId)
|
|
12648
|
+
);
|
|
12327
12649
|
return /* @__PURE__ */ jsxs26(Fragment13, { children: [
|
|
12328
12650
|
/* @__PURE__ */ jsx29("h2", { className: "sk-cart__heading", children: "Your cart" }),
|
|
12329
12651
|
quote && quote.problems.length > 0 ? /* @__PURE__ */ jsx29("ul", { className: "sk-cart__problems", role: "status", children: quote.problems.map((p, i) => /* @__PURE__ */ jsx29("li", { children: p.message }, `${p.code}-${p.variantId ?? i}`)) }) : null,
|
|
12330
12652
|
failed ? /* @__PURE__ */ jsx29("p", { className: "sk-cart__placeholder", role: "alert", children: "We couldn't price your cart just now. Please try again in a moment." }) : null,
|
|
12331
|
-
|
|
12653
|
+
mutationUnavailable ? /* @__PURE__ */ jsx29("p", { className: "sk-cart__placeholder", role: "alert", children: "This browser can\u2019t safely update the cart. Try a supported browser with site storage enabled." }) : null,
|
|
12654
|
+
unsupportedLines.length > 0 ? /* @__PURE__ */ jsx29("ul", { className: "sk-cart__lines", children: unsupportedLines.map((line) => /* @__PURE__ */ jsxs26(
|
|
12655
|
+
"li",
|
|
12656
|
+
{
|
|
12657
|
+
className: "sk-cart__line",
|
|
12658
|
+
children: [
|
|
12659
|
+
/* @__PURE__ */ jsx29("span", { className: "sk-cart__line-title", children: line.kind === "fill" ? "Prescription item" : "Unavailable item" }),
|
|
12660
|
+
/* @__PURE__ */ jsx29("span", { className: "sk-cart__line-unit", children: "Not supported by retail checkout" }),
|
|
12661
|
+
/* @__PURE__ */ jsxs26("span", { className: "sk-cart__qty", children: [
|
|
12662
|
+
"Quantity ",
|
|
12663
|
+
line.quantity
|
|
12664
|
+
] }),
|
|
12665
|
+
/* @__PURE__ */ jsx29(
|
|
12666
|
+
"button",
|
|
12667
|
+
{
|
|
12668
|
+
type: "button",
|
|
12669
|
+
className: "sk-cart__remove",
|
|
12670
|
+
onClick: () => void runMutation(removeFromCart(line.variantId, line.kind)),
|
|
12671
|
+
children: "Remove"
|
|
12672
|
+
}
|
|
12673
|
+
)
|
|
12674
|
+
]
|
|
12675
|
+
},
|
|
12676
|
+
`${line.kind}-${line.variantId}`
|
|
12677
|
+
)) }) : null,
|
|
12678
|
+
quote ? /* @__PURE__ */ jsxs26(Fragment13, { children: [
|
|
12332
12679
|
/* @__PURE__ */ jsx29("ul", { className: "sk-cart__lines", children: quote.lines.map((line) => /* @__PURE__ */ jsxs26("li", { className: "sk-cart__line", children: [
|
|
12333
|
-
/* @__PURE__ */ jsx29(
|
|
12680
|
+
/* @__PURE__ */ jsx29(
|
|
12681
|
+
"a",
|
|
12682
|
+
{
|
|
12683
|
+
className: "sk-cart__line-title",
|
|
12684
|
+
href: `/store/${line.productHandle}`,
|
|
12685
|
+
children: line.title
|
|
12686
|
+
}
|
|
12687
|
+
),
|
|
12334
12688
|
/* @__PURE__ */ jsxs26("span", { className: "sk-cart__line-unit", children: [
|
|
12335
12689
|
formatMoney(line.unitPrice),
|
|
12336
12690
|
" each"
|
|
@@ -12341,7 +12695,14 @@ function CartClient(_props) {
|
|
|
12341
12695
|
{
|
|
12342
12696
|
type: "button",
|
|
12343
12697
|
"aria-label": `Decrease quantity of ${line.title}`,
|
|
12344
|
-
onClick: () =>
|
|
12698
|
+
onClick: () => void runMutation(
|
|
12699
|
+
adjustCartQuantity(
|
|
12700
|
+
"retail",
|
|
12701
|
+
line.variantId,
|
|
12702
|
+
-1,
|
|
12703
|
+
(quoteSourceQuantities.get(line.variantId) ?? 0) > line.quantity ? line.quantity : void 0
|
|
12704
|
+
)
|
|
12705
|
+
),
|
|
12345
12706
|
children: "\u2212"
|
|
12346
12707
|
}
|
|
12347
12708
|
),
|
|
@@ -12351,7 +12712,14 @@ function CartClient(_props) {
|
|
|
12351
12712
|
{
|
|
12352
12713
|
type: "button",
|
|
12353
12714
|
"aria-label": `Increase quantity of ${line.title}`,
|
|
12354
|
-
onClick: () =>
|
|
12715
|
+
onClick: () => void runMutation(
|
|
12716
|
+
adjustCartQuantity(
|
|
12717
|
+
"retail",
|
|
12718
|
+
line.variantId,
|
|
12719
|
+
1,
|
|
12720
|
+
(quoteSourceQuantities.get(line.variantId) ?? 0) > line.quantity ? line.quantity : void 0
|
|
12721
|
+
)
|
|
12722
|
+
),
|
|
12355
12723
|
children: "+"
|
|
12356
12724
|
}
|
|
12357
12725
|
)
|
|
@@ -12362,7 +12730,7 @@ function CartClient(_props) {
|
|
|
12362
12730
|
{
|
|
12363
12731
|
type: "button",
|
|
12364
12732
|
className: "sk-cart__remove",
|
|
12365
|
-
onClick: () => removeFromCart(line.variantId),
|
|
12733
|
+
onClick: () => void runMutation(removeFromCart(line.variantId, "retail")),
|
|
12366
12734
|
children: "Remove"
|
|
12367
12735
|
}
|
|
12368
12736
|
)
|
|
@@ -12384,173 +12752,768 @@ function CartClient(_props) {
|
|
|
12384
12752
|
quote.requiresShipping ? /* @__PURE__ */ jsx29("p", { className: "sk-cart__note", children: "Delivery is chosen at checkout." }) : null,
|
|
12385
12753
|
/* @__PURE__ */ jsxs26("div", { className: "sk-cart__actions", children: [
|
|
12386
12754
|
/* @__PURE__ */ jsx29("a", { className: "sk-cart__continue", href: "/store", children: "Keep shopping" }),
|
|
12387
|
-
/* @__PURE__ */ jsx29(
|
|
12388
|
-
"a",
|
|
12389
|
-
{
|
|
12390
|
-
className: "sk-cart__checkout",
|
|
12391
|
-
href: "/checkout",
|
|
12392
|
-
"aria-disabled": quote.lines.length === 0,
|
|
12393
|
-
children: "Checkout"
|
|
12394
|
-
}
|
|
12395
|
-
)
|
|
12755
|
+
unsupportedLines.length === 0 && quote.lines.length > 0 ? /* @__PURE__ */ jsx29("a", { className: "sk-cart__checkout", href: "/checkout", children: "Checkout" }) : /* @__PURE__ */ jsx29("span", { className: "sk-cart__checkout", "aria-disabled": "true", children: "Remove unsupported items to checkout" })
|
|
12396
12756
|
] })
|
|
12397
|
-
] })
|
|
12757
|
+
] }) : /* @__PURE__ */ jsx29("p", { className: "sk-cart__placeholder", role: "status", children: "Pricing your cart\u2026" })
|
|
12398
12758
|
] });
|
|
12399
12759
|
}
|
|
12400
12760
|
|
|
12401
12761
|
// src/checkout-block.client.tsx
|
|
12402
12762
|
import * as React24 from "react";
|
|
12763
|
+
|
|
12764
|
+
// src/checkout-attempt-token.ts
|
|
12765
|
+
var CHECKOUT_ATTEMPT_TOKEN = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
12766
|
+
function isCheckoutAttemptToken(value) {
|
|
12767
|
+
return typeof value === "string" && CHECKOUT_ATTEMPT_TOKEN.test(value);
|
|
12768
|
+
}
|
|
12769
|
+
|
|
12770
|
+
// src/checkout-completion.ts
|
|
12771
|
+
function checkoutCompletionUrl(configuredHref, orderId, siteOrigin) {
|
|
12772
|
+
const fallback = new URL("/checkout/complete", siteOrigin);
|
|
12773
|
+
let destination;
|
|
12774
|
+
try {
|
|
12775
|
+
destination = new URL(configuredHref || fallback.pathname, siteOrigin);
|
|
12776
|
+
} catch {
|
|
12777
|
+
destination = fallback;
|
|
12778
|
+
}
|
|
12779
|
+
if (destination.origin !== fallback.origin || destination.protocol !== "http:" && destination.protocol !== "https:") {
|
|
12780
|
+
destination = fallback;
|
|
12781
|
+
}
|
|
12782
|
+
destination.searchParams.set("order", orderId);
|
|
12783
|
+
return `${destination.pathname}${destination.search}${destination.hash}`;
|
|
12784
|
+
}
|
|
12785
|
+
|
|
12786
|
+
// src/checkout-block.client.tsx
|
|
12403
12787
|
import { Fragment as Fragment14, jsx as jsx30, jsxs as jsxs27 } from "react/jsx-runtime";
|
|
12404
|
-
|
|
12788
|
+
var CHECKOUT_RECOVERY_PREFIX = "patientos.store.checkout-order.v1";
|
|
12789
|
+
function checkoutRecoveryKey(apiOrigin) {
|
|
12790
|
+
const identity = apiOrigin || (typeof window === "undefined" ? "same-origin" : window.location.origin);
|
|
12791
|
+
let hash = 2166136261;
|
|
12792
|
+
for (let index = 0; index < identity.length; index += 1) {
|
|
12793
|
+
hash ^= identity.charCodeAt(index);
|
|
12794
|
+
hash = Math.imul(hash, 16777619);
|
|
12795
|
+
}
|
|
12796
|
+
return `${CHECKOUT_RECOVERY_PREFIX}:${(hash >>> 0).toString(16)}`;
|
|
12797
|
+
}
|
|
12798
|
+
function discardCheckoutRecovery(key) {
|
|
12799
|
+
try {
|
|
12800
|
+
window.localStorage.removeItem(key);
|
|
12801
|
+
} catch {
|
|
12802
|
+
return;
|
|
12803
|
+
}
|
|
12804
|
+
}
|
|
12805
|
+
function readCheckoutRecovery(key) {
|
|
12806
|
+
try {
|
|
12807
|
+
const value = window.localStorage.getItem(key);
|
|
12808
|
+
if (!value) return null;
|
|
12809
|
+
if (isCheckoutAttemptToken(value))
|
|
12810
|
+
return { v: 2, attemptToken: value, lines: null };
|
|
12811
|
+
const parsed = JSON.parse(value);
|
|
12812
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
12813
|
+
discardCheckoutRecovery(key);
|
|
12814
|
+
return null;
|
|
12815
|
+
}
|
|
12816
|
+
const record = parsed;
|
|
12817
|
+
if (record.v !== 1 && record.v !== 2 || !isCheckoutAttemptToken(record.attemptToken)) {
|
|
12818
|
+
discardCheckoutRecovery(key);
|
|
12819
|
+
return null;
|
|
12820
|
+
}
|
|
12821
|
+
if (record.v !== 2 || !Array.isArray(record.lines)) {
|
|
12822
|
+
return { v: 2, attemptToken: record.attemptToken, lines: null };
|
|
12823
|
+
}
|
|
12824
|
+
const rawLines = record.lines;
|
|
12825
|
+
const lines = normalizeCartLines(rawLines);
|
|
12826
|
+
if (lines.length === 0 || lines.some(
|
|
12827
|
+
(line) => !rawLines.some(
|
|
12828
|
+
(candidate) => candidate !== null && typeof candidate === "object" && !Array.isArray(candidate) && candidate.lineId === line.lineId
|
|
12829
|
+
)
|
|
12830
|
+
)) {
|
|
12831
|
+
return { v: 2, attemptToken: record.attemptToken, lines: null };
|
|
12832
|
+
}
|
|
12833
|
+
return { v: 2, attemptToken: record.attemptToken, lines };
|
|
12834
|
+
} catch {
|
|
12835
|
+
discardCheckoutRecovery(key);
|
|
12836
|
+
return null;
|
|
12837
|
+
}
|
|
12838
|
+
}
|
|
12839
|
+
function sameRecoveryRecord(left, right) {
|
|
12840
|
+
return left?.attemptToken === right.attemptToken && left.lines !== null && right.lines !== null && JSON.stringify(left.lines) === JSON.stringify(right.lines);
|
|
12841
|
+
}
|
|
12842
|
+
function writeCheckoutRecovery(key, record, expectedToken) {
|
|
12843
|
+
try {
|
|
12844
|
+
const existing = readCheckoutRecovery(key);
|
|
12845
|
+
if ((existing?.attemptToken ?? null) !== expectedToken) return false;
|
|
12846
|
+
window.localStorage.setItem(key, JSON.stringify(record));
|
|
12847
|
+
return sameRecoveryRecord(readCheckoutRecovery(key), record);
|
|
12848
|
+
} catch {
|
|
12849
|
+
return false;
|
|
12850
|
+
}
|
|
12851
|
+
}
|
|
12852
|
+
function removeCheckoutRecovery(key, attemptToken) {
|
|
12853
|
+
try {
|
|
12854
|
+
const existing = readCheckoutRecovery(key);
|
|
12855
|
+
if (!existing) return true;
|
|
12856
|
+
if (existing.attemptToken !== attemptToken) return false;
|
|
12857
|
+
window.localStorage.removeItem(key);
|
|
12858
|
+
return readCheckoutRecovery(key)?.attemptToken !== attemptToken;
|
|
12859
|
+
} catch {
|
|
12860
|
+
return false;
|
|
12861
|
+
}
|
|
12862
|
+
}
|
|
12863
|
+
function snapshotPurchasedCartLines(cartLines, quotedLines) {
|
|
12864
|
+
const cartByVariant = new Map(
|
|
12865
|
+
cartLines.filter((line) => line.kind === "retail").map((line) => [line.variantId, line])
|
|
12866
|
+
);
|
|
12867
|
+
const snapshot = [];
|
|
12868
|
+
for (const quoted of quotedLines) {
|
|
12869
|
+
const line = cartByVariant.get(quoted.variantId);
|
|
12870
|
+
if (!line) return null;
|
|
12871
|
+
snapshot.push({ ...line, quantity: quoted.quantity });
|
|
12872
|
+
}
|
|
12873
|
+
return snapshot.length > 0 ? snapshot : null;
|
|
12874
|
+
}
|
|
12875
|
+
function sameCartLineSnapshot(left, right) {
|
|
12876
|
+
if (left.length !== right.length) return false;
|
|
12877
|
+
const rightById = new Map(right.map((line) => [line.lineId, line]));
|
|
12878
|
+
return left.every((line) => {
|
|
12879
|
+
const expected = rightById.get(line.lineId);
|
|
12880
|
+
return expected?.kind === line.kind && expected.variantId === line.variantId && expected.quantity === line.quantity;
|
|
12881
|
+
});
|
|
12882
|
+
}
|
|
12883
|
+
function samePayableQuote(left, right, shippingOptionId) {
|
|
12884
|
+
return JSON.stringify(expectedQuote(left, shippingOptionId)) === JSON.stringify(expectedQuote(right, shippingOptionId));
|
|
12885
|
+
}
|
|
12886
|
+
function expectedQuote(quote, shippingOptionId) {
|
|
12887
|
+
return {
|
|
12888
|
+
shippingOptionId,
|
|
12889
|
+
lines: quote.lines.map(({ variantId, quantity, unitPrice, lineTotal }) => ({
|
|
12890
|
+
variantId,
|
|
12891
|
+
quantity,
|
|
12892
|
+
unitPrice,
|
|
12893
|
+
lineTotal
|
|
12894
|
+
})),
|
|
12895
|
+
subtotal: quote.subtotal,
|
|
12896
|
+
shippingTotal: quote.shippingTotal,
|
|
12897
|
+
taxTotal: quote.taxTotal,
|
|
12898
|
+
total: quote.total,
|
|
12899
|
+
currency: quote.currency
|
|
12900
|
+
};
|
|
12901
|
+
}
|
|
12902
|
+
var StoreRequestError = class extends Error {
|
|
12903
|
+
constructor(message, code2) {
|
|
12904
|
+
super(message);
|
|
12905
|
+
this.code = code2;
|
|
12906
|
+
this.name = "StoreRequestError";
|
|
12907
|
+
}
|
|
12908
|
+
code;
|
|
12909
|
+
};
|
|
12910
|
+
async function searchStoreAddresses(query, client) {
|
|
12405
12911
|
const q = query.trim();
|
|
12406
12912
|
if (q.length < 3) return [];
|
|
12407
|
-
const
|
|
12408
|
-
|
|
12409
|
-
|
|
12410
|
-
|
|
12411
|
-
|
|
12412
|
-
|
|
12413
|
-
|
|
12414
|
-
|
|
12415
|
-
|
|
12416
|
-
|
|
12417
|
-
|
|
12418
|
-
credentials: "same-origin",
|
|
12419
|
-
headers: { "content-type": "application/json" },
|
|
12420
|
-
body: JSON.stringify({ address })
|
|
12421
|
-
});
|
|
12422
|
-
if (!res.ok) return null;
|
|
12423
|
-
const v = await res.json();
|
|
12424
|
-
if (v.error || !v.formattedAddress) return null;
|
|
12425
|
-
return { address: v.formattedAddress, placeId: v.googlePlaceId ?? null };
|
|
12913
|
+
const result = await client.get(`/api/address/autocomplete?q=${encodeURIComponent(q)}`);
|
|
12914
|
+
return result.ok ? result.data.suggestions ?? [] : [];
|
|
12915
|
+
}
|
|
12916
|
+
async function validateStoreAddress(address, client) {
|
|
12917
|
+
const result = await client.post("/api/address/validate", { address });
|
|
12918
|
+
if (!result.ok || result.data.error || !result.data.formattedAddress)
|
|
12919
|
+
return null;
|
|
12920
|
+
return {
|
|
12921
|
+
address: result.data.formattedAddress,
|
|
12922
|
+
placeId: result.data.googlePlaceId ?? null
|
|
12923
|
+
};
|
|
12426
12924
|
}
|
|
12427
|
-
function CheckoutClient(
|
|
12925
|
+
function CheckoutClient({
|
|
12926
|
+
storeApiOrigin,
|
|
12927
|
+
ordersHref = "/portal/orders",
|
|
12928
|
+
completionHref = "/checkout/complete"
|
|
12929
|
+
}) {
|
|
12930
|
+
const client = React24.useMemo(
|
|
12931
|
+
() => createStoreClient({ apiOrigin: storeApiOrigin }),
|
|
12932
|
+
[storeApiOrigin]
|
|
12933
|
+
);
|
|
12428
12934
|
const [stage, setStage] = React24.useState("loading");
|
|
12429
12935
|
const [givenName, setGivenName] = React24.useState(null);
|
|
12430
12936
|
const [cart] = React24.useState(() => readCart());
|
|
12937
|
+
const recoveryKey = React24.useMemo(
|
|
12938
|
+
() => checkoutRecoveryKey(client.apiOrigin),
|
|
12939
|
+
[client]
|
|
12940
|
+
);
|
|
12941
|
+
const [initialRecovery] = React24.useState(
|
|
12942
|
+
() => readCheckoutRecovery(recoveryKey)
|
|
12943
|
+
);
|
|
12944
|
+
const recoveryRef = React24.useRef(
|
|
12945
|
+
initialRecovery
|
|
12946
|
+
);
|
|
12947
|
+
const attemptTokenRef = React24.useRef(
|
|
12948
|
+
initialRecovery?.attemptToken ?? null
|
|
12949
|
+
);
|
|
12950
|
+
const [checkoutLines, setCheckoutLines] = React24.useState(
|
|
12951
|
+
() => cartToWireLines(cart)
|
|
12952
|
+
);
|
|
12953
|
+
const checkoutCartSnapshotRef = React24.useRef(cart.lines);
|
|
12431
12954
|
const [quote, setQuote] = React24.useState(null);
|
|
12955
|
+
const [pendingQuote, setPendingQuote] = React24.useState(null);
|
|
12956
|
+
const [pendingQuoteBaseline, setPendingQuoteBaseline] = React24.useState(null);
|
|
12432
12957
|
const [shippingOptions, setShippingOptions] = React24.useState([]);
|
|
12433
12958
|
const [error2, setError] = React24.useState(null);
|
|
12434
|
-
const [
|
|
12435
|
-
const [
|
|
12959
|
+
const [resumableCheckout, setResumableCheckout] = React24.useState(null);
|
|
12960
|
+
const [fulfilment, setFulfilment] = React24.useState(
|
|
12961
|
+
"pickup"
|
|
12962
|
+
);
|
|
12963
|
+
const [shippingOptionId, setShippingOptionId] = React24.useState(
|
|
12964
|
+
null
|
|
12965
|
+
);
|
|
12436
12966
|
const [address, setAddress] = React24.useState(null);
|
|
12437
|
-
const [card, setCard] = React24.useState({
|
|
12967
|
+
const [card, setCard] = React24.useState({
|
|
12968
|
+
number: "",
|
|
12969
|
+
expiry: "",
|
|
12970
|
+
name: "",
|
|
12971
|
+
cvn: ""
|
|
12972
|
+
});
|
|
12438
12973
|
const orderRef = React24.useRef(null);
|
|
12439
12974
|
const [iframeUrl, setIframeUrl] = React24.useState(null);
|
|
12440
12975
|
const iframeOriginRef = React24.useRef(null);
|
|
12441
12976
|
const iframeElRef = React24.useRef(null);
|
|
12442
|
-
React24.
|
|
12443
|
-
|
|
12444
|
-
|
|
12445
|
-
|
|
12446
|
-
|
|
12447
|
-
|
|
12448
|
-
|
|
12449
|
-
|
|
12450
|
-
|
|
12451
|
-
|
|
12452
|
-
|
|
12453
|
-
|
|
12454
|
-
|
|
12455
|
-
|
|
12456
|
-
|
|
12457
|
-
|
|
12458
|
-
(
|
|
12459
|
-
|
|
12460
|
-
|
|
12461
|
-
|
|
12462
|
-
|
|
12463
|
-
|
|
12464
|
-
|
|
12465
|
-
|
|
12466
|
-
|
|
12467
|
-
|
|
12977
|
+
const ownsCheckoutRef = React24.useRef(false);
|
|
12978
|
+
const ownershipClaimRef = React24.useRef(null);
|
|
12979
|
+
const releaseCheckoutOwnershipRef = React24.useRef(null);
|
|
12980
|
+
async function claimCheckoutOwnership() {
|
|
12981
|
+
if (ownsCheckoutRef.current) return true;
|
|
12982
|
+
if (ownershipClaimRef.current) return ownershipClaimRef.current;
|
|
12983
|
+
const locks = navigator.locks;
|
|
12984
|
+
if (!locks) return false;
|
|
12985
|
+
const claim = new Promise((resolveClaim) => {
|
|
12986
|
+
let resolveHold = null;
|
|
12987
|
+
const hold = new Promise((resolve) => {
|
|
12988
|
+
resolveHold = resolve;
|
|
12989
|
+
});
|
|
12990
|
+
void locks.request(
|
|
12991
|
+
`patientos-store-checkout:${recoveryKey}`,
|
|
12992
|
+
{ mode: "exclusive", ifAvailable: true },
|
|
12993
|
+
async (lock) => {
|
|
12994
|
+
if (!lock) {
|
|
12995
|
+
resolveClaim(false);
|
|
12996
|
+
return;
|
|
12997
|
+
}
|
|
12998
|
+
ownsCheckoutRef.current = true;
|
|
12999
|
+
releaseCheckoutOwnershipRef.current = () => {
|
|
13000
|
+
if (!ownsCheckoutRef.current) return;
|
|
13001
|
+
ownsCheckoutRef.current = false;
|
|
13002
|
+
releaseCheckoutOwnershipRef.current = null;
|
|
13003
|
+
resolveHold?.();
|
|
13004
|
+
};
|
|
13005
|
+
resolveClaim(true);
|
|
13006
|
+
await hold;
|
|
13007
|
+
}
|
|
13008
|
+
).catch(() => resolveClaim(false));
|
|
12468
13009
|
});
|
|
12469
|
-
|
|
12470
|
-
|
|
12471
|
-
|
|
12472
|
-
|
|
13010
|
+
ownershipClaimRef.current = claim;
|
|
13011
|
+
const acquired = await claim;
|
|
13012
|
+
ownershipClaimRef.current = null;
|
|
13013
|
+
return acquired;
|
|
13014
|
+
}
|
|
13015
|
+
function releaseCheckoutOwnership() {
|
|
13016
|
+
releaseCheckoutOwnershipRef.current?.();
|
|
13017
|
+
}
|
|
13018
|
+
function goToCompletion(orderId) {
|
|
13019
|
+
window.location.assign(
|
|
13020
|
+
checkoutCompletionUrl(completionHref, orderId, window.location.origin)
|
|
13021
|
+
);
|
|
13022
|
+
}
|
|
13023
|
+
React24.useEffect(
|
|
13024
|
+
() => () => {
|
|
13025
|
+
releaseCheckoutOwnership();
|
|
13026
|
+
},
|
|
13027
|
+
[]
|
|
13028
|
+
);
|
|
13029
|
+
function releaseOwnedAttempt(attemptToken) {
|
|
13030
|
+
const removed = removeCheckoutRecovery(recoveryKey, attemptToken);
|
|
13031
|
+
if (removed && recoveryRef.current?.attemptToken === attemptToken) {
|
|
13032
|
+
recoveryRef.current = null;
|
|
13033
|
+
attemptTokenRef.current = null;
|
|
13034
|
+
}
|
|
13035
|
+
return removed;
|
|
13036
|
+
}
|
|
13037
|
+
async function finishPaidAttemptWhileLocked(attemptToken) {
|
|
13038
|
+
const stored = readCheckoutRecovery(recoveryKey);
|
|
13039
|
+
if (stored?.attemptToken !== attemptToken || stored.lines === null)
|
|
13040
|
+
return "snapshot_unavailable";
|
|
13041
|
+
const settled = await subtractPurchasedCartLines(
|
|
13042
|
+
stored.lines,
|
|
13043
|
+
attemptToken
|
|
13044
|
+
);
|
|
13045
|
+
if (!settled.persisted) return "storage_unavailable";
|
|
13046
|
+
return releaseOwnedAttempt(attemptToken) ? "settled" : "storage_unavailable";
|
|
13047
|
+
}
|
|
13048
|
+
async function finishPaidAttempt(attemptToken) {
|
|
13049
|
+
if (ownsCheckoutRef.current) {
|
|
13050
|
+
const result = await finishPaidAttemptWhileLocked(attemptToken);
|
|
13051
|
+
if (result === "settled") releaseCheckoutOwnership();
|
|
13052
|
+
return result;
|
|
13053
|
+
}
|
|
13054
|
+
const locks = navigator.locks;
|
|
13055
|
+
if (!locks) return "storage_unavailable";
|
|
13056
|
+
return locks.request(
|
|
13057
|
+
`patientos-store-checkout:${recoveryKey}`,
|
|
13058
|
+
() => finishPaidAttemptWhileLocked(attemptToken)
|
|
13059
|
+
);
|
|
13060
|
+
}
|
|
13061
|
+
function paidReconciliationPending(result) {
|
|
13062
|
+
setError(
|
|
13063
|
+
result === "snapshot_unavailable" ? "Payment was received, but this browser no longer has a safe cart snapshot. Review your cart and order before checking out again." : "Payment was received, but your cart update could not be saved. Allow site storage, then try again."
|
|
13064
|
+
);
|
|
13065
|
+
setStage("reconciliation_pending");
|
|
13066
|
+
}
|
|
13067
|
+
async function completePaidAttempt(attemptToken, orderId, alreadyLocked = false) {
|
|
13068
|
+
const result = alreadyLocked ? await finishPaidAttemptWhileLocked(attemptToken) : await finishPaidAttempt(attemptToken);
|
|
13069
|
+
if (result !== "settled") {
|
|
13070
|
+
paidReconciliationPending(result);
|
|
13071
|
+
return false;
|
|
13072
|
+
}
|
|
13073
|
+
if (alreadyLocked) releaseCheckoutOwnership();
|
|
13074
|
+
setStage("paid");
|
|
13075
|
+
goToCompletion(orderId);
|
|
13076
|
+
return true;
|
|
13077
|
+
}
|
|
13078
|
+
async function retryPaidReconciliation() {
|
|
13079
|
+
const attemptToken = attemptTokenRef.current;
|
|
13080
|
+
if (!attemptToken) return;
|
|
13081
|
+
await completePaidAttempt(
|
|
13082
|
+
attemptToken,
|
|
13083
|
+
orderRef.current?.orderId ?? attemptToken
|
|
13084
|
+
);
|
|
13085
|
+
}
|
|
13086
|
+
function recoverySnapshotUnavailable() {
|
|
13087
|
+
releaseCheckoutOwnership();
|
|
13088
|
+
setError(
|
|
13089
|
+
"This checkout can\u2019t be safely continued in this browser. Review My orders and your cart before trying again."
|
|
13090
|
+
);
|
|
13091
|
+
setStage("closed");
|
|
13092
|
+
}
|
|
13093
|
+
function storageUnavailable() {
|
|
13094
|
+
releaseCheckoutOwnership();
|
|
13095
|
+
setError(
|
|
13096
|
+
"Your browser couldn\u2019t safely save and coordinate checkout progress. Allow site storage, then reload and try again."
|
|
13097
|
+
);
|
|
13098
|
+
setStage("storage_error");
|
|
13099
|
+
}
|
|
12473
13100
|
React24.useEffect(() => {
|
|
12474
|
-
if (stage !== "details" || cart.lines.length === 0) return;
|
|
12475
13101
|
let alive = true;
|
|
12476
|
-
|
|
12477
|
-
|
|
12478
|
-
|
|
12479
|
-
|
|
12480
|
-
|
|
12481
|
-
|
|
12482
|
-
}
|
|
12483
|
-
|
|
12484
|
-
|
|
12485
|
-
|
|
13102
|
+
void (async () => {
|
|
13103
|
+
const locks = navigator.locks;
|
|
13104
|
+
if (!locks) {
|
|
13105
|
+
if (!alive) return;
|
|
13106
|
+
storageUnavailable();
|
|
13107
|
+
return;
|
|
13108
|
+
}
|
|
13109
|
+
const storedAttemptToken = attemptTokenRef.current;
|
|
13110
|
+
let bootRecovery = {
|
|
13111
|
+
status: "continue"
|
|
13112
|
+
};
|
|
13113
|
+
if (storedAttemptToken) {
|
|
13114
|
+
const acquired = await claimCheckoutOwnership();
|
|
13115
|
+
if (acquired) {
|
|
13116
|
+
const recovered = await client.post(
|
|
13117
|
+
"/api/store/checkout/recover",
|
|
13118
|
+
{ attemptToken: storedAttemptToken }
|
|
13119
|
+
);
|
|
13120
|
+
if (recovered.ok && recovered.data.status === "paid") {
|
|
13121
|
+
await completePaidAttempt(
|
|
13122
|
+
storedAttemptToken,
|
|
13123
|
+
recovered.data.orderId ?? storedAttemptToken,
|
|
13124
|
+
true
|
|
13125
|
+
);
|
|
13126
|
+
return;
|
|
13127
|
+
} else if (recovered.ok && recovered.data.status === "active") {
|
|
13128
|
+
releaseCheckoutOwnership();
|
|
13129
|
+
bootRecovery = { status: "active" };
|
|
13130
|
+
} else if (recovered.ok && recovered.data.status === "resumable" && !Array.isArray(readCheckoutRecovery(recoveryKey)?.lines)) {
|
|
13131
|
+
recoverySnapshotUnavailable();
|
|
13132
|
+
return;
|
|
13133
|
+
} else if (recovered.ok && recovered.data.status === "resumable" && Array.isArray(readCheckoutRecovery(recoveryKey)?.lines) && recovered.data.orderId && recovered.data.sessionId && recovered.data.sessionStatus && recovered.data.providerKey && recovered.data.amount && recovered.data.clientArtifacts) {
|
|
13134
|
+
bootRecovery = {
|
|
13135
|
+
status: "resumable",
|
|
13136
|
+
session: recovered.data
|
|
13137
|
+
};
|
|
13138
|
+
} else if (recovered.ok && recovered.data.status === "processing") {
|
|
13139
|
+
releaseCheckoutOwnership();
|
|
13140
|
+
bootRecovery = { status: "processing" };
|
|
13141
|
+
} else if (recovered.ok) {
|
|
13142
|
+
if (!releaseOwnedAttempt(storedAttemptToken)) {
|
|
13143
|
+
recoverySnapshotUnavailable();
|
|
13144
|
+
return;
|
|
13145
|
+
}
|
|
13146
|
+
releaseCheckoutOwnership();
|
|
13147
|
+
} else {
|
|
13148
|
+
releaseCheckoutOwnership();
|
|
13149
|
+
bootRecovery = {
|
|
13150
|
+
status: recovered.error === "sign_in_required" ? "continue" : "error"
|
|
13151
|
+
};
|
|
13152
|
+
}
|
|
13153
|
+
} else {
|
|
13154
|
+
bootRecovery = { status: "active_elsewhere" };
|
|
13155
|
+
}
|
|
13156
|
+
}
|
|
13157
|
+
if (!alive) return;
|
|
13158
|
+
if (bootRecovery.status === "active") {
|
|
13159
|
+
setStage("active");
|
|
13160
|
+
return;
|
|
13161
|
+
}
|
|
13162
|
+
if (bootRecovery.status === "active_elsewhere") {
|
|
13163
|
+
setStage("active_elsewhere");
|
|
13164
|
+
return;
|
|
13165
|
+
}
|
|
13166
|
+
if (bootRecovery.status === "resumable") {
|
|
13167
|
+
setResumableCheckout(bootRecovery.session);
|
|
13168
|
+
setStage("resume");
|
|
13169
|
+
return;
|
|
13170
|
+
}
|
|
13171
|
+
if (bootRecovery.status === "processing") {
|
|
13172
|
+
setStage("pending");
|
|
13173
|
+
return;
|
|
13174
|
+
}
|
|
13175
|
+
if (bootRecovery.status === "error") {
|
|
13176
|
+
setError(
|
|
13177
|
+
"We couldn\u2019t recover your previous checkout. Please try again."
|
|
13178
|
+
);
|
|
13179
|
+
setStage("quote_error");
|
|
13180
|
+
return;
|
|
13181
|
+
}
|
|
13182
|
+
if (cart.lines.length === 0) {
|
|
13183
|
+
if (!alive) return;
|
|
13184
|
+
setStage("empty");
|
|
13185
|
+
return;
|
|
13186
|
+
}
|
|
13187
|
+
const [sessionResult, quoteResult, catalogResult] = await Promise.all([
|
|
13188
|
+
client.get("/api/store/session"),
|
|
13189
|
+
client.post("/api/store/quote", {
|
|
13190
|
+
lines: checkoutLines
|
|
13191
|
+
}),
|
|
13192
|
+
client.get("/api/store/catalog")
|
|
13193
|
+
]);
|
|
13194
|
+
if (!alive) return;
|
|
13195
|
+
const session = sessionResult.ok && typeof sessionResult.data.signedIn === "boolean" ? sessionResult.data : null;
|
|
13196
|
+
const unsupportedQuote = quoteResult.ok && quoteResult.data.problems.some(
|
|
13197
|
+
(problem) => problem.code === "not_purchasable"
|
|
13198
|
+
);
|
|
13199
|
+
const payableQuote = quoteResult.ok && quoteResult.data.lines.length > 0 && !unsupportedQuote ? quoteResult.data : null;
|
|
13200
|
+
const catalogLoaded = catalogResult.ok && Array.isArray(catalogResult.data.shippingOptions);
|
|
13201
|
+
setQuote(payableQuote);
|
|
13202
|
+
setShippingOptions(
|
|
13203
|
+
catalogLoaded ? catalogResult.data.shippingOptions : []
|
|
13204
|
+
);
|
|
13205
|
+
setGivenName(session?.givenName ?? null);
|
|
13206
|
+
if (!session) {
|
|
13207
|
+
setError("We couldn\u2019t load your checkout. Please try again.");
|
|
13208
|
+
setStage("quote_error");
|
|
13209
|
+
} else if (unsupportedQuote) {
|
|
13210
|
+
setError(
|
|
13211
|
+
"Remove unsupported items from your cart before starting retail checkout."
|
|
13212
|
+
);
|
|
13213
|
+
setStage("unsupported");
|
|
13214
|
+
} else if (quoteResult.ok && quoteResult.data.lines.length === 0) {
|
|
13215
|
+
setError(null);
|
|
13216
|
+
setStage("empty");
|
|
13217
|
+
} else if (session.signedIn === false) {
|
|
13218
|
+
setStage("signed_out");
|
|
13219
|
+
} else if (!catalogLoaded) {
|
|
13220
|
+
setError("We couldn\u2019t load delivery options. Please try again.");
|
|
13221
|
+
setStage("quote_error");
|
|
13222
|
+
} else if (quoteResult.ok) {
|
|
13223
|
+
setError(null);
|
|
13224
|
+
setStage("details");
|
|
13225
|
+
} else {
|
|
13226
|
+
setError("We couldn\u2019t load your checkout. Please try again.");
|
|
13227
|
+
setStage("quote_error");
|
|
13228
|
+
}
|
|
13229
|
+
})();
|
|
13230
|
+
return () => {
|
|
13231
|
+
alive = false;
|
|
13232
|
+
};
|
|
13233
|
+
}, [cart, client]);
|
|
13234
|
+
React24.useEffect(() => {
|
|
13235
|
+
if (stage !== "details" || cart.lines.length === 0) return;
|
|
13236
|
+
let alive = true;
|
|
13237
|
+
setQuote(null);
|
|
13238
|
+
setError(null);
|
|
13239
|
+
void client.post("/api/store/quote", {
|
|
13240
|
+
lines: checkoutLines,
|
|
13241
|
+
shippingOptionId: fulfilment === "ship" ? shippingOptionId : null
|
|
13242
|
+
}).then((result) => {
|
|
13243
|
+
if (!alive) return;
|
|
13244
|
+
if (result.ok && result.data.lines.length > 0) {
|
|
13245
|
+
setQuote(result.data);
|
|
13246
|
+
} else if (result.ok) {
|
|
13247
|
+
setQuote(null);
|
|
13248
|
+
setStage("empty");
|
|
13249
|
+
} else {
|
|
13250
|
+
setError("We couldn\u2019t update your checkout total. Please try again.");
|
|
13251
|
+
setStage("quote_error");
|
|
13252
|
+
}
|
|
13253
|
+
});
|
|
12486
13254
|
return () => {
|
|
12487
13255
|
alive = false;
|
|
12488
13256
|
};
|
|
12489
|
-
}, [fulfilment, shippingOptionId]);
|
|
13257
|
+
}, [client, fulfilment, shippingOptionId]);
|
|
12490
13258
|
React24.useEffect(() => {
|
|
12491
13259
|
if (stage !== "three_ds") return;
|
|
13260
|
+
let terminalHandled = false;
|
|
12492
13261
|
function onMessage(event) {
|
|
12493
|
-
if (iframeOriginRef.current && event.origin !== iframeOriginRef.current)
|
|
12494
|
-
|
|
12495
|
-
const
|
|
12496
|
-
if (!
|
|
12497
|
-
|
|
12498
|
-
|
|
12499
|
-
|
|
12500
|
-
|
|
13262
|
+
if (iframeOriginRef.current && event.origin !== iframeOriginRef.current)
|
|
13263
|
+
return;
|
|
13264
|
+
const iframe = iframeElRef.current;
|
|
13265
|
+
if (!iframe || event.source !== iframe.contentWindow) return;
|
|
13266
|
+
const type = event.data?.type;
|
|
13267
|
+
if (type === "AuthenticationUserInteractionRequired") {
|
|
13268
|
+
if (iframe) {
|
|
13269
|
+
iframe.style.visibility = "visible";
|
|
13270
|
+
iframe.style.height = "500px";
|
|
13271
|
+
}
|
|
12501
13272
|
return;
|
|
12502
13273
|
}
|
|
12503
|
-
if (
|
|
12504
|
-
|
|
12505
|
-
|
|
13274
|
+
if (type === "AuthenticationUserInteractionFinished") {
|
|
13275
|
+
if (iframe) {
|
|
13276
|
+
iframe.style.visibility = "hidden";
|
|
13277
|
+
iframe.style.height = "1px";
|
|
13278
|
+
}
|
|
13279
|
+
return;
|
|
12506
13280
|
}
|
|
13281
|
+
if (type !== "AuthenticationInternalError" && type !== "AuthenticationComplete")
|
|
13282
|
+
return;
|
|
13283
|
+
if (terminalHandled) return;
|
|
13284
|
+
terminalHandled = true;
|
|
13285
|
+
window.removeEventListener("message", onMessage);
|
|
13286
|
+
setIframeUrl(null);
|
|
13287
|
+
if (type === "AuthenticationInternalError") {
|
|
13288
|
+
void cancelFailedChallenge();
|
|
13289
|
+
return;
|
|
13290
|
+
}
|
|
13291
|
+
void afterChallenge();
|
|
12507
13292
|
}
|
|
12508
13293
|
window.addEventListener("message", onMessage);
|
|
12509
13294
|
return () => window.removeEventListener("message", onMessage);
|
|
12510
13295
|
}, [stage]);
|
|
12511
13296
|
async function post(path, body) {
|
|
12512
|
-
const
|
|
12513
|
-
|
|
12514
|
-
|
|
12515
|
-
body: JSON.stringify(body)
|
|
12516
|
-
});
|
|
12517
|
-
const data = await res.json().catch(() => ({}));
|
|
12518
|
-
if (!res.ok) {
|
|
12519
|
-
if (data.error === "sign_in_required") {
|
|
13297
|
+
const result = await client.post(path, body);
|
|
13298
|
+
if (!result.ok) {
|
|
13299
|
+
if (result.error === "sign_in_required") {
|
|
12520
13300
|
setStage("signed_out");
|
|
12521
13301
|
throw new Error("sign_in_required");
|
|
12522
13302
|
}
|
|
12523
|
-
throw new
|
|
13303
|
+
throw new StoreRequestError(
|
|
13304
|
+
result.message ?? "Something went wrong.",
|
|
13305
|
+
result.error
|
|
13306
|
+
);
|
|
13307
|
+
}
|
|
13308
|
+
return result.data;
|
|
13309
|
+
}
|
|
13310
|
+
function showChallenge(artifacts) {
|
|
13311
|
+
if (!artifacts.iframeUrl) return false;
|
|
13312
|
+
const iframeUrl2 = client.url(artifacts.iframeUrl);
|
|
13313
|
+
try {
|
|
13314
|
+
const parsedIframeUrl = new URL(iframeUrl2, window.location.href);
|
|
13315
|
+
const ownedMockUrl = new URL(
|
|
13316
|
+
client.url("/api/store/mock-3ds-challenge"),
|
|
13317
|
+
window.location.href
|
|
13318
|
+
);
|
|
13319
|
+
const isOwnedMock = parsedIframeUrl.origin === ownedMockUrl.origin && parsedIframeUrl.pathname === ownedMockUrl.pathname && parsedIframeUrl.search === "" && parsedIframeUrl.hash === "";
|
|
13320
|
+
if (parsedIframeUrl.username || parsedIframeUrl.password) return false;
|
|
13321
|
+
if (isOwnedMock) {
|
|
13322
|
+
const allowedProtocol = parsedIframeUrl.protocol === "https:" || parsedIframeUrl.protocol === "http:" && parsedIframeUrl.hostname === "localhost";
|
|
13323
|
+
if (!allowedProtocol) return false;
|
|
13324
|
+
iframeOriginRef.current = parsedIframeUrl.origin;
|
|
13325
|
+
} else {
|
|
13326
|
+
if (parsedIframeUrl.protocol !== "https:" || !artifacts.iframeOrigin)
|
|
13327
|
+
return false;
|
|
13328
|
+
const suppliedOrigin = new URL(artifacts.iframeOrigin);
|
|
13329
|
+
if (suppliedOrigin.protocol !== "https:" || suppliedOrigin.username || suppliedOrigin.password || suppliedOrigin.pathname !== "/" || suppliedOrigin.search || suppliedOrigin.hash)
|
|
13330
|
+
return false;
|
|
13331
|
+
if (parsedIframeUrl.origin !== suppliedOrigin.origin) return false;
|
|
13332
|
+
iframeOriginRef.current = suppliedOrigin.origin;
|
|
13333
|
+
}
|
|
13334
|
+
} catch {
|
|
13335
|
+
return false;
|
|
13336
|
+
}
|
|
13337
|
+
setIframeUrl(iframeUrl2);
|
|
13338
|
+
setStage("three_ds");
|
|
13339
|
+
return true;
|
|
13340
|
+
}
|
|
13341
|
+
async function cancelFailedChallenge() {
|
|
13342
|
+
const orderId = orderRef.current?.orderId;
|
|
13343
|
+
if (!orderId) {
|
|
13344
|
+
setStage("pending");
|
|
13345
|
+
return;
|
|
13346
|
+
}
|
|
13347
|
+
setStage("paying");
|
|
13348
|
+
try {
|
|
13349
|
+
await post("/api/store/checkout/cancel-challenge", { orderId });
|
|
13350
|
+
const attemptToken = attemptTokenRef.current;
|
|
13351
|
+
if (attemptToken) releaseOwnedAttempt(attemptToken);
|
|
13352
|
+
setError("The card check could not be completed. Please try again.");
|
|
13353
|
+
setStage(quote ? "details" : "closed");
|
|
13354
|
+
releaseCheckoutOwnership();
|
|
13355
|
+
} catch {
|
|
13356
|
+
setError(
|
|
13357
|
+
"We couldn\u2019t safely close the failed card check. Go to your orders before trying again."
|
|
13358
|
+
);
|
|
13359
|
+
setStage("pending");
|
|
13360
|
+
}
|
|
13361
|
+
}
|
|
13362
|
+
async function recoverAttemptWhileLocked(attemptToken, continueRestart = false) {
|
|
13363
|
+
const recovered = await client.post(
|
|
13364
|
+
"/api/store/checkout/recover",
|
|
13365
|
+
{ attemptToken }
|
|
13366
|
+
);
|
|
13367
|
+
if (recovered.ok && recovered.data.status === "active") {
|
|
13368
|
+
setStage("active");
|
|
13369
|
+
releaseCheckoutOwnership();
|
|
13370
|
+
return "active";
|
|
13371
|
+
}
|
|
13372
|
+
if (recovered.ok && recovered.data.status === "resumable" && !Array.isArray(readCheckoutRecovery(recoveryKey)?.lines)) {
|
|
13373
|
+
recoverySnapshotUnavailable();
|
|
13374
|
+
return "blocked";
|
|
13375
|
+
}
|
|
13376
|
+
if (recovered.ok && recovered.data.status === "resumable" && Array.isArray(readCheckoutRecovery(recoveryKey)?.lines) && recovered.data.orderId && recovered.data.sessionId && recovered.data.sessionStatus && recovered.data.providerKey && recovered.data.amount && recovered.data.clientArtifacts) {
|
|
13377
|
+
setResumableCheckout(recovered.data);
|
|
13378
|
+
setStage("resume");
|
|
13379
|
+
return "active";
|
|
13380
|
+
}
|
|
13381
|
+
if (recovered.ok && recovered.data.status === "processing") {
|
|
13382
|
+
setStage("pending");
|
|
13383
|
+
releaseCheckoutOwnership();
|
|
13384
|
+
return "processing";
|
|
13385
|
+
}
|
|
13386
|
+
if (recovered.ok && recovered.data.status === "paid") {
|
|
13387
|
+
return await completePaidAttempt(
|
|
13388
|
+
attemptToken,
|
|
13389
|
+
recovered.data.orderId ?? attemptToken,
|
|
13390
|
+
true
|
|
13391
|
+
) ? "paid" : "blocked";
|
|
13392
|
+
}
|
|
13393
|
+
if (recovered.ok) {
|
|
13394
|
+
if (!releaseOwnedAttempt(attemptToken)) {
|
|
13395
|
+
recoverySnapshotUnavailable();
|
|
13396
|
+
return "blocked";
|
|
13397
|
+
}
|
|
13398
|
+
if (!continueRestart) {
|
|
13399
|
+
releaseCheckoutOwnership();
|
|
13400
|
+
setError(
|
|
13401
|
+
"Your previous checkout was closed. Review the total and try again."
|
|
13402
|
+
);
|
|
13403
|
+
setStage("details");
|
|
13404
|
+
}
|
|
13405
|
+
return "restart";
|
|
12524
13406
|
}
|
|
12525
|
-
|
|
13407
|
+
setError(
|
|
13408
|
+
"We couldn\u2019t safely close your previous checkout. Go to your orders before trying again."
|
|
13409
|
+
);
|
|
13410
|
+
setStage("closed");
|
|
13411
|
+
releaseCheckoutOwnership();
|
|
13412
|
+
return "blocked";
|
|
13413
|
+
}
|
|
13414
|
+
async function recoverAttemptForRestart(attemptToken) {
|
|
13415
|
+
if (!await claimCheckoutOwnership()) {
|
|
13416
|
+
setStage("active_elsewhere");
|
|
13417
|
+
return "active";
|
|
13418
|
+
}
|
|
13419
|
+
return recoverAttemptWhileLocked(attemptToken);
|
|
13420
|
+
}
|
|
13421
|
+
async function transitionRetryableDecline(message) {
|
|
13422
|
+
const orderId = orderRef.current?.orderId;
|
|
13423
|
+
if (!orderId) {
|
|
13424
|
+
const attemptToken = attemptTokenRef.current;
|
|
13425
|
+
if (attemptToken) await recoverAttemptWhileLocked(attemptToken);
|
|
13426
|
+
else setStage("pending");
|
|
13427
|
+
return;
|
|
13428
|
+
}
|
|
13429
|
+
setError(message);
|
|
13430
|
+
setStage("loading");
|
|
13431
|
+
const result = await client.post("/api/store/checkout/retry-payment", {
|
|
13432
|
+
orderId
|
|
13433
|
+
}).catch(() => null);
|
|
13434
|
+
const retryData = result?.ok && result.data !== null && typeof result.data === "object" && !Array.isArray(result.data) ? result.data : null;
|
|
13435
|
+
const retryArtifacts = retryData?.clientArtifacts !== null && typeof retryData?.clientArtifacts === "object" && !Array.isArray(retryData.clientArtifacts) ? retryData.clientArtifacts : null;
|
|
13436
|
+
if (retryData?.status !== "ready" || retryData.orderId !== orderId || typeof retryData.sessionId !== "string" || typeof retryData.providerKey !== "string" || typeof retryData.amount !== "string" || !retryArtifacts) {
|
|
13437
|
+
await recoverAttemptWhileLocked(orderId);
|
|
13438
|
+
return;
|
|
13439
|
+
}
|
|
13440
|
+
setResumableCheckout({
|
|
13441
|
+
orderId,
|
|
13442
|
+
sessionId: retryData.sessionId,
|
|
13443
|
+
sessionStatus: "pending",
|
|
13444
|
+
providerKey: retryData.providerKey,
|
|
13445
|
+
amount: retryData.amount,
|
|
13446
|
+
expiresAt: typeof retryData.expiresAt === "string" ? retryData.expiresAt : null,
|
|
13447
|
+
clientArtifacts: retryArtifacts
|
|
13448
|
+
});
|
|
13449
|
+
setStage("resume_payment");
|
|
13450
|
+
}
|
|
13451
|
+
async function handlePaymentRequestError(error3) {
|
|
13452
|
+
if (!(error3 instanceof StoreRequestError)) return false;
|
|
13453
|
+
if (error3.code === "payment_in_progress" || error3.code === "payment_initializing") {
|
|
13454
|
+
setStage("pending");
|
|
13455
|
+
releaseCheckoutOwnership();
|
|
13456
|
+
return true;
|
|
13457
|
+
}
|
|
13458
|
+
if (error3.code === "payment_failed") {
|
|
13459
|
+
await transitionRetryableDecline(
|
|
13460
|
+
"This payment attempt failed. Please check the details or try another card."
|
|
13461
|
+
);
|
|
13462
|
+
return true;
|
|
13463
|
+
}
|
|
13464
|
+
if (error3.code === "payment_cancelled") {
|
|
13465
|
+
setError(
|
|
13466
|
+
"This payment attempt is closed. Go to your orders before trying again."
|
|
13467
|
+
);
|
|
13468
|
+
setStage("closed");
|
|
13469
|
+
releaseCheckoutOwnership();
|
|
13470
|
+
return true;
|
|
13471
|
+
}
|
|
13472
|
+
return false;
|
|
12526
13473
|
}
|
|
12527
13474
|
async function applyConfirm(result) {
|
|
12528
13475
|
const status = result.status;
|
|
12529
13476
|
if (status === "paid") {
|
|
12530
13477
|
const orderId = orderRef.current?.orderId ?? "";
|
|
12531
|
-
|
|
12532
|
-
|
|
12533
|
-
|
|
13478
|
+
const attemptToken = attemptTokenRef.current;
|
|
13479
|
+
if (!attemptToken) {
|
|
13480
|
+
paidReconciliationPending("snapshot_unavailable");
|
|
13481
|
+
return;
|
|
13482
|
+
}
|
|
13483
|
+
await completePaidAttempt(attemptToken, orderId);
|
|
12534
13484
|
return;
|
|
12535
13485
|
}
|
|
12536
13486
|
if (status === "requires_action") {
|
|
12537
13487
|
const artifacts = result.clientArtifacts ?? {};
|
|
12538
|
-
if (!artifacts
|
|
12539
|
-
setError("Your bank asked for a check we couldn\u2019t show. Please try another card.");
|
|
12540
|
-
setStage("details");
|
|
12541
|
-
return;
|
|
12542
|
-
}
|
|
12543
|
-
iframeOriginRef.current = artifacts.iframeOrigin ?? null;
|
|
12544
|
-
setIframeUrl(artifacts.iframeUrl);
|
|
12545
|
-
setStage("three_ds");
|
|
13488
|
+
if (!showChallenge(artifacts)) await cancelFailedChallenge();
|
|
12546
13489
|
return;
|
|
12547
13490
|
}
|
|
12548
13491
|
if (status === "declined") {
|
|
12549
|
-
|
|
12550
|
-
|
|
13492
|
+
await transitionRetryableDecline(
|
|
13493
|
+
"Your card was declined. Please check the details or try another card."
|
|
13494
|
+
);
|
|
12551
13495
|
return;
|
|
12552
13496
|
}
|
|
12553
13497
|
setStage("pending");
|
|
13498
|
+
releaseCheckoutOwnership();
|
|
13499
|
+
}
|
|
13500
|
+
async function dispatchConfirm(orderId) {
|
|
13501
|
+
try {
|
|
13502
|
+
await applyConfirm(
|
|
13503
|
+
await post("/api/store/checkout/confirm", { orderId })
|
|
13504
|
+
);
|
|
13505
|
+
} catch (err) {
|
|
13506
|
+
if (err instanceof Error && err.message === "sign_in_required") return;
|
|
13507
|
+
const attemptToken = attemptTokenRef.current;
|
|
13508
|
+
if (attemptToken && err instanceof StoreRequestError && err.code === "checkout_paid") {
|
|
13509
|
+
await completePaidAttempt(attemptToken, orderId);
|
|
13510
|
+
return;
|
|
13511
|
+
}
|
|
13512
|
+
if (await handlePaymentRequestError(err)) return;
|
|
13513
|
+
setError(null);
|
|
13514
|
+
setStage("pending");
|
|
13515
|
+
releaseCheckoutOwnership();
|
|
13516
|
+
}
|
|
12554
13517
|
}
|
|
12555
13518
|
async function afterChallenge() {
|
|
12556
13519
|
const orderId = orderRef.current?.orderId;
|
|
@@ -12559,45 +13522,245 @@ function CheckoutClient(_props) {
|
|
|
12559
13522
|
try {
|
|
12560
13523
|
const auth = await post("/api/store/checkout/authenticate", { orderId });
|
|
12561
13524
|
if (auth.status === "payment_failed") {
|
|
12562
|
-
|
|
12563
|
-
|
|
13525
|
+
await transitionRetryableDecline(
|
|
13526
|
+
"The card check failed. Please try another card."
|
|
13527
|
+
);
|
|
12564
13528
|
return;
|
|
12565
13529
|
}
|
|
12566
13530
|
if (auth.status === "requires_action") {
|
|
12567
13531
|
const artifacts = auth.clientArtifacts ?? {};
|
|
12568
|
-
if (artifacts
|
|
12569
|
-
|
|
12570
|
-
|
|
12571
|
-
|
|
13532
|
+
if (showChallenge(artifacts)) return;
|
|
13533
|
+
await cancelFailedChallenge();
|
|
13534
|
+
return;
|
|
13535
|
+
}
|
|
13536
|
+
await dispatchConfirm(orderId);
|
|
13537
|
+
} catch (err) {
|
|
13538
|
+
if (err instanceof Error && err.message === "sign_in_required") return;
|
|
13539
|
+
if (await handlePaymentRequestError(err)) return;
|
|
13540
|
+
setError(err instanceof Error ? err.message : "Something went wrong.");
|
|
13541
|
+
setStage(resumableCheckout ? "pending" : "details");
|
|
13542
|
+
if (resumableCheckout) releaseCheckoutOwnership();
|
|
13543
|
+
}
|
|
13544
|
+
}
|
|
13545
|
+
async function initializeCheckoutAttempt(currentQuote) {
|
|
13546
|
+
if (!ownsCheckoutRef.current) return null;
|
|
13547
|
+
while (true) {
|
|
13548
|
+
const ownedToken = attemptTokenRef.current;
|
|
13549
|
+
const stored = readCheckoutRecovery(recoveryKey);
|
|
13550
|
+
if (ownedToken) {
|
|
13551
|
+
if (stored?.attemptToken !== ownedToken || stored.lines === null) {
|
|
13552
|
+
recoverySnapshotUnavailable();
|
|
13553
|
+
return null;
|
|
13554
|
+
}
|
|
13555
|
+
recoveryRef.current = stored;
|
|
13556
|
+
break;
|
|
13557
|
+
}
|
|
13558
|
+
if (stored) {
|
|
13559
|
+
recoveryRef.current = stored;
|
|
13560
|
+
attemptTokenRef.current = stored.attemptToken;
|
|
13561
|
+
const disposition = await recoverAttemptWhileLocked(
|
|
13562
|
+
stored.attemptToken,
|
|
13563
|
+
true
|
|
13564
|
+
);
|
|
13565
|
+
if (disposition !== "restart") return null;
|
|
13566
|
+
continue;
|
|
13567
|
+
}
|
|
13568
|
+
break;
|
|
13569
|
+
}
|
|
13570
|
+
const existingToken = attemptTokenRef.current;
|
|
13571
|
+
const expectedLines = recoveryRef.current?.lines ?? checkoutCartSnapshotRef.current;
|
|
13572
|
+
if (expectedLines === null) {
|
|
13573
|
+
recoverySnapshotUnavailable();
|
|
13574
|
+
return null;
|
|
13575
|
+
}
|
|
13576
|
+
const selectedShippingOptionId = fulfilment === "ship" ? shippingOptionId : null;
|
|
13577
|
+
const coordinated = await withCartLock(async (latest, signal) => {
|
|
13578
|
+
const wireLines = cartToWireLines(latest);
|
|
13579
|
+
const quoteResult = await client.post(
|
|
13580
|
+
"/api/store/quote",
|
|
13581
|
+
{
|
|
13582
|
+
lines: wireLines,
|
|
13583
|
+
shippingOptionId: selectedShippingOptionId
|
|
13584
|
+
},
|
|
13585
|
+
{ signal }
|
|
13586
|
+
);
|
|
13587
|
+
if (!quoteResult.ok || signal.aborted)
|
|
13588
|
+
return { status: "quote_error" };
|
|
13589
|
+
const freshQuote = quoteResult.data;
|
|
13590
|
+
const purchasedLines = snapshotPurchasedCartLines(
|
|
13591
|
+
latest.lines,
|
|
13592
|
+
freshQuote.lines
|
|
13593
|
+
);
|
|
13594
|
+
if (!purchasedLines || freshQuote.lines.length === 0 || freshQuote.problems.length > 0 || !sameCartLineSnapshot(latest.lines, expectedLines) || !samePayableQuote(currentQuote, freshQuote, selectedShippingOptionId)) {
|
|
13595
|
+
return {
|
|
13596
|
+
status: "cart_changed",
|
|
13597
|
+
quote: freshQuote,
|
|
13598
|
+
baseline: latest
|
|
13599
|
+
};
|
|
13600
|
+
}
|
|
13601
|
+
const attemptToken = existingToken ?? crypto.randomUUID();
|
|
13602
|
+
if (existingToken) {
|
|
13603
|
+
if (!sameCartLineSnapshot(purchasedLines, expectedLines)) {
|
|
13604
|
+
return {
|
|
13605
|
+
status: "cart_changed",
|
|
13606
|
+
quote: freshQuote,
|
|
13607
|
+
baseline: latest
|
|
13608
|
+
};
|
|
13609
|
+
}
|
|
13610
|
+
} else {
|
|
13611
|
+
const recovery = {
|
|
13612
|
+
v: 2,
|
|
13613
|
+
attemptToken,
|
|
13614
|
+
lines: purchasedLines
|
|
13615
|
+
};
|
|
13616
|
+
if (!writeCheckoutRecovery(recoveryKey, recovery, null))
|
|
13617
|
+
return { status: "storage_error" };
|
|
13618
|
+
recoveryRef.current = recovery;
|
|
13619
|
+
attemptTokenRef.current = attemptToken;
|
|
13620
|
+
}
|
|
13621
|
+
const initPromise = post("/api/store/checkout/init", {
|
|
13622
|
+
lines: wireLines,
|
|
13623
|
+
attemptToken,
|
|
13624
|
+
expectedQuote: expectedQuote(freshQuote, selectedShippingOptionId),
|
|
13625
|
+
fulfilmentMethod: fulfilment,
|
|
13626
|
+
shippingOptionId: selectedShippingOptionId,
|
|
13627
|
+
shippingPlace: fulfilment === "ship" && address ? { address: address.address, placeId: address.placeId ?? null } : null
|
|
13628
|
+
});
|
|
13629
|
+
return {
|
|
13630
|
+
status: "ready",
|
|
13631
|
+
attemptToken,
|
|
13632
|
+
initPromise
|
|
13633
|
+
};
|
|
13634
|
+
});
|
|
13635
|
+
if (coordinated.status === "cart_storage_unavailable") {
|
|
13636
|
+
storageUnavailable();
|
|
13637
|
+
return null;
|
|
13638
|
+
}
|
|
13639
|
+
if (coordinated.status === "not_completed") {
|
|
13640
|
+
setError("We couldn\u2019t confirm your checkout total. Please try again.");
|
|
13641
|
+
setStage("quote_error");
|
|
13642
|
+
releaseCheckoutOwnership();
|
|
13643
|
+
return null;
|
|
13644
|
+
}
|
|
13645
|
+
const result = coordinated.result;
|
|
13646
|
+
if (result.status === "storage_error") {
|
|
13647
|
+
storageUnavailable();
|
|
13648
|
+
return null;
|
|
13649
|
+
}
|
|
13650
|
+
if (result.status === "quote_error") {
|
|
13651
|
+
setError("We couldn\u2019t confirm your checkout total. Please try again.");
|
|
13652
|
+
setStage("quote_error");
|
|
13653
|
+
releaseCheckoutOwnership();
|
|
13654
|
+
return null;
|
|
13655
|
+
}
|
|
13656
|
+
if (result.status === "cart_changed") {
|
|
13657
|
+
if (existingToken) {
|
|
13658
|
+
const disposition = await recoverAttemptWhileLocked(
|
|
13659
|
+
existingToken,
|
|
13660
|
+
true
|
|
13661
|
+
);
|
|
13662
|
+
if (disposition !== "restart") return null;
|
|
13663
|
+
}
|
|
13664
|
+
setPendingQuote(result.quote);
|
|
13665
|
+
setPendingQuoteBaseline(result.baseline);
|
|
13666
|
+
setQuote(null);
|
|
13667
|
+
setError(null);
|
|
13668
|
+
setStage("cart_changed");
|
|
13669
|
+
return null;
|
|
13670
|
+
}
|
|
13671
|
+
return {
|
|
13672
|
+
attemptToken: result.attemptToken,
|
|
13673
|
+
init: await result.initPromise
|
|
13674
|
+
};
|
|
13675
|
+
}
|
|
13676
|
+
async function beginResume() {
|
|
13677
|
+
if (!resumableCheckout) return;
|
|
13678
|
+
if (!await claimCheckoutOwnership()) {
|
|
13679
|
+
setStage("active_elsewhere");
|
|
13680
|
+
return;
|
|
13681
|
+
}
|
|
13682
|
+
orderRef.current = {
|
|
13683
|
+
orderId: resumableCheckout.orderId,
|
|
13684
|
+
providerKey: resumableCheckout.providerKey
|
|
13685
|
+
};
|
|
13686
|
+
if (resumableCheckout.sessionStatus === "requires_action") {
|
|
13687
|
+
if (!showChallenge(resumableCheckout.clientArtifacts))
|
|
13688
|
+
await cancelFailedChallenge();
|
|
13689
|
+
return;
|
|
13690
|
+
}
|
|
13691
|
+
setError(null);
|
|
13692
|
+
setStage("resume_payment");
|
|
13693
|
+
}
|
|
13694
|
+
async function onResumePayment(e) {
|
|
13695
|
+
e.preventDefault();
|
|
13696
|
+
if (!resumableCheckout) return;
|
|
13697
|
+
if (!await claimCheckoutOwnership()) {
|
|
13698
|
+
setStage("active_elsewhere");
|
|
13699
|
+
return;
|
|
13700
|
+
}
|
|
13701
|
+
setError(null);
|
|
13702
|
+
setStage("paying");
|
|
13703
|
+
orderRef.current = {
|
|
13704
|
+
orderId: resumableCheckout.orderId,
|
|
13705
|
+
providerKey: resumableCheckout.providerKey
|
|
13706
|
+
};
|
|
13707
|
+
try {
|
|
13708
|
+
if (resumableCheckout.providerKey !== "mock") {
|
|
13709
|
+
const { authKey } = resumableCheckout.clientArtifacts;
|
|
13710
|
+
if (!authKey) {
|
|
13711
|
+
setStage("pending");
|
|
13712
|
+
releaseCheckoutOwnership();
|
|
12572
13713
|
return;
|
|
12573
13714
|
}
|
|
13715
|
+
await loadBpointScript();
|
|
13716
|
+
await attachCardToAuthKey(authKey, card);
|
|
12574
13717
|
}
|
|
12575
|
-
await
|
|
13718
|
+
await dispatchConfirm(resumableCheckout.orderId);
|
|
12576
13719
|
} catch (err) {
|
|
12577
|
-
if (err instanceof Error && err.message === "sign_in_required")
|
|
13720
|
+
if (err instanceof Error && err.message === "sign_in_required") {
|
|
13721
|
+
releaseCheckoutOwnership();
|
|
13722
|
+
return;
|
|
13723
|
+
}
|
|
13724
|
+
if (await handlePaymentRequestError(err)) return;
|
|
12578
13725
|
setError(err instanceof Error ? err.message : "Something went wrong.");
|
|
12579
|
-
setStage("
|
|
13726
|
+
setStage("resume_payment");
|
|
12580
13727
|
}
|
|
12581
13728
|
}
|
|
12582
13729
|
async function onPay(e) {
|
|
12583
13730
|
e.preventDefault();
|
|
13731
|
+
if (!quote || quote.lines.length === 0) {
|
|
13732
|
+
setQuote(null);
|
|
13733
|
+
setError(
|
|
13734
|
+
quote ? null : "We couldn\u2019t confirm your checkout total. Please try again."
|
|
13735
|
+
);
|
|
13736
|
+
setStage(quote ? "empty" : "quote_error");
|
|
13737
|
+
return;
|
|
13738
|
+
}
|
|
13739
|
+
if (!await claimCheckoutOwnership()) {
|
|
13740
|
+
setStage("active_elsewhere");
|
|
13741
|
+
return;
|
|
13742
|
+
}
|
|
12584
13743
|
setError(null);
|
|
12585
13744
|
setStage("paying");
|
|
13745
|
+
let attemptToken = attemptTokenRef.current;
|
|
12586
13746
|
try {
|
|
12587
|
-
const
|
|
12588
|
-
|
|
12589
|
-
|
|
12590
|
-
|
|
12591
|
-
|
|
12592
|
-
shippingPlace: fulfilment === "ship" && address ? { address: address.address, placeId: address.placeId ?? null } : null,
|
|
12593
|
-
// A previous, abandoned attempt by this shopper — the server cancels it and
|
|
12594
|
-
// gives its stock back in the SAME tx that mints the replacement.
|
|
12595
|
-
priorOrderId: orderRef.current?.orderId ?? null
|
|
12596
|
-
});
|
|
13747
|
+
const initialized = await initializeCheckoutAttempt(quote);
|
|
13748
|
+
if (!initialized) return;
|
|
13749
|
+
attemptToken = initialized.attemptToken;
|
|
13750
|
+
const { init } = initialized;
|
|
13751
|
+
setError(null);
|
|
12597
13752
|
if (init.status === "cart_changed") {
|
|
12598
|
-
|
|
12599
|
-
|
|
12600
|
-
|
|
13753
|
+
const changedQuote = init.quote;
|
|
13754
|
+
setPendingQuote(changedQuote);
|
|
13755
|
+
const baselineLines = recoveryRef.current?.lines;
|
|
13756
|
+
if (!baselineLines) {
|
|
13757
|
+
recoverySnapshotUnavailable();
|
|
13758
|
+
return;
|
|
13759
|
+
}
|
|
13760
|
+
setPendingQuoteBaseline({ v: 1, lines: baselineLines });
|
|
13761
|
+
setQuote(null);
|
|
13762
|
+
setError(null);
|
|
13763
|
+
setStage("cart_changed");
|
|
12601
13764
|
return;
|
|
12602
13765
|
}
|
|
12603
13766
|
const orderId = String(init.orderId);
|
|
@@ -12605,17 +13768,220 @@ function CheckoutClient(_props) {
|
|
|
12605
13768
|
orderRef.current = { orderId, providerKey };
|
|
12606
13769
|
if (providerKey !== "mock") {
|
|
12607
13770
|
const artifacts = init.clientArtifacts ?? {};
|
|
12608
|
-
if (!artifacts.authKey)
|
|
13771
|
+
if (!artifacts.authKey)
|
|
13772
|
+
throw new Error(
|
|
13773
|
+
"We couldn\u2019t start a secure payment. Please try again."
|
|
13774
|
+
);
|
|
12609
13775
|
await loadBpointScript();
|
|
12610
13776
|
await attachCardToAuthKey(artifacts.authKey, card);
|
|
12611
13777
|
}
|
|
12612
|
-
await
|
|
13778
|
+
await dispatchConfirm(orderId);
|
|
12613
13779
|
} catch (err) {
|
|
12614
|
-
if (err instanceof Error && err.message === "sign_in_required")
|
|
13780
|
+
if (err instanceof Error && err.message === "sign_in_required") {
|
|
13781
|
+
releaseCheckoutOwnership();
|
|
13782
|
+
return;
|
|
13783
|
+
}
|
|
13784
|
+
const failedAttemptToken = attemptToken ?? attemptTokenRef.current;
|
|
13785
|
+
if (failedAttemptToken && err instanceof StoreRequestError && err.code === "checkout_paid") {
|
|
13786
|
+
await completePaidAttempt(failedAttemptToken, failedAttemptToken);
|
|
13787
|
+
return;
|
|
13788
|
+
}
|
|
13789
|
+
if (await handlePaymentRequestError(err)) return;
|
|
13790
|
+
if (failedAttemptToken && err instanceof StoreRequestError && (err.code === "checkout_attempt_changed" || err.code === "checkout_attempt_closed" || err.code === "checkout_init_failed" || err.code === "invalid_checkout_attempt")) {
|
|
13791
|
+
await recoverAttemptForRestart(failedAttemptToken);
|
|
13792
|
+
return;
|
|
13793
|
+
}
|
|
12615
13794
|
setError(err instanceof Error ? err.message : "Something went wrong.");
|
|
12616
13795
|
setStage("details");
|
|
12617
13796
|
}
|
|
12618
13797
|
}
|
|
13798
|
+
async function retryQuote() {
|
|
13799
|
+
setError(null);
|
|
13800
|
+
setStage("loading");
|
|
13801
|
+
const sessionResult = await client.get("/api/store/session");
|
|
13802
|
+
if (!sessionResult.ok || typeof sessionResult.data.signedIn !== "boolean") {
|
|
13803
|
+
setQuote(null);
|
|
13804
|
+
setPendingQuote(null);
|
|
13805
|
+
setPendingQuoteBaseline(null);
|
|
13806
|
+
setError("We couldn\u2019t load your checkout. Please try again.");
|
|
13807
|
+
setStage("quote_error");
|
|
13808
|
+
return;
|
|
13809
|
+
}
|
|
13810
|
+
const session = sessionResult.data;
|
|
13811
|
+
setGivenName(session.givenName ?? null);
|
|
13812
|
+
if (session.signedIn === false) {
|
|
13813
|
+
setQuote(null);
|
|
13814
|
+
setPendingQuote(null);
|
|
13815
|
+
setPendingQuoteBaseline(null);
|
|
13816
|
+
setStage("signed_out");
|
|
13817
|
+
return;
|
|
13818
|
+
}
|
|
13819
|
+
const [result, catalogResult] = await Promise.all([
|
|
13820
|
+
client.post("/api/store/quote", {
|
|
13821
|
+
lines: checkoutLines,
|
|
13822
|
+
shippingOptionId: fulfilment === "ship" ? shippingOptionId : null
|
|
13823
|
+
}),
|
|
13824
|
+
client.get("/api/store/catalog")
|
|
13825
|
+
]);
|
|
13826
|
+
const catalogLoaded = catalogResult.ok && Array.isArray(catalogResult.data.shippingOptions);
|
|
13827
|
+
if (!catalogLoaded) {
|
|
13828
|
+
setQuote(null);
|
|
13829
|
+
setShippingOptions([]);
|
|
13830
|
+
setError("We couldn\u2019t load delivery options. Please try again.");
|
|
13831
|
+
setStage("quote_error");
|
|
13832
|
+
} else if (result.ok && result.data.problems.some((problem) => problem.code === "not_purchasable")) {
|
|
13833
|
+
setQuote(null);
|
|
13834
|
+
setError(
|
|
13835
|
+
"Remove unsupported items from your cart before starting retail checkout."
|
|
13836
|
+
);
|
|
13837
|
+
setStage("unsupported");
|
|
13838
|
+
} else if (result.ok && result.data.lines.length > 0) {
|
|
13839
|
+
setShippingOptions(catalogResult.data.shippingOptions);
|
|
13840
|
+
setQuote(result.data);
|
|
13841
|
+
setStage("details");
|
|
13842
|
+
} else if (result.ok) {
|
|
13843
|
+
setQuote(null);
|
|
13844
|
+
setStage("empty");
|
|
13845
|
+
} else {
|
|
13846
|
+
setQuote(null);
|
|
13847
|
+
setError("We couldn\u2019t load your checkout. Please try again.");
|
|
13848
|
+
setStage("quote_error");
|
|
13849
|
+
}
|
|
13850
|
+
}
|
|
13851
|
+
async function acceptHealedQuote() {
|
|
13852
|
+
if (!pendingQuote) return;
|
|
13853
|
+
const locks = navigator.locks;
|
|
13854
|
+
if (!locks) {
|
|
13855
|
+
storageUnavailable();
|
|
13856
|
+
return;
|
|
13857
|
+
}
|
|
13858
|
+
const attemptedRetail = new Map(
|
|
13859
|
+
(pendingQuoteBaseline?.lines ?? checkoutLines).filter((line) => line.kind === "retail").map((line) => [line.variantId, line.quantity])
|
|
13860
|
+
);
|
|
13861
|
+
const healedRetail = new Map(
|
|
13862
|
+
pendingQuote.lines.map((line) => [line.variantId, line.quantity])
|
|
13863
|
+
);
|
|
13864
|
+
const optionUnavailable = pendingQuote.problems.some(
|
|
13865
|
+
(problem) => problem.code === "shipping_option_unavailable"
|
|
13866
|
+
);
|
|
13867
|
+
const nextShippingOptionId = optionUnavailable ? null : shippingOptionId;
|
|
13868
|
+
setPendingQuote(null);
|
|
13869
|
+
setPendingQuoteBaseline(null);
|
|
13870
|
+
setQuote(null);
|
|
13871
|
+
setError(null);
|
|
13872
|
+
setStage("loading");
|
|
13873
|
+
const outcome = {
|
|
13874
|
+
confirmedQuote: null,
|
|
13875
|
+
nextProblemQuote: null,
|
|
13876
|
+
nextProblemBaseline: null,
|
|
13877
|
+
quoteFailed: false
|
|
13878
|
+
};
|
|
13879
|
+
const continueWhileCoordinated = async () => {
|
|
13880
|
+
const attemptToken = attemptTokenRef.current;
|
|
13881
|
+
const stored = readCheckoutRecovery(recoveryKey);
|
|
13882
|
+
if (attemptToken && (stored?.attemptToken !== attemptToken || stored.lines === null) || !attemptToken && stored)
|
|
13883
|
+
return null;
|
|
13884
|
+
return mutateCart(
|
|
13885
|
+
(latest) => ({
|
|
13886
|
+
v: 1,
|
|
13887
|
+
lines: latest.lines.flatMap((line) => {
|
|
13888
|
+
if (line.kind !== "retail") return [line];
|
|
13889
|
+
const attemptedQuantity = attemptedRetail.get(line.variantId);
|
|
13890
|
+
if (attemptedQuantity === void 0) return [line];
|
|
13891
|
+
const healedQuantity = healedRetail.get(line.variantId);
|
|
13892
|
+
if (healedQuantity === void 0) return [];
|
|
13893
|
+
return [
|
|
13894
|
+
{
|
|
13895
|
+
...line,
|
|
13896
|
+
quantity: line.quantity <= attemptedQuantity ? Math.min(line.quantity, healedQuantity) : Math.min(
|
|
13897
|
+
99,
|
|
13898
|
+
healedQuantity + (line.quantity - attemptedQuantity)
|
|
13899
|
+
)
|
|
13900
|
+
}
|
|
13901
|
+
];
|
|
13902
|
+
})
|
|
13903
|
+
}),
|
|
13904
|
+
async (proposed, signal) => {
|
|
13905
|
+
const payableLines = cartToWireLines({
|
|
13906
|
+
v: 1,
|
|
13907
|
+
lines: proposed.lines.filter((line) => line.kind === "retail")
|
|
13908
|
+
});
|
|
13909
|
+
const result = await client.post(
|
|
13910
|
+
"/api/store/quote",
|
|
13911
|
+
{
|
|
13912
|
+
lines: payableLines,
|
|
13913
|
+
shippingOptionId: fulfilment === "ship" ? nextShippingOptionId : null
|
|
13914
|
+
},
|
|
13915
|
+
{ signal }
|
|
13916
|
+
);
|
|
13917
|
+
if (!result.ok) {
|
|
13918
|
+
outcome.quoteFailed = true;
|
|
13919
|
+
return false;
|
|
13920
|
+
}
|
|
13921
|
+
if (result.data.lines.length === 0 || result.data.problems.length > 0) {
|
|
13922
|
+
outcome.nextProblemQuote = result.data;
|
|
13923
|
+
outcome.nextProblemBaseline = proposed;
|
|
13924
|
+
return false;
|
|
13925
|
+
}
|
|
13926
|
+
const purchasedLines = snapshotPurchasedCartLines(
|
|
13927
|
+
proposed.lines,
|
|
13928
|
+
result.data.lines
|
|
13929
|
+
);
|
|
13930
|
+
if (!purchasedLines) return false;
|
|
13931
|
+
if (attemptToken) {
|
|
13932
|
+
const recovery = {
|
|
13933
|
+
v: 2,
|
|
13934
|
+
attemptToken,
|
|
13935
|
+
lines: purchasedLines
|
|
13936
|
+
};
|
|
13937
|
+
if (!writeCheckoutRecovery(recoveryKey, recovery, attemptToken))
|
|
13938
|
+
return false;
|
|
13939
|
+
recoveryRef.current = recovery;
|
|
13940
|
+
}
|
|
13941
|
+
outcome.confirmedQuote = result.data;
|
|
13942
|
+
return true;
|
|
13943
|
+
}
|
|
13944
|
+
);
|
|
13945
|
+
};
|
|
13946
|
+
const persisted = ownsCheckoutRef.current ? await continueWhileCoordinated() : await locks.request(
|
|
13947
|
+
`patientos-store-checkout:${recoveryKey}`,
|
|
13948
|
+
continueWhileCoordinated
|
|
13949
|
+
);
|
|
13950
|
+
if (!persisted || persisted.status === "cart_storage_unavailable") {
|
|
13951
|
+
storageUnavailable();
|
|
13952
|
+
return;
|
|
13953
|
+
}
|
|
13954
|
+
if (persisted.status === "not_committed") {
|
|
13955
|
+
if (outcome.quoteFailed) {
|
|
13956
|
+
setError("We couldn\u2019t load your checkout. Please try again.");
|
|
13957
|
+
setStage("quote_error");
|
|
13958
|
+
} else if (outcome.nextProblemQuote?.lines.length === 0) {
|
|
13959
|
+
setStage("empty");
|
|
13960
|
+
} else if (outcome.nextProblemQuote && outcome.nextProblemBaseline) {
|
|
13961
|
+
setPendingQuote(outcome.nextProblemQuote);
|
|
13962
|
+
setPendingQuoteBaseline(outcome.nextProblemBaseline);
|
|
13963
|
+
setStage("cart_changed");
|
|
13964
|
+
} else {
|
|
13965
|
+
storageUnavailable();
|
|
13966
|
+
}
|
|
13967
|
+
return;
|
|
13968
|
+
}
|
|
13969
|
+
if (optionUnavailable) setShippingOptionId(null);
|
|
13970
|
+
if (!outcome.confirmedQuote) {
|
|
13971
|
+
storageUnavailable();
|
|
13972
|
+
return;
|
|
13973
|
+
}
|
|
13974
|
+
checkoutCartSnapshotRef.current = persisted.cart.lines;
|
|
13975
|
+
setCheckoutLines(
|
|
13976
|
+
outcome.confirmedQuote.lines.map((line) => ({
|
|
13977
|
+
kind: "retail",
|
|
13978
|
+
variantId: line.variantId,
|
|
13979
|
+
quantity: line.quantity
|
|
13980
|
+
}))
|
|
13981
|
+
);
|
|
13982
|
+
setQuote(outcome.confirmedQuote);
|
|
13983
|
+
setStage("details");
|
|
13984
|
+
}
|
|
12619
13985
|
if (stage === "loading") {
|
|
12620
13986
|
return /* @__PURE__ */ jsxs27(Fragment14, { children: [
|
|
12621
13987
|
/* @__PURE__ */ jsx30("h2", { className: "sk-checkout__heading", children: "Checkout" }),
|
|
@@ -12633,7 +13999,69 @@ function CheckoutClient(_props) {
|
|
|
12633
13999
|
return /* @__PURE__ */ jsxs27(Fragment14, { children: [
|
|
12634
14000
|
/* @__PURE__ */ jsx30("h2", { className: "sk-checkout__heading", children: "Checkout" }),
|
|
12635
14001
|
/* @__PURE__ */ jsx30("p", { className: "sk-checkout__note", children: "Please sign in to finish your order. Your order is kept with your clinic record so you can see it later." }),
|
|
12636
|
-
/* @__PURE__ */ jsx30(
|
|
14002
|
+
/* @__PURE__ */ jsx30(
|
|
14003
|
+
"a",
|
|
14004
|
+
{
|
|
14005
|
+
className: "sk-checkout__signin",
|
|
14006
|
+
href: client.signInHref("/checkout"),
|
|
14007
|
+
children: "Sign in to continue"
|
|
14008
|
+
}
|
|
14009
|
+
)
|
|
14010
|
+
] });
|
|
14011
|
+
}
|
|
14012
|
+
if (stage === "unsupported") {
|
|
14013
|
+
return /* @__PURE__ */ jsxs27(Fragment14, { children: [
|
|
14014
|
+
/* @__PURE__ */ jsx30("h2", { className: "sk-checkout__heading", children: "Update your cart" }),
|
|
14015
|
+
/* @__PURE__ */ jsx30("p", { className: "sk-checkout__note", role: "alert", children: error2 }),
|
|
14016
|
+
/* @__PURE__ */ jsx30("a", { className: "sk-checkout__link", href: "/cart", children: "Return to cart" })
|
|
14017
|
+
] });
|
|
14018
|
+
}
|
|
14019
|
+
if (stage === "quote_error") {
|
|
14020
|
+
return /* @__PURE__ */ jsxs27(Fragment14, { children: [
|
|
14021
|
+
/* @__PURE__ */ jsx30("h2", { className: "sk-checkout__heading", children: givenName ? `Checkout \u2014 hello, ${givenName}` : "Checkout" }),
|
|
14022
|
+
/* @__PURE__ */ jsx30("p", { className: "sk-checkout__error", role: "alert", children: error2 ?? "We couldn\u2019t load your checkout. Please try again." }),
|
|
14023
|
+
/* @__PURE__ */ jsx30(
|
|
14024
|
+
"button",
|
|
14025
|
+
{
|
|
14026
|
+
type: "button",
|
|
14027
|
+
className: "sk-checkout__pay",
|
|
14028
|
+
onClick: () => void retryQuote(),
|
|
14029
|
+
children: "Try again"
|
|
14030
|
+
}
|
|
14031
|
+
)
|
|
14032
|
+
] });
|
|
14033
|
+
}
|
|
14034
|
+
if (stage === "cart_changed" && pendingQuote) {
|
|
14035
|
+
return /* @__PURE__ */ jsxs27(Fragment14, { children: [
|
|
14036
|
+
/* @__PURE__ */ jsx30("h2", { className: "sk-checkout__heading", children: "Your cart was updated" }),
|
|
14037
|
+
/* @__PURE__ */ jsx30("div", { className: "sk-checkout__error", role: "alert", children: pendingQuote.problems.length > 0 ? /* @__PURE__ */ jsx30("ul", { children: pendingQuote.problems.map((problem, index) => /* @__PURE__ */ jsx30("li", { children: problem.message }, `${problem.code}-${index}`)) }) : /* @__PURE__ */ jsx30("p", { children: "Prices or delivery details changed while you were checking out." }) }),
|
|
14038
|
+
/* @__PURE__ */ jsx30(
|
|
14039
|
+
"button",
|
|
14040
|
+
{
|
|
14041
|
+
type: "button",
|
|
14042
|
+
className: "sk-checkout__pay",
|
|
14043
|
+
onClick: () => void acceptHealedQuote(),
|
|
14044
|
+
children: "Continue with updated cart"
|
|
14045
|
+
}
|
|
14046
|
+
)
|
|
14047
|
+
] });
|
|
14048
|
+
}
|
|
14049
|
+
if (stage === "reconciliation_pending") {
|
|
14050
|
+
const persistedRecovery = readCheckoutRecovery(recoveryKey);
|
|
14051
|
+
const canRetry = persistedRecovery?.attemptToken === attemptTokenRef.current && persistedRecovery.lines !== null;
|
|
14052
|
+
return /* @__PURE__ */ jsxs27(Fragment14, { children: [
|
|
14053
|
+
/* @__PURE__ */ jsx30("h2", { className: "sk-checkout__heading", children: "Payment received, cart update pending" }),
|
|
14054
|
+
/* @__PURE__ */ jsx30("p", { className: "sk-checkout__note", role: "alert", children: error2 }),
|
|
14055
|
+
canRetry ? /* @__PURE__ */ jsx30(
|
|
14056
|
+
"button",
|
|
14057
|
+
{
|
|
14058
|
+
type: "button",
|
|
14059
|
+
className: "sk-checkout__pay",
|
|
14060
|
+
onClick: () => void retryPaidReconciliation(),
|
|
14061
|
+
children: "Try updating cart again"
|
|
14062
|
+
}
|
|
14063
|
+
) : /* @__PURE__ */ jsx30("a", { className: "sk-checkout__link", href: "/cart", children: "Review cart" }),
|
|
14064
|
+
/* @__PURE__ */ jsx30("a", { className: "sk-checkout__link", href: ordersHref, children: "View My orders" })
|
|
12637
14065
|
] });
|
|
12638
14066
|
}
|
|
12639
14067
|
if (stage === "paid") {
|
|
@@ -12642,11 +14070,130 @@ function CheckoutClient(_props) {
|
|
|
12642
14070
|
/* @__PURE__ */ jsx30("p", { className: "sk-checkout__note", role: "status", children: "Taking you to your order\u2026" })
|
|
12643
14071
|
] });
|
|
12644
14072
|
}
|
|
14073
|
+
if (stage === "storage_error") {
|
|
14074
|
+
return /* @__PURE__ */ jsxs27(Fragment14, { children: [
|
|
14075
|
+
/* @__PURE__ */ jsx30("h2", { className: "sk-checkout__heading", children: "Checkout unavailable" }),
|
|
14076
|
+
/* @__PURE__ */ jsx30("p", { className: "sk-checkout__note", role: "alert", children: error2 })
|
|
14077
|
+
] });
|
|
14078
|
+
}
|
|
14079
|
+
if (stage === "closed") {
|
|
14080
|
+
return /* @__PURE__ */ jsxs27(Fragment14, { children: [
|
|
14081
|
+
/* @__PURE__ */ jsx30("h2", { className: "sk-checkout__heading", children: "Payment not completed" }),
|
|
14082
|
+
/* @__PURE__ */ jsx30("p", { className: "sk-checkout__note", role: "status", children: error2 }),
|
|
14083
|
+
/* @__PURE__ */ jsx30("a", { className: "sk-checkout__link", href: ordersHref, children: "Go to my orders" })
|
|
14084
|
+
] });
|
|
14085
|
+
}
|
|
14086
|
+
if (stage === "resume" && resumableCheckout) {
|
|
14087
|
+
return /* @__PURE__ */ jsxs27(Fragment14, { children: [
|
|
14088
|
+
/* @__PURE__ */ jsx30("h2", { className: "sk-checkout__heading", children: "Resume checkout" }),
|
|
14089
|
+
/* @__PURE__ */ jsxs27("p", { className: "sk-checkout__note", role: "status", children: [
|
|
14090
|
+
"Your checkout for ",
|
|
14091
|
+
formatMoney(resumableCheckout.amount),
|
|
14092
|
+
" is ready to continue."
|
|
14093
|
+
] }),
|
|
14094
|
+
/* @__PURE__ */ jsx30(
|
|
14095
|
+
"button",
|
|
14096
|
+
{
|
|
14097
|
+
type: "button",
|
|
14098
|
+
className: "sk-checkout__pay",
|
|
14099
|
+
onClick: beginResume,
|
|
14100
|
+
children: "Resume checkout"
|
|
14101
|
+
}
|
|
14102
|
+
),
|
|
14103
|
+
/* @__PURE__ */ jsx30("a", { className: "sk-checkout__link", href: ordersHref, children: "Go to my orders" })
|
|
14104
|
+
] });
|
|
14105
|
+
}
|
|
14106
|
+
if (stage === "resume_payment" && resumableCheckout) {
|
|
14107
|
+
return /* @__PURE__ */ jsxs27(Fragment14, { children: [
|
|
14108
|
+
/* @__PURE__ */ jsx30("h2", { className: "sk-checkout__heading", children: "Resume checkout" }),
|
|
14109
|
+
error2 ? /* @__PURE__ */ jsx30("p", { className: "sk-checkout__error", role: "alert", children: error2 }) : null,
|
|
14110
|
+
/* @__PURE__ */ jsxs27("p", { className: "sk-checkout__note", role: "status", children: [
|
|
14111
|
+
"Order total: ",
|
|
14112
|
+
formatMoney(resumableCheckout.amount)
|
|
14113
|
+
] }),
|
|
14114
|
+
/* @__PURE__ */ jsxs27("form", { className: "sk-checkout__form", onSubmit: onResumePayment, children: [
|
|
14115
|
+
/* @__PURE__ */ jsxs27("fieldset", { className: "sk-checkout__fieldset", children: [
|
|
14116
|
+
/* @__PURE__ */ jsx30("legend", { children: "Card details" }),
|
|
14117
|
+
/* @__PURE__ */ jsxs27("label", { children: [
|
|
14118
|
+
/* @__PURE__ */ jsx30("span", { children: "Name on card" }),
|
|
14119
|
+
/* @__PURE__ */ jsx30(
|
|
14120
|
+
"input",
|
|
14121
|
+
{
|
|
14122
|
+
required: true,
|
|
14123
|
+
autoComplete: "cc-name",
|
|
14124
|
+
value: card.name,
|
|
14125
|
+
onChange: (e) => setCard({ ...card, name: e.target.value })
|
|
14126
|
+
}
|
|
14127
|
+
)
|
|
14128
|
+
] }),
|
|
14129
|
+
/* @__PURE__ */ jsxs27("label", { children: [
|
|
14130
|
+
/* @__PURE__ */ jsx30("span", { children: "Card number" }),
|
|
14131
|
+
/* @__PURE__ */ jsx30(
|
|
14132
|
+
"input",
|
|
14133
|
+
{
|
|
14134
|
+
required: true,
|
|
14135
|
+
inputMode: "numeric",
|
|
14136
|
+
autoComplete: "cc-number",
|
|
14137
|
+
value: card.number,
|
|
14138
|
+
onChange: (e) => setCard({ ...card, number: e.target.value })
|
|
14139
|
+
}
|
|
14140
|
+
)
|
|
14141
|
+
] }),
|
|
14142
|
+
/* @__PURE__ */ jsxs27("label", { children: [
|
|
14143
|
+
/* @__PURE__ */ jsx30("span", { children: "Expiry (MM/YY)" }),
|
|
14144
|
+
/* @__PURE__ */ jsx30(
|
|
14145
|
+
"input",
|
|
14146
|
+
{
|
|
14147
|
+
required: true,
|
|
14148
|
+
inputMode: "numeric",
|
|
14149
|
+
autoComplete: "cc-exp",
|
|
14150
|
+
placeholder: "MM/YY",
|
|
14151
|
+
value: card.expiry,
|
|
14152
|
+
onChange: (e) => setCard({ ...card, expiry: e.target.value })
|
|
14153
|
+
}
|
|
14154
|
+
)
|
|
14155
|
+
] }),
|
|
14156
|
+
/* @__PURE__ */ jsxs27("label", { children: [
|
|
14157
|
+
/* @__PURE__ */ jsx30("span", { children: "Security code" }),
|
|
14158
|
+
/* @__PURE__ */ jsx30(
|
|
14159
|
+
"input",
|
|
14160
|
+
{
|
|
14161
|
+
required: true,
|
|
14162
|
+
inputMode: "numeric",
|
|
14163
|
+
autoComplete: "cc-csc",
|
|
14164
|
+
value: card.cvn,
|
|
14165
|
+
onChange: (e) => setCard({ ...card, cvn: e.target.value })
|
|
14166
|
+
}
|
|
14167
|
+
)
|
|
14168
|
+
] }),
|
|
14169
|
+
/* @__PURE__ */ jsx30("p", { className: "sk-checkout__note", children: "Your card details go straight to our payment provider \u2014 they never reach this clinic's systems." })
|
|
14170
|
+
] }),
|
|
14171
|
+
/* @__PURE__ */ jsxs27("button", { type: "submit", className: "sk-checkout__pay", children: [
|
|
14172
|
+
"Pay ",
|
|
14173
|
+
formatMoney(resumableCheckout.amount)
|
|
14174
|
+
] })
|
|
14175
|
+
] })
|
|
14176
|
+
] });
|
|
14177
|
+
}
|
|
14178
|
+
if (stage === "active_elsewhere") {
|
|
14179
|
+
return /* @__PURE__ */ jsxs27(Fragment14, { children: [
|
|
14180
|
+
/* @__PURE__ */ jsx30("h2", { className: "sk-checkout__heading", children: "Checkout active elsewhere" }),
|
|
14181
|
+
/* @__PURE__ */ jsx30("p", { className: "sk-checkout__note", role: "status", children: "Finish checkout in your other tab, or close it and reload this page." }),
|
|
14182
|
+
/* @__PURE__ */ jsx30("a", { className: "sk-checkout__link", href: ordersHref, children: "Go to my orders" })
|
|
14183
|
+
] });
|
|
14184
|
+
}
|
|
14185
|
+
if (stage === "active") {
|
|
14186
|
+
return /* @__PURE__ */ jsxs27(Fragment14, { children: [
|
|
14187
|
+
/* @__PURE__ */ jsx30("h2", { className: "sk-checkout__heading", children: "Checkout is still opening" }),
|
|
14188
|
+
/* @__PURE__ */ jsx30("p", { className: "sk-checkout__note", role: "status", children: "Wait a moment, then reload this page before trying again." }),
|
|
14189
|
+
/* @__PURE__ */ jsx30("a", { className: "sk-checkout__link", href: ordersHref, children: "Go to my orders" })
|
|
14190
|
+
] });
|
|
14191
|
+
}
|
|
12645
14192
|
if (stage === "pending") {
|
|
12646
14193
|
return /* @__PURE__ */ jsxs27(Fragment14, { children: [
|
|
12647
14194
|
/* @__PURE__ */ jsx30("h2", { className: "sk-checkout__heading", children: "Confirming your payment" }),
|
|
12648
|
-
/* @__PURE__ */ jsx30("p", { className: "sk-checkout__note", role: "status", children: "Your bank hasn't confirmed the result yet. We're checking with them \u2014 please don't pay again. You'll see the order in your account once it's confirmed." }),
|
|
12649
|
-
/* @__PURE__ */ jsx30("a", { className: "sk-checkout__link", href:
|
|
14195
|
+
/* @__PURE__ */ jsx30("p", { className: "sk-checkout__note", role: "status", children: error2 ?? "Your bank hasn't confirmed the result yet. We're checking with them \u2014 please don't pay again. You'll see the order in your account once it's confirmed." }),
|
|
14196
|
+
/* @__PURE__ */ jsx30("a", { className: "sk-checkout__link", href: ordersHref, children: "Go to my orders" })
|
|
12650
14197
|
] });
|
|
12651
14198
|
}
|
|
12652
14199
|
if (stage === "three_ds" && iframeUrl) {
|
|
@@ -12658,16 +14205,23 @@ function CheckoutClient(_props) {
|
|
|
12658
14205
|
ref: iframeElRef,
|
|
12659
14206
|
className: "sk-checkout__3ds",
|
|
12660
14207
|
src: iframeUrl,
|
|
14208
|
+
style: { visibility: "hidden", height: "1px" },
|
|
12661
14209
|
title: "Card verification"
|
|
12662
14210
|
}
|
|
12663
14211
|
)
|
|
12664
14212
|
] });
|
|
12665
14213
|
}
|
|
12666
14214
|
const busy = stage === "paying";
|
|
14215
|
+
if (!quote) {
|
|
14216
|
+
return /* @__PURE__ */ jsxs27(Fragment14, { children: [
|
|
14217
|
+
/* @__PURE__ */ jsx30("h2", { className: "sk-checkout__heading", children: givenName ? `Checkout \u2014 hello, ${givenName}` : "Checkout" }),
|
|
14218
|
+
/* @__PURE__ */ jsx30("p", { className: "sk-checkout__placeholder", role: "status", children: "Updating your order total\u2026" })
|
|
14219
|
+
] });
|
|
14220
|
+
}
|
|
12667
14221
|
return /* @__PURE__ */ jsxs27(Fragment14, { children: [
|
|
12668
14222
|
/* @__PURE__ */ jsx30("h2", { className: "sk-checkout__heading", children: givenName ? `Checkout \u2014 hello, ${givenName}` : "Checkout" }),
|
|
12669
14223
|
error2 ? /* @__PURE__ */ jsx30("p", { className: "sk-checkout__error", role: "alert", children: error2 }) : null,
|
|
12670
|
-
|
|
14224
|
+
/* @__PURE__ */ jsxs27("div", { className: "sk-checkout__summary", children: [
|
|
12671
14225
|
/* @__PURE__ */ jsx30("ul", { className: "sk-checkout__lines", children: quote.lines.map((l) => /* @__PURE__ */ jsxs27("li", { children: [
|
|
12672
14226
|
/* @__PURE__ */ jsxs27("span", { children: [
|
|
12673
14227
|
l.title,
|
|
@@ -12694,9 +14248,9 @@ function CheckoutClient(_props) {
|
|
|
12694
14248
|
/* @__PURE__ */ jsx30("dd", { children: formatMoney(quote.total) })
|
|
12695
14249
|
] })
|
|
12696
14250
|
] })
|
|
12697
|
-
] })
|
|
14251
|
+
] }),
|
|
12698
14252
|
/* @__PURE__ */ jsxs27("form", { className: "sk-checkout__form", onSubmit: onPay, children: [
|
|
12699
|
-
quote
|
|
14253
|
+
quote.requiresShipping ? /* @__PURE__ */ jsxs27("fieldset", { className: "sk-checkout__fieldset", children: [
|
|
12700
14254
|
/* @__PURE__ */ jsx30("legend", { children: "How would you like to get this?" }),
|
|
12701
14255
|
/* @__PURE__ */ jsxs27("label", { children: [
|
|
12702
14256
|
/* @__PURE__ */ jsx30(
|
|
@@ -12749,8 +14303,8 @@ function CheckoutClient(_props) {
|
|
|
12749
14303
|
label: "Delivery address",
|
|
12750
14304
|
value: address,
|
|
12751
14305
|
onChange: setAddress,
|
|
12752
|
-
search: searchStoreAddresses,
|
|
12753
|
-
validate: validateStoreAddress
|
|
14306
|
+
search: (query) => searchStoreAddresses(query, client),
|
|
14307
|
+
validate: (value) => validateStoreAddress(value, client)
|
|
12754
14308
|
}
|
|
12755
14309
|
)
|
|
12756
14310
|
] }) : null
|
|
@@ -12811,7 +14365,7 @@ function CheckoutClient(_props) {
|
|
|
12811
14365
|
] }),
|
|
12812
14366
|
/* @__PURE__ */ jsx30("p", { className: "sk-checkout__note", children: "Your card details go straight to our payment provider \u2014 they never reach this clinic's systems." })
|
|
12813
14367
|
] }),
|
|
12814
|
-
/* @__PURE__ */ jsx30("button", { type: "submit", className: "sk-checkout__pay", disabled: busy, children: busy ? "Processing\u2026" :
|
|
14368
|
+
/* @__PURE__ */ jsx30("button", { type: "submit", className: "sk-checkout__pay", disabled: busy, children: busy ? "Processing\u2026" : `Pay ${formatMoney(quote.total)}` })
|
|
12815
14369
|
] })
|
|
12816
14370
|
] });
|
|
12817
14371
|
}
|
|
@@ -12831,6 +14385,7 @@ export {
|
|
|
12831
14385
|
getThemeTokens,
|
|
12832
14386
|
PortalAccount,
|
|
12833
14387
|
PortalAccountClient,
|
|
14388
|
+
createStoreClient,
|
|
12834
14389
|
StoreClient,
|
|
12835
14390
|
CartClient,
|
|
12836
14391
|
CheckoutClient
|