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

59.45% Statements 22/37
27.77% Branches 5/18
72.72% Functions 8/11
68.96% Lines 20/29

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                                          9x   9x         9x                     1x 1x 1x                                                         588x 180x     588x   181x 181x 1x 1x   1260x 180x     180x   180x 180x 180x       588x    
"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 {
  if (typeof sessionStorage === "undefined") return;
  sessionStorage.setItem(DEMO_PERSONA_KEY, username);
  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);
  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;
}