WellNuo/services/NavigationController.ts
Sergei d453126c89 feat: Room location picker + robster credentials
- Backend: Update Legacy API credentials to robster/rob2
- Frontend: ROOM_LOCATIONS with icons and legacyCode mapping
- Device Settings: Modal picker for room selection
- api.ts: Bidirectional conversion (code ↔ name)
- Various UI/UX improvements across screens

PRD-DEPLOYMENT.md completed (Score: 9/10)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-24 15:22:40 -08:00

387 lines
10 KiB
TypeScript

/**
* NavigationController - Centralized navigation logic for WellNuo app
*
* This service provides all routing decisions based on user state,
* beneficiary data, and equipment status.
*
* USAGE:
* import { NavigationController } from '@/services/NavigationController';
* const { path, params } = NavigationController.getRouteAfterLogin(profile, beneficiaries);
* router.replace(path);
*/
import type { Beneficiary, EquipmentStatus } from '@/types';
// ==================== ROUTE CONSTANTS ====================
export const ROUTES = {
// Auth Flow
AUTH: {
LOGIN: '/(auth)/login',
VERIFY_OTP: '/(auth)/verify-otp',
ENTER_NAME: '/(auth)/enter-name',
ADD_LOVED_ONE: '/(auth)/add-loved-one',
PURCHASE: '/(auth)/purchase',
ACTIVATE: '/(auth)/activate',
WELCOME_BACK: '/(auth)/welcome-back',
COMPLETE_PROFILE: '/(auth)/complete-profile',
},
// Main Tabs
TABS: {
DASHBOARD: '/(tabs)/dashboard',
BENEFICIARIES: '/(tabs)', // index.tsx shows beneficiary list
CHAT: '/(tabs)/chat',
VOICE: '/(tabs)/voice',
PROFILE: '/(tabs)/profile',
},
// Beneficiary Management
BENEFICIARY: {
ADD: '/(tabs)/beneficiaries/add',
DETAIL: (id: number) => `/(tabs)/beneficiaries/${id}` as const,
SUBSCRIPTION: (id: number) => `/(tabs)/beneficiaries/${id}/subscription` as const,
EQUIPMENT: (id: number) => `/(tabs)/beneficiaries/${id}/equipment` as const,
SHARE: (id: number) => `/(tabs)/beneficiaries/${id}/share` as const,
},
// Profile
PROFILE: {
INDEX: '/(tabs)/profile',
EDIT: '/(tabs)/profile/edit',
NOTIFICATIONS: '/(tabs)/profile/notifications',
LANGUAGE: '/(tabs)/profile/language',
PRIVACY: '/(tabs)/profile/privacy',
TERMS: '/(tabs)/profile/terms',
SUPPORT: '/(tabs)/profile/support',
ABOUT: '/(tabs)/profile/about',
HELP: '/(tabs)/profile/help',
},
} as const;
// ==================== TYPE DEFINITIONS ====================
export interface NavigationResult {
path: string;
params?: Record<string, string | number | boolean>;
}
export interface UserProfile {
id: number;
email: string;
firstName: string | null;
lastName: string | null;
phone: string | null;
}
// ==================== NAVIGATION CONTROLLER ====================
export const NavigationController = {
/**
* Determine where to navigate after successful OTP verification
*
* Flow:
* 1. New user without name → enter-name
* 2. User without beneficiaries → add-loved-one
* 3. User has beneficiary without devices → check hasDevices
* - hasDevices=false → purchase (need to buy equipment)
* - hasDevices=true but equipment not activated → activate
* 4. User has active beneficiary → dashboard
*/
getRouteAfterLogin(
profile: UserProfile | null,
beneficiaries: Beneficiary[]
): NavigationResult {
// Step 1: Check if user has name
if (!profile?.firstName) {
return {
path: ROUTES.AUTH.ENTER_NAME,
};
}
// Step 2: Check if user has any beneficiaries
if (!beneficiaries || beneficiaries.length === 0) {
return {
path: ROUTES.AUTH.ADD_LOVED_ONE,
};
}
// Step 3: Multiple beneficiaries - go to list, let user choose
if (beneficiaries.length > 1) {
return {
path: ROUTES.TABS.BENEFICIARIES,
};
}
// Step 4: Single beneficiary - check if needs setup
const singleBeneficiary = beneficiaries[0];
const isActive = singleBeneficiary.hasDevices ||
singleBeneficiary.equipmentStatus === 'active' ||
singleBeneficiary.equipmentStatus === 'demo';
if (isActive) {
// Single active beneficiary - go to dashboard
return {
path: ROUTES.TABS.DASHBOARD,
};
}
// Single beneficiary needs setup
return this.getRouteForBeneficiarySetup(singleBeneficiary);
},
/**
* Determine where to navigate for beneficiary equipment setup
*/
getRouteForBeneficiarySetup(beneficiary: Beneficiary): NavigationResult {
const status = beneficiary.equipmentStatus || 'none';
switch (status) {
case 'none':
// No equipment ordered - go to purchase
return {
path: ROUTES.AUTH.PURCHASE,
params: { beneficiaryId: beneficiary.id },
};
case 'ordered':
case 'shipped':
// Equipment is on the way - can show tracking or wait screen
// For now, go to equipment status page
return {
path: ROUTES.BENEFICIARY.EQUIPMENT(beneficiary.id),
params: { beneficiaryId: beneficiary.id },
};
case 'delivered':
// Equipment delivered, needs activation
return {
path: ROUTES.AUTH.ACTIVATE,
params: { beneficiaryId: beneficiary.id },
};
case 'active':
case 'demo':
// Already active, go to dashboard
return {
path: ROUTES.TABS.DASHBOARD,
};
default:
// Unknown status - go to dashboard
return {
path: ROUTES.TABS.DASHBOARD,
};
}
},
/**
* After creating a new beneficiary, determine next step
*
* @param beneficiaryId - ID of newly created beneficiary
* @param hasExistingDevices - User indicated they already have WellNuo devices
*/
getRouteAfterAddBeneficiary(
beneficiaryId: number,
hasExistingDevices: boolean
): NavigationResult {
if (hasExistingDevices) {
// User has existing devices - go directly to activation
return {
path: ROUTES.AUTH.ACTIVATE,
params: { beneficiaryId },
};
}
// User needs to purchase equipment
return {
path: ROUTES.AUTH.PURCHASE,
params: { beneficiaryId },
};
},
/**
* After successful purchase, navigate to next step
*/
getRouteAfterPurchase(
beneficiaryId: number,
purchaseResult: { skipToActivate?: boolean; demo?: boolean }
): NavigationResult {
if (purchaseResult.demo || purchaseResult.skipToActivate) {
// Demo mode or skip - go to activate
return {
path: ROUTES.AUTH.ACTIVATE,
params: purchaseResult.demo
? { beneficiaryId, demo: purchaseResult.demo }
: { beneficiaryId },
};
}
// Normal purchase - wait for equipment delivery
// Go to equipment tracking page
return {
path: ROUTES.BENEFICIARY.EQUIPMENT(beneficiaryId),
params: { beneficiaryId },
};
},
/**
* After successful activation, navigate to beneficiary detail page
*/
getRouteAfterActivation(beneficiaryId: number): NavigationResult {
return {
path: ROUTES.BENEFICIARY.DETAIL(beneficiaryId),
params: { justActivated: true },
};
},
/**
* Get route for returning user (already has account)
*
* Used when user logs in with existing account
*/
getRouteForReturningUser(
profile: UserProfile,
beneficiaries: Beneficiary[]
): NavigationResult {
// Same logic as after login, but could add welcome-back screen
return this.getRouteAfterLogin(profile, beneficiaries);
},
/**
* Check if user should see onboarding
*/
shouldShowOnboarding(
isNewUser: boolean,
profile: UserProfile | null
): boolean {
return isNewUser || !profile?.firstName;
},
/**
* Get route for a specific beneficiary action
*/
getBeneficiaryRoute(
beneficiary: Beneficiary,
action: 'view' | 'subscription' | 'equipment' | 'share'
): NavigationResult {
switch (action) {
case 'view':
return { path: ROUTES.BENEFICIARY.DETAIL(beneficiary.id) };
case 'subscription':
return { path: ROUTES.BENEFICIARY.SUBSCRIPTION(beneficiary.id) };
case 'equipment':
return { path: ROUTES.BENEFICIARY.EQUIPMENT(beneficiary.id) };
case 'share':
return { path: ROUTES.BENEFICIARY.SHARE(beneficiary.id) };
default:
return { path: ROUTES.BENEFICIARY.DETAIL(beneficiary.id) };
}
},
/**
* Determine if beneficiary card should show "Setup Equipment" button
*/
shouldShowEquipmentSetup(beneficiary: Beneficiary): boolean {
return !beneficiary.hasDevices &&
beneficiary.equipmentStatus !== 'active' &&
beneficiary.equipmentStatus !== 'demo';
},
/**
* Get call-to-action for beneficiary based on status
*/
getBeneficiaryCallToAction(
beneficiary: Beneficiary
): { label: string; action: () => NavigationResult } | null {
const status = beneficiary.equipmentStatus || 'none';
if (beneficiary.hasDevices || status === 'active' || status === 'demo') {
// No CTA needed - beneficiary is active
return null;
}
switch (status) {
case 'none':
return {
label: 'Setup Equipment',
action: () => ({
path: ROUTES.AUTH.PURCHASE,
params: { beneficiaryId: beneficiary.id },
}),
};
case 'ordered':
return {
label: 'Track Delivery',
action: () => ({
path: ROUTES.BENEFICIARY.EQUIPMENT(beneficiary.id),
}),
};
case 'shipped':
return {
label: 'Track Package',
action: () => ({
path: ROUTES.BENEFICIARY.EQUIPMENT(beneficiary.id),
}),
};
case 'delivered':
return {
label: 'Activate Equipment',
action: () => ({
path: ROUTES.AUTH.ACTIVATE,
params: { beneficiaryId: beneficiary.id },
}),
};
default:
return null;
}
},
/**
* Get status text for equipment
*/
getEquipmentStatusText(status: EquipmentStatus | undefined): string {
switch (status) {
case 'none':
return 'No equipment';
case 'ordered':
return 'Equipment ordered';
case 'shipped':
return 'In transit';
case 'delivered':
return 'Ready to activate';
case 'active':
return 'Active';
case 'demo':
return 'Demo mode';
default:
return 'Unknown';
}
},
/**
* Get status color for equipment
*/
getEquipmentStatusColor(status: EquipmentStatus | undefined): string {
switch (status) {
case 'none':
return '#888888'; // gray
case 'ordered':
return '#FFA500'; // orange
case 'shipped':
return '#007AFF'; // blue
case 'delivered':
return '#34C759'; // green
case 'active':
return '#34C759'; // green
case 'demo':
return '#9C27B0'; // purple
default:
return '#888888'; // gray
}
},
};
export default NavigationController;