import type { ApiError, ApiResponse, AuthResponse, Beneficiary, BeneficiaryDashboardData, ChatResponse, DashboardSingleResponse } from '@/types'; import * as Crypto from 'expo-crypto'; import * as SecureStore from 'expo-secure-store'; // Callback for handling unauthorized responses (401) let onUnauthorizedCallback: (() => void) | null = null; export function setOnUnauthorizedCallback(callback: () => void) { onUnauthorizedCallback = callback; } const API_BASE_URL = 'https://eluxnetworks.net/function/well-api/api'; const CLIENT_ID = 'MA_001'; // WellNuo Backend API (our own API for auth, OTP, etc.) // TODO: Update to production URL when deployed const WELLNUO_API_URL = 'https://wellnuo.smartlaunchhub.com/api'; // Avatar images for elderly beneficiaries - grandmothers (бабушки) const ELDERLY_AVATARS = [ 'https://images.unsplash.com/photo-1566616213894-2d4e1baee5d8?w=200&h=200&fit=crop&crop=face', // grandmother with gray hair 'https://images.unsplash.com/photo-1544027993-37dbfe43562a?w=200&h=200&fit=crop&crop=face', // elderly woman smiling 'https://images.unsplash.com/photo-1491308056676-205b7c9a7dc1?w=200&h=200&fit=crop&crop=face', // senior woman portrait 'https://images.unsplash.com/photo-1580489944761-15a19d654956?w=200&h=200&fit=crop&crop=face', // older woman glasses 'https://images.unsplash.com/photo-1548142813-c348350df52b?w=200&h=200&fit=crop&crop=face', // grandmother portrait ]; // Get consistent avatar based on deployment_id function getAvatarForBeneficiary(deploymentId: number): string { const index = deploymentId % ELDERLY_AVATARS.length; return ELDERLY_AVATARS[index]; } // Helper function to format time ago function formatTimeAgo(date: Date): string { const now = new Date(); const diffMs = now.getTime() - date.getTime(); const diffMins = Math.floor(diffMs / 60000); const diffHours = Math.floor(diffMins / 60); const diffDays = Math.floor(diffHours / 24); if (diffMins < 1) return 'Just now'; if (diffMins < 60) return `${diffMins} min ago`; if (diffHours < 24) return `${diffHours} hour${diffHours > 1 ? 's' : ''} ago`; return `${diffDays} day${diffDays > 1 ? 's' : ''} ago`; } class ApiService { private async getToken(): Promise { try { return await SecureStore.getItemAsync('accessToken'); } catch { return null; } } private async getUserName(): Promise { try { return await SecureStore.getItemAsync('userName'); } catch { return null; } } private generateNonce(): string { const randomBytes = Crypto.getRandomBytes(16); return Array.from(randomBytes) .map(b => b.toString(16).padStart(2, '0')) .join(''); } private async makeRequest(params: Record): Promise> { try { const formData = new URLSearchParams(); Object.entries(params).forEach(([key, value]) => { formData.append(key, value); }); const response = await fetch(API_BASE_URL, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: formData.toString(), }); const data = await response.json(); // Handle 401 Unauthorized - trigger logout if (response.status === 401 || data.status === '401' || data.error === 'Unauthorized') { if (onUnauthorizedCallback) { onUnauthorizedCallback(); } return { ok: false, error: { message: 'Session expired. Please login again.', code: 'UNAUTHORIZED', status: 401, }, }; } if (data.status === '200 OK' || data.ok === true) { return { data: data as T, ok: true }; } return { ok: false, error: { message: data.message || data.error || 'Request failed', status: response.status, }, }; } catch (error) { const apiError: ApiError = { message: error instanceof Error ? error.message : 'Network error', code: 'NETWORK_ERROR', }; return { ok: false, error: apiError }; } } // Authentication async login(username: string, password: string): Promise> { const response = await this.makeRequest({ function: 'credentials', user_name: username, ps: password, clientId: CLIENT_ID, nonce: this.generateNonce(), }); if (response.ok && response.data) { // Save credentials to SecureStore await SecureStore.setItemAsync('accessToken', response.data.access_token); await SecureStore.setItemAsync('userId', response.data.user_id.toString()); await SecureStore.setItemAsync('userName', username); await SecureStore.setItemAsync('privileges', response.data.privileges); await SecureStore.setItemAsync('maxRole', response.data.max_role.toString()); } return response; } async logout(): Promise { await SecureStore.deleteItemAsync('accessToken'); await SecureStore.deleteItemAsync('userId'); await SecureStore.deleteItemAsync('userName'); await SecureStore.deleteItemAsync('privileges'); await SecureStore.deleteItemAsync('maxRole'); await SecureStore.deleteItemAsync('userEmail'); await SecureStore.deleteItemAsync('onboardingCompleted'); } // Save user email (for OTP auth flow) async saveEmail(email: string): Promise { await SecureStore.setItemAsync('userEmail', email); } // Get stored email async getStoredEmail(): Promise { try { return await SecureStore.getItemAsync('userEmail'); } catch { return null; } } // Onboarding completion flag - persists across app restarts async setOnboardingCompleted(completed: boolean): Promise { await SecureStore.setItemAsync('onboardingCompleted', completed ? '1' : '0'); } async isOnboardingCompleted(): Promise { try { const value = await SecureStore.getItemAsync('onboardingCompleted'); return value === '1'; } catch { return false; } } // Save mock user (for dev mode OTP flow) async saveMockUser(user: { user_id: string; user_name: string; email: string; max_role: string; privileges: string[] }): Promise { await SecureStore.setItemAsync('accessToken', `mock-token-${user.user_id}`); await SecureStore.setItemAsync('userId', user.user_id); await SecureStore.setItemAsync('userName', user.user_name); await SecureStore.setItemAsync('privileges', user.privileges.join(',')); await SecureStore.setItemAsync('maxRole', user.max_role); await SecureStore.setItemAsync('userEmail', user.email); } // ==================== OTP Authentication (WellNuo Backend) ==================== // Check if email exists in database async checkEmail(email: string): Promise> { try { const response = await fetch(`${WELLNUO_API_URL}/auth/check-email`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ email }), }); const data = await response.json(); if (response.ok) { return { data, ok: true }; } return { ok: false, error: { message: data.error || 'Failed to check email' }, }; } catch (error) { return { ok: false, error: { message: 'Network error. Please check your connection.' }, }; } } // Request OTP code - sends email via Brevo async requestOTP(email: string): Promise> { try { const response = await fetch(`${WELLNUO_API_URL}/auth/request-otp`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ email }), }); const data = await response.json(); if (response.ok) { return { data, ok: true }; } return { ok: false, error: { message: data.error || 'Failed to send OTP' }, }; } catch (error) { return { ok: false, error: { message: 'Network error. Please check your connection.' }, }; } } // Verify OTP code and get JWT token async verifyOTP(email: string, code: string): Promise> { try { const payload = { email: email.trim().toLowerCase(), code }; console.log('[API] verifyOTP request:', JSON.stringify(payload)); const response = await fetch(`${WELLNUO_API_URL}/auth/verify-otp`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(payload), }); const data = await response.json(); console.log('[API] verifyOTP response:', JSON.stringify(data)); if (response.ok && data.token) { // Save auth data await SecureStore.setItemAsync('accessToken', data.token); // Ensure user_id is string to prevent type errors await SecureStore.setItemAsync('userId', String(data.user.id)); await SecureStore.setItemAsync('userName', data.user.first_name || email.split('@')[0]); await SecureStore.setItemAsync('userEmail', email); await SecureStore.setItemAsync('maxRole', 'USER'); await SecureStore.setItemAsync('privileges', ''); return { data, ok: true }; } return { ok: false, error: { message: data.error || data.message || 'Invalid or expired code' }, }; } catch (error) { console.error('[API] verifyOTP network error:', error); return { ok: false, error: { message: 'Network error. Please check your connection.' }, }; } } async isAuthenticated(): Promise { const token = await this.getToken(); return !!token; } // Get stored user info async getStoredUser() { try { const userId = await SecureStore.getItemAsync('userId'); const userName = await SecureStore.getItemAsync('userName'); const privileges = await SecureStore.getItemAsync('privileges'); const maxRole = await SecureStore.getItemAsync('maxRole'); const email = await SecureStore.getItemAsync('userEmail'); if (!userId || !userName) return null; return { user_id: parseInt(userId, 10), user_name: userName, email: email || undefined, privileges: privileges || '', max_role: parseInt(maxRole || '0', 10), }; } catch { return null; } } // Beneficiaries (elderly people being monitored) async getBeneficiaries(): Promise> { const token = await this.getToken(); if (!token) { return { ok: false, error: { message: 'Not authenticated', code: 'UNAUTHORIZED' } }; } // Note: Using mock data since API structure is not fully documented // Replace with actual API call when available const mockBeneficiaries: Beneficiary[] = [ { id: 1, name: 'Julia Smith', status: 'online', relationship: 'Mother', last_activity: '2 min ago', sensor_data: { motion_detected: true, last_motion: '2 min ago', door_status: 'closed', temperature: 22, humidity: 45, }, }, { id: 2, name: 'Robert Johnson', status: 'offline', relationship: 'Father', last_activity: '1 hour ago', sensor_data: { motion_detected: false, last_motion: '1 hour ago', door_status: 'closed', temperature: 21, humidity: 50, }, }, ]; return { data: { beneficiaries: mockBeneficiaries }, ok: true }; } async getBeneficiary(id: number): Promise> { // Use real API data via getBeneficiaryDashboard const response = await this.getBeneficiaryDashboard(id.toString()); if (!response.ok || !response.data) { return { ok: false, error: response.error || { message: 'Beneficiary not found', code: 'NOT_FOUND' } }; } const data = response.data; // Determine if beneficiary is "online" based on last_detected_time const lastDetected = data.last_detected_time ? new Date(data.last_detected_time) : null; const isRecent = lastDetected && (Date.now() - lastDetected.getTime()) < 30 * 60 * 1000; // 30 min const deploymentId = parseInt(data.deployment_id, 10); const beneficiary: Beneficiary = { id: deploymentId, name: data.name, avatar: getAvatarForBeneficiary(deploymentId), status: isRecent ? 'online' : 'offline', address: data.address, timezone: data.time_zone, wellness_score: data.wellness_score_percent, wellness_descriptor: data.wellness_descriptor, last_location: data.last_location, temperature: data.temperature, units: data.units, sleep_hours: data.sleep_hours, bedroom_temperature: data.bedroom_temperature, before_last_location: data.before_last_location, last_detected_time: data.last_detected_time, last_activity: data.last_detected_time ? formatTimeAgo(new Date(data.last_detected_time)) : undefined, }; return { data: beneficiary, ok: true }; } // Get beneficiary dashboard data by deployment_id async getBeneficiaryDashboard(deploymentId: string): Promise> { const token = await this.getToken(); const userName = await this.getUserName(); if (!token || !userName) { return { ok: false, error: { message: 'Not authenticated', code: 'UNAUTHORIZED' } }; } const today = new Date().toISOString().split('T')[0]; // YYYY-MM-DD const response = await this.makeRequest({ function: 'dashboard_single', user_name: userName, token: token, deployment_id: deploymentId, date: today, nonce: this.generateNonce(), }); if (response.ok && response.data?.result_list?.[0]) { return { data: response.data.result_list[0], ok: true }; } return { ok: false, error: response.error || { message: 'Failed to get beneficiary data' }, }; } // Get all beneficiaries using deployments_list API (single fast request) async getAllBeneficiaries(): Promise> { const token = await this.getToken(); const userName = await this.getUserName(); if (!token || !userName) { return { ok: false, error: { message: 'Not authenticated', code: 'UNAUTHORIZED' } }; } // Use deployments_list API - single request for all beneficiaries const response = await this.makeRequest<{ result_list: Array<{ deployment_id: number; email: string; first_name: string; last_name: string; }> }>({ function: 'deployments_list', user_name: userName, token: token, first: '0', last: '100', }); if (!response.ok || !response.data?.result_list) { return { ok: false, error: response.error || { message: 'Failed to get beneficiaries' } }; } const beneficiaries: Beneficiary[] = response.data.result_list.map(item => ({ id: item.deployment_id, name: `${item.first_name} ${item.last_name}`.trim(), avatar: getAvatarForBeneficiary(item.deployment_id), status: 'offline' as const, // Will be updated when dashboard is loaded email: item.email, })); return { data: beneficiaries, ok: true }; } // AI Chat - deploymentId is required, no default value for security async sendMessage(question: string, deploymentId: string): Promise> { if (!deploymentId) { return { ok: false, error: { message: 'Please select a beneficiary first', code: 'NO_BENEFICIARY_SELECTED' } }; } const token = await this.getToken(); const userName = await this.getUserName(); if (!token || !userName) { return { ok: false, error: { message: 'Not authenticated', code: 'UNAUTHORIZED' } }; } return this.makeRequest({ function: 'voice_ask', clientId: CLIENT_ID, user_name: userName, token: token, question: question, deployment_id: deploymentId, }); } } export const api = new ApiService();