);
/**
* Every page on the demo origin. The DEMO MODE strip sits above everything, through checking, the app itself
* and an ended visit, and has no close. The app mounts only once this tab holds the visit exclusively and the
* server has confirmed its capability, generation, role and clinic are still live.
*/
const DemoVisit = ({
children,
session = demoTabAtLoad,
store = typeof window === 'undefined' ? undefined : window.sessionStorage,
claim = claimDemoTab,
confirm = confirmDemoStanding,
now = Date.now,
}: {
children: ReactNode;
session?: DemoTabSession;
store?: Storage;
claim?: (leaseId: string) => Promise<'held' | 'copy'>;
confirm?: (session: DemoTabSession, store: Storage, operation: DemoOperation) => Promise;
now?: () => number;
}) => {
const prefix = environment.localStoragePrefix;
const [state, setState] = useState(session && store ? 'checking' : 'none');
const [attempt, setAttempt] = useState(0);
const exiting = useRef(false);
const checking = useRef(null);
useEffect(() => {
const leave = () => {
exiting.current = true;
checking.current?.cancel();
setState('left');
};
window.addEventListener(DEMO_EXIT_EVENT, leave);
return () => window.removeEventListener(DEMO_EXIT_EVENT, leave);
}, []);
useEffect(() => {
if (!session || !store || state !== 'checking') return;
// One allowance from the start of the check, settled whatever the lock, key store or server do.
let live = true;
const operation = startDemoOperation(session.tabId, DEMO_STANDING_BUDGET_MS, () => live && !exiting.current);
checking.current = operation;
void (async () => {
let next: VisitState;
try {
const holder = await operation.settle(claim(session.leaseId));
if (holder === 'copy') {
next = 'copy';
} else {
const standing = await operation.settle(confirm(session, store, operation));
// Admitted only by a current answer while the visit is still within its expiry.
operation.admit();
next = standing === 'active'
? (Date.parse(session.expiresAt) > now() ? 'admitted' : 'ended')
: standing === 'unavailable' ? 'unavailable' : standing === 'mismatch' ? 'left' : 'ended';
}
} catch {
next = 'unavailable';
}
if (live && !exiting.current) setState(next);
})();
return () => {
live = false;
operation.cancel();
};
}, [session, store, state, attempt, claim, confirm, now]);
// A refused tab drops its own copy of the visit and lets go of it; nothing else is touched.
useEffect(() => {
if (exiting.current || !store || (state !== 'none' && state !== 'copy' && state !== 'ended' && state !== 'left')) return;
clearDemoTab(prefix, store);
// Only the tab that held the visit takes its key; a copy shares the record with the tab that holds it.
if (session && (state === 'ended' || state === 'left')) void discardDemoKey(session.tabId).catch(() => undefined);
if (session && state !== 'copy') releaseDemoTab(session.leaseId);
}, [state, store, prefix, session]);
// Admission is checked again where the app would render: a visit that expired before this render shows as
// ended and never mounts the app, however late the admitted update was rendered.
const expiredAtRender = state === 'admitted' && (!session || !(Date.parse(session.expiresAt) > now()));
const shown: VisitState = expiredAtRender ? 'ended' : state;
useEffect(() => {
if (expiredAtRender) setState('ended');
}, [expiredAtRender]);
useEffect(() => {
if (!session || state !== 'admitted') return;
// Signing out ends the visit in this tab only; the clinic itself is not closed by it, and neither is it
// by tokens that no longer match the visit.
const end = (event?: Event) => setState((event as CustomEvent | undefined)?.detail === 'SIGNED_OUT' ? 'left' : 'ended');
const timer = window.setTimeout(() => end(), Math.max(0, Date.parse(session.expiresAt) - Date.now()));
window.addEventListener(DEMO_ENDED_EVENT, end);
return () => {
window.clearTimeout(timer);
window.removeEventListener(DEMO_ENDED_EVENT, end);
};
}, [session, state]);
return (
<>
{shown === 'admitted' && session && }
{shown === 'admitted' && session && {children}}
{shown === 'checking' &&