@patientos/website-kit 0.2.4 → 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 +39 -0
- package/dist/{checkout-block-Cog4H4I4.d.ts → checkout-block-BdpPIuQ5.d.ts} +23 -9
- package/dist/{chunk-S7U2HOV7.js → chunk-7ZOQ6UKG.js} +2515 -439
- package/dist/{chunk-BHVDY6SC.js → chunk-ZYX4TXBN.js} +200 -13
- package/dist/{chunk-Y67WZPV5.js → chunk-ZZFBTLR4.js} +1 -1
- package/dist/index.d.ts +32 -4
- package/dist/index.js +39 -14
- package/dist/islands-impl.d.ts +7 -6
- package/dist/islands-impl.js +2 -2
- package/dist/islands-registry.js +3 -3
- package/dist/{portal-account-pI1cWgsT.d.ts → portal-account-8uQc_Ncx.d.ts} +8 -2
- package/dist/{portal-account.client-DPRRmfCJ.d.ts → portal-account.client-BBe_q9yC.d.ts} +1 -1
- package/dist/portal-booking-client.d.ts +3 -2
- package/dist/portal-booking-client.js +1 -1
- package/dist/website-kit.css +3 -1
- package/package.json +3 -2
|
@@ -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,
|
|
@@ -59,11 +61,13 @@ import {
|
|
|
59
61
|
portalSend,
|
|
60
62
|
readPortalSignedInHint,
|
|
61
63
|
resetSession,
|
|
64
|
+
restoreApplicationRunToken,
|
|
65
|
+
saveApplicationRunToken,
|
|
62
66
|
saveFlowRecord,
|
|
63
67
|
signOutOfPortal,
|
|
64
68
|
writePortalSignedInHint,
|
|
65
69
|
zoneAbbr
|
|
66
|
-
} from "./chunk-
|
|
70
|
+
} from "./chunk-ZYX4TXBN.js";
|
|
67
71
|
import {
|
|
68
72
|
__export
|
|
69
73
|
} from "./chunk-MLKGABMK.js";
|
|
@@ -525,7 +529,7 @@ function toSessionInput(v) {
|
|
|
525
529
|
dateOfBirth: v.dateOfBirth
|
|
526
530
|
};
|
|
527
531
|
}
|
|
528
|
-
function SignInRamp({ redirect }) {
|
|
532
|
+
function SignInRamp({ redirect, prepareRedirect }) {
|
|
529
533
|
const portal = usePortalClient();
|
|
530
534
|
const [open, setOpen] = React2.useState(false);
|
|
531
535
|
const [email, setEmail] = React2.useState("");
|
|
@@ -587,7 +591,7 @@ function SignInRamp({ redirect }) {
|
|
|
587
591
|
onClick: () => {
|
|
588
592
|
if (busy) return;
|
|
589
593
|
setBusy(true);
|
|
590
|
-
void
|
|
594
|
+
void (prepareRedirect ? prepareRedirect() : Promise.resolve(redirect ?? portal.returnPath())).then((target) => portal.requestMagicLink(email.trim(), target)).then((reached) => setOutcome(reached ? "sent" : "failed")).catch(() => setOutcome("failed")).finally(() => {
|
|
591
595
|
setBusy(false);
|
|
592
596
|
});
|
|
593
597
|
},
|
|
@@ -1181,18 +1185,56 @@ function Booking({
|
|
|
1181
1185
|
|
|
1182
1186
|
// src/certificate-funnel.client.tsx
|
|
1183
1187
|
import * as React16 from "react";
|
|
1188
|
+
import {
|
|
1189
|
+
PatientOSApiError as PatientOSApiError2
|
|
1190
|
+
} from "@patientos/public-sdk";
|
|
1191
|
+
|
|
1192
|
+
// src/application-run-command.ts
|
|
1193
|
+
var inMemoryPromotionKeys = /* @__PURE__ */ new Map();
|
|
1194
|
+
function retainedPromotionCommandKey(applicationRunId, storage2, createKey = () => crypto.randomUUID()) {
|
|
1195
|
+
const memoryKey = inMemoryPromotionKeys.get(applicationRunId);
|
|
1196
|
+
if (memoryKey) return memoryKey;
|
|
1197
|
+
const storageKey = `patientos:application-promotion:${applicationRunId}`;
|
|
1198
|
+
try {
|
|
1199
|
+
const retained = storage2?.getItem(storageKey);
|
|
1200
|
+
if (retained) {
|
|
1201
|
+
inMemoryPromotionKeys.set(applicationRunId, retained);
|
|
1202
|
+
return retained;
|
|
1203
|
+
}
|
|
1204
|
+
const created = createKey();
|
|
1205
|
+
inMemoryPromotionKeys.set(applicationRunId, created);
|
|
1206
|
+
storage2?.setItem(storageKey, created);
|
|
1207
|
+
return created;
|
|
1208
|
+
} catch {
|
|
1209
|
+
const created = createKey();
|
|
1210
|
+
inMemoryPromotionKeys.set(applicationRunId, created);
|
|
1211
|
+
return created;
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
var ApplicationRunCommandRetrier = class {
|
|
1215
|
+
keys = /* @__PURE__ */ new Map();
|
|
1216
|
+
async act(client, applicationRunId, input) {
|
|
1217
|
+
const fingerprint = JSON.stringify({ applicationRunId, ...input });
|
|
1218
|
+
const idempotencyKey = this.keys.get(fingerprint) ?? crypto.randomUUID();
|
|
1219
|
+
this.keys.set(fingerprint, idempotencyKey);
|
|
1220
|
+
const command = { ...input, idempotencyKey };
|
|
1221
|
+
const run = await client.actApplicationRun(applicationRunId, command);
|
|
1222
|
+
this.keys.delete(fingerprint);
|
|
1223
|
+
return run;
|
|
1224
|
+
}
|
|
1225
|
+
};
|
|
1184
1226
|
|
|
1185
1227
|
// src/certificate-funnel-machine.ts
|
|
1186
1228
|
var FUNNEL_STEPS = [
|
|
1187
1229
|
"service",
|
|
1188
1230
|
"preflight",
|
|
1231
|
+
"intake",
|
|
1232
|
+
"slot",
|
|
1189
1233
|
"identity",
|
|
1190
1234
|
"confirm-identity",
|
|
1191
|
-
"intake",
|
|
1192
1235
|
"medicare",
|
|
1193
1236
|
"consent",
|
|
1194
1237
|
"payment",
|
|
1195
|
-
"slot",
|
|
1196
1238
|
"submitted",
|
|
1197
1239
|
"unavailable",
|
|
1198
1240
|
"blocked",
|
|
@@ -1201,14 +1243,18 @@ var FUNNEL_STEPS = [
|
|
|
1201
1243
|
"session-expired"
|
|
1202
1244
|
];
|
|
1203
1245
|
var REQUIRES_REQUEST = /* @__PURE__ */ new Set([
|
|
1204
|
-
"intake",
|
|
1205
1246
|
"medicare",
|
|
1206
1247
|
"consent",
|
|
1207
1248
|
"payment",
|
|
1208
|
-
"slot",
|
|
1209
1249
|
"submitted"
|
|
1210
1250
|
]);
|
|
1211
|
-
var
|
|
1251
|
+
var REQUIRES_APPLICATION_RUN = /* @__PURE__ */ new Set([
|
|
1252
|
+
"intake",
|
|
1253
|
+
"slot",
|
|
1254
|
+
"identity",
|
|
1255
|
+
"confirm-identity"
|
|
1256
|
+
]);
|
|
1257
|
+
var REQUIRES_PREFLIGHT = /* @__PURE__ */ new Set(["identity", "confirm-identity", "intake", "slot"]);
|
|
1212
1258
|
var REQUIRES_SERVICE = /* @__PURE__ */ new Set([
|
|
1213
1259
|
"preflight",
|
|
1214
1260
|
"identity",
|
|
@@ -1225,15 +1271,6 @@ var DEFAULT_SHAPE = {
|
|
|
1225
1271
|
scheduled: true,
|
|
1226
1272
|
intakeSteps: 1
|
|
1227
1273
|
};
|
|
1228
|
-
var SPINE_OFFSET = {
|
|
1229
|
-
preflight: 1,
|
|
1230
|
-
identity: 2,
|
|
1231
|
-
// The signed-in confirmation REPLACES the anonymous form; it is the same step.
|
|
1232
|
-
"confirm-identity": 2,
|
|
1233
|
-
intake: 3,
|
|
1234
|
-
medicare: 4,
|
|
1235
|
-
consent: 5
|
|
1236
|
-
};
|
|
1237
1274
|
function intakeSpan(shape) {
|
|
1238
1275
|
return Math.max(1, Math.trunc(shape.intakeSteps ?? 1) || 1);
|
|
1239
1276
|
}
|
|
@@ -1248,14 +1285,17 @@ function totalSteps(shape) {
|
|
|
1248
1285
|
}
|
|
1249
1286
|
function stepNumberFor(step, shape) {
|
|
1250
1287
|
const picker = shape.pinned ? 0 : 1;
|
|
1251
|
-
const
|
|
1288
|
+
const intake = intakeSpan(shape);
|
|
1252
1289
|
if (step === "service") return shape.pinned ? void 0 : 1;
|
|
1253
|
-
if (step === "
|
|
1254
|
-
if (step === "
|
|
1255
|
-
|
|
1256
|
-
const
|
|
1257
|
-
if (
|
|
1258
|
-
|
|
1290
|
+
if (step === "preflight") return picker + 1;
|
|
1291
|
+
if (step === "intake") return picker + 2;
|
|
1292
|
+
if (step === "slot") return shape.scheduled ? picker + 2 + intake : void 0;
|
|
1293
|
+
const identity = picker + 2 + intake + (shape.scheduled ? 1 : 0);
|
|
1294
|
+
if (step === "identity" || step === "confirm-identity") return identity;
|
|
1295
|
+
if (step === "medicare") return identity + 1;
|
|
1296
|
+
if (step === "consent") return identity + 2;
|
|
1297
|
+
if (step === "payment") return shape.paid ? identity + 3 : void 0;
|
|
1298
|
+
return void 0;
|
|
1259
1299
|
}
|
|
1260
1300
|
function stepProgress(step, shape = DEFAULT_SHAPE, questionIndex = 0) {
|
|
1261
1301
|
const total = totalSteps(shape);
|
|
@@ -1275,7 +1315,9 @@ function initialFunnelState(serviceKey) {
|
|
|
1275
1315
|
// safety check, never past it: pinning chooses the product, not the screening.
|
|
1276
1316
|
step: serviceKey ? "preflight" : "service",
|
|
1277
1317
|
serviceKey: serviceKey ?? null,
|
|
1318
|
+
applicationRunId: null,
|
|
1278
1319
|
requestId: null,
|
|
1320
|
+
scheduled: null,
|
|
1279
1321
|
gate: null,
|
|
1280
1322
|
failure: null
|
|
1281
1323
|
};
|
|
@@ -1296,9 +1338,13 @@ function reduceFunnel(state, event) {
|
|
|
1296
1338
|
case "SELECT_SERVICE":
|
|
1297
1339
|
if (state.step !== "service") return stay2(state);
|
|
1298
1340
|
return go2(state, { step: "preflight", serviceKey: event.serviceKey });
|
|
1299
|
-
case "
|
|
1341
|
+
case "RUN_STARTED":
|
|
1300
1342
|
if (state.step !== "preflight") return stay2(state);
|
|
1301
|
-
return go2(state, {
|
|
1343
|
+
return go2(state, {
|
|
1344
|
+
step: "intake",
|
|
1345
|
+
applicationRunId: event.applicationRunId,
|
|
1346
|
+
scheduled: event.scheduled
|
|
1347
|
+
});
|
|
1302
1348
|
case "NEED_DIFFERENT":
|
|
1303
1349
|
return go2(state, { step: "unavailable" });
|
|
1304
1350
|
case "SIGNED_IN":
|
|
@@ -1311,7 +1357,7 @@ function reduceFunnel(state, event) {
|
|
|
1311
1357
|
}
|
|
1312
1358
|
case "REQUEST_STARTED":
|
|
1313
1359
|
if (state.step !== "identity" && state.step !== "confirm-identity") return stay2(state);
|
|
1314
|
-
return go2(state, { step: "
|
|
1360
|
+
return go2(state, { step: "medicare", requestId: event.requestId });
|
|
1315
1361
|
case "GATE":
|
|
1316
1362
|
return go2(state, {
|
|
1317
1363
|
step: event.gate.kind === "escalate" ? "escalate" : "blocked",
|
|
@@ -1319,7 +1365,10 @@ function reduceFunnel(state, event) {
|
|
|
1319
1365
|
});
|
|
1320
1366
|
case "ANSWERS_COMPLETE":
|
|
1321
1367
|
if (state.step !== "intake") return stay2(state);
|
|
1322
|
-
return go2(state, { step: "
|
|
1368
|
+
return go2(state, { step: state.scheduled ? "slot" : "identity" });
|
|
1369
|
+
case "RESERVED":
|
|
1370
|
+
if (state.step !== "slot") return stay2(state);
|
|
1371
|
+
return go2(state, { step: "identity" });
|
|
1323
1372
|
case "MEDICARE_DONE":
|
|
1324
1373
|
if (state.step !== "medicare") return stay2(state);
|
|
1325
1374
|
return go2(state, { step: "consent" });
|
|
@@ -1348,12 +1397,21 @@ function reduceFunnel(state, event) {
|
|
|
1348
1397
|
case "BOOKED":
|
|
1349
1398
|
if (state.step !== "slot") return stay2(state);
|
|
1350
1399
|
return go2(state, { step: "submitted" });
|
|
1400
|
+
case "RESERVATION_LOST":
|
|
1401
|
+
if (state.step !== "consent" && state.step !== "payment") return stay2(state);
|
|
1402
|
+
return go2(state, { step: "slot" });
|
|
1351
1403
|
case "SESSION_EXPIRED":
|
|
1352
1404
|
if (state.step === "session-expired") return stay2(state);
|
|
1353
1405
|
return go2(state, { step: "session-expired" });
|
|
1354
1406
|
case "RETRY_PAYMENT":
|
|
1355
1407
|
if (state.step !== "payment-failed") return stay2(state);
|
|
1356
|
-
return go2(state, {
|
|
1408
|
+
return go2(state, {
|
|
1409
|
+
step: "preflight",
|
|
1410
|
+
applicationRunId: null,
|
|
1411
|
+
requestId: null,
|
|
1412
|
+
scheduled: null,
|
|
1413
|
+
failure: null
|
|
1414
|
+
});
|
|
1357
1415
|
case "RESTART": {
|
|
1358
1416
|
const next = go2(initialFunnelState(), { serviceKey: null });
|
|
1359
1417
|
return next;
|
|
@@ -1366,14 +1424,16 @@ function back(state) {
|
|
|
1366
1424
|
switch (state.step) {
|
|
1367
1425
|
case "preflight":
|
|
1368
1426
|
return go2(state, { step: "service", serviceKey: null });
|
|
1427
|
+
case "intake":
|
|
1428
|
+
return go2(state, { step: "preflight", applicationRunId: null, scheduled: null });
|
|
1429
|
+
case "slot":
|
|
1430
|
+
return go2(state, { step: "intake" });
|
|
1369
1431
|
case "identity":
|
|
1370
|
-
return go2(state, { step: "
|
|
1432
|
+
return go2(state, { step: state.scheduled ? "slot" : "intake" });
|
|
1371
1433
|
case "confirm-identity":
|
|
1372
1434
|
return go2(state, { step: "identity" });
|
|
1373
|
-
case "intake":
|
|
1374
|
-
return go2(state, { step: "identity" });
|
|
1375
1435
|
case "medicare":
|
|
1376
|
-
return go2(state, { step: "
|
|
1436
|
+
return go2(state, { step: "identity" });
|
|
1377
1437
|
case "consent":
|
|
1378
1438
|
return go2(state, { step: "medicare" });
|
|
1379
1439
|
case "payment":
|
|
@@ -1381,7 +1441,13 @@ function back(state) {
|
|
|
1381
1441
|
case "unavailable":
|
|
1382
1442
|
return go2(state, { step: "service" });
|
|
1383
1443
|
case "payment-failed":
|
|
1384
|
-
return go2(state, {
|
|
1444
|
+
return go2(state, {
|
|
1445
|
+
step: "preflight",
|
|
1446
|
+
applicationRunId: null,
|
|
1447
|
+
requestId: null,
|
|
1448
|
+
scheduled: null,
|
|
1449
|
+
failure: null
|
|
1450
|
+
});
|
|
1385
1451
|
default:
|
|
1386
1452
|
return stay2(state);
|
|
1387
1453
|
}
|
|
@@ -1390,10 +1456,6 @@ function canGoBack(state, opts = {}) {
|
|
|
1390
1456
|
if (state.step === "preflight" && opts.pinnedService) return false;
|
|
1391
1457
|
return back(state).effects.length > 0;
|
|
1392
1458
|
}
|
|
1393
|
-
function resumableRequestId(requestId, submittedSnapshot, currentSnapshot) {
|
|
1394
|
-
if (!requestId || !submittedSnapshot || !currentSnapshot) return null;
|
|
1395
|
-
return submittedSnapshot === currentSnapshot ? requestId : null;
|
|
1396
|
-
}
|
|
1397
1459
|
var TERMINAL_WALL = /* @__PURE__ */ new Set([
|
|
1398
1460
|
"escalate",
|
|
1399
1461
|
"blocked",
|
|
@@ -1414,18 +1476,26 @@ function popAction(current, search, pinnedService) {
|
|
|
1414
1476
|
if (TERMINAL_WALL.has(current.step)) return { type: "refuse" };
|
|
1415
1477
|
return { type: "restore", state: stepFromUrl(search, pinnedService) };
|
|
1416
1478
|
}
|
|
1417
|
-
var URL_KEYS = {
|
|
1479
|
+
var URL_KEYS = {
|
|
1480
|
+
step: "cf",
|
|
1481
|
+
requestId: "cfid",
|
|
1482
|
+
applicationRunId: "cfrun",
|
|
1483
|
+
serviceKey: "cfsvc"
|
|
1484
|
+
};
|
|
1418
1485
|
function stepFromUrl(search, pinnedService) {
|
|
1419
1486
|
const params = new URLSearchParams(search.startsWith("?") ? search.slice(1) : search);
|
|
1420
1487
|
const rawStep = params.get(URL_KEYS.step) ?? "";
|
|
1421
1488
|
const requestId = params.get(URL_KEYS.requestId) || null;
|
|
1489
|
+
const applicationRunId = params.get(URL_KEYS.applicationRunId) || null;
|
|
1422
1490
|
const urlService = params.get(URL_KEYS.serviceKey) || null;
|
|
1423
1491
|
const serviceKey = pinnedService ?? urlService;
|
|
1424
1492
|
const known = FUNNEL_STEPS.includes(rawStep) ? rawStep : null;
|
|
1425
1493
|
const base2 = {
|
|
1426
1494
|
step: known ?? initialFunnelState(serviceKey).step,
|
|
1427
1495
|
serviceKey,
|
|
1496
|
+
applicationRunId,
|
|
1428
1497
|
requestId,
|
|
1498
|
+
scheduled: null,
|
|
1429
1499
|
// Gate + failure detail are NOT in the URL: they are server verdicts with clinical
|
|
1430
1500
|
// copy attached, and a URL that could assert "you are blocked" (or, worse, that you
|
|
1431
1501
|
// are not) would be a forgeable claim about a safety decision.
|
|
@@ -1433,6 +1503,9 @@ function stepFromUrl(search, pinnedService) {
|
|
|
1433
1503
|
failure: null
|
|
1434
1504
|
};
|
|
1435
1505
|
if (REQUIRES_REQUEST.has(base2.step) && !base2.requestId) {
|
|
1506
|
+
return { ...base2, step: applicationRunId ? "identity" : serviceKey ? "preflight" : "service" };
|
|
1507
|
+
}
|
|
1508
|
+
if (REQUIRES_APPLICATION_RUN.has(base2.step) && !base2.applicationRunId) {
|
|
1436
1509
|
return { ...base2, step: serviceKey ? "preflight" : "service" };
|
|
1437
1510
|
}
|
|
1438
1511
|
if (base2.step === "payment-failed" && !base2.requestId) {
|
|
@@ -1444,7 +1517,7 @@ function stepFromUrl(search, pinnedService) {
|
|
|
1444
1517
|
if (REQUIRES_SERVICE.has(base2.step) && !base2.serviceKey) {
|
|
1445
1518
|
return { ...base2, step: "service" };
|
|
1446
1519
|
}
|
|
1447
|
-
if (REQUIRES_PREFLIGHT.has(base2.step)) {
|
|
1520
|
+
if (REQUIRES_PREFLIGHT.has(base2.step) && !base2.applicationRunId) {
|
|
1448
1521
|
return { ...base2, step: base2.serviceKey ? "preflight" : "service" };
|
|
1449
1522
|
}
|
|
1450
1523
|
if (base2.step === "blocked" || base2.step === "escalate") {
|
|
@@ -1456,6 +1529,7 @@ function urlFromState(state, opts = {}) {
|
|
|
1456
1529
|
const params = new URLSearchParams();
|
|
1457
1530
|
params.set(URL_KEYS.step, state.step);
|
|
1458
1531
|
if (state.requestId) params.set(URL_KEYS.requestId, state.requestId);
|
|
1532
|
+
if (state.applicationRunId) params.set(URL_KEYS.applicationRunId, state.applicationRunId);
|
|
1459
1533
|
if (state.serviceKey && !opts.pinnedService) params.set(URL_KEYS.serviceKey, state.serviceKey);
|
|
1460
1534
|
return params.toString();
|
|
1461
1535
|
}
|
|
@@ -1685,17 +1759,17 @@ function PaymentStep({
|
|
|
1685
1759
|
const res = await client.payConfirm(requestId);
|
|
1686
1760
|
switch (res.status) {
|
|
1687
1761
|
case "awaiting_slot":
|
|
1688
|
-
onOutcome({ kind: "awaiting_slot" });
|
|
1762
|
+
await onOutcome({ kind: "awaiting_slot" });
|
|
1689
1763
|
return;
|
|
1690
1764
|
case "ready":
|
|
1691
|
-
onOutcome({ kind: "ready" });
|
|
1765
|
+
await onOutcome({ kind: "ready" });
|
|
1692
1766
|
return;
|
|
1693
1767
|
case "requires_action":
|
|
1694
1768
|
setError("Your bank needs to verify this card. Please try again.");
|
|
1695
1769
|
setPhase("ready");
|
|
1696
1770
|
return;
|
|
1697
1771
|
case "payment_failed":
|
|
1698
|
-
onOutcome({
|
|
1772
|
+
await onOutcome({
|
|
1699
1773
|
kind: "failed",
|
|
1700
1774
|
message: "Your card was declined and the pre-authorisation could not be completed."
|
|
1701
1775
|
});
|
|
@@ -2291,13 +2365,43 @@ function IdentityStep(props) {
|
|
|
2291
2365
|
},
|
|
2292
2366
|
[client]
|
|
2293
2367
|
);
|
|
2368
|
+
const promotionIdempotencyRef = React8.useRef(null);
|
|
2369
|
+
const promotionIdempotencyKey = React8.useCallback((applicationRunId) => {
|
|
2370
|
+
if (promotionIdempotencyRef.current) return promotionIdempotencyRef.current;
|
|
2371
|
+
const retained = retainedPromotionCommandKey(
|
|
2372
|
+
applicationRunId,
|
|
2373
|
+
typeof window === "undefined" ? null : window.sessionStorage
|
|
2374
|
+
);
|
|
2375
|
+
promotionIdempotencyRef.current = retained;
|
|
2376
|
+
return retained;
|
|
2377
|
+
}, []);
|
|
2294
2378
|
const startRequest = React8.useCallback(
|
|
2295
|
-
async (
|
|
2296
|
-
|
|
2297
|
-
|
|
2379
|
+
async (promotion) => {
|
|
2380
|
+
if (!props.applicationRunId) {
|
|
2381
|
+
return (await client.startRequest({ serviceKey: props.serviceKey })).requestId;
|
|
2382
|
+
}
|
|
2383
|
+
const result = await client.promoteApplicationRun(props.applicationRunId, {
|
|
2384
|
+
...promotion,
|
|
2385
|
+
idempotencyKey: promotionIdempotencyKey(props.applicationRunId)
|
|
2386
|
+
});
|
|
2387
|
+
return result.requestId;
|
|
2298
2388
|
},
|
|
2299
|
-
[client, props.
|
|
2389
|
+
[client, promotionIdempotencyKey, props.applicationRunId, props.serviceKey]
|
|
2300
2390
|
);
|
|
2391
|
+
const prepareSignInRedirect = React8.useCallback(async () => {
|
|
2392
|
+
if (!props.applicationRunId || typeof window === "undefined") return portal.returnPath();
|
|
2393
|
+
const { handoffId } = await client.createApplicationRunHandoff(props.applicationRunId);
|
|
2394
|
+
let target;
|
|
2395
|
+
try {
|
|
2396
|
+
target = new URL(window.location.href);
|
|
2397
|
+
} catch {
|
|
2398
|
+
return portal.returnPath();
|
|
2399
|
+
}
|
|
2400
|
+
target.searchParams.set("cf", "confirm-identity");
|
|
2401
|
+
target.searchParams.set("cfrun", props.applicationRunId);
|
|
2402
|
+
target.searchParams.set("cfhandoff", handoffId);
|
|
2403
|
+
return portal.returnPath(target.href);
|
|
2404
|
+
}, [client, portal, props.applicationRunId]);
|
|
2301
2405
|
async function handleConfirmSignedIn() {
|
|
2302
2406
|
if (busy) return;
|
|
2303
2407
|
setBusy(true);
|
|
@@ -2311,13 +2415,15 @@ function IdentityStep(props) {
|
|
|
2311
2415
|
return;
|
|
2312
2416
|
}
|
|
2313
2417
|
adoptClaimToken(client, claim.claimToken);
|
|
2314
|
-
|
|
2418
|
+
const confirmedIdentity = {
|
|
2315
2419
|
givenName: prefill?.givenName ?? "",
|
|
2316
2420
|
familyName: prefill?.familyName ?? "",
|
|
2317
2421
|
email: prefill?.email ?? "",
|
|
2318
2422
|
phone: prefill?.phone ?? "",
|
|
2319
2423
|
dateOfBirth: prefill?.dateOfBirth ?? ""
|
|
2320
|
-
}
|
|
2424
|
+
};
|
|
2425
|
+
const requestId = await startRequest();
|
|
2426
|
+
onStarted(requestId, confirmedIdentity);
|
|
2321
2427
|
} catch (err) {
|
|
2322
2428
|
setBusy(false);
|
|
2323
2429
|
setError(errorMessage(err, "We could not start your request. Please try again."));
|
|
@@ -2342,17 +2448,29 @@ function IdentityStep(props) {
|
|
|
2342
2448
|
setBusy(true);
|
|
2343
2449
|
setError(null);
|
|
2344
2450
|
try {
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2451
|
+
let requestId;
|
|
2452
|
+
if (props.applicationRunId) {
|
|
2453
|
+
requestId = await startRequest({
|
|
2454
|
+
identity: toSessionInput(identity),
|
|
2455
|
+
address: {
|
|
2456
|
+
address: address.place.address,
|
|
2457
|
+
placeId: address.place?.placeId ?? null
|
|
2458
|
+
},
|
|
2459
|
+
turnstileToken: turnstileToken ?? void 0
|
|
2460
|
+
});
|
|
2461
|
+
persistClaimToken(client);
|
|
2462
|
+
} else {
|
|
2463
|
+
await client.startSession({
|
|
2464
|
+
...toSessionInput(identity),
|
|
2465
|
+
turnstileToken: turnstileToken ?? void 0
|
|
2466
|
+
});
|
|
2467
|
+
persistClaimToken(client);
|
|
2468
|
+
requestId = await startRequest();
|
|
2469
|
+
await client.saveAddress(requestId, {
|
|
2470
|
+
address: address.place.address,
|
|
2471
|
+
placeId: address.place?.placeId ?? null
|
|
2472
|
+
});
|
|
2473
|
+
}
|
|
2356
2474
|
onStarted(requestId, identity);
|
|
2357
2475
|
} catch (err) {
|
|
2358
2476
|
setBusy(false);
|
|
@@ -2376,6 +2494,10 @@ function IdentityStep(props) {
|
|
|
2376
2494
|
setError(errorMessage(err, "We could not start your request. Please try again."));
|
|
2377
2495
|
}
|
|
2378
2496
|
}
|
|
2497
|
+
const reservationNote = props.reservationExpiresAt ? ` Your appointment time is reserved until ${new Intl.DateTimeFormat("en-AU", {
|
|
2498
|
+
hour: "numeric",
|
|
2499
|
+
minute: "2-digit"
|
|
2500
|
+
}).format(new Date(props.reservationExpiresAt))}.` : "";
|
|
2379
2501
|
if (mode === "confirm-identity") {
|
|
2380
2502
|
return /* @__PURE__ */ jsxs8(
|
|
2381
2503
|
FlowShell,
|
|
@@ -2383,7 +2505,7 @@ function IdentityStep(props) {
|
|
|
2383
2505
|
...progress,
|
|
2384
2506
|
stepKey: "confirm-identity",
|
|
2385
2507
|
heading: "Confirm your details",
|
|
2386
|
-
subheading: detailsSubheading(props.serviceLabel)
|
|
2508
|
+
subheading: `${detailsSubheading(props.serviceLabel)}${reservationNote}`,
|
|
2387
2509
|
onBack,
|
|
2388
2510
|
documentTitle,
|
|
2389
2511
|
children: [
|
|
@@ -2408,12 +2530,12 @@ function IdentityStep(props) {
|
|
|
2408
2530
|
...progress,
|
|
2409
2531
|
stepKey: "identity",
|
|
2410
2532
|
heading: "Your details",
|
|
2411
|
-
subheading: detailsSubheading(props.serviceLabel)
|
|
2533
|
+
subheading: `${detailsSubheading(props.serviceLabel)}${reservationNote}`,
|
|
2412
2534
|
onBack,
|
|
2413
2535
|
documentTitle,
|
|
2414
2536
|
footer: probing ? void 0 : /* @__PURE__ */ jsx10(FlowButton, { type: "submit", form: "sk-cf-identity", variant: "primary", busy, children: busy ? "Starting\u2026" : "Continue" }),
|
|
2415
2537
|
children: probing ? /* @__PURE__ */ jsx10(FlowLoading, { message: "Checking if you\u2019re signed in\u2026" }) : /* @__PURE__ */ jsxs8(Fragment3, { children: [
|
|
2416
|
-
/* @__PURE__ */ jsx10(SignInRamp, { redirect: props.returnUrl }),
|
|
2538
|
+
/* @__PURE__ */ jsx10(SignInRamp, { redirect: props.returnUrl, prepareRedirect: prepareSignInRedirect }),
|
|
2417
2539
|
/* @__PURE__ */ jsx10(FlowErrorSummary, { fields: invalid }),
|
|
2418
2540
|
/* @__PURE__ */ jsxs8("form", { id: "sk-cf-identity", onSubmit: (e) => void handleSubmitAnonymous(e), noValidate: true, children: [
|
|
2419
2541
|
/* @__PURE__ */ jsx10(SummarisedFieldErrors, { children: /* @__PURE__ */ jsx10(
|
|
@@ -2743,6 +2865,9 @@ function intakeRowsAction(rows) {
|
|
|
2743
2865
|
function IntakeStep({
|
|
2744
2866
|
client,
|
|
2745
2867
|
requestId,
|
|
2868
|
+
applicationRunId,
|
|
2869
|
+
applicationRunRevision = 0,
|
|
2870
|
+
onApplicationRunChange,
|
|
2746
2871
|
questionSetId,
|
|
2747
2872
|
resolveError,
|
|
2748
2873
|
onGate,
|
|
@@ -2756,6 +2881,15 @@ function IntakeStep({
|
|
|
2756
2881
|
const [draft, setDraft] = React10.useState(null);
|
|
2757
2882
|
const [busy, setBusy] = React10.useState(false);
|
|
2758
2883
|
const [error2, setError] = React10.useState(null);
|
|
2884
|
+
const [runRevision, setRunRevision] = React10.useState(applicationRunRevision);
|
|
2885
|
+
const [completed, setCompleted] = React10.useState(false);
|
|
2886
|
+
const [emptyCompletionRetry, setEmptyCompletionRetry] = React10.useState(0);
|
|
2887
|
+
const commandRetrier = React10.useRef(new ApplicationRunCommandRetrier()).current;
|
|
2888
|
+
const pendingCompletionRevision = React10.useRef(null);
|
|
2889
|
+
const emptyCompletionAttempted = React10.useRef(false);
|
|
2890
|
+
React10.useEffect(() => {
|
|
2891
|
+
setRunRevision(applicationRunRevision);
|
|
2892
|
+
}, [applicationRunRevision]);
|
|
2759
2893
|
const [reload2, setReload] = React10.useState(0);
|
|
2760
2894
|
const progress = useStepProgress("intake", index);
|
|
2761
2895
|
React10.useEffect(() => {
|
|
@@ -2784,8 +2918,28 @@ function IntakeStep({
|
|
|
2784
2918
|
};
|
|
2785
2919
|
}, [client, questionSetId, reload2]);
|
|
2786
2920
|
React10.useEffect(() => {
|
|
2787
|
-
if (questions
|
|
2788
|
-
|
|
2921
|
+
if (!questions || questions.length !== 0 || busy || completed || emptyCompletionAttempted.current) return;
|
|
2922
|
+
emptyCompletionAttempted.current = true;
|
|
2923
|
+
if (!applicationRunId) {
|
|
2924
|
+
setCompleted(true);
|
|
2925
|
+
onComplete();
|
|
2926
|
+
return;
|
|
2927
|
+
}
|
|
2928
|
+
setBusy(true);
|
|
2929
|
+
void commandRetrier.act(client, applicationRunId, {
|
|
2930
|
+
expectedRevision: runRevision,
|
|
2931
|
+
action: { type: "complete_screening" }
|
|
2932
|
+
}).then((run) => {
|
|
2933
|
+
setRunRevision(run.revision);
|
|
2934
|
+
onApplicationRunChange?.(run);
|
|
2935
|
+
const exit = gateToExit(run.gate);
|
|
2936
|
+
if (exit) onGate(exit);
|
|
2937
|
+
else {
|
|
2938
|
+
setCompleted(true);
|
|
2939
|
+
onComplete();
|
|
2940
|
+
}
|
|
2941
|
+
}).catch((e) => setError(errorMessage(e, "We could not complete the screening. Please try again."))).finally(() => setBusy(false));
|
|
2942
|
+
}, [questions, busy, completed, emptyCompletionRetry, applicationRunId, client, runRevision, onApplicationRunChange, onGate, onComplete]);
|
|
2789
2943
|
const visible = React10.useMemo(
|
|
2790
2944
|
() => questions ? visibleQuestions(questions, answers) : [],
|
|
2791
2945
|
[questions, answers]
|
|
@@ -2795,17 +2949,60 @@ function IntakeStep({
|
|
|
2795
2949
|
React10.useEffect(() => {
|
|
2796
2950
|
setDraft(current ? answers[current.key] ?? null : null);
|
|
2797
2951
|
}, [current?.key]);
|
|
2952
|
+
async function retryPendingCompletion() {
|
|
2953
|
+
if (busy || !applicationRunId || pendingCompletionRevision.current == null) return;
|
|
2954
|
+
setBusy(true);
|
|
2955
|
+
setError(null);
|
|
2956
|
+
try {
|
|
2957
|
+
const completedRun = await commandRetrier.act(client, applicationRunId, {
|
|
2958
|
+
expectedRevision: pendingCompletionRevision.current,
|
|
2959
|
+
action: { type: "complete_screening" }
|
|
2960
|
+
});
|
|
2961
|
+
pendingCompletionRevision.current = null;
|
|
2962
|
+
setRunRevision(completedRun.revision);
|
|
2963
|
+
onApplicationRunChange?.(completedRun);
|
|
2964
|
+
const completedExit = gateToExit(completedRun.gate);
|
|
2965
|
+
if (completedExit) onGate(completedExit);
|
|
2966
|
+
else {
|
|
2967
|
+
setCompleted(true);
|
|
2968
|
+
onComplete();
|
|
2969
|
+
}
|
|
2970
|
+
} catch (e) {
|
|
2971
|
+
setError(errorMessage(e, "We could not complete the screening. Please try again."));
|
|
2972
|
+
} finally {
|
|
2973
|
+
setBusy(false);
|
|
2974
|
+
}
|
|
2975
|
+
}
|
|
2798
2976
|
async function record(value) {
|
|
2799
2977
|
if (!current || busy) return;
|
|
2800
|
-
|
|
2801
|
-
|
|
2978
|
+
if (applicationRunId && pendingCompletionRevision.current != null) {
|
|
2979
|
+
await retryPendingCompletion();
|
|
2980
|
+
return;
|
|
2981
|
+
}
|
|
2802
2982
|
setBusy(true);
|
|
2803
2983
|
setError(null);
|
|
2984
|
+
const next = { ...answers, [current.key]: value };
|
|
2985
|
+
setAnswers(next);
|
|
2804
2986
|
try {
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2987
|
+
let savedRun = null;
|
|
2988
|
+
let gate;
|
|
2989
|
+
if (applicationRunId) {
|
|
2990
|
+
savedRun = await commandRetrier.act(client, applicationRunId, {
|
|
2991
|
+
expectedRevision: runRevision,
|
|
2992
|
+
action: {
|
|
2993
|
+
type: "save_answers",
|
|
2994
|
+
answers: next
|
|
2995
|
+
}
|
|
2996
|
+
});
|
|
2997
|
+
setRunRevision(savedRun.revision);
|
|
2998
|
+
onApplicationRunChange?.(savedRun);
|
|
2999
|
+
gate = savedRun.gate;
|
|
3000
|
+
} else {
|
|
3001
|
+
gate = (await client.saveAnswers(
|
|
3002
|
+
requestId ?? "",
|
|
3003
|
+
next
|
|
3004
|
+
)).gate;
|
|
3005
|
+
}
|
|
2809
3006
|
const exit = gateToExit(gate);
|
|
2810
3007
|
if (exit) {
|
|
2811
3008
|
onGate(exit);
|
|
@@ -2813,7 +3010,25 @@ function IntakeStep({
|
|
|
2813
3010
|
}
|
|
2814
3011
|
const nextVisible = questions ? visibleQuestions(questions, next) : [];
|
|
2815
3012
|
if (index + 1 >= nextVisible.length) {
|
|
2816
|
-
|
|
3013
|
+
if (applicationRunId && savedRun) {
|
|
3014
|
+
pendingCompletionRevision.current = savedRun.revision;
|
|
3015
|
+
const completedRun = await commandRetrier.act(client, applicationRunId, {
|
|
3016
|
+
expectedRevision: savedRun.revision,
|
|
3017
|
+
action: { type: "complete_screening" }
|
|
3018
|
+
});
|
|
3019
|
+
pendingCompletionRevision.current = null;
|
|
3020
|
+
setRunRevision(completedRun.revision);
|
|
3021
|
+
onApplicationRunChange?.(completedRun);
|
|
3022
|
+
const completedExit = gateToExit(completedRun.gate);
|
|
3023
|
+
if (completedExit) onGate(completedExit);
|
|
3024
|
+
else {
|
|
3025
|
+
setCompleted(true);
|
|
3026
|
+
onComplete();
|
|
3027
|
+
}
|
|
3028
|
+
} else {
|
|
3029
|
+
setCompleted(true);
|
|
3030
|
+
onComplete();
|
|
3031
|
+
}
|
|
2817
3032
|
return;
|
|
2818
3033
|
}
|
|
2819
3034
|
setIndex(index + 1);
|
|
@@ -2859,6 +3074,23 @@ function IntakeStep({
|
|
|
2859
3074
|
}
|
|
2860
3075
|
);
|
|
2861
3076
|
}
|
|
3077
|
+
if (questions.length === 0) {
|
|
3078
|
+
return /* @__PURE__ */ jsx12(
|
|
3079
|
+
IntakeInterstitial,
|
|
3080
|
+
{
|
|
3081
|
+
progress,
|
|
3082
|
+
documentTitle,
|
|
3083
|
+
onBack,
|
|
3084
|
+
error: error2,
|
|
3085
|
+
message: busy || !error2 ? "Completing your screening\u2026" : void 0,
|
|
3086
|
+
onRetry: error2 ? () => {
|
|
3087
|
+
emptyCompletionAttempted.current = false;
|
|
3088
|
+
setError(null);
|
|
3089
|
+
setEmptyCompletionRetry((value) => value + 1);
|
|
3090
|
+
} : void 0
|
|
3091
|
+
}
|
|
3092
|
+
);
|
|
3093
|
+
}
|
|
2862
3094
|
if (!current) {
|
|
2863
3095
|
return /* @__PURE__ */ jsx12(
|
|
2864
3096
|
IntakeInterstitial,
|
|
@@ -2906,7 +3138,17 @@ function IntakeStep({
|
|
|
2906
3138
|
idPrefix: `sk-cf-${index}`
|
|
2907
3139
|
}
|
|
2908
3140
|
),
|
|
2909
|
-
/* @__PURE__ */ jsx12(FlowError, { children: error2 })
|
|
3141
|
+
/* @__PURE__ */ jsx12(FlowError, { children: error2 }),
|
|
3142
|
+
error2 && pendingCompletionRevision.current != null && /* @__PURE__ */ jsx12(
|
|
3143
|
+
FlowButton,
|
|
3144
|
+
{
|
|
3145
|
+
type: "button",
|
|
3146
|
+
variant: "primary",
|
|
3147
|
+
busy,
|
|
3148
|
+
onClick: () => void retryPendingCompletion(),
|
|
3149
|
+
children: "Try again"
|
|
3150
|
+
}
|
|
3151
|
+
)
|
|
2910
3152
|
]
|
|
2911
3153
|
}
|
|
2912
3154
|
);
|
|
@@ -3310,7 +3552,7 @@ function ConsentStep({
|
|
|
3310
3552
|
onGate(gate ?? { kind: "block" });
|
|
3311
3553
|
return;
|
|
3312
3554
|
}
|
|
3313
|
-
onSubmitted(result.status, gate);
|
|
3555
|
+
await onSubmitted(result.status, gate);
|
|
3314
3556
|
} catch (err) {
|
|
3315
3557
|
setBusy(false);
|
|
3316
3558
|
setError(errorMessage(err, "We could not submit your request. Please try again."));
|
|
@@ -3415,6 +3657,10 @@ function SlotCancelControl({
|
|
|
3415
3657
|
function SlotStep({
|
|
3416
3658
|
client,
|
|
3417
3659
|
requestId,
|
|
3660
|
+
applicationRunId,
|
|
3661
|
+
applicationRunRevision = 0,
|
|
3662
|
+
onApplicationRunChange,
|
|
3663
|
+
replacementReservation = false,
|
|
3418
3664
|
appointmentTypeId,
|
|
3419
3665
|
isFree,
|
|
3420
3666
|
onBooked,
|
|
@@ -3425,13 +3671,15 @@ function SlotStep({
|
|
|
3425
3671
|
const [cancelling, setCancelling] = React13.useState(false);
|
|
3426
3672
|
const [confirmCancel, setConfirmCancel] = React13.useState(false);
|
|
3427
3673
|
const [cancelError, setCancelError] = React13.useState(null);
|
|
3674
|
+
const commandRetrier = React13.useRef(new ApplicationRunCommandRetrier()).current;
|
|
3428
3675
|
const progress = useStepProgress("slot");
|
|
3429
3676
|
async function releaseAndExit() {
|
|
3430
3677
|
if (cancelling) return;
|
|
3431
3678
|
setCancelling(true);
|
|
3432
3679
|
setCancelError(null);
|
|
3433
3680
|
try {
|
|
3434
|
-
await client.
|
|
3681
|
+
if (applicationRunId) await client.closeApplicationRun(applicationRunId);
|
|
3682
|
+
else await client.cancelRequest(requestId ?? "");
|
|
3435
3683
|
onCancelled();
|
|
3436
3684
|
} catch (err) {
|
|
3437
3685
|
setCancelling(false);
|
|
@@ -3457,18 +3705,33 @@ function SlotStep({
|
|
|
3457
3705
|
appointmentTypeId,
|
|
3458
3706
|
mode: "request",
|
|
3459
3707
|
onConfirm: async (slot, tz) => {
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3708
|
+
if (applicationRunId) {
|
|
3709
|
+
const run = await commandRetrier.act(client, applicationRunId, {
|
|
3710
|
+
expectedRevision: applicationRunRevision,
|
|
3711
|
+
action: {
|
|
3712
|
+
type: "reserve_slot",
|
|
3713
|
+
practitionerId: slot.practitionerId,
|
|
3714
|
+
slotStart: slot.start
|
|
3715
|
+
}
|
|
3716
|
+
});
|
|
3717
|
+
onApplicationRunChange?.(run);
|
|
3718
|
+
} else {
|
|
3719
|
+
const hold = replacementReservation ? await client.replaceRequestSlot(requestId ?? "", {
|
|
3720
|
+
practitionerId: slot.practitionerId,
|
|
3721
|
+
slotStart: slot.start
|
|
3722
|
+
}) : await client.createHold({
|
|
3723
|
+
practitionerId: slot.practitionerId,
|
|
3724
|
+
appointmentTypeId,
|
|
3725
|
+
slotStart: slot.start,
|
|
3726
|
+
serviceRequestId: requestId
|
|
3727
|
+
});
|
|
3728
|
+
await client.bookRequest(requestId ?? "", {
|
|
3729
|
+
practitionerId: slot.practitionerId,
|
|
3730
|
+
slotStart: slot.start,
|
|
3731
|
+
holdId: hold.holdId,
|
|
3732
|
+
holderToken: hold.holderToken
|
|
3733
|
+
});
|
|
3734
|
+
}
|
|
3472
3735
|
onBooked(formatBookedFor(slot.start, tz));
|
|
3473
3736
|
}
|
|
3474
3737
|
}
|
|
@@ -3485,11 +3748,12 @@ function SlotStep({
|
|
|
3485
3748
|
clinicPhone
|
|
3486
3749
|
] })
|
|
3487
3750
|
] }),
|
|
3488
|
-
isFree === false && /* @__PURE__ */ jsxs13("p", { className: "sk-slots__empty-note", children: [
|
|
3751
|
+
isFree === false && !applicationRunId && /* @__PURE__ */ jsxs13("p", { className: "sk-slots__empty-note", children: [
|
|
3489
3752
|
"You haven\u2019t been charged \u2014 only a hold was placed on your card.",
|
|
3490
3753
|
" ",
|
|
3491
3754
|
HOLD_RELEASE_NOTE
|
|
3492
|
-
] })
|
|
3755
|
+
] }),
|
|
3756
|
+
applicationRunId && /* @__PURE__ */ jsx15("p", { className: "sk-slots__empty-note", children: "We\u2019ll reserve this time for 10 minutes while you confirm your details and payment." })
|
|
3493
3757
|
] }),
|
|
3494
3758
|
/* @__PURE__ */ jsx15(
|
|
3495
3759
|
SlotCancelControl,
|
|
@@ -4035,6 +4299,13 @@ function CertificateFunnelClient(props) {
|
|
|
4035
4299
|
}
|
|
4036
4300
|
return /* @__PURE__ */ jsx19(PortalProvider, { client: portalClient, children: /* @__PURE__ */ jsx19(Funnel, { ...props }) });
|
|
4037
4301
|
}
|
|
4302
|
+
var RESERVATION_REQUIRED_STEPS = /* @__PURE__ */ new Set([
|
|
4303
|
+
"identity",
|
|
4304
|
+
"confirm-identity",
|
|
4305
|
+
"medicare",
|
|
4306
|
+
"consent",
|
|
4307
|
+
"payment"
|
|
4308
|
+
]);
|
|
4038
4309
|
function Funnel({
|
|
4039
4310
|
publicApi,
|
|
4040
4311
|
services = [],
|
|
@@ -4044,6 +4315,7 @@ function Funnel({
|
|
|
4044
4315
|
serviceKey: pinnedService
|
|
4045
4316
|
}) {
|
|
4046
4317
|
const api = publicApi;
|
|
4318
|
+
const portalClient = usePortalClient();
|
|
4047
4319
|
const rawClient = React16.useMemo(
|
|
4048
4320
|
() => patientos({ publishableKey: api.publishableKey, apiBase: api.apiBase }),
|
|
4049
4321
|
[api.publishableKey, api.apiBase]
|
|
@@ -4051,15 +4323,17 @@ function Funnel({
|
|
|
4051
4323
|
const pinned = React16.useMemo(() => ({ pinnedService: !!pinnedService }), [pinnedService]);
|
|
4052
4324
|
const [state, setState] = React16.useState(null);
|
|
4053
4325
|
const [flow, setFlow] = React16.useState(null);
|
|
4326
|
+
const [applicationRun, setApplicationRun] = React16.useState(null);
|
|
4054
4327
|
const [flowChecked, setFlowChecked] = React16.useState(false);
|
|
4055
4328
|
const [capturing, setCapturing] = React16.useState(false);
|
|
4056
4329
|
const [cancellingLostSlot, setCancellingLostSlot] = React16.useState(false);
|
|
4057
4330
|
const [lostSlotCancelError, setLostSlotCancelError] = React16.useState(null);
|
|
4058
4331
|
const [resolveError, setResolveError] = React16.useState(null);
|
|
4332
|
+
const [reopeningScreening, setReopeningScreening] = React16.useState(false);
|
|
4333
|
+
const [reopenScreeningError, setReopenScreeningError] = React16.useState(null);
|
|
4334
|
+
const applicationRunCommands = React16.useRef(new ApplicationRunCommandRetrier()).current;
|
|
4335
|
+
const beginAttemptRef = React16.useRef(null);
|
|
4059
4336
|
const [identityDraft, setIdentityDraft] = React16.useState(null);
|
|
4060
|
-
const [submittedIdentitySnapshot, setSubmittedIdentitySnapshot] = React16.useState(
|
|
4061
|
-
null
|
|
4062
|
-
);
|
|
4063
4337
|
const onDraftChange = React16.useCallback((draft) => setIdentityDraft(draft), []);
|
|
4064
4338
|
const [medicareDraft, setMedicareDraft] = React16.useState(null);
|
|
4065
4339
|
const onMedicareDraftChange = React16.useCallback(
|
|
@@ -4097,6 +4371,7 @@ function Funnel({
|
|
|
4097
4371
|
null
|
|
4098
4372
|
);
|
|
4099
4373
|
const [questionCounts, setQuestionCounts] = React16.useState({});
|
|
4374
|
+
const [enabledServiceKeys, setEnabledServiceKeys] = React16.useState(null);
|
|
4100
4375
|
React16.useEffect(() => {
|
|
4101
4376
|
let alive = true;
|
|
4102
4377
|
void (async () => {
|
|
@@ -4109,7 +4384,10 @@ function Funnel({
|
|
|
4109
4384
|
const nextFees = {};
|
|
4110
4385
|
const nextDeliveryModes = {};
|
|
4111
4386
|
const nextCounts = {};
|
|
4387
|
+
const nextEnabledKeys = /* @__PURE__ */ new Set();
|
|
4112
4388
|
for (const svc of svcs) {
|
|
4389
|
+
if (svc.anonymousApplicationRuns === false) continue;
|
|
4390
|
+
nextEnabledKeys.add(svc.key);
|
|
4113
4391
|
const option = pickDeliveryOption(options, svc.id);
|
|
4114
4392
|
nextFees[svc.key] = effectiveFee(svc, option);
|
|
4115
4393
|
if (option) nextDeliveryModes[svc.key] = option.deliveryMode;
|
|
@@ -4118,10 +4396,12 @@ function Funnel({
|
|
|
4118
4396
|
setFees(nextFees);
|
|
4119
4397
|
setDeliveryModes(nextDeliveryModes);
|
|
4120
4398
|
setQuestionCounts(nextCounts);
|
|
4399
|
+
setEnabledServiceKeys(nextEnabledKeys);
|
|
4121
4400
|
} catch {
|
|
4122
4401
|
if (alive) {
|
|
4123
4402
|
setFees({});
|
|
4124
4403
|
setDeliveryModes({});
|
|
4404
|
+
setEnabledServiceKeys(/* @__PURE__ */ new Set());
|
|
4125
4405
|
}
|
|
4126
4406
|
}
|
|
4127
4407
|
})();
|
|
@@ -4148,66 +4428,221 @@ function Funnel({
|
|
|
4148
4428
|
return () => window.removeEventListener("popstate", onPop);
|
|
4149
4429
|
}, [dispatch, pinnedService, pinned]);
|
|
4150
4430
|
const requestId = state?.requestId ?? null;
|
|
4431
|
+
const applicationRunId = state?.applicationRunId ?? null;
|
|
4432
|
+
const flowId = requestId ?? applicationRunId;
|
|
4151
4433
|
React16.useEffect(() => {
|
|
4152
|
-
if (!
|
|
4434
|
+
if (!flowId) {
|
|
4153
4435
|
setFlow(null);
|
|
4154
4436
|
setFlowChecked(false);
|
|
4437
|
+
setApplicationRun(null);
|
|
4155
4438
|
return;
|
|
4156
4439
|
}
|
|
4157
|
-
if (flow)
|
|
4158
|
-
|
|
4440
|
+
if (!flow) setFlow(loadFlowRecord(flowId));
|
|
4441
|
+
setFlowChecked(true);
|
|
4442
|
+
}, [flowId, flow]);
|
|
4443
|
+
const hydrateRunFlow = React16.useCallback(async (run) => {
|
|
4444
|
+
const existing = loadFlowRecord(run.id);
|
|
4445
|
+
if (existing) {
|
|
4446
|
+
setFlow(existing);
|
|
4159
4447
|
return;
|
|
4160
4448
|
}
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
|
|
4165
|
-
|
|
4449
|
+
const [svc, options] = await Promise.all([
|
|
4450
|
+
findServiceByKey(client, run.serviceKey),
|
|
4451
|
+
once(client.subscribeDeliveryOptions)
|
|
4452
|
+
]);
|
|
4453
|
+
const option = options.find((candidate) => candidate.id === run.serviceDeliveryOptionId);
|
|
4454
|
+
if (!svc || !option) throw new Error("This application option is no longer available.");
|
|
4455
|
+
const record = {
|
|
4456
|
+
serviceId: svc.id,
|
|
4457
|
+
serviceKey: svc.key,
|
|
4458
|
+
serviceLabel: svc.label,
|
|
4459
|
+
deliveryOptionId: option.id,
|
|
4460
|
+
appointmentTypeId: run.appointmentTypeId ?? "",
|
|
4461
|
+
feeAmount: option.feeOverride ?? svc.feeAmount,
|
|
4462
|
+
requiresPayment: svc.requiresPayment,
|
|
4463
|
+
deliveryMode: run.deliveryMode,
|
|
4464
|
+
givenName: "",
|
|
4465
|
+
familyName: "",
|
|
4466
|
+
dateOfBirth: null,
|
|
4467
|
+
questionSetId: run.questionSetId,
|
|
4468
|
+
bookedFor: void 0
|
|
4469
|
+
};
|
|
4470
|
+
saveFlowRecord(run.id, record);
|
|
4471
|
+
setFlow(record);
|
|
4472
|
+
}, [client]);
|
|
4473
|
+
React16.useEffect(() => {
|
|
4474
|
+
if (!applicationRunId || applicationRun?.id === applicationRunId) return;
|
|
4475
|
+
let alive = true;
|
|
4476
|
+
void (async () => {
|
|
4477
|
+
const params = new URLSearchParams(window.location.search);
|
|
4478
|
+
const handoffId = params.get("cfhandoff");
|
|
4479
|
+
if (handoffId) {
|
|
4480
|
+
const redeemed = await portalClient.redeemApplicationRunHandoff(handoffId);
|
|
4481
|
+
if (!redeemed || !alive) throw new Error("This sign-in continuation has expired.");
|
|
4482
|
+
rawClient.setApplicationRunToken(applicationRunId, redeemed.capabilityToken);
|
|
4483
|
+
saveApplicationRunToken(rawClient, applicationRunId);
|
|
4484
|
+
setApplicationRun(redeemed.run);
|
|
4485
|
+
await hydrateRunFlow(redeemed.run);
|
|
4486
|
+
const next = {
|
|
4487
|
+
...stateRef.current ?? initialFunnelState(pinnedService),
|
|
4488
|
+
step: redeemed.run.deliveryMode === "scheduled" && !redeemed.run.reservation ? "slot" : "confirm-identity",
|
|
4489
|
+
applicationRunId,
|
|
4490
|
+
serviceKey: redeemed.run.serviceKey,
|
|
4491
|
+
scheduled: redeemed.run.deliveryMode === "scheduled"
|
|
4492
|
+
};
|
|
4493
|
+
dispatch({ type: "RESTORE", state: next });
|
|
4494
|
+
params.delete("cfhandoff");
|
|
4495
|
+
window.history.replaceState(null, "", `${window.location.pathname}?${params.toString()}${window.location.hash}`);
|
|
4496
|
+
return;
|
|
4497
|
+
}
|
|
4498
|
+
if (!restoreApplicationRunToken(rawClient, applicationRunId)) {
|
|
4499
|
+
throw new Error("This application can only be resumed from its secure sign-in link.");
|
|
4500
|
+
}
|
|
4501
|
+
const resumed = await rawClient.resumeApplicationRun(applicationRunId);
|
|
4502
|
+
if (!alive) return;
|
|
4503
|
+
setApplicationRun(resumed);
|
|
4504
|
+
await hydrateRunFlow(resumed);
|
|
4505
|
+
const current = stateRef.current;
|
|
4506
|
+
if (current) {
|
|
4507
|
+
dispatch({
|
|
4508
|
+
type: "RESTORE",
|
|
4509
|
+
state: {
|
|
4510
|
+
...current,
|
|
4511
|
+
step: resumed.deliveryMode === "scheduled" && !resumed.reservation && RESERVATION_REQUIRED_STEPS.has(current.step) ? "slot" : current.step,
|
|
4512
|
+
serviceKey: resumed.serviceKey,
|
|
4513
|
+
scheduled: resumed.deliveryMode === "scheduled",
|
|
4514
|
+
gate: resumed.gate ? { kind: resumed.gate.verdict === "escalate" ? "escalate" : "block", ruleId: resumed.gate.ruleId, message: resumed.gate.message } : current.gate
|
|
4515
|
+
}
|
|
4516
|
+
});
|
|
4517
|
+
}
|
|
4518
|
+
})().catch((err) => {
|
|
4519
|
+
if (alive) setResolveError(err instanceof Error ? err.message : "We could not resume this application.");
|
|
4520
|
+
});
|
|
4521
|
+
return () => {
|
|
4522
|
+
alive = false;
|
|
4523
|
+
};
|
|
4524
|
+
}, [applicationRunId, applicationRun?.id, dispatch, hydrateRunFlow, pinnedService, portalClient, rawClient]);
|
|
4525
|
+
const reopenScreening = React16.useCallback(async () => {
|
|
4526
|
+
if (!applicationRunId || !applicationRun || applicationRun.status !== "screening_complete" && applicationRun.status !== "reserved") {
|
|
4527
|
+
return;
|
|
4528
|
+
}
|
|
4529
|
+
setReopeningScreening(true);
|
|
4530
|
+
setReopenScreeningError(null);
|
|
4531
|
+
try {
|
|
4532
|
+
const run = await applicationRunCommands.act(client, applicationRunId, {
|
|
4533
|
+
expectedRevision: applicationRun.revision,
|
|
4534
|
+
action: { type: "reopen_screening" }
|
|
4535
|
+
});
|
|
4536
|
+
setApplicationRun(run);
|
|
4537
|
+
} catch (err) {
|
|
4538
|
+
setReopenScreeningError(
|
|
4539
|
+
errorMessage(err, "We could not reopen your screening. Please try again.")
|
|
4540
|
+
);
|
|
4541
|
+
} finally {
|
|
4542
|
+
setReopeningScreening(false);
|
|
4543
|
+
}
|
|
4544
|
+
}, [applicationRun, applicationRunCommands, applicationRunId, client]);
|
|
4545
|
+
React16.useEffect(() => {
|
|
4546
|
+
if (state?.step === "intake") void reopenScreening();
|
|
4547
|
+
}, [reopenScreening, state?.step]);
|
|
4548
|
+
const beginRun = React16.useCallback(async (serviceKey) => {
|
|
4549
|
+
if (capturing) return;
|
|
4550
|
+
setCapturing(true);
|
|
4551
|
+
setResolveError(null);
|
|
4552
|
+
try {
|
|
4166
4553
|
const svc = await findServiceByKey(client, serviceKey);
|
|
4167
|
-
if (!svc
|
|
4554
|
+
if (!svc || svc.anonymousApplicationRuns === false) {
|
|
4555
|
+
throw new Error("This certificate type is not currently available.");
|
|
4556
|
+
}
|
|
4168
4557
|
const option = await findDeliveryOption(client, svc.id);
|
|
4169
4558
|
if (!option) throw new Error("No booking option is available for this service.");
|
|
4559
|
+
let attempt = beginAttemptRef.current;
|
|
4560
|
+
if (!attempt || attempt.serviceKey !== serviceKey || attempt.serviceDeliveryOptionId !== option.id) {
|
|
4561
|
+
const bytes = new Uint8Array(32);
|
|
4562
|
+
crypto.getRandomValues(bytes);
|
|
4563
|
+
attempt = {
|
|
4564
|
+
serviceKey,
|
|
4565
|
+
serviceDeliveryOptionId: option.id,
|
|
4566
|
+
idempotencyKey: crypto.randomUUID(),
|
|
4567
|
+
capabilityToken: Array.from(
|
|
4568
|
+
bytes,
|
|
4569
|
+
(b) => b.toString(16).padStart(2, "0")
|
|
4570
|
+
).join("")
|
|
4571
|
+
};
|
|
4572
|
+
beginAttemptRef.current = attempt;
|
|
4573
|
+
}
|
|
4574
|
+
const result = await client.beginApplicationRun(attempt);
|
|
4575
|
+
beginAttemptRef.current = null;
|
|
4576
|
+
saveApplicationRunToken(client, result.run.id);
|
|
4170
4577
|
const record = {
|
|
4171
4578
|
serviceId: svc.id,
|
|
4172
4579
|
serviceKey: svc.key,
|
|
4173
4580
|
serviceLabel: svc.label,
|
|
4174
4581
|
deliveryOptionId: option.id,
|
|
4175
4582
|
appointmentTypeId: option.appointmentTypeId ?? "",
|
|
4176
|
-
// The EFFECTIVE fee: the option's override wins over the service base (PAT-710).
|
|
4177
4583
|
feeAmount: option.feeOverride ?? svc.feeAmount,
|
|
4178
|
-
// Whether money is taken AT ALL — a separate fact from the amount, and the one
|
|
4179
|
-
// the consent wording branches on. A null fee alone could not distinguish
|
|
4180
|
-
// "bulk billed" from "not resolved yet" (PAT-853).
|
|
4181
4584
|
requiresPayment: svc.requiresPayment,
|
|
4182
4585
|
deliveryMode: option.deliveryMode,
|
|
4183
|
-
givenName:
|
|
4184
|
-
familyName:
|
|
4185
|
-
dateOfBirth:
|
|
4586
|
+
givenName: "",
|
|
4587
|
+
familyName: "",
|
|
4588
|
+
dateOfBirth: null,
|
|
4186
4589
|
questionSetId: svc.questionSetId
|
|
4187
4590
|
};
|
|
4188
|
-
saveFlowRecord(
|
|
4591
|
+
saveFlowRecord(result.run.id, record);
|
|
4189
4592
|
setFlow(record);
|
|
4190
|
-
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
|
|
4593
|
+
setApplicationRun(result.run);
|
|
4594
|
+
dispatch({
|
|
4595
|
+
type: "RUN_STARTED",
|
|
4596
|
+
applicationRunId: result.run.id,
|
|
4597
|
+
scheduled: result.run.deliveryMode === "scheduled"
|
|
4598
|
+
});
|
|
4599
|
+
} catch (err) {
|
|
4600
|
+
setResolveError(err instanceof Error ? err.message : "We could not start your application.");
|
|
4601
|
+
} finally {
|
|
4602
|
+
setCapturing(false);
|
|
4603
|
+
}
|
|
4604
|
+
}, [capturing, client, dispatch]);
|
|
4605
|
+
const back2 = React16.useCallback(() => {
|
|
4606
|
+
const runId = state?.applicationRunId;
|
|
4607
|
+
if (state?.step === "intake" && runId) {
|
|
4608
|
+
void client.closeApplicationRun(runId).finally(() => dispatch({ type: "BACK" }));
|
|
4609
|
+
return;
|
|
4610
|
+
}
|
|
4611
|
+
const returnsToIntake = state?.step === "slot" || state?.step === "identity" && state.scheduled === false;
|
|
4612
|
+
if (returnsToIntake && runId && applicationRun && (applicationRun.status === "screening_complete" || applicationRun.status === "reserved")) {
|
|
4613
|
+
void applicationRunCommands.act(client, runId, {
|
|
4614
|
+
expectedRevision: applicationRun.revision,
|
|
4615
|
+
action: { type: "reopen_screening" }
|
|
4616
|
+
}).then((run) => {
|
|
4617
|
+
setApplicationRun(run);
|
|
4618
|
+
dispatch({ type: "BACK" });
|
|
4619
|
+
}).catch((err) => {
|
|
4620
|
+
setResolveError(errorMessage(err, "We could not reopen your screening. Please try again."));
|
|
4621
|
+
});
|
|
4622
|
+
return;
|
|
4623
|
+
}
|
|
4624
|
+
dispatch({ type: "BACK" });
|
|
4625
|
+
}, [applicationRun, applicationRunCommands, client, dispatch, state]);
|
|
4194
4626
|
const restart = React16.useCallback(() => {
|
|
4195
4627
|
setIdentityDraft(null);
|
|
4196
|
-
setSubmittedIdentitySnapshot(null);
|
|
4197
4628
|
setMedicareDraft(null);
|
|
4198
4629
|
dispatch({ type: "RESTART" });
|
|
4199
4630
|
}, [dispatch]);
|
|
4200
4631
|
const retryPayment = React16.useCallback(() => {
|
|
4201
|
-
setSubmittedIdentitySnapshot(null);
|
|
4202
4632
|
dispatch({ type: "RETRY_PAYMENT" });
|
|
4203
4633
|
}, [dispatch]);
|
|
4204
4634
|
const cancelLostSlot = React16.useCallback(async () => {
|
|
4205
4635
|
const id = state?.requestId;
|
|
4206
|
-
|
|
4636
|
+
const runId = state?.applicationRunId;
|
|
4637
|
+
if (!id && !runId || cancellingLostSlot) return;
|
|
4207
4638
|
setCancellingLostSlot(true);
|
|
4208
4639
|
setLostSlotCancelError(null);
|
|
4209
4640
|
try {
|
|
4210
|
-
await cancelRequestThenRestart(client, id, restart);
|
|
4641
|
+
if (id) await cancelRequestThenRestart(client, id, restart);
|
|
4642
|
+
else if (runId) {
|
|
4643
|
+
await client.closeApplicationRun(runId);
|
|
4644
|
+
restart();
|
|
4645
|
+
}
|
|
4211
4646
|
setCancellingLostSlot(false);
|
|
4212
4647
|
} catch (err) {
|
|
4213
4648
|
setCancellingLostSlot(false);
|
|
@@ -4215,7 +4650,7 @@ function Funnel({
|
|
|
4215
4650
|
errorCode(err) === "not_cancellable" ? "This request can no longer be cancelled \u2014 please call the clinic." : "We could not cancel your request. Please try again."
|
|
4216
4651
|
);
|
|
4217
4652
|
}
|
|
4218
|
-
}, [cancellingLostSlot, client, restart, state?.requestId]);
|
|
4653
|
+
}, [cancellingLostSlot, client, restart, state?.applicationRunId, state?.requestId]);
|
|
4219
4654
|
const chosenKey = state?.serviceKey ?? null;
|
|
4220
4655
|
const step = state?.step;
|
|
4221
4656
|
const shape = React16.useMemo(() => {
|
|
@@ -4241,16 +4676,32 @@ function Funnel({
|
|
|
4241
4676
|
}, [flow, fees, pinnedService, chosenKey, step, questionCounts]);
|
|
4242
4677
|
if (!state) return /* @__PURE__ */ jsx19(FlowLoading, {});
|
|
4243
4678
|
const title = stepTitle(state.step, clinicName);
|
|
4244
|
-
const
|
|
4679
|
+
const selectableServices = enabledServiceKeys ? services.filter((candidate) => enabledServiceKeys.has(candidate.key)) : [];
|
|
4680
|
+
const service = selectableServices.find((candidate) => candidate.key === state.serviceKey) ?? null;
|
|
4245
4681
|
const serviceLabel = service?.label ?? "certificate";
|
|
4246
|
-
const identitySnapshot = identityDraft ? JSON.stringify(identityDraft) : null;
|
|
4247
|
-
const resumeRequestId = resumableRequestId(
|
|
4248
|
-
state.requestId,
|
|
4249
|
-
submittedIdentitySnapshot,
|
|
4250
|
-
identitySnapshot
|
|
4251
|
-
);
|
|
4252
4682
|
const showBack = canGoBack(state, pinned);
|
|
4253
4683
|
const flowLost = slotFlowIsLost(flowChecked, flow, capturing);
|
|
4684
|
+
const reservationWasLost = (err) => {
|
|
4685
|
+
const code2 = errorCode(err);
|
|
4686
|
+
return code2 === "reservation_expired" || code2 === "slot_taken";
|
|
4687
|
+
};
|
|
4688
|
+
async function bookReservedSlot() {
|
|
4689
|
+
const id = requestId;
|
|
4690
|
+
const reservation = applicationRun?.reservation;
|
|
4691
|
+
if (!id || !reservation) {
|
|
4692
|
+
throw new PatientOSApiError2({
|
|
4693
|
+
status: 409,
|
|
4694
|
+
code: "reservation_expired",
|
|
4695
|
+
message: "Your appointment reservation has expired. Please choose another time.",
|
|
4696
|
+
path: `/requests/${id ?? "unknown"}/book`
|
|
4697
|
+
});
|
|
4698
|
+
}
|
|
4699
|
+
await client.bookRequest(id, {
|
|
4700
|
+
practitionerId: reservation.practitionerId,
|
|
4701
|
+
slotStart: reservation.slotStart,
|
|
4702
|
+
holdId: reservation.holdId
|
|
4703
|
+
});
|
|
4704
|
+
}
|
|
4254
4705
|
const quietMoney = HOLDLESS_STEPS.has(state.step) || state.step === "consent" || state.step === "payment";
|
|
4255
4706
|
const rail = /* @__PURE__ */ jsx19(
|
|
4256
4707
|
TrustRail,
|
|
@@ -4265,7 +4716,7 @@ function Funnel({
|
|
|
4265
4716
|
return /* @__PURE__ */ jsx19(
|
|
4266
4717
|
ServiceStep,
|
|
4267
4718
|
{
|
|
4268
|
-
services,
|
|
4719
|
+
services: selectableServices,
|
|
4269
4720
|
fees,
|
|
4270
4721
|
deliveryModes,
|
|
4271
4722
|
heading: heading2,
|
|
@@ -4276,69 +4727,111 @@ function Funnel({
|
|
|
4276
4727
|
}
|
|
4277
4728
|
);
|
|
4278
4729
|
case "preflight":
|
|
4279
|
-
return /* @__PURE__ */ jsx19(
|
|
4280
|
-
|
|
4281
|
-
|
|
4282
|
-
|
|
4283
|
-
|
|
4284
|
-
|
|
4285
|
-
|
|
4286
|
-
|
|
4287
|
-
|
|
4288
|
-
|
|
4730
|
+
if (!enabledServiceKeys) return /* @__PURE__ */ jsx19(FlowLoading, {});
|
|
4731
|
+
if (!state.serviceKey || !enabledServiceKeys.has(state.serviceKey)) {
|
|
4732
|
+
return /* @__PURE__ */ jsxs17(
|
|
4733
|
+
FlowShell,
|
|
4734
|
+
{
|
|
4735
|
+
...stepProgress("preflight", shape),
|
|
4736
|
+
stepKey: "preflight:unavailable",
|
|
4737
|
+
heading: "This certificate type is unavailable",
|
|
4738
|
+
documentTitle: title,
|
|
4739
|
+
onBack: showBack ? back2 : void 0,
|
|
4740
|
+
children: [
|
|
4741
|
+
/* @__PURE__ */ jsx19(FlowError, { children: "This certificate type is not currently available through this website." }),
|
|
4742
|
+
/* @__PURE__ */ jsx19(
|
|
4743
|
+
FlowButton,
|
|
4744
|
+
{
|
|
4745
|
+
type: "button",
|
|
4746
|
+
variant: "primary",
|
|
4747
|
+
onClick: () => dispatch({ type: "NEED_DIFFERENT" }),
|
|
4748
|
+
children: "Choose another option"
|
|
4749
|
+
}
|
|
4750
|
+
)
|
|
4751
|
+
]
|
|
4752
|
+
}
|
|
4753
|
+
);
|
|
4754
|
+
}
|
|
4755
|
+
return /* @__PURE__ */ jsxs17("div", { children: [
|
|
4756
|
+
/* @__PURE__ */ jsx19(
|
|
4757
|
+
PreflightStep,
|
|
4758
|
+
{
|
|
4759
|
+
documentTitle: title,
|
|
4760
|
+
onConfirm: () => void beginRun(state.serviceKey ?? ""),
|
|
4761
|
+
onExit: (gate) => dispatch({ type: "GATE", gate }),
|
|
4762
|
+
onOutOfScope: () => dispatch({ type: "NEED_DIFFERENT" }),
|
|
4763
|
+
onBack: showBack ? back2 : void 0
|
|
4764
|
+
}
|
|
4765
|
+
),
|
|
4766
|
+
/* @__PURE__ */ jsx19(FlowError, { children: resolveError })
|
|
4767
|
+
] });
|
|
4289
4768
|
case "identity":
|
|
4290
4769
|
case "confirm-identity":
|
|
4291
|
-
return /* @__PURE__ */ jsxs17(
|
|
4292
|
-
|
|
4293
|
-
|
|
4294
|
-
|
|
4295
|
-
|
|
4296
|
-
|
|
4297
|
-
|
|
4298
|
-
|
|
4299
|
-
|
|
4300
|
-
|
|
4301
|
-
|
|
4302
|
-
|
|
4303
|
-
|
|
4304
|
-
|
|
4305
|
-
|
|
4306
|
-
|
|
4307
|
-
|
|
4308
|
-
|
|
4309
|
-
|
|
4310
|
-
|
|
4311
|
-
|
|
4312
|
-
|
|
4313
|
-
|
|
4314
|
-
|
|
4315
|
-
|
|
4316
|
-
|
|
4317
|
-
|
|
4318
|
-
|
|
4319
|
-
|
|
4320
|
-
|
|
4321
|
-
|
|
4322
|
-
|
|
4323
|
-
|
|
4324
|
-
setSubmittedIdentitySnapshot(JSON.stringify(capturedDraft));
|
|
4325
|
-
dispatch({ type: "REQUEST_STARTED", requestId: newRequestId });
|
|
4326
|
-
void captureFlow(newRequestId, state.serviceKey ?? "", identity).catch(
|
|
4327
|
-
(e) => setResolveError(e instanceof Error ? e.message : "Something went wrong.")
|
|
4328
|
-
).finally(() => setCapturing(false));
|
|
4329
|
-
}
|
|
4770
|
+
return /* @__PURE__ */ jsxs17("div", { children: [
|
|
4771
|
+
/* @__PURE__ */ jsx19(
|
|
4772
|
+
IdentityStep,
|
|
4773
|
+
{
|
|
4774
|
+
client,
|
|
4775
|
+
turnstileSiteKey: api.turnstileSiteKey,
|
|
4776
|
+
serviceKey: state.serviceKey ?? "",
|
|
4777
|
+
serviceLabel,
|
|
4778
|
+
applicationRunId: state.applicationRunId ?? void 0,
|
|
4779
|
+
reservationExpiresAt: applicationRun?.reservation?.expiresAt ?? null,
|
|
4780
|
+
mode: state.step,
|
|
4781
|
+
documentTitle: title,
|
|
4782
|
+
onSignedIn: () => dispatch({ type: "SIGNED_IN" }),
|
|
4783
|
+
onRejected: () => {
|
|
4784
|
+
setIdentityDraft(null);
|
|
4785
|
+
setMedicareDraft(null);
|
|
4786
|
+
dispatch({ type: "IDENTITY_REJECTED" });
|
|
4787
|
+
},
|
|
4788
|
+
onBack: showBack ? back2 : void 0,
|
|
4789
|
+
draft: identityDraft ?? void 0,
|
|
4790
|
+
onDraftChange,
|
|
4791
|
+
onStarted: (newRequestId, identity) => {
|
|
4792
|
+
const capturedDraft = capturedIdentityDraft(identity, identityDraft);
|
|
4793
|
+
setIdentityDraft(capturedDraft);
|
|
4794
|
+
if (flow) {
|
|
4795
|
+
const next = {
|
|
4796
|
+
...flow,
|
|
4797
|
+
givenName: identity.givenName,
|
|
4798
|
+
familyName: identity.familyName,
|
|
4799
|
+
dateOfBirth: identity.dateOfBirth || null
|
|
4800
|
+
};
|
|
4801
|
+
saveFlowRecord(newRequestId, next);
|
|
4802
|
+
setFlow(next);
|
|
4330
4803
|
}
|
|
4331
|
-
|
|
4332
|
-
|
|
4333
|
-
|
|
4334
|
-
|
|
4335
|
-
|
|
4804
|
+
dispatch({ type: "REQUEST_STARTED", requestId: newRequestId });
|
|
4805
|
+
}
|
|
4806
|
+
}
|
|
4807
|
+
),
|
|
4808
|
+
/* @__PURE__ */ jsx19(FlowError, { children: resolveError })
|
|
4809
|
+
] });
|
|
4336
4810
|
case "intake":
|
|
4811
|
+
if (reopeningScreening || reopenScreeningError || applicationRun?.status === "screening_complete" || applicationRun?.status === "reserved") {
|
|
4812
|
+
return /* @__PURE__ */ jsxs17(
|
|
4813
|
+
FlowShell,
|
|
4814
|
+
{
|
|
4815
|
+
...stepProgress("intake", shape),
|
|
4816
|
+
stepKey: "intake:reopening",
|
|
4817
|
+
heading: "Review your screening",
|
|
4818
|
+
documentTitle: title,
|
|
4819
|
+
onBack: showBack ? back2 : void 0,
|
|
4820
|
+
children: [
|
|
4821
|
+
reopeningScreening && /* @__PURE__ */ jsx19(FlowLoading, { message: "Reopening your screening\u2026" }),
|
|
4822
|
+
/* @__PURE__ */ jsx19(FlowError, { children: reopenScreeningError }),
|
|
4823
|
+
reopenScreeningError && /* @__PURE__ */ jsx19(FlowButton, { type: "button", variant: "primary", onClick: () => void reopenScreening(), children: "Try again" })
|
|
4824
|
+
]
|
|
4825
|
+
}
|
|
4826
|
+
);
|
|
4827
|
+
}
|
|
4337
4828
|
return /* @__PURE__ */ jsx19(
|
|
4338
4829
|
IntakeStep,
|
|
4339
4830
|
{
|
|
4340
4831
|
client,
|
|
4341
|
-
|
|
4832
|
+
applicationRunId: state.applicationRunId ?? void 0,
|
|
4833
|
+
applicationRunRevision: applicationRun?.revision ?? 0,
|
|
4834
|
+
onApplicationRunChange: setApplicationRun,
|
|
4342
4835
|
questionSetId: flow ? flow.questionSetId ?? null : void 0,
|
|
4343
4836
|
resolveError,
|
|
4344
4837
|
documentTitle: title,
|
|
@@ -4389,7 +4882,22 @@ function Funnel({
|
|
|
4389
4882
|
phone: identityDraft?.identity.phone ?? "",
|
|
4390
4883
|
serviceLabel: service?.label ?? "",
|
|
4391
4884
|
documentTitle: title,
|
|
4392
|
-
onSubmitted: (status, gate) =>
|
|
4885
|
+
onSubmitted: async (status, gate) => {
|
|
4886
|
+
if (status === "awaiting_slot") {
|
|
4887
|
+
try {
|
|
4888
|
+
await bookReservedSlot();
|
|
4889
|
+
dispatch({ type: "SUBMITTED", status: "ready", gate });
|
|
4890
|
+
} catch (err) {
|
|
4891
|
+
if (reservationWasLost(err)) {
|
|
4892
|
+
dispatch({ type: "RESERVATION_LOST" });
|
|
4893
|
+
return;
|
|
4894
|
+
}
|
|
4895
|
+
throw err;
|
|
4896
|
+
}
|
|
4897
|
+
} else {
|
|
4898
|
+
dispatch({ type: "SUBMITTED", status, gate });
|
|
4899
|
+
}
|
|
4900
|
+
},
|
|
4393
4901
|
onGate: (gate) => dispatch({ type: "GATE", gate }),
|
|
4394
4902
|
onBack: showBack ? back2 : void 0
|
|
4395
4903
|
}
|
|
@@ -4412,7 +4920,22 @@ function Funnel({
|
|
|
4412
4920
|
feeAmount: flow?.feeAmount ?? null,
|
|
4413
4921
|
serviceLabel,
|
|
4414
4922
|
clinicPhone,
|
|
4415
|
-
onOutcome: (outcome) =>
|
|
4923
|
+
onOutcome: async (outcome) => {
|
|
4924
|
+
if (outcome.kind === "awaiting_slot") {
|
|
4925
|
+
try {
|
|
4926
|
+
await bookReservedSlot();
|
|
4927
|
+
dispatch({ type: "PAY_OUTCOME", outcome: { kind: "ready" } });
|
|
4928
|
+
} catch (err) {
|
|
4929
|
+
if (reservationWasLost(err)) {
|
|
4930
|
+
dispatch({ type: "RESERVATION_LOST" });
|
|
4931
|
+
return;
|
|
4932
|
+
}
|
|
4933
|
+
throw err;
|
|
4934
|
+
}
|
|
4935
|
+
} else {
|
|
4936
|
+
dispatch({ type: "PAY_OUTCOME", outcome });
|
|
4937
|
+
}
|
|
4938
|
+
}
|
|
4416
4939
|
}
|
|
4417
4940
|
)
|
|
4418
4941
|
}
|
|
@@ -4422,20 +4945,24 @@ function Funnel({
|
|
|
4422
4945
|
SlotStep,
|
|
4423
4946
|
{
|
|
4424
4947
|
client,
|
|
4425
|
-
requestId: state.requestId ??
|
|
4948
|
+
requestId: state.requestId ?? void 0,
|
|
4949
|
+
applicationRunId: state.requestId ? void 0 : state.applicationRunId ?? void 0,
|
|
4950
|
+
applicationRunRevision: applicationRun?.revision ?? 0,
|
|
4951
|
+
onApplicationRunChange: setApplicationRun,
|
|
4952
|
+
replacementReservation: Boolean(state.requestId && state.applicationRunId),
|
|
4426
4953
|
appointmentTypeId: flow.appointmentTypeId,
|
|
4427
4954
|
isFree: flowIsFree(flow),
|
|
4428
4955
|
clinicPhone,
|
|
4429
4956
|
onCancelled: restart,
|
|
4430
4957
|
documentTitle: title,
|
|
4431
4958
|
onBooked: (bookedFor) => {
|
|
4432
|
-
const id = state.requestId;
|
|
4959
|
+
const id = state.requestId ?? state.applicationRunId;
|
|
4433
4960
|
if (id && flow) {
|
|
4434
4961
|
const next = { ...flow, bookedFor };
|
|
4435
4962
|
saveFlowRecord(id, next);
|
|
4436
4963
|
setFlow(next);
|
|
4437
4964
|
}
|
|
4438
|
-
dispatch({ type: "BOOKED" });
|
|
4965
|
+
dispatch({ type: state.requestId ? "BOOKED" : "RESERVED" });
|
|
4439
4966
|
}
|
|
4440
4967
|
}
|
|
4441
4968
|
) : flowLost ? (
|
|
@@ -4557,14 +5084,14 @@ function SessionExpiredState({
|
|
|
4557
5084
|
stepKey: "session-expired",
|
|
4558
5085
|
heading: "Your secure session ended",
|
|
4559
5086
|
documentTitle,
|
|
4560
|
-
footer: portalUrl ? /* @__PURE__ */ jsx19("a", { className: "sk-flow__btn sk-flow__btn--primary", href: portalUrl, children: "Continue in the patient portal" }) :
|
|
5087
|
+
footer: portalUrl ? /* @__PURE__ */ jsx19("a", { className: "sk-flow__btn sk-flow__btn--primary", href: portalUrl, children: "Continue in the patient portal" }) : requestStarted ? void 0 : /* @__PURE__ */ jsx19(FlowButton, { type: "button", variant: "primary", onClick: onRestart, children: "Start again securely" }),
|
|
4561
5088
|
children: [
|
|
4562
5089
|
/* @__PURE__ */ jsxs17("p", { className: "sk-cf__body", children: [
|
|
4563
5090
|
"This secure session is no longer available.",
|
|
4564
5091
|
" ",
|
|
4565
5092
|
requestStarted ? "Your request is still with the clinic." : "You can start again securely."
|
|
4566
5093
|
] }),
|
|
4567
|
-
clinicPhone ? /* @__PURE__ */ jsx19(ClinicPhone, { clinicPhone }) :
|
|
5094
|
+
clinicPhone ? /* @__PURE__ */ jsx19(ClinicPhone, { clinicPhone }) : portalUrl ? null : /* @__PURE__ */ jsx19("p", { className: "sk-cf__aside", children: "Please contact the clinic using the details on this website." })
|
|
4568
5095
|
]
|
|
4569
5096
|
}
|
|
4570
5097
|
);
|
|
@@ -10954,14 +11481,10 @@ function PortalAccountSurfaceClient({
|
|
|
10954
11481
|
portalUrl,
|
|
10955
11482
|
bookHref,
|
|
10956
11483
|
initialState,
|
|
10957
|
-
fetchImpl: rawFetchImpl,
|
|
10958
11484
|
openUrl
|
|
10959
11485
|
}) {
|
|
10960
11486
|
const portalClient = usePortalClient();
|
|
10961
|
-
const fetchImpl = React21.useMemo(
|
|
10962
|
-
() => portalFetchForClient(portalClient, rawFetchImpl),
|
|
10963
|
-
[portalClient, rawFetchImpl]
|
|
10964
|
-
);
|
|
11487
|
+
const fetchImpl = React21.useMemo(() => portalFetchForClient(portalClient), [portalClient]);
|
|
10965
11488
|
const [state, setState] = React21.useState(
|
|
10966
11489
|
// `readPortalSignedInHint` is in the initialiser (not an effect) on purpose: the
|
|
10967
11490
|
// whole point is that the FIRST paint already knows, so there is no placeholder
|
|
@@ -11546,6 +12069,25 @@ var EMPTY_CART = Object.freeze({
|
|
|
11546
12069
|
v: 1,
|
|
11547
12070
|
lines: Object.freeze([])
|
|
11548
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
|
+
}
|
|
11549
12091
|
function storage() {
|
|
11550
12092
|
try {
|
|
11551
12093
|
if (typeof window === "undefined" || !window.localStorage) return null;
|
|
@@ -11563,7 +12105,39 @@ function parseLine(raw) {
|
|
|
11563
12105
|
const n = typeof line.quantity === "number" ? line.quantity : Number(line.quantity);
|
|
11564
12106
|
if (!Number.isFinite(n)) return null;
|
|
11565
12107
|
const quantity = Math.min(Math.max(Math.floor(n), 1), MAX_QUANTITY);
|
|
11566
|
-
|
|
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);
|
|
11567
12141
|
}
|
|
11568
12142
|
function readCart() {
|
|
11569
12143
|
const store = storage();
|
|
@@ -11577,75 +12151,216 @@ function readCart() {
|
|
|
11577
12151
|
if (!raw) return emptyCart();
|
|
11578
12152
|
try {
|
|
11579
12153
|
const parsed = JSON.parse(raw);
|
|
11580
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
12154
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
12155
|
+
return emptyCart();
|
|
11581
12156
|
const bag = parsed;
|
|
11582
12157
|
if (bag.v !== 1 || !Array.isArray(bag.lines)) return emptyCart();
|
|
11583
|
-
const
|
|
11584
|
-
|
|
11585
|
-
|
|
11586
|
-
|
|
11587
|
-
|
|
11588
|
-
|
|
12158
|
+
const settledAttemptTokens = normalizeSettledAttemptTokens(
|
|
12159
|
+
bag.settledAttemptTokens
|
|
12160
|
+
);
|
|
12161
|
+
return {
|
|
12162
|
+
v: 1,
|
|
12163
|
+
lines: normalizeCartLines(bag.lines),
|
|
12164
|
+
...settledAttemptTokens.length > 0 ? { settledAttemptTokens } : {}
|
|
12165
|
+
};
|
|
11589
12166
|
} catch {
|
|
11590
12167
|
return emptyCart();
|
|
11591
12168
|
}
|
|
11592
12169
|
}
|
|
11593
|
-
|
|
11594
|
-
|
|
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
|
+
};
|
|
11595
12181
|
const store = storage();
|
|
11596
|
-
if (store)
|
|
11597
|
-
|
|
11598
|
-
|
|
11599
|
-
|
|
11600
|
-
|
|
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
|
+
};
|
|
11601
12196
|
}
|
|
11602
12197
|
notify(normalised);
|
|
11603
|
-
return normalised;
|
|
11604
|
-
}
|
|
11605
|
-
function
|
|
11606
|
-
|
|
11607
|
-
|
|
11608
|
-
|
|
11609
|
-
|
|
11610
|
-
|
|
11611
|
-
|
|
11612
|
-
|
|
11613
|
-
|
|
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();
|
|
11614
12244
|
}
|
|
11615
|
-
return writeCart(cart);
|
|
11616
12245
|
}
|
|
11617
|
-
function
|
|
11618
|
-
const
|
|
11619
|
-
|
|
11620
|
-
|
|
11621
|
-
|
|
11622
|
-
|
|
11623
|
-
|
|
11624
|
-
|
|
11625
|
-
|
|
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;
|
|
11626
12300
|
});
|
|
12301
|
+
return {
|
|
12302
|
+
cart: result.cart,
|
|
12303
|
+
status: result.status === "cart_storage_unavailable" ? result.status : status
|
|
12304
|
+
};
|
|
11627
12305
|
}
|
|
11628
|
-
function
|
|
11629
|
-
const
|
|
11630
|
-
|
|
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;
|
|
11631
12317
|
}
|
|
11632
|
-
function
|
|
11633
|
-
const
|
|
11634
|
-
|
|
11635
|
-
|
|
11636
|
-
|
|
11637
|
-
|
|
11638
|
-
|
|
11639
|
-
|
|
11640
|
-
|
|
11641
|
-
|
|
11642
|
-
|
|
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
|
+
});
|
|
11643
12354
|
}
|
|
11644
12355
|
function cartItemCount(cart = readCart()) {
|
|
11645
12356
|
return cart.lines.reduce((sum, l) => sum + l.quantity, 0);
|
|
11646
12357
|
}
|
|
11647
12358
|
function cartToWireLines(cart) {
|
|
11648
|
-
return cart.lines.map((l) => ({
|
|
12359
|
+
return cart.lines.map((l) => ({
|
|
12360
|
+
kind: l.kind,
|
|
12361
|
+
variantId: l.variantId,
|
|
12362
|
+
quantity: l.quantity
|
|
12363
|
+
}));
|
|
11649
12364
|
}
|
|
11650
12365
|
var listeners = /* @__PURE__ */ new Set();
|
|
11651
12366
|
function notify(cart) {
|
|
@@ -11662,10 +12377,76 @@ function subscribeToCart(fn) {
|
|
|
11662
12377
|
if (e.key !== null && e.key !== CART_STORAGE_KEY) return;
|
|
11663
12378
|
fn(readCart());
|
|
11664
12379
|
};
|
|
11665
|
-
if (typeof window !== "undefined")
|
|
12380
|
+
if (typeof window !== "undefined")
|
|
12381
|
+
window.addEventListener("storage", onStorage);
|
|
11666
12382
|
return () => {
|
|
11667
12383
|
listeners.delete(fn);
|
|
11668
|
-
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
|
+
}
|
|
11669
12450
|
};
|
|
11670
12451
|
}
|
|
11671
12452
|
|
|
@@ -11676,7 +12457,17 @@ function formatMoney(value) {
|
|
|
11676
12457
|
if (!Number.isFinite(n)) return value;
|
|
11677
12458
|
return `$${n.toFixed(2)}`;
|
|
11678
12459
|
}
|
|
11679
|
-
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
|
+
);
|
|
11680
12471
|
const [catalog, setCatalog] = React22.useState(null);
|
|
11681
12472
|
const [failed, setFailed] = React22.useState(false);
|
|
11682
12473
|
const [count, setCount] = React22.useState(0);
|
|
@@ -11686,22 +12477,32 @@ function StoreClient({ categoryHandle, title, columns }) {
|
|
|
11686
12477
|
}, []);
|
|
11687
12478
|
React22.useEffect(() => {
|
|
11688
12479
|
let alive = true;
|
|
11689
|
-
|
|
11690
|
-
|
|
11691
|
-
|
|
11692
|
-
if (
|
|
11693
|
-
|
|
11694
|
-
|
|
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);
|
|
11695
12494
|
});
|
|
11696
12495
|
return () => {
|
|
11697
12496
|
alive = false;
|
|
11698
12497
|
};
|
|
11699
|
-
}, []);
|
|
12498
|
+
}, [client, productHandle]);
|
|
11700
12499
|
const products = React22.useMemo(() => {
|
|
11701
12500
|
if (!catalog) return [];
|
|
11702
|
-
if (!categoryHandle) return catalog.products;
|
|
11703
|
-
return catalog.products.filter(
|
|
11704
|
-
|
|
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]);
|
|
11705
12506
|
if (failed) {
|
|
11706
12507
|
return /* @__PURE__ */ jsxs25(Fragment12, { children: [
|
|
11707
12508
|
/* @__PURE__ */ jsx28("h2", { className: "sk-store__heading", children: title ?? "Shop" }),
|
|
@@ -11730,17 +12531,30 @@ function StoreCard({ product }) {
|
|
|
11730
12531
|
const [variantId, setVariantId] = React22.useState(
|
|
11731
12532
|
() => (product.variants.find((v) => v.inStock) ?? product.variants[0]).id
|
|
11732
12533
|
);
|
|
11733
|
-
const [
|
|
12534
|
+
const [addFeedback, setAddFeedback] = React22.useState(null);
|
|
11734
12535
|
const variant = product.variants.find((v) => v.id === variantId) ?? product.variants[0];
|
|
11735
12536
|
const image2 = product.thumbnailUrl ?? product.images[0]?.url ?? null;
|
|
11736
|
-
function onAdd() {
|
|
11737
|
-
addToCart(variant.id, 1);
|
|
11738
|
-
|
|
11739
|
-
window.setTimeout(() =>
|
|
12537
|
+
async function onAdd() {
|
|
12538
|
+
const result = await addToCart(variant.id, 1);
|
|
12539
|
+
setAddFeedback(result.status);
|
|
12540
|
+
window.setTimeout(() => setAddFeedback(null), 1600);
|
|
11740
12541
|
}
|
|
11741
12542
|
return /* @__PURE__ */ jsxs25("li", { className: "sk-store__card", children: [
|
|
11742
12543
|
/* @__PURE__ */ jsxs25("a", { className: "sk-store__card-link", href: `/store/${product.handle}`, children: [
|
|
11743
|
-
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
|
+
),
|
|
11744
12558
|
/* @__PURE__ */ jsx28("span", { className: "sk-store__title", children: product.title })
|
|
11745
12559
|
] }),
|
|
11746
12560
|
/* @__PURE__ */ jsxs25("span", { className: "sk-store__price", children: [
|
|
@@ -11749,16 +12563,23 @@ function StoreCard({ product }) {
|
|
|
11749
12563
|
] }),
|
|
11750
12564
|
product.variants.length > 1 ? /* @__PURE__ */ jsxs25("label", { className: "sk-store__variant", children: [
|
|
11751
12565
|
/* @__PURE__ */ jsx28("span", { className: "sk-store__variant-label", children: "Option" }),
|
|
11752
|
-
/* @__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
|
+
)
|
|
11753
12574
|
] }) : null,
|
|
11754
12575
|
/* @__PURE__ */ jsx28(
|
|
11755
12576
|
"button",
|
|
11756
12577
|
{
|
|
11757
12578
|
type: "button",
|
|
11758
12579
|
className: "sk-store__add",
|
|
11759
|
-
onClick: onAdd,
|
|
12580
|
+
onClick: () => void onAdd(),
|
|
11760
12581
|
disabled: !variant.inStock,
|
|
11761
|
-
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"
|
|
11762
12583
|
}
|
|
11763
12584
|
)
|
|
11764
12585
|
] });
|
|
@@ -11767,35 +12588,48 @@ function StoreCard({ product }) {
|
|
|
11767
12588
|
// src/cart-block.client.tsx
|
|
11768
12589
|
import * as React23 from "react";
|
|
11769
12590
|
import { Fragment as Fragment13, jsx as jsx29, jsxs as jsxs26 } from "react/jsx-runtime";
|
|
11770
|
-
function CartClient(
|
|
12591
|
+
function CartClient({ storeApiOrigin }) {
|
|
12592
|
+
const client = React23.useMemo(
|
|
12593
|
+
() => createStoreClient({ apiOrigin: storeApiOrigin }),
|
|
12594
|
+
[storeApiOrigin]
|
|
12595
|
+
);
|
|
11771
12596
|
const [cart, setCart] = React23.useState(() => readCart());
|
|
11772
12597
|
const [quote, setQuote] = React23.useState(null);
|
|
12598
|
+
const [quoteSourceCart, setQuoteSourceCart] = React23.useState(
|
|
12599
|
+
null
|
|
12600
|
+
);
|
|
11773
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
|
+
}
|
|
11774
12607
|
React23.useEffect(() => subscribeToCart(setCart), []);
|
|
11775
12608
|
React23.useEffect(() => {
|
|
11776
12609
|
if (cart.lines.length === 0) {
|
|
11777
12610
|
setQuote(null);
|
|
12611
|
+
setQuoteSourceCart(null);
|
|
11778
12612
|
setFailed(false);
|
|
11779
12613
|
return;
|
|
11780
12614
|
}
|
|
11781
12615
|
let alive = true;
|
|
11782
|
-
|
|
11783
|
-
|
|
11784
|
-
|
|
11785
|
-
|
|
11786
|
-
}).then(
|
|
11787
|
-
(res) => res.ok ? res.json() : Promise.reject(new Error(String(res.status)))
|
|
11788
|
-
).then((data) => {
|
|
12616
|
+
setQuote(null);
|
|
12617
|
+
setQuoteSourceCart(null);
|
|
12618
|
+
setFailed(false);
|
|
12619
|
+
void client.post("/api/store/quote", { lines: cartToWireLines(cart) }).then((result) => {
|
|
11789
12620
|
if (!alive) return;
|
|
11790
|
-
|
|
12621
|
+
if (!result.ok) {
|
|
12622
|
+
setFailed(true);
|
|
12623
|
+
return;
|
|
12624
|
+
}
|
|
12625
|
+
setQuote(result.data);
|
|
12626
|
+
setQuoteSourceCart(cart);
|
|
11791
12627
|
setFailed(false);
|
|
11792
|
-
}).catch(() => {
|
|
11793
|
-
if (alive) setFailed(true);
|
|
11794
12628
|
});
|
|
11795
12629
|
return () => {
|
|
11796
12630
|
alive = false;
|
|
11797
12631
|
};
|
|
11798
|
-
}, [cart]);
|
|
12632
|
+
}, [cart, client]);
|
|
11799
12633
|
if (cart.lines.length === 0) {
|
|
11800
12634
|
return /* @__PURE__ */ jsxs26(Fragment13, { children: [
|
|
11801
12635
|
/* @__PURE__ */ jsx29("h2", { className: "sk-cart__heading", children: "Your cart" }),
|
|
@@ -11803,13 +12637,54 @@ function CartClient(_props) {
|
|
|
11803
12637
|
/* @__PURE__ */ jsx29("a", { className: "sk-cart__continue", href: "/store", children: "Browse the shop" })
|
|
11804
12638
|
] });
|
|
11805
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
|
+
);
|
|
11806
12649
|
return /* @__PURE__ */ jsxs26(Fragment13, { children: [
|
|
11807
12650
|
/* @__PURE__ */ jsx29("h2", { className: "sk-cart__heading", children: "Your cart" }),
|
|
11808
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,
|
|
11809
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,
|
|
11810
|
-
|
|
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: [
|
|
11811
12679
|
/* @__PURE__ */ jsx29("ul", { className: "sk-cart__lines", children: quote.lines.map((line) => /* @__PURE__ */ jsxs26("li", { className: "sk-cart__line", children: [
|
|
11812
|
-
/* @__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
|
+
),
|
|
11813
12688
|
/* @__PURE__ */ jsxs26("span", { className: "sk-cart__line-unit", children: [
|
|
11814
12689
|
formatMoney(line.unitPrice),
|
|
11815
12690
|
" each"
|
|
@@ -11820,7 +12695,14 @@ function CartClient(_props) {
|
|
|
11820
12695
|
{
|
|
11821
12696
|
type: "button",
|
|
11822
12697
|
"aria-label": `Decrease quantity of ${line.title}`,
|
|
11823
|
-
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
|
+
),
|
|
11824
12706
|
children: "\u2212"
|
|
11825
12707
|
}
|
|
11826
12708
|
),
|
|
@@ -11830,7 +12712,14 @@ function CartClient(_props) {
|
|
|
11830
12712
|
{
|
|
11831
12713
|
type: "button",
|
|
11832
12714
|
"aria-label": `Increase quantity of ${line.title}`,
|
|
11833
|
-
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
|
+
),
|
|
11834
12723
|
children: "+"
|
|
11835
12724
|
}
|
|
11836
12725
|
)
|
|
@@ -11841,7 +12730,7 @@ function CartClient(_props) {
|
|
|
11841
12730
|
{
|
|
11842
12731
|
type: "button",
|
|
11843
12732
|
className: "sk-cart__remove",
|
|
11844
|
-
onClick: () => removeFromCart(line.variantId),
|
|
12733
|
+
onClick: () => void runMutation(removeFromCart(line.variantId, "retail")),
|
|
11845
12734
|
children: "Remove"
|
|
11846
12735
|
}
|
|
11847
12736
|
)
|
|
@@ -11863,173 +12752,768 @@ function CartClient(_props) {
|
|
|
11863
12752
|
quote.requiresShipping ? /* @__PURE__ */ jsx29("p", { className: "sk-cart__note", children: "Delivery is chosen at checkout." }) : null,
|
|
11864
12753
|
/* @__PURE__ */ jsxs26("div", { className: "sk-cart__actions", children: [
|
|
11865
12754
|
/* @__PURE__ */ jsx29("a", { className: "sk-cart__continue", href: "/store", children: "Keep shopping" }),
|
|
11866
|
-
/* @__PURE__ */ jsx29(
|
|
11867
|
-
"a",
|
|
11868
|
-
{
|
|
11869
|
-
className: "sk-cart__checkout",
|
|
11870
|
-
href: "/checkout",
|
|
11871
|
-
"aria-disabled": quote.lines.length === 0,
|
|
11872
|
-
children: "Checkout"
|
|
11873
|
-
}
|
|
11874
|
-
)
|
|
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" })
|
|
11875
12756
|
] })
|
|
11876
|
-
] })
|
|
12757
|
+
] }) : /* @__PURE__ */ jsx29("p", { className: "sk-cart__placeholder", role: "status", children: "Pricing your cart\u2026" })
|
|
11877
12758
|
] });
|
|
11878
12759
|
}
|
|
11879
12760
|
|
|
11880
12761
|
// src/checkout-block.client.tsx
|
|
11881
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
|
|
11882
12787
|
import { Fragment as Fragment14, jsx as jsx30, jsxs as jsxs27 } from "react/jsx-runtime";
|
|
11883
|
-
|
|
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) {
|
|
11884
12911
|
const q = query.trim();
|
|
11885
12912
|
if (q.length < 3) return [];
|
|
11886
|
-
const
|
|
11887
|
-
|
|
11888
|
-
|
|
11889
|
-
|
|
11890
|
-
|
|
11891
|
-
|
|
11892
|
-
|
|
11893
|
-
|
|
11894
|
-
|
|
11895
|
-
|
|
11896
|
-
|
|
11897
|
-
credentials: "same-origin",
|
|
11898
|
-
headers: { "content-type": "application/json" },
|
|
11899
|
-
body: JSON.stringify({ address })
|
|
11900
|
-
});
|
|
11901
|
-
if (!res.ok) return null;
|
|
11902
|
-
const v = await res.json();
|
|
11903
|
-
if (v.error || !v.formattedAddress) return null;
|
|
11904
|
-
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
|
+
};
|
|
11905
12924
|
}
|
|
11906
|
-
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
|
+
);
|
|
11907
12934
|
const [stage, setStage] = React24.useState("loading");
|
|
11908
12935
|
const [givenName, setGivenName] = React24.useState(null);
|
|
11909
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);
|
|
11910
12954
|
const [quote, setQuote] = React24.useState(null);
|
|
12955
|
+
const [pendingQuote, setPendingQuote] = React24.useState(null);
|
|
12956
|
+
const [pendingQuoteBaseline, setPendingQuoteBaseline] = React24.useState(null);
|
|
11911
12957
|
const [shippingOptions, setShippingOptions] = React24.useState([]);
|
|
11912
12958
|
const [error2, setError] = React24.useState(null);
|
|
11913
|
-
const [
|
|
11914
|
-
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
|
+
);
|
|
11915
12966
|
const [address, setAddress] = React24.useState(null);
|
|
11916
|
-
const [card, setCard] = React24.useState({
|
|
12967
|
+
const [card, setCard] = React24.useState({
|
|
12968
|
+
number: "",
|
|
12969
|
+
expiry: "",
|
|
12970
|
+
name: "",
|
|
12971
|
+
cvn: ""
|
|
12972
|
+
});
|
|
11917
12973
|
const orderRef = React24.useRef(null);
|
|
11918
12974
|
const [iframeUrl, setIframeUrl] = React24.useState(null);
|
|
11919
12975
|
const iframeOriginRef = React24.useRef(null);
|
|
11920
12976
|
const iframeElRef = React24.useRef(null);
|
|
11921
|
-
React24.
|
|
11922
|
-
|
|
11923
|
-
|
|
11924
|
-
|
|
11925
|
-
|
|
11926
|
-
|
|
11927
|
-
|
|
11928
|
-
|
|
11929
|
-
|
|
11930
|
-
|
|
11931
|
-
|
|
11932
|
-
|
|
11933
|
-
|
|
11934
|
-
|
|
11935
|
-
|
|
11936
|
-
|
|
11937
|
-
(
|
|
11938
|
-
|
|
11939
|
-
|
|
11940
|
-
|
|
11941
|
-
|
|
11942
|
-
|
|
11943
|
-
|
|
11944
|
-
|
|
11945
|
-
|
|
11946
|
-
|
|
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));
|
|
11947
13009
|
});
|
|
11948
|
-
|
|
11949
|
-
|
|
11950
|
-
|
|
11951
|
-
|
|
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
|
+
}
|
|
11952
13100
|
React24.useEffect(() => {
|
|
11953
|
-
if (stage !== "details" || cart.lines.length === 0) return;
|
|
11954
13101
|
let alive = true;
|
|
11955
|
-
|
|
11956
|
-
|
|
11957
|
-
|
|
11958
|
-
|
|
11959
|
-
|
|
11960
|
-
shippingOptionId: fulfilment === "ship" ? shippingOptionId : null
|
|
11961
|
-
})
|
|
11962
|
-
}).then((r) => r.ok ? r.json() : null).then((q) => {
|
|
11963
|
-
if (alive && q) setQuote(q);
|
|
11964
|
-
}).catch(() => void 0);
|
|
11965
|
-
return () => {
|
|
11966
|
-
alive = false;
|
|
11967
|
-
};
|
|
11968
|
-
}, [fulfilment, shippingOptionId]);
|
|
11969
|
-
React24.useEffect(() => {
|
|
11970
|
-
if (stage !== "three_ds") return;
|
|
11971
|
-
function onMessage(event) {
|
|
11972
|
-
if (iframeOriginRef.current && event.origin !== iframeOriginRef.current) return;
|
|
11973
|
-
if (iframeElRef.current && event.source !== iframeElRef.current.contentWindow) return;
|
|
11974
|
-
const data = typeof event.data === "string" ? event.data : null;
|
|
11975
|
-
if (!data) return;
|
|
11976
|
-
if (data === "AuthenticationInternalError") {
|
|
11977
|
-
setIframeUrl(null);
|
|
11978
|
-
setError("The card check could not be completed. Please try another card.");
|
|
11979
|
-
setStage("details");
|
|
13102
|
+
void (async () => {
|
|
13103
|
+
const locks = navigator.locks;
|
|
13104
|
+
if (!locks) {
|
|
13105
|
+
if (!alive) return;
|
|
13106
|
+
storageUnavailable();
|
|
11980
13107
|
return;
|
|
11981
13108
|
}
|
|
11982
|
-
|
|
11983
|
-
|
|
11984
|
-
|
|
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
|
+
}
|
|
11985
13156
|
}
|
|
11986
|
-
|
|
11987
|
-
|
|
11988
|
-
|
|
11989
|
-
|
|
11990
|
-
|
|
11991
|
-
|
|
11992
|
-
|
|
11993
|
-
|
|
11994
|
-
|
|
11995
|
-
|
|
11996
|
-
|
|
11997
|
-
|
|
11998
|
-
|
|
11999
|
-
|
|
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
|
+
});
|
|
13254
|
+
return () => {
|
|
13255
|
+
alive = false;
|
|
13256
|
+
};
|
|
13257
|
+
}, [client, fulfilment, shippingOptionId]);
|
|
13258
|
+
React24.useEffect(() => {
|
|
13259
|
+
if (stage !== "three_ds") return;
|
|
13260
|
+
let terminalHandled = false;
|
|
13261
|
+
function onMessage(event) {
|
|
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
|
+
}
|
|
13272
|
+
return;
|
|
13273
|
+
}
|
|
13274
|
+
if (type === "AuthenticationUserInteractionFinished") {
|
|
13275
|
+
if (iframe) {
|
|
13276
|
+
iframe.style.visibility = "hidden";
|
|
13277
|
+
iframe.style.height = "1px";
|
|
13278
|
+
}
|
|
13279
|
+
return;
|
|
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();
|
|
13292
|
+
}
|
|
13293
|
+
window.addEventListener("message", onMessage);
|
|
13294
|
+
return () => window.removeEventListener("message", onMessage);
|
|
13295
|
+
}, [stage]);
|
|
13296
|
+
async function post(path, body) {
|
|
13297
|
+
const result = await client.post(path, body);
|
|
13298
|
+
if (!result.ok) {
|
|
13299
|
+
if (result.error === "sign_in_required") {
|
|
13300
|
+
setStage("signed_out");
|
|
12000
13301
|
throw new Error("sign_in_required");
|
|
12001
13302
|
}
|
|
12002
|
-
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";
|
|
13406
|
+
}
|
|
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";
|
|
12003
13418
|
}
|
|
12004
|
-
return
|
|
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;
|
|
12005
13473
|
}
|
|
12006
13474
|
async function applyConfirm(result) {
|
|
12007
13475
|
const status = result.status;
|
|
12008
13476
|
if (status === "paid") {
|
|
12009
13477
|
const orderId = orderRef.current?.orderId ?? "";
|
|
12010
|
-
|
|
12011
|
-
|
|
12012
|
-
|
|
13478
|
+
const attemptToken = attemptTokenRef.current;
|
|
13479
|
+
if (!attemptToken) {
|
|
13480
|
+
paidReconciliationPending("snapshot_unavailable");
|
|
13481
|
+
return;
|
|
13482
|
+
}
|
|
13483
|
+
await completePaidAttempt(attemptToken, orderId);
|
|
12013
13484
|
return;
|
|
12014
13485
|
}
|
|
12015
13486
|
if (status === "requires_action") {
|
|
12016
13487
|
const artifacts = result.clientArtifacts ?? {};
|
|
12017
|
-
if (!artifacts
|
|
12018
|
-
setError("Your bank asked for a check we couldn\u2019t show. Please try another card.");
|
|
12019
|
-
setStage("details");
|
|
12020
|
-
return;
|
|
12021
|
-
}
|
|
12022
|
-
iframeOriginRef.current = artifacts.iframeOrigin ?? null;
|
|
12023
|
-
setIframeUrl(artifacts.iframeUrl);
|
|
12024
|
-
setStage("three_ds");
|
|
13488
|
+
if (!showChallenge(artifacts)) await cancelFailedChallenge();
|
|
12025
13489
|
return;
|
|
12026
13490
|
}
|
|
12027
13491
|
if (status === "declined") {
|
|
12028
|
-
|
|
12029
|
-
|
|
13492
|
+
await transitionRetryableDecline(
|
|
13493
|
+
"Your card was declined. Please check the details or try another card."
|
|
13494
|
+
);
|
|
12030
13495
|
return;
|
|
12031
13496
|
}
|
|
12032
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
|
+
}
|
|
12033
13517
|
}
|
|
12034
13518
|
async function afterChallenge() {
|
|
12035
13519
|
const orderId = orderRef.current?.orderId;
|
|
@@ -12038,45 +13522,245 @@ function CheckoutClient(_props) {
|
|
|
12038
13522
|
try {
|
|
12039
13523
|
const auth = await post("/api/store/checkout/authenticate", { orderId });
|
|
12040
13524
|
if (auth.status === "payment_failed") {
|
|
12041
|
-
|
|
12042
|
-
|
|
13525
|
+
await transitionRetryableDecline(
|
|
13526
|
+
"The card check failed. Please try another card."
|
|
13527
|
+
);
|
|
12043
13528
|
return;
|
|
12044
13529
|
}
|
|
12045
13530
|
if (auth.status === "requires_action") {
|
|
12046
13531
|
const artifacts = auth.clientArtifacts ?? {};
|
|
12047
|
-
if (artifacts
|
|
12048
|
-
|
|
12049
|
-
|
|
12050
|
-
|
|
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();
|
|
12051
13713
|
return;
|
|
12052
13714
|
}
|
|
13715
|
+
await loadBpointScript();
|
|
13716
|
+
await attachCardToAuthKey(authKey, card);
|
|
12053
13717
|
}
|
|
12054
|
-
await
|
|
13718
|
+
await dispatchConfirm(resumableCheckout.orderId);
|
|
12055
13719
|
} catch (err) {
|
|
12056
|
-
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;
|
|
12057
13725
|
setError(err instanceof Error ? err.message : "Something went wrong.");
|
|
12058
|
-
setStage("
|
|
13726
|
+
setStage("resume_payment");
|
|
12059
13727
|
}
|
|
12060
13728
|
}
|
|
12061
13729
|
async function onPay(e) {
|
|
12062
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
|
+
}
|
|
12063
13743
|
setError(null);
|
|
12064
13744
|
setStage("paying");
|
|
13745
|
+
let attemptToken = attemptTokenRef.current;
|
|
12065
13746
|
try {
|
|
12066
|
-
const
|
|
12067
|
-
|
|
12068
|
-
|
|
12069
|
-
|
|
12070
|
-
|
|
12071
|
-
shippingPlace: fulfilment === "ship" && address ? { address: address.address, placeId: address.placeId ?? null } : null,
|
|
12072
|
-
// A previous, abandoned attempt by this shopper — the server cancels it and
|
|
12073
|
-
// gives its stock back in the SAME tx that mints the replacement.
|
|
12074
|
-
priorOrderId: orderRef.current?.orderId ?? null
|
|
12075
|
-
});
|
|
13747
|
+
const initialized = await initializeCheckoutAttempt(quote);
|
|
13748
|
+
if (!initialized) return;
|
|
13749
|
+
attemptToken = initialized.attemptToken;
|
|
13750
|
+
const { init } = initialized;
|
|
13751
|
+
setError(null);
|
|
12076
13752
|
if (init.status === "cart_changed") {
|
|
12077
|
-
|
|
12078
|
-
|
|
12079
|
-
|
|
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");
|
|
12080
13764
|
return;
|
|
12081
13765
|
}
|
|
12082
13766
|
const orderId = String(init.orderId);
|
|
@@ -12084,17 +13768,220 @@ function CheckoutClient(_props) {
|
|
|
12084
13768
|
orderRef.current = { orderId, providerKey };
|
|
12085
13769
|
if (providerKey !== "mock") {
|
|
12086
13770
|
const artifacts = init.clientArtifacts ?? {};
|
|
12087
|
-
if (!artifacts.authKey)
|
|
13771
|
+
if (!artifacts.authKey)
|
|
13772
|
+
throw new Error(
|
|
13773
|
+
"We couldn\u2019t start a secure payment. Please try again."
|
|
13774
|
+
);
|
|
12088
13775
|
await loadBpointScript();
|
|
12089
13776
|
await attachCardToAuthKey(artifacts.authKey, card);
|
|
12090
13777
|
}
|
|
12091
|
-
await
|
|
13778
|
+
await dispatchConfirm(orderId);
|
|
12092
13779
|
} catch (err) {
|
|
12093
|
-
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
|
+
}
|
|
12094
13794
|
setError(err instanceof Error ? err.message : "Something went wrong.");
|
|
12095
13795
|
setStage("details");
|
|
12096
13796
|
}
|
|
12097
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
|
+
}
|
|
12098
13985
|
if (stage === "loading") {
|
|
12099
13986
|
return /* @__PURE__ */ jsxs27(Fragment14, { children: [
|
|
12100
13987
|
/* @__PURE__ */ jsx30("h2", { className: "sk-checkout__heading", children: "Checkout" }),
|
|
@@ -12112,7 +13999,69 @@ function CheckoutClient(_props) {
|
|
|
12112
13999
|
return /* @__PURE__ */ jsxs27(Fragment14, { children: [
|
|
12113
14000
|
/* @__PURE__ */ jsx30("h2", { className: "sk-checkout__heading", children: "Checkout" }),
|
|
12114
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." }),
|
|
12115
|
-
/* @__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" })
|
|
12116
14065
|
] });
|
|
12117
14066
|
}
|
|
12118
14067
|
if (stage === "paid") {
|
|
@@ -12121,11 +14070,130 @@ function CheckoutClient(_props) {
|
|
|
12121
14070
|
/* @__PURE__ */ jsx30("p", { className: "sk-checkout__note", role: "status", children: "Taking you to your order\u2026" })
|
|
12122
14071
|
] });
|
|
12123
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
|
+
}
|
|
12124
14192
|
if (stage === "pending") {
|
|
12125
14193
|
return /* @__PURE__ */ jsxs27(Fragment14, { children: [
|
|
12126
14194
|
/* @__PURE__ */ jsx30("h2", { className: "sk-checkout__heading", children: "Confirming your payment" }),
|
|
12127
|
-
/* @__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." }),
|
|
12128
|
-
/* @__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" })
|
|
12129
14197
|
] });
|
|
12130
14198
|
}
|
|
12131
14199
|
if (stage === "three_ds" && iframeUrl) {
|
|
@@ -12137,16 +14205,23 @@ function CheckoutClient(_props) {
|
|
|
12137
14205
|
ref: iframeElRef,
|
|
12138
14206
|
className: "sk-checkout__3ds",
|
|
12139
14207
|
src: iframeUrl,
|
|
14208
|
+
style: { visibility: "hidden", height: "1px" },
|
|
12140
14209
|
title: "Card verification"
|
|
12141
14210
|
}
|
|
12142
14211
|
)
|
|
12143
14212
|
] });
|
|
12144
14213
|
}
|
|
12145
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
|
+
}
|
|
12146
14221
|
return /* @__PURE__ */ jsxs27(Fragment14, { children: [
|
|
12147
14222
|
/* @__PURE__ */ jsx30("h2", { className: "sk-checkout__heading", children: givenName ? `Checkout \u2014 hello, ${givenName}` : "Checkout" }),
|
|
12148
14223
|
error2 ? /* @__PURE__ */ jsx30("p", { className: "sk-checkout__error", role: "alert", children: error2 }) : null,
|
|
12149
|
-
|
|
14224
|
+
/* @__PURE__ */ jsxs27("div", { className: "sk-checkout__summary", children: [
|
|
12150
14225
|
/* @__PURE__ */ jsx30("ul", { className: "sk-checkout__lines", children: quote.lines.map((l) => /* @__PURE__ */ jsxs27("li", { children: [
|
|
12151
14226
|
/* @__PURE__ */ jsxs27("span", { children: [
|
|
12152
14227
|
l.title,
|
|
@@ -12173,9 +14248,9 @@ function CheckoutClient(_props) {
|
|
|
12173
14248
|
/* @__PURE__ */ jsx30("dd", { children: formatMoney(quote.total) })
|
|
12174
14249
|
] })
|
|
12175
14250
|
] })
|
|
12176
|
-
] })
|
|
14251
|
+
] }),
|
|
12177
14252
|
/* @__PURE__ */ jsxs27("form", { className: "sk-checkout__form", onSubmit: onPay, children: [
|
|
12178
|
-
quote
|
|
14253
|
+
quote.requiresShipping ? /* @__PURE__ */ jsxs27("fieldset", { className: "sk-checkout__fieldset", children: [
|
|
12179
14254
|
/* @__PURE__ */ jsx30("legend", { children: "How would you like to get this?" }),
|
|
12180
14255
|
/* @__PURE__ */ jsxs27("label", { children: [
|
|
12181
14256
|
/* @__PURE__ */ jsx30(
|
|
@@ -12228,8 +14303,8 @@ function CheckoutClient(_props) {
|
|
|
12228
14303
|
label: "Delivery address",
|
|
12229
14304
|
value: address,
|
|
12230
14305
|
onChange: setAddress,
|
|
12231
|
-
search: searchStoreAddresses,
|
|
12232
|
-
validate: validateStoreAddress
|
|
14306
|
+
search: (query) => searchStoreAddresses(query, client),
|
|
14307
|
+
validate: (value) => validateStoreAddress(value, client)
|
|
12233
14308
|
}
|
|
12234
14309
|
)
|
|
12235
14310
|
] }) : null
|
|
@@ -12290,7 +14365,7 @@ function CheckoutClient(_props) {
|
|
|
12290
14365
|
] }),
|
|
12291
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." })
|
|
12292
14367
|
] }),
|
|
12293
|
-
/* @__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)}` })
|
|
12294
14369
|
] })
|
|
12295
14370
|
] });
|
|
12296
14371
|
}
|
|
@@ -12310,6 +14385,7 @@ export {
|
|
|
12310
14385
|
getThemeTokens,
|
|
12311
14386
|
PortalAccount,
|
|
12312
14387
|
PortalAccountClient,
|
|
14388
|
+
createStoreClient,
|
|
12313
14389
|
StoreClient,
|
|
12314
14390
|
CartClient,
|
|
12315
14391
|
CheckoutClient
|