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