Files
CBS-Mobile-App/ARCHITECTURE.md
Abin ToppoandClaude Opus 5.5 f960a119e8 Add Positive Pay, statement guide v1.0, onboarding, and switch every feature on
- 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>
2026-10-14 18:18:40 +05:30

510 lines
25 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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'`.