import { useEffect, useRef, useState, type ReactNode } from 'react'; import { useLocation } from 'react-router-dom'; import DemoEntranceBanner from './DemoEntranceBanner'; import { DEMO_ROLES, demoAppOrigin, demoEntranceUrl, demoRoleFrom, type DemoRole } from '../../utils/demoEntrance'; import { AgreementBlocks } from '../../components/FirstSignInGate/AgreementBlocks'; import { hostedMountContext } from '../../utils/hostedMount'; import { DEMO_AGREEMENT_READ_MS, DEMO_ENTRY_BUDGET_MS, readDemoAgreement, requestDemoLease, requestDemoRole, signInDemo, useDemoTurnstile, type DemoAgreementCopy, } from '../../services/demoEntry'; import { startDemoOperation, type DemoOperation } from '../../services/demoAllowance'; import { newTabId, readDemoSession } from '../../services/demoTab'; import { DEMO_EXIT_EVENT } from '../../services/demoExit'; import DemoExitLink from './DemoExitLink'; type EntryState = | { step: 'agreement' } | { step: 'entering' } | { step: 'full'; minutes: number } | { step: 'unavailable' } | { step: 'agreement-changed' } | { step: 'failed' } | { step: 'copy' } | { step: 'ended' }; // What one opening of this page has been told. Each opening - each change of origin, mount or role, even back // to an earlier one - is a new lifetime and starts fresh. type SwitchState = 'idle' | 'switching' | 'failed' | 'ended' | 'unavailable' | 'copy'; // The published agreement as this opening has read it: only what is shown here may be accepted. type CopyState = { status: 'loading' } | { status: 'ready'; copy: DemoAgreementCopy } | { status: 'none' } | { status: 'failed' }; type View = { lifetime: number; agreed: boolean; state: EntryState; newDemo: boolean; switching: SwitchState; copy: CopyState }; const freshView = (lifetime: number): View => ({ lifetime, agreed: false, state: { step: 'agreement' }, newDemo: false, switching: 'idle', copy: { status: 'loading' }, }); const mountPath = (role: DemoRole) => (DEMO_ROLES[role].mount === 'staff' ? '/staff-portal/' : '/'); const roleName = (role: DemoRole) => DEMO_ROLES[role].label.toLowerCase(); const Page = ({ children }: { children: ReactNode }) => (
{children}

); /** * The demo-only agreement, asked before anything is leased. The box starts unticked and Enter stays * disabled until it is ticked; leaving asks for nothing. A full pool or a failed entry waits for the * visitor to try again; nothing here retries on its own. */ // The physical mount is passed in so a test can state it; on a page it is read from the path. A role is // accepted only on its own mount, so a staff role never enters through the patient portal or the reverse. // The page also checks its own origin, so rendering it anywhere but the configured demo origin asks for nothing. const DemoEntryPage = ({ mount = hostedMountContext(), origin = window.location.origin, appOrigin = demoAppOrigin(), }: { mount?: 'frontend' | 'staff'; origin?: string; appOrigin?: string | null }) => { const { search } = useLocation(); const approved = !!appOrigin && origin === appOrigin; const named = demoRoleFrom(search); const role = approved && named && DEMO_ROLES[named].mount === mount ? named : undefined; const leaveTo = demoEntranceUrl(); const opening = role ? origin + '|' + mount + '|' + role : ''; const turnstile = useDemoTurnstile(opening); const unchallenged = !!turnstile.required && !turnstile.token; // An entry belongs to the opening it was started in. A change of role or mount - even back to the same // one - retires it in the render that sees the change, and so does leaving the page. const owner = useRef({ opening, number: 0 }); if (owner.current.opening !== opening) owner.current = { opening, number: owner.current.number + 1 }; const lifetime = owner.current.number; const mounted = useRef(true); // Operations still running, by the lifetime that started them; a lifetime that ends cancels its own. const running = useRef(new Map()); const cancelWhere = (ended: (started: number) => boolean) => { for (const [operation, started] of [...running.current]) { if (ended(started)) { running.current.delete(operation); operation.cancel(); } } }; useEffect(() => { mounted.current = true; return () => { mounted.current = false; cancelWhere(() => true); }; }, []); useEffect(() => () => cancelWhere((started) => started === lifetime), [lifetime]); const [stored, setView] = useState(() => freshView(lifetime)); const view = stored.lifetime === lifetime ? stored : freshView(lifetime); const update = (forLifetime: number, change: Partial) => setView((prev) => ({ ...(prev.lifetime === forLifetime ? prev : freshView(forLifetime)), ...change })); const held = approved ? readDemoSession() : undefined; // An open visit keeps the version its visitor accepted; only a new entry needs the one published now. const heldLive = !!held && Date.parse(held.expiresAt) > Date.now(); // One read of the agreement at a time per opening, settled within its own allowance: a later read, another // opening or leaving drops an earlier answer, a read past its allowance fails and can be asked again, and // different copy clears the tick, so only what is on screen can be accepted. const copyRead = useRef(null); const readCopy = (mine: number) => { copyRead.current?.cancel(); const holder: { operation?: DemoOperation } = {}; const current = () => mounted.current && owner.current.number === mine && copyRead.current === holder.operation; const operation = startDemoOperation(newTabId(), DEMO_AGREEMENT_READ_MS, current); holder.operation = operation; copyRead.current = operation; update(mine, { copy: { status: 'loading' }, agreed: false }); readDemoAgreement(operation) .then((answer) => { if (!current()) return; update(mine, { copy: answer.kind === 'ready' ? { status: 'ready', copy: answer.copy } : { status: 'none' }, agreed: false }); }) .catch(() => { if (current()) update(mine, { copy: { status: 'failed' }, agreed: false }); }); }; useEffect(() => { if (!approved || !role) return undefined; readCopy(lifetime); return () => { copyRead.current?.cancel(); copyRead.current = null; }; }, [lifetime, approved, role]); // One operation per press, owned by this lifetime and settled within its allowance whatever its // dependencies do. Only the owner shows what became of it. // Within an opening, one consequential action owns the tab at a time: starting an entry or a switch, or // choosing to replace the held visit, retires whatever was running before it synchronously. const action = useRef(0); const retireActions = () => { action.current += 1; cancelWhere(() => true); }; useEffect(() => { const leave = () => { mounted.current = false; retireActions(); copyRead.current?.cancel(); }; window.addEventListener(DEMO_EXIT_EVENT, leave); return () => window.removeEventListener(DEMO_EXIT_EVENT, leave); }, []); const begin = () => { retireActions(); const mine = lifetime; const act = action.current; const owned = () => mounted.current && owner.current.number === mine && action.current === act; const operation = startDemoOperation(newTabId(), DEMO_ENTRY_BUDGET_MS, owned); running.current.set(operation, mine); const done = () => { running.current.delete(operation); operation.cancel(); }; return { operation, owned, mine, done }; }; const switchRole = async () => { if (!role || !held || view.switching === 'switching') return; const { operation, owned, mine, done } = begin(); update(mine, { switching: 'switching' }); try { const answer = await requestDemoRole({ leaseId: held.leaseId, leaseSecret: held.leaseSecret }, role, operation); operation.admit(); if (answer.kind !== 'leased') { update(mine, { switching: answer.kind }); return; } const outcome = await signInDemo( { ...answer, agreementVersion: held.agreementVersion }, role, operation, undefined, { generation: held.generation, clinicId: held.clinicId }, ); if (outcome === 'copy') update(mine, { switching: 'copy' }); if (outcome === 'ended') update(mine, { switching: 'ended' }); } catch { // Out of time or uncertain: the held visit is untouched, so Try again asks once more. if (owned()) update(mine, { switching: 'failed' }); } finally { done(); } }; const enter = async () => { if (!role || !view.agreed || view.state.step === 'entering' || view.copy.status !== 'ready') return; const version = view.copy.copy.version; // This attempt alone spends the pass; the next one waits for a fresh challenge. const turnstileToken = turnstile.take(); if (turnstile.required && !turnstileToken) return; const { operation, owned, mine, done } = begin(); const replaces = view.newDemo && held ? { leaseId: held.leaseId, leaseSecret: held.leaseSecret } : undefined; update(mine, { state: { step: 'entering' } }); try { const answer = await requestDemoLease({ role, turnstileToken, agreement: { accepted: true, version }, ...(replaces && { replaces }), }, operation); operation.admit(); if (answer.kind === 'full') { update(mine, { state: { step: 'full', minutes: answer.estimatedFreeInMinutes } }); return; } if (answer.kind === 'agreement-changed') { // Nothing was leased; the current version is read and shown, to be read and agreed afresh. update(mine, { state: { step: 'agreement-changed' } }); readCopy(mine); return; } if (answer.kind === 'unavailable') { update(mine, { state: { step: answer.kind } }); return; } const outcome = await signInDemo(answer, role, operation); if (outcome !== 'committed') update(mine, { state: { step: outcome } }); } catch { // Out of time or uncertain: the entry record keeps this lease, so Try again recovers it. if (owned()) update(mine, { state: { step: 'failed' } }); } finally { done(); } }; if (!approved) { return (

The demo is not open here

This address does not offer the demo.

Go to the demo entrance
); } if (!role) { return (

Choose a role to try

This demo link does not name a role.

Choose a role
); } if (heldLive && held.role === role) { return (

Your demo visit is open

{'You are already in the demo as ' + roleName(role) + ' in this tab.'}

Continue your demo
); } // Another role switches inside the clinic this tab holds, never by leasing another one. A new demo is a // separate, explicit choice. if (heldLive && !view.newDemo) { const { switching } = view; return (

{'You are in the demo as ' + roleName(held.role)}

{'You can switch to ' + roleName(role) + ' in the same demo clinic.'}

{switching === 'failed' &&

We could not switch roles. Try again.

} {switching === 'ended' &&

Your demo visit has ended. Start a new demo to continue.

} {switching === 'unavailable' &&

This role is not open in the demo right now.

} {switching === 'copy' &&

This demo visit is open in another tab.

}
{switching !== 'ended' && switching !== 'unavailable' && switching !== 'copy' && ( )} {switching !== 'ended' && ( {'Continue as ' + roleName(held.role)} )}
); } const { state, agreed, copy } = view; return (

{'Try Akashic as ' + roleName(role)}

You are about to enter a fictional demo clinic. Before you go in, please read and agree to this.

{view.newDemo &&

Starting a new demo closes the demo clinic you are in now.

}
{copy.status === 'loading' &&

Loading the demo agreement…

} {copy.status === 'none' && (

The demo agreement is not available right now, so the demo cannot be entered. Please come back soon.

)} {copy.status === 'failed' && (

We could not load the demo agreement.

)} {copy.status === 'ready' && ( <>
{!copy.copy.blocks?.some((block) => block.type === 'title') &&

{copy.copy.title}

} {copy.copy.blocks ? : copy.copy.body.split(/\n{2,}/).map((paragraph, i) =>

{paragraph}

)}

{'Version ' + copy.copy.version}

)} {state.step === 'full' && (

{'Every demo clinic is in use right now. One is expected to be free in about ' + state.minutes + ' minutes.'}

)} {state.step === 'unavailable' && (

The demo clinics are not open yet. Please come back soon.

)} {state.step === 'agreement-changed' && (

The demo agreement has changed. Read the current version above and agree to it to enter.

)} {state.step === 'failed' && (

We could not open your demo clinic. Try again.

)} {state.step === 'ended' && (

That demo visit ended before it opened. Enter again to start a new one.

)} {state.step === 'copy' && (

This demo visit is being opened in another tab. Enter again to start a new one here.

)} {/* The challenge; a pass is spent by each attempt and asked for again before the next. */} {turnstile.required &&
} {turnstile.required && turnstile.status === 'failed' && (

The security check did not load, so the demo cannot open yet.

)}
Leave {state.step === 'full' || state.step === 'failed' ? ( ) : ( )}
); }; export default DemoEntryPage;