- Positive Pay on the 12 Oct /pps contract (account number, not account ID): OTP enrollment, cheque submit, saved cheques, detail and stop payment. Full flow passed on the phone against mobile-banking-backend.lincpay. - Statement & Mini Statement guide v1.0 (14 Oct): mini statement JSON on Home and the Statement screen; dated PDF export with the password rule. - Onboarding slides and splash screen, Inter font, shared Icon and TypewriterText components, nearby branches via OpenStreetMap. - Every feature switched on; the 14 Oct switch set is kept in featureConfig.stage-2026-10-14.ts. Loans run on demo data until the backend has /loans routes. - npm run android wraps run-android for shells spawned from VS Code. - OpenProject work packages updated to 14-10-2026 status, with screenshots of Positive Pay, statement, loans and cards. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
510 lines
25 KiB
Markdown
510 lines
25 KiB
Markdown
# BankCBS — Mobile Application Architecture
|
||
|
||
A React Native mobile banking client for a Core Banking System (CBS) backend. It
|
||
serves **two distinct applications from one binary** — a retail customer app and
|
||
a field-agent app — selected at runtime by the authenticated user's role, with
|
||
every navigable route, tab and control individually switchable through a
|
||
hierarchical feature-flag engine.
|
||
|
||
| | |
|
||
|---|---|
|
||
| **Package** | `bankcbs` v1.0.0 (`com.bankcbs.mobile`) |
|
||
| **Runtime** | React Native 0.83.4, React 19.2, TypeScript 5.9 (`strict`, `noUncheckedIndexedAccess`) |
|
||
| **State** | Redux Toolkit 2.11 + RTK Query, `react-redux` 9.2 |
|
||
| **Navigation** | React Navigation 7 (native-stack + bottom-tabs) |
|
||
| **Transport** | Axios 1.14 wrapped by a custom RTK Query `baseQuery` |
|
||
| **Crypto / secrets** | `node-forge` (RSA-OAEP), `react-native-keychain`, `react-native-biometrics` |
|
||
| **Scale** | 381 source files · ~58,300 LOC · 103 screens · 17 modules · 182 feature keys |
|
||
|
||
---
|
||
|
||
## 1. Layering
|
||
|
||
Dependencies flow strictly downward. Nothing in a lower layer imports from a
|
||
higher one.
|
||
|
||
```
|
||
┌───────────────────────────────────────────────────────────────────┐
|
||
│ App.tsx / index.js composition root, provider order │
|
||
├───────────────────────────────────────────────────────────────────┤
|
||
│ src/navigation/ role routing + feature-gated routes │
|
||
├───────────────────────────────────────────────────────────────────┤
|
||
│ src/modules/<domain>/ 17 self-contained domain modules │
|
||
│ screens · components · hooks · store · services · types · utils│
|
||
├───────────────────────────────────────────────────────────────────┤
|
||
│ src/features/ feature-flag engine (cross-cutting) │
|
||
│ src/components/ 16 shared presentational primitives │
|
||
│ src/theme/ design tokens + ThemeProvider │
|
||
│ src/hooks/ app-wide hooks (bootstrap, session…) │
|
||
├───────────────────────────────────────────────────────────────────┤
|
||
│ src/services/ api · security · storage · device … │
|
||
│ src/app/ store, rootReducer, typed hooks │
|
||
│ src/config/ constants/ types/ utils/ │
|
||
└───────────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### Directory map
|
||
|
||
| Path | Responsibility |
|
||
|---|---|
|
||
| [src/app/](src/app/) | Store configuration, `rootReducer`, typed `useAppDispatch`/`useAppSelector` |
|
||
| [src/navigation/](src/navigation/) | Root/tab/stack navigators, param lists, route→feature maps |
|
||
| [src/modules/](src/modules/) | Domain modules (see §7) |
|
||
| [src/features/](src/features/) | Feature-flag service, provider, gates, hooks |
|
||
| [src/services/api/](src/services/api/) | Axios client, interceptors, endpoint registry, error catalogue |
|
||
| [src/services/security/](src/services/security/) | Session manager, device identity, MPIN crypto, biometrics |
|
||
| [src/services/storage/](src/services/storage/) | Keychain/AsyncStorage-backed secure storage |
|
||
| [src/components/](src/components/) | Shared UI primitives, barrel-exported |
|
||
| [src/theme/](src/theme/) | `FintechTheme` token contract + context |
|
||
| [src/constants/](src/constants/) | Feature keys, route names, RTK Query tags, translations, fixtures |
|
||
| [src/types/](src/types/) | `api.ts` (client errors), `app.ts` (domain), `cbs.ts` (wire types) |
|
||
|
||
---
|
||
|
||
## 2. Composition root
|
||
|
||
[App.tsx](App.tsx) fixes the provider order, and the order is load-bearing:
|
||
|
||
```
|
||
GestureHandlerRootView
|
||
└── SafeAreaProvider
|
||
└── redux Provider ← store must exist before features read session
|
||
└── FeatureProvider ← flags must resolve before navigators register routes
|
||
└── ThemeProvider
|
||
└── AppShell
|
||
├── useAppBootstrap()
|
||
└── ActivityTracker (touch capture → session keep-alive)
|
||
├── RootNavigator
|
||
└── ToastHost
|
||
```
|
||
|
||
[index.js](index.js) handles three pre-React concerns:
|
||
|
||
1. `react-native-get-random-values` is imported **first** — `uuid` binds
|
||
`crypto.getRandomValues` at module load, and both the device ID and the MPIN
|
||
nonce import `uuid`, so a later import throws at startup.
|
||
2. `notifee.onBackgroundEvent` is registered (required by Notifee even as a no-op).
|
||
3. `Text` / `TextInput` `defaultProps` are patched to the `Inter-Regular` font family.
|
||
|
||
### Bootstrap sequence
|
||
|
||
[src/hooks/useAppBootstrap.ts](src/hooks/useAppBootstrap.ts) runs once on mount:
|
||
|
||
1. `bootstrapAuthSession()` — mints the device ID and **validates any stored
|
||
session against the server** before trusting it.
|
||
2. `seedAllData()` — populates the fixture-backed modules (profile, dashboard,
|
||
beneficiaries, payees, deposits, service requests, loans, insurance, cards).
|
||
3. `markAppReady()`, then `registerForPushNotifications()`.
|
||
4. Registers a global unauthorized handler that dispatches `logoutSession()` and
|
||
raises a toast.
|
||
|
||
It also re-seeds on every sign-in transition, because logout wipes the entire
|
||
Redux tree (§4), and on sign-*out* re-reads only the sign-in context — a full
|
||
bootstrap would raise `isHydrating` and blank the app behind a loader on the way
|
||
out.
|
||
|
||
---
|
||
|
||
## 3. Navigation architecture
|
||
|
||
```
|
||
RootNavigator ← isHydrating? Loader : isAuthenticated?
|
||
├── AuthNavigator (unauthenticated)
|
||
│ Login · Register · VerifyOtp · CreateMpin · MpinLocked
|
||
├── MainTabNavigator (role: 'User')
|
||
│ ├── Home → HomeNavigator (+ nested LoanFlow, InsuranceFlow)
|
||
│ ├── Payments → PaymentsNavigator
|
||
│ ├── Services → ServicesNavigator (Hub; tabPress resets the stack)
|
||
│ ├── Cards → CardsNavigator
|
||
│ └── Profile → ProfileNavigator (lives in src/modules/profile/navigation/)
|
||
└── AgentNavigator (role: 'Agent')
|
||
├── AgentDashboard · AgentCustomers · AgentCollections
|
||
└── AgentLoans · AgentProfile (each a nested native stack)
|
||
```
|
||
|
||
Three deliberate decisions are documented in the code:
|
||
|
||
- **Role switch, not a route.** [RootNavigator.tsx](src/navigation/RootNavigator.tsx)
|
||
picks the app stack from `auth.role`. Disabling `FeatureKey.AGENT` retires the
|
||
whole field-agent app; an already-signed-in agent falls back to the customer
|
||
app rather than landing on an empty navigator.
|
||
- **Initial route computed, never navigated to.**
|
||
[AuthNavigator.tsx](src/navigation/AuthNavigator.tsx) derives
|
||
`initialRouteName` from a stored profile instead of `replace()`-ing in an
|
||
effect — an effect-driven replace races navigation-container readiness and is
|
||
silently dropped, and the warning that would reveal it is stripped from
|
||
release builds. `signInContextLoaded` gates this so a null profile means
|
||
"unknown" rather than "never registered" before the Keystore has been read.
|
||
- **`CreateMpin` and `MpinLocked` disable the back gesture.** The setup token is
|
||
single-use; leaving halfway means restarting registration.
|
||
|
||
### Route-level feature gating
|
||
|
||
[src/navigation/featureRoutes.ts](src/navigation/featureRoutes.ts) maps every
|
||
route name to the `FeatureKey` that owns it, and each navigator wraps its
|
||
`Screen` in `canRoute(...)`:
|
||
|
||
```tsx
|
||
const canRoute = useRouteGuard(CARDS_ROUTE_FEATURES);
|
||
{canRoute('CardDetails') && <Stack.Screen name="CardDetails" component={CardDetailsScreen} />}
|
||
```
|
||
|
||
A disabled route is **never registered**, so it cannot be reached by
|
||
`navigate()`, a deep link, or restored navigation state. Hiding the menu entry
|
||
alone would not achieve that.
|
||
[FeatureRoute.tsx](src/navigation/FeatureRoute.tsx) is a secondary screen-level
|
||
guard for defence in depth.
|
||
|
||
---
|
||
|
||
## 4. State management
|
||
|
||
[src/app/store.ts](src/app/store.ts) · [src/app/rootReducer.ts](src/app/rootReducer.ts)
|
||
|
||
16 domain slices plus two RTK Query reducers (`baseApi`, `schemesApi`).
|
||
`serializableCheck` is disabled.
|
||
|
||
**Logout wipes everything.** `rootReducer` intercepts
|
||
`auth/logoutSession/fulfilled` and sets `state = undefined`, so no slice can
|
||
leak data across sessions. Re-seeding on sign-in (§2) is the counterpart to this.
|
||
|
||
Three coexisting data-access patterns:
|
||
|
||
| Pattern | Where | Used by |
|
||
|---|---|---|
|
||
| `baseApi.injectEndpoints` (RTK Query over Axios) | `modules/*/services/*Api.ts` | agent, cards, deposit, home ×2, insurance, loan, payments, profile, services ×2, transfer |
|
||
| `createSlice` + `createAsyncThunk` | `modules/*/store/` | auth, cards, deposit, home, insurance, loan, payments, profile, services, transfer, ui |
|
||
| Standalone `createApi` | `modules/schemes/services/schemesApi.ts` | schemes only — its own reducer path, fixture `queryFn`s |
|
||
|
||
Cache invalidation runs on a central tag list —
|
||
[src/constants/queryTags.ts](src/constants/queryTags.ts), 25 tags — registered on
|
||
`baseApi` so any module can invalidate another's cache.
|
||
|
||
---
|
||
|
||
## 5. Data & API layer
|
||
|
||
### The envelope
|
||
|
||
Every CBS response shares one wrapper ([src/types/cbs.ts](src/types/cbs.ts)):
|
||
|
||
```ts
|
||
interface CbsEnvelope<T> {
|
||
success: boolean; message: string;
|
||
data?: T; // present on success; VALIDATION_ERROR carries a field map here
|
||
errorCode?: string; // only on failure — this is what the app switches on
|
||
timestamp: string;
|
||
}
|
||
```
|
||
|
||
### Interceptors
|
||
|
||
[src/services/api/interceptors.ts](src/services/api/interceptors.ts) is where the
|
||
contract is enforced:
|
||
|
||
- **Request:** attaches `Authorization: Bearer <token>` from `secureStorage`.
|
||
- **Response:** branches on `envelope.success`, **not the HTTP status** — a
|
||
failure can arrive with a 2xx, and treating it as a payload hands callers
|
||
`undefined`. Failures are thrown as a `CbsApiError`; successes are unwrapped so
|
||
callers read `response.data` as the payload.
|
||
- **Session mirroring:** any call carrying an `Authorization` header calls
|
||
`sessionManager.touch()`, because that same call reset the server's idle timer.
|
||
- **Session loss:** only `SESSION_INVALID` clears storage and fires the global
|
||
unauthorized handlers. `INVALID_CREDENTIALS` shares the 401 but means a wrong
|
||
MPIN on the sign-in form — bouncing the user out of a screen they aren't in yet
|
||
would be nonsense.
|
||
|
||
### Error catalogue
|
||
|
||
[src/services/api/cbsErrors.ts](src/services/api/cbsErrors.ts) enumerates the
|
||
**31 backend error codes** as a `const` tuple, plus 3 client-synthesised codes
|
||
(`NETWORK_ERROR`, `REQUEST_TIMEOUT`, `UNKNOWN_ERROR`). Each has authored
|
||
`{ title, description, retryable }` copy, so no failure can render as a blank
|
||
toast — asserted by [cbsErrors.test.ts](src/services/api/cbsErrors.test.ts).
|
||
`errorCode` is treated as the stable contract; `message` is assumed re-wordable
|
||
server-side, so no branch reads the message or the status.
|
||
|
||
### Endpoint registry & mock strategy
|
||
|
||
All paths live in one object,
|
||
[src/services/api/endpoints.ts](src/services/api/endpoints.ts), with
|
||
parameterised routes as functions.
|
||
|
||
`resolveWithMock` in
|
||
[serviceHelpers.ts](src/services/api/serviceHelpers.ts) returns fixtures when
|
||
`appConfig.features.useMockServer` is set. Per its own contract, endpoints the
|
||
CBS API actually implements **must not** go through it — they call `apiClient`
|
||
directly so failures surface rather than being papered over with demo data.
|
||
Live today: registration, sign-in, accounts/MMID, payees. Fixture-backed:
|
||
dashboard, cards, loans, insurance, deposits, service requests, the agent app.
|
||
|
||
---
|
||
|
||
## 6. Feature-flag engine
|
||
|
||
The most distinctive part of the architecture: **182 keys** covering every
|
||
module, screen, tab, menu row, button and action.
|
||
|
||
### Keys encode the hierarchy
|
||
|
||
[src/constants/featureKeys.ts](src/constants/featureKeys.ts) declares a
|
||
dot-notation enum; the service derives parent/child from the path itself, so the
|
||
tree can never drift from the naming.
|
||
|
||
```
|
||
loans → loans.apply → loans.apply.submit
|
||
```
|
||
|
||
[featureConfig.ts](src/features/featureConfig.ts) is a flat
|
||
`Record<FeatureKey, FeatureDefinition>` **on purpose** — the `Record` makes
|
||
TypeScript reject the file if any declared key lacks config, which a nested
|
||
object literal cannot guarantee.
|
||
|
||
### Resolution
|
||
|
||
[featureService.ts](src/features/featureService.ts) — one instance, holding a
|
||
registry, a layer stack, a resolution cache, and a listener set:
|
||
|
||
```
|
||
default → environment → remote → tenant → user (lowest → highest priority)
|
||
```
|
||
|
||
A feature is enabled only if it exists, its own flag is on, **and every ancestor
|
||
resolves enabled**. Resolution is **fail-closed**: an unknown key, or a key with
|
||
missing config, is `false` — in a banking app an unrecognised flag must hide the
|
||
feature, never reveal it. `validate()` runs in `__DEV__` and reports
|
||
`missing-config`, `unknown-key` and `missing-parent` issues, catching a typo like
|
||
`loans.aply.submit` that would otherwise become a root feature no parent could
|
||
switch off.
|
||
|
||
### Consumption surface
|
||
|
||
| API | Use |
|
||
|---|---|
|
||
| `useFeature(key)` / `useFeatures([...])` | reactive boolean(s); subscribes via `useSyncExternalStore` on a version counter |
|
||
| `<FeatureGate feature={...} mode="all\|any">` | renders children or **nothing** — deliberately no `fallback` prop, so a disabled feature leaves no trace: no greyed control, no "coming soon", no hidden-but-present node |
|
||
| `<DynamicFeatureGate feature="string">` | JSON-driven UI where the key arrives as data |
|
||
| `useFeatureAction` / `guardFeature` | guards a *handler* some other path may still hold |
|
||
| `useRouteGuard(map)` | navigator-level route registration (§3) |
|
||
| `FeatureProvider` | resolves async remote/tenant layers at startup, exposes `ready` |
|
||
|
||
`FeatureProvider` swallows loader failures so the app always falls back to
|
||
compiled-in defaults rather than blocking startup. Remote and tenant loaders are
|
||
optional props — nothing is wired to Firebase Remote Config or a config API yet,
|
||
but the seam exists without touching a single UI component.
|
||
|
||
---
|
||
|
||
## 7. Module anatomy
|
||
|
||
Every module under [src/modules/](src/modules/) follows the same internal shape,
|
||
so any module is navigable once one of them is:
|
||
|
||
```
|
||
modules/<domain>/
|
||
├── screens/ route components
|
||
├── components/ domain-specific presentational pieces
|
||
├── hooks/ form/flow logic extracted from screens
|
||
├── store/ <domain>Slice.ts · <domain>Thunk.ts · <domain>Selectors.ts
|
||
├── services/ <domain>Api.ts (baseApi.injectEndpoints)
|
||
├── types/ domain types
|
||
├── utils/ calculators, validators, fixtures
|
||
├── constants/ domain constants (agent, awareness, schemes)
|
||
└── navigation/ only where the module owns its own navigator (agent, profile)
|
||
```
|
||
|
||
| Module | Screens | Components | Store | Services | Hooks | Files |
|
||
|---|---:|---:|---:|---:|---:|---:|
|
||
| agent | 18 | 9 | 2 | 1 | 5 | 41 |
|
||
| services | 13 | 6 | 3 | 2 | 3 | 30 |
|
||
| profile | 12 | 5 | 3 | 1 | 1 | 25 |
|
||
| insurance | 10 | 9 | 3 | 2 | 2 | 29 |
|
||
| loan | 10 | 11 | 3 | 1 | 4 | 33 |
|
||
| home | 7 | 7 | 3 | 2 | 1 | 22 |
|
||
| payments | 7 | 1 | 4 | 1 | 1 | 17 |
|
||
| schemes | 7 | 5 | 2 | 1 | 1 | 18 |
|
||
| auth | 5 | 7 | 3 | 1 | 1 | 19 |
|
||
| deposit | 5 | 4 | 3 | 1 | 2 | 19 |
|
||
| cards | 4 | 9 | 3 | 1 | 2 | 21 |
|
||
| awareness | 2 | 1 | — | — | 1 | 6 |
|
||
| notifications | 1 | — | 1 | — | — | 2 |
|
||
| search | 1 | — | — | — | — | 2 |
|
||
| employee | 1 | — | — | — | — | 1 |
|
||
| transfer | — | 1 | 3 | 1 | 1 | 8 |
|
||
| ui | — | — | 3 | — | 1 | 4 |
|
||
|
||
`transfer` is state-and-service only (its screens live under `payments`); `ui`
|
||
holds cross-cutting UI state (toasts, app-ready, session-expired).
|
||
|
||
---
|
||
|
||
## 8. Security & session architecture
|
||
|
||
### MPIN never leaves the device in the clear
|
||
|
||
[mpinCrypto.ts](src/services/security/mpinCrypto.ts) encrypts
|
||
`{ mpin, nonce, ts }` under the server's RSA public key (**RSA-OAEP, SHA-256,
|
||
MGF1 explicitly SHA-256**) and sends it as `encryptedMpin`. Three integration
|
||
hazards are called out in the source, all of which fail silently:
|
||
|
||
- MGF1 **must** be passed explicitly. node-forge happens to default it to `md`,
|
||
but Java's `OAEPWithSHA-256AndMGF1Padding` leaves MGF1 on SHA-1 — relying on a
|
||
default produces ciphertext the server cannot decrypt, with a useless error.
|
||
- `ts` is in **seconds**. Milliseconds read as ~55,000 years ahead and are rejected.
|
||
- `nonce` must be fresh per call or it is refused as a replay.
|
||
|
||
The key is fetched from `/crypto/public-key` and cached; `PINNED_PUBLIC_KEY` is a
|
||
fallback for when the fetch fails. `invalidate()` drops the cache after a
|
||
`MALFORMED_REQUEST`, which is what a stale key looks like client-side (the server
|
||
cannot say "your key is wrong" without leaking whether the profile exists).
|
||
|
||
### Device binding
|
||
|
||
[deviceIdentity.ts](src/services/security/deviceIdentity.ts) mints one UUID on
|
||
first launch, persisted to the Keychain under
|
||
`ACCESSIBLE.AFTER_FIRST_UNLOCK_THIS_DEVICE_ONLY` (not `WHEN_UNLOCKED_*` — the ID
|
||
is not a secret and sign-in must read it *before* the user authenticates). It is
|
||
memoised in memory, and concurrent cold-start callers share one in-flight promise
|
||
— otherwise each would mint a UUID, last write wins, and the device silently
|
||
unbinds. The registered profile is bound to this value; a mismatch returns
|
||
`DEVICE_NOT_BOUND`, which reads like a server fault but is a client bug.
|
||
|
||
### Secure storage
|
||
|
||
[secureStorage.ts](src/services/storage/secureStorage.ts) keeps three Keychain
|
||
services — session, last profile, biometric MPIN — plus a biometric-enabled flag
|
||
in AsyncStorage (the device ID is a fourth, owned by `deviceIdentity`). The
|
||
session is cached in memory because the request interceptor needs the token on
|
||
*every* call and an Android Keystore read costs tens of ms.
|
||
|
||
The MPIN is retained **only** when the customer opts into biometric sign-in: the
|
||
API has no refresh token, so a session can only be opened by encrypting the MPIN,
|
||
which makes biometric sign-in impossible otherwise. It sits behind the device's
|
||
own biometric prompt, never in AsyncStorage. An unparseable session (written by
|
||
an older build) is treated as absent rather than crashing bootstrap.
|
||
|
||
### Session lifetime — three independent triggers
|
||
|
||
[sessionManager.ts](src/services/security/sessionManager.ts) adopts the server's
|
||
own limits at sign-in (`idleTimeoutSeconds`, `expiresAt`) so the app never
|
||
guesses and never drifts from what the backend enforces. It tracks **two clocks
|
||
separately**:
|
||
|
||
- `lastServerContactAt` — the only thing the server's idle window is measured
|
||
from, so the only thing expiry may be judged on.
|
||
- `lastInteractionAt` — screen touches. Because most of the app reads from
|
||
fixtures, a user can tap around for five minutes without one authenticated
|
||
call; treating that as idle would sign them out mid-use. So interaction drives
|
||
a **keep-alive**, never the expiry clock itself.
|
||
|
||
[useSessionTimeout.ts](src/hooks/useSessionTimeout.ts) polls every 5s and expires
|
||
on whichever of three triggers fires first — `idle`, `absolute`, or `background`
|
||
(returning to the foreground after `lockAfterBackgroundMs`, default 30s). Each
|
||
has its own copy; `background` is toned as informational because a deliberate
|
||
re-lock is not a failure, and dressing it up as one would train users to ignore
|
||
the banner that matters.
|
||
|
||
[ActivityTracker](src/components/ActivityTracker/) wraps the whole tree and reads
|
||
touches via `onStartShouldSetResponderCapture`, returning `false` so it sees every
|
||
touch on the way down but never becomes the responder — a wrapper that actually
|
||
captured touches would break every pressable beneath it.
|
||
|
||
`appConfig.security.requireMpinOnLaunch` defaults to **true**: every launch asks
|
||
for the MPIN rather than resuming a stored session, because there is no local
|
||
MPIN verifier — the only one is `POST /auth/login`, so "ask again" necessarily
|
||
means "open a fresh session".
|
||
|
||
---
|
||
|
||
## 9. Presentation layer
|
||
|
||
**Theme.** [src/theme/](src/theme/) defines a `FintechTheme` token contract —
|
||
colours (including a "Cyber Nexus" accent set), spacing, typography scale, radii,
|
||
shadows — consumed via `useTheme()`. `RootNavigator` maps these onto React
|
||
Navigation's `DarkTheme`.
|
||
|
||
**Shared primitives.** 16 barrel-exported components in
|
||
[src/components/](src/components/): `Button`, `Input`, `Card`, `Modal`,
|
||
`BottomSheet`, `DatePicker`, `Loader`, `EmptyState`, `BalanceCard`,
|
||
`TransactionItem`, `AppHeader`, `ToastHost`, `ActivityTracker`, plus three
|
||
keyboard/layout containers.
|
||
|
||
**`ScreenShell`** is the standard screen wrapper — gradient background, safe
|
||
area, `AppHeader`, optional scroll, per-platform `KeyboardAvoidingView` behaviour
|
||
(`padding` on iOS, `height` on Android), `withTabBar` padding so content never
|
||
hides behind the absolutely-positioned tab bar, and a `fixedFooter` slot.
|
||
|
||
**i18n.** [useTranslation.ts](src/hooks/useTranslation.ts) resolves keys against
|
||
[translations.ts](src/constants/translations.ts) using the language in the profile
|
||
slice, falling back to `en` then to the key itself. Currently a small dictionary
|
||
rather than a full i18n library.
|
||
|
||
**Notifications.**
|
||
[pushNotifications.ts](src/services/notifications/pushNotifications.ts) (Notifee)
|
||
for delivery; the `notifications` slice holds the in-app inbox.
|
||
|
||
---
|
||
|
||
## 10. Build & platform
|
||
|
||
- **Android permissions:** `INTERNET`, fine/coarse location (branch locator,
|
||
agent routes), `POST_NOTIFICATIONS`, `READ_PHONE_STATE` / `READ_PHONE_NUMBERS`
|
||
(SIM-card checks in [simCard.ts](src/services/device/simCard.ts)).
|
||
- **Metro** ([metro.config.js](metro.config.js)) block-lists native build output
|
||
— `android/app/build` alone exceeds 2 GB after a debug build, and without
|
||
watchman Metro walks it on startup and never becomes responsive.
|
||
- **TypeScript** is `strict` with `noUncheckedIndexedAccess` and `bundler` module
|
||
resolution.
|
||
- **Scripts:** `start`, `android`, `ios`, `lint`, `test`, `typecheck`,
|
||
`clean:android`.
|
||
- **Notable native deps:** MapLibre (branch locator / agent routes), Reanimated 4
|
||
+ Worklets, gesture-handler, `@gorhom/bottom-sheet`, SVG, linear-gradient,
|
||
haptic feedback, date-picker.
|
||
|
||
---
|
||
|
||
## 11. Testing
|
||
|
||
Jest, with 5 unit suites concentrated on the logic where a silent error is
|
||
costly:
|
||
|
||
| Suite | Covers |
|
||
|---|---|
|
||
| [featureService.test.ts](src/features/featureService.test.ts) | flag resolution, hierarchy, layer precedence, fail-closed behaviour |
|
||
| [cbsErrors.test.ts](src/services/api/cbsErrors.test.ts) | all 31 codes present and unique, copy for every code, retryability, `CbsApiError` identity |
|
||
| [validation.test.ts](src/utils/validation.test.ts) | shared input validators |
|
||
| [emi.test.ts](src/modules/loan/utils/emi.test.ts) | loan EMI maths |
|
||
| [depositCalculators.test.ts](src/modules/deposit/utils/depositCalculators.test.ts) | FD/RD maturity maths |
|
||
|
||
There is no component, navigation or integration test layer.
|
||
|
||
---
|
||
|
||
## 12. Current state & known gaps
|
||
|
||
Factual observations from the code, not recommendations:
|
||
|
||
1. **`useMockServer` is `true`** in [src/config/env.ts](src/config/env.ts).
|
||
Registration, sign-in, accounts/MMID and payees hit the live API; the other
|
||
modules resolve from ~1,390 lines of fixtures in
|
||
[src/constants/mockData.ts](src/constants/mockData.ts).
|
||
2. **`API_ORIGIN` is a hard-coded LAN address** (`http://192.168.6.138:8079`) over
|
||
plain HTTP. The file documents that debug builds permit cleartext and release
|
||
builds will need HTTPS or a network-security-config. There is no
|
||
per-environment config mechanism.
|
||
3. **`PINNED_PUBLIC_KEY` is committed in source** as the fallback when the key
|
||
fetch fails.
|
||
4. **`schemesApi` bypasses the shared stack** — its own `createApi` with
|
||
`fetchBaseQuery({ baseUrl: '/' })` and fixture `queryFn`s, including a
|
||
hard-coded `otp !== '123456'` check. It does not use `baseApi`, the endpoint
|
||
registry, or the CBS error catalogue.
|
||
5. **Remote and tenant flag layers are unwired.** `ENVIRONMENT_FEATURE_OVERRIDES`
|
||
is `{}` and `FeatureProvider` is mounted without loader props, so all 182 keys
|
||
resolve from the compiled-in defaults. The layering exists as a seam.
|
||
6. **Three data-access patterns coexist** (RTK Query, slice+thunk, standalone
|
||
`createApi`), several modules using two of them at once.
|
||
7. **`serializableCheck` is disabled** store-wide.
|
||
8. **The `employee` module is a single placeholder screen**; `EmployeeNavigator`
|
||
is defined but not reachable from `RootNavigator`, which routes only `'Agent'`
|
||
and `'User'`.
|