All files / src/utils appointmentLoginStorage.ts

95.23% Statements 40/42
94.87% Branches 37/39
100% Functions 9/9
95.23% Lines 40/42

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 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143                2x           6x       45x   45x 45x 45x 45x       1x   42x   2x                       4x 2x     2x 2x 2x       4x             102x 4x         102x 1x                   94x 27x     67x     67x 66x     1x 1x       6x 6x                   6x   6x 6x                 6x                               95x 95x 90x   5x 5x 1x   4x    
import { AppointmentHash } from "@/types/AppointmentHashTypes";
import { LocalStorageUiData } from "@/types/LocalStorageAppointmentData";
import {
  LOCALSTORAGE_PARAM_APPOINTMENT_DATA,
  SESSIONSTORAGE_PARAM_APPOINTMENT_AUTH_HASH,
} from "@/utils/Constants";
 
/** UI localStorage is only valid for login resume within this window. */
export const LOCALSTORAGE_UI_TTL_MS = 30 * 60 * 1000;
 
export function encodeAppointmentAuthHash(
  processId: string,
  authKey: string
): string {
  return btoa(JSON.stringify({ id: processId, authKey }));
}
 
export function parseAppointmentHash(hash: string): AppointmentHash | null {
  try {
    // Add missing base64 padding if needed (padding may be stripped from URL)
    const padding = (4 - (hash.length % 4)) % 4;
    const paddedHash = padding > 0 ? hash + "=".repeat(padding) : hash;
    const appointmentData = JSON.parse(window.atob(paddedHash));
    if (
      appointmentData.id == undefined ||
      appointmentData.authKey == undefined
    ) {
      return null;
    }
    return appointmentData;
  } catch {
    return null;
  }
}
 
/**
 * Store credentials for the OAuth hop: sessionStorage survives IdP redirect
 * (URL fragment does not); replaceState avoids firing hashchange before leave.
 */
export function setAppointmentAuthHashForLogin(
  processId: string | undefined,
  authKey: string | undefined
): void {
  if (!processId || !authKey) {
    return;
  }
 
  const encoded = encodeAppointmentAuthHash(processId, authKey);
  sessionStorage.setItem(SESSIONSTORAGE_PARAM_APPOINTMENT_AUTH_HASH, encoded);
  history.replaceState(null, "", `#/appointment/${encoded}`);
}
 
export function saveUiToLocalStorage(uiData: LocalStorageUiData): void {
  localStorage.setItem(
    LOCALSTORAGE_PARAM_APPOINTMENT_DATA,
    JSON.stringify(uiData)
  );
}
 
export function clearAppointmentLocalStorage(): void {
  if (localStorage.getItem(LOCALSTORAGE_PARAM_APPOINTMENT_DATA)) {
    localStorage.removeItem(LOCALSTORAGE_PARAM_APPOINTMENT_DATA);
  }
}
 
export function clearAppointmentAuthHashSession(): void {
  if (sessionStorage.getItem(SESSIONSTORAGE_PARAM_APPOINTMENT_AUTH_HASH)) {
    sessionStorage.removeItem(SESSIONSTORAGE_PARAM_APPOINTMENT_AUTH_HASH);
  }
}
 
/**
 * Prefer hash from the URL/prop; else restore from the short-lived session bridge.
 */
export function resolveAppointmentAuthHash(
  appointmentHashFromProps?: string | null
): string | undefined {
  if (appointmentHashFromProps) {
    return appointmentHashFromProps;
  }
 
  const pendingHash = sessionStorage.getItem(
    SESSIONSTORAGE_PARAM_APPOINTMENT_AUTH_HASH
  );
  if (!pendingHash) {
    return undefined;
  }
 
  history.replaceState(null, "", `#/appointment/${pendingHash}`);
  return pendingHash;
}
 
export function parseUiLocalStorage(data: string): LocalStorageUiData | null {
  try {
    const raw = JSON.parse(data) as {
      timestamp?: number;
      currentView?: number;
      selectedServiceId?: string;
      selectedProviderId?: string;
      selectedServiceMap?: Record<string, number>;
      selectedTimeslot?: number;
      selectedService?: { id?: string };
      selectedProvider?: { id?: string };
    };
    const selectedServiceId = raw.selectedServiceId ?? raw.selectedService?.id;
    const selectedProviderId =
      raw.selectedProviderId ?? raw.selectedProvider?.id;
    Iif (
      raw.timestamp == undefined ||
      raw.currentView == undefined ||
      selectedServiceId == undefined ||
      selectedProviderId == undefined
    ) {
      return null;
    }
    // Persist IDs only — never restore legacy full objects / credentials / PII.
    return {
      timestamp: raw.timestamp,
      currentView: raw.currentView,
      selectedServiceId: String(selectedServiceId),
      selectedServiceMap: raw.selectedServiceMap ?? {},
      selectedProviderId: String(selectedProviderId),
      selectedTimeslot: raw.selectedTimeslot ?? 0,
    };
  } catch {
    return null;
  }
}
 
export function getFreshLocalStorageUiData(
  nowMs: number = Date.now()
): LocalStorageUiData | null {
  const raw = localStorage.getItem(LOCALSTORAGE_PARAM_APPOINTMENT_DATA);
  if (!raw) {
    return null;
  }
  const parsed = parseUiLocalStorage(raw);
  if (!parsed || nowMs - parsed.timestamp >= LOCALSTORAGE_UI_TTL_MS) {
    return null;
  }
  return parsed;
}