@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.
@@ -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-BHVDY6SC.js";
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 portal.requestMagicLink(email.trim(), redirect ?? portal.returnPath()).then((reached) => setOutcome(reached ? "sent" : "failed")).catch(() => setOutcome("failed")).finally(() => {
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 REQUIRES_PREFLIGHT = /* @__PURE__ */ new Set(["identity", "confirm-identity"]);
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 extra = intakeSpan(shape) - 1;
1288
+ const intake = intakeSpan(shape);
1252
1289
  if (step === "service") return shape.pinned ? void 0 : 1;
1253
- if (step === "payment") return shape.paid ? picker + 6 + extra : void 0;
1254
- if (step === "slot")
1255
- return shape.scheduled ? picker + 5 + extra + (shape.paid ? 2 : 1) : void 0;
1256
- const offset = SPINE_OFFSET[step];
1257
- if (offset === void 0) return void 0;
1258
- return picker + offset + (offset > SPINE_OFFSET.intake ? extra : 0);
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 "PREFLIGHT_CONFIRMED":
1341
+ case "RUN_STARTED":
1300
1342
  if (state.step !== "preflight") return stay2(state);
1301
- return go2(state, { step: "identity" });
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: "intake", requestId: event.requestId });
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: "medicare" });
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, { step: "preflight", requestId: null, failure: null });
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: "preflight" });
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: "intake" });
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, { step: "preflight", requestId: null, failure: null });
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 = { step: "cf", requestId: "cfid", serviceKey: "cfsvc" };
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 (capturedIdentity) => {
2296
- const { requestId } = await client.startRequest({ serviceKey: props.serviceKey });
2297
- onStarted(requestId, capturedIdentity);
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.serviceKey, onStarted]
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
- await startRequest({
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
- await client.startSession({
2346
- // SPLIT names — never a joined `fullName` (PAT-776).
2347
- ...toSessionInput(identity),
2348
- turnstileToken: turnstileToken ?? void 0
2349
- });
2350
- persistClaimToken(client);
2351
- const { requestId } = await client.startRequest({ serviceKey: props.serviceKey });
2352
- await client.saveAddress(requestId, {
2353
- address: address.place.address,
2354
- placeId: address.place?.placeId ?? null
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 && questions.length === 0 && !busy) onComplete();
2788
- }, [questions, busy, onComplete]);
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
- const next = { ...answers, [current.key]: value };
2801
- setAnswers(next);
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
- const { gate } = await client.saveAnswers(
2806
- requestId,
2807
- next
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
- onComplete();
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.cancelRequest(requestId);
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
- const hold = await client.createHold({
3461
- practitionerId: slot.practitionerId,
3462
- appointmentTypeId,
3463
- slotStart: slot.start,
3464
- serviceRequestId: requestId
3465
- });
3466
- await client.bookRequest(requestId, {
3467
- practitionerId: slot.practitionerId,
3468
- slotStart: slot.start,
3469
- holdId: hold.holdId,
3470
- holderToken: hold.holderToken
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 (!requestId) {
4434
+ if (!flowId) {
4153
4435
  setFlow(null);
4154
4436
  setFlowChecked(false);
4437
+ setApplicationRun(null);
4155
4438
  return;
4156
4439
  }
4157
- if (flow) {
4158
- setFlowChecked(true);
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
- setFlow(loadFlowRecord(requestId));
4162
- setFlowChecked(true);
4163
- }, [requestId, flow]);
4164
- const captureFlow = React16.useCallback(
4165
- async (newRequestId, serviceKey, identity) => {
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) throw new Error("This certificate type is not currently available.");
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: identity.givenName,
4184
- familyName: identity.familyName,
4185
- dateOfBirth: identity.dateOfBirth || null,
4586
+ givenName: "",
4587
+ familyName: "",
4588
+ dateOfBirth: null,
4186
4589
  questionSetId: svc.questionSetId
4187
4590
  };
4188
- saveFlowRecord(newRequestId, record);
4591
+ saveFlowRecord(result.run.id, record);
4189
4592
  setFlow(record);
4190
- },
4191
- [client]
4192
- );
4193
- const back2 = React16.useCallback(() => dispatch({ type: "BACK" }), [dispatch]);
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
- if (!id || cancellingLostSlot) return;
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 service = services.find((s) => s.key === state.serviceKey) ?? null;
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
- PreflightStep,
4281
- {
4282
- documentTitle: title,
4283
- onConfirm: () => dispatch({ type: "PREFLIGHT_CONFIRMED" }),
4284
- onExit: (gate) => dispatch({ type: "GATE", gate }),
4285
- onOutOfScope: () => dispatch({ type: "NEED_DIFFERENT" }),
4286
- onBack: showBack ? back2 : void 0
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
- "div",
4293
- {
4294
- onSubmitCapture: (event) => {
4295
- if (!resumeRequestId) return;
4296
- event.preventDefault();
4297
- event.stopPropagation();
4298
- setResolveError(null);
4299
- dispatch({ type: "REQUEST_STARTED", requestId: resumeRequestId });
4300
- },
4301
- children: [
4302
- /* @__PURE__ */ jsx19(
4303
- IdentityStep,
4304
- {
4305
- client,
4306
- turnstileSiteKey: api.turnstileSiteKey,
4307
- serviceKey: state.serviceKey ?? "",
4308
- serviceLabel,
4309
- mode: state.step,
4310
- documentTitle: title,
4311
- onSignedIn: () => dispatch({ type: "SIGNED_IN" }),
4312
- onRejected: () => {
4313
- setIdentityDraft(null);
4314
- setMedicareDraft(null);
4315
- dispatch({ type: "IDENTITY_REJECTED" });
4316
- },
4317
- onBack: showBack ? back2 : void 0,
4318
- draft: identityDraft ?? void 0,
4319
- onDraftChange,
4320
- onStarted: (newRequestId, identity) => {
4321
- const capturedDraft = capturedIdentityDraft(identity, identityDraft);
4322
- setCapturing(true);
4323
- setIdentityDraft(capturedDraft);
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
- /* @__PURE__ */ jsx19(FlowError, { children: resolveError })
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
- requestId: state.requestId ?? "",
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) => dispatch({ type: "SUBMITTED", 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) => dispatch({ type: "PAY_OUTCOME", 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" }) : !requestStarted ? /* @__PURE__ */ jsx19(FlowButton, { type: "button", variant: "primary", onClick: onRestart, children: "Start again securely" }) : void 0,
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 }) : !portalUrl ? /* @__PURE__ */ jsx19("p", { className: "sk-cf__aside", children: "Please contact the clinic using the details on this website." }) : null
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
- return { kind, variantId, quantity };
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)) return emptyCart();
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 lines = [];
11584
- for (const entry of bag.lines.slice(0, MAX_LINES)) {
11585
- const line = parseLine(entry);
11586
- if (line) lines.push(line);
11587
- }
11588
- return { v: 1, lines };
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
- function writeCart(cart) {
11594
- const normalised = { v: 1, lines: cart.lines.slice(0, MAX_LINES) };
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
- try {
11598
- store.setItem(CART_STORAGE_KEY, JSON.stringify(normalised));
11599
- } catch {
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 addToCart(variantId, quantity = 1) {
11606
- const cart = readCart();
11607
- const add2 = Math.min(Math.max(Math.floor(quantity), 1), MAX_QUANTITY);
11608
- const existing = cart.lines.find((l) => l.kind === "retail" && l.variantId === variantId);
11609
- if (existing) {
11610
- existing.quantity = Math.min(existing.quantity + add2, MAX_QUANTITY);
11611
- } else {
11612
- if (cart.lines.length >= MAX_LINES) return cart;
11613
- cart.lines.push({ kind: "retail", variantId, quantity: add2 });
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 setCartQuantity(variantId, quantity) {
11618
- const cart = readCart();
11619
- const next = Math.floor(quantity);
11620
- if (next < 1) return removeFromCart(variantId);
11621
- return writeCart({
11622
- v: 1,
11623
- lines: cart.lines.map(
11624
- (l) => l.variantId === variantId ? { ...l, quantity: Math.min(next, MAX_QUANTITY) } : l
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 removeFromCart(variantId) {
11629
- const cart = readCart();
11630
- return writeCart({ v: 1, lines: cart.lines.filter((l) => l.variantId !== variantId) });
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 clearCart() {
11633
- const store = storage();
11634
- if (store) {
11635
- try {
11636
- store.removeItem(CART_STORAGE_KEY);
11637
- } catch {
11638
- }
11639
- }
11640
- const empty = emptyCart();
11641
- notify(empty);
11642
- return empty;
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) => ({ kind: l.kind, variantId: l.variantId, quantity: l.quantity }));
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") window.addEventListener("storage", onStorage);
12380
+ if (typeof window !== "undefined")
12381
+ window.addEventListener("storage", onStorage);
11666
12382
  return () => {
11667
12383
  listeners.delete(fn);
11668
- if (typeof window !== "undefined") window.removeEventListener("storage", onStorage);
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({ categoryHandle, title, columns }) {
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
- fetch("/api/store/catalog", { headers: { accept: "application/json" } }).then(
11690
- (res) => res.ok ? res.json() : Promise.reject(new Error(String(res.status)))
11691
- ).then((data) => {
11692
- if (alive) setCatalog(data);
11693
- }).catch(() => {
11694
- if (alive) setFailed(true);
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((p) => p.handle.startsWith(categoryHandle));
11704
- }, [catalog, categoryHandle]);
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 [added, setAdded] = React22.useState(false);
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
- setAdded(true);
11739
- window.setTimeout(() => setAdded(false), 1600);
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("img", { className: "sk-store__image", src: image2, alt: product.images[0]?.alt ?? "" }) : /* @__PURE__ */ jsx28("span", { className: "sk-store__image sk-store__image--empty", "aria-hidden": true }),
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("select", { value: variantId, onChange: (e) => setVariantId(e.target.value), 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)) })
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(_props) {
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
- fetch("/api/store/quote", {
11783
- method: "POST",
11784
- headers: { "content-type": "application/json" },
11785
- body: JSON.stringify({ lines: cartToWireLines(cart) })
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
- setQuote(data);
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
- !quote ? /* @__PURE__ */ jsx29("p", { className: "sk-cart__placeholder", role: "status", children: "Pricing your cart\u2026" }) : /* @__PURE__ */ jsxs26(Fragment13, { children: [
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("a", { className: "sk-cart__line-title", href: `/store/${line.productHandle}`, children: line.title }),
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: () => setCartQuantity(line.variantId, line.quantity - 1),
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: () => setCartQuantity(line.variantId, line.quantity + 1),
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
- async function searchStoreAddresses(query) {
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 res = await fetch(`/api/address/autocomplete?q=${encodeURIComponent(q)}`, {
11887
- credentials: "same-origin",
11888
- headers: { accept: "application/json" }
11889
- });
11890
- if (!res.ok) return [];
11891
- const body = await res.json();
11892
- return body.suggestions ?? [];
11893
- }
11894
- async function validateStoreAddress(address) {
11895
- const res = await fetch("/api/address/validate", {
11896
- method: "POST",
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(_props) {
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 [fulfilment, setFulfilment] = React24.useState("pickup");
11914
- const [shippingOptionId, setShippingOptionId] = React24.useState(null);
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({ number: "", expiry: "", name: "", cvn: "" });
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.useEffect(() => {
11922
- let alive = true;
11923
- if (cart.lines.length === 0) {
11924
- setStage("empty");
11925
- return;
11926
- }
11927
- Promise.all([
11928
- fetch("/api/store/session", { headers: { accept: "application/json" } }).then(
11929
- (r) => r.ok ? r.json() : { signedIn: false }
11930
- ),
11931
- fetch("/api/store/quote", {
11932
- method: "POST",
11933
- headers: { "content-type": "application/json" },
11934
- body: JSON.stringify({ lines: cartToWireLines(cart) })
11935
- }).then((r) => r.ok ? r.json() : null),
11936
- fetch("/api/store/catalog", { headers: { accept: "application/json" } }).then(
11937
- (r) => r.ok ? r.json() : { shippingOptions: [] }
11938
- )
11939
- ]).then(([session, q, catalog]) => {
11940
- if (!alive) return;
11941
- setQuote(q);
11942
- setShippingOptions(catalog.shippingOptions ?? []);
11943
- setGivenName(session.givenName ?? null);
11944
- setStage(session.signedIn ? "details" : "signed_out");
11945
- }).catch(() => {
11946
- if (alive) setError("We couldn\u2019t load your checkout. Please try again.");
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
- return () => {
11949
- alive = false;
11950
- };
11951
- }, [cart]);
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
- fetch("/api/store/quote", {
11956
- method: "POST",
11957
- headers: { "content-type": "application/json" },
11958
- body: JSON.stringify({
11959
- lines: cartToWireLines(cart),
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
- if (data === "AuthenticationComplete" || data === "AuthenticationUserInteractionFinished") {
11983
- setIframeUrl(null);
11984
- void afterChallenge();
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
- window.addEventListener("message", onMessage);
11988
- return () => window.removeEventListener("message", onMessage);
11989
- }, [stage]);
11990
- async function post(path, body) {
11991
- const res = await fetch(path, {
11992
- method: "POST",
11993
- headers: { "content-type": "application/json" },
11994
- body: JSON.stringify(body)
11995
- });
11996
- const data = await res.json().catch(() => ({}));
11997
- if (!res.ok) {
11998
- if (data.error === "sign_in_required") {
11999
- setStage("signed_out");
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 Error(typeof data.message === "string" ? data.message : "Something went wrong.");
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 data;
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
- clearCart();
12011
- setStage("paid");
12012
- window.location.assign(`/checkout/complete?order=${encodeURIComponent(orderId)}`);
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.iframeUrl) {
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
- setError("Your card was declined. Please check the details or try another card.");
12029
- setStage("details");
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
- setError("The card check failed. Please try another card.");
12042
- setStage("details");
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.iframeUrl) {
12048
- iframeOriginRef.current = artifacts.iframeOrigin ?? null;
12049
- setIframeUrl(artifacts.iframeUrl);
12050
- setStage("three_ds");
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 applyConfirm(await post("/api/store/checkout/confirm", { orderId }));
13718
+ await dispatchConfirm(resumableCheckout.orderId);
12055
13719
  } catch (err) {
12056
- if (err instanceof Error && err.message === "sign_in_required") return;
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("details");
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 init = await post("/api/store/checkout/init", {
12067
- lines: cartToWireLines(cart),
12068
- fulfilmentMethod: fulfilment,
12069
- shippingOptionId: fulfilment === "ship" ? shippingOptionId : null,
12070
- // The REFERENCE, never components — /api/store resolves it against Google.
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
- setQuote(init.quote);
12078
- setError("Your cart changed \u2014 please review it before paying.");
12079
- setStage("details");
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) throw new Error("We couldn\u2019t start a secure payment. Please try again.");
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 applyConfirm(await post("/api/store/checkout/confirm", { orderId }));
13778
+ await dispatchConfirm(orderId);
12092
13779
  } catch (err) {
12093
- if (err instanceof Error && err.message === "sign_in_required") return;
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("a", { className: "sk-checkout__signin", href: "/portal/sign-in?redirect=/checkout", children: "Sign in to continue" })
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: "/portal/orders", children: "Go to my orders" })
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
- quote ? /* @__PURE__ */ jsxs27("div", { className: "sk-checkout__summary", children: [
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
- ] }) : null,
14251
+ ] }),
12177
14252
  /* @__PURE__ */ jsxs27("form", { className: "sk-checkout__form", onSubmit: onPay, children: [
12178
- quote?.requiresShipping ? /* @__PURE__ */ jsxs27("fieldset", { className: "sk-checkout__fieldset", children: [
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" : quote ? `Pay ${formatMoney(quote.total)}` : "Pay" })
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