All files / src/lib use-demo-persona.ts

70.73% Statements 29/41
33.33% Branches 6/18
81.81% Functions 9/11
81.81% Lines 27/33

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111                                          12x   12x         12x       2x 2x     2x 2x       2x         2x 2x       2x 2x       2x                                                         589x 181x     589x   182x 182x 1x 1x   1267x 181x     181x   181x 181x 181x       589x    
"use client";
 
/**
 * Demo persona store for static GitHub Pages export.
 *
 * In a static build (NEXT_PUBLIC_STATIC_EXPORT=true) there is no server
 * session, so useSession() always returns unauthenticated.  This module
 * provides a sessionStorage-backed persona that Navigation and UserMenu
 * read to simulate the correct role-filtered view for each demo user.
 *
 * sessionStorage is intentionally tab-scoped: switching persona in one tab
 * does NOT affect other open tabs (use localStorage if you want shared state).
 *
 * Usage:
 *   setDemoPersona("patient1")   — call from the /demo hub page
 *   const persona = useDemoPersona()  — read in Navigation / UserMenu
 */
 
import { useState, useEffect } from "react";
import { DEMO_PERSONAS } from "@/lib/auth";
 
export const DEMO_PERSONA_KEY = "demo-persona";
/** Sentinel value indicating user explicitly signed out. */
export const SIGNED_OUT = "__signed_out__";
 
// Module-level EventTarget so setDemoPersona() triggers re-renders on the
// same tab (window "storage" event only fires in *other* tabs).
const emitter: EventTarget | null =
  typeof window !== "undefined" ? new EventTarget() : null;
 
/** Write the active demo persona to sessionStorage and notify same-tab hooks. */
export function setDemoPersona(username: string): void {
  Iif (typeof sessionStorage === "undefined") return;
  sessionStorage.setItem(DEMO_PERSONA_KEY, username);
  // Mirror to localStorage: lib/api.ts reads localStorage to resolve the
  // restricted patient mock, so the two stores must stay in sync.
  try {
    localStorage.setItem(DEMO_PERSONA_KEY, username);
  } catch {
    /* storage unavailable — non-fatal */
  }
  emitter?.dispatchEvent(new Event("change"));
}
 
/** Mark the user as signed out in static demo mode. */
export function clearDemoPersona(): void {
  Iif (typeof sessionStorage === "undefined") return;
  sessionStorage.setItem(DEMO_PERSONA_KEY, SIGNED_OUT);
  // Clear localStorage too so lib/api.ts no longer resolves the patient's own
  // record after sign-out (otherwise /patient keeps showing the signed-out
  // patient's data).
  try {
    localStorage.removeItem(DEMO_PERSONA_KEY);
  } catch {
    /* storage unavailable — non-fatal */
  }
  emitter?.dispatchEvent(new Event("change"));
}
 
/** Read the active demo persona username from sessionStorage (sync, no hooks). */
export function getDemoPersonaUsername(): string {
  if (typeof sessionStorage === "undefined") return "edcadmin";
  const stored = sessionStorage.getItem(DEMO_PERSONA_KEY);
  if (!stored || stored === SIGNED_OUT) return "edcadmin";
  return stored;
}
 
/** Check if user is signed out (static mode only). */
export function isDemoSignedOut(): boolean {
  if (typeof sessionStorage === "undefined") return false;
  return sessionStorage.getItem(DEMO_PERSONA_KEY) === SIGNED_OUT;
}
 
export type DemoPersona = (typeof DEMO_PERSONAS)[number];
 
/**
 * React hook — returns the active demo persona object, or null if signed out.
 * Initialises with the edcadmin fallback (matches legacy DEMO_SESSION),
 * then updates synchronously once the component mounts and sessionStorage
 * can be read.
 *
 * Must be called unconditionally (React Rules of Hooks).
 * In live (non-static) mode its return value should simply be ignored.
 */
export function useDemoPersona(): DemoPersona | null {
  const [persona, setPersona] = useState<DemoPersona | null>(
    () => DEMO_PERSONAS.find((p) => p.username === "edcadmin")!,
  );
 
  useEffect(() => {
    function read() {
      const stored = sessionStorage.getItem(DEMO_PERSONA_KEY);
      if (stored === SIGNED_OUT) {
        setPersona(null);
        return;
      }
      const found = DEMO_PERSONAS.find((p) => p.username === stored);
      Iif (found) setPersona(found);
    }
 
    read(); // synchronous first read after hydration
    // emitter handles same-tab reactivity; no cross-tab sync by design
    emitter?.addEventListener("change", read);
    return () => {
      emitter?.removeEventListener("change", read);
    };
  }, []);
 
  return persona;
}