Update CBS Mobile App codebase
This commit is contained in:
+150
-131
@@ -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',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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
@@ -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 = {
|
||||
|
||||
Reference in New Issue
Block a user