Update CBS Mobile App codebase
This commit is contained in:
+509
@@ -0,0 +1,509 @@
|
||||
# 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 `Nunito-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://10.209.40.168:8089`) 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'`.
|
||||
Reference in New Issue
Block a user