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.
202 lines
5.7 KiB
TypeScript
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;
|