@patientos/website-kit 0.2.3 → 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 +8 -0
- package/dist/{chunk-LHP4RZ3N.js → chunk-75FJ5AJB.js} +721 -199
- package/dist/{chunk-MTOKBMBG.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";
|
|
@@ -403,6 +405,7 @@ function normalizeAuPhone(input) {
|
|
|
403
405
|
const cleaned = input.replace(/[\s()-]/g, "");
|
|
404
406
|
if (cleaned.startsWith("+")) return cleaned;
|
|
405
407
|
if (cleaned.startsWith("0")) return `+61${cleaned.slice(1)}`;
|
|
408
|
+
if (/^(?:13\d{4}|1300\d{6}|1800\d{6})$/.test(cleaned)) return `+61${cleaned}`;
|
|
406
409
|
if (cleaned.startsWith("61")) return `+${cleaned}`;
|
|
407
410
|
return cleaned;
|
|
408
411
|
}
|
|
@@ -524,7 +527,7 @@ function toSessionInput(v) {
|
|
|
524
527
|
dateOfBirth: v.dateOfBirth
|
|
525
528
|
};
|
|
526
529
|
}
|
|
527
|
-
function SignInRamp({ redirect }) {
|
|
530
|
+
function SignInRamp({ redirect, prepareRedirect }) {
|
|
528
531
|
const portal = usePortalClient();
|
|
529
532
|
const [open, setOpen] = React2.useState(false);
|
|
530
533
|
const [email, setEmail] = React2.useState("");
|
|
@@ -586,7 +589,7 @@ function SignInRamp({ redirect }) {
|
|
|
586
589
|
onClick: () => {
|
|
587
590
|
if (busy) return;
|
|
588
591
|
setBusy(true);
|
|
589
|
-
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(() => {
|
|
590
593
|
setBusy(false);
|
|
591
594
|
});
|
|
592
595
|
},
|
|
@@ -1180,18 +1183,56 @@ function Booking({
|
|
|
1180
1183
|
|
|
1181
1184
|
// src/certificate-funnel.client.tsx
|
|
1182
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
|
+
};
|
|
1183
1224
|
|
|
1184
1225
|
// src/certificate-funnel-machine.ts
|
|
1185
1226
|
var FUNNEL_STEPS = [
|
|
1186
1227
|
"service",
|
|
1187
1228
|
"preflight",
|
|
1229
|
+
"intake",
|
|
1230
|
+
"slot",
|
|
1188
1231
|
"identity",
|
|
1189
1232
|
"confirm-identity",
|
|
1190
|
-
"intake",
|
|
1191
1233
|
"medicare",
|
|
1192
1234
|
"consent",
|
|
1193
1235
|
"payment",
|
|
1194
|
-
"slot",
|
|
1195
1236
|
"submitted",
|
|
1196
1237
|
"unavailable",
|
|
1197
1238
|
"blocked",
|
|
@@ -1200,14 +1241,18 @@ var FUNNEL_STEPS = [
|
|
|
1200
1241
|
"session-expired"
|
|
1201
1242
|
];
|
|
1202
1243
|
var REQUIRES_REQUEST = /* @__PURE__ */ new Set([
|
|
1203
|
-
"intake",
|
|
1204
1244
|
"medicare",
|
|
1205
1245
|
"consent",
|
|
1206
1246
|
"payment",
|
|
1207
|
-
"slot",
|
|
1208
1247
|
"submitted"
|
|
1209
1248
|
]);
|
|
1210
|
-
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"]);
|
|
1211
1256
|
var REQUIRES_SERVICE = /* @__PURE__ */ new Set([
|
|
1212
1257
|
"preflight",
|
|
1213
1258
|
"identity",
|
|
@@ -1224,15 +1269,6 @@ var DEFAULT_SHAPE = {
|
|
|
1224
1269
|
scheduled: true,
|
|
1225
1270
|
intakeSteps: 1
|
|
1226
1271
|
};
|
|
1227
|
-
var SPINE_OFFSET = {
|
|
1228
|
-
preflight: 1,
|
|
1229
|
-
identity: 2,
|
|
1230
|
-
// The signed-in confirmation REPLACES the anonymous form; it is the same step.
|
|
1231
|
-
"confirm-identity": 2,
|
|
1232
|
-
intake: 3,
|
|
1233
|
-
medicare: 4,
|
|
1234
|
-
consent: 5
|
|
1235
|
-
};
|
|
1236
1272
|
function intakeSpan(shape) {
|
|
1237
1273
|
return Math.max(1, Math.trunc(shape.intakeSteps ?? 1) || 1);
|
|
1238
1274
|
}
|
|
@@ -1247,14 +1283,17 @@ function totalSteps(shape) {
|
|
|
1247
1283
|
}
|
|
1248
1284
|
function stepNumberFor(step, shape) {
|
|
1249
1285
|
const picker = shape.pinned ? 0 : 1;
|
|
1250
|
-
const
|
|
1286
|
+
const intake = intakeSpan(shape);
|
|
1251
1287
|
if (step === "service") return shape.pinned ? void 0 : 1;
|
|
1252
|
-
if (step === "
|
|
1253
|
-
if (step === "
|
|
1254
|
-
|
|
1255
|
-
const
|
|
1256
|
-
if (
|
|
1257
|
-
|
|
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;
|
|
1258
1297
|
}
|
|
1259
1298
|
function stepProgress(step, shape = DEFAULT_SHAPE, questionIndex = 0) {
|
|
1260
1299
|
const total = totalSteps(shape);
|
|
@@ -1274,7 +1313,9 @@ function initialFunnelState(serviceKey) {
|
|
|
1274
1313
|
// safety check, never past it: pinning chooses the product, not the screening.
|
|
1275
1314
|
step: serviceKey ? "preflight" : "service",
|
|
1276
1315
|
serviceKey: serviceKey ?? null,
|
|
1316
|
+
applicationRunId: null,
|
|
1277
1317
|
requestId: null,
|
|
1318
|
+
scheduled: null,
|
|
1278
1319
|
gate: null,
|
|
1279
1320
|
failure: null
|
|
1280
1321
|
};
|
|
@@ -1295,9 +1336,13 @@ function reduceFunnel(state, event) {
|
|
|
1295
1336
|
case "SELECT_SERVICE":
|
|
1296
1337
|
if (state.step !== "service") return stay2(state);
|
|
1297
1338
|
return go2(state, { step: "preflight", serviceKey: event.serviceKey });
|
|
1298
|
-
case "
|
|
1339
|
+
case "RUN_STARTED":
|
|
1299
1340
|
if (state.step !== "preflight") return stay2(state);
|
|
1300
|
-
return go2(state, {
|
|
1341
|
+
return go2(state, {
|
|
1342
|
+
step: "intake",
|
|
1343
|
+
applicationRunId: event.applicationRunId,
|
|
1344
|
+
scheduled: event.scheduled
|
|
1345
|
+
});
|
|
1301
1346
|
case "NEED_DIFFERENT":
|
|
1302
1347
|
return go2(state, { step: "unavailable" });
|
|
1303
1348
|
case "SIGNED_IN":
|
|
@@ -1310,7 +1355,7 @@ function reduceFunnel(state, event) {
|
|
|
1310
1355
|
}
|
|
1311
1356
|
case "REQUEST_STARTED":
|
|
1312
1357
|
if (state.step !== "identity" && state.step !== "confirm-identity") return stay2(state);
|
|
1313
|
-
return go2(state, { step: "
|
|
1358
|
+
return go2(state, { step: "medicare", requestId: event.requestId });
|
|
1314
1359
|
case "GATE":
|
|
1315
1360
|
return go2(state, {
|
|
1316
1361
|
step: event.gate.kind === "escalate" ? "escalate" : "blocked",
|
|
@@ -1318,7 +1363,10 @@ function reduceFunnel(state, event) {
|
|
|
1318
1363
|
});
|
|
1319
1364
|
case "ANSWERS_COMPLETE":
|
|
1320
1365
|
if (state.step !== "intake") return stay2(state);
|
|
1321
|
-
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" });
|
|
1322
1370
|
case "MEDICARE_DONE":
|
|
1323
1371
|
if (state.step !== "medicare") return stay2(state);
|
|
1324
1372
|
return go2(state, { step: "consent" });
|
|
@@ -1347,12 +1395,21 @@ function reduceFunnel(state, event) {
|
|
|
1347
1395
|
case "BOOKED":
|
|
1348
1396
|
if (state.step !== "slot") return stay2(state);
|
|
1349
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" });
|
|
1350
1401
|
case "SESSION_EXPIRED":
|
|
1351
1402
|
if (state.step === "session-expired") return stay2(state);
|
|
1352
1403
|
return go2(state, { step: "session-expired" });
|
|
1353
1404
|
case "RETRY_PAYMENT":
|
|
1354
1405
|
if (state.step !== "payment-failed") return stay2(state);
|
|
1355
|
-
return go2(state, {
|
|
1406
|
+
return go2(state, {
|
|
1407
|
+
step: "preflight",
|
|
1408
|
+
applicationRunId: null,
|
|
1409
|
+
requestId: null,
|
|
1410
|
+
scheduled: null,
|
|
1411
|
+
failure: null
|
|
1412
|
+
});
|
|
1356
1413
|
case "RESTART": {
|
|
1357
1414
|
const next = go2(initialFunnelState(), { serviceKey: null });
|
|
1358
1415
|
return next;
|
|
@@ -1365,14 +1422,16 @@ function back(state) {
|
|
|
1365
1422
|
switch (state.step) {
|
|
1366
1423
|
case "preflight":
|
|
1367
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" });
|
|
1368
1429
|
case "identity":
|
|
1369
|
-
return go2(state, { step: "
|
|
1430
|
+
return go2(state, { step: state.scheduled ? "slot" : "intake" });
|
|
1370
1431
|
case "confirm-identity":
|
|
1371
1432
|
return go2(state, { step: "identity" });
|
|
1372
|
-
case "intake":
|
|
1373
|
-
return go2(state, { step: "identity" });
|
|
1374
1433
|
case "medicare":
|
|
1375
|
-
return go2(state, { step: "
|
|
1434
|
+
return go2(state, { step: "identity" });
|
|
1376
1435
|
case "consent":
|
|
1377
1436
|
return go2(state, { step: "medicare" });
|
|
1378
1437
|
case "payment":
|
|
@@ -1380,7 +1439,13 @@ function back(state) {
|
|
|
1380
1439
|
case "unavailable":
|
|
1381
1440
|
return go2(state, { step: "service" });
|
|
1382
1441
|
case "payment-failed":
|
|
1383
|
-
return go2(state, {
|
|
1442
|
+
return go2(state, {
|
|
1443
|
+
step: "preflight",
|
|
1444
|
+
applicationRunId: null,
|
|
1445
|
+
requestId: null,
|
|
1446
|
+
scheduled: null,
|
|
1447
|
+
failure: null
|
|
1448
|
+
});
|
|
1384
1449
|
default:
|
|
1385
1450
|
return stay2(state);
|
|
1386
1451
|
}
|
|
@@ -1389,10 +1454,6 @@ function canGoBack(state, opts = {}) {
|
|
|
1389
1454
|
if (state.step === "preflight" && opts.pinnedService) return false;
|
|
1390
1455
|
return back(state).effects.length > 0;
|
|
1391
1456
|
}
|
|
1392
|
-
function resumableRequestId(requestId, submittedSnapshot, currentSnapshot) {
|
|
1393
|
-
if (!requestId || !submittedSnapshot || !currentSnapshot) return null;
|
|
1394
|
-
return submittedSnapshot === currentSnapshot ? requestId : null;
|
|
1395
|
-
}
|
|
1396
1457
|
var TERMINAL_WALL = /* @__PURE__ */ new Set([
|
|
1397
1458
|
"escalate",
|
|
1398
1459
|
"blocked",
|
|
@@ -1413,18 +1474,26 @@ function popAction(current, search, pinnedService) {
|
|
|
1413
1474
|
if (TERMINAL_WALL.has(current.step)) return { type: "refuse" };
|
|
1414
1475
|
return { type: "restore", state: stepFromUrl(search, pinnedService) };
|
|
1415
1476
|
}
|
|
1416
|
-
var URL_KEYS = {
|
|
1477
|
+
var URL_KEYS = {
|
|
1478
|
+
step: "cf",
|
|
1479
|
+
requestId: "cfid",
|
|
1480
|
+
applicationRunId: "cfrun",
|
|
1481
|
+
serviceKey: "cfsvc"
|
|
1482
|
+
};
|
|
1417
1483
|
function stepFromUrl(search, pinnedService) {
|
|
1418
1484
|
const params = new URLSearchParams(search.startsWith("?") ? search.slice(1) : search);
|
|
1419
1485
|
const rawStep = params.get(URL_KEYS.step) ?? "";
|
|
1420
1486
|
const requestId = params.get(URL_KEYS.requestId) || null;
|
|
1487
|
+
const applicationRunId = params.get(URL_KEYS.applicationRunId) || null;
|
|
1421
1488
|
const urlService = params.get(URL_KEYS.serviceKey) || null;
|
|
1422
1489
|
const serviceKey = pinnedService ?? urlService;
|
|
1423
1490
|
const known = FUNNEL_STEPS.includes(rawStep) ? rawStep : null;
|
|
1424
1491
|
const base2 = {
|
|
1425
1492
|
step: known ?? initialFunnelState(serviceKey).step,
|
|
1426
1493
|
serviceKey,
|
|
1494
|
+
applicationRunId,
|
|
1427
1495
|
requestId,
|
|
1496
|
+
scheduled: null,
|
|
1428
1497
|
// Gate + failure detail are NOT in the URL: they are server verdicts with clinical
|
|
1429
1498
|
// copy attached, and a URL that could assert "you are blocked" (or, worse, that you
|
|
1430
1499
|
// are not) would be a forgeable claim about a safety decision.
|
|
@@ -1432,6 +1501,9 @@ function stepFromUrl(search, pinnedService) {
|
|
|
1432
1501
|
failure: null
|
|
1433
1502
|
};
|
|
1434
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) {
|
|
1435
1507
|
return { ...base2, step: serviceKey ? "preflight" : "service" };
|
|
1436
1508
|
}
|
|
1437
1509
|
if (base2.step === "payment-failed" && !base2.requestId) {
|
|
@@ -1443,7 +1515,7 @@ function stepFromUrl(search, pinnedService) {
|
|
|
1443
1515
|
if (REQUIRES_SERVICE.has(base2.step) && !base2.serviceKey) {
|
|
1444
1516
|
return { ...base2, step: "service" };
|
|
1445
1517
|
}
|
|
1446
|
-
if (REQUIRES_PREFLIGHT.has(base2.step)) {
|
|
1518
|
+
if (REQUIRES_PREFLIGHT.has(base2.step) && !base2.applicationRunId) {
|
|
1447
1519
|
return { ...base2, step: base2.serviceKey ? "preflight" : "service" };
|
|
1448
1520
|
}
|
|
1449
1521
|
if (base2.step === "blocked" || base2.step === "escalate") {
|
|
@@ -1455,6 +1527,7 @@ function urlFromState(state, opts = {}) {
|
|
|
1455
1527
|
const params = new URLSearchParams();
|
|
1456
1528
|
params.set(URL_KEYS.step, state.step);
|
|
1457
1529
|
if (state.requestId) params.set(URL_KEYS.requestId, state.requestId);
|
|
1530
|
+
if (state.applicationRunId) params.set(URL_KEYS.applicationRunId, state.applicationRunId);
|
|
1458
1531
|
if (state.serviceKey && !opts.pinnedService) params.set(URL_KEYS.serviceKey, state.serviceKey);
|
|
1459
1532
|
return params.toString();
|
|
1460
1533
|
}
|
|
@@ -1684,17 +1757,17 @@ function PaymentStep({
|
|
|
1684
1757
|
const res = await client.payConfirm(requestId);
|
|
1685
1758
|
switch (res.status) {
|
|
1686
1759
|
case "awaiting_slot":
|
|
1687
|
-
onOutcome({ kind: "awaiting_slot" });
|
|
1760
|
+
await onOutcome({ kind: "awaiting_slot" });
|
|
1688
1761
|
return;
|
|
1689
1762
|
case "ready":
|
|
1690
|
-
onOutcome({ kind: "ready" });
|
|
1763
|
+
await onOutcome({ kind: "ready" });
|
|
1691
1764
|
return;
|
|
1692
1765
|
case "requires_action":
|
|
1693
1766
|
setError("Your bank needs to verify this card. Please try again.");
|
|
1694
1767
|
setPhase("ready");
|
|
1695
1768
|
return;
|
|
1696
1769
|
case "payment_failed":
|
|
1697
|
-
onOutcome({
|
|
1770
|
+
await onOutcome({
|
|
1698
1771
|
kind: "failed",
|
|
1699
1772
|
message: "Your card was declined and the pre-authorisation could not be completed."
|
|
1700
1773
|
});
|
|
@@ -1938,7 +2011,7 @@ function PaymentStep({
|
|
|
1938
2011
|
phase === "init_failed" && clinicPhone && /* @__PURE__ */ jsxs5("p", { className: "sk-cf__body", children: [
|
|
1939
2012
|
"Still not working?",
|
|
1940
2013
|
" ",
|
|
1941
|
-
/* @__PURE__ */ jsxs5("a", { className: "sk-cf__link", href: `tel:${clinicPhone
|
|
2014
|
+
/* @__PURE__ */ jsxs5("a", { className: "sk-cf__link", href: `tel:${normalizeAuPhone(clinicPhone)}`, children: [
|
|
1942
2015
|
"Call the clinic on ",
|
|
1943
2016
|
clinicPhone
|
|
1944
2017
|
] })
|
|
@@ -2290,13 +2363,43 @@ function IdentityStep(props) {
|
|
|
2290
2363
|
},
|
|
2291
2364
|
[client]
|
|
2292
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
|
+
}, []);
|
|
2293
2376
|
const startRequest = React8.useCallback(
|
|
2294
|
-
async (
|
|
2295
|
-
|
|
2296
|
-
|
|
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;
|
|
2297
2386
|
},
|
|
2298
|
-
[client, props.
|
|
2387
|
+
[client, promotionIdempotencyKey, props.applicationRunId, props.serviceKey]
|
|
2299
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]);
|
|
2300
2403
|
async function handleConfirmSignedIn() {
|
|
2301
2404
|
if (busy) return;
|
|
2302
2405
|
setBusy(true);
|
|
@@ -2310,13 +2413,15 @@ function IdentityStep(props) {
|
|
|
2310
2413
|
return;
|
|
2311
2414
|
}
|
|
2312
2415
|
adoptClaimToken(client, claim.claimToken);
|
|
2313
|
-
|
|
2416
|
+
const confirmedIdentity = {
|
|
2314
2417
|
givenName: prefill?.givenName ?? "",
|
|
2315
2418
|
familyName: prefill?.familyName ?? "",
|
|
2316
2419
|
email: prefill?.email ?? "",
|
|
2317
2420
|
phone: prefill?.phone ?? "",
|
|
2318
2421
|
dateOfBirth: prefill?.dateOfBirth ?? ""
|
|
2319
|
-
}
|
|
2422
|
+
};
|
|
2423
|
+
const requestId = await startRequest();
|
|
2424
|
+
onStarted(requestId, confirmedIdentity);
|
|
2320
2425
|
} catch (err) {
|
|
2321
2426
|
setBusy(false);
|
|
2322
2427
|
setError(errorMessage(err, "We could not start your request. Please try again."));
|
|
@@ -2341,17 +2446,29 @@ function IdentityStep(props) {
|
|
|
2341
2446
|
setBusy(true);
|
|
2342
2447
|
setError(null);
|
|
2343
2448
|
try {
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
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
|
+
}
|
|
2355
2472
|
onStarted(requestId, identity);
|
|
2356
2473
|
} catch (err) {
|
|
2357
2474
|
setBusy(false);
|
|
@@ -2375,6 +2492,10 @@ function IdentityStep(props) {
|
|
|
2375
2492
|
setError(errorMessage(err, "We could not start your request. Please try again."));
|
|
2376
2493
|
}
|
|
2377
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))}.` : "";
|
|
2378
2499
|
if (mode === "confirm-identity") {
|
|
2379
2500
|
return /* @__PURE__ */ jsxs8(
|
|
2380
2501
|
FlowShell,
|
|
@@ -2382,7 +2503,7 @@ function IdentityStep(props) {
|
|
|
2382
2503
|
...progress,
|
|
2383
2504
|
stepKey: "confirm-identity",
|
|
2384
2505
|
heading: "Confirm your details",
|
|
2385
|
-
subheading: detailsSubheading(props.serviceLabel)
|
|
2506
|
+
subheading: `${detailsSubheading(props.serviceLabel)}${reservationNote}`,
|
|
2386
2507
|
onBack,
|
|
2387
2508
|
documentTitle,
|
|
2388
2509
|
children: [
|
|
@@ -2407,12 +2528,12 @@ function IdentityStep(props) {
|
|
|
2407
2528
|
...progress,
|
|
2408
2529
|
stepKey: "identity",
|
|
2409
2530
|
heading: "Your details",
|
|
2410
|
-
subheading: detailsSubheading(props.serviceLabel)
|
|
2531
|
+
subheading: `${detailsSubheading(props.serviceLabel)}${reservationNote}`,
|
|
2411
2532
|
onBack,
|
|
2412
2533
|
documentTitle,
|
|
2413
2534
|
footer: probing ? void 0 : /* @__PURE__ */ jsx10(FlowButton, { type: "submit", form: "sk-cf-identity", variant: "primary", busy, children: busy ? "Starting\u2026" : "Continue" }),
|
|
2414
2535
|
children: probing ? /* @__PURE__ */ jsx10(FlowLoading, { message: "Checking if you\u2019re signed in\u2026" }) : /* @__PURE__ */ jsxs8(Fragment3, { children: [
|
|
2415
|
-
/* @__PURE__ */ jsx10(SignInRamp, { redirect: props.returnUrl }),
|
|
2536
|
+
/* @__PURE__ */ jsx10(SignInRamp, { redirect: props.returnUrl, prepareRedirect: prepareSignInRedirect }),
|
|
2416
2537
|
/* @__PURE__ */ jsx10(FlowErrorSummary, { fields: invalid }),
|
|
2417
2538
|
/* @__PURE__ */ jsxs8("form", { id: "sk-cf-identity", onSubmit: (e) => void handleSubmitAnonymous(e), noValidate: true, children: [
|
|
2418
2539
|
/* @__PURE__ */ jsx10(SummarisedFieldErrors, { children: /* @__PURE__ */ jsx10(
|
|
@@ -2742,6 +2863,9 @@ function intakeRowsAction(rows) {
|
|
|
2742
2863
|
function IntakeStep({
|
|
2743
2864
|
client,
|
|
2744
2865
|
requestId,
|
|
2866
|
+
applicationRunId,
|
|
2867
|
+
applicationRunRevision = 0,
|
|
2868
|
+
onApplicationRunChange,
|
|
2745
2869
|
questionSetId,
|
|
2746
2870
|
resolveError,
|
|
2747
2871
|
onGate,
|
|
@@ -2755,6 +2879,15 @@ function IntakeStep({
|
|
|
2755
2879
|
const [draft, setDraft] = React10.useState(null);
|
|
2756
2880
|
const [busy, setBusy] = React10.useState(false);
|
|
2757
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]);
|
|
2758
2891
|
const [reload2, setReload] = React10.useState(0);
|
|
2759
2892
|
const progress = useStepProgress("intake", index);
|
|
2760
2893
|
React10.useEffect(() => {
|
|
@@ -2783,8 +2916,28 @@ function IntakeStep({
|
|
|
2783
2916
|
};
|
|
2784
2917
|
}, [client, questionSetId, reload2]);
|
|
2785
2918
|
React10.useEffect(() => {
|
|
2786
|
-
if (questions
|
|
2787
|
-
|
|
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]);
|
|
2788
2941
|
const visible = React10.useMemo(
|
|
2789
2942
|
() => questions ? visibleQuestions(questions, answers) : [],
|
|
2790
2943
|
[questions, answers]
|
|
@@ -2794,17 +2947,60 @@ function IntakeStep({
|
|
|
2794
2947
|
React10.useEffect(() => {
|
|
2795
2948
|
setDraft(current ? answers[current.key] ?? null : null);
|
|
2796
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
|
+
}
|
|
2797
2974
|
async function record(value) {
|
|
2798
2975
|
if (!current || busy) return;
|
|
2799
|
-
|
|
2800
|
-
|
|
2976
|
+
if (applicationRunId && pendingCompletionRevision.current != null) {
|
|
2977
|
+
await retryPendingCompletion();
|
|
2978
|
+
return;
|
|
2979
|
+
}
|
|
2801
2980
|
setBusy(true);
|
|
2802
2981
|
setError(null);
|
|
2982
|
+
const next = { ...answers, [current.key]: value };
|
|
2983
|
+
setAnswers(next);
|
|
2803
2984
|
try {
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
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
|
+
}
|
|
2808
3004
|
const exit = gateToExit(gate);
|
|
2809
3005
|
if (exit) {
|
|
2810
3006
|
onGate(exit);
|
|
@@ -2812,7 +3008,25 @@ function IntakeStep({
|
|
|
2812
3008
|
}
|
|
2813
3009
|
const nextVisible = questions ? visibleQuestions(questions, next) : [];
|
|
2814
3010
|
if (index + 1 >= nextVisible.length) {
|
|
2815
|
-
|
|
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
|
+
}
|
|
2816
3030
|
return;
|
|
2817
3031
|
}
|
|
2818
3032
|
setIndex(index + 1);
|
|
@@ -2858,6 +3072,23 @@ function IntakeStep({
|
|
|
2858
3072
|
}
|
|
2859
3073
|
);
|
|
2860
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
|
+
}
|
|
2861
3092
|
if (!current) {
|
|
2862
3093
|
return /* @__PURE__ */ jsx12(
|
|
2863
3094
|
IntakeInterstitial,
|
|
@@ -2905,7 +3136,17 @@ function IntakeStep({
|
|
|
2905
3136
|
idPrefix: `sk-cf-${index}`
|
|
2906
3137
|
}
|
|
2907
3138
|
),
|
|
2908
|
-
/* @__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
|
+
)
|
|
2909
3150
|
]
|
|
2910
3151
|
}
|
|
2911
3152
|
);
|
|
@@ -3309,7 +3550,7 @@ function ConsentStep({
|
|
|
3309
3550
|
onGate(gate ?? { kind: "block" });
|
|
3310
3551
|
return;
|
|
3311
3552
|
}
|
|
3312
|
-
onSubmitted(result.status, gate);
|
|
3553
|
+
await onSubmitted(result.status, gate);
|
|
3313
3554
|
} catch (err) {
|
|
3314
3555
|
setBusy(false);
|
|
3315
3556
|
setError(errorMessage(err, "We could not submit your request. Please try again."));
|
|
@@ -3414,6 +3655,10 @@ function SlotCancelControl({
|
|
|
3414
3655
|
function SlotStep({
|
|
3415
3656
|
client,
|
|
3416
3657
|
requestId,
|
|
3658
|
+
applicationRunId,
|
|
3659
|
+
applicationRunRevision = 0,
|
|
3660
|
+
onApplicationRunChange,
|
|
3661
|
+
replacementReservation = false,
|
|
3417
3662
|
appointmentTypeId,
|
|
3418
3663
|
isFree,
|
|
3419
3664
|
onBooked,
|
|
@@ -3424,13 +3669,15 @@ function SlotStep({
|
|
|
3424
3669
|
const [cancelling, setCancelling] = React13.useState(false);
|
|
3425
3670
|
const [confirmCancel, setConfirmCancel] = React13.useState(false);
|
|
3426
3671
|
const [cancelError, setCancelError] = React13.useState(null);
|
|
3672
|
+
const commandRetrier = React13.useRef(new ApplicationRunCommandRetrier()).current;
|
|
3427
3673
|
const progress = useStepProgress("slot");
|
|
3428
3674
|
async function releaseAndExit() {
|
|
3429
3675
|
if (cancelling) return;
|
|
3430
3676
|
setCancelling(true);
|
|
3431
3677
|
setCancelError(null);
|
|
3432
3678
|
try {
|
|
3433
|
-
await client.
|
|
3679
|
+
if (applicationRunId) await client.closeApplicationRun(applicationRunId);
|
|
3680
|
+
else await client.cancelRequest(requestId ?? "");
|
|
3434
3681
|
onCancelled();
|
|
3435
3682
|
} catch (err) {
|
|
3436
3683
|
setCancelling(false);
|
|
@@ -3456,18 +3703,33 @@ function SlotStep({
|
|
|
3456
3703
|
appointmentTypeId,
|
|
3457
3704
|
mode: "request",
|
|
3458
3705
|
onConfirm: async (slot, tz) => {
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
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
|
+
}
|
|
3471
3733
|
onBooked(formatBookedFor(slot.start, tz));
|
|
3472
3734
|
}
|
|
3473
3735
|
}
|
|
@@ -3479,16 +3741,17 @@ function SlotStep({
|
|
|
3479
3741
|
/* @__PURE__ */ jsxs13("p", { className: "sk-slots__empty-note", children: [
|
|
3480
3742
|
"Can\u2019t find a time that suits?",
|
|
3481
3743
|
" ",
|
|
3482
|
-
/* @__PURE__ */ jsxs13("a", { className: "sk-cf__link", href: `tel:${clinicPhone
|
|
3744
|
+
/* @__PURE__ */ jsxs13("a", { className: "sk-cf__link", href: `tel:${normalizeAuPhone(clinicPhone)}`, children: [
|
|
3483
3745
|
"Call the clinic on ",
|
|
3484
3746
|
clinicPhone
|
|
3485
3747
|
] })
|
|
3486
3748
|
] }),
|
|
3487
|
-
isFree === false && /* @__PURE__ */ jsxs13("p", { className: "sk-slots__empty-note", children: [
|
|
3749
|
+
isFree === false && !applicationRunId && /* @__PURE__ */ jsxs13("p", { className: "sk-slots__empty-note", children: [
|
|
3488
3750
|
"You haven\u2019t been charged \u2014 only a hold was placed on your card.",
|
|
3489
3751
|
" ",
|
|
3490
3752
|
HOLD_RELEASE_NOTE
|
|
3491
|
-
] })
|
|
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." })
|
|
3492
3755
|
] }),
|
|
3493
3756
|
/* @__PURE__ */ jsx15(
|
|
3494
3757
|
SlotCancelControl,
|
|
@@ -3907,7 +4170,7 @@ function BlockedStep({
|
|
|
3907
4170
|
EMERGENCY2
|
|
3908
4171
|
] }) }),
|
|
3909
4172
|
/* @__PURE__ */ jsx18("li", { children: /* @__PURE__ */ jsx18("a", { className: "sk-cf__link", href: "tel:1800022222", children: "Health advice, 24 hours \u2014 healthdirect on 1800 022 222" }) }),
|
|
3910
|
-
clinicPhone && /* @__PURE__ */ jsx18("li", { children: /* @__PURE__ */ jsxs16("a", { className: "sk-cf__link", href: `tel:${clinicPhone
|
|
4173
|
+
clinicPhone && /* @__PURE__ */ jsx18("li", { children: /* @__PURE__ */ jsxs16("a", { className: "sk-cf__link", href: `tel:${normalizeAuPhone(clinicPhone)}`, children: [
|
|
3911
4174
|
"Call the clinic on ",
|
|
3912
4175
|
clinicPhone
|
|
3913
4176
|
] }) }),
|
|
@@ -3945,7 +4208,7 @@ function PaymentFailedStep({
|
|
|
3945
4208
|
clinicPhone && /* @__PURE__ */ jsxs16("p", { className: "sk-cf__body", children: [
|
|
3946
4209
|
"Still not working?",
|
|
3947
4210
|
" ",
|
|
3948
|
-
/* @__PURE__ */ jsxs16("a", { className: "sk-cf__link", href: `tel:${clinicPhone
|
|
4211
|
+
/* @__PURE__ */ jsxs16("a", { className: "sk-cf__link", href: `tel:${normalizeAuPhone(clinicPhone)}`, children: [
|
|
3949
4212
|
"Call the clinic on ",
|
|
3950
4213
|
clinicPhone
|
|
3951
4214
|
] })
|
|
@@ -3987,7 +4250,7 @@ function UnavailableStep({
|
|
|
3987
4250
|
children: "Find a GP near you"
|
|
3988
4251
|
}
|
|
3989
4252
|
) }),
|
|
3990
|
-
clinicPhone && /* @__PURE__ */ jsx18("li", { children: /* @__PURE__ */ jsxs16("a", { className: "sk-cf__link", href: `tel:${clinicPhone
|
|
4253
|
+
clinicPhone && /* @__PURE__ */ jsx18("li", { children: /* @__PURE__ */ jsxs16("a", { className: "sk-cf__link", href: `tel:${normalizeAuPhone(clinicPhone)}`, children: [
|
|
3991
4254
|
"Call the clinic on ",
|
|
3992
4255
|
clinicPhone
|
|
3993
4256
|
] }) }),
|
|
@@ -4034,6 +4297,13 @@ function CertificateFunnelClient(props) {
|
|
|
4034
4297
|
}
|
|
4035
4298
|
return /* @__PURE__ */ jsx19(PortalProvider, { client: portalClient, children: /* @__PURE__ */ jsx19(Funnel, { ...props }) });
|
|
4036
4299
|
}
|
|
4300
|
+
var RESERVATION_REQUIRED_STEPS = /* @__PURE__ */ new Set([
|
|
4301
|
+
"identity",
|
|
4302
|
+
"confirm-identity",
|
|
4303
|
+
"medicare",
|
|
4304
|
+
"consent",
|
|
4305
|
+
"payment"
|
|
4306
|
+
]);
|
|
4037
4307
|
function Funnel({
|
|
4038
4308
|
publicApi,
|
|
4039
4309
|
services = [],
|
|
@@ -4043,6 +4313,7 @@ function Funnel({
|
|
|
4043
4313
|
serviceKey: pinnedService
|
|
4044
4314
|
}) {
|
|
4045
4315
|
const api = publicApi;
|
|
4316
|
+
const portalClient = usePortalClient();
|
|
4046
4317
|
const rawClient = React16.useMemo(
|
|
4047
4318
|
() => patientos({ publishableKey: api.publishableKey, apiBase: api.apiBase }),
|
|
4048
4319
|
[api.publishableKey, api.apiBase]
|
|
@@ -4050,15 +4321,17 @@ function Funnel({
|
|
|
4050
4321
|
const pinned = React16.useMemo(() => ({ pinnedService: !!pinnedService }), [pinnedService]);
|
|
4051
4322
|
const [state, setState] = React16.useState(null);
|
|
4052
4323
|
const [flow, setFlow] = React16.useState(null);
|
|
4324
|
+
const [applicationRun, setApplicationRun] = React16.useState(null);
|
|
4053
4325
|
const [flowChecked, setFlowChecked] = React16.useState(false);
|
|
4054
4326
|
const [capturing, setCapturing] = React16.useState(false);
|
|
4055
4327
|
const [cancellingLostSlot, setCancellingLostSlot] = React16.useState(false);
|
|
4056
4328
|
const [lostSlotCancelError, setLostSlotCancelError] = React16.useState(null);
|
|
4057
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);
|
|
4058
4334
|
const [identityDraft, setIdentityDraft] = React16.useState(null);
|
|
4059
|
-
const [submittedIdentitySnapshot, setSubmittedIdentitySnapshot] = React16.useState(
|
|
4060
|
-
null
|
|
4061
|
-
);
|
|
4062
4335
|
const onDraftChange = React16.useCallback((draft) => setIdentityDraft(draft), []);
|
|
4063
4336
|
const [medicareDraft, setMedicareDraft] = React16.useState(null);
|
|
4064
4337
|
const onMedicareDraftChange = React16.useCallback(
|
|
@@ -4096,6 +4369,7 @@ function Funnel({
|
|
|
4096
4369
|
null
|
|
4097
4370
|
);
|
|
4098
4371
|
const [questionCounts, setQuestionCounts] = React16.useState({});
|
|
4372
|
+
const [enabledServiceKeys, setEnabledServiceKeys] = React16.useState(null);
|
|
4099
4373
|
React16.useEffect(() => {
|
|
4100
4374
|
let alive = true;
|
|
4101
4375
|
void (async () => {
|
|
@@ -4108,7 +4382,10 @@ function Funnel({
|
|
|
4108
4382
|
const nextFees = {};
|
|
4109
4383
|
const nextDeliveryModes = {};
|
|
4110
4384
|
const nextCounts = {};
|
|
4385
|
+
const nextEnabledKeys = /* @__PURE__ */ new Set();
|
|
4111
4386
|
for (const svc of svcs) {
|
|
4387
|
+
if (svc.anonymousApplicationRuns === false) continue;
|
|
4388
|
+
nextEnabledKeys.add(svc.key);
|
|
4112
4389
|
const option = pickDeliveryOption(options, svc.id);
|
|
4113
4390
|
nextFees[svc.key] = effectiveFee(svc, option);
|
|
4114
4391
|
if (option) nextDeliveryModes[svc.key] = option.deliveryMode;
|
|
@@ -4117,10 +4394,12 @@ function Funnel({
|
|
|
4117
4394
|
setFees(nextFees);
|
|
4118
4395
|
setDeliveryModes(nextDeliveryModes);
|
|
4119
4396
|
setQuestionCounts(nextCounts);
|
|
4397
|
+
setEnabledServiceKeys(nextEnabledKeys);
|
|
4120
4398
|
} catch {
|
|
4121
4399
|
if (alive) {
|
|
4122
4400
|
setFees({});
|
|
4123
4401
|
setDeliveryModes({});
|
|
4402
|
+
setEnabledServiceKeys(/* @__PURE__ */ new Set());
|
|
4124
4403
|
}
|
|
4125
4404
|
}
|
|
4126
4405
|
})();
|
|
@@ -4147,66 +4426,221 @@ function Funnel({
|
|
|
4147
4426
|
return () => window.removeEventListener("popstate", onPop);
|
|
4148
4427
|
}, [dispatch, pinnedService, pinned]);
|
|
4149
4428
|
const requestId = state?.requestId ?? null;
|
|
4429
|
+
const applicationRunId = state?.applicationRunId ?? null;
|
|
4430
|
+
const flowId = requestId ?? applicationRunId;
|
|
4150
4431
|
React16.useEffect(() => {
|
|
4151
|
-
if (!
|
|
4432
|
+
if (!flowId) {
|
|
4152
4433
|
setFlow(null);
|
|
4153
4434
|
setFlowChecked(false);
|
|
4435
|
+
setApplicationRun(null);
|
|
4154
4436
|
return;
|
|
4155
4437
|
}
|
|
4156
|
-
if (flow)
|
|
4157
|
-
|
|
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);
|
|
4158
4445
|
return;
|
|
4159
4446
|
}
|
|
4160
|
-
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
|
|
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 {
|
|
4165
4551
|
const svc = await findServiceByKey(client, serviceKey);
|
|
4166
|
-
if (!svc
|
|
4552
|
+
if (!svc || svc.anonymousApplicationRuns === false) {
|
|
4553
|
+
throw new Error("This certificate type is not currently available.");
|
|
4554
|
+
}
|
|
4167
4555
|
const option = await findDeliveryOption(client, svc.id);
|
|
4168
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);
|
|
4169
4575
|
const record = {
|
|
4170
4576
|
serviceId: svc.id,
|
|
4171
4577
|
serviceKey: svc.key,
|
|
4172
4578
|
serviceLabel: svc.label,
|
|
4173
4579
|
deliveryOptionId: option.id,
|
|
4174
4580
|
appointmentTypeId: option.appointmentTypeId ?? "",
|
|
4175
|
-
// The EFFECTIVE fee: the option's override wins over the service base (PAT-710).
|
|
4176
4581
|
feeAmount: option.feeOverride ?? svc.feeAmount,
|
|
4177
|
-
// Whether money is taken AT ALL — a separate fact from the amount, and the one
|
|
4178
|
-
// the consent wording branches on. A null fee alone could not distinguish
|
|
4179
|
-
// "bulk billed" from "not resolved yet" (PAT-853).
|
|
4180
4582
|
requiresPayment: svc.requiresPayment,
|
|
4181
4583
|
deliveryMode: option.deliveryMode,
|
|
4182
|
-
givenName:
|
|
4183
|
-
familyName:
|
|
4184
|
-
dateOfBirth:
|
|
4584
|
+
givenName: "",
|
|
4585
|
+
familyName: "",
|
|
4586
|
+
dateOfBirth: null,
|
|
4185
4587
|
questionSetId: svc.questionSetId
|
|
4186
4588
|
};
|
|
4187
|
-
saveFlowRecord(
|
|
4589
|
+
saveFlowRecord(result.run.id, record);
|
|
4188
4590
|
setFlow(record);
|
|
4189
|
-
|
|
4190
|
-
|
|
4191
|
-
|
|
4192
|
-
|
|
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]);
|
|
4193
4624
|
const restart = React16.useCallback(() => {
|
|
4194
4625
|
setIdentityDraft(null);
|
|
4195
|
-
setSubmittedIdentitySnapshot(null);
|
|
4196
4626
|
setMedicareDraft(null);
|
|
4197
4627
|
dispatch({ type: "RESTART" });
|
|
4198
4628
|
}, [dispatch]);
|
|
4199
4629
|
const retryPayment = React16.useCallback(() => {
|
|
4200
|
-
setSubmittedIdentitySnapshot(null);
|
|
4201
4630
|
dispatch({ type: "RETRY_PAYMENT" });
|
|
4202
4631
|
}, [dispatch]);
|
|
4203
4632
|
const cancelLostSlot = React16.useCallback(async () => {
|
|
4204
4633
|
const id = state?.requestId;
|
|
4205
|
-
|
|
4634
|
+
const runId = state?.applicationRunId;
|
|
4635
|
+
if (!id && !runId || cancellingLostSlot) return;
|
|
4206
4636
|
setCancellingLostSlot(true);
|
|
4207
4637
|
setLostSlotCancelError(null);
|
|
4208
4638
|
try {
|
|
4209
|
-
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
|
+
}
|
|
4210
4644
|
setCancellingLostSlot(false);
|
|
4211
4645
|
} catch (err) {
|
|
4212
4646
|
setCancellingLostSlot(false);
|
|
@@ -4214,7 +4648,7 @@ function Funnel({
|
|
|
4214
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."
|
|
4215
4649
|
);
|
|
4216
4650
|
}
|
|
4217
|
-
}, [cancellingLostSlot, client, restart, state?.requestId]);
|
|
4651
|
+
}, [cancellingLostSlot, client, restart, state?.applicationRunId, state?.requestId]);
|
|
4218
4652
|
const chosenKey = state?.serviceKey ?? null;
|
|
4219
4653
|
const step = state?.step;
|
|
4220
4654
|
const shape = React16.useMemo(() => {
|
|
@@ -4240,16 +4674,32 @@ function Funnel({
|
|
|
4240
4674
|
}, [flow, fees, pinnedService, chosenKey, step, questionCounts]);
|
|
4241
4675
|
if (!state) return /* @__PURE__ */ jsx19(FlowLoading, {});
|
|
4242
4676
|
const title = stepTitle(state.step, clinicName);
|
|
4243
|
-
const
|
|
4677
|
+
const selectableServices = enabledServiceKeys ? services.filter((candidate) => enabledServiceKeys.has(candidate.key)) : [];
|
|
4678
|
+
const service = selectableServices.find((candidate) => candidate.key === state.serviceKey) ?? null;
|
|
4244
4679
|
const serviceLabel = service?.label ?? "certificate";
|
|
4245
|
-
const identitySnapshot = identityDraft ? JSON.stringify(identityDraft) : null;
|
|
4246
|
-
const resumeRequestId = resumableRequestId(
|
|
4247
|
-
state.requestId,
|
|
4248
|
-
submittedIdentitySnapshot,
|
|
4249
|
-
identitySnapshot
|
|
4250
|
-
);
|
|
4251
4680
|
const showBack = canGoBack(state, pinned);
|
|
4252
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
|
+
}
|
|
4253
4703
|
const quietMoney = HOLDLESS_STEPS.has(state.step) || state.step === "consent" || state.step === "payment";
|
|
4254
4704
|
const rail = /* @__PURE__ */ jsx19(
|
|
4255
4705
|
TrustRail,
|
|
@@ -4264,7 +4714,7 @@ function Funnel({
|
|
|
4264
4714
|
return /* @__PURE__ */ jsx19(
|
|
4265
4715
|
ServiceStep,
|
|
4266
4716
|
{
|
|
4267
|
-
services,
|
|
4717
|
+
services: selectableServices,
|
|
4268
4718
|
fees,
|
|
4269
4719
|
deliveryModes,
|
|
4270
4720
|
heading: heading2,
|
|
@@ -4275,69 +4725,111 @@ function Funnel({
|
|
|
4275
4725
|
}
|
|
4276
4726
|
);
|
|
4277
4727
|
case "preflight":
|
|
4278
|
-
return /* @__PURE__ */ jsx19(
|
|
4279
|
-
|
|
4280
|
-
|
|
4281
|
-
|
|
4282
|
-
|
|
4283
|
-
|
|
4284
|
-
|
|
4285
|
-
|
|
4286
|
-
|
|
4287
|
-
|
|
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
|
+
] });
|
|
4288
4766
|
case "identity":
|
|
4289
4767
|
case "confirm-identity":
|
|
4290
|
-
return /* @__PURE__ */ jsxs17(
|
|
4291
|
-
|
|
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
|
-
setSubmittedIdentitySnapshot(JSON.stringify(capturedDraft));
|
|
4324
|
-
dispatch({ type: "REQUEST_STARTED", requestId: newRequestId });
|
|
4325
|
-
void captureFlow(newRequestId, state.serviceKey ?? "", identity).catch(
|
|
4326
|
-
(e) => setResolveError(e instanceof Error ? e.message : "Something went wrong.")
|
|
4327
|
-
).finally(() => setCapturing(false));
|
|
4328
|
-
}
|
|
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);
|
|
4329
4801
|
}
|
|
4330
|
-
|
|
4331
|
-
|
|
4332
|
-
|
|
4333
|
-
|
|
4334
|
-
|
|
4802
|
+
dispatch({ type: "REQUEST_STARTED", requestId: newRequestId });
|
|
4803
|
+
}
|
|
4804
|
+
}
|
|
4805
|
+
),
|
|
4806
|
+
/* @__PURE__ */ jsx19(FlowError, { children: resolveError })
|
|
4807
|
+
] });
|
|
4335
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
|
+
}
|
|
4336
4826
|
return /* @__PURE__ */ jsx19(
|
|
4337
4827
|
IntakeStep,
|
|
4338
4828
|
{
|
|
4339
4829
|
client,
|
|
4340
|
-
|
|
4830
|
+
applicationRunId: state.applicationRunId ?? void 0,
|
|
4831
|
+
applicationRunRevision: applicationRun?.revision ?? 0,
|
|
4832
|
+
onApplicationRunChange: setApplicationRun,
|
|
4341
4833
|
questionSetId: flow ? flow.questionSetId ?? null : void 0,
|
|
4342
4834
|
resolveError,
|
|
4343
4835
|
documentTitle: title,
|
|
@@ -4388,7 +4880,22 @@ function Funnel({
|
|
|
4388
4880
|
phone: identityDraft?.identity.phone ?? "",
|
|
4389
4881
|
serviceLabel: service?.label ?? "",
|
|
4390
4882
|
documentTitle: title,
|
|
4391
|
-
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
|
+
},
|
|
4392
4899
|
onGate: (gate) => dispatch({ type: "GATE", gate }),
|
|
4393
4900
|
onBack: showBack ? back2 : void 0
|
|
4394
4901
|
}
|
|
@@ -4411,7 +4918,22 @@ function Funnel({
|
|
|
4411
4918
|
feeAmount: flow?.feeAmount ?? null,
|
|
4412
4919
|
serviceLabel,
|
|
4413
4920
|
clinicPhone,
|
|
4414
|
-
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
|
+
}
|
|
4415
4937
|
}
|
|
4416
4938
|
)
|
|
4417
4939
|
}
|
|
@@ -4421,20 +4943,24 @@ function Funnel({
|
|
|
4421
4943
|
SlotStep,
|
|
4422
4944
|
{
|
|
4423
4945
|
client,
|
|
4424
|
-
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),
|
|
4425
4951
|
appointmentTypeId: flow.appointmentTypeId,
|
|
4426
4952
|
isFree: flowIsFree(flow),
|
|
4427
4953
|
clinicPhone,
|
|
4428
4954
|
onCancelled: restart,
|
|
4429
4955
|
documentTitle: title,
|
|
4430
4956
|
onBooked: (bookedFor) => {
|
|
4431
|
-
const id = state.requestId;
|
|
4957
|
+
const id = state.requestId ?? state.applicationRunId;
|
|
4432
4958
|
if (id && flow) {
|
|
4433
4959
|
const next = { ...flow, bookedFor };
|
|
4434
4960
|
saveFlowRecord(id, next);
|
|
4435
4961
|
setFlow(next);
|
|
4436
4962
|
}
|
|
4437
|
-
dispatch({ type: "BOOKED" });
|
|
4963
|
+
dispatch({ type: state.requestId ? "BOOKED" : "RESERVED" });
|
|
4438
4964
|
}
|
|
4439
4965
|
}
|
|
4440
4966
|
) : flowLost ? (
|
|
@@ -4462,7 +4988,7 @@ function Funnel({
|
|
|
4462
4988
|
clinicPhone && /* @__PURE__ */ jsxs17("p", { className: "sk-slots__empty-note", children: [
|
|
4463
4989
|
"Can\u2019t find a time that suits?",
|
|
4464
4990
|
" ",
|
|
4465
|
-
/* @__PURE__ */ jsxs17("a", { className: "sk-cf__link", href: `tel:${clinicPhone
|
|
4991
|
+
/* @__PURE__ */ jsxs17("a", { className: "sk-cf__link", href: `tel:${normalizeAuPhone(clinicPhone)}`, children: [
|
|
4466
4992
|
"Call the clinic on ",
|
|
4467
4993
|
clinicPhone
|
|
4468
4994
|
] })
|
|
@@ -4556,14 +5082,14 @@ function SessionExpiredState({
|
|
|
4556
5082
|
stepKey: "session-expired",
|
|
4557
5083
|
heading: "Your secure session ended",
|
|
4558
5084
|
documentTitle,
|
|
4559
|
-
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" }),
|
|
4560
5086
|
children: [
|
|
4561
5087
|
/* @__PURE__ */ jsxs17("p", { className: "sk-cf__body", children: [
|
|
4562
5088
|
"This secure session is no longer available.",
|
|
4563
5089
|
" ",
|
|
4564
5090
|
requestStarted ? "Your request is still with the clinic." : "You can start again securely."
|
|
4565
5091
|
] }),
|
|
4566
|
-
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." })
|
|
4567
5093
|
]
|
|
4568
5094
|
}
|
|
4569
5095
|
);
|
|
@@ -10953,14 +11479,10 @@ function PortalAccountSurfaceClient({
|
|
|
10953
11479
|
portalUrl,
|
|
10954
11480
|
bookHref,
|
|
10955
11481
|
initialState,
|
|
10956
|
-
fetchImpl: rawFetchImpl,
|
|
10957
11482
|
openUrl
|
|
10958
11483
|
}) {
|
|
10959
11484
|
const portalClient = usePortalClient();
|
|
10960
|
-
const fetchImpl = React21.useMemo(
|
|
10961
|
-
() => portalFetchForClient(portalClient, rawFetchImpl),
|
|
10962
|
-
[portalClient, rawFetchImpl]
|
|
10963
|
-
);
|
|
11485
|
+
const fetchImpl = React21.useMemo(() => portalFetchForClient(portalClient), [portalClient]);
|
|
10964
11486
|
const [state, setState] = React21.useState(
|
|
10965
11487
|
// `readPortalSignedInHint` is in the initialiser (not an effect) on purpose: the
|
|
10966
11488
|
// whole point is that the FIRST paint already knows, so there is no placeholder
|