/** * DocuElevate API client for the mobile app. * * All requests authenticate via a Bearer token stored in the device's secure * keychain (via expo-secure-store). The token is obtained once through the * SSO flow and cached until the user explicitly logs out. */ import * as SecureStore from "expo-secure-store"; // --------------------------------------------------------------------------- // Constants // --------------------------------------------------------------------------- export const SECURE_STORE_API_TOKEN_KEY = "de_api_token"; export const SECURE_STORE_BASE_URL_KEY = "de_base_url"; export const SECURE_STORE_OWNER_ID_KEY = "de_owner_id"; // --------------------------------------------------------------------------- // Types // --------------------------------------------------------------------------- export interface WhoAmIResponse { owner_id: string; display_name: string | null; email: string | null; avatar_url: string | null; is_admin: boolean; } export interface GenerateTokenResponse { token: string; token_id: number; name: string; created_at: string; } export interface QRClaimResponse { token: string; token_id: number; name: string; owner_id: string; created_at: string; } export interface DeviceRegistration { push_token: string; device_name?: string; platform: "ios" | "android" | "web"; } export interface ProcessingStatus { status: string; last_step: string | null; has_errors: boolean; total_steps: number; } export interface FileRecord { id: number; original_filename: string; file_size: number | null; mime_type: string | null; created_at: string; processing_status: ProcessingStatus; } export interface UploadResponse { task_id?: string; status: string; original_filename: string; stored_filename: string; duplicate_of?: { duplicate_type: string; original_file_id: number; original_filename: string; message: string; }; } export interface ProcessingLog { id: number; task_id: string; step_name: string; status: string; message: string; timestamp: string; } export interface FileDetail { file: { id: number; filehash: string; original_filename: string; local_filename: string; file_size: number; mime_type: string; created_at: string; }; processing_status: ProcessingStatus; logs: ProcessingLog[]; files_on_disk: { original: boolean; }; } // --------------------------------------------------------------------------- // Base API client // --------------------------------------------------------------------------- class DocuElevateAPI { private baseUrl: string = ""; async init(baseUrl: string): Promise { this.baseUrl = baseUrl.replace(/\/$/, ""); await SecureStore.setItemAsync(SECURE_STORE_BASE_URL_KEY, this.baseUrl); } async loadFromStorage(): Promise { try { const url = await SecureStore.getItemAsync(SECURE_STORE_BASE_URL_KEY); if (url) { this.baseUrl = url; return true; } } catch { // ignore } return false; } getBaseUrl(): string { return this.baseUrl; } private async getToken(): Promise { try { return await SecureStore.getItemAsync(SECURE_STORE_API_TOKEN_KEY); } catch { return null; } } private async request( method: string, path: string, options?: { body?: unknown; formData?: FormData } ): Promise { const token = await this.getToken(); const headers: Record = {}; if (token) { headers["Authorization"] = `Bearer ${token}`; } let body: string | FormData | undefined; if (options?.formData) { body = options.formData; // Let fetch set multipart content-type with boundary automatically } else if (options?.body !== undefined) { headers["Content-Type"] = "application/json"; body = JSON.stringify(options.body); } const response = await fetch(`${this.baseUrl}${path}`, { method, headers, body, }); if (!response.ok) { let detail = `HTTP ${response.status}`; try { const err = await response.json(); detail = err.detail || JSON.stringify(err); } catch { // ignore } throw new Error(detail); } if (response.status === 204) { return undefined as unknown as T; } return response.json(); } // ------------------------------------------------------------------------- // Auth // ------------------------------------------------------------------------- /** Exchange the current session (cookie) for a long-lived API token. */ async generateMobileToken(deviceName: string): Promise { return this.request("POST", "/api/mobile/generate-token", { body: { device_name: deviceName }, }); } /** Claim a QR login challenge and receive an API token. */ async claimQRChallenge(challengeToken: string, deviceName: string): Promise { return this.request("POST", "/api/qr-auth/claim", { body: { challenge_token: challengeToken, device_name: deviceName }, }); } /** Return profile information for the authenticated user. */ async whoAmI(): Promise { return this.request("GET", "/api/mobile/whoami"); } // ------------------------------------------------------------------------- // Push notifications // ------------------------------------------------------------------------- /** Register a push notification device token. */ async registerDevice(data: DeviceRegistration): Promise { await this.request("POST", "/api/mobile/register-device", { body: data }); } /** Deactivate a device registration. */ async deactivateDevice(deviceId: number): Promise { await this.request("DELETE", `/api/mobile/devices/${deviceId}`); } // ------------------------------------------------------------------------- // Files // ------------------------------------------------------------------------- /** Upload a file for processing. */ async uploadFile(uri: string, filename: string, mimeType?: string): Promise { const formData = new FormData(); formData.append("file", { uri, name: filename, type: mimeType || "application/octet-stream", } as unknown as Blob); return this.request("POST", "/api/ui-upload", { formData }); } /** List recently processed files, optionally filtered by filename search. */ async listFiles(page = 1, pageSize = 20, search?: string): Promise { let url = `/api/files?page=${page}&per_page=${pageSize}`; if (search) url += `&search=${encodeURIComponent(search)}`; const data = await this.request<{ files: FileRecord[]; pagination: unknown }>("GET", url); return data.files; } /** Get the processing status for a single file by its ID. */ async getFileStatus(fileId: number): Promise { const data = await this.request<{ processing_status: ProcessingStatus }>( "GET", `/api/files/${fileId}` ); return data.processing_status; } /** Get full file details including processing logs. */ async getFileDetail(fileId: number): Promise { return this.request("GET", `/api/files/${fileId}`); } } export const api = new DocuElevateAPI(); export default api;