import * as SecureStore from 'expo-secure-store'; import { AuthDb } from './database'; const SESSION_KEY = 'greenlens_session_v3'; const BACKEND_URL = ( process.env.EXPO_PUBLIC_BACKEND_URL || process.env.EXPO_PUBLIC_PAYMENT_SERVER_URL || '' ).trim(); export interface AuthSession { userId: number; // local SQLite id (for plants/settings queries) serverUserId: string; // server-side user id (in JWT) email: string; name: string; token: string; // JWT from server loggedInAt: string; } // ─── Internal helpers ────────────────────────────────────────────────────── const clearStoredSession = async (): Promise => { await SecureStore.deleteItemAsync(SESSION_KEY); }; const authPost = async (path: string, body: object): Promise<{ userId: string; email: string; name: string; token: string }> => { const hasBackendUrl = Boolean(BACKEND_URL); const url = hasBackendUrl ? `${BACKEND_URL}${path}` : path; let response: Response; try { response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }); } catch (e) { if (!hasBackendUrl) { throw new Error('BACKEND_URL_MISSING'); } throw new Error('NETWORK_ERROR'); } const data = await response.json().catch(() => ({})); if (!response.ok) { const code = (data as any).code || 'AUTH_ERROR'; const msg = (data as any).message || ''; console.warn(`[Auth] ${path} failed:`, response.status, code, msg); throw new Error(code); } return data as any; }; const buildSession = (data: { userId: string; email: string; name: string; token: string }): AuthSession => { const localUser = AuthDb.ensureLocalUser(data.email, data.name); return { userId: localUser.id, serverUserId: data.userId, email: data.email, name: data.name, token: data.token, loggedInAt: new Date().toISOString(), }; }; // ─── AuthService ─────────────────────────────────────────────────────────── export const AuthService = { async getSession(): Promise { try { const raw = await SecureStore.getItemAsync(SESSION_KEY); if (!raw) return null; const parsed = JSON.parse(raw) as Partial; if (!parsed.token || !parsed.serverUserId || !parsed.userId) { await clearStoredSession(); return null; } return parsed as AuthSession; } catch { await clearStoredSession(); return null; } }, async signUp(email: string, name: string, password: string): Promise { const data = await authPost('/auth/signup', { email, name, password }); const session = buildSession(data); await SecureStore.setItemAsync(SESSION_KEY, JSON.stringify(session)); return session; }, async login(email: string, password: string): Promise { const data = await authPost('/auth/login', { email, password }); const session = buildSession(data); await SecureStore.setItemAsync(SESSION_KEY, JSON.stringify(session)); return session; }, async logout(): Promise { await clearStoredSession(); }, async updateSessionName(name: string): Promise { const session = await this.getSession(); if (!session) return; await SecureStore.setItemAsync(SESSION_KEY, JSON.stringify({ ...session, name })); }, async validateWithServer(): Promise<'valid' | 'invalid' | 'unreachable'> { const session = await this.getSession(); if (!session) return 'invalid'; if (!BACKEND_URL) return 'unreachable'; try { const response = await fetch(`${BACKEND_URL}/v1/billing/summary`, { headers: { Authorization: `Bearer ${session.token}` }, }); if (response.status === 401 || response.status === 403) return 'invalid'; return 'valid'; } catch { return 'unreachable'; } }, async checkIfFirstRun(): Promise { const flag = await SecureStore.getItemAsync('greenlens_first_run_complete'); return flag !== 'true'; }, async markFirstRunComplete(): Promise { await SecureStore.setItemAsync('greenlens_first_run_complete', 'true'); }, async clearAllData(): Promise { await clearStoredSession(); await SecureStore.deleteItemAsync('greenlens_first_run_complete'); // Note: SQLite tables aren't cleared here to avoid destroying user data // without explicit consent, but session tokens are wiped. }, };