import { Platform } from "react-native"; import { loadToken, saveToken, clearToken } from "./storage"; /** * API base URL. On web the app is served from the same origin as the API, so we * use a relative path. On native (the Android APK) it must point at the public * HTTPS host, baked in at build time via EXPO_PUBLIC_API_URL. */ export const API_BASE = Platform.OS === "web" ? "" : (process.env.EXPO_PUBLIC_API_URL ?? "https://scan.beartariacampgrounds.com").replace(/\/+$/, ""); export interface ResourceCount { total: number; redeemed: number; remaining: number; } export interface TicketView { code: string; name: string; email: string; total: number; redeemed: number; remaining: number; ice: ResourceCount; extras: { carParking: boolean; rvParking: boolean; iceAccess: boolean; isDonor: boolean; freeUnder4: number; }; ages: { bracket: string; count: number; free: boolean }[]; } export class AuthError extends Error {} export class ApiError extends Error {} let cachedToken: string | null = null; export async function getToken(): Promise { if (cachedToken) return cachedToken; cachedToken = await loadToken(); return cachedToken; } export async function login(pin: string): Promise { const res = await fetch(`${API_BASE}/api/auth/login`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ pin }), }); if (res.status === 401) throw new AuthError("Incorrect PIN"); if (!res.ok) throw new ApiError(`Login failed (${res.status})`); const { token } = (await res.json()) as { token: string }; cachedToken = token; await saveToken(token); } export async function logout(): Promise { cachedToken = null; await clearToken(); } async function authed(path: string, init: RequestInit = {}): Promise { const token = await getToken(); if (!token) throw new AuthError("Not logged in"); const res = await fetch(`${API_BASE}${path}`, { ...init, headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}`, ...(init.headers || {}), }, }); if (res.status === 401) { await logout(); throw new AuthError("Session expired"); } const text = await res.text(); let body: any = undefined; if (text) { try { body = JSON.parse(text); } catch { body = text; } } if (!res.ok) { throw new ApiError(body?.error ?? body?.detail ?? `Request failed (${res.status})`); } return body as T; } export type LookupResult = | { ok: true; found: true; ticket: TicketView } | { ok: true; found: false } | { ok: false; reason: "db_error"; detail: string }; export function lookup(code: string): Promise { return authed("/api/lookup", { method: "POST", body: JSON.stringify({ code }), }); } export type RedeemResult = | { ok: true; ticket: TicketView; checkedIn: number } | { ok: false; reason: "not_found" | "exhausted" | "insufficient" | "db_error"; ticket?: TicketView; detail?: string; }; export type Resource = "tickets" | "ice"; export function redeem(code: string, count: number, resource: Resource = "tickets"): Promise { return authed("/api/redeem", { method: "POST", body: JSON.stringify({ code, count, resource }), }); } export interface DonorLookup { found: boolean; email: string; name: string; online: number; offline: number; total: number; source: "master" | "transactions" | "none"; } export type BanquetResult = | { ok: true; ticketName: string; donor: DonorLookup } | { ok: false; reason: "not_found" | "no_email" | "db_error" | "banquet_disabled"; detail?: string }; export function banquet(input: { code?: string; email?: string }): Promise { return authed("/api/banquet", { method: "POST", body: JSON.stringify(input), }); } export function searchTickets(q: string): Promise<{ results: TicketView[] }> { return authed<{ results: TicketView[] }>(`/api/tickets?q=${encodeURIComponent(q)}`); } export interface AuditEntry { id: number; code: string; people: number; name: string; remainingAfter: number; at: string; action: "check-in" | "undo"; } export function getAudit(opts: { code?: string; limit?: number } = {}): Promise<{ enabled: boolean; entries: AuditEntry[]; }> { const params = new URLSearchParams(); if (opts.code) params.set("code", opts.code); if (opts.limit) params.set("limit", String(opts.limit)); const qs = params.toString(); return authed<{ enabled: boolean; entries: AuditEntry[] }>(`/api/audit${qs ? `?${qs}` : ""}`); }