@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.
- package/README.md +5 -0
- package/dist/{chunk-S7U2HOV7.js → chunk-75FJ5AJB.js} +714 -193
- package/dist/{chunk-Y67WZPV5.js → chunk-7UPUEMXX.js} +1 -1
- package/dist/{chunk-BHVDY6SC.js → chunk-FSZ726DM.js} +198 -13
- package/dist/index.d.ts +3 -2
- package/dist/index.js +3 -3
- package/dist/islands-impl.d.ts +3 -2
- package/dist/islands-impl.js +2 -2
- package/dist/islands-registry.js +3 -3
- package/dist/{portal-account-pI1cWgsT.d.ts → portal-account-8uQc_Ncx.d.ts} +8 -2
- package/dist/{portal-account.client-DPRRmfCJ.d.ts → portal-account.client-BBe_q9yC.d.ts} +1 -1
- package/dist/portal-booking-client.d.ts +3 -2
- package/dist/portal-booking-client.js +1 -1
- package/package.json +3 -2
|
@@ -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-
|
|
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
|
|
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
|
|
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
|
|
1286
|
+
const intake = intakeSpan(shape);
|
|
1252
1287
|
if (step === "service") return shape.pinned ? void 0 : 1;
|
|
1253
|
-
if (step === "
|
|
1254
|
-
if (step === "
|
|
1255
|
-
|
|
1256
|
-
const
|
|
1257
|
-
if (
|
|
1258
|
-
|
|
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 "
|
|
1339
|
+
case "RUN_STARTED":
|
|
1300
1340
|
if (state.step !== "preflight") return stay2(state);
|
|
1301
|
-
return go2(state, {
|
|
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: "
|
|
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: "
|
|
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, {
|
|
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: "
|
|
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: "
|
|
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, {
|
|
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 = {
|
|
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 (
|
|
2296
|
-
|
|
2297
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
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
|
|
2788
|
-
|
|
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
|
-
|
|
2801
|
-
|
|
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
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
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 (!
|
|
4432
|
+
if (!flowId) {
|
|
4153
4433
|
setFlow(null);
|
|
4154
4434
|
setFlowChecked(false);
|
|
4435
|
+
setApplicationRun(null);
|
|
4155
4436
|
return;
|
|
4156
4437
|
}
|
|
4157
|
-
if (flow)
|
|
4158
|
-
|
|
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
|
-
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
|
|
4165
|
-
|
|
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
|
|
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:
|
|
4184
|
-
familyName:
|
|
4185
|
-
dateOfBirth:
|
|
4584
|
+
givenName: "",
|
|
4585
|
+
familyName: "",
|
|
4586
|
+
dateOfBirth: null,
|
|
4186
4587
|
questionSetId: svc.questionSetId
|
|
4187
4588
|
};
|
|
4188
|
-
saveFlowRecord(
|
|
4589
|
+
saveFlowRecord(result.run.id, record);
|
|
4189
4590
|
setFlow(record);
|
|
4190
|
-
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
4281
|
-
|
|
4282
|
-
|
|
4283
|
-
|
|
4284
|
-
|
|
4285
|
-
|
|
4286
|
-
|
|
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
|
-
|
|
4293
|
-
|
|
4294
|
-
|
|
4295
|
-
|
|
4296
|
-
|
|
4297
|
-
|
|
4298
|
-
|
|
4299
|
-
|
|
4300
|
-
|
|
4301
|
-
|
|
4302
|
-
|
|
4303
|
-
|
|
4304
|
-
|
|
4305
|
-
|
|
4306
|
-
|
|
4307
|
-
|
|
4308
|
-
|
|
4309
|
-
|
|
4310
|
-
|
|
4311
|
-
|
|
4312
|
-
|
|
4313
|
-
|
|
4314
|
-
|
|
4315
|
-
|
|
4316
|
-
|
|
4317
|
-
|
|
4318
|
-
|
|
4319
|
-
|
|
4320
|
-
|
|
4321
|
-
|
|
4322
|
-
|
|
4323
|
-
|
|
4324
|
-
setSubmittedIdentitySnapshot(JSON.stringify(capturedDraft));
|
|
4325
|
-
dispatch({ type: "REQUEST_STARTED", requestId: newRequestId });
|
|
4326
|
-
void captureFlow(newRequestId, state.serviceKey ?? "", identity).catch(
|
|
4327
|
-
(e) => setResolveError(e instanceof Error ? e.message : "Something went wrong.")
|
|
4328
|
-
).finally(() => setCapturing(false));
|
|
4329
|
-
}
|
|
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
|
-
|
|
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
|
-
|
|
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) =>
|
|
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) =>
|
|
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" }) :
|
|
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 }) :
|
|
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
|