File size: 4,297 Bytes
65d68f5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
/**
 * Day 5 — controlled beta access gate.
 *
 * This is a SOFT, frontend-only gate so the builder can share DocDoe with a
 * small, selected group of SSLC beta students. It is intentionally NOT strong
 * security: it is a shared access code checked entirely in the browser, with
 * no backend, no account system, and no payment. Do not describe this as
 * "secure authentication" anywhere in the UI or docs.
 *
 * Privacy: only a beta code, an optional nickname, class, board and language
 * preference are stored — all on this device. No email, phone, full name,
 * exact school, location, or parent contact is ever asked for or stored.
 */

export type BetaAccessState = {
  hasAccess: boolean;
  accessCodeLabel?: string;
  grantedAt?: string;
  studentNickname?: string;
  classLevel?: string;
  board?: string;
  languagePreference?: string;
};

export type BetaAccessProfile = {
  studentNickname?: string;
  classLevel?: string;
  board?: string;
  languagePreference?: string;
};

export type BetaAccessGrantResult =
  | { ok: true; state: BetaAccessState }
  | { ok: false; error: "invalid_code" | "storage_unavailable" };

export const BETA_ACCESS_STORAGE_KEY = "docdoe:beta-access";

/**
 * Small local allowlist for this beta. Easy to update: add/remove strings.
 * Codes are matched case-insensitively after trimming.
 */
export const BETA_ACCESS_CODES = ["DOCDOE-BETA", "SSLC-PHYSICS", "ASSESS-CLASS10"];

export const BETA_ACCESS_DEFAULTS = {
  classLevel: "Class 10",
  board: "Kerala SSLC",
  languagePreference: "English",
};

function hasStorage(): boolean {
  return typeof window !== "undefined" && typeof window.localStorage !== "undefined";
}

function normalizeCode(code: string): string {
  return code.trim().toUpperCase();
}

/** True if the code matches an active beta allowlist entry (case-insensitive). */
export function validateBetaAccessCode(code: string): boolean {
  const normalized = normalizeCode(code ?? "");
  if (!normalized) return false;
  return BETA_ACCESS_CODES.some((allowed) => normalizeCode(allowed) === normalized);
}

function isAccessStateLike(value: unknown): value is BetaAccessState {
  return Boolean(value) && typeof value === "object" && typeof (value as Record<string, unknown>).hasAccess === "boolean";
}

/** Load access state. Corrupt/absent storage yields "no access", never throws. */
export function loadBetaAccess(): BetaAccessState {
  if (!hasStorage()) return { hasAccess: false };
  try {
    const raw = window.localStorage.getItem(BETA_ACCESS_STORAGE_KEY);
    if (!raw) return { hasAccess: false };
    const parsed = JSON.parse(raw);
    if (!isAccessStateLike(parsed)) return { hasAccess: false };
    return parsed;
  } catch {
    return { hasAccess: false };
  }
}

export function saveBetaAccess(state: BetaAccessState): void {
  if (!hasStorage()) return;
  try {
    window.localStorage.setItem(BETA_ACCESS_STORAGE_KEY, JSON.stringify(state));
  } catch {
    // Best-effort only; the gate simply re-prompts on the next load.
  }
}

export function clearBetaAccess(): void {
  if (!hasStorage()) return;
  try {
    window.localStorage.removeItem(BETA_ACCESS_STORAGE_KEY);
  } catch {
    // ignore
  }
}

export function hasBetaAccess(): boolean {
  return loadBetaAccess().hasAccess;
}

/**
 * Validate a code and, if active, grant + persist access with the given
 * (fully optional, non-identifying) profile fields. Returns a friendly error
 * on failure without explaining exactly why the code was rejected.
 */
export function grantBetaAccess(code: string, profile: BetaAccessProfile = {}): BetaAccessGrantResult {
  if (!validateBetaAccessCode(code)) {
    return { ok: false, error: "invalid_code" };
  }
  if (!hasStorage()) {
    return { ok: false, error: "storage_unavailable" };
  }

  const state: BetaAccessState = {
    hasAccess: true,
    accessCodeLabel: normalizeCode(code),
    grantedAt: new Date().toISOString(),
    studentNickname: profile.studentNickname?.trim() || undefined,
    classLevel: profile.classLevel?.trim() || BETA_ACCESS_DEFAULTS.classLevel,
    board: profile.board?.trim() || BETA_ACCESS_DEFAULTS.board,
    languagePreference: profile.languagePreference?.trim() || BETA_ACCESS_DEFAULTS.languagePreference,
  };
  saveBetaAccess(state);
  return { ok: true, state };
}