Update CBS Mobile App codebase

This commit is contained in:
Abin Toppo
2026-09-21 13:21:25 +05:30
parent 58af541536
commit 2dcb10775c
339 changed files with 26750 additions and 16767 deletions
+150 -131
View File
@@ -1,10 +1,8 @@
import React from 'react';
import { View, StyleSheet } from 'react-native';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import Ionicons from '@react-native-vector-icons/ionicons';
import { useTheme } from '../theme';
import { TabBarIcon, useTabBarScreenOptions } from './tabBarTheme';
import type {
AgentTabParamList,
AgentDashboardStackParamList,
@@ -13,6 +11,15 @@ import type {
AgentLoansStackParamList,
AgentProfileStackParamList,
} from '../modules/agent/navigation/agentNavigationTypes';
import {
AGENT_COLLECTIONS_ROUTE_FEATURES,
AGENT_CUSTOMERS_ROUTE_FEATURES,
AGENT_DASHBOARD_ROUTE_FEATURES,
AGENT_LOANS_ROUTE_FEATURES,
AGENT_PROFILE_ROUTE_FEATURES,
AGENT_TAB_FEATURES,
useRouteGuard,
} from './featureRoutes';
import AgentDashboardScreen from '../modules/agent/screens/AgentDashboardScreen';
import AgentRouteScreen from '../modules/agent/screens/AgentRouteScreen';
@@ -38,156 +45,168 @@ import AgentChangePasswordScreen from '../modules/agent/screens/AgentChangePassw
const Tab = createBottomTabNavigator<AgentTabParamList>();
const DashboardStack = createNativeStackNavigator<AgentDashboardStackParamList>();
const DashboardNavigator = () => (
<DashboardStack.Navigator screenOptions={{ headerShown: false }}>
<DashboardStack.Screen name="AgentDashboardMain" component={AgentDashboardScreen} />
<DashboardStack.Screen name="AgentRoute" component={AgentRouteScreen} />
</DashboardStack.Navigator>
);
const DashboardNavigator = () => {
const canRoute = useRouteGuard(AGENT_DASHBOARD_ROUTE_FEATURES);
return (
<DashboardStack.Navigator screenOptions={{ headerShown: false }}>
{canRoute('AgentDashboardMain') && (
<DashboardStack.Screen name="AgentDashboardMain" component={AgentDashboardScreen} />
)}
{canRoute('AgentRoute') && (
<DashboardStack.Screen name="AgentRoute" component={AgentRouteScreen} />
)}
</DashboardStack.Navigator>
);
};
const CustomersStack = createNativeStackNavigator<AgentCustomersStackParamList>();
const CustomersNavigator = () => (
<CustomersStack.Navigator screenOptions={{ headerShown: false }}>
<CustomersStack.Screen name="CustomerList" component={CustomerListScreen} />
<CustomersStack.Screen name="CustomerDetail" component={CustomerDetailScreen} />
<CustomersStack.Screen name="AddEditCustomer" component={AddEditCustomerScreen} />
<CustomersStack.Screen name="CustomerInstallmentHistory" component={CustomerInstallmentHistoryScreen} />
</CustomersStack.Navigator>
);
const CustomersNavigator = () => {
const canRoute = useRouteGuard(AGENT_CUSTOMERS_ROUTE_FEATURES);
return (
<CustomersStack.Navigator screenOptions={{ headerShown: false }}>
{canRoute('CustomerList') && (
<CustomersStack.Screen name="CustomerList" component={CustomerListScreen} />
)}
{canRoute('CustomerDetail') && (
<CustomersStack.Screen name="CustomerDetail" component={CustomerDetailScreen} />
)}
{canRoute('AddEditCustomer') && (
<CustomersStack.Screen name="AddEditCustomer" component={AddEditCustomerScreen} />
)}
{canRoute('CustomerInstallmentHistory') && (
<CustomersStack.Screen
name="CustomerInstallmentHistory"
component={CustomerInstallmentHistoryScreen}
/>
)}
</CustomersStack.Navigator>
);
};
const CollectionsStack = createNativeStackNavigator<AgentCollectionsStackParamList>();
const CollectionsNavigator = () => (
<CollectionsStack.Navigator screenOptions={{ headerShown: false }}>
<CollectionsStack.Screen name="DailyCollection" component={DailyCollectionScreen} />
<CollectionsStack.Screen name="CollectionHistory" component={CollectionHistoryScreen} />
<CollectionsStack.Screen name="CollectionReceipt" component={CollectionReceiptScreen} />
</CollectionsStack.Navigator>
);
const CollectionsNavigator = () => {
const canRoute = useRouteGuard(AGENT_COLLECTIONS_ROUTE_FEATURES);
return (
<CollectionsStack.Navigator screenOptions={{ headerShown: false }}>
{canRoute('DailyCollection') && (
<CollectionsStack.Screen name="DailyCollection" component={DailyCollectionScreen} />
)}
{canRoute('CollectionHistory') && (
<CollectionsStack.Screen name="CollectionHistory" component={CollectionHistoryScreen} />
)}
{canRoute('CollectionReceipt') && (
<CollectionsStack.Screen name="CollectionReceipt" component={CollectionReceiptScreen} />
)}
</CollectionsStack.Navigator>
);
};
const LoansStack = createNativeStackNavigator<AgentLoansStackParamList>();
const LoansNavigator = () => (
<LoansStack.Navigator screenOptions={{ headerShown: false }}>
<LoansStack.Screen name="AgentLoansMain" component={AgentLoansScreen} />
<LoansStack.Screen name="CreateLoanRequest" component={CreateLoanRequestScreen} />
<LoansStack.Screen name="AccountOpening" component={AccountOpeningScreen} />
<LoansStack.Screen name="CommissionDashboard" component={CommissionDashboardScreen} />
</LoansStack.Navigator>
);
const LoansNavigator = () => {
const canRoute = useRouteGuard(AGENT_LOANS_ROUTE_FEATURES);
return (
<LoansStack.Navigator screenOptions={{ headerShown: false }}>
{canRoute('AgentLoansMain') && (
<LoansStack.Screen name="AgentLoansMain" component={AgentLoansScreen} />
)}
{canRoute('CreateLoanRequest') && (
<LoansStack.Screen name="CreateLoanRequest" component={CreateLoanRequestScreen} />
)}
{canRoute('AccountOpening') && (
<LoansStack.Screen name="AccountOpening" component={AccountOpeningScreen} />
)}
{canRoute('CommissionDashboard') && (
<LoansStack.Screen name="CommissionDashboard" component={CommissionDashboardScreen} />
)}
</LoansStack.Navigator>
);
};
const ProfileStack = createNativeStackNavigator<AgentProfileStackParamList>();
const ProfileNavigator = () => (
<ProfileStack.Navigator screenOptions={{ headerShown: false }}>
<ProfileStack.Screen name="AgentProfileMain" component={AgentProfileScreen} />
<ProfileStack.Screen name="SmsHistory" component={SmsHistoryScreen} />
<ProfileStack.Screen name="AgentChangePassword" component={AgentChangePasswordScreen} />
</ProfileStack.Navigator>
);
const ProfileNavigator = () => {
const canRoute = useRouteGuard(AGENT_PROFILE_ROUTE_FEATURES);
type TabIconName =
| 'home-outline' | 'home'
| 'people-outline' | 'people'
| 'wallet-outline' | 'wallet'
| 'document-text-outline' | 'document-text'
| 'person-outline' | 'person';
const TabIcon = ({ color, focused, name }: { color: string; focused: boolean; name: TabIconName }) => {
const activeBackground = focused ? `${color}22` : 'transparent';
return (
<View style={[styles.iconFrame, { borderColor: color, backgroundColor: activeBackground }]}>
<Ionicons name={name} size={18} color={color} />
</View>
<ProfileStack.Navigator screenOptions={{ headerShown: false }}>
{canRoute('AgentProfileMain') && (
<ProfileStack.Screen name="AgentProfileMain" component={AgentProfileScreen} />
)}
{canRoute('SmsHistory') && (
<ProfileStack.Screen name="SmsHistory" component={SmsHistoryScreen} />
)}
{canRoute('AgentChangePassword') && (
<ProfileStack.Screen name="AgentChangePassword" component={AgentChangePasswordScreen} />
)}
</ProfileStack.Navigator>
);
};
export const AgentNavigator = () => {
const theme = useTheme();
const canRoute = useRouteGuard(AGENT_TAB_FEATURES);
const tabBarOptions = useTabBarScreenOptions();
return (
<Tab.Navigator
screenOptions={{
headerShown: false,
lazy: true,
tabBarActiveTintColor: theme.colors.primary,
tabBarInactiveTintColor: theme.colors.textMuted,
tabBarStyle: {
backgroundColor: theme.colors.white,
borderTopColor: theme.colors.cardBorder,
borderTopWidth: 1,
height: 90,
paddingBottom: 30,
paddingTop: 12,
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
elevation: 20,
shadowColor: '#000',
shadowOffset: { width: 0, height: -4 },
shadowOpacity: 0.05,
shadowRadius: 8,
},
tabBarLabelStyle: {
fontSize: 10,
fontWeight: '700',
marginTop: 6,
textTransform: 'uppercase',
letterSpacing: 0.5,
},
tabBarItemStyle: {
paddingVertical: 5,
},
...tabBarOptions,
}}
>
<Tab.Screen
name="AgentDashboard"
component={DashboardNavigator}
options={{
title: 'Home',
tabBarIcon: ({ color, focused }) => <TabIcon color={color} focused={focused} name={focused ? 'home' : 'home-outline'} />,
}}
/>
<Tab.Screen
name="AgentCustomers"
component={CustomersNavigator}
options={{
title: 'Clients',
tabBarIcon: ({ color, focused }) => <TabIcon color={color} focused={focused} name={focused ? 'people' : 'people-outline'} />,
}}
/>
<Tab.Screen
name="AgentCollections"
component={CollectionsNavigator}
options={{
title: 'Collect',
tabBarIcon: ({ color, focused }) => <TabIcon color={color} focused={focused} name={focused ? 'wallet' : 'wallet-outline'} />,
}}
/>
<Tab.Screen
name="AgentLoans"
component={LoansNavigator}
options={{
title: 'Workspace',
tabBarIcon: ({ color, focused }) => <TabIcon color={color} focused={focused} name={focused ? 'document-text' : 'document-text-outline'} />,
}}
/>
<Tab.Screen
name="AgentProfile"
component={ProfileNavigator}
options={{
title: 'Profile',
tabBarIcon: ({ color, focused }) => <TabIcon color={color} focused={focused} name={focused ? 'person' : 'person-outline'} />,
}}
/>
{canRoute('AgentDashboard') && (
<Tab.Screen
name="AgentDashboard"
component={DashboardNavigator}
options={{
title: 'Home',
tabBarIcon: ({ color, focused }) => <TabBarIcon color={color} focused={focused} name={focused ? 'home' : 'home-outline'} />,
}}
/>
)}
{canRoute('AgentCustomers') && (
<Tab.Screen
name="AgentCustomers"
component={CustomersNavigator}
options={{
title: 'Clients',
tabBarIcon: ({ color, focused }) => <TabBarIcon color={color} focused={focused} name={focused ? 'people' : 'people-outline'} />,
}}
/>
)}
{canRoute('AgentCollections') && (
<Tab.Screen
name="AgentCollections"
component={CollectionsNavigator}
options={{
title: 'Collect',
tabBarIcon: ({ color, focused }) => <TabBarIcon color={color} focused={focused} name={focused ? 'wallet' : 'wallet-outline'} />,
}}
/>
)}
{canRoute('AgentLoans') && (
<Tab.Screen
name="AgentLoans"
component={LoansNavigator}
options={{
title: 'Workspace',
tabBarIcon: ({ color, focused }) => <TabBarIcon color={color} focused={focused} name={focused ? 'document-text' : 'document-text-outline'} />,
}}
/>
)}
{canRoute('AgentProfile') && (
<Tab.Screen
name="AgentProfile"
component={ProfileNavigator}
options={{
title: 'Profile',
tabBarIcon: ({ color, focused }) => <TabBarIcon color={color} focused={focused} name={focused ? 'person' : 'person-outline'} />,
}}
/>
)}
</Tab.Navigator>
);
};
const styles = StyleSheet.create({
iconFrame: {
width: 32,
height: 32,
borderRadius: 8,
alignItems: 'center',
justifyContent: 'center',
},
});
+101 -12
View File
@@ -1,19 +1,108 @@
import React from 'react';
import React, { useRef } from 'react';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { useAppSelector } from '../app/hooks';
import {
selectLastProfile,
selectSignInContextLoaded,
} from '../modules/auth/store/authSelectors';
import { FeatureKey } from '../constants/featureKeys';
import { useFeature } from '../features';
import type { AuthStackParamList } from './types';
import LoginScreen from '../modules/auth/screens/LoginScreen';
import RegisterScreen from '../modules/auth/screens/RegisterScreen';
import VerifyOtpScreen from '../modules/auth/screens/VerifyOtpScreen';
import CreateMpinScreen from '../modules/auth/screens/CreateMpinScreen';
import MpinLockedScreen from '../modules/auth/screens/MpinLockedScreen';
const Stack = createNativeStackNavigator<AuthStackParamList>();
export const AuthNavigator = () => (
<Stack.Navigator
initialRouteName="Login"
screenOptions={{
headerShown: false,
animation: 'slide_from_right',
}}
>
<Stack.Screen name="Login" component={LoginScreen} />
</Stack.Navigator>
);
export const AuthNavigator = () => {
const lastProfile = useAppSelector(selectLastProfile);
const contextLoaded = useAppSelector(selectSignInContextLoaded);
/**
* Which screen a customer owes us depends on whether this device has ever
* completed registration: a stored profile means they only owe an MPIN,
* nothing stored means registration.
*
* Decided here rather than by navigating out of the sign-in screen. An
* effect-driven `replace()` on a screen's first render races the navigation
* container's readiness and the action is silently dropped — and the warning
* that would tell you so is stripped from release builds, so it fails only
* where it is hardest to see. This is also flash-free.
*
* `contextLoaded` is what makes it safe: before the Keystore has been read, a
* null profile means "unknown", not "never registered".
*/
/**
* Registration is switchable, and turning it off unregisters the three screens
* that make it up — so the initial route has to fall back to sign-in, or it
* would name a route that no longer exists and the navigator would throw.
*
* A device with nothing stored then lands on a sign-in screen it cannot
* complete, which is the correct outcome: the bank has stopped onboarding, and
* the app should say nothing rather than offer a flow that will be refused.
*/
const registrationEnabled = useFeature(FeatureKey.AUTH_REGISTER);
/**
* Read once, on the navigator's first render, and frozen from then on.
*
* `initialRouteName` is not a live prop — React Navigation rebuilds its route
* list when it changes, and the inputs here are anything but stable:
* verify-otp records a `lastProfile` and flips this from 'Register' to
* 'Login' in the very same tick that the OTP screen calls `reset()`. The
* rebuild and the reset then describe two different stacks to Fabric in one
* commit, and mounting the result throws "The specified child already has a
* parent" — a red box on the way in, and a blank white screen once it is
* dismissed, with no way to sign in.
*
* Freezing it costs nothing, because nothing here needs it to be live:
* `lastProfile` arriving later is what `LoginScreen` already watches, and it
* routes to registration itself.
*/
const frozenInitialRoute = useRef<keyof AuthStackParamList | null>(null);
if (frozenInitialRoute.current === null) {
frozenInitialRoute.current =
registrationEnabled && contextLoaded && !lastProfile ? 'Register' : 'Login';
}
const initialRouteName = frozenInitialRoute.current;
return (
<Stack.Navigator
initialRouteName={initialRouteName}
screenOptions={{
headerShown: false,
animation: 'slide_from_right',
}}
>
<Stack.Screen name="Login" component={LoginScreen} />
{registrationEnabled && (
<>
<Stack.Screen name="Register" component={RegisterScreen} />
<Stack.Screen name="VerifyOtp" component={VerifyOtpScreen} />
</>
)}
{/* Deliberately still registered when registration is switched off. A
customer who has already passed OTP holds a single-use setup token —
unregistering this screen under them would leave them unable to finish
and unable to start again, because the token is spent. Switching the
feature off closes the entrance; it does not trap anyone inside.
No swipe back out of MPIN creation, for that same single-use reason. */}
<Stack.Screen
name="CreateMpin"
component={CreateMpinScreen}
options={{ gestureEnabled: false }}
/>
<Stack.Screen
name="MpinLocked"
component={MpinLockedScreen}
options={{ gestureEnabled: false, animation: 'fade' }}
/>
</Stack.Navigator>
);
};
+7 -4
View File
@@ -6,16 +6,19 @@ import CardDetailsScreen from '../modules/cards/screens/CardDetailsScreen';
import RequestPhysicalCardScreen from '../modules/cards/screens/RequestPhysicalCardScreen';
import ApplyCreditCardScreen from '../modules/cards/screens/ApplyCreditCardScreen';
import type { CardsStackParamList } from './types';
import { CARDS_ROUTE_FEATURES, useRouteGuard } from './featureRoutes';
const Stack = createNativeStackNavigator<CardsStackParamList>();
export const CardsNavigator = () => {
const canRoute = useRouteGuard(CARDS_ROUTE_FEATURES);
return (
<Stack.Navigator screenOptions={{ headerShown: false }}>
<Stack.Screen name="CardsOverview" component={CardsOverviewScreen} />
<Stack.Screen name="CardDetails" component={CardDetailsScreen} />
<Stack.Screen name="RequestPhysicalCard" component={RequestPhysicalCardScreen} />
<Stack.Screen name="ApplyCreditCard" component={ApplyCreditCardScreen} />
{canRoute('CardsOverview') && <Stack.Screen name="CardsOverview" component={CardsOverviewScreen} />}
{canRoute('CardDetails') && <Stack.Screen name="CardDetails" component={CardDetailsScreen} />}
{canRoute('RequestPhysicalCard') && <Stack.Screen name="RequestPhysicalCard" component={RequestPhysicalCardScreen} />}
{canRoute('ApplyCreditCard') && <Stack.Screen name="ApplyCreditCard" component={ApplyCreditCardScreen} />}
</Stack.Navigator>
);
};
+88
View File
@@ -0,0 +1,88 @@
import React, { useEffect, type ComponentType, type ReactElement, type ReactNode } from 'react';
import { useNavigation } from '@react-navigation/native';
import type { FeatureKey } from '../constants/featureKeys';
import { useFeature } from '../features/useFeature';
type FeatureRouteProps = {
/** Feature that owns this screen. */
feature: FeatureKey;
/**
* Where to send the user when the feature is off. Defaults to going back,
* which returns them to whatever enabled screen they came from.
*/
redirectTo?: { name: string; params?: object };
children: ReactNode;
};
/**
* Screen-level guard.
*
* Primary navigation protection is *not* this component — it is the navigators,
* which never register a screen whose feature is disabled, so the route cannot
* be resolved by `navigate()`, a deep link, or restored state.
*
* `FeatureRoute` is the second layer, for screens that stay registered because a
* broader feature owns the route while a narrower one owns the content, and for
* the window where a remote-config update disables a feature while the user is
* already standing on it. In that case it renders nothing and navigates away, so
* the user never sees an "access denied" page — the screen simply is not there.
*/
export const FeatureRoute = ({
feature,
redirectTo,
children,
}: FeatureRouteProps): ReactElement | null => {
const enabled = useFeature(feature);
const navigation = useNavigation();
useEffect(() => {
if (enabled) {
return;
}
if (redirectTo) {
// `navigate` is typed per-navigator; this component is deliberately
// navigator-agnostic, so the call is widened at the boundary.
(navigation.navigate as (name: string, params?: object) => void)(
redirectTo.name,
redirectTo.params,
);
return;
}
if (navigation.canGoBack()) {
navigation.goBack();
}
}, [enabled, navigation, redirectTo]);
if (!enabled) {
return null;
}
return <>{children}</>;
};
/**
* HOC form, for wrapping a screen at its registration site:
*
* <Stack.Screen
* name="StopPayment"
* component={withFeatureRoute(StopPaymentScreen, FeatureKey.SERVICES_CHEQUE_STOP_PAYMENT)}
* />
*/
export function withFeatureRoute<P extends object>(
Screen: ComponentType<P>,
feature: FeatureKey,
redirectTo?: { name: string; params?: object },
): ComponentType<P> {
const Guarded = (props: P) => (
<FeatureRoute feature={feature} redirectTo={redirectTo}>
<Screen {...props} />
</FeatureRoute>
);
Guarded.displayName = `withFeatureRoute(${Screen.displayName ?? Screen.name ?? 'Screen'})`;
return Guarded;
}
+17 -8
View File
@@ -6,23 +6,32 @@ import NotificationsScreen from '../modules/notifications/screens/NotificationsS
import MiniStatementScreen from '../modules/home/screens/MiniStatementScreen';
import AccountDetailsScreen from '../modules/home/screens/AccountDetailsScreen';
import ReKycScreen from '../modules/home/screens/ReKycScreen';
import EPassbookScreen from '../modules/home/screens/EPassbookScreen';
import VideoKycScreen from '../modules/home/screens/VideoKycScreen';
import AccountsMmidScreen from '../modules/home/screens/AccountsMmidScreen';
import { LoanNavigator } from './LoanNavigator';
import { InsuranceNavigator } from './InsuranceNavigator';
import type { HomeStackParamList } from './types';
import { HOME_ROUTE_FEATURES, useRouteGuard } from './featureRoutes';
const Stack = createNativeStackNavigator<HomeStackParamList>();
export const HomeNavigator = () => {
const canRoute = useRouteGuard(HOME_ROUTE_FEATURES);
return (
<Stack.Navigator screenOptions={{ headerShown: false }}>
<Stack.Screen name="HomeMain" component={HomeScreen} />
<Stack.Screen name="GlobalSearch" component={GlobalSearchScreen} />
<Stack.Screen name="Notifications" component={NotificationsScreen} />
<Stack.Screen name="MiniStatement" component={MiniStatementScreen} />
<Stack.Screen name="AccountDetails" component={AccountDetailsScreen} />
<Stack.Screen name="ReKyc" component={ReKycScreen} />
<Stack.Screen name="LoanFlow" component={LoanNavigator} />
<Stack.Screen name="InsuranceFlow" component={InsuranceNavigator} />
{canRoute('HomeMain') && <Stack.Screen name="HomeMain" component={HomeScreen} />}
{canRoute('GlobalSearch') && <Stack.Screen name="GlobalSearch" component={GlobalSearchScreen} />}
{canRoute('Notifications') && <Stack.Screen name="Notifications" component={NotificationsScreen} />}
{canRoute('MiniStatement') && <Stack.Screen name="MiniStatement" component={MiniStatementScreen} />}
{canRoute('AccountDetails') && <Stack.Screen name="AccountDetails" component={AccountDetailsScreen} />}
{canRoute('ReKyc') && <Stack.Screen name="ReKyc" component={ReKycScreen} />}
{canRoute('EPassbook') && <Stack.Screen name="EPassbook" component={EPassbookScreen} />}
{canRoute('VideoKyc') && <Stack.Screen name="VideoKyc" component={VideoKycScreen} />}
{canRoute('AccountsMmid') && <Stack.Screen name="AccountsMmid" component={AccountsMmidScreen} />}
{canRoute('LoanFlow') && <Stack.Screen name="LoanFlow" component={LoanNavigator} />}
{canRoute('InsuranceFlow') && <Stack.Screen name="InsuranceFlow" component={InsuranceNavigator} />}
</Stack.Navigator>
);
};
+13 -10
View File
@@ -12,22 +12,25 @@ import InsuranceNotificationsScreen from '../modules/insurance/screens/Insurance
import InsurancePoliciesScreen from '../modules/insurance/screens/InsurancePoliciesScreen';
import InsurancePolicyDetailsScreen from '../modules/insurance/screens/InsurancePolicyDetailsScreen';
import InsuranceRenewalsScreen from '../modules/insurance/screens/InsuranceRenewalsScreen';
import { INSURANCE_ROUTE_FEATURES, useRouteGuard } from './featureRoutes';
const Stack = createNativeStackNavigator<InsuranceStackParamList>();
export const InsuranceNavigator = () => {
const canRoute = useRouteGuard(INSURANCE_ROUTE_FEATURES);
return (
<Stack.Navigator screenOptions={{ headerShown: false }}>
<Stack.Screen name="InsuranceDashboard" component={InsuranceDashboardScreen} />
<Stack.Screen name="BuyInsurance" component={BuyInsuranceScreen} />
<Stack.Screen name="InsurancePolicies" component={InsurancePoliciesScreen} />
<Stack.Screen name="InsurancePolicyDetails" component={InsurancePolicyDetailsScreen} />
<Stack.Screen name="InsuranceRenewals" component={InsuranceRenewalsScreen} />
<Stack.Screen name="InsuranceClaims" component={InsuranceClaimsScreen} />
<Stack.Screen name="InsuranceDocuments" component={InsuranceDocumentsScreen} />
<Stack.Screen name="InsuranceCalculator" component={InsuranceCalculatorScreen} />
<Stack.Screen name="InsuranceComparison" component={InsuranceComparisonScreen} />
<Stack.Screen name="InsuranceNotifications" component={InsuranceNotificationsScreen} />
{canRoute('InsuranceDashboard') && <Stack.Screen name="InsuranceDashboard" component={InsuranceDashboardScreen} />}
{canRoute('BuyInsurance') && <Stack.Screen name="BuyInsurance" component={BuyInsuranceScreen} />}
{canRoute('InsurancePolicies') && <Stack.Screen name="InsurancePolicies" component={InsurancePoliciesScreen} />}
{canRoute('InsurancePolicyDetails') && <Stack.Screen name="InsurancePolicyDetails" component={InsurancePolicyDetailsScreen} />}
{canRoute('InsuranceRenewals') && <Stack.Screen name="InsuranceRenewals" component={InsuranceRenewalsScreen} />}
{canRoute('InsuranceClaims') && <Stack.Screen name="InsuranceClaims" component={InsuranceClaimsScreen} />}
{canRoute('InsuranceDocuments') && <Stack.Screen name="InsuranceDocuments" component={InsuranceDocumentsScreen} />}
{canRoute('InsuranceCalculator') && <Stack.Screen name="InsuranceCalculator" component={InsuranceCalculatorScreen} />}
{canRoute('InsuranceComparison') && <Stack.Screen name="InsuranceComparison" component={InsuranceComparisonScreen} />}
{canRoute('InsuranceNotifications') && <Stack.Screen name="InsuranceNotifications" component={InsuranceNotificationsScreen} />}
</Stack.Navigator>
);
};
+13 -10
View File
@@ -12,22 +12,25 @@ import ActiveLoanDetailsScreen from '../modules/loan/screens/ActiveLoanDetailsSc
import EmiPaymentScreen from '../modules/loan/screens/EmiPaymentScreen';
import LoanHistoryScreen from '../modules/loan/screens/LoanHistoryScreen';
import LoanDocumentsScreen from '../modules/loan/screens/LoanDocumentsScreen';
import { LOAN_ROUTE_FEATURES, useRouteGuard } from './featureRoutes';
const Stack = createNativeStackNavigator<LoanStackParamList>();
export const LoanNavigator = () => {
const canRoute = useRouteGuard(LOAN_ROUTE_FEATURES);
return (
<Stack.Navigator screenOptions={{ headerShown: false }}>
<Stack.Screen name="LoanDashboard" component={LoanDashboardScreen} />
<Stack.Screen name="LoanTypesListing" component={LoanTypesListingScreen} />
<Stack.Screen name="LoanEligibilityCheck" component={LoanEligibilityCheckScreen} />
<Stack.Screen name="LoanCalculator" component={LoanCalculatorScreen} />
<Stack.Screen name="ApplyLoan" component={ApplyLoanScreen} />
<Stack.Screen name="LoanTracking" component={LoanTrackingScreen} />
<Stack.Screen name="ActiveLoanDetails" component={ActiveLoanDetailsScreen} />
<Stack.Screen name="EmiPayment" component={EmiPaymentScreen} />
<Stack.Screen name="LoanHistory" component={LoanHistoryScreen} />
<Stack.Screen name="LoanDocuments" component={LoanDocumentsScreen} />
{canRoute('LoanDashboard') && <Stack.Screen name="LoanDashboard" component={LoanDashboardScreen} />}
{canRoute('LoanTypesListing') && <Stack.Screen name="LoanTypesListing" component={LoanTypesListingScreen} />}
{canRoute('LoanEligibilityCheck') && <Stack.Screen name="LoanEligibilityCheck" component={LoanEligibilityCheckScreen} />}
{canRoute('LoanCalculator') && <Stack.Screen name="LoanCalculator" component={LoanCalculatorScreen} />}
{canRoute('ApplyLoan') && <Stack.Screen name="ApplyLoan" component={ApplyLoanScreen} />}
{canRoute('LoanTracking') && <Stack.Screen name="LoanTracking" component={LoanTrackingScreen} />}
{canRoute('ActiveLoanDetails') && <Stack.Screen name="ActiveLoanDetails" component={ActiveLoanDetailsScreen} />}
{canRoute('EmiPayment') && <Stack.Screen name="EmiPayment" component={EmiPaymentScreen} />}
{canRoute('LoanHistory') && <Stack.Screen name="LoanHistory" component={LoanHistoryScreen} />}
{canRoute('LoanDocuments') && <Stack.Screen name="LoanDocuments" component={LoanDocumentsScreen} />}
</Stack.Navigator>
);
};
+54 -110
View File
@@ -1,133 +1,82 @@
import React from 'react';
import { StyleSheet, View } from 'react-native';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import {
CommonActions,
type NavigationState,
type PartialState,
} from '@react-navigation/native';
import Ionicons from '@react-native-vector-icons/ionicons';
import { useTheme } from '../theme';
import { TabBarIcon, useTabBarScreenOptions } from './tabBarTheme';
import { HomeNavigator } from './HomeNavigator';
import { CardsNavigator } from './CardsNavigator';
import { ServicesNavigator } from './ServicesNavigator';
import { PaymentsNavigator } from './PaymentsNavigator';
import { ProfileNavigator } from '../modules/profile/navigation/ProfileNavigator';
import { MAIN_TAB_FEATURES, useRouteGuard } from './featureRoutes';
import type { MainTabParamList } from './types';
const Tab = createBottomTabNavigator<MainTabParamList>();
type TabIconName =
| 'home-outline'
| 'home'
| 'wallet-outline'
| 'wallet'
| 'grid-outline'
| 'grid'
| 'card-outline'
| 'card'
| 'person-outline'
| 'person';
type TabBarIconProps = {
color: string;
focused: boolean;
};
const TabIcon = ({
color,
focused,
name,
}: {
color: string;
focused: boolean;
name: TabIconName;
}) => {
const activeBackground = focused ? `${color}22` : 'transparent';
return (
<View style={[styles.iconFrame, { borderColor: color, backgroundColor: activeBackground }]}>
<Ionicons name={name} size={18} color={color} />
</View>
);
};
const HomeTabBarIcon = ({ color, focused }: TabBarIconProps) => (
<TabIcon color={color} focused={focused} name={focused ? 'home' : 'home-outline'} />
<TabBarIcon color={color} focused={focused} name={focused ? 'home' : 'home-outline'} />
);
const PaymentsTabBarIcon = ({ color, focused }: TabBarIconProps) => (
<TabIcon color={color} focused={focused} name={focused ? 'wallet' : 'wallet-outline'} />
<TabBarIcon color={color} focused={focused} name={focused ? 'wallet' : 'wallet-outline'} />
);
const ServicesTabBarIcon = ({ color, focused }: TabBarIconProps) => (
<TabIcon color={color} focused={focused} name={focused ? 'grid' : 'grid-outline'} />
<TabBarIcon color={color} focused={focused} name={focused ? 'grid' : 'grid-outline'} />
);
const CardsTabBarIcon = ({ color, focused }: TabBarIconProps) => (
<TabIcon color={color} focused={focused} name={focused ? 'card' : 'card-outline'} />
<TabBarIcon color={color} focused={focused} name={focused ? 'card' : 'card-outline'} />
);
const ProfileTabBarIcon = ({ color, focused }: TabBarIconProps) => (
<TabIcon color={color} focused={focused} name={focused ? 'person' : 'person-outline'} />
<TabBarIcon color={color} focused={focused} name={focused ? 'person' : 'person-outline'} />
);
export const MainTabNavigator = () => {
const theme = useTheme();
// A tab whose feature is disabled is never registered: no tab bar entry, and
// no route for anything to navigate to.
const canRoute = useRouteGuard(MAIN_TAB_FEATURES);
const tabBarOptions = useTabBarScreenOptions();
return (
<Tab.Navigator
screenOptions={{
headerShown: false,
lazy: true,
tabBarActiveTintColor: theme.colors.primary,
tabBarInactiveTintColor: theme.colors.textMuted,
tabBarStyle: {
backgroundColor: theme.colors.white,
borderTopColor: theme.colors.cardBorder,
borderTopWidth: 1,
height: 90,
paddingBottom: 30,
paddingTop: 12,
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
elevation: 20,
shadowColor: '#000',
shadowOffset: { width: 0, height: -4 },
shadowOpacity: 0.05,
shadowRadius: 8,
},
tabBarLabelStyle: {
fontSize: 10,
fontWeight: '700',
marginTop: 6,
textTransform: 'uppercase',
letterSpacing: 0.5,
},
tabBarItemStyle: {
paddingVertical: 5,
},
...tabBarOptions,
}}
>
<Tab.Screen
name="Home"
component={HomeNavigator}
options={{
title: 'Home',
tabBarIcon: HomeTabBarIcon,
}}
/>
<Tab.Screen
name="Payments"
component={PaymentsNavigator}
options={{
title: 'Payments',
tabBarIcon: PaymentsTabBarIcon,
}}
/>
{canRoute('Home') && (
<Tab.Screen
name="Home"
component={HomeNavigator}
options={{
title: 'Home',
tabBarIcon: HomeTabBarIcon,
}}
/>
)}
{canRoute('Payments') && (
<Tab.Screen
name="Payments"
component={PaymentsNavigator}
options={{
title: 'Payments',
tabBarIcon: PaymentsTabBarIcon,
}}
/>
)}
{canRoute('Services') && (
<Tab.Screen
name="Services"
component={ServicesNavigator}
@@ -173,32 +122,27 @@ export const MainTabNavigator = () => {
},
})}
/>
<Tab.Screen
name="Cards"
component={CardsNavigator}
options={{
title: 'Cards',
tabBarIcon: CardsTabBarIcon,
}}
/>
<Tab.Screen
name="Profile"
component={ProfileNavigator}
options={{
title: 'Profile',
tabBarIcon: ProfileTabBarIcon,
}}
/>
)}
{canRoute('Cards') && (
<Tab.Screen
name="Cards"
component={CardsNavigator}
options={{
title: 'Cards',
tabBarIcon: CardsTabBarIcon,
}}
/>
)}
{canRoute('Profile') && (
<Tab.Screen
name="Profile"
component={ProfileNavigator}
options={{
title: 'Profile',
tabBarIcon: ProfileTabBarIcon,
}}
/>
)}
</Tab.Navigator>
);
};
const styles = StyleSheet.create({
iconFrame: {
width: 32,
height: 32,
borderRadius: 8,
alignItems: 'center',
justifyContent: 'center',
},
});
+37 -17
View File
@@ -2,12 +2,15 @@ import React from 'react';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { useTheme } from '../theme';
import type { PaymentsStackParamList } from './types';
import { PAYMENTS_ROUTE_FEATURES, useRouteGuard } from './featureRoutes';
import PaymentsHomeScreen from '../modules/payments/screens/PaymentsHomeScreen';
import BeneficiariesListScreen from '../modules/payments/screens/BeneficiariesListScreen';
import AddBeneficiaryScreen from '../modules/payments/screens/AddBeneficiaryScreen';
import TransferTypeScreen from '../modules/payments/screens/TransferTypeScreen';
import { PaymentFormScreen } from '../modules/payments/screens/PaymentFormScreen';
import TransferToMobileScreen from '../modules/payments/screens/TransferToMobileScreen';
import TransferToAccountScreen from '../modules/payments/screens/TransferToAccountScreen';
import TransferToOwnAccountScreen from '../modules/payments/screens/TransferToOwnAccountScreen';
import ImpsMmidScreen from '../modules/payments/screens/ImpsMmidScreen';
import { PaymentConfirmationScreen } from '../modules/payments/screens/PaymentConfirmationScreen';
import { TransactionSuccessScreen } from '../modules/payments/screens/TransactionSuccessScreen';
@@ -15,6 +18,7 @@ const Stack = createNativeStackNavigator<PaymentsStackParamList>();
export const PaymentsNavigator = () => {
const theme = useTheme();
const canRoute = useRouteGuard(PAYMENTS_ROUTE_FEATURES);
return (
<Stack.Navigator
@@ -24,21 +28,37 @@ export const PaymentsNavigator = () => {
animation: 'slide_from_right',
}}
>
<Stack.Screen name="PaymentsHub" component={PaymentsHomeScreen} />
<Stack.Screen name="BeneficiariesList" component={BeneficiariesListScreen} />
<Stack.Screen
name="AddBeneficiary"
component={AddBeneficiaryScreen}
options={{ presentation: 'modal', animation: 'slide_from_bottom' }}
/>
<Stack.Screen name="TransferTypeSelection" component={TransferTypeScreen} />
<Stack.Screen name="PaymentForm" component={PaymentFormScreen} />
<Stack.Screen name="PaymentConfirmation" component={PaymentConfirmationScreen} />
<Stack.Screen
name="TransactionSuccess"
component={TransactionSuccessScreen}
options={{ gestureEnabled: false, animation: 'fade' }}
/>
{canRoute('PaymentsHub') && <Stack.Screen name="PaymentsHub" component={PaymentsHomeScreen} />}
{canRoute('BeneficiariesList') && (
<Stack.Screen name="BeneficiariesList" component={BeneficiariesListScreen} />
)}
{canRoute('AddBeneficiary') && (
<Stack.Screen
name="AddBeneficiary"
component={AddBeneficiaryScreen}
options={{ presentation: 'modal', animation: 'slide_from_bottom' }}
/>
)}
{canRoute('TransferToMobile') && (
<Stack.Screen name="TransferToMobile" component={TransferToMobileScreen} />
)}
{canRoute('TransferToAccount') && (
<Stack.Screen name="TransferToAccount" component={TransferToAccountScreen} />
)}
{canRoute('TransferToOwnAccount') && (
<Stack.Screen name="TransferToOwnAccount" component={TransferToOwnAccountScreen} />
)}
{canRoute('ImpsMmid') && <Stack.Screen name="ImpsMmid" component={ImpsMmidScreen} />}
{canRoute('PaymentConfirmation') && (
<Stack.Screen name="PaymentConfirmation" component={PaymentConfirmationScreen} />
)}
{canRoute('TransactionSuccess') && (
<Stack.Screen
name="TransactionSuccess"
component={TransactionSuccessScreen}
options={{ gestureEnabled: false, animation: 'fade' }}
/>
)}
</Stack.Navigator>
);
};
+28 -3
View File
@@ -1,4 +1,5 @@
import React from 'react';
import { View } from 'react-native';
import {
DarkTheme,
NavigationContainer,
@@ -6,10 +7,12 @@ import {
import { useAppSelector } from '../app/hooks';
import { selectAuthHydrating, selectIsAuthenticated, selectAuthRole } from '../modules/auth/store/authSelectors';
import { Loader } from '../components';
import { FeatureKey } from '../constants/featureKeys';
import { useFeature, useFeatureContext } from '../features';
import { useTheme } from '../theme';
import { AuthNavigator } from './AuthNavigator';
import { MainTabNavigator } from './MainTabNavigator';
import { EmployeeNavigator } from './EmployeeNavigator';
import { AgentNavigator } from './AgentNavigator';
export const RootNavigator = () => {
@@ -17,11 +20,22 @@ export const RootNavigator = () => {
const isHydrating = useAppSelector(selectAuthHydrating);
const isAuthenticated = useAppSelector(selectIsAuthenticated);
const role = useAppSelector(selectAuthRole);
const agentAppEnabled = useFeature(FeatureKey.AGENT);
/**
* Navigators register their screens at mount time, so mounting one before the
* feature layers have resolved would leave a disabled route reachable — by
* `navigate()`, a deep link or restored state — until the next launch. Waiting
* is what makes a server-side kill switch take effect on this launch.
*/
const { ready: featuresReady } = useFeatureContext();
const renderAppStack = () => {
switch (role) {
// Turning off `agent` retires the whole field-agent application. An
// already-signed-in agent falls back to the customer app rather than
// landing on an empty navigator.
case 'Agent':
return <AgentNavigator />;
return agentAppEnabled ? <AgentNavigator /> : <MainTabNavigator />;
case 'User':
default:
return <MainTabNavigator />;
@@ -43,7 +57,18 @@ export const RootNavigator = () => {
},
}}
>
{isAuthenticated ? renderAppStack() : <AuthNavigator />}
{/* Bootstrap validates a stored session against the server before
trusting it, so rendering the sign-in screen meanwhile would flash it
at a user who turns out to be signed in already. */}
{isHydrating || !featuresReady ? (
<View style={{ flex: 1, backgroundColor: theme.colors.background }}>
<Loader />
</View>
) : isAuthenticated ? (
renderAppStack()
) : (
<AuthNavigator />
)}
</NavigationContainer>
);
};
+40 -27
View File
@@ -7,6 +7,9 @@ import DepositsOverviewScreen from '../modules/deposit/screens/DepositsOverviewS
import OpenFixedDepositScreen from '../modules/deposit/screens/OpenFixedDepositScreen';
import OpenRecurringDepositScreen from '../modules/deposit/screens/OpenRecurringDepositScreen';
import ServicesHubScreen from '../modules/services/screens/ServicesHubScreen';
import BalanceEnquiryScreen from '../modules/services/screens/BalanceEnquiryScreen';
import FateOfChequeScreen from '../modules/services/screens/FateOfChequeScreen';
import SetTpinScreen from '../modules/services/screens/SetTpinScreen';
// New Modules
import ChequeServicesHubScreen from '../modules/services/screens/ChequeServicesHubScreen';
@@ -17,8 +20,9 @@ import CertificatesHubScreen from '../modules/services/screens/CertificatesHubSc
import CertificateListScreen from '../modules/services/screens/CertificateListScreen';
import ServiceRequestsHubScreen from '../modules/services/screens/ServiceRequestsHubScreen';
import PositivePayScreen from '../modules/services/screens/PositivePayScreen';
import SpecialInstructionsScreen from '../modules/services/screens/SpecialInstructionsScreen';
import StandingInstructionsScreen from '../modules/services/screens/StandingInstructionsScreen';
import Form15G15HScreen from '../modules/services/screens/Form15G15HScreen';
import NominationScreen from '../modules/services/screens/NominationScreen';
import BranchLocatorScreen from '../modules/services/screens/BranchLocatorScreen';
import AwarenessListScreen from '../modules/awareness/screens/AwarenessListScreen';
import AwarenessDetailScreen from '../modules/awareness/screens/AwarenessDetailScreen';
@@ -31,50 +35,59 @@ import SchemeEnrollmentScreen from '../modules/schemes/screens/SchemeEnrollmentS
import SchemeSuccessScreen from '../modules/schemes/screens/SchemeSuccessScreen';
import type { ServicesStackParamList } from './types';
import { SERVICES_ROUTE_FEATURES, useRouteGuard } from './featureRoutes';
const Stack = createNativeStackNavigator<ServicesStackParamList>();
export const ServicesNavigator = () => {
const canRoute = useRouteGuard(SERVICES_ROUTE_FEATURES);
return (
<Stack.Navigator screenOptions={{ headerShown: false }} initialRouteName="ServicesHub">
<Stack.Screen name="ServicesHub" component={ServicesHubScreen} />
<Stack.Screen name="DepositsOverview" component={DepositsOverviewScreen} />
<Stack.Screen name="DepositDetails" component={DepositDetailsScreen} />
<Stack.Screen name="OpenFixedDeposit" component={OpenFixedDepositScreen} />
<Stack.Screen name="OpenRecurringDeposit" component={OpenRecurringDepositScreen} />
<Stack.Screen name="DepositCalculator" component={DepositCalculatorScreen} />
{canRoute('ServicesHub') && <Stack.Screen name="ServicesHub" component={ServicesHubScreen} />}
{canRoute('DepositsOverview') && <Stack.Screen name="DepositsOverview" component={DepositsOverviewScreen} />}
{canRoute('DepositDetails') && <Stack.Screen name="DepositDetails" component={DepositDetailsScreen} />}
{canRoute('OpenFixedDeposit') && <Stack.Screen name="OpenFixedDeposit" component={OpenFixedDepositScreen} />}
{canRoute('OpenRecurringDeposit') && <Stack.Screen name="OpenRecurringDeposit" component={OpenRecurringDepositScreen} />}
{canRoute('DepositCalculator') && <Stack.Screen name="DepositCalculator" component={DepositCalculatorScreen} />}
{/* Enquiry desk */}
{canRoute('BalanceEnquiry') && <Stack.Screen name="BalanceEnquiry" component={BalanceEnquiryScreen} />}
{canRoute('FateOfCheque') && <Stack.Screen name="FateOfCheque" component={FateOfChequeScreen} />}
{canRoute('SetTpin') && <Stack.Screen name="SetTpin" component={SetTpinScreen} />}
{/* Cheque Services */}
<Stack.Screen name="ChequeServicesHub" component={ChequeServicesHubScreen} />
<Stack.Screen name="ChequeBookRequest" component={ChequeBookRequestScreen} />
<Stack.Screen name="StopPayment" component={StopPaymentScreen} />
<Stack.Screen name="ChequeTracking" component={ChequeTrackingScreen} />
{canRoute('ChequeServicesHub') && <Stack.Screen name="ChequeServicesHub" component={ChequeServicesHubScreen} />}
{canRoute('ChequeBookRequest') && <Stack.Screen name="ChequeBookRequest" component={ChequeBookRequestScreen} />}
{canRoute('StopPayment') && <Stack.Screen name="StopPayment" component={StopPaymentScreen} />}
{canRoute('ChequeTracking') && <Stack.Screen name="ChequeTracking" component={ChequeTrackingScreen} />}
{/* Certificates */}
<Stack.Screen name="CertificatesHub" component={CertificatesHubScreen} />
<Stack.Screen name="CertificateList" component={CertificateListScreen} />
{canRoute('CertificatesHub') && <Stack.Screen name="CertificatesHub" component={CertificatesHubScreen} />}
{canRoute('CertificateList') && <Stack.Screen name="CertificateList" component={CertificateListScreen} />}
{/* Service Requests */}
<Stack.Screen name="ServiceRequestsHub" component={ServiceRequestsHubScreen} />
<Stack.Screen name="PositivePay" component={PositivePayScreen} />
<Stack.Screen name="SpecialInstructions" component={SpecialInstructionsScreen} />
<Stack.Screen name="Form15G15H" component={Form15G15HScreen} />
{canRoute('ServiceRequestsHub') && <Stack.Screen name="ServiceRequestsHub" component={ServiceRequestsHubScreen} />}
{canRoute('PositivePay') && <Stack.Screen name="PositivePay" component={PositivePayScreen} />}
{canRoute('StandingInstructions') && <Stack.Screen name="StandingInstructions" component={StandingInstructionsScreen} />}
{canRoute('Form15G15H') && <Stack.Screen name="Form15G15H" component={Form15G15HScreen} />}
{canRoute('Nomination') && <Stack.Screen name="Nomination" component={NominationScreen} />}
{/* Branch Locator */}
<Stack.Screen name="BranchLocator" component={BranchLocatorScreen} />
{canRoute('BranchLocator') && <Stack.Screen name="BranchLocator" component={BranchLocatorScreen} />}
{/* Cyber Security Awareness */}
<Stack.Screen name="AwarenessList" component={AwarenessListScreen} />
<Stack.Screen name="AwarenessDetail" component={AwarenessDetailScreen} />
{canRoute('AwarenessList') && <Stack.Screen name="AwarenessList" component={AwarenessListScreen} />}
{canRoute('AwarenessDetail') && <Stack.Screen name="AwarenessDetail" component={AwarenessDetailScreen} />}
{/* Financial Protection & Schemes */}
<Stack.Screen name="FinancialProtectionMain" component={FinancialProtectionScreen} />
<Stack.Screen name="SchemeDetails" component={SchemeDetailScreen} />
<Stack.Screen name="DeafHub" component={DeafHubScreen} />
<Stack.Screen name="DeafClaimFlow" component={DeafClaimFlowScreen} />
<Stack.Screen name="DicgcHub" component={DicgcHubScreen} />
<Stack.Screen name="SchemeEnrollment" component={SchemeEnrollmentScreen} />
<Stack.Screen name="SchemeSuccess" component={SchemeSuccessScreen} />
{canRoute('FinancialProtectionMain') && <Stack.Screen name="FinancialProtectionMain" component={FinancialProtectionScreen} />}
{canRoute('SchemeDetails') && <Stack.Screen name="SchemeDetails" component={SchemeDetailScreen} />}
{canRoute('DeafHub') && <Stack.Screen name="DeafHub" component={DeafHubScreen} />}
{canRoute('DeafClaimFlow') && <Stack.Screen name="DeafClaimFlow" component={DeafClaimFlowScreen} />}
{canRoute('DicgcHub') && <Stack.Screen name="DicgcHub" component={DicgcHubScreen} />}
{canRoute('SchemeEnrollment') && <Stack.Screen name="SchemeEnrollment" component={SchemeEnrollmentScreen} />}
{canRoute('SchemeSuccess') && <Stack.Screen name="SchemeSuccess" component={SchemeSuccessScreen} />}
</Stack.Navigator>
);
};
+236
View File
@@ -0,0 +1,236 @@
import { useCallback, useMemo } from 'react';
import { FeatureKey } from '../constants/featureKeys';
import { isFeatureEnabled } from '../features/featureService';
import { useFeatureVersion } from '../features/useFeature';
import type {
AgentCollectionsStackParamList,
AgentCustomersStackParamList,
AgentDashboardStackParamList,
AgentLoansStackParamList,
AgentProfileStackParamList,
AgentTabParamList,
} from '../modules/agent/navigation/agentNavigationTypes';
import type {
CardsStackParamList,
HomeStackParamList,
InsuranceStackParamList,
LoanStackParamList,
MainTabParamList,
PaymentsStackParamList,
ProfileStackParamList,
ServicesStackParamList,
} from './types';
/**
* Maps every navigable route to the feature that owns it.
*
* These maps are the contract behind navigation protection: a route whose
* feature is disabled is never registered on its navigator, so it cannot be
* reached by `navigate()`, by a deep link, or by restoring persisted state — the
* route simply does not exist. Hiding the menu entry alone would not achieve
* that.
*
* Routes intentionally left unmapped are always available (e.g. the landing
* screen of a stack, which is governed by its parent tab).
*/
type RouteFeatureMap<T> = Partial<Record<keyof T, FeatureKey>>;
export const MAIN_TAB_FEATURES: RouteFeatureMap<MainTabParamList> = {
Home: FeatureKey.HOME,
Payments: FeatureKey.PAYMENTS,
Services: FeatureKey.SERVICES,
Cards: FeatureKey.CARDS,
Profile: FeatureKey.PROFILE,
};
export const HOME_ROUTE_FEATURES: RouteFeatureMap<HomeStackParamList> = {
HomeMain: FeatureKey.HOME,
GlobalSearch: FeatureKey.SEARCH_GLOBAL,
Notifications: FeatureKey.NOTIFICATIONS_LIST,
MiniStatement: FeatureKey.HOME_ACCOUNTS_MINI_STATEMENT,
AccountDetails: FeatureKey.HOME_ACCOUNTS_DETAILS,
ReKyc: FeatureKey.HOME_ACCOUNTS_REKYC,
EPassbook: FeatureKey.HOME_ACCOUNTS_EPASSBOOK,
VideoKyc: FeatureKey.HOME_ACCOUNTS_VIDEO_KYC,
AccountsMmid: FeatureKey.HOME_ACCOUNTS_MMID,
LoanFlow: FeatureKey.LOANS,
InsuranceFlow: FeatureKey.INSURANCE,
};
export const PAYMENTS_ROUTE_FEATURES: RouteFeatureMap<PaymentsStackParamList> = {
PaymentsHub: FeatureKey.PAYMENTS_HUB,
BeneficiariesList: FeatureKey.PAYMENTS_BENEFICIARIES_LIST,
AddBeneficiary: FeatureKey.PAYMENTS_BENEFICIARIES_ADD,
TransferToMobile: FeatureKey.PAYMENTS_TRANSFER_MOBILE,
TransferToAccount: FeatureKey.PAYMENTS_TRANSFER_ACCOUNT,
TransferToOwnAccount: FeatureKey.PAYMENTS_TRANSFER_OWN,
ImpsMmid: FeatureKey.PAYMENTS_MMID,
PaymentConfirmation: FeatureKey.PAYMENTS_TRANSFER,
TransactionSuccess: FeatureKey.PAYMENTS_TRANSFER,
};
export const CARDS_ROUTE_FEATURES: RouteFeatureMap<CardsStackParamList> = {
CardsOverview: FeatureKey.CARDS_OVERVIEW,
CardDetails: FeatureKey.CARDS_DETAILS,
RequestPhysicalCard: FeatureKey.CARDS_REQUEST_PHYSICAL,
ApplyCreditCard: FeatureKey.CARDS_APPLY_CREDIT,
};
export const SERVICES_ROUTE_FEATURES: RouteFeatureMap<ServicesStackParamList> = {
ServicesHub: FeatureKey.SERVICES_HUB,
DepositsOverview: FeatureKey.SERVICES_DEPOSITS_OVERVIEW,
DepositDetails: FeatureKey.SERVICES_DEPOSITS_DETAILS,
OpenFixedDeposit: FeatureKey.SERVICES_DEPOSITS_OPEN_FD,
OpenRecurringDeposit: FeatureKey.SERVICES_DEPOSITS_OPEN_RD,
DepositCalculator: FeatureKey.SERVICES_DEPOSITS_CALCULATOR,
BalanceEnquiry: FeatureKey.SERVICES_BALANCE_ENQUIRY,
FateOfCheque: FeatureKey.SERVICES_FATE_OF_CHEQUE,
SetTpin: FeatureKey.SERVICES_TPIN_CHANGE,
ChequeServicesHub: FeatureKey.SERVICES_CHEQUE_HUB,
ChequeBookRequest: FeatureKey.SERVICES_CHEQUE_BOOK_REQUEST,
StopPayment: FeatureKey.SERVICES_CHEQUE_STOP_PAYMENT,
ChequeTracking: FeatureKey.SERVICES_CHEQUE_TRACKING,
PositivePay: FeatureKey.SERVICES_CHEQUE_POSITIVE_PAY,
CertificatesHub: FeatureKey.SERVICES_CERTIFICATES_HUB,
CertificateList: FeatureKey.SERVICES_CERTIFICATES_LIST,
ServiceRequestsHub: FeatureKey.SERVICES_REQUESTS_HUB,
StandingInstructions: FeatureKey.SERVICES_STANDING_INSTRUCTIONS,
Form15G15H: FeatureKey.SERVICES_FORM_15G15H,
Nomination: FeatureKey.SERVICES_NOMINATION,
BranchLocator: FeatureKey.SERVICES_BRANCH_LOCATOR,
AwarenessList: FeatureKey.SERVICES_AWARENESS_LIST,
AwarenessDetail: FeatureKey.SERVICES_AWARENESS_DETAIL,
FinancialProtectionMain: FeatureKey.SERVICES_PROTECTION_HUB,
SchemeDetails: FeatureKey.SERVICES_PROTECTION_SCHEME_DETAIL,
DeafHub: FeatureKey.SERVICES_PROTECTION_DEAF,
DeafClaimFlow: FeatureKey.SERVICES_PROTECTION_DEAF_CLAIM,
DicgcHub: FeatureKey.SERVICES_PROTECTION_DICGC,
SchemeEnrollment: FeatureKey.SERVICES_PROTECTION_ENROLLMENT,
SchemeSuccess: FeatureKey.SERVICES_PROTECTION_ENROLLMENT,
};
export const LOAN_ROUTE_FEATURES: RouteFeatureMap<LoanStackParamList> = {
LoanDashboard: FeatureKey.LOANS_DASHBOARD,
LoanTypesListing: FeatureKey.LOANS_TYPES,
LoanEligibilityCheck: FeatureKey.LOANS_ELIGIBILITY,
LoanCalculator: FeatureKey.LOANS_CALCULATOR,
ApplyLoan: FeatureKey.LOANS_APPLY,
LoanTracking: FeatureKey.LOANS_TRACKING,
ActiveLoanDetails: FeatureKey.LOANS_ACTIVE_DETAILS,
EmiPayment: FeatureKey.LOANS_EMI_PAYMENT,
LoanHistory: FeatureKey.LOANS_HISTORY,
LoanDocuments: FeatureKey.LOANS_DOCUMENTS,
};
export const INSURANCE_ROUTE_FEATURES: RouteFeatureMap<InsuranceStackParamList> = {
InsuranceDashboard: FeatureKey.INSURANCE_DASHBOARD,
BuyInsurance: FeatureKey.INSURANCE_BUY,
InsurancePolicies: FeatureKey.INSURANCE_POLICIES,
InsurancePolicyDetails: FeatureKey.INSURANCE_POLICY_DETAILS,
InsuranceRenewals: FeatureKey.INSURANCE_RENEWALS,
InsuranceClaims: FeatureKey.INSURANCE_CLAIMS,
InsuranceDocuments: FeatureKey.INSURANCE_DOCUMENTS,
InsuranceCalculator: FeatureKey.INSURANCE_CALCULATOR,
InsuranceComparison: FeatureKey.INSURANCE_COMPARISON,
InsuranceNotifications: FeatureKey.INSURANCE_NOTIFICATIONS,
};
export const PROFILE_ROUTE_FEATURES: RouteFeatureMap<ProfileStackParamList> = {
ProfileMain: FeatureKey.PROFILE_MAIN,
AccountManagement: FeatureKey.PROFILE_ACCOUNTS,
AddAccount: FeatureKey.PROFILE_ACCOUNTS_ADD,
HelpSupport: FeatureKey.PROFILE_SUPPORT,
LegalInfo: FeatureKey.PROFILE_LEGAL,
EditProfile: FeatureKey.PROFILE_EDIT,
LanguageSettings: FeatureKey.PROFILE_SETTINGS_LANGUAGE,
NotificationSettings: FeatureKey.PROFILE_SETTINGS_NOTIFICATIONS,
PrivacySettings: FeatureKey.PROFILE_SETTINGS_PRIVACY,
AboutApp: FeatureKey.PROFILE_ABOUT,
ChangePin: FeatureKey.PROFILE_SECURITY_CHANGE_PIN,
ChangePassword: FeatureKey.PROFILE_SECURITY_CHANGE_PASSWORD,
SecuritySettings: FeatureKey.PROFILE_SECURITY,
FeatureFlags: FeatureKey.PROFILE_FEATURE_FLAGS,
};
export const AGENT_TAB_FEATURES: RouteFeatureMap<AgentTabParamList> = {
AgentDashboard: FeatureKey.AGENT_DASHBOARD,
AgentCustomers: FeatureKey.AGENT_CUSTOMERS,
AgentCollections: FeatureKey.AGENT_COLLECTIONS,
AgentLoans: FeatureKey.AGENT_LOANS,
AgentProfile: FeatureKey.AGENT_PROFILE,
};
export const AGENT_DASHBOARD_ROUTE_FEATURES: RouteFeatureMap<AgentDashboardStackParamList> = {
AgentDashboardMain: FeatureKey.AGENT_DASHBOARD_MAIN,
AgentRoute: FeatureKey.AGENT_DASHBOARD_ROUTE,
};
export const AGENT_CUSTOMERS_ROUTE_FEATURES: RouteFeatureMap<AgentCustomersStackParamList> = {
CustomerList: FeatureKey.AGENT_CUSTOMERS_LIST,
CustomerDetail: FeatureKey.AGENT_CUSTOMERS_DETAIL,
// The add/edit screen is reachable for either capability; the screen itself
// gates the create-vs-edit affordances.
AddEditCustomer: FeatureKey.AGENT_CUSTOMERS,
CustomerInstallmentHistory: FeatureKey.AGENT_CUSTOMERS_INSTALLMENT_HISTORY,
};
export const AGENT_COLLECTIONS_ROUTE_FEATURES: RouteFeatureMap<AgentCollectionsStackParamList> = {
DailyCollection: FeatureKey.AGENT_COLLECTIONS_DAILY,
CollectionHistory: FeatureKey.AGENT_COLLECTIONS_HISTORY,
CollectionReceipt: FeatureKey.AGENT_COLLECTIONS_RECEIPT,
};
export const AGENT_LOANS_ROUTE_FEATURES: RouteFeatureMap<AgentLoansStackParamList> = {
AgentLoansMain: FeatureKey.AGENT_LOANS_LIST,
CreateLoanRequest: FeatureKey.AGENT_LOANS_CREATE_REQUEST,
AccountOpening: FeatureKey.AGENT_LOANS_ACCOUNT_OPENING,
CommissionDashboard: FeatureKey.AGENT_LOANS_COMMISSION,
};
export const AGENT_PROFILE_ROUTE_FEATURES: RouteFeatureMap<AgentProfileStackParamList> = {
AgentProfileMain: FeatureKey.AGENT_PROFILE_MAIN,
SmsHistory: FeatureKey.AGENT_PROFILE_SMS_HISTORY,
AgentChangePassword: FeatureKey.AGENT_PROFILE_CHANGE_PASSWORD,
};
/**
* Returns a predicate telling a navigator whether a given route may be
* registered. Unmapped routes are always allowed.
*
* const canRoute = useRouteGuard(SERVICES_ROUTE_FEATURES);
* …
* {canRoute('StopPayment') && <Stack.Screen name="StopPayment" … />}
*/
export const useRouteGuard = <T,>(
map: RouteFeatureMap<T>,
): ((route: keyof T) => boolean) => {
const version = useFeatureVersion();
const enabled = useMemo(() => {
const allowed = new Set<keyof T>();
for (const route of Object.keys(map) as Array<keyof T>) {
const key = map[route];
if (!key || isFeatureEnabled(key)) {
allowed.add(route);
}
}
return allowed;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [map, version]);
return useCallback(
(route: keyof T) => !(route in map) || enabled.has(route),
[enabled, map],
);
};
/** Non-reactive equivalent of `useRouteGuard`, for use outside React. */
export const isRouteEnabled = <T,>(map: RouteFeatureMap<T>, route: keyof T): boolean => {
const key = map[route];
return !key || isFeatureEnabled(key);
};
+139
View File
@@ -0,0 +1,139 @@
import React from 'react';
import { StyleSheet, View } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import type { BottomTabNavigationOptions } from '@react-navigation/bottom-tabs';
import Ionicons from '@react-native-vector-icons/ionicons';
import { useTheme, fontFamily } from '../theme';
import { TAB_BAR_HEIGHT } from '../utils/layoutConstants';
type IoniconName = React.ComponentProps<typeof Ionicons>['name'];
/**
* The inset is clamped rather than used raw, and the bar's total height stays
* `TAB_BAR_HEIGHT`.
*
* Fourteen screens pad their scroll content by `TAB_BAR_HEIGHT + n` to clear
* this bar, so a height that moved with the device would leave every one of
* them either short (content trapped under the bar) or long (a band of dead
* space). Holding the height and spending the inset on padding keeps that
* contract while still respecting the gesture bar: the floor stops the labels
* sitting on the screen edge on a device that reports nothing, and the ceiling
* stops a 34pt home indicator squeezing the icons into the labels.
*/
const MIN_BOTTOM_PAD = 10;
const MAX_BOTTOM_PAD = 26;
const TOP_PAD = 8;
/*
* The box the pill and label have to live in is what the padding leaves of
* TAB_BAR_HEIGHT, and the tightest case is the one to size against: a 34pt
* home indicator clamps to 26, so 90 - 8 - 26 = 56pt. A 28pt pill, a 4pt gap
* and a 10pt uppercase label come to about 50, which leaves the arrangement a
* few points of slack rather than clipping the label's descenders.
*/
/**
* Everything the two bottom tab bars share.
*
* They were two copies of one style block, and both carried the same faults:
* `theme.colors.white` for the background, which the token's own doc comment
* warns against — it is literal white in *both* themes, so in dark mode the bar
* was a lit slab under a navy app, and `textMuted` on it fell to grey-on-white
* for the inactive labels. The padding was a hard 30pt that knew nothing about
* the device, and the icon frame set `borderColor` with no `borderWidth`, so
* the bordered pill it was describing never drew at all.
*/
export const useTabBarScreenOptions = (): BottomTabNavigationOptions => {
const theme = useTheme();
const insets = useSafeAreaInsets();
const paddingBottom = Math.min(Math.max(insets.bottom, MIN_BOTTOM_PAD), MAX_BOTTOM_PAD);
return {
tabBarActiveTintColor: theme.colors.primary,
tabBarInactiveTintColor: theme.colors.textSecondary,
tabBarStyle: {
// An elevated surface, not literal white: #FFFFFF in light mode, so
// nothing changes there, and the dark ramp's raised tone in dark mode.
backgroundColor: theme.colors.backgroundElevated,
borderTopColor: theme.colors.cardBorder,
borderTopWidth: StyleSheet.hairlineWidth,
// Curved top edge, on the same radius the bottom sheet uses for the same
// gesture — both are surfaces that rise from the bottom edge, so sharing
// the token keeps them reading as one family.
//
// No `overflow: 'hidden'` to go with it: the background already clips to
// the radius, and on iOS that property on this view would take the
// shadow below with it.
borderTopLeftRadius: theme.radius.xxl,
borderTopRightRadius: theme.radius.xxl,
height: TAB_BAR_HEIGHT,
paddingBottom,
paddingTop: TOP_PAD,
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
// Upward, because the bar lifts off content that runs underneath it.
// Elevation 20 was two steps above the modal layer for a surface that is
// never above a modal; 8 sits it just clear of the page.
elevation: 8,
shadowColor: theme.colors.shadow,
shadowOffset: { width: 0, height: -3 },
shadowOpacity: 0.08,
shadowRadius: 10,
},
tabBarLabelStyle: {
fontSize: 10,
fontFamily: fontFamily.semibold,
// 4, not 6: the pill below is taller than the bare icon it replaced, and
// the content box is what the bottom inset leaves of a fixed height.
marginTop: 4,
textTransform: 'uppercase',
letterSpacing: 0.4,
},
tabBarItemStyle: {
paddingVertical: 2,
},
};
};
/**
* The icon, in a pill that fills on the focused tab.
*
* The fill is what marks the tab — colour alone is a 3:1 difference between two
* small glyphs, which is not a signal someone reads at a glance or in sunlight.
*/
export const TabBarIcon = ({
color,
focused,
name,
}: {
color: string;
focused: boolean;
name: IoniconName;
}) => {
const theme = useTheme();
return (
<View
style={[
styles.pill,
focused && { backgroundColor: theme.colors.primarySurface },
]}
>
<Ionicons name={name} size={20} color={color} />
</View>
);
};
const styles = StyleSheet.create({
pill: {
width: 46,
height: 28,
borderRadius: 14,
alignItems: 'center',
justifyContent: 'center',
},
});
+55 -8
View File
@@ -1,7 +1,15 @@
import type { NavigatorScreenParams } from '@react-navigation/native';
/**
* Registration state lives in the auth slice rather than in route params, so a
* screen that gets remounted cannot lose the single-use setup token mid-flow.
*/
export type AuthStackParamList = {
Login: undefined;
Register: undefined;
VerifyOtp: undefined;
CreateMpin: undefined;
MpinLocked: undefined;
};
import type { LoanCategory } from '../types/app';
@@ -24,9 +32,12 @@ export type HomeStackParamList = {
HomeMain: undefined;
GlobalSearch: undefined;
Notifications: undefined;
MiniStatement: { accountId: string };
AccountDetails: { accountId: string };
ReKyc: { accountId: string };
MiniStatement: { accountNumber: string };
AccountDetails: { accountNumber?: string } | undefined;
ReKyc: { accountNumber: string };
EPassbook: { accountNumber?: string } | undefined;
VideoKyc: { accountNumber?: string } | undefined;
AccountsMmid: undefined;
LoanFlow: NavigatorScreenParams<LoanStackParamList> | undefined;
InsuranceFlow: NavigatorScreenParams<InsuranceStackParamList> | undefined;
};
@@ -51,17 +62,48 @@ export type CardsStackParamList = {
ApplyCreditCard: undefined;
};
/** How a transfer settles. `Internal` never leaves the bank. */
export type TransferRail = 'IMPS' | 'NEFT' | 'RTGS' | 'Internal';
/**
* Who the money is going to, when it is not a payee already on file.
*
* The dedicated transfer screens collect the recipient inline, so the
* confirmation screen has to be able to describe someone who has no
* beneficiary record. Only `accountMasked` is ever rendered; `accountNumber`
* is carried solely so a "save as beneficiary" tick has something to save.
*/
export type AdHocRecipient = {
name: string;
bankName?: string;
accountMasked?: string;
accountNumber?: string;
ifsc?: string;
mobileNumber?: string;
mmid?: string;
};
export type PaymentsStackParamList = {
PaymentsHub: undefined;
BeneficiariesList: undefined;
AddBeneficiary: undefined;
TransferTypeSelection: { beneficiaryId: string };
PaymentForm: { beneficiaryId: string; transferType: 'IMPS' | 'NEFT' | 'RTGS' };
/** `beneficiaryId` prefills the form when arriving from a saved payee. */
TransferToMobile: { beneficiaryId?: string } | undefined;
TransferToAccount: { beneficiaryId?: string } | undefined;
TransferToOwnAccount: undefined;
ImpsMmid: undefined;
PaymentConfirmation: {
beneficiaryId: string;
transferType: 'IMPS' | 'NEFT' | 'RTGS';
transferType: TransferRail;
amount: number;
remarks?: string;
/** The debit account, by number — accounts have no local id any more. */
sourceAccountNumber?: string;
/** Set when the transfer started from a payee already on file. */
beneficiaryId?: string;
/** Set when the recipient was typed in on the form. */
recipient?: AdHocRecipient;
/** The "save as beneficiary" tick, actioned once the transfer succeeds. */
saveRecipient?: boolean;
};
TransactionSuccess: {
transactionId: string;
@@ -78,6 +120,9 @@ export type ServicesStackParamList = {
OpenFixedDeposit: undefined;
OpenRecurringDeposit: undefined;
DepositCalculator: { initialKind?: 'fd' | 'rd' } | undefined;
BalanceEnquiry: undefined;
FateOfCheque: undefined;
SetTpin: undefined;
ChequeServicesHub: undefined;
ChequeBookRequest: undefined;
StopPayment: undefined;
@@ -86,8 +131,9 @@ export type ServicesStackParamList = {
CertificateList: { category: string; title: string };
ServiceRequestsHub: undefined;
PositivePay: undefined;
SpecialInstructions: undefined;
StandingInstructions: undefined;
Form15G15H: undefined;
Nomination: { accountNumber?: string } | undefined;
BranchLocator: undefined;
AwarenessList: undefined;
AwarenessDetail: { articleId: string };
@@ -115,6 +161,7 @@ export type ProfileStackParamList = {
ChangePin: undefined;
ChangePassword: undefined;
SecuritySettings: undefined;
FeatureFlags: undefined;
};
export type MainTabParamList = {