- Refactor subscription page with simplified UI flow - Update Stripe routes and config for price handling - Improve AuthContext with better profile management - Fix equipment status and beneficiary screens - Update voice screen and profile pages - Simplify purchase flow
1192 lines
37 KiB
TypeScript
1192 lines
37 KiB
TypeScript
import type { ApiError, ApiResponse, AuthResponse, Beneficiary, BeneficiaryDashboardData, ChatResponse, DashboardSingleResponse, NotificationSettings } 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 {
|
|
// Public method to get the access token (used by AuthContext)
|
|
async getToken(): Promise<string | null> {
|
|
try {
|
|
return await SecureStore.getItemAsync('accessToken');
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// Get legacy API token (for eluxnetworks.net API - dashboard, voice_ask)
|
|
private async getLegacyToken(): Promise<string | null> {
|
|
try {
|
|
return await SecureStore.getItemAsync('legacyAccessToken');
|
|
} 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<T>(params: Record<string, string>): Promise<ApiResponse<T>> {
|
|
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<ApiResponse<AuthResponse>> {
|
|
const response = await this.makeRequest<AuthResponse>({
|
|
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<void> {
|
|
// 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');
|
|
}
|
|
|
|
// Save user email (for OTP auth flow)
|
|
async saveEmail(email: string): Promise<void> {
|
|
await SecureStore.setItemAsync('userEmail', email);
|
|
}
|
|
|
|
// Get stored email
|
|
async getStoredEmail(): Promise<string | null> {
|
|
try {
|
|
return await SecureStore.getItemAsync('userEmail');
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// Onboarding completion flag - persists across app restarts
|
|
async setOnboardingCompleted(completed: boolean): Promise<void> {
|
|
await SecureStore.setItemAsync('onboardingCompleted', completed ? '1' : '0');
|
|
}
|
|
|
|
async isOnboardingCompleted(): Promise<boolean> {
|
|
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<void> {
|
|
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<ApiResponse<{ exists: boolean; name?: string | null }>> {
|
|
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<ApiResponse<{ message: string }>> {
|
|
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<ApiResponse<{ token: string; user: { id: string; email: string; first_name?: string; last_name?: string } }>> {
|
|
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 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) {
|
|
console.error('[API] verifyOTP network error:', error);
|
|
return {
|
|
ok: false,
|
|
error: { message: 'Network error. Please check your connection.' },
|
|
};
|
|
}
|
|
}
|
|
|
|
async isAuthenticated(): Promise<boolean> {
|
|
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');
|
|
|
|
console.log('[API] getStoredUser: token exists =', !!token, ', userId =', userId);
|
|
|
|
if (!token || !userId) {
|
|
console.log('[API] getStoredUser: No token or userId, returning null');
|
|
return null;
|
|
}
|
|
|
|
// Fetch profile from server
|
|
console.log('[API] getStoredUser: Fetching profile from server...');
|
|
const profile = await this.getProfile();
|
|
console.log('[API] getStoredUser: Profile response ok =', profile.ok, ', error =', profile.error);
|
|
|
|
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') {
|
|
console.log('[API] getStoredUser: Token invalid (401), clearing auth data');
|
|
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
|
|
console.log('[API] getStoredUser: API error, falling back to local data');
|
|
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
|
|
console.log('[API] getStoredUser: Unexpected error, falling back to local data', error);
|
|
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
|
|
async getProfile(): Promise<ApiResponse<{
|
|
id: number;
|
|
email: string;
|
|
firstName: string | null;
|
|
lastName: string | null;
|
|
phone: string | null;
|
|
address: {
|
|
street: string | null;
|
|
city: string | null;
|
|
zip: string | null;
|
|
state: string | null;
|
|
country: string | null;
|
|
};
|
|
createdAt: string;
|
|
}>> {
|
|
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<ApiResponse<{ id: number; email: string; firstName: string | null; lastName: string | null; phone: string | null }>> {
|
|
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' } };
|
|
}
|
|
}
|
|
|
|
// Beneficiaries (elderly people being monitored)
|
|
async getBeneficiaries(): Promise<ApiResponse<{ beneficiaries: Beneficiary[] }>> {
|
|
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<ApiResponse<Beneficiary>> {
|
|
// 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 (LEGACY API - eluxnetworks.net)
|
|
async getBeneficiaryDashboard(deploymentId: string): Promise<ApiResponse<BeneficiaryDashboardData>> {
|
|
// 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<DashboardSingleResponse>({
|
|
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<ApiResponse<Beneficiary[]>> {
|
|
const token = await this.getToken();
|
|
console.log('[API] getAllBeneficiaries - token exists:', !!token, 'length:', token?.length);
|
|
|
|
if (!token) {
|
|
console.log('[API] getAllBeneficiaries - No token found!');
|
|
return { ok: false, error: { message: 'Not authenticated', code: 'UNAUTHORIZED' } };
|
|
}
|
|
|
|
try {
|
|
console.log('[API] getAllBeneficiaries - Fetching from:', `${WELLNUO_API_URL}/me/beneficiaries`);
|
|
const response = await fetch(`${WELLNUO_API_URL}/me/beneficiaries`, {
|
|
method: 'GET',
|
|
headers: {
|
|
'Authorization': `Bearer ${token}`,
|
|
},
|
|
});
|
|
|
|
console.log('[API] getAllBeneficiaries - Response status:', response.status);
|
|
const data = await response.json();
|
|
console.log('[API] getAllBeneficiaries - Data:', JSON.stringify(data).substring(0, 200));
|
|
|
|
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.name || item.email,
|
|
avatar: undefined, // No auto-generated avatars - only show if user uploaded one
|
|
status: 'offline' as const,
|
|
email: item.email,
|
|
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,
|
|
}));
|
|
|
|
return { data: beneficiaries, ok: true };
|
|
} catch (error) {
|
|
console.log('[API] getAllBeneficiaries - Catch error:', error);
|
|
return { ok: false, error: { message: 'Network error', code: 'NETWORK_ERROR' } };
|
|
}
|
|
}
|
|
|
|
// Get single beneficiary details from WellNuo API
|
|
async getWellNuoBeneficiary(id: number): Promise<ApiResponse<Beneficiary>> {
|
|
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();
|
|
|
|
console.log('[API] getWellNuoBeneficiary - Raw response:', JSON.stringify({
|
|
id: data.id,
|
|
name: data.name,
|
|
hasDevices: data.hasDevices,
|
|
equipmentStatus: data.equipmentStatus
|
|
}));
|
|
|
|
if (!response.ok) {
|
|
return { ok: false, error: { message: data.error || 'Failed to get beneficiary' } };
|
|
}
|
|
|
|
const beneficiary: Beneficiary = {
|
|
id: data.id,
|
|
name: data.name || data.email,
|
|
avatar: undefined, // No auto-generated avatars - only show if user uploaded one
|
|
status: 'offline' as const,
|
|
email: data.email,
|
|
address: data.address?.street ? `${data.address.street}, ${data.address.city}` : undefined,
|
|
subscription: data.subscription ? {
|
|
status: data.subscription.status,
|
|
plan: data.subscription.plan,
|
|
endDate: data.subscription.currentPeriodEnd,
|
|
} : undefined,
|
|
// Equipment status from orders
|
|
equipmentStatus: data.equipmentStatus,
|
|
hasDevices: data.hasDevices || false,
|
|
trackingNumber: data.trackingNumber,
|
|
};
|
|
|
|
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<ApiResponse<Beneficiary>> {
|
|
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,
|
|
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<ApiResponse<Beneficiary>> {
|
|
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 || '',
|
|
status: 'offline' as const,
|
|
};
|
|
|
|
return { data: beneficiary, ok: true };
|
|
} catch (error) {
|
|
return { ok: false, error: { message: 'Network error', code: 'NETWORK_ERROR' } };
|
|
}
|
|
}
|
|
|
|
// Delete beneficiary (removes access record)
|
|
async deleteBeneficiary(id: number): Promise<ApiResponse<{ success: 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}/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<ApiResponse<{ success: 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}/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' } };
|
|
}
|
|
}
|
|
|
|
// AI Chat - deploymentId is required, no default value for security (LEGACY API)
|
|
async sendMessage(question: string, deploymentId: string): Promise<ApiResponse<ChatResponse>> {
|
|
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<ChatResponse>({
|
|
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<ApiResponse<{ invitation: { id: string; status: string } }>> {
|
|
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<ApiResponse<{
|
|
invitations: Array<{
|
|
id: string;
|
|
email: string;
|
|
role: string;
|
|
label?: string;
|
|
status: string;
|
|
created_at: string;
|
|
}>
|
|
}>> {
|
|
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<ApiResponse<{ success: 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}/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.' },
|
|
};
|
|
}
|
|
}
|
|
|
|
// Activate equipment for beneficiary (saves to server)
|
|
async activateBeneficiary(beneficiaryId: number, serialNumber: string): Promise<ApiResponse<{
|
|
success: boolean;
|
|
beneficiary: {
|
|
id: number;
|
|
firstName: string | null;
|
|
lastName: string | null;
|
|
hasDevices: boolean;
|
|
equipmentStatus: string;
|
|
device_id: string;
|
|
};
|
|
}>> {
|
|
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<ApiResponse<{ success: boolean; cancelAt: string }>> {
|
|
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.' },
|
|
};
|
|
}
|
|
}
|
|
|
|
// Reactivate subscription that was set to cancel
|
|
async reactivateSubscription(beneficiaryId: number): Promise<ApiResponse<{ success: boolean; status: string }>> {
|
|
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<ApiResponse<{ success: boolean; invitation: { id: string; role: string; email: string } }>> {
|
|
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<ApiResponse<NotificationSettings>> {
|
|
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<NotificationSettings>): Promise<ApiResponse<NotificationSettings>> {
|
|
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.' },
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
export const api = new ApiService();
|