@patientos/website-kit 0.2.4 → 0.2.5

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.
@@ -59,11 +59,13 @@ import {
59
59
  portalSend,
60
60
  readPortalSignedInHint,
61
61
  resetSession,
62
+ restoreApplicationRunToken,
63
+ saveApplicationRunToken,
62
64
  saveFlowRecord,
63
65
  signOutOfPortal,
64
66
  writePortalSignedInHint,
65
67
  zoneAbbr
66
- } from "./chunk-BHVDY6SC.js";
68
+ } from "./chunk-FSZ726DM.js";
67
69
  import {
68
70
  __export
69
71
  } from "./chunk-MLKGABMK.js";
@@ -525,7 +527,7 @@ function toSessionInput(v) {
525
527
  dateOfBirth: v.dateOfBirth
526
528
  };
527
529
  }
528
- function SignInRamp({ redirect }) {
530
+ function SignInRamp({ redirect, prepareRedirect }) {
529
531
  const portal = usePortalClient();
530
532
  const [open, setOpen] = React2.useState(false);
531
533
  const [email, setEmail] = React2.useState("");
@@ -587,7 +589,7 @@ function SignInRamp({ redirect }) {
587
589
  onClick: () => {
588
590
  if (busy) return;
589
591
  setBusy(true);
590
- void portal.requestMagicLink(email.trim(), redirect ?? portal.returnPath()).then((reached) => setOutcome(reached ? "sent" : "failed")).catch(() => setOutcome("failed")).finally(() => {
592
+ 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
593
  setBusy(false);
592
594
  });
593
595
  },
@@ -1181,18 +1183,56 @@ function Booking({
1181
1183
 
1182
1184
  // src/certificate-funnel.client.tsx
1183
1185
  import * as React16 from "react";
1186
+ import {
1187
+ PatientOSApiError as PatientOSApiError2
1188
+ } from "@patientos/public-sdk";
1189
+
1190
+ // src/application-run-command.ts
1191
+ var inMemoryPromotionKeys = /* @__PURE__ */ new Map();
1192
+ function retainedPromotionCommandKey(applicationRunId, storage2, createKey = () => crypto.randomUUID()) {
1193
+ const memoryKey = inMemoryPromotionKeys.get(applicationRunId);
1194
+ if (memoryKey) return memoryKey;
1195
+ const storageKey = `patientos:application-promotion:${applicationRunId}`;
1196
+ try {
1197
+ const retained = storage2?.getItem(storageKey);
1198
+ if (retained) {
1199
+ inMemoryPromotionKeys.set(applicationRunId, retained);
1200
+ return retained;
1201
+ }
1202
+ const created = createKey();
1203
+ inMemoryPromotionKeys.set(applicationRunId, created);
1204
+ storage2?.setItem(storageKey, created);
1205
+ return created;
1206
+ } catch {
1207
+ const created = createKey();
1208
+ inMemoryPromotionKeys.set(applicationRunId, created);
1209
+ return created;
1210
+ }
1211
+ }
1212
+ var ApplicationRunCommandRetrier = class {
1213
+ keys = /* @__PURE__ */ new Map();
1214
+ async act(client, applicationRunId, input) {
1215
+ const fingerprint = JSON.stringify({ applicationRunId, ...input });
1216
+ const idempotencyKey = this.keys.get(fingerprint) ?? crypto.randomUUID();
1217
+ this.keys.set(fingerprint, idempotencyKey);
1218
+ const command = { ...input, idempotencyKey };
1219
+ const run = await client.actApplicationRun(applicationRunId, command);
1220
+ this.keys.delete(fingerprint);
1221
+ return run;
1222
+ }
1223
+ };
1184
1224
 
1185
1225
  // src/certificate-funnel-machine.ts
1186
1226
  var FUNNEL_STEPS = [
1187
1227
  "service",
1188
1228
  "preflight",
1229
+ "intake",
1230
+ "slot",
1189
1231
  "identity",
1190
1232
  "confirm-identity",
1191
- "intake",
1192
1233
  "medicare",
1193
1234
  "consent",
1194
1235
  "payment",
1195
- "slot",
1196
1236
  "submitted",
1197
1237
  "unavailable",
1198
1238
  "blocked",
@@ -1201,14 +1241,18 @@ var FUNNEL_STEPS = [
1201
1241
  "session-expired"
1202
1242
  ];
1203
1243
  var REQUIRES_REQUEST = /* @__PURE__ */ new Set([
1204
- "intake",
1205
1244
  "medicare",
1206
1245
  "consent",
1207
1246
  "payment",
1208
- "slot",
1209
1247
  "submitted"
1210
1248
  ]);
1211
- var REQUIRES_PREFLIGHT = /* @__PURE__ */ new Set(["identity", "confirm-identity"]);
1249
+ var REQUIRES_APPLICATION_RUN = /* @__PURE__ */ new Set([
1250
+ "intake",
1251
+ "slot",
1252
+ "identity",
1253
+ "confirm-identity"
1254
+ ]);
1255
+ var REQUIRES_PREFLIGHT = /* @__PURE__ */ new Set(["identity", "confirm-identity", "intake", "slot"]);
1212
1256
  var REQUIRES_SERVICE = /* @__PURE__ */ new Set([
1213
1257
  "preflight",
1214
1258
  "identity",
@@ -1225,15 +1269,6 @@ var DEFAULT_SHAPE = {
1225
1269
  scheduled: true,
1226
1270
  intakeSteps: 1
1227
1271
  };
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
1272
  function intakeSpan(shape) {
1238
1273
  return Math.max(1, Math.trunc(shape.intakeSteps ?? 1) || 1);
1239
1274
  }
@@ -1248,14 +1283,17 @@ function totalSteps(shape) {
1248
1283
  }
1249
1284
  function stepNumberFor(step, shape) {
1250
1285
  const picker = shape.pinned ? 0 : 1;
1251
- const extra = intakeSpan(shape) - 1;
1286
+ const intake = intakeSpan(shape);
1252
1287
  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);
1288
+ if (step === "preflight") return picker + 1;
1289
+ if (step === "intake") return picker + 2;
1290
+ if (step === "slot") return shape.scheduled ? picker + 2 + intake : void 0;
1291
+ const identity = picker + 2 + intake + (shape.scheduled ? 1 : 0);
1292
+ if (step === "identity" || step === "confirm-identity") return identity;
1293
+ if (step === "medicare") return identity + 1;
1294
+ if (step === "consent") return identity + 2;
1295
+ if (step === "payment") return shape.paid ? identity + 3 : void 0;
1296
+ return void 0;
1259
1297
  }
1260
1298
  function stepProgress(step, shape = DEFAULT_SHAPE, questionIndex = 0) {
1261
1299
  const total = totalSteps(shape);
@@ -1275,7 +1313,9 @@ function initialFunnelState(serviceKey) {
1275
1313
  // safety check, never past it: pinning chooses the product, not the screening.
1276
1314
  step: serviceKey ? "preflight" : "service",
1277
1315
  serviceKey: serviceKey ?? null,
1316
+ applicationRunId: null,
1278
1317
  requestId: null,
1318
+ scheduled: null,
1279
1319
  gate: null,
1280
1320
  failure: null
1281
1321
  };
@@ -1296,9 +1336,13 @@ function reduceFunnel(state, event) {
1296
1336
  case "SELECT_SERVICE":
1297
1337
  if (state.step !== "service") return stay2(state);
1298
1338
  return go2(state, { step: "preflight", serviceKey: event.serviceKey });
1299
- case "PREFLIGHT_CONFIRMED":
1339
+ case "RUN_STARTED":
1300
1340
  if (state.step !== "preflight") return stay2(state);
1301
- return go2(state, { step: "identity" });
1341
+ return go2(state, {
1342
+ step: "intake",
1343
+ applicationRunId: event.applicationRunId,
1344
+ scheduled: event.scheduled
1345
+ });
1302
1346
  case "NEED_DIFFERENT":
1303
1347
  return go2(state, { step: "unavailable" });
1304
1348
  case "SIGNED_IN":
@@ -1311,7 +1355,7 @@ function reduceFunnel(state, event) {
1311
1355
  }
1312
1356
  case "REQUEST_STARTED":
1313
1357
  if (state.step !== "identity" && state.step !== "confirm-identity") return stay2(state);
1314
- return go2(state, { step: "intake", requestId: event.requestId });
1358
+ return go2(state, { step: "medicare", requestId: event.requestId });
1315
1359
  case "GATE":
1316
1360
  return go2(state, {
1317
1361
  step: event.gate.kind === "escalate" ? "escalate" : "blocked",
@@ -1319,7 +1363,10 @@ function reduceFunnel(state, event) {
1319
1363
  });
1320
1364
  case "ANSWERS_COMPLETE":
1321
1365
  if (state.step !== "intake") return stay2(state);
1322
- return go2(state, { step: "medicare" });
1366
+ return go2(state, { step: state.scheduled ? "slot" : "identity" });
1367
+ case "RESERVED":
1368
+ if (state.step !== "slot") return stay2(state);
1369
+ return go2(state, { step: "identity" });
1323
1370
  case "MEDICARE_DONE":
1324
1371
  if (state.step !== "medicare") return stay2(state);
1325
1372
  return go2(state, { step: "consent" });
@@ -1348,12 +1395,21 @@ function reduceFunnel(state, event) {
1348
1395
  case "BOOKED":
1349
1396
  if (state.step !== "slot") return stay2(state);
1350
1397
  return go2(state, { step: "submitted" });
1398
+ case "RESERVATION_LOST":
1399
+ if (state.step !== "consent" && state.step !== "payment") return stay2(state);
1400
+ return go2(state, { step: "slot" });
1351
1401
  case "SESSION_EXPIRED":
1352
1402
  if (state.step === "session-expired") return stay2(state);
1353
1403
  return go2(state, { step: "session-expired" });
1354
1404
  case "RETRY_PAYMENT":
1355
1405
  if (state.step !== "payment-failed") return stay2(state);
1356
- return go2(state, { step: "preflight", requestId: null, failure: null });
1406
+ return go2(state, {
1407
+ step: "preflight",
1408
+ applicationRunId: null,
1409
+ requestId: null,
1410
+ scheduled: null,
1411
+ failure: null
1412
+ });
1357
1413
  case "RESTART": {
1358
1414
  const next = go2(initialFunnelState(), { serviceKey: null });
1359
1415
  return next;
@@ -1366,14 +1422,16 @@ function back(state) {
1366
1422
  switch (state.step) {
1367
1423
  case "preflight":
1368
1424
  return go2(state, { step: "service", serviceKey: null });
1425
+ case "intake":
1426
+ return go2(state, { step: "preflight", applicationRunId: null, scheduled: null });
1427
+ case "slot":
1428
+ return go2(state, { step: "intake" });
1369
1429
  case "identity":
1370
- return go2(state, { step: "preflight" });
1430
+ return go2(state, { step: state.scheduled ? "slot" : "intake" });
1371
1431
  case "confirm-identity":
1372
1432
  return go2(state, { step: "identity" });
1373
- case "intake":
1374
- return go2(state, { step: "identity" });
1375
1433
  case "medicare":
1376
- return go2(state, { step: "intake" });
1434
+ return go2(state, { step: "identity" });
1377
1435
  case "consent":
1378
1436
  return go2(state, { step: "medicare" });
1379
1437
  case "payment":
@@ -1381,7 +1439,13 @@ function back(state) {
1381
1439
  case "unavailable":
1382
1440
  return go2(state, { step: "service" });
1383
1441
  case "payment-failed":
1384
- return go2(state, { step: "preflight", requestId: null, failure: null });
1442
+ return go2(state, {
1443
+ step: "preflight",
1444
+ applicationRunId: null,
1445
+ requestId: null,
1446
+ scheduled: null,
1447
+ failure: null
1448
+ });
1385
1449
  default:
1386
1450
  return stay2(state);
1387
1451
  }
@@ -1390,10 +1454,6 @@ function canGoBack(state, opts = {}) {
1390
1454
  if (state.step === "preflight" && opts.pinnedService) return false;
1391
1455
  return back(state).effects.length > 0;
1392
1456
  }
1393
- function resumableRequestId(requestId, submittedSnapshot, currentSnapshot) {
1394
- if (!requestId || !submittedSnapshot || !currentSnapshot) return null;
1395
- return submittedSnapshot === currentSnapshot ? requestId : null;
1396
- }
1397
1457
  var TERMINAL_WALL = /* @__PURE__ */ new Set([
1398
1458
  "escalate",
1399
1459
  "blocked",
@@ -1414,18 +1474,26 @@ function popAction(current, search, pinnedService) {
1414
1474
  if (TERMINAL_WALL.has(current.step)) return { type: "refuse" };
1415
1475
  return { type: "restore", state: stepFromUrl(search, pinnedService) };
1416
1476
  }
1417
- var URL_KEYS = { step: "cf", requestId: "cfid", serviceKey: "cfsvc" };
1477
+ var URL_KEYS = {
1478
+ step: "cf",
1479
+ requestId: "cfid",
1480
+ applicationRunId: "cfrun",
1481
+ serviceKey: "cfsvc"
1482
+ };
1418
1483
  function stepFromUrl(search, pinnedService) {
1419
1484
  const params = new URLSearchParams(search.startsWith("?") ? search.slice(1) : search);
1420
1485
  const rawStep = params.get(URL_KEYS.step) ?? "";
1421
1486
  const requestId = params.get(URL_KEYS.requestId) || null;
1487
+ const applicationRunId = params.get(URL_KEYS.applicationRunId) || null;
1422
1488
  const urlService = params.get(URL_KEYS.serviceKey) || null;
1423
1489
  const serviceKey = pinnedService ?? urlService;
1424
1490
  const known = FUNNEL_STEPS.includes(rawStep) ? rawStep : null;
1425
1491
  const base2 = {
1426
1492
  step: known ?? initialFunnelState(serviceKey).step,
1427
1493
  serviceKey,
1494
+ applicationRunId,
1428
1495
  requestId,
1496
+ scheduled: null,
1429
1497
  // Gate + failure detail are NOT in the URL: they are server verdicts with clinical
1430
1498
  // copy attached, and a URL that could assert "you are blocked" (or, worse, that you
1431
1499
  // are not) would be a forgeable claim about a safety decision.
@@ -1433,6 +1501,9 @@ function stepFromUrl(search, pinnedService) {
1433
1501
  failure: null
1434
1502
  };
1435
1503
  if (REQUIRES_REQUEST.has(base2.step) && !base2.requestId) {
1504
+ return { ...base2, step: applicationRunId ? "identity" : serviceKey ? "preflight" : "service" };
1505
+ }
1506
+ if (REQUIRES_APPLICATION_RUN.has(base2.step) && !base2.applicationRunId) {
1436
1507
  return { ...base2, step: serviceKey ? "preflight" : "service" };
1437
1508
  }
1438
1509
  if (base2.step === "payment-failed" && !base2.requestId) {
@@ -1444,7 +1515,7 @@ function stepFromUrl(search, pinnedService) {
1444
1515
  if (REQUIRES_SERVICE.has(base2.step) && !base2.serviceKey) {
1445
1516
  return { ...base2, step: "service" };
1446
1517
  }
1447
- if (REQUIRES_PREFLIGHT.has(base2.step)) {
1518
+ if (REQUIRES_PREFLIGHT.has(base2.step) && !base2.applicationRunId) {
1448
1519
  return { ...base2, step: base2.serviceKey ? "preflight" : "service" };
1449
1520
  }
1450
1521
  if (base2.step === "blocked" || base2.step === "escalate") {
@@ -1456,6 +1527,7 @@ function urlFromState(state, opts = {}) {
1456
1527
  const params = new URLSearchParams();
1457
1528
  params.set(URL_KEYS.step, state.step);
1458
1529
  if (state.requestId) params.set(URL_KEYS.requestId, state.requestId);
1530
+ if (state.applicationRunId) params.set(URL_KEYS.applicationRunId, state.applicationRunId);
1459
1531
  if (state.serviceKey && !opts.pinnedService) params.set(URL_KEYS.serviceKey, state.serviceKey);
1460
1532
  return params.toString();
1461
1533
  }
@@ -1685,17 +1757,17 @@ function PaymentStep({
1685
1757
  const res = await client.payConfirm(requestId);
1686
1758
  switch (res.status) {
1687
1759
  case "awaiting_slot":
1688
- onOutcome({ kind: "awaiting_slot" });
1760
+ await onOutcome({ kind: "awaiting_slot" });
1689
1761
  return;
1690
1762
  case "ready":
1691
- onOutcome({ kind: "ready" });
1763
+ await onOutcome({ kind: "ready" });
1692
1764
  return;
1693
1765
  case "requires_action":
1694
1766
  setError("Your bank needs to verify this card. Please try again.");
1695
1767
  setPhase("ready");
1696
1768
  return;
1697
1769
  case "payment_failed":
1698
- onOutcome({
1770
+ await onOutcome({
1699
1771
  kind: "failed",
1700
1772
  message: "Your card was declined and the pre-authorisation could not be completed."
1701
1773
  });
@@ -2291,13 +2363,43 @@ function IdentityStep(props) {
2291
2363
  },
2292
2364
  [client]
2293
2365
  );
2366
+ const promotionIdempotencyRef = React8.useRef(null);
2367
+ const promotionIdempotencyKey = React8.useCallback((applicationRunId) => {
2368
+ if (promotionIdempotencyRef.current) return promotionIdempotencyRef.current;
2369
+ const retained = retainedPromotionCommandKey(
2370
+ applicationRunId,
2371
+ typeof window === "undefined" ? null : window.sessionStorage
2372
+ );
2373
+ promotionIdempotencyRef.current = retained;
2374
+ return retained;
2375
+ }, []);
2294
2376
  const startRequest = React8.useCallback(
2295
- async (capturedIdentity) => {
2296
- const { requestId } = await client.startRequest({ serviceKey: props.serviceKey });
2297
- onStarted(requestId, capturedIdentity);
2377
+ async (promotion) => {
2378
+ if (!props.applicationRunId) {
2379
+ return (await client.startRequest({ serviceKey: props.serviceKey })).requestId;
2380
+ }
2381
+ const result = await client.promoteApplicationRun(props.applicationRunId, {
2382
+ ...promotion,
2383
+ idempotencyKey: promotionIdempotencyKey(props.applicationRunId)
2384
+ });
2385
+ return result.requestId;
2298
2386
  },
2299
- [client, props.serviceKey, onStarted]
2387
+ [client, promotionIdempotencyKey, props.applicationRunId, props.serviceKey]
2300
2388
  );
2389
+ const prepareSignInRedirect = React8.useCallback(async () => {
2390
+ if (!props.applicationRunId || typeof window === "undefined") return portal.returnPath();
2391
+ const { handoffId } = await client.createApplicationRunHandoff(props.applicationRunId);
2392
+ let target;
2393
+ try {
2394
+ target = new URL(window.location.href);
2395
+ } catch {
2396
+ return portal.returnPath();
2397
+ }
2398
+ target.searchParams.set("cf", "confirm-identity");
2399
+ target.searchParams.set("cfrun", props.applicationRunId);
2400
+ target.searchParams.set("cfhandoff", handoffId);
2401
+ return portal.returnPath(target.href);
2402
+ }, [client, portal, props.applicationRunId]);
2301
2403
  async function handleConfirmSignedIn() {
2302
2404
  if (busy) return;
2303
2405
  setBusy(true);
@@ -2311,13 +2413,15 @@ function IdentityStep(props) {
2311
2413
  return;
2312
2414
  }
2313
2415
  adoptClaimToken(client, claim.claimToken);
2314
- await startRequest({
2416
+ const confirmedIdentity = {
2315
2417
  givenName: prefill?.givenName ?? "",
2316
2418
  familyName: prefill?.familyName ?? "",
2317
2419
  email: prefill?.email ?? "",
2318
2420
  phone: prefill?.phone ?? "",
2319
2421
  dateOfBirth: prefill?.dateOfBirth ?? ""
2320
- });
2422
+ };
2423
+ const requestId = await startRequest();
2424
+ onStarted(requestId, confirmedIdentity);
2321
2425
  } catch (err) {
2322
2426
  setBusy(false);
2323
2427
  setError(errorMessage(err, "We could not start your request. Please try again."));
@@ -2342,17 +2446,29 @@ function IdentityStep(props) {
2342
2446
  setBusy(true);
2343
2447
  setError(null);
2344
2448
  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
- });
2449
+ let requestId;
2450
+ if (props.applicationRunId) {
2451
+ requestId = await startRequest({
2452
+ identity: toSessionInput(identity),
2453
+ address: {
2454
+ address: address.place.address,
2455
+ placeId: address.place?.placeId ?? null
2456
+ },
2457
+ turnstileToken: turnstileToken ?? void 0
2458
+ });
2459
+ persistClaimToken(client);
2460
+ } else {
2461
+ await client.startSession({
2462
+ ...toSessionInput(identity),
2463
+ turnstileToken: turnstileToken ?? void 0
2464
+ });
2465
+ persistClaimToken(client);
2466
+ requestId = await startRequest();
2467
+ await client.saveAddress(requestId, {
2468
+ address: address.place.address,
2469
+ placeId: address.place?.placeId ?? null
2470
+ });
2471
+ }
2356
2472
  onStarted(requestId, identity);
2357
2473
  } catch (err) {
2358
2474
  setBusy(false);
@@ -2376,6 +2492,10 @@ function IdentityStep(props) {
2376
2492
  setError(errorMessage(err, "We could not start your request. Please try again."));
2377
2493
  }
2378
2494
  }
2495
+ const reservationNote = props.reservationExpiresAt ? ` Your appointment time is reserved until ${new Intl.DateTimeFormat("en-AU", {
2496
+ hour: "numeric",
2497
+ minute: "2-digit"
2498
+ }).format(new Date(props.reservationExpiresAt))}.` : "";
2379
2499
  if (mode === "confirm-identity") {
2380
2500
  return /* @__PURE__ */ jsxs8(
2381
2501
  FlowShell,
@@ -2383,7 +2503,7 @@ function IdentityStep(props) {
2383
2503
  ...progress,
2384
2504
  stepKey: "confirm-identity",
2385
2505
  heading: "Confirm your details",
2386
- subheading: detailsSubheading(props.serviceLabel),
2506
+ subheading: `${detailsSubheading(props.serviceLabel)}${reservationNote}`,
2387
2507
  onBack,
2388
2508
  documentTitle,
2389
2509
  children: [
@@ -2408,12 +2528,12 @@ function IdentityStep(props) {
2408
2528
  ...progress,
2409
2529
  stepKey: "identity",
2410
2530
  heading: "Your details",
2411
- subheading: detailsSubheading(props.serviceLabel),
2531
+ subheading: `${detailsSubheading(props.serviceLabel)}${reservationNote}`,
2412
2532
  onBack,
2413
2533
  documentTitle,
2414
2534
  footer: probing ? void 0 : /* @__PURE__ */ jsx10(FlowButton, { type: "submit", form: "sk-cf-identity", variant: "primary", busy, children: busy ? "Starting\u2026" : "Continue" }),
2415
2535
  children: probing ? /* @__PURE__ */ jsx10(FlowLoading, { message: "Checking if you\u2019re signed in\u2026" }) : /* @__PURE__ */ jsxs8(Fragment3, { children: [
2416
- /* @__PURE__ */ jsx10(SignInRamp, { redirect: props.returnUrl }),
2536
+ /* @__PURE__ */ jsx10(SignInRamp, { redirect: props.returnUrl, prepareRedirect: prepareSignInRedirect }),
2417
2537
  /* @__PURE__ */ jsx10(FlowErrorSummary, { fields: invalid }),
2418
2538
  /* @__PURE__ */ jsxs8("form", { id: "sk-cf-identity", onSubmit: (e) => void handleSubmitAnonymous(e), noValidate: true, children: [
2419
2539
  /* @__PURE__ */ jsx10(SummarisedFieldErrors, { children: /* @__PURE__ */ jsx10(
@@ -2743,6 +2863,9 @@ function intakeRowsAction(rows) {
2743
2863
  function IntakeStep({
2744
2864
  client,
2745
2865
  requestId,
2866
+ applicationRunId,
2867
+ applicationRunRevision = 0,
2868
+ onApplicationRunChange,
2746
2869
  questionSetId,
2747
2870
  resolveError,
2748
2871
  onGate,
@@ -2756,6 +2879,15 @@ function IntakeStep({
2756
2879
  const [draft, setDraft] = React10.useState(null);
2757
2880
  const [busy, setBusy] = React10.useState(false);
2758
2881
  const [error2, setError] = React10.useState(null);
2882
+ const [runRevision, setRunRevision] = React10.useState(applicationRunRevision);
2883
+ const [completed, setCompleted] = React10.useState(false);
2884
+ const [emptyCompletionRetry, setEmptyCompletionRetry] = React10.useState(0);
2885
+ const commandRetrier = React10.useRef(new ApplicationRunCommandRetrier()).current;
2886
+ const pendingCompletionRevision = React10.useRef(null);
2887
+ const emptyCompletionAttempted = React10.useRef(false);
2888
+ React10.useEffect(() => {
2889
+ setRunRevision(applicationRunRevision);
2890
+ }, [applicationRunRevision]);
2759
2891
  const [reload2, setReload] = React10.useState(0);
2760
2892
  const progress = useStepProgress("intake", index);
2761
2893
  React10.useEffect(() => {
@@ -2784,8 +2916,28 @@ function IntakeStep({
2784
2916
  };
2785
2917
  }, [client, questionSetId, reload2]);
2786
2918
  React10.useEffect(() => {
2787
- if (questions && questions.length === 0 && !busy) onComplete();
2788
- }, [questions, busy, onComplete]);
2919
+ if (!questions || questions.length !== 0 || busy || completed || emptyCompletionAttempted.current) return;
2920
+ emptyCompletionAttempted.current = true;
2921
+ if (!applicationRunId) {
2922
+ setCompleted(true);
2923
+ onComplete();
2924
+ return;
2925
+ }
2926
+ setBusy(true);
2927
+ void commandRetrier.act(client, applicationRunId, {
2928
+ expectedRevision: runRevision,
2929
+ action: { type: "complete_screening" }
2930
+ }).then((run) => {
2931
+ setRunRevision(run.revision);
2932
+ onApplicationRunChange?.(run);
2933
+ const exit = gateToExit(run.gate);
2934
+ if (exit) onGate(exit);
2935
+ else {
2936
+ setCompleted(true);
2937
+ onComplete();
2938
+ }
2939
+ }).catch((e) => setError(errorMessage(e, "We could not complete the screening. Please try again."))).finally(() => setBusy(false));
2940
+ }, [questions, busy, completed, emptyCompletionRetry, applicationRunId, client, runRevision, onApplicationRunChange, onGate, onComplete]);
2789
2941
  const visible = React10.useMemo(
2790
2942
  () => questions ? visibleQuestions(questions, answers) : [],
2791
2943
  [questions, answers]
@@ -2795,17 +2947,60 @@ function IntakeStep({
2795
2947
  React10.useEffect(() => {
2796
2948
  setDraft(current ? answers[current.key] ?? null : null);
2797
2949
  }, [current?.key]);
2950
+ async function retryPendingCompletion() {
2951
+ if (busy || !applicationRunId || pendingCompletionRevision.current == null) return;
2952
+ setBusy(true);
2953
+ setError(null);
2954
+ try {
2955
+ const completedRun = await commandRetrier.act(client, applicationRunId, {
2956
+ expectedRevision: pendingCompletionRevision.current,
2957
+ action: { type: "complete_screening" }
2958
+ });
2959
+ pendingCompletionRevision.current = null;
2960
+ setRunRevision(completedRun.revision);
2961
+ onApplicationRunChange?.(completedRun);
2962
+ const completedExit = gateToExit(completedRun.gate);
2963
+ if (completedExit) onGate(completedExit);
2964
+ else {
2965
+ setCompleted(true);
2966
+ onComplete();
2967
+ }
2968
+ } catch (e) {
2969
+ setError(errorMessage(e, "We could not complete the screening. Please try again."));
2970
+ } finally {
2971
+ setBusy(false);
2972
+ }
2973
+ }
2798
2974
  async function record(value) {
2799
2975
  if (!current || busy) return;
2800
- const next = { ...answers, [current.key]: value };
2801
- setAnswers(next);
2976
+ if (applicationRunId && pendingCompletionRevision.current != null) {
2977
+ await retryPendingCompletion();
2978
+ return;
2979
+ }
2802
2980
  setBusy(true);
2803
2981
  setError(null);
2982
+ const next = { ...answers, [current.key]: value };
2983
+ setAnswers(next);
2804
2984
  try {
2805
- const { gate } = await client.saveAnswers(
2806
- requestId,
2807
- next
2808
- );
2985
+ let savedRun = null;
2986
+ let gate;
2987
+ if (applicationRunId) {
2988
+ savedRun = await commandRetrier.act(client, applicationRunId, {
2989
+ expectedRevision: runRevision,
2990
+ action: {
2991
+ type: "save_answers",
2992
+ answers: next
2993
+ }
2994
+ });
2995
+ setRunRevision(savedRun.revision);
2996
+ onApplicationRunChange?.(savedRun);
2997
+ gate = savedRun.gate;
2998
+ } else {
2999
+ gate = (await client.saveAnswers(
3000
+ requestId ?? "",
3001
+ next
3002
+ )).gate;
3003
+ }
2809
3004
  const exit = gateToExit(gate);
2810
3005
  if (exit) {
2811
3006
  onGate(exit);
@@ -2813,7 +3008,25 @@ function IntakeStep({
2813
3008
  }
2814
3009
  const nextVisible = questions ? visibleQuestions(questions, next) : [];
2815
3010
  if (index + 1 >= nextVisible.length) {
2816
- onComplete();
3011
+ if (applicationRunId && savedRun) {
3012
+ pendingCompletionRevision.current = savedRun.revision;
3013
+ const completedRun = await commandRetrier.act(client, applicationRunId, {
3014
+ expectedRevision: savedRun.revision,
3015
+ action: { type: "complete_screening" }
3016
+ });
3017
+ pendingCompletionRevision.current = null;
3018
+ setRunRevision(completedRun.revision);
3019
+ onApplicationRunChange?.(completedRun);
3020
+ const completedExit = gateToExit(completedRun.gate);
3021
+ if (completedExit) onGate(completedExit);
3022
+ else {
3023
+ setCompleted(true);
3024
+ onComplete();
3025
+ }
3026
+ } else {
3027
+ setCompleted(true);
3028
+ onComplete();
3029
+ }
2817
3030
  return;
2818
3031
  }
2819
3032
  setIndex(index + 1);
@@ -2859,6 +3072,23 @@ function IntakeStep({
2859
3072
  }
2860
3073
  );
2861
3074
  }
3075
+ if (questions.length === 0) {
3076
+ return /* @__PURE__ */ jsx12(
3077
+ IntakeInterstitial,
3078
+ {
3079
+ progress,
3080
+ documentTitle,
3081
+ onBack,
3082
+ error: error2,
3083
+ message: busy || !error2 ? "Completing your screening\u2026" : void 0,
3084
+ onRetry: error2 ? () => {
3085
+ emptyCompletionAttempted.current = false;
3086
+ setError(null);
3087
+ setEmptyCompletionRetry((value) => value + 1);
3088
+ } : void 0
3089
+ }
3090
+ );
3091
+ }
2862
3092
  if (!current) {
2863
3093
  return /* @__PURE__ */ jsx12(
2864
3094
  IntakeInterstitial,
@@ -2906,7 +3136,17 @@ function IntakeStep({
2906
3136
  idPrefix: `sk-cf-${index}`
2907
3137
  }
2908
3138
  ),
2909
- /* @__PURE__ */ jsx12(FlowError, { children: error2 })
3139
+ /* @__PURE__ */ jsx12(FlowError, { children: error2 }),
3140
+ error2 && pendingCompletionRevision.current != null && /* @__PURE__ */ jsx12(
3141
+ FlowButton,
3142
+ {
3143
+ type: "button",
3144
+ variant: "primary",
3145
+ busy,
3146
+ onClick: () => void retryPendingCompletion(),
3147
+ children: "Try again"
3148
+ }
3149
+ )
2910
3150
  ]
2911
3151
  }
2912
3152
  );
@@ -3310,7 +3550,7 @@ function ConsentStep({
3310
3550
  onGate(gate ?? { kind: "block" });
3311
3551
  return;
3312
3552
  }
3313
- onSubmitted(result.status, gate);
3553
+ await onSubmitted(result.status, gate);
3314
3554
  } catch (err) {
3315
3555
  setBusy(false);
3316
3556
  setError(errorMessage(err, "We could not submit your request. Please try again."));
@@ -3415,6 +3655,10 @@ function SlotCancelControl({
3415
3655
  function SlotStep({
3416
3656
  client,
3417
3657
  requestId,
3658
+ applicationRunId,
3659
+ applicationRunRevision = 0,
3660
+ onApplicationRunChange,
3661
+ replacementReservation = false,
3418
3662
  appointmentTypeId,
3419
3663
  isFree,
3420
3664
  onBooked,
@@ -3425,13 +3669,15 @@ function SlotStep({
3425
3669
  const [cancelling, setCancelling] = React13.useState(false);
3426
3670
  const [confirmCancel, setConfirmCancel] = React13.useState(false);
3427
3671
  const [cancelError, setCancelError] = React13.useState(null);
3672
+ const commandRetrier = React13.useRef(new ApplicationRunCommandRetrier()).current;
3428
3673
  const progress = useStepProgress("slot");
3429
3674
  async function releaseAndExit() {
3430
3675
  if (cancelling) return;
3431
3676
  setCancelling(true);
3432
3677
  setCancelError(null);
3433
3678
  try {
3434
- await client.cancelRequest(requestId);
3679
+ if (applicationRunId) await client.closeApplicationRun(applicationRunId);
3680
+ else await client.cancelRequest(requestId ?? "");
3435
3681
  onCancelled();
3436
3682
  } catch (err) {
3437
3683
  setCancelling(false);
@@ -3457,18 +3703,33 @@ function SlotStep({
3457
3703
  appointmentTypeId,
3458
3704
  mode: "request",
3459
3705
  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
- });
3706
+ if (applicationRunId) {
3707
+ const run = await commandRetrier.act(client, applicationRunId, {
3708
+ expectedRevision: applicationRunRevision,
3709
+ action: {
3710
+ type: "reserve_slot",
3711
+ practitionerId: slot.practitionerId,
3712
+ slotStart: slot.start
3713
+ }
3714
+ });
3715
+ onApplicationRunChange?.(run);
3716
+ } else {
3717
+ const hold = replacementReservation ? await client.replaceRequestSlot(requestId ?? "", {
3718
+ practitionerId: slot.practitionerId,
3719
+ slotStart: slot.start
3720
+ }) : await client.createHold({
3721
+ practitionerId: slot.practitionerId,
3722
+ appointmentTypeId,
3723
+ slotStart: slot.start,
3724
+ serviceRequestId: requestId
3725
+ });
3726
+ await client.bookRequest(requestId ?? "", {
3727
+ practitionerId: slot.practitionerId,
3728
+ slotStart: slot.start,
3729
+ holdId: hold.holdId,
3730
+ holderToken: hold.holderToken
3731
+ });
3732
+ }
3472
3733
  onBooked(formatBookedFor(slot.start, tz));
3473
3734
  }
3474
3735
  }
@@ -3485,11 +3746,12 @@ function SlotStep({
3485
3746
  clinicPhone
3486
3747
  ] })
3487
3748
  ] }),
3488
- isFree === false && /* @__PURE__ */ jsxs13("p", { className: "sk-slots__empty-note", children: [
3749
+ isFree === false && !applicationRunId && /* @__PURE__ */ jsxs13("p", { className: "sk-slots__empty-note", children: [
3489
3750
  "You haven\u2019t been charged \u2014 only a hold was placed on your card.",
3490
3751
  " ",
3491
3752
  HOLD_RELEASE_NOTE
3492
- ] })
3753
+ ] }),
3754
+ 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
3755
  ] }),
3494
3756
  /* @__PURE__ */ jsx15(
3495
3757
  SlotCancelControl,
@@ -4035,6 +4297,13 @@ function CertificateFunnelClient(props) {
4035
4297
  }
4036
4298
  return /* @__PURE__ */ jsx19(PortalProvider, { client: portalClient, children: /* @__PURE__ */ jsx19(Funnel, { ...props }) });
4037
4299
  }
4300
+ var RESERVATION_REQUIRED_STEPS = /* @__PURE__ */ new Set([
4301
+ "identity",
4302
+ "confirm-identity",
4303
+ "medicare",
4304
+ "consent",
4305
+ "payment"
4306
+ ]);
4038
4307
  function Funnel({
4039
4308
  publicApi,
4040
4309
  services = [],
@@ -4044,6 +4313,7 @@ function Funnel({
4044
4313
  serviceKey: pinnedService
4045
4314
  }) {
4046
4315
  const api = publicApi;
4316
+ const portalClient = usePortalClient();
4047
4317
  const rawClient = React16.useMemo(
4048
4318
  () => patientos({ publishableKey: api.publishableKey, apiBase: api.apiBase }),
4049
4319
  [api.publishableKey, api.apiBase]
@@ -4051,15 +4321,17 @@ function Funnel({
4051
4321
  const pinned = React16.useMemo(() => ({ pinnedService: !!pinnedService }), [pinnedService]);
4052
4322
  const [state, setState] = React16.useState(null);
4053
4323
  const [flow, setFlow] = React16.useState(null);
4324
+ const [applicationRun, setApplicationRun] = React16.useState(null);
4054
4325
  const [flowChecked, setFlowChecked] = React16.useState(false);
4055
4326
  const [capturing, setCapturing] = React16.useState(false);
4056
4327
  const [cancellingLostSlot, setCancellingLostSlot] = React16.useState(false);
4057
4328
  const [lostSlotCancelError, setLostSlotCancelError] = React16.useState(null);
4058
4329
  const [resolveError, setResolveError] = React16.useState(null);
4330
+ const [reopeningScreening, setReopeningScreening] = React16.useState(false);
4331
+ const [reopenScreeningError, setReopenScreeningError] = React16.useState(null);
4332
+ const applicationRunCommands = React16.useRef(new ApplicationRunCommandRetrier()).current;
4333
+ const beginAttemptRef = React16.useRef(null);
4059
4334
  const [identityDraft, setIdentityDraft] = React16.useState(null);
4060
- const [submittedIdentitySnapshot, setSubmittedIdentitySnapshot] = React16.useState(
4061
- null
4062
- );
4063
4335
  const onDraftChange = React16.useCallback((draft) => setIdentityDraft(draft), []);
4064
4336
  const [medicareDraft, setMedicareDraft] = React16.useState(null);
4065
4337
  const onMedicareDraftChange = React16.useCallback(
@@ -4097,6 +4369,7 @@ function Funnel({
4097
4369
  null
4098
4370
  );
4099
4371
  const [questionCounts, setQuestionCounts] = React16.useState({});
4372
+ const [enabledServiceKeys, setEnabledServiceKeys] = React16.useState(null);
4100
4373
  React16.useEffect(() => {
4101
4374
  let alive = true;
4102
4375
  void (async () => {
@@ -4109,7 +4382,10 @@ function Funnel({
4109
4382
  const nextFees = {};
4110
4383
  const nextDeliveryModes = {};
4111
4384
  const nextCounts = {};
4385
+ const nextEnabledKeys = /* @__PURE__ */ new Set();
4112
4386
  for (const svc of svcs) {
4387
+ if (svc.anonymousApplicationRuns === false) continue;
4388
+ nextEnabledKeys.add(svc.key);
4113
4389
  const option = pickDeliveryOption(options, svc.id);
4114
4390
  nextFees[svc.key] = effectiveFee(svc, option);
4115
4391
  if (option) nextDeliveryModes[svc.key] = option.deliveryMode;
@@ -4118,10 +4394,12 @@ function Funnel({
4118
4394
  setFees(nextFees);
4119
4395
  setDeliveryModes(nextDeliveryModes);
4120
4396
  setQuestionCounts(nextCounts);
4397
+ setEnabledServiceKeys(nextEnabledKeys);
4121
4398
  } catch {
4122
4399
  if (alive) {
4123
4400
  setFees({});
4124
4401
  setDeliveryModes({});
4402
+ setEnabledServiceKeys(/* @__PURE__ */ new Set());
4125
4403
  }
4126
4404
  }
4127
4405
  })();
@@ -4148,66 +4426,221 @@ function Funnel({
4148
4426
  return () => window.removeEventListener("popstate", onPop);
4149
4427
  }, [dispatch, pinnedService, pinned]);
4150
4428
  const requestId = state?.requestId ?? null;
4429
+ const applicationRunId = state?.applicationRunId ?? null;
4430
+ const flowId = requestId ?? applicationRunId;
4151
4431
  React16.useEffect(() => {
4152
- if (!requestId) {
4432
+ if (!flowId) {
4153
4433
  setFlow(null);
4154
4434
  setFlowChecked(false);
4435
+ setApplicationRun(null);
4155
4436
  return;
4156
4437
  }
4157
- if (flow) {
4158
- setFlowChecked(true);
4438
+ if (!flow) setFlow(loadFlowRecord(flowId));
4439
+ setFlowChecked(true);
4440
+ }, [flowId, flow]);
4441
+ const hydrateRunFlow = React16.useCallback(async (run) => {
4442
+ const existing = loadFlowRecord(run.id);
4443
+ if (existing) {
4444
+ setFlow(existing);
4159
4445
  return;
4160
4446
  }
4161
- setFlow(loadFlowRecord(requestId));
4162
- setFlowChecked(true);
4163
- }, [requestId, flow]);
4164
- const captureFlow = React16.useCallback(
4165
- async (newRequestId, serviceKey, identity) => {
4447
+ const [svc, options] = await Promise.all([
4448
+ findServiceByKey(client, run.serviceKey),
4449
+ once(client.subscribeDeliveryOptions)
4450
+ ]);
4451
+ const option = options.find((candidate) => candidate.id === run.serviceDeliveryOptionId);
4452
+ if (!svc || !option) throw new Error("This application option is no longer available.");
4453
+ const record = {
4454
+ serviceId: svc.id,
4455
+ serviceKey: svc.key,
4456
+ serviceLabel: svc.label,
4457
+ deliveryOptionId: option.id,
4458
+ appointmentTypeId: run.appointmentTypeId ?? "",
4459
+ feeAmount: option.feeOverride ?? svc.feeAmount,
4460
+ requiresPayment: svc.requiresPayment,
4461
+ deliveryMode: run.deliveryMode,
4462
+ givenName: "",
4463
+ familyName: "",
4464
+ dateOfBirth: null,
4465
+ questionSetId: run.questionSetId,
4466
+ bookedFor: void 0
4467
+ };
4468
+ saveFlowRecord(run.id, record);
4469
+ setFlow(record);
4470
+ }, [client]);
4471
+ React16.useEffect(() => {
4472
+ if (!applicationRunId || applicationRun?.id === applicationRunId) return;
4473
+ let alive = true;
4474
+ void (async () => {
4475
+ const params = new URLSearchParams(window.location.search);
4476
+ const handoffId = params.get("cfhandoff");
4477
+ if (handoffId) {
4478
+ const redeemed = await portalClient.redeemApplicationRunHandoff(handoffId);
4479
+ if (!redeemed || !alive) throw new Error("This sign-in continuation has expired.");
4480
+ rawClient.setApplicationRunToken(applicationRunId, redeemed.capabilityToken);
4481
+ saveApplicationRunToken(rawClient, applicationRunId);
4482
+ setApplicationRun(redeemed.run);
4483
+ await hydrateRunFlow(redeemed.run);
4484
+ const next = {
4485
+ ...stateRef.current ?? initialFunnelState(pinnedService),
4486
+ step: redeemed.run.deliveryMode === "scheduled" && !redeemed.run.reservation ? "slot" : "confirm-identity",
4487
+ applicationRunId,
4488
+ serviceKey: redeemed.run.serviceKey,
4489
+ scheduled: redeemed.run.deliveryMode === "scheduled"
4490
+ };
4491
+ dispatch({ type: "RESTORE", state: next });
4492
+ params.delete("cfhandoff");
4493
+ window.history.replaceState(null, "", `${window.location.pathname}?${params.toString()}${window.location.hash}`);
4494
+ return;
4495
+ }
4496
+ if (!restoreApplicationRunToken(rawClient, applicationRunId)) {
4497
+ throw new Error("This application can only be resumed from its secure sign-in link.");
4498
+ }
4499
+ const resumed = await rawClient.resumeApplicationRun(applicationRunId);
4500
+ if (!alive) return;
4501
+ setApplicationRun(resumed);
4502
+ await hydrateRunFlow(resumed);
4503
+ const current = stateRef.current;
4504
+ if (current) {
4505
+ dispatch({
4506
+ type: "RESTORE",
4507
+ state: {
4508
+ ...current,
4509
+ step: resumed.deliveryMode === "scheduled" && !resumed.reservation && RESERVATION_REQUIRED_STEPS.has(current.step) ? "slot" : current.step,
4510
+ serviceKey: resumed.serviceKey,
4511
+ scheduled: resumed.deliveryMode === "scheduled",
4512
+ gate: resumed.gate ? { kind: resumed.gate.verdict === "escalate" ? "escalate" : "block", ruleId: resumed.gate.ruleId, message: resumed.gate.message } : current.gate
4513
+ }
4514
+ });
4515
+ }
4516
+ })().catch((err) => {
4517
+ if (alive) setResolveError(err instanceof Error ? err.message : "We could not resume this application.");
4518
+ });
4519
+ return () => {
4520
+ alive = false;
4521
+ };
4522
+ }, [applicationRunId, applicationRun?.id, dispatch, hydrateRunFlow, pinnedService, portalClient, rawClient]);
4523
+ const reopenScreening = React16.useCallback(async () => {
4524
+ if (!applicationRunId || !applicationRun || applicationRun.status !== "screening_complete" && applicationRun.status !== "reserved") {
4525
+ return;
4526
+ }
4527
+ setReopeningScreening(true);
4528
+ setReopenScreeningError(null);
4529
+ try {
4530
+ const run = await applicationRunCommands.act(client, applicationRunId, {
4531
+ expectedRevision: applicationRun.revision,
4532
+ action: { type: "reopen_screening" }
4533
+ });
4534
+ setApplicationRun(run);
4535
+ } catch (err) {
4536
+ setReopenScreeningError(
4537
+ errorMessage(err, "We could not reopen your screening. Please try again.")
4538
+ );
4539
+ } finally {
4540
+ setReopeningScreening(false);
4541
+ }
4542
+ }, [applicationRun, applicationRunCommands, applicationRunId, client]);
4543
+ React16.useEffect(() => {
4544
+ if (state?.step === "intake") void reopenScreening();
4545
+ }, [reopenScreening, state?.step]);
4546
+ const beginRun = React16.useCallback(async (serviceKey) => {
4547
+ if (capturing) return;
4548
+ setCapturing(true);
4549
+ setResolveError(null);
4550
+ try {
4166
4551
  const svc = await findServiceByKey(client, serviceKey);
4167
- if (!svc) throw new Error("This certificate type is not currently available.");
4552
+ if (!svc || svc.anonymousApplicationRuns === false) {
4553
+ throw new Error("This certificate type is not currently available.");
4554
+ }
4168
4555
  const option = await findDeliveryOption(client, svc.id);
4169
4556
  if (!option) throw new Error("No booking option is available for this service.");
4557
+ let attempt = beginAttemptRef.current;
4558
+ if (!attempt || attempt.serviceKey !== serviceKey || attempt.serviceDeliveryOptionId !== option.id) {
4559
+ const bytes = new Uint8Array(32);
4560
+ crypto.getRandomValues(bytes);
4561
+ attempt = {
4562
+ serviceKey,
4563
+ serviceDeliveryOptionId: option.id,
4564
+ idempotencyKey: crypto.randomUUID(),
4565
+ capabilityToken: Array.from(
4566
+ bytes,
4567
+ (b) => b.toString(16).padStart(2, "0")
4568
+ ).join("")
4569
+ };
4570
+ beginAttemptRef.current = attempt;
4571
+ }
4572
+ const result = await client.beginApplicationRun(attempt);
4573
+ beginAttemptRef.current = null;
4574
+ saveApplicationRunToken(client, result.run.id);
4170
4575
  const record = {
4171
4576
  serviceId: svc.id,
4172
4577
  serviceKey: svc.key,
4173
4578
  serviceLabel: svc.label,
4174
4579
  deliveryOptionId: option.id,
4175
4580
  appointmentTypeId: option.appointmentTypeId ?? "",
4176
- // The EFFECTIVE fee: the option's override wins over the service base (PAT-710).
4177
4581
  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
4582
  requiresPayment: svc.requiresPayment,
4182
4583
  deliveryMode: option.deliveryMode,
4183
- givenName: identity.givenName,
4184
- familyName: identity.familyName,
4185
- dateOfBirth: identity.dateOfBirth || null,
4584
+ givenName: "",
4585
+ familyName: "",
4586
+ dateOfBirth: null,
4186
4587
  questionSetId: svc.questionSetId
4187
4588
  };
4188
- saveFlowRecord(newRequestId, record);
4589
+ saveFlowRecord(result.run.id, record);
4189
4590
  setFlow(record);
4190
- },
4191
- [client]
4192
- );
4193
- const back2 = React16.useCallback(() => dispatch({ type: "BACK" }), [dispatch]);
4591
+ setApplicationRun(result.run);
4592
+ dispatch({
4593
+ type: "RUN_STARTED",
4594
+ applicationRunId: result.run.id,
4595
+ scheduled: result.run.deliveryMode === "scheduled"
4596
+ });
4597
+ } catch (err) {
4598
+ setResolveError(err instanceof Error ? err.message : "We could not start your application.");
4599
+ } finally {
4600
+ setCapturing(false);
4601
+ }
4602
+ }, [capturing, client, dispatch]);
4603
+ const back2 = React16.useCallback(() => {
4604
+ const runId = state?.applicationRunId;
4605
+ if (state?.step === "intake" && runId) {
4606
+ void client.closeApplicationRun(runId).finally(() => dispatch({ type: "BACK" }));
4607
+ return;
4608
+ }
4609
+ const returnsToIntake = state?.step === "slot" || state?.step === "identity" && state.scheduled === false;
4610
+ if (returnsToIntake && runId && applicationRun && (applicationRun.status === "screening_complete" || applicationRun.status === "reserved")) {
4611
+ void applicationRunCommands.act(client, runId, {
4612
+ expectedRevision: applicationRun.revision,
4613
+ action: { type: "reopen_screening" }
4614
+ }).then((run) => {
4615
+ setApplicationRun(run);
4616
+ dispatch({ type: "BACK" });
4617
+ }).catch((err) => {
4618
+ setResolveError(errorMessage(err, "We could not reopen your screening. Please try again."));
4619
+ });
4620
+ return;
4621
+ }
4622
+ dispatch({ type: "BACK" });
4623
+ }, [applicationRun, applicationRunCommands, client, dispatch, state]);
4194
4624
  const restart = React16.useCallback(() => {
4195
4625
  setIdentityDraft(null);
4196
- setSubmittedIdentitySnapshot(null);
4197
4626
  setMedicareDraft(null);
4198
4627
  dispatch({ type: "RESTART" });
4199
4628
  }, [dispatch]);
4200
4629
  const retryPayment = React16.useCallback(() => {
4201
- setSubmittedIdentitySnapshot(null);
4202
4630
  dispatch({ type: "RETRY_PAYMENT" });
4203
4631
  }, [dispatch]);
4204
4632
  const cancelLostSlot = React16.useCallback(async () => {
4205
4633
  const id = state?.requestId;
4206
- if (!id || cancellingLostSlot) return;
4634
+ const runId = state?.applicationRunId;
4635
+ if (!id && !runId || cancellingLostSlot) return;
4207
4636
  setCancellingLostSlot(true);
4208
4637
  setLostSlotCancelError(null);
4209
4638
  try {
4210
- await cancelRequestThenRestart(client, id, restart);
4639
+ if (id) await cancelRequestThenRestart(client, id, restart);
4640
+ else if (runId) {
4641
+ await client.closeApplicationRun(runId);
4642
+ restart();
4643
+ }
4211
4644
  setCancellingLostSlot(false);
4212
4645
  } catch (err) {
4213
4646
  setCancellingLostSlot(false);
@@ -4215,7 +4648,7 @@ function Funnel({
4215
4648
  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
4649
  );
4217
4650
  }
4218
- }, [cancellingLostSlot, client, restart, state?.requestId]);
4651
+ }, [cancellingLostSlot, client, restart, state?.applicationRunId, state?.requestId]);
4219
4652
  const chosenKey = state?.serviceKey ?? null;
4220
4653
  const step = state?.step;
4221
4654
  const shape = React16.useMemo(() => {
@@ -4241,16 +4674,32 @@ function Funnel({
4241
4674
  }, [flow, fees, pinnedService, chosenKey, step, questionCounts]);
4242
4675
  if (!state) return /* @__PURE__ */ jsx19(FlowLoading, {});
4243
4676
  const title = stepTitle(state.step, clinicName);
4244
- const service = services.find((s) => s.key === state.serviceKey) ?? null;
4677
+ const selectableServices = enabledServiceKeys ? services.filter((candidate) => enabledServiceKeys.has(candidate.key)) : [];
4678
+ const service = selectableServices.find((candidate) => candidate.key === state.serviceKey) ?? null;
4245
4679
  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
4680
  const showBack = canGoBack(state, pinned);
4253
4681
  const flowLost = slotFlowIsLost(flowChecked, flow, capturing);
4682
+ const reservationWasLost = (err) => {
4683
+ const code2 = errorCode(err);
4684
+ return code2 === "reservation_expired" || code2 === "slot_taken";
4685
+ };
4686
+ async function bookReservedSlot() {
4687
+ const id = requestId;
4688
+ const reservation = applicationRun?.reservation;
4689
+ if (!id || !reservation) {
4690
+ throw new PatientOSApiError2({
4691
+ status: 409,
4692
+ code: "reservation_expired",
4693
+ message: "Your appointment reservation has expired. Please choose another time.",
4694
+ path: `/requests/${id ?? "unknown"}/book`
4695
+ });
4696
+ }
4697
+ await client.bookRequest(id, {
4698
+ practitionerId: reservation.practitionerId,
4699
+ slotStart: reservation.slotStart,
4700
+ holdId: reservation.holdId
4701
+ });
4702
+ }
4254
4703
  const quietMoney = HOLDLESS_STEPS.has(state.step) || state.step === "consent" || state.step === "payment";
4255
4704
  const rail = /* @__PURE__ */ jsx19(
4256
4705
  TrustRail,
@@ -4265,7 +4714,7 @@ function Funnel({
4265
4714
  return /* @__PURE__ */ jsx19(
4266
4715
  ServiceStep,
4267
4716
  {
4268
- services,
4717
+ services: selectableServices,
4269
4718
  fees,
4270
4719
  deliveryModes,
4271
4720
  heading: heading2,
@@ -4276,69 +4725,111 @@ function Funnel({
4276
4725
  }
4277
4726
  );
4278
4727
  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
- );
4728
+ if (!enabledServiceKeys) return /* @__PURE__ */ jsx19(FlowLoading, {});
4729
+ if (!state.serviceKey || !enabledServiceKeys.has(state.serviceKey)) {
4730
+ return /* @__PURE__ */ jsxs17(
4731
+ FlowShell,
4732
+ {
4733
+ ...stepProgress("preflight", shape),
4734
+ stepKey: "preflight:unavailable",
4735
+ heading: "This certificate type is unavailable",
4736
+ documentTitle: title,
4737
+ onBack: showBack ? back2 : void 0,
4738
+ children: [
4739
+ /* @__PURE__ */ jsx19(FlowError, { children: "This certificate type is not currently available through this website." }),
4740
+ /* @__PURE__ */ jsx19(
4741
+ FlowButton,
4742
+ {
4743
+ type: "button",
4744
+ variant: "primary",
4745
+ onClick: () => dispatch({ type: "NEED_DIFFERENT" }),
4746
+ children: "Choose another option"
4747
+ }
4748
+ )
4749
+ ]
4750
+ }
4751
+ );
4752
+ }
4753
+ return /* @__PURE__ */ jsxs17("div", { children: [
4754
+ /* @__PURE__ */ jsx19(
4755
+ PreflightStep,
4756
+ {
4757
+ documentTitle: title,
4758
+ onConfirm: () => void beginRun(state.serviceKey ?? ""),
4759
+ onExit: (gate) => dispatch({ type: "GATE", gate }),
4760
+ onOutOfScope: () => dispatch({ type: "NEED_DIFFERENT" }),
4761
+ onBack: showBack ? back2 : void 0
4762
+ }
4763
+ ),
4764
+ /* @__PURE__ */ jsx19(FlowError, { children: resolveError })
4765
+ ] });
4289
4766
  case "identity":
4290
4767
  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
- }
4768
+ return /* @__PURE__ */ jsxs17("div", { children: [
4769
+ /* @__PURE__ */ jsx19(
4770
+ IdentityStep,
4771
+ {
4772
+ client,
4773
+ turnstileSiteKey: api.turnstileSiteKey,
4774
+ serviceKey: state.serviceKey ?? "",
4775
+ serviceLabel,
4776
+ applicationRunId: state.applicationRunId ?? void 0,
4777
+ reservationExpiresAt: applicationRun?.reservation?.expiresAt ?? null,
4778
+ mode: state.step,
4779
+ documentTitle: title,
4780
+ onSignedIn: () => dispatch({ type: "SIGNED_IN" }),
4781
+ onRejected: () => {
4782
+ setIdentityDraft(null);
4783
+ setMedicareDraft(null);
4784
+ dispatch({ type: "IDENTITY_REJECTED" });
4785
+ },
4786
+ onBack: showBack ? back2 : void 0,
4787
+ draft: identityDraft ?? void 0,
4788
+ onDraftChange,
4789
+ onStarted: (newRequestId, identity) => {
4790
+ const capturedDraft = capturedIdentityDraft(identity, identityDraft);
4791
+ setIdentityDraft(capturedDraft);
4792
+ if (flow) {
4793
+ const next = {
4794
+ ...flow,
4795
+ givenName: identity.givenName,
4796
+ familyName: identity.familyName,
4797
+ dateOfBirth: identity.dateOfBirth || null
4798
+ };
4799
+ saveFlowRecord(newRequestId, next);
4800
+ setFlow(next);
4330
4801
  }
4331
- ),
4332
- /* @__PURE__ */ jsx19(FlowError, { children: resolveError })
4333
- ]
4334
- }
4335
- );
4802
+ dispatch({ type: "REQUEST_STARTED", requestId: newRequestId });
4803
+ }
4804
+ }
4805
+ ),
4806
+ /* @__PURE__ */ jsx19(FlowError, { children: resolveError })
4807
+ ] });
4336
4808
  case "intake":
4809
+ if (reopeningScreening || reopenScreeningError || applicationRun?.status === "screening_complete" || applicationRun?.status === "reserved") {
4810
+ return /* @__PURE__ */ jsxs17(
4811
+ FlowShell,
4812
+ {
4813
+ ...stepProgress("intake", shape),
4814
+ stepKey: "intake:reopening",
4815
+ heading: "Review your screening",
4816
+ documentTitle: title,
4817
+ onBack: showBack ? back2 : void 0,
4818
+ children: [
4819
+ reopeningScreening && /* @__PURE__ */ jsx19(FlowLoading, { message: "Reopening your screening\u2026" }),
4820
+ /* @__PURE__ */ jsx19(FlowError, { children: reopenScreeningError }),
4821
+ reopenScreeningError && /* @__PURE__ */ jsx19(FlowButton, { type: "button", variant: "primary", onClick: () => void reopenScreening(), children: "Try again" })
4822
+ ]
4823
+ }
4824
+ );
4825
+ }
4337
4826
  return /* @__PURE__ */ jsx19(
4338
4827
  IntakeStep,
4339
4828
  {
4340
4829
  client,
4341
- requestId: state.requestId ?? "",
4830
+ applicationRunId: state.applicationRunId ?? void 0,
4831
+ applicationRunRevision: applicationRun?.revision ?? 0,
4832
+ onApplicationRunChange: setApplicationRun,
4342
4833
  questionSetId: flow ? flow.questionSetId ?? null : void 0,
4343
4834
  resolveError,
4344
4835
  documentTitle: title,
@@ -4389,7 +4880,22 @@ function Funnel({
4389
4880
  phone: identityDraft?.identity.phone ?? "",
4390
4881
  serviceLabel: service?.label ?? "",
4391
4882
  documentTitle: title,
4392
- onSubmitted: (status, gate) => dispatch({ type: "SUBMITTED", status, gate }),
4883
+ onSubmitted: async (status, gate) => {
4884
+ if (status === "awaiting_slot") {
4885
+ try {
4886
+ await bookReservedSlot();
4887
+ dispatch({ type: "SUBMITTED", status: "ready", gate });
4888
+ } catch (err) {
4889
+ if (reservationWasLost(err)) {
4890
+ dispatch({ type: "RESERVATION_LOST" });
4891
+ return;
4892
+ }
4893
+ throw err;
4894
+ }
4895
+ } else {
4896
+ dispatch({ type: "SUBMITTED", status, gate });
4897
+ }
4898
+ },
4393
4899
  onGate: (gate) => dispatch({ type: "GATE", gate }),
4394
4900
  onBack: showBack ? back2 : void 0
4395
4901
  }
@@ -4412,7 +4918,22 @@ function Funnel({
4412
4918
  feeAmount: flow?.feeAmount ?? null,
4413
4919
  serviceLabel,
4414
4920
  clinicPhone,
4415
- onOutcome: (outcome) => dispatch({ type: "PAY_OUTCOME", outcome })
4921
+ onOutcome: async (outcome) => {
4922
+ if (outcome.kind === "awaiting_slot") {
4923
+ try {
4924
+ await bookReservedSlot();
4925
+ dispatch({ type: "PAY_OUTCOME", outcome: { kind: "ready" } });
4926
+ } catch (err) {
4927
+ if (reservationWasLost(err)) {
4928
+ dispatch({ type: "RESERVATION_LOST" });
4929
+ return;
4930
+ }
4931
+ throw err;
4932
+ }
4933
+ } else {
4934
+ dispatch({ type: "PAY_OUTCOME", outcome });
4935
+ }
4936
+ }
4416
4937
  }
4417
4938
  )
4418
4939
  }
@@ -4422,20 +4943,24 @@ function Funnel({
4422
4943
  SlotStep,
4423
4944
  {
4424
4945
  client,
4425
- requestId: state.requestId ?? "",
4946
+ requestId: state.requestId ?? void 0,
4947
+ applicationRunId: state.requestId ? void 0 : state.applicationRunId ?? void 0,
4948
+ applicationRunRevision: applicationRun?.revision ?? 0,
4949
+ onApplicationRunChange: setApplicationRun,
4950
+ replacementReservation: Boolean(state.requestId && state.applicationRunId),
4426
4951
  appointmentTypeId: flow.appointmentTypeId,
4427
4952
  isFree: flowIsFree(flow),
4428
4953
  clinicPhone,
4429
4954
  onCancelled: restart,
4430
4955
  documentTitle: title,
4431
4956
  onBooked: (bookedFor) => {
4432
- const id = state.requestId;
4957
+ const id = state.requestId ?? state.applicationRunId;
4433
4958
  if (id && flow) {
4434
4959
  const next = { ...flow, bookedFor };
4435
4960
  saveFlowRecord(id, next);
4436
4961
  setFlow(next);
4437
4962
  }
4438
- dispatch({ type: "BOOKED" });
4963
+ dispatch({ type: state.requestId ? "BOOKED" : "RESERVED" });
4439
4964
  }
4440
4965
  }
4441
4966
  ) : flowLost ? (
@@ -4557,14 +5082,14 @@ function SessionExpiredState({
4557
5082
  stepKey: "session-expired",
4558
5083
  heading: "Your secure session ended",
4559
5084
  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,
5085
+ 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
5086
  children: [
4562
5087
  /* @__PURE__ */ jsxs17("p", { className: "sk-cf__body", children: [
4563
5088
  "This secure session is no longer available.",
4564
5089
  " ",
4565
5090
  requestStarted ? "Your request is still with the clinic." : "You can start again securely."
4566
5091
  ] }),
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
5092
+ 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
5093
  ]
4569
5094
  }
4570
5095
  );
@@ -10954,14 +11479,10 @@ function PortalAccountSurfaceClient({
10954
11479
  portalUrl,
10955
11480
  bookHref,
10956
11481
  initialState,
10957
- fetchImpl: rawFetchImpl,
10958
11482
  openUrl
10959
11483
  }) {
10960
11484
  const portalClient = usePortalClient();
10961
- const fetchImpl = React21.useMemo(
10962
- () => portalFetchForClient(portalClient, rawFetchImpl),
10963
- [portalClient, rawFetchImpl]
10964
- );
11485
+ const fetchImpl = React21.useMemo(() => portalFetchForClient(portalClient), [portalClient]);
10965
11486
  const [state, setState] = React21.useState(
10966
11487
  // `readPortalSignedInHint` is in the initialiser (not an effect) on purpose: the
10967
11488
  // whole point is that the FIRST paint already knows, so there is no placeholder