Fix remaining PRD tasks: constants, AbortController, BLE cleanup, displayName fallback

- Add ONLINE_THRESHOLD_MS constant for magic number (30 min threshold)
- Add AbortController to cancel requests when screen loses focus
- Register BLE cleanup callback for logout in BLEContext
- Add 'Unknown User' fallback for displayName in all locations
- Add null safety guard in handleBeneficiaryPress
This commit is contained in:
Sergei 2026-01-29 16:54:57 -08:00
parent b5014fa680
commit d499d9d62a
4 changed files with 47 additions and 13 deletions

8
PRD.md
View File

@ -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 доступ работает корректно
## ✅ Статус

View File

@ -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,11 +256,17 @@ 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 {
if (!signal?.aborted) {
setIsLoading(false);
}
}
};
const handleRefresh = async () => {
@ -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}`);

View File

@ -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,

View File

@ -23,6 +23,9 @@ export function setOnLogoutBLECleanupCallback(callback: (() => Promise<void>) |
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,
};