All files / 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       29x   29x 29x 29x 29x       1x   26x   2x                       4x 2x     2x 2x 2x       4x             79x 3x         79x 1x                   71x 11x     60x     60x 59x     1x 1x       5x 5x                   5x   5x 5x                 5x                               72x 72x 68x   4x 4x 1x   3x    
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;
}