import type { ApiError, ApiResponse, AuthResponse, Beneficiary, BeneficiaryDashboardData, ChatResponse, DashboardSingleResponse, NotificationSettings, WPSensor } from '@/types'; import { File } from 'expo-file-system'; import * as SecureStore from 'expo-secure-store'; import AsyncStorage from '@react-native-async-storage/async-storage'; import 'react-native-get-random-values'; // Polyfill for crypto.getRandomValues // 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 ]; // Room locations for sensor placement // Used in device settings to select where sensor is installed 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; } // 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 { // 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 await SecureStore.getItemAsync('accessToken'); } catch { return null; } } // Get legacy API token (for eluxnetworks.net API - dashboard, voice_ask) private async getLegacyToken(): Promise { try { return await SecureStore.getItemAsync('legacyAccessToken'); } catch { return null; } } private generateNonce(): string { // Use Web Crypto API (polyfilled by react-native-get-random-values) 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) // Used for dev mode and dashboard access 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 (not to accessToken!) // accessToken is reserved for WellNuo API JWT tokens await SecureStore.setItemAsync('legacyAccessToken', response.data.access_token); // Keep these for backward compatibility await SecureStore.setItemAsync('userId', response.data.user_id.toString()); await SecureStore.setItemAsync('privileges', response.data.privileges); await SecureStore.setItemAsync('maxRole', response.data.max_role.toString()); } return response; } async logout(): Promise { // Clear WellNuo API auth data await SecureStore.deleteItemAsync('accessToken'); await SecureStore.deleteItemAsync('userId'); await SecureStore.deleteItemAsync('userEmail'); await SecureStore.deleteItemAsync('onboardingCompleted'); // Clear legacy API auth data await SecureStore.deleteItemAsync('legacyAccessToken'); await SecureStore.deleteItemAsync('privileges'); await SecureStore.deleteItemAsync('maxRole'); // Clear user profile data (avatar, etc.) await SecureStore.deleteItemAsync('userAvatar'); // Clear local cached data (beneficiaries, etc.) await AsyncStorage.removeItem('wellnuo_local_beneficiaries'); } // 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; 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('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 }; 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 (token, userId, email) // User profile data is fetched from API, NOT stored locally await SecureStore.setItemAsync('accessToken', data.token); await SecureStore.setItemAsync('userId', String(data.user.id)); await SecureStore.setItemAsync('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 (not local storage!) async getStoredUser() { try { const token = await this.getToken(); const userId = await SecureStore.getItemAsync('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 // This will trigger re-authentication if (profile.error?.code === 'UNAUTHORIZED') { await this.logout(); return null; } // For network errors OR other API errors, fall back to minimal info // We don't want to log out the user just because the server is temporarily unavailable const email = await SecureStore.getItemAsync('userEmail'); return { user_id: parseInt(userId, 10), email: email || undefined, privileges: '', max_role: 0, }; } // /auth/me returns { user: {...}, beneficiaries: [...] } // 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 instead of logging out const userId = await SecureStore.getItemAsync('userId'); const email = await SecureStore.getItemAsync('userEmail'); if (userId) { return { user_id: parseInt(userId, 10), email: email || undefined, privileges: '', max_role: 0, }; } return null; } } // Get user profile from WellNuo API // Note: /auth/me can return { user: {...}, beneficiaries: [...] } or just { id, email, ... } 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(imageUri: string | 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 (imageUri) { // Read image as base64 using new expo-file-system v19+ File API const file = new File(imageUri); const base64Data = await file.base64(); // Determine MIME type from URI const extension = imageUri.split('.').pop()?.toLowerCase() || 'jpeg'; const mimeType = extension === 'png' ? 'image/png' : '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' } }; } } // 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', displayName: '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', displayName: '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, displayName: data.name, // For UI display 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 (LEGACY API - eluxnetworks.net) async getBeneficiaryDashboard(deploymentId: string): Promise> { // Use legacy API credentials for dashboard const token = await this.getLegacyToken(); if (!token) { // Fallback to regular credentials if legacy not available const fallbackToken = await this.getToken(); if (!fallbackToken) { return { ok: false, error: { message: 'Not authenticated for dashboard access', code: 'UNAUTHORIZED' } }; } // Note: This will likely fail if using WellNuo token, but we try anyway } const today = new Date().toISOString().split('T')[0]; // YYYY-MM-DD const response = await this.makeRequest({ function: 'dashboard_single', token: token || await this.getToken() || '', 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 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, // Original name from server customName: item.customName || null, // User's custom name for this beneficiary displayName: item.displayName || item.customName || item.name || item.email, // Server-provided displayName originalName: item.originalName || item.name, // Original name from beneficiaries table avatar: item.avatarUrl || undefined, // Use uploaded avatar from server 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, // Equipment status from orders equipmentStatus: item.equipmentStatus, hasDevices: item.hasDevices || false, trackingNumber: item.trackingNumber, role: item.role, // User's role for this beneficiary (custodian, guardian, caretaker) })); 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, // Original name from server customName: data.customName || null, // User's custom name for this beneficiary displayName: data.displayName || data.customName || data.name || data.email, // Server-provided displayName originalName: data.originalName || data.name, // Original name from beneficiaries table avatar: 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, // Equipment status from orders equipmentStatus: data.equipmentStatus, hasDevices: data.hasDevices || false, trackingNumber: data.trackingNumber, // User's role for this beneficiary (custodian, guardian, caretaker) role: data.role, }; return { data: beneficiary, ok: true }; } catch (error) { return { ok: false, error: { message: 'Network error', code: 'NETWORK_ERROR' } }; } } // Update beneficiary in WellNuo API async updateWellNuoBeneficiary(id: number, updates: { 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/${id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}`, }, body: JSON.stringify(updates), }); const data = await response.json(); if (!response.ok) { return { ok: false, error: { message: data.error || 'Failed to update beneficiary' } }; } const beneficiary: Beneficiary = { id: data.beneficiary.id, name: data.beneficiary.name || data.beneficiary.email, displayName: data.beneficiary.displayName || data.beneficiary.name || data.beneficiary.email, email: data.beneficiary.email, status: 'offline' as const, }; return { data: beneficiary, ok: true }; } catch (error) { return { ok: false, error: { message: 'Network error', code: 'NETWORK_ERROR' } }; } } // Create new beneficiary (grants owner access automatically) 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 || '', // For UI display status: 'offline' as const, }; return { data: beneficiary, ok: true }; } catch (error) { return { ok: false, error: { message: 'Network error', code: 'NETWORK_ERROR' } }; } } // Upload/update beneficiary avatar async updateBeneficiaryAvatar(id: number, imageUri: string | 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 (imageUri) { // Read file as base64 using new expo-file-system v19+ File API const file = new File(imageUri); const base64Data = await file.base64(); // Determine mime type from URI extension const extension = imageUri.split('.').pop()?.toLowerCase() || 'jpeg'; const mimeType = extension === 'png' ? 'image/png' : 'image/jpeg'; // Create data URI 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 (removes access record) 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' } }; } } // Update beneficiary equipment status async updateBeneficiaryEquipmentStatus( id: number, status: 'none' | 'ordered' | 'shipped' | 'delivered' ): 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}/equipment-status`, { method: 'PATCH', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}`, }, body: JSON.stringify({ status }), }); const data = await response.json(); if (!response.ok) { return { ok: false, error: { message: data.error || 'Failed to update equipment status' } }; } return { data: { success: true }, ok: true }; } catch (error) { return { ok: false, error: { message: 'Network error', code: 'NETWORK_ERROR' } }; } } // Update beneficiary custom name (per-user, stored in user_access) async updateBeneficiaryCustomName( id: number, customName: string | null ): 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}/custom-name`, { method: 'PATCH', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}`, }, body: JSON.stringify({ customName }), }); const data = await response.json(); if (!response.ok) { return { ok: false, error: { message: data.error || 'Failed to update custom name' } }; } return { data: { customName: data.customName }, ok: true }; } catch (error) { return { ok: false, error: { message: 'Network error', code: 'NETWORK_ERROR' } }; } } // AI Chat - deploymentId is required, no default value for security (LEGACY API) async sendMessage(question: string, deploymentId: string): Promise> { if (!deploymentId) { return { ok: false, error: { message: 'Please select a beneficiary first', code: 'NO_BENEFICIARY_SELECTED' } }; } // Use legacy API credentials for voice_ask const token = await this.getLegacyToken() || await this.getToken(); if (!token) { return { ok: false, error: { message: 'Not authenticated', code: 'UNAUTHORIZED' } }; } return this.makeRequest({ function: 'voice_ask', clientId: CLIENT_ID, token: token, question: question, deployment_id: deploymentId, }); } // ==================== Invitations API ==================== // Send invitation to share access to a beneficiary async sendInvitation(params: { beneficiaryId: string; email: string; role: 'caretaker' | 'guardian'; label?: 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}/invitations`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}`, }, body: JSON.stringify({ beneficiaryId: params.beneficiaryId, email: params.email, role: params.role, // Backend expects 'caretaker' or 'guardian' label: params.label, }), }); const data = await response.json(); if (response.ok) { return { data, ok: true }; } return { ok: false, error: { message: data.error || 'Failed to send invitation' }, }; } catch (error) { return { ok: false, error: { message: 'Network error. Please check your connection.' }, }; } } // Get invitations for a beneficiary async getInvitations(beneficiaryId: 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}/invitations/beneficiary/${beneficiaryId}`, { method: 'GET', headers: { 'Authorization': `Bearer ${token}`, }, }); const data = await response.json(); if (response.ok) { return { data, ok: true }; } return { ok: false, error: { message: data.error || 'Failed to get invitations' }, }; } catch (error) { return { ok: false, error: { message: 'Network error. Please check your connection.' }, }; } } // Delete invitation async deleteInvitation(invitationId: 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}/invitations/${invitationId}`, { method: 'DELETE', headers: { 'Authorization': `Bearer ${token}`, }, }); const data = await response.json(); if (response.ok) { return { data, ok: true }; } return { ok: false, error: { message: data.error || 'Failed to delete invitation' }, }; } catch (error) { return { ok: false, error: { message: 'Network error. Please check your connection.' }, }; } } // Accept invitation code async acceptInvitation(code: 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}/invitations/accept`, { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ code }), }); const data = await response.json(); if (response.ok) { return { data, ok: true }; } return { ok: false, error: { message: data.error || 'Failed to accept invitation' }, }; } catch (error) { return { ok: false, error: { message: 'Network error. Please check your connection.' }, }; } } // Activate equipment for beneficiary (saves to server) async activateBeneficiary(beneficiaryId: number, serialNumber: 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/${beneficiaryId}/activate`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}`, }, body: JSON.stringify({ serialNumber }), }); const data = await response.json(); if (response.ok) { return { data, ok: true }; } return { ok: false, error: { message: data.error || 'Failed to activate equipment' }, }; } catch (error) { return { ok: false, error: { message: 'Network error. Please check your connection.' }, }; } } // ==================== SUBSCRIPTION MANAGEMENT ==================== // Cancel subscription at period end async cancelSubscription(beneficiaryId: 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}/stripe/cancel-subscription`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}`, }, body: JSON.stringify({ beneficiaryId }), }); const data = await response.json(); if (response.ok) { return { data, ok: true }; } return { ok: false, error: { message: data.error || 'Failed to cancel subscription' }, }; } catch (error) { return { ok: false, error: { message: 'Network error. Please check your connection.' }, }; } } // Get transaction history from Stripe async getTransactionHistory(beneficiaryId: number, limit = 10): Promise; hasMore: boolean; }>> { const token = await this.getToken(); if (!token) { return { ok: false, error: { message: 'Not authenticated', code: 'UNAUTHORIZED' } }; } try { const response = await fetch(`${WELLNUO_API_URL}/stripe/transaction-history/${beneficiaryId}?limit=${limit}`, { method: 'GET', headers: { 'Authorization': `Bearer ${token}`, }, }); const data = await response.json(); if (response.ok) { return { data, ok: true }; } return { ok: false, error: { message: data.error || 'Failed to get transaction history' }, }; } catch (error) { return { ok: false, error: { message: 'Network error. Please check your connection.' }, }; } } // Reactivate subscription that was set to cancel async reactivateSubscription(beneficiaryId: 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}/stripe/reactivate-subscription`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}`, }, body: JSON.stringify({ beneficiaryId }), }); const data = await response.json(); if (response.ok) { return { data, ok: true }; } return { ok: false, error: { message: data.error || 'Failed to reactivate subscription' }, }; } catch (error) { return { ok: false, error: { message: 'Network error. Please check your connection.' }, }; } } // Update invitation role async updateInvitation(invitationId: string, role: 'caretaker' | 'guardian'): 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}/invitations/${invitationId}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}`, }, body: JSON.stringify({ role }), }); const data = await response.json(); if (response.ok) { return { data, ok: true }; } return { ok: false, error: { message: data.error || 'Failed to update invitation' }, }; } catch (error) { return { ok: false, error: { message: 'Network error. Please check your connection.' }, }; } } // ==================== NOTIFICATION SETTINGS ==================== // Get notification settings for current user async getNotificationSettings(): 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}/notification-settings`, { method: 'GET', headers: { 'Authorization': `Bearer ${token}`, }, }); const data = await response.json(); if (response.ok) { return { data: data.settings, ok: true }; } return { ok: false, error: { message: data.error || 'Failed to get notification settings' }, }; } catch (error) { return { ok: false, error: { message: 'Network error. Please check your connection.' }, }; } } // Update notification settings for current user async updateNotificationSettings(settings: Partial): 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}/notification-settings`, { method: 'PATCH', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}`, }, body: JSON.stringify(settings), }); const data = await response.json(); if (response.ok) { return { data: data.settings, ok: true }; } return { ok: false, error: { message: data.error || 'Failed to update notification settings' }, }; } catch (error) { return { ok: false, error: { message: 'Network error. Please check your connection.' }, }; } } // ========================================== // Legacy Dashboard Methods (Developer Mode) // For eluxnetworks.net dashboard WebView // ========================================== // Demo credentials for legacy dashboard private readonly DEMO_LEGACY_USER = 'anandk'; private readonly DEMO_LEGACY_PASSWORD = 'anandk_8'; private readonly DEMO_DEPLOYMENT_ID = 21; // Ferdinand's deployment // 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(); // Check that access_token is a valid JWT string (not 0 or empty) if (data.status === '200 OK' && data.access_token && typeof data.access_token === 'string' && data.access_token.includes('.')) { // Save legacy credentials await SecureStore.setItemAsync('legacyAccessToken', data.access_token); await SecureStore.setItemAsync('legacyUserId', String(data.user_id)); await SecureStore.setItemAsync('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' }, }; } } // Refresh legacy token async refreshLegacyToken(): Promise> { return this.loginToLegacyDashboard(); } // Check if legacy token is expiring soon (within 1 hour) async isLegacyTokenExpiringSoon(): Promise { try { const token = await this.getLegacyToken(); if (!token) return true; // Decode JWT to get expiration const parts = token.split('.'); if (parts.length !== 3) return true; const payload = JSON.parse(atob(parts[1])); const exp = payload.exp; if (!exp) return true; const now = Math.floor(Date.now() / 1000); const oneHour = 60 * 60; const isExpiring = (exp - now) < oneHour; return isExpiring; } catch (e) { return true; } } // Get legacy credentials for WebView injection async getLegacyWebViewCredentials(): Promise<{ token: string; userName: string; userId: string; } | null> { try { const token = await SecureStore.getItemAsync('legacyAccessToken'); const userName = await SecureStore.getItemAsync('legacyUserName'); const userId = await SecureStore.getItemAsync('legacyUserId'); // Check if credentials exist AND token is valid JWT (contains dots) const isValidToken = token && typeof token === 'string' && token.includes('.'); if (!isValidToken || !userName || !userId) { // Clear any invalid cached credentials if (token && !isValidToken) { await SecureStore.deleteItemAsync('legacyAccessToken'); await SecureStore.deleteItemAsync('legacyUserName'); await SecureStore.deleteItemAsync('legacyUserId'); } const loginResult = await this.loginToLegacyDashboard(); if (!loginResult.ok) return null; // Get freshly saved credentials const newToken = await SecureStore.getItemAsync('legacyAccessToken'); const newUserName = await SecureStore.getItemAsync('legacyUserName'); const newUserId = await SecureStore.getItemAsync('legacyUserId'); if (!newToken || !newUserName || !newUserId) return null; return { token: newToken, userName: newUserName, userId: newUserId }; } return { token, userName, userId }; } catch (e) { return null; } } // Get demo deployment ID getDemoDeploymentId(): number { return this.DEMO_DEPLOYMENT_ID; } /** * Get Legacy API credentials for device operations * Uses the same credentials as getLegacyWebViewCredentials but returns only what's needed */ async getLegacyCredentials(): Promise<{ userName: string; token: string } | null> { const creds = await this.getLegacyWebViewCredentials(); if (!creds) return null; return { userName: creds.userName, token: creds.token }; } // ============================================================================ // WP SENSORS / DEVICES MANAGEMENT // ============================================================================ /** * Get all devices for a beneficiary * Returns WP sensors with online/offline status */ async getDevicesForBeneficiary(beneficiaryId: string) { try { // Get auth token for WellNuo API const token = await this.getToken(); if (!token) return { ok: false, error: 'Not authenticated' }; // Get beneficiary's deployment_id from PostgreSQL const response = await fetch(`${this.baseUrl}/me/beneficiaries/${beneficiaryId}`, { method: 'GET', headers: { 'Authorization': `Bearer ${token}`, }, }); if (!response.ok) throw new Error('Failed to get beneficiary'); const beneficiary = await response.json(); const deploymentId = beneficiary.deploymentId; if (!deploymentId) { return { ok: true, data: [] }; // No deployment = no devices } // Get Legacy API credentials const creds = await this.getLegacyCredentials(); if (!creds) return { ok: false, error: 'Not authenticated with Legacy API' }; // Get devices from Legacy API const formData = new URLSearchParams({ function: 'device_list_by_deployment', user_name: creds.userName, token: creds.token, deployment_id: deploymentId.toString(), first: '0', last: '100', }); const devicesResponse = await fetch(this.legacyApiUrl, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: formData.toString(), }); if (!devicesResponse.ok) { throw new Error('Failed to fetch devices from Legacy API'); } const devicesData = await devicesResponse.json(); if (!devicesData.result_list || devicesData.result_list.length === 0) { return { ok: true, data: [] }; } // Get online status const onlineDevices = await this.getOnlineDevices(deploymentId); // Transform to WPSensor format with status calculation const sensors: WPSensor[] = devicesData.result_list.map((device: any) => { const [deviceId, wellId, mac, lastSeenTimestamp, location, description] = device; const lastSeen = new Date(lastSeenTimestamp * 1000); // Calculate status based on lastSeen time const now = new Date(); const diffMinutes = (now.getTime() - lastSeen.getTime()) / (1000 * 60); let status: 'online' | 'warning' | 'offline'; if (diffMinutes < 5) { status = 'online'; // 🟢 Fresh data } else if (diffMinutes < 60) { status = 'warning'; // 🟡 Might be issue } else { status = 'offline'; // 🔴 Definitely problem } // Convert numeric location code to string ID if needed let locationId = ''; if (location) { const numericLocation = parseInt(location, 10); if (!isNaN(numericLocation)) { // It's a numeric code from Legacy API - convert to our ID locationId = getLocationIdFromCode(numericLocation) || ''; } else { // It's already a string (legacy data or custom location) locationId = location; } } return { deviceId: deviceId.toString(), wellId: parseInt(wellId, 10), mac: mac, name: `WP_${wellId}_${mac.slice(-6).toLowerCase()}`, status: status, lastSeen: lastSeen, location: locationId, description: description || '', beneficiaryId: beneficiaryId, deploymentId: deploymentId, source: 'api', // From API = attached to beneficiary }; }); return { ok: true, data: sensors }; } catch (error: any) { return { ok: false, error: error.message }; } } /** * Get online devices for a deployment (using fresh=true) * Returns Set of device_ids that are online */ private async getOnlineDevices(deploymentId: number): Promise> { try { const creds = await this.getLegacyCredentials(); if (!creds) return new Set(); const formData = new URLSearchParams({ function: 'request_devices', user_name: creds.userName, token: creds.token, deployment_id: deploymentId.toString(), group_id: 'All', location: 'All', fresh: 'true', // Only online devices }); const response = await fetch(this.legacyApiUrl, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: formData.toString(), }); if (!response.ok) return new Set(); const data = await response.json(); if (!data.result_list) return new Set(); // Extract device_ids from result const deviceIds = data.result_list.map((device: any) => device[0]); return new Set(deviceIds); } catch (error) { return new Set(); } } /** * Attach device to beneficiary's deployment */ async attachDeviceToBeneficiary( beneficiaryId: string, wellId: number, ssid: string, password: string ) { try { // Get auth token for WellNuo API const token = await this.getToken(); if (!token) throw new Error('Not authenticated'); // Get beneficiary details const response = await fetch(`${this.baseUrl}/me/beneficiaries/${beneficiaryId}`, { method: 'GET', headers: { 'Authorization': `Bearer ${token}`, }, }); if (!response.ok) throw new Error('Failed to get beneficiary'); const beneficiary = await response.json(); const deploymentId = beneficiary.deploymentId; if (!deploymentId) { throw new Error('Beneficiary has no deployment'); } const creds = await this.getLegacyCredentials(); if (!creds) throw new Error('Not authenticated with Legacy API'); // Call set_deployment to attach device const formData = new URLSearchParams({ function: 'set_deployment', user_name: creds.userName, token: creds.token, deployment: deploymentId.toString(), devices: JSON.stringify([wellId]), wifis: JSON.stringify([`${ssid}|${password}`]), reuse_existing_devices: '1', }); const attachResponse = await fetch(this.legacyApiUrl, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: formData.toString(), }); if (!attachResponse.ok) { throw new Error('Failed to attach device'); } const data = await attachResponse.json(); if (data.status !== '200 OK') { throw new Error(data.message || 'Failed to attach device'); } return { ok: true }; } catch (error: any) { return { ok: false, error: error.message }; } } /** * Update device metadata (location, description) in Legacy API * Uses device_form endpoint */ async updateDeviceMetadata( deviceId: string, updates: { location?: string; // Location ID (e.g., 'bedroom', 'kitchen') - will be converted to Legacy API code description?: string; } ): Promise> { try { const creds = await this.getLegacyWebViewCredentials(); if (!creds) { return { ok: false, error: { message: 'Not authenticated with Legacy API', code: 'UNAUTHORIZED' } }; } const formData = new URLSearchParams({ function: 'device_form', user_name: creds.userName, token: creds.token, device_id: deviceId, }); // Add optional fields if provided // Location must be converted from ID to Legacy API numeric code if (updates.location !== undefined) { const legacyCode = getLocationLegacyCode(updates.location); if (legacyCode !== undefined) { formData.append('location', legacyCode.toString()); } else { // If location ID not found, log warning but don't fail console.warn(`Unknown location ID: ${updates.location}, skipping location update`); } } if (updates.description !== undefined) { formData.append('description', updates.description); } const response = await fetch(API_BASE_URL, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: formData.toString(), }); if (!response.ok) { return { ok: false, error: { message: 'Failed to update device' } }; } const data = await response.json(); if (data.status !== '200 OK') { return { ok: false, error: { message: data.message || 'Failed to update device' } }; } return { ok: true, data: { success: true } }; } catch (error: any) { return { ok: false, error: { message: error.message || 'Network error', code: 'NETWORK_ERROR' } }; } } /** * Attach device to deployment via Legacy API * Uses set_deployment endpoint to link a WP sensor to a beneficiary's deployment * * @param deploymentId - The deployment ID to attach the device to * @param wellId - The device's well_id (from BLE scan, e.g., 497) * @param ssid - WiFi network SSID * @param password - WiFi network password */ async attachDeviceToDeployment( deploymentId: number, wellId: number, ssid: string, password: string ): Promise> { try { const creds = await this.getLegacyWebViewCredentials(); if (!creds) { return { ok: false, error: { message: 'Not authenticated with Legacy API', code: 'UNAUTHORIZED' } }; } // Call set_deployment to attach device const formData = new URLSearchParams({ function: 'set_deployment', user_name: creds.userName, token: creds.token, deployment: deploymentId.toString(), devices: JSON.stringify([wellId]), wifis: JSON.stringify([`${ssid}|${password}`]), reuse_existing_devices: '1', }); const response = await fetch(API_BASE_URL, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: formData.toString(), }); if (!response.ok) { return { ok: false, error: { message: 'Failed to attach device to deployment' } }; } const data = await response.json(); if (data.status !== '200 OK') { return { ok: false, error: { message: data.message || 'Failed to attach device' } }; } return { ok: true, data: { success: true } }; } catch (error: any) { return { ok: false, error: { message: error.message || 'Network error', code: 'NETWORK_ERROR' } }; } } /** * Detach device from beneficiary */ async detachDeviceFromBeneficiary(beneficiaryId: string, deviceId: string) { try { const creds = await this.getLegacyCredentials(); if (!creds) throw new Error('Not authenticated with Legacy API'); // Set device's deployment to 0 (unassigned) const formData = new URLSearchParams({ function: 'device_form', user_name: creds.userName, token: creds.token, device_id: deviceId, deployment_id: '0', // Unassign }); const response = await fetch(this.legacyApiUrl, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: formData.toString(), }); if (!response.ok) throw new Error('Failed to detach device'); return { ok: true }; } catch (error: any) { return { ok: false, error: error.message }; } } } export const api = new ApiService();