WellNuo/hooks/useNavigationFlow.ts
Sergei deddd3d5bc Add comprehensive null safety to navigation system
Implemented null/undefined handling throughout NavigationController
and useNavigationFlow hook to prevent crashes from invalid data:

- Added null checks for all profile and beneficiary parameters
- Validated beneficiary IDs before navigation (type and value checks)
- Added fallback routes when data is invalid or missing
- Implemented safe navigation with error handling and logging
- Added defensive guards for optional purchaseResult parameter

Key improvements:
- getRouteAfterLogin: handles null profile, null beneficiaries, invalid IDs
- getRouteForBeneficiarySetup: validates beneficiary exists before routing
- getRouteAfterAddBeneficiary: validates beneficiary ID type and value
- getRouteAfterPurchase: handles null purchaseResult safely
- getBeneficiaryRoute: returns fallback route for invalid beneficiaries
- navigate hook: wraps router calls in try-catch with validation

All methods now gracefully handle edge cases without crashing,
logging warnings for debugging while maintaining UX flow.

Tests included for all null/undefined scenarios.
2026-01-29 12:05:29 -08:00

202 lines
5.7 KiB
TypeScript

/**
* useNavigationFlow - React hook for centralized navigation
*
* This hook wraps NavigationController and provides easy-to-use
* navigation methods for components.
*
* USAGE:
* const nav = useNavigationFlow();
* nav.navigateAfterLogin(profile, beneficiaries);
* nav.navigateAfterAddBeneficiary(42, false);
*/
import { useCallback } from 'react';
import { useRouter } from 'expo-router';
import { NavigationController, ROUTES, type NavigationResult, type UserProfile } from '@/services/NavigationController';
import type { Beneficiary } from '@/types';
export function useNavigationFlow() {
const router = useRouter();
/**
* Execute navigation based on NavigationResult
*/
const navigate = useCallback((result: NavigationResult | null | undefined, replace = false) => {
// Null safety: validate navigation result
if (!result || !result.path) {
console.warn('[useNavigationFlow] Invalid navigation result:', result);
return;
}
const href = result.params
? { pathname: result.path, params: result.params }
: result.path;
try {
if (replace) {
router.replace(href as any);
} else {
router.push(href as any);
}
} catch (error) {
console.error('[useNavigationFlow] Navigation error:', error);
}
}, [router]);
/**
* Navigate after successful login/OTP verification
*/
const navigateAfterLogin = useCallback((
profile: UserProfile | null | undefined,
beneficiaries: Beneficiary[] | null | undefined
) => {
const result = NavigationController.getRouteAfterLogin(profile, beneficiaries);
navigate(result, true); // replace to prevent going back to login
}, [navigate]);
/**
* Navigate after creating a new beneficiary
*/
const navigateAfterAddBeneficiary = useCallback((
beneficiaryId: number | null | undefined,
hasExistingDevices: boolean
) => {
const result = NavigationController.getRouteAfterAddBeneficiary(
beneficiaryId,
hasExistingDevices
);
navigate(result);
}, [navigate]);
/**
* Navigate after purchase completion
*/
const navigateAfterPurchase = useCallback((
beneficiaryId: number | null | undefined,
options: { skipToActivate?: boolean; demo?: boolean } | null | undefined = {}
) => {
const result = NavigationController.getRouteAfterPurchase(beneficiaryId, options);
navigate(result);
}, [navigate]);
/**
* Navigate after device activation
*/
const navigateAfterActivation = useCallback((beneficiaryId: number | null | undefined) => {
const result = NavigationController.getRouteAfterActivation(beneficiaryId);
navigate(result, true); // replace to prevent going back
}, [navigate]);
/**
* Navigate to beneficiary-specific screens
*/
const navigateToBeneficiary = useCallback((
beneficiary: Beneficiary | null | undefined,
action: 'view' | 'subscription' | 'equipment' | 'share' = 'view'
) => {
const result = NavigationController.getBeneficiaryRoute(beneficiary, action);
navigate(result);
}, [navigate]);
/**
* Navigate to beneficiary setup flow (purchase or activate)
*/
const navigateToBeneficiarySetup = useCallback((beneficiary: Beneficiary | null | undefined) => {
const result = NavigationController.getRouteForBeneficiarySetup(beneficiary);
navigate(result);
}, [navigate]);
/**
* Navigate to specific route
*/
const goTo = useCallback((
path: string,
params?: Record<string, string | number | boolean>,
replace = false
) => {
navigate({ path, params }, replace);
}, [navigate]);
/**
* Quick navigation shortcuts
*/
const goToDashboard = useCallback(() => {
navigate({ path: ROUTES.TABS.DASHBOARD }, true);
}, [navigate]);
const goToLogin = useCallback(() => {
navigate({ path: ROUTES.AUTH.LOGIN }, true);
}, [navigate]);
const goToAddBeneficiary = useCallback(() => {
navigate({ path: ROUTES.AUTH.ADD_LOVED_ONE });
}, [navigate]);
const goToPurchase = useCallback((beneficiaryId: number | null | undefined) => {
// Null safety: only navigate if beneficiaryId is valid
if (!beneficiaryId || typeof beneficiaryId !== 'number') {
console.warn('[useNavigationFlow] Invalid beneficiaryId for purchase:', beneficiaryId);
return;
}
navigate({
path: ROUTES.AUTH.PURCHASE,
params: { beneficiaryId },
});
}, [navigate]);
const goToActivate = useCallback((beneficiaryId: number | null | undefined, demo?: boolean) => {
// Null safety: only navigate if beneficiaryId is valid
if (!beneficiaryId || typeof beneficiaryId !== 'number') {
console.warn('[useNavigationFlow] Invalid beneficiaryId for activation:', beneficiaryId);
return;
}
navigate({
path: ROUTES.AUTH.ACTIVATE,
params: demo !== undefined ? { beneficiaryId, demo } : { beneficiaryId },
});
}, [navigate]);
const goToProfile = useCallback(() => {
navigate({ path: ROUTES.TABS.PROFILE });
}, [navigate]);
const goToEditProfile = useCallback(() => {
navigate({ path: ROUTES.PROFILE.EDIT });
}, [navigate]);
return {
// Core navigation methods
navigate,
goTo,
// Flow-based navigation
navigateAfterLogin,
navigateAfterAddBeneficiary,
navigateAfterPurchase,
navigateAfterActivation,
// Beneficiary navigation
navigateToBeneficiary,
navigateToBeneficiarySetup,
// Quick shortcuts
goToDashboard,
goToLogin,
goToAddBeneficiary,
goToPurchase,
goToActivate,
goToProfile,
goToEditProfile,
// Direct access to routes
ROUTES,
// Direct access to controller for advanced use
controller: NavigationController,
};
}
export default useNavigationFlow;