import { useEffect, useState, type ReactNode } from 'react'; import { useTenant } from '../../services/TenantService.tsx'; import { useAuthServiceContext } from '../../services/AuthService.tsx'; import { useOrganizationOwnerService } from '../../services/OrganizationOwnerService.tsx'; import { readInvitationToken } from '../ClinicianInvitationNotice/invitationToken'; import OrganizationOwnerNotice from '../OrganizationOwnerNotice/OrganizationOwnerNotice'; import './TenantGate.css'; // What the owner read said for a member the gateway refused here. `claim`: nobody owns the practice. `owned`: // somebody does, so the way back is their re-admission. `refused`: the read was refused too; the screens below stand. // `held`: no answer (a failure, or no boolean `needsOwner`), which says nothing about the owner, so no claim. type OwnerRecovery = 'checking' | 'claim' | 'owned' | 'refused' | 'held'; // A member whose roles here are all unproved is refused /tenant, yet the authorizer admits them to exactly the // owner read, claim and confirm. So once the gateway has refused, ask the owner read. const useOwnerRecovery = (organizationId: string | undefined) => { const { fetchOwner } = useOrganizationOwnerService(); const [answer, setAnswer] = useState<{ organizationId: string; attempt: number; recovery: OwnerRecovery } | undefined>(undefined); const [attempt, setAttempt] = useState(0); useEffect(() => { if (!organizationId) return; let live = true; const settle = (recovery: OwnerRecovery) => { if (live) setAnswer({ organizationId, attempt, recovery }); }; fetchOwner(organizationId) .then((state) => { const needsOwner = (state as { needsOwner?: unknown } | undefined)?.needsOwner; settle(needsOwner === true ? 'claim' : needsOwner === false ? 'owned' : 'held'); }) .catch((error: unknown) => settle((error as { status?: unknown } | null)?.status === 403 ? 'refused' : 'held')); return () => { live = false; }; }, [organizationId, attempt, fetchOwner]); const recovery: OwnerRecovery = !organizationId ? 'refused' : answer?.organizationId === organizationId && answer.attempt === attempt ? answer.recovery : 'checking'; return { recovery, askAgain: () => setAttempt((n) => n + 1) }; }; // admin-v2 "If Akashic cannot confirm the mode": a signed-in workspace is shown only once the API has // said live or sandbox. Neither claim is made while checking or after a failed read; the server's own // authorization is unchanged - this only withholds the page. export const TenantGate = ({ children }: { children: ReactNode }) => { const { kind, settled, retry, noAccess, accessEnded } = useTenant(); const { logout, wpOrganization } = useAuthServiceContext(); const organizationId = noAccess ? wpOrganization?.current?.id : undefined; const { recovery, askAgain } = useOwnerRecovery(organizationId); if (kind !== 'unknown') return <>{children}; if (settled === false) { return (

Checking which mode this is…

); } // The gateway refused this identity here (NP-5 ruling). Usually the practice no longer lists them, but the same // deny covers a token or DPoP glitch, so it says "right now" and points at signing in again (Captain 08:14). // The same layout as the mode refusal; Sign out only, since asking again with this session is refused again. // An emailed invitation opened while signed in is remembered for the dashboard, which this screen // hides. This session holds no role here and cannot answer it; the link opened signed out can. if (noAccess && readInvitationToken()) { return (

You have an invitation to this practice

To answer it, sign out, then open the invitation link from your email again.

); } if (noAccess && recovery === 'checking') { return (

Checking your access to this practice…

); } // Nobody owns the practice: the owner notice offers this member the claim (a code to the practice's own // recorded address, then the attestation). It is the same notice the dashboard shows, read for this practice. if (noAccess && recovery === 'claim' && organizationId) { return (
{/* The server admits the new owner with the confirm; the gate's /tenant answer predates it. */}
); } if (noAccess && recovery === 'held') { return (

We could not check whether this practice needs an owner

Your sign-in doesn't include a current staff role here. Try again; if it keeps failing, ask the practice's administrator.

); } // The practice has an owner: the way back is their re-admission, never a claim. const readmission = noAccess && recovery === 'owned' ?

This practice has an owner. Ask them to add you back to the staff.

: null; // The token names no current role here: ended, or not started yet. Nothing here tells which, so the // wording claims neither, and does not offer another sign-in, which would land here again. if (noAccess && accessEnded) { return (

You don't have a current role at this practice

Your sign-in doesn't include a current staff role here. If you think you should have access, ask the practice's administrator.

{readmission}
); } if (noAccess) { return (

You don't have access to this practice right now

Sign out and sign in again; if it continues, ask the practice's administrator.

{readmission}
); } return (

We could not check which mode this is

We cannot safely show this workspace until the connection is checked. Try again. If it keeps failing, sign out and contact support.

); }; export default TenantGate;