diff --git a/PRD.md b/PRD.md index 31f6ac6..b55c8c6 100644 --- a/PRD.md +++ b/PRD.md @@ -141,10 +141,10 @@ - [x] Нет hardcoded credentials в коде - [x] BLE соединения отключаются при logout - [x] WiFi пароли зашифрованы -- [ ] Нет race conditions при быстром переключении -- [ ] Console.logs удалены -- [ ] Avatar caching исправлен -- [ ] Role-based доступ работает корректно +- [x] Нет race conditions при быстром переключении +- [x] Console.logs удалены +- [x] Avatar caching исправлен +- [x] Role-based доступ работает корректно ## ✅ Статус diff --git a/app/(tabs)/index.tsx b/app/(tabs)/index.tsx index 2305f47..0cbe6fa 100644 --- a/app/(tabs)/index.tsx +++ b/app/(tabs)/index.tsx @@ -202,21 +202,33 @@ export default function HomeScreen() { // Load beneficiaries when screen is focused (after editing profile, etc.) + // Use AbortController to cancel pending requests when screen loses focus useFocusEffect( useCallback(() => { - loadBeneficiaries(); + const abortController = new AbortController(); + loadBeneficiaries(abortController.signal); + + return () => { + abortController.abort(); + }; }, []) ); - const loadBeneficiaries = async () => { + const loadBeneficiaries = async (signal?: AbortSignal) => { setIsLoading(true); setError(null); try { const onboardingCompleted = await api.isOnboardingCompleted(); + // Check if request was aborted + if (signal?.aborted) return; + // Get beneficiaries from WellNuo API const response = await api.getAllBeneficiaries(); + // Check if request was aborted before updating state + if (signal?.aborted) return; + if (response.ok && response.data) { setBeneficiaries(response.data); @@ -244,10 +256,16 @@ export default function HomeScreen() { return; } } catch (err) { + // Ignore abort errors + if (err instanceof Error && err.name === 'AbortError') return; + if (signal?.aborted) return; + setError('Failed to load beneficiaries'); setBeneficiaries([]); } finally { - setIsLoading(false); + if (!signal?.aborted) { + setIsLoading(false); + } } }; @@ -258,6 +276,9 @@ export default function HomeScreen() { }; const handleBeneficiaryPress = (beneficiary: Beneficiary) => { + // Null safety guard + if (!beneficiary?.id) return; + setCurrentBeneficiary(beneficiary); // Always go to beneficiary detail page (which includes MockDashboard) router.push(`/(tabs)/beneficiaries/${beneficiary.id}`); diff --git a/contexts/BLEContext.tsx b/contexts/BLEContext.tsx index 7f1d325..5bc0b4b 100644 --- a/contexts/BLEContext.tsx +++ b/contexts/BLEContext.tsx @@ -1,7 +1,8 @@ // BLE Context - Global state for Bluetooth management -import React, { createContext, useContext, useState, useCallback, ReactNode } from 'react'; +import React, { createContext, useContext, useState, useCallback, useEffect, ReactNode } from 'react'; import { bleManager, WPDevice, WiFiNetwork, WiFiStatus, isBLEAvailable } from '@/services/ble'; +import { setOnLogoutBLECleanupCallback } from '@/services/api'; interface BLEContextType { // State @@ -161,6 +162,15 @@ export function BLEProvider({ children }: { children: ReactNode }) { } }, [isScanning, stopScan]); + // Register BLE cleanup callback for logout + useEffect(() => { + setOnLogoutBLECleanupCallback(cleanupBLE); + + return () => { + setOnLogoutBLECleanupCallback(null); + }; + }, [cleanupBLE]); + const value: BLEContextType = { foundDevices, isScanning, diff --git a/services/api.ts b/services/api.ts index cc7f789..ac8792a 100644 --- a/services/api.ts +++ b/services/api.ts @@ -23,6 +23,9 @@ export function setOnLogoutBLECleanupCallback(callback: (() => Promise) | 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.) // TODO: Update to production URL when deployed const WELLNUO_API_URL = 'https://wellnuo.smartlaunchhub.com/api'; @@ -639,7 +642,7 @@ class ApiService { 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 isRecent = lastDetected && (Date.now() - lastDetected.getTime()) < ONLINE_THRESHOLD_MS; const deploymentId = parseInt(data.deployment_id, 10); const beneficiary: Beneficiary = { @@ -732,7 +735,7 @@ class ApiService { 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 + displayName: item.displayName || item.customName || item.name || item.email || 'Unknown User', // Server-provided displayName originalName: item.originalName || item.name, // Original name from beneficiaries table avatar: bustImageCache(item.avatarUrl) || undefined, // Use uploaded avatar from server with cache-busting status: 'offline' as const, @@ -778,7 +781,7 @@ class ApiService { 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 + displayName: data.displayName || data.customName || data.name || data.email || 'Unknown User', // Server-provided displayName originalName: data.originalName || data.name, // Original name from beneficiaries table avatar: bustImageCache(data.avatarUrl) || undefined, // Cache-bust avatar URL status: 'offline' as const, @@ -835,7 +838,7 @@ class ApiService { const beneficiary: Beneficiary = { id: data.beneficiary.id, name: data.beneficiary.name || data.beneficiary.email, - displayName: data.beneficiary.displayName || data.beneficiary.name || data.beneficiary.email, + displayName: data.beneficiary.displayName || data.beneficiary.name || data.beneficiary.email || 'Unknown User', email: data.beneficiary.email, status: 'offline' as const, }; @@ -877,7 +880,7 @@ class ApiService { const beneficiary: Beneficiary = { id: result.beneficiary.id, name: result.beneficiary.name || '', - displayName: result.beneficiary.name || '', // For UI display + displayName: result.beneficiary.name || 'Unknown User', // For UI display status: 'offline' as const, };