All files / spotlight-tour/src persistence.ts

86.36% Statements 19/22
80% Branches 12/15
100% Functions 3/3
100% Lines 18/18

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                      4x                                                   12x 12x 12x   1x                   9x 9x 9x 9x 7x 7x             4x   2x   2x                       10x 10x 10x         10x                
/**
 * Guarded localStorage helpers for `tourId` persistence.
 *
 * Deliberately plain functions rather than `@usefy/use-local-storage`: the
 * tour writes imperatively on finish/skip events and reads once per
 * open-attempt — there is no reactive state to subscribe to, and cross-tab
 * sync is explicitly out of scope. All access is try/catch-guarded (privacy
 * mode, disabled storage, SSR) and silently no-ops on failure.
 */
 
/** localStorage key prefix for persisted tours (`usefy-tour:<tourId>`). */
export const TOUR_STORAGE_PREFIX = "usefy-tour:";
 
/**
 * The versioned value stored under `usefy-tour:<tourId>` when a tour is
 * finished or skipped. `v` allows future formats to migrate.
 */
export interface PersistedTourState {
  /** Format version. */
  v: 1;
  /** How the tour ended. */
  status: "finished" | "skipped";
  /** ISO timestamp of when it ended. */
  at: string;
}
 
/**
 * Whether a completion flag exists for `tourId` — the check that suppresses
 * *automatic* opens (`defaultOpen`).
 *
 * Presence-based on purpose: the key is only ever written on finish/skip, so
 * any value there (including a corrupt or legacy-format one) means "this user
 * already completed/dismissed the tour" — re-showing the tour because a value
 * failed to parse would be the worse failure mode. `resetTour` removes the
 * key regardless of its shape.
 */
export function isTourDone(tourId: string): boolean {
  Iif (typeof window === "undefined") return false;
  try {
    return window.localStorage.getItem(TOUR_STORAGE_PREFIX + tourId) !== null;
  } catch {
    return false;
  }
}
 
/**
 * Read the persisted state for `tourId`, or `null` when absent or not in a
 * known format (corrupt/legacy values are tolerated — see {@link isTourDone}
 * for why they still count as "done").
 */
export function readTourState(tourId: string): PersistedTourState | null {
  Iif (typeof window === "undefined") return null;
  try {
    const raw = window.localStorage.getItem(TOUR_STORAGE_PREFIX + tourId);
    if (raw === null) return null;
    const parsed: unknown = JSON.parse(raw);
    if (
      typeof parsed === "object" &&
      parsed !== null &&
      (parsed as { v?: unknown }).v === 1 &&
      ((parsed as { status?: unknown }).status === "finished" ||
        (parsed as { status?: unknown }).status === "skipped")
    ) {
      return parsed as PersistedTourState;
    }
    return null;
  } catch {
    return null;
  }
}
 
/**
 * Persist that `tourId` ended with `status`. Silently no-ops when storage is
 * unavailable.
 */
export function writeTourState(
  tourId: string,
  status: PersistedTourState["status"]
): void {
  Iif (typeof window === "undefined") return;
  try {
    const state: PersistedTourState = {
      v: 1,
      status,
      at: new Date().toISOString(),
    };
    window.localStorage.setItem(
      TOUR_STORAGE_PREFIX + tourId,
      JSON.stringify(state)
    );
  } catch {
    // Storage can throw (quota, privacy mode) — the tour still closes.
  }
}