import type { ApiError, ApiResponse, AuthResponse, Beneficiary, BeneficiaryDashboardData, ChatResponse, DashboardSingleResponse, NotificationHistoryResponse, NotificationSettings, WPSensor } from '@/types'; // Callback for handling unauthorized responses (401) let onUnauthorizedCallback: (() => void) | null = null; export function setOnUnauthorizedCallback(callback: () => void) { onUnauthorizedCallback = callback; } // Callback for BLE cleanup on logout let onLogoutBLECleanupCallback: (() => Promise) | null = null; export function setOnLogoutBLECleanupCallback(callback: (() => Promise) | null) { onLogoutBLECleanupCallback = callback; } const API_BASE_URL = 'https://eluxnetworks.net/function/well-api/api'; const CLIENT_ID = 'MA_001'; // Threshold for considering a beneficiary "online" (30 minutes in milliseconds) const ONLINE_THRESHOLD_MS = 30 * 60 * 1000; // WellNuo Backend API (our own API for auth, OTP, etc.) 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 ]; // Room locations for sensor placement export const ROOM_LOCATIONS = [ { id: 'bedroom', label: 'Bedroom', icon: '🛏️', legacyCode: 102 }, { id: 'living_room', label: 'Living Room', icon: '🛋️', legacyCode: 103 }, { id: 'kitchen', label: 'Kitchen', icon: '🍳', legacyCode: 104 }, { id: 'bathroom', label: 'Bathroom', icon: '🚿', legacyCode: 105 }, { id: 'hallway', label: 'Hallway', icon: '🚪', legacyCode: 106 }, { id: 'entrance', label: 'Entrance', icon: '🏠', legacyCode: 111 }, { id: 'garage', label: 'Garage', icon: '🚗', legacyCode: 108 }, { id: 'basement', label: 'Basement', icon: '🪜', legacyCode: 110 }, { id: 'office', label: 'Office', icon: '💼', legacyCode: 107 }, { id: 'other', label: 'Other', icon: '📍', legacyCode: 200 }, ] as const; export type RoomLocationId = typeof ROOM_LOCATIONS[number]['id']; // Helper to convert location ID to Legacy API code function getLocationLegacyCode(locationId: string): number | undefined { const location = ROOM_LOCATIONS.find(loc => loc.id === locationId); return location?.legacyCode; } // Helper to convert Legacy API code to location ID export function getLocationIdFromCode(code: number): RoomLocationId | undefined { const location = ROOM_LOCATIONS.find(loc => loc.legacyCode === code); return location?.id; } // Helper to convert location label to location ID export function getLocationIdFromLabel(label: string): RoomLocationId | undefined { const labelLower = label.toLowerCase().trim(); const location = ROOM_LOCATIONS.find(loc => loc.label.toLowerCase() === labelLower); return location?.id; } // 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`; } // Bust image cache by appending timestamp function bustImageCache(url: string | null | undefined): string | null { if (!url) return null; const separator = url.includes('?') ? '&' : '?'; return `${url}${separator}t=${Date.now()}`; } class ApiService { // API URLs as instance properties for consistency private readonly baseUrl = WELLNUO_API_URL; private readonly legacyApiUrl = API_BASE_URL; // Public method to get the access token (used by AuthContext) async getToken(): Promise { try { return localStorage.getItem('accessToken'); } catch { return null; } } // Get legacy API token (for eluxnetworks.net API - dashboard, voice_ask) private async getLegacyToken(): Promise { try { return localStorage.getItem('legacyAccessToken'); } catch { return null; } } private generateNonce(): string { // Use Web Crypto API const randomBytes = new Uint8Array(16); crypto.getRandomValues(randomBytes); 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 (Legacy API - eluxnetworks.net) async login(username: string, password: string): Promise> { const response = await this.makeRequest({ function: 'credentials', email: username, ps: password, clientId: CLIENT_ID, nonce: this.generateNonce(), }); if (response.ok && response.data) { // Save LEGACY credentials separately localStorage.setItem('legacyAccessToken', response.data.access_token); localStorage.setItem('userId', response.data.user_id.toString()); localStorage.setItem('privileges', response.data.privileges); localStorage.setItem('maxRole', response.data.max_role.toString()); } return response; } async logout(): Promise { // Call BLE cleanup callback if set if (onLogoutBLECleanupCallback) { try { await onLogoutBLECleanupCallback(); } catch (error) { // Continue with logout even if BLE cleanup fails } } // Clear WellNuo API auth data localStorage.removeItem('accessToken'); localStorage.removeItem('userId'); localStorage.removeItem('userEmail'); localStorage.removeItem('onboardingCompleted'); // Clear legacy API auth data localStorage.removeItem('legacyAccessToken'); localStorage.removeItem('privileges'); localStorage.removeItem('maxRole'); // Clear user profile data localStorage.removeItem('userAvatar'); // Clear local cached data localStorage.removeItem('wellnuo_local_beneficiaries'); } // Save user email (for OTP auth flow) async saveEmail(email: string): Promise { localStorage.setItem('userEmail', email); } // Get stored email async getStoredEmail(): Promise { try { return localStorage.getItem('userEmail'); } catch { return null; } } // Onboarding completion flag async setOnboardingCompleted(completed: boolean): Promise { localStorage.setItem('onboardingCompleted', completed ? '1' : '0'); } async isOnboardingCompleted(): Promise { try { const value = localStorage.getItem('onboardingCompleted'); return value === '1'; } catch { return false; } } // ==================== 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 }; 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(); if (response.ok && data.token) { // Save ONLY technical auth data localStorage.setItem('accessToken', data.token); localStorage.setItem('userId', String(data.user.id)); localStorage.setItem('userEmail', email); return { data, ok: true }; } return { ok: false, error: { message: data.error || data.message || 'Invalid or expired code' }, }; } catch (error) { return { ok: false, error: { message: 'Network error. Please check your connection.' }, }; } } async isAuthenticated(): Promise { const token = await this.getToken(); return !!token; } // Get current user profile from API async getStoredUser() { try { const token = await this.getToken(); const userId = localStorage.getItem('userId'); if (!token || !userId) { return null; } // Fetch profile from server const profile = await this.getProfile(); if (!profile.ok || !profile.data) { // If token is invalid (401), clear all tokens and return null if (profile.error?.code === 'UNAUTHORIZED') { await this.logout(); return null; } // For network errors OR other API errors, fall back to minimal info const email = localStorage.getItem('userEmail'); return { user_id: parseInt(userId, 10), email: email || undefined, privileges: '', max_role: 0, }; } // Extract user data from nested 'user' object const userData = profile.data.user || profile.data; return { user_id: userData.id, email: userData.email, firstName: userData.firstName, lastName: userData.lastName, phone: userData.phone, privileges: '', max_role: 0, }; } catch (error) { // On any unexpected error, fall back to local data const userId = localStorage.getItem('userId'); const email = localStorage.getItem('userEmail'); if (userId) { return { user_id: parseInt(userId, 10), email: email || undefined, privileges: '', max_role: 0, }; } return null; } } // Get user profile from WellNuo API async getProfile(): Promise> { const token = await this.getToken(); if (!token) { return { ok: false, error: { message: 'Not authenticated', code: 'UNAUTHORIZED' } }; } try { const response = await fetch(`${WELLNUO_API_URL}/auth/me`, { method: 'GET', headers: { 'Authorization': `Bearer ${token}`, }, }); const data = await response.json(); if (!response.ok) { return { ok: false, error: { message: data.error || 'Failed to get profile', code: response.status === 401 ? 'UNAUTHORIZED' : 'API_ERROR', } }; } return { data, ok: true }; } catch (error) { return { ok: false, error: { message: 'Network error', code: 'NETWORK_ERROR' } }; } } // Update user profile on WellNuo API async updateProfile(updates: { firstName?: string; lastName?: string; phone?: string; address?: { street?: string; city?: string; zip?: string; state?: string; country?: string; }; }): Promise> { const token = await this.getToken(); if (!token) { return { ok: false, error: { message: 'Not authenticated', code: 'UNAUTHORIZED' } }; } try { const response = await fetch(`${WELLNUO_API_URL}/auth/profile`, { method: 'PATCH', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json', }, body: JSON.stringify(updates), }); const data = await response.json(); if (!response.ok) { return { ok: false, error: { message: data.error || 'Failed to update profile' } }; } return { data, ok: true }; } catch (error) { return { ok: false, error: { message: 'Network error', code: 'NETWORK_ERROR' } }; } } // Update user profile avatar on WellNuo API async updateProfileAvatar(imageFile: File | null): Promise> { const token = await this.getToken(); if (!token) { return { ok: false, error: { message: 'Not authenticated', code: 'UNAUTHORIZED' } }; } try { let avatarData: string | null = null; if (imageFile) { // Convert File to base64 const base64Data = await this.fileToBase64(imageFile); const mimeType = imageFile.type || 'image/jpeg'; avatarData = `data:${mimeType};base64,${base64Data}`; } const response = await fetch(`${WELLNUO_API_URL}/auth/avatar`, { method: 'PATCH', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ avatar: avatarData }), }); const data = await response.json(); if (!response.ok) { return { ok: false, error: { message: data.error || 'Failed to update avatar' } }; } return { data: data.user, ok: true }; } catch (error) { return { ok: false, error: { message: 'Network error', code: 'NETWORK_ERROR' } }; } } // Helper to convert File to base64 private fileToBase64(file: File): Promise { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onloadend = () => { const base64 = reader.result as string; // Remove data URL prefix const base64Data = base64.split(',')[1]; resolve(base64Data); }; reader.onerror = reject; reader.readAsDataURL(file); }); } // Get all beneficiaries from WellNuo API async getAllBeneficiaries(): Promise> { const token = await this.getToken(); if (!token) { return { ok: false, error: { message: 'Not authenticated', code: 'UNAUTHORIZED' } }; } try { const response = await fetch(`${WELLNUO_API_URL}/me/beneficiaries`, { method: 'GET', headers: { 'Authorization': `Bearer ${token}`, }, }); const data = await response.json(); if (!response.ok) { if (response.status === 401) { if (onUnauthorizedCallback) onUnauthorizedCallback(); return { ok: false, error: { message: 'Session expired', code: 'UNAUTHORIZED', status: 401 } }; } return { ok: false, error: { message: data.error || 'Failed to get beneficiaries' } }; } // Map API response to Beneficiary type const beneficiaries: Beneficiary[] = (data.beneficiaries || []).map((item: any) => ({ id: item.id, name: item.originalName || item.name || item.email, customName: item.customName || null, displayName: item.displayName || item.customName || item.name || item.email || 'Unknown User', originalName: item.originalName || item.name, avatar: bustImageCache(item.avatarUrl) || undefined, status: 'offline' as const, email: item.email, address: typeof item.address === 'string' ? item.address : (item.address?.street ? `${item.address.street}, ${item.address.city}` : undefined), subscription: item.subscription, equipmentStatus: item.equipmentStatus, hasDevices: item.hasDevices || false, trackingNumber: item.trackingNumber, role: item.role, })); return { data: beneficiaries, ok: true }; } catch (error) { return { ok: false, error: { message: 'Network error', code: 'NETWORK_ERROR' } }; } } // Get single beneficiary details from WellNuo API async getWellNuoBeneficiary(id: number): Promise> { const token = await this.getToken(); if (!token) { return { ok: false, error: { message: 'Not authenticated', code: 'UNAUTHORIZED' } }; } try { const response = await fetch(`${WELLNUO_API_URL}/me/beneficiaries/${id}`, { method: 'GET', headers: { 'Authorization': `Bearer ${token}`, }, }); const data = await response.json(); if (!response.ok) { return { ok: false, error: { message: data.error || 'Failed to get beneficiary' } }; } const beneficiary: Beneficiary = { id: data.id, name: data.originalName || data.name || data.email, customName: data.customName || null, displayName: data.displayName || data.customName || data.name || data.email || 'Unknown User', originalName: data.originalName || data.name, avatar: bustImageCache(data.avatarUrl) || undefined, status: 'offline' as const, email: data.email, address: typeof data.address === 'string' ? data.address : (data.address?.street ? `${data.address.street}, ${data.address.city}` : undefined), subscription: data.subscription ? { status: data.subscription.status, planType: data.subscription.planType || data.subscription.plan, endDate: data.subscription.endDate, cancelAtPeriodEnd: data.subscription.cancelAtPeriodEnd, } : undefined, equipmentStatus: data.equipmentStatus, hasDevices: data.hasDevices || false, trackingNumber: data.trackingNumber, role: data.role, }; return { data: beneficiary, ok: true }; } catch (error) { return { ok: false, error: { message: 'Network error', code: 'NETWORK_ERROR' } }; } } // Create new beneficiary async createBeneficiary(data: { name: string; phone?: string; address?: string; }): Promise> { const token = await this.getToken(); if (!token) { return { ok: false, error: { message: 'Not authenticated', code: 'UNAUTHORIZED' } }; } try { const response = await fetch(`${WELLNUO_API_URL}/me/beneficiaries`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}`, }, body: JSON.stringify(data), }); const result = await response.json(); if (!response.ok) { return { ok: false, error: { message: result.error || 'Failed to create beneficiary' } }; } const beneficiary: Beneficiary = { id: result.beneficiary.id, name: result.beneficiary.name || '', displayName: result.beneficiary.name || 'Unknown User', status: 'offline' as const, }; return { data: beneficiary, ok: true }; } catch (error) { return { ok: false, error: { message: 'Network error', code: 'NETWORK_ERROR' } }; } } // Update beneficiary avatar async updateBeneficiaryAvatar(id: number, imageFile: File | null): Promise> { const token = await this.getToken(); if (!token) { return { ok: false, error: { message: 'Not authenticated', code: 'UNAUTHORIZED' } }; } try { let base64Image: string | null = null; if (imageFile) { const base64Data = await this.fileToBase64(imageFile); const mimeType = imageFile.type || 'image/jpeg'; base64Image = `data:${mimeType};base64,${base64Data}`; } const apiResponse = await fetch(`${WELLNUO_API_URL}/me/beneficiaries/${id}/avatar`, { method: 'PATCH', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}`, }, body: JSON.stringify({ avatar: base64Image }), }); const data = await apiResponse.json(); if (!apiResponse.ok) { return { ok: false, error: { message: data.error || 'Failed to update avatar' } }; } return { data: { avatarUrl: data.beneficiary?.avatarUrl || null }, ok: true }; } catch (error) { return { ok: false, error: { message: 'Network error', code: 'NETWORK_ERROR' } }; } } // Delete beneficiary async deleteBeneficiary(id: number): Promise> { const token = await this.getToken(); if (!token) { return { ok: false, error: { message: 'Not authenticated', code: 'UNAUTHORIZED' } }; } try { const response = await fetch(`${WELLNUO_API_URL}/me/beneficiaries/${id}`, { method: 'DELETE', headers: { 'Authorization': `Bearer ${token}`, }, }); const data = await response.json(); if (!response.ok) { return { ok: false, error: { message: data.error || 'Failed to delete beneficiary' } }; } return { data: { success: true }, ok: true }; } catch (error) { return { ok: false, error: { message: 'Network error', code: 'NETWORK_ERROR' } }; } } // Get Legacy API credentials for device operations async getLegacyCredentials(): Promise<{ userName: string; token: string } | null> { const creds = await this.getLegacyWebViewCredentials(); if (!creds) return null; return { userName: creds.userName, token: creds.token }; } // Demo credentials for legacy dashboard private readonly DEMO_LEGACY_USER = 'robster'; private readonly DEMO_LEGACY_PASSWORD = 'rob2'; // Login to legacy dashboard API async loginToLegacyDashboard(): Promise> { try { const formData = new URLSearchParams(); formData.append('function', 'credentials'); formData.append('user_name', this.DEMO_LEGACY_USER); formData.append('ps', this.DEMO_LEGACY_PASSWORD); formData.append('clientId', CLIENT_ID); formData.append('nonce', this.generateNonce()); 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(); if (data.status === '200 OK' && data.access_token && typeof data.access_token === 'string' && data.access_token.includes('.')) { localStorage.setItem('legacyAccessToken', data.access_token); localStorage.setItem('legacyUserId', String(data.user_id)); localStorage.setItem('legacyUserName', this.DEMO_LEGACY_USER); return { data: data as AuthResponse, ok: true }; } return { ok: false, error: { message: data.message || 'Legacy login failed - invalid credentials' }, }; } catch (error) { return { ok: false, error: { message: 'Failed to connect to dashboard API' }, }; } } // Get legacy credentials for WebView injection async getLegacyWebViewCredentials(): Promise<{ token: string; userName: string; userId: string; } | null> { try { const token = localStorage.getItem('legacyAccessToken'); const userName = localStorage.getItem('legacyUserName'); const userId = localStorage.getItem('legacyUserId'); const isValidToken = token && typeof token === 'string' && token.includes('.'); let needsRefresh = false; if (isValidToken && userName && userId) { if (userName !== this.DEMO_LEGACY_USER) { needsRefresh = true; } if (!needsRefresh) { try { const parts = token.split('.'); if (parts.length === 3) { const payload = JSON.parse(atob(parts[1])); const exp = payload.exp; if (exp) { const now = Math.floor(Date.now() / 1000); if (now >= exp) { needsRefresh = true; } } } } catch (e) { needsRefresh = true; } } } if (!isValidToken || !userName || !userId || needsRefresh) { localStorage.removeItem('legacyAccessToken'); localStorage.removeItem('legacyUserName'); localStorage.removeItem('legacyUserId'); const loginResult = await this.loginToLegacyDashboard(); if (!loginResult.ok) return null; const newToken = localStorage.getItem('legacyAccessToken'); const newUserName = localStorage.getItem('legacyUserName'); const newUserId = localStorage.getItem('legacyUserId'); if (!newToken || !newUserName || !newUserId) return null; return { token: newToken, userName: newUserName, userId: newUserId }; } return { token, userName, userId }; } catch (e) { return null; } } } export const api = new ApiService(); export default api;