update docs
Auto Tag on Version Change / check-version (push) Failing after 13m32s

This commit is contained in:
2026-06-13 21:30:12 +05:00
parent 281864347e
commit a8cd22cbe1
51 changed files with 1830 additions and 469 deletions
+10
View File
@@ -211,6 +211,16 @@ GET https://www.bankofmaldives.com.mv/internetbanking/web/profile
`302` redirect (to `/web/redirect` or similar). The server has auto-activated the sole profile and set the `blaze_identity` cookie. Skip Step 6 and proceed directly to [OAuth Token Exchange](03-oauth-token.md).
In this fast-path the client has no real `profile_id` to track, so a placeholder `BmlProfile` is synthesized (`BmlLoginFlow.kt:127-135`):
| Field | Value |
|---|---|
| `profileId` | `username` (used as a stable temporary ID — replaced by the real customer number after `fetchUserInfo`) |
| `name` | `"Personal"` |
| `type` | `"Profile"` |
| `profileType` | `"default"` |
| `autoActivated` | `true` — sentinel; `activateProfile` skips the Step 6 GET and goes straight to OAuth |
---
## Step 6 — Activate Profile
+4 -2
View File
@@ -135,19 +135,21 @@ POST https://www.bankofmaldives.com.mv/internetbanking/oauth/token
**Content-Type:** `application/x-www-form-urlencoded`
**HTTP `User-Agent` header**: the browser/web UA (same as the initial token exchange), not the app UA. See `BmlLoginFlow.kt:341`.
| Field | Value |
|---|---|
| `grant_type` | `refresh_token` |
| `refresh_token` | Stored refresh token |
| `client_id` | `98C83590-513F-4716-B02B-EC68B7D9E7E7` |
| `Device-ID` | Same device ID from the original login |
| `User-Agent` | App user agent string |
| `User-Agent` | App user agent string (form field — distinct from the HTTP header above) |
| `x-app-version` | `2.1.44.348` |
```bash
curl --request POST \
--url 'https://www.bankofmaldives.com.mv/internetbanking/oauth/token' \
--header 'User-Agent: bml-mobile-banking/348 ({manufacturer}; Android {version}; {model})' \
--header 'User-Agent: Mozilla/5.0 (Linux; Android {version}; {model}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/... Mobile Safari/537.36' \
--data 'grant_type=refresh_token' \
--data 'refresh_token=def50200aabbcc...' \
--data 'client_id=98C83590-513F-4716-B02B-EC68B7D9E7E7' \
+11 -6
View File
@@ -78,7 +78,7 @@ curl --request GET \
"currency": "MVR",
"account_status": "Active",
"prepaid_card": false,
"product_code": "VISA",
"product_code": "C1007",
"account_visible": false,
"cardBalance": {
"AvailableLimit": 0.0,
@@ -94,7 +94,7 @@ curl --request GET \
"currency": "MVR",
"account_status": "Active",
"prepaid_card": true,
"product_code": "VISA",
"product_code": "C1007",
"account_visible": true,
"cardBalance": {
"AvailableLimit": 200.00,
@@ -164,7 +164,7 @@ curl --request GET \
| Field | Type | Description |
|---|---|---|
| `prepaid_card` | `bool` | `true` for prepaid cards |
| `product_code` | `string` | Card scheme (e.g. `"VISA"`) |
| `product_code` | `string` | BML product code, always `Cxxxx` (e.g. `"C1007"`) — see mapping below |
| `account_visible` | `bool` | `true` for credit cards, `false` for debit cards |
| `cardBalance.AvailableLimit` | `number` | Available credit/prepaid balance |
| `cardBalance.CurrentBalance` | `number` | Current outstanding balance |
@@ -179,7 +179,12 @@ curl --request GET \
### Product Code → Card Name
The `product_code` field identifies the specific card product. Known mappings:
The `product_code` field identifies the specific card product. Resolution is two-tiered (`util/bmlapi/BmlCardParser.kt`):
1. **Network icon** (Visa / Mastercard / Amex chip in the corner) — by prefix: `C1*` → Visa, `C3*` → Amex, `C8*` → Mastercard, with `C8905` and `C8995` overridden to Visa.
2. **Card image asset** — by exact-match list below; unknown codes fall back to `defaultcard.png`.
Known asset mappings:
| `product_code` | Card name | Asset |
|---|---|---|
@@ -202,12 +207,12 @@ The `product_code` field identifies the specific card product. Known mappings:
| `C3009`, `C3019`, `C3029`, `C3099`, `C3088`, `C3188` | Amex Credit Gold | `amex_credit_gold` |
| `C1001`, `C1011`, `C1082`, `C1081`, `C1101`, `C1111`, `C1181`, `C1182` | Visa Debit Generic | `visa_debit_generic` |
| `C1003`, `C1013`, `C1083`, `C1084`, `C1103`, `C1113`, `C1183`, `C1184` | Visa Gold | `visa_gold` |
| `C1005`, `C1006`, `C1089` | Visa Debit Islamic | `visa_debit_islamic` |
| `C1005`, `C1006`, `C1030`, `C1089` | Visa Debit Islamic | `visa_debit_islamic` |
| `C1007`, `C1027`, `C1097`, `C1107`, `C1197`, `C1077`, `C1177` | Visa Debit | `visa_debit` |
| `C1009`, `C1019`, `C1085`, `C1086`, `C1109`, `C1119`, `C1185`, `C1186` | Visa Platinum | `visa_platinum` |
| `C1017` | Visa Infinite | `visa_infinite` |
| `C1020`, `C1021` | Visa Debit Platinum | `visa_debit_platinum` |
| `C1030`, `C1090`, `C1130`, `C1033`, `C1133` | Visa Corporate | `visa_corporate` |
| `C1090`, `C1130`, `C1033`, `C1133` | Visa Corporate | `visa_corporate` |
| `C1040`, `C1041`, `C1047`, `C1048`, `C1050`, `C1051`, `C1087`, `C1088`, `C1140`, `C1141`, `C1147`, `C1148`, `C1150`, `C1151`, `C1187`, `C1188` | Visa Student Black | `visa_student_black` |
| `C1059`, `C1062`, `C1070`, `C1072`, `C1159`, `C1162` | Mastercard Prepaid Business | `master_prepaid_business` |
| `C1061`, `C1063`, `C1071`, `C1073`, `C1161`, `C1163` | Mastercard | `master` |
+70
View File
@@ -187,6 +187,76 @@ Fall back to `bookingDate` as-is.
---
## Pending History
Locked / pending amounts for a CASA account (e.g. unsettled card authorisations, holds). Returned as a flat list — no pagination.
### Endpoint
```
GET https://www.bankofmaldives.com.mv/internetbanking/api/mobile/history/pending/{accountId}
```
| Path parameter | Description |
|---|---|
| `accountId` | Internal account ID (`id` field from [dashboard](04-dashboard.md)) |
### Headers
| Header | Value |
|---|---|
| `Authorization` | `Bearer <access_token>` |
| `User-Agent` | `bml-mobile-banking/348 ({manufacturer}; Android {version}; {model})` |
| `x-app-version` | `2.1.44.348` |
```bash
curl --request GET \
--url 'https://www.bankofmaldives.com.mv/internetbanking/api/mobile/history/pending/abc123def456' \
--header 'Authorization: Bearer <access_token>' \
--header 'User-Agent: bml-mobile-banking/348 ({manufacturer}; Android {version}; {model})' \
--header 'x-app-version: 2.1.44.348'
```
### Response
```json
{
"success": true,
"payload": [
{
"LockedID": "L00012345",
"FromDate": "2026-05-16",
"LockedAmount": 75.00,
"Description": "Card authorisation — Merchant Name"
}
]
}
```
### Response Fields
| Field | Type | Description |
|---|---|---|
| `success` | `bool` | `true` on success |
| `payload` | `array` | List of pending entries (top-level array, **not** under `payload.history`) |
### Pending Entry
| Field | Type | Description |
|---|---|---|
| `LockedID` | `string` | Unique ID for the pending entry |
| `FromDate` | `string` | Date the hold was placed |
| `LockedAmount` | `number` | Held amount — always a positive number on the wire; the client treats it as a **debit** by negating (`BmlHistoryClient.kt:184`) |
| `Description` | `string` | Free-form description (counterparty/merchant) |
> **Amount sign:** the server returns `LockedAmount` as a positive number with no debit/credit indicator. All pending entries are debits (funds reserved out of the available balance), so the client negates the value before display.
> **Currency:** not returned by the server. The client assumes MVR.
Called from `AccountHistoryFragment.kt:263` to populate the pending tab of the account history view.
---
&nbsp;
---
+8 -1
View File
@@ -224,7 +224,14 @@ curl --request POST \
}
```
`success: false` — the `message` field contains the reason. Common causes: wrong OTP, insufficient balance, invalid account.
`success: false` — the error text may appear in either of two fields. The client prefers `payload` (when it is a non-blank string and not `"null"`) and falls back to `message` (`BmlTransferClient.kt:86-88`):
| Field | When used |
|---|---|
| `payload` (string) | Validation-style errors — e.g. insufficient balance, account-specific failures |
| `message` | Generic errors — e.g. invalid OTP, generic transfer failure |
Common causes: wrong OTP, insufficient balance, invalid account, currency mismatch.
---
+4
View File
@@ -148,6 +148,10 @@ curl --request GET \
| `name` | `string` | Account holder name |
| `agnt` | `string` | BIC of MIB — send as the `bank` field in the [transfer](08-transfer.md) request |
> **Client-synthesized fields**: when the app wraps this response for downstream code (`BmlValidateClient.kt:68`), it sets `trnType = "DOT"` and `validationType = "MIB"`. Neither is returned by the server.
>
> **No currency**: this endpoint does NOT return the MIB account's currency. The client sets `currency = ""` (`BmlValidateClient.kt:74-75`). Important for USD-vs-MVR transfer routing — currency must be sourced elsewhere (e.g. MIB's own lookup, see [MIB transfer docs](../mibapi/08-transfer.md)).
### Failure
```json
+37
View File
@@ -77,6 +77,16 @@ Expected response: `{ "code": 0, "payload": [...] }`
The OTP is a standard TOTP (RFC 6238, SHA-1, 30-second window, 6 digits) derived from the stored BML authenticator seed.
### Failure Handling
Each of the three POSTs validates the server's `code` field and throws on mismatch (`BmlTapToPayClient.kt:37, 42, 47`). The exception message is the server's `message` field:
| Step | Expected code | Throws if |
|---|---|---|
| 1a | `0` (rare) or `99` | code is neither — `message` propagated |
| 1b | `22` | code is not `22``message` propagated |
| 1c | `0` | code is not `0``message` propagated |
### Token Response
```json
@@ -239,6 +249,33 @@ All APDU responses use BER-TLV encoding. Tags are 1 or 2 bytes (hex string). Len
---
## Lifecycle
The HCE service (`BmlHostCardEmulatorService`) keeps a single active `BmlWalletToken` in a volatile companion-object field. Tokens are single-use — exactly one tap consumes one token.
### Companion API
```kotlin
BmlHostCardEmulatorService.setToken(token: BmlWalletToken)
BmlHostCardEmulatorService.clearToken()
BmlHostCardEmulatorService.onTransactionComplete: (success: Boolean) -> Unit
```
| Call | When |
|---|---|
| `setToken(token)` | After fetching a token, before prompting the user to tap |
| `clearToken()` | After the tap completes, when the prompt is dismissed, or on error |
| `onTransactionComplete(true)` | Fired immediately after the `READ RECORD` response (`BmlHostCardEmulatorService.kt:78`) |
| `onTransactionComplete(false)` | Fired from `onDeactivated` if GPO was never seen (`BmlHostCardEmulatorService.kt:35-38`) — i.e. the reader walked away before completing the EMV exchange |
### State Rules
- A token MUST be installed via `setToken` before the user taps. With no active token, `SELECT PPSE` launches `BmlTapToPayActivity` (a redirector to `MainActivity`) and returns `6F00`.
- The service tracks `gpoSent` to distinguish "user pulled the phone away" from a successful read. A successful `handleReadRecord` resets `gpoSent` to `false` via `onDeactivated` after the success callback has already fired.
- `BmlTapToPayActivity` provides the "Tap your phone…" prompt UI. The activity is responsible for calling `setToken` before showing the prompt and `clearToken` when dismissed.
---
## Prerequisites
- Valid `access_token` from [OAuth Token Exchange](03-oauth-token.md)
+4 -2
View File
@@ -150,7 +150,7 @@ POST https://www.bankofmaldives.com.mv/internetbanking/api/mobile/walletpayments
Expected response: `{ "success": true, "code": 99 }` (OTP required)
> **Note:** This step may be skipped. The app proceeds directly to Step 2b if the gateway already indicates OTP is required.
> **When this step is used:** the client only calls `preInitiatePayment` for **gateway QRs** — QR URLs that begin with `https://pay.bml.com.mv/app/` (`TransferFragment.kt:349, 1419-1423`). For PayMV-native static QRs (`QRS`), Step 2a is skipped and the flow starts at Step 2b.
### Step 2b — Request OTP Channel
@@ -195,6 +195,8 @@ Expected response:
}
```
> **Currency fallback:** if the server's `payload.currency` is blank, the client falls back to the `currency` value sent in the request (`BmlQrPayClient.kt:148`). The same applies to `merchant` and `amount` at the UI layer.
On failure:
```json
@@ -240,4 +242,4 @@ The OTP is a standard TOTP (RFC 6238, SHA-1, 30-second window, 6 digits) derived
---
[← Tap-to-Pay](12-tap-to-pay.md)
[← Tap-to-Pay](12-tap-to-pay.md) &nbsp;&nbsp;&nbsp; **Next →** [Notifications](14-notifications.md)
+171
View File
@@ -0,0 +1,171 @@
# Notifications
In-app notifications (transaction alerts, security events, marketing) are served from a separate host. Notifications are fetched in pages and can be bulk-marked as read.
The polling service runs in the background and posts an Android system notification for each unseen item (`service/NotificationPollingService.kt:64`).
---
## Base URL
```
https://app.bankofmaldives.com.mv/api/v2
```
Distinct from the main `internetbanking/api/mobile` host, but uses the same Bearer token.
---
## Prerequisites
- Valid `access_token` from [OAuth Token Exchange](03-oauth-token.md)
---
## Fetch Notifications
```
GET https://app.bankofmaldives.com.mv/api/v2/notifications?group={group}&page={page}
```
### Query Parameters
| Parameter | Type | Description |
|---|---|---|
| `group` | `string` | Filter by group — `ALL` (default), or a specific group (e.g. `ALERTS`) |
| `page` | `int` | 1-based page number |
### Headers
| Header | Value |
|---|---|
| `Authorization` | `Bearer <access_token>` |
| `User-Agent` | `bml-mobile-banking/348 ({manufacturer}; Android {version}; {model})` |
| `x-app-version` | `2.1.44.348` |
```bash
curl --request GET \
--url 'https://app.bankofmaldives.com.mv/api/v2/notifications?group=ALL&page=1' \
--header 'Authorization: Bearer <access_token>' \
--header 'User-Agent: bml-mobile-banking/348 ({manufacturer}; Android {version}; {model})' \
--header 'x-app-version: 2.1.44.348'
```
### Response
```json
{
"success": true,
"total": 137,
"payload": [
{
"id": "abc123",
"group": "ALERTS",
"type": "TRANSACTION",
"title": "Transaction Alert",
"message": "MVR 100.00 debited from 7730000000001",
"created_at": "2026-05-16T15:10:25",
"is_read": false,
"data": {
"account_number": "7730000000001",
"amount": "100.00",
"currency": "MVR",
"reference": "FT20260516123456"
}
}
]
}
```
### Top-level Fields
| Field | Type | Description |
|---|---|---|
| `success` | `bool` | `true` on success |
| `total` | `int` | Total notification count across all pages |
| `payload` | `array` | List of notifications for this page |
### Notification Object
| Field | Type | Description |
|---|---|---|
| `id` | `string` | Unique notification ID |
| `group` | `string` | Logical grouping (e.g. `ALERTS`) — also the value passed back to the `group` filter |
| `type` | `string` | Sub-type within the group (e.g. `TRANSACTION`) |
| `title` | `string` | Short headline |
| `message` | `string` | Body text |
| `created_at` | `string` | Timestamp — `yyyy-MM-dd'T'HH:mm:ss` (no timezone) |
| `is_read` | `bool` | Read state |
| `data` | `object?` | Optional structured detail payload — fields vary by type |
### `data` Field Flattening
Where present, the `data` object is flattened into the notification's detail view as key-value rows. The client transforms each `data` key with underscore → space and title-case (`BmlNotificationsClient.kt:93-94`):
```
"account_number" → "Account Number"
"reference" → "Reference"
```
Three synthetic rows are prepended:
| Row | Value |
|---|---|
| `Bank` | `BML` |
| `Group` | from `group` field |
| `Type` | from `type` field |
---
## Mark All Read
```
PUT https://app.bankofmaldives.com.mv/api/v2/notifications/read
```
### Headers
| Header | Value |
|---|---|
| `Authorization` | `Bearer <access_token>` |
| `User-Agent` | `bml-mobile-banking/348 ({manufacturer}; Android {version}; {model})` |
| `x-app-version` | `2.1.44.348` |
| `accept` | `application/json` |
| `Content-Type` | `application/json` |
### Request Body
```json
{
"all": true
}
```
```bash
curl --request PUT \
--url 'https://app.bankofmaldives.com.mv/api/v2/notifications/read' \
--header 'Authorization: Bearer <access_token>' \
--header 'User-Agent: bml-mobile-banking/348 ({manufacturer}; Android {version}; {model})' \
--header 'x-app-version: 2.1.44.348' \
--header 'accept: application/json' \
--header 'Content-Type: application/json' \
--data '{"all":true}'
```
### Response
The client treats any 2xx response as success — the response body is discarded.
---
## Polling
`service/NotificationPollingService.kt:64` polls page 1 of every active BML session at a fixed interval, diffs the result against a local cache, and posts an Android system notification for each new item.
---
&nbsp;
---
[← QR Payment](13-qr-payment.md) &nbsp;&nbsp;&nbsp; **Next →** [Card Freeze](15-card-freeze.md)
+121
View File
@@ -0,0 +1,121 @@
# Card Freeze / Unfreeze
Lock or unlock a BML card to block / allow new authorisations. The same endpoint handles both actions, distinguished by the `action` field.
---
## Endpoint
```
POST https://www.bankofmaldives.com.mv/internetbanking/api/mobile/services/card/freeze
```
---
## Prerequisites
- Valid `access_token` from [OAuth Token Exchange](03-oauth-token.md)
- `cardId` is the **internal card UUID** (`BankAccount.internalId`, sourced from the `id` field of a `Card` entry in the [dashboard](04-dashboard.md) response) — NOT the displayed 16-digit card number
---
## Request
### Body
**Content-Type:** `application/json`
```json
{
"card": "<internalId>",
"action": "freeze"
}
```
| Field | Type | Notes |
|---|---|---|
| `card` | `string` | Internal card UUID — the `id` from the dashboard Card object |
| `action` | `string` | `"freeze"` to lock the card; `"unfreeze"` to unlock |
### Headers
| Header | Value |
|---|---|
| `Authorization` | `Bearer <access_token>` |
| `User-Agent` | `bml-mobile-banking/348 ({manufacturer}; Android {version}; {model})` |
| `x-app-version` | `2.1.44.348` |
| `accept` | `application/json` |
| `Content-Type` | `application/json` |
```bash
curl --request POST \
--url 'https://www.bankofmaldives.com.mv/internetbanking/api/mobile/services/card/freeze' \
--header 'Authorization: Bearer <access_token>' \
--header 'User-Agent: bml-mobile-banking/348 ({manufacturer}; Android {version}; {model})' \
--header 'x-app-version: 2.1.44.348' \
--header 'accept: application/json' \
--header 'Content-Type: application/json' \
--data '{"card":"abc-123-def-456","action":"freeze"}'
```
---
## Response
```json
{
"success": true,
"code": 0,
"payload": "Card frozen successfully",
"message": ""
}
```
### Fields
| Field | Type | Description |
|---|---|---|
| `success` | `bool` | `true` on success |
| `code` | `int` | `0` on success; non-zero indicates an error |
| `payload` | `string` | Human-readable confirmation text (may be blank) |
| `message` | `string` | Fallback error/info text |
Success is determined by **both** `success == true` AND `code == 0` (`BmlCardClient.kt:46`). Either condition alone is not enough.
### Display Message
The client prefers `payload` for the confirmation text and falls back to `message` when `payload` is blank (`BmlCardClient.kt:49`):
```
payload (if non-blank) → fallback message
```
---
## Failure
```json
{
"success": false,
"code": 1,
"payload": "",
"message": "Card cannot be frozen at this time"
}
```
Returned with HTTP `200` for application-level errors. The `message` (or `payload`) field contains the reason.
### Server / Auth Errors
| HTTP Code | Behaviour |
|---|---|
| `401` / `419` | Throws `AuthExpiredException` — refresh the token or re-login |
| `5xx` | Throws `BankServerException("BML")` — server-side failure, retry |
---
&nbsp;
---
[← Notifications](14-notifications.md)
+3
View File
@@ -25,6 +25,7 @@ The login process is stateful and must be executed in order:
| Web login / OAuth | `https://www.bankofmaldives.com.mv/internetbanking` |
| REST API (authenticated) | `https://www.bankofmaldives.com.mv/internetbanking/api/mobile` |
| Foreign limits API | `https://app.bankofmaldives.com.mv/api/v2` |
| Notifications API | `https://app.bankofmaldives.com.mv/api/v2` |
---
@@ -190,6 +191,8 @@ The access token expires after `expires_in` seconds (typically 3600). On a `401`
| 11 | [Foreign Limits](11-foreign-limits.md) | USD foreign transaction limits by card and channel |
| 12 | [Tap-to-Pay](12-tap-to-pay.md) | NFC HCE contactless payment — token fetch and EMV APDU exchange |
| 13 | [QR Payment](13-qr-payment.md) | PayMV QR payment — QR formats, payrequest lookup, 3-step pay flow |
| 14 | [Notifications](14-notifications.md) | Notifications list, mark-as-read, and polling |
| 15 | [Card Freeze](15-card-freeze.md) | Freeze / unfreeze a BML card |
---
+23 -92
View File
@@ -1,112 +1,43 @@
# Profile Picture
Fetch the authenticated user's profile picture. The endpoint redirects to the actual image URL.
Fahipay profile pictures are **stored locally** by the app. There is no Fahipay endpoint involved.
The official Fahipay app exposes `GET https://fahipay.mv/images/profiles/picture/?t={timestamp}` (a 302 redirect to the actual image), but this client never calls it — Fahipay accounts get a user-set picture saved on the device only.
---
## Endpoint
## Storage
```
GET https://fahipay.mv/images/profiles/picture/?t={timestamp}
```
`util/ProfileImageStore.kt` — keyed file storage under `filesDir/profile_images/`.
---
## Prerequisites
- Valid `authID` from [login](01-login.md) or [OTP](02-otp.md)
- Valid `__Secure-sess` session cookie
---
## Request
### Headers
| Header | Value |
| Bank | Key |
|---|---|
| `authid` | `xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx` |
| `User-Agent` | `okhttp/4.12.0` |
| `Accept-Encoding` | `gzip` |
| `Connection` | `Keep-Alive` |
| `Cookie` | `__Secure-sess=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx` |
| Fahipay | `fahipay_{loginId}` |
| BML | `bml_{profileId}` |
| MIB | (n/a — fetched from server via [P41](../mibapi/02-login.md)) |
### Query Parameters
Helpers:
| Parameter | Description | Example |
|---|---|---|
| `t` | Cache-busting timestamp string | `Sat May 16 2026 14:57:52 GMT+0500` |
```kotlin
ProfileImageStore.fahipayKey(loginId) // "fahipay_abc123"
ProfileImageStore.save(context, key, bitmap)
ProfileImageStore.load(context, key) // Bitmap?
ProfileImageStore.delete(context, key)
```
The `t` parameter is a URL-encoded timestamp used to prevent browser caching. The value can be any string — the server ignores it for routing purposes.
Files are JPEGs at quality 90. The filename is the key with non-alphanumerics replaced by `_`.
---
## curl Example
## UI entry points
```bash
curl --request GET \
--url 'https://fahipay.mv/images/profiles/picture/?t=Sat%20Jan%2001%202026%2012:00:00%20GMT+0500' \
--compressed \
--header 'Accept-Encoding: gzip' \
--header 'Connection: Keep-Alive' \
--header 'Cookie: __Secure-sess=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' \
--header 'User-Agent: okhttp/4.12.0' \
--header 'authid: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
```
Set/replace/remove a Fahipay profile picture from **Settings → Logins** (`ui/home/SettingsLoginsFragment.kt`). The pencil icon next to a Fahipay login opens a chooser:
---
- Pick from gallery (`ACTION_OPEN_DOCUMENT` image/*)
- Take a photo (camera launcher, temp file at `cacheDir/profile_photo_tmp.jpg`)
- Remove current picture
## Response
### Success
The server responds with `HTTP 302` and a `Location` header pointing to the actual image URL.
```
HTTP/1.1 302 Found
Location: https://fahipay.mv/images/profiles/0000/avatar.jpg?v=0000000000
```
Follow the redirect to download the image. The final response is the raw image bytes (`image/jpeg` or `image/png`).
---
### No Picture Set
If the user has not uploaded a profile picture, the redirect points to a default placeholder image:
```
Location: https://fahipay.mv/images/profiles/default.png
```
---
### Error
If the session is invalid, the server returns `HTTP 401` or redirects to an error page.
---
## Implementation Notes
- HTTP clients that follow redirects automatically (e.g. `OkHttpClient` with `followRedirects(true)`) will return the image bytes directly.
- Use `followRedirects(false)` and read the `Location` header if you need the resolved image URL separately.
- The image URL contains the user's `profileID` in the path — this matches the `profileID` field from the [profile response](03-profile.md).
- The `v=` query parameter in the image URL is a version/cache key. It changes when the user updates their picture.
---
## Suggested Usage
```
timestamp = current time formatted as URL-safe string
GET /images/profiles/picture/?t={timestamp}
→ 302 Location: <image URL>
→ GET <image URL>
→ image bytes
```
Cache the downloaded image by `profileID` and re-fetch when the user explicitly refreshes, rather than on every app launch.
Saved images are surfaced anywhere the account is shown — accounts list, contact picker, receipts — via a `localProfileImageLoader` that resolves the key with `ProfileImageStore.load`.
---
+7 -7
View File
@@ -42,14 +42,14 @@ GET https://fahipay.mv/api/app/favs/?page={serviceName}&lang=en
## Service Groups
Call this endpoint once per service group:
Call this endpoint once per service group. Labels shown are the ones the app surfaces in the UI (`FahipayContactsClient.kt:22-25`):
| `page` value | Service | Description |
|---|---|---|
| `ooredooraastas` | Ooredoo Raastas | Ooredoo mobile top-up |
| `dhiraagureload` | Dhiraagu Reload | Dhiraagu mobile top-up |
| `ooredoobillpay` | Ooredoo Bill | Ooredoo bill payment |
| `dhiraagubillpay` | Dhiraagu Bill | Dhiraagu bill payment |
| `page` value | Group label | `benefCategoryId` | Description |
|---|---|---|---|
| `ooredooraastas` | `Raastas` | `FAHIPAY_RAASTAS` | Ooredoo mobile top-up |
| `dhiraagureload` | `Reload` | `FAHIPAY_RELOAD` | Dhiraagu mobile top-up |
| `ooredoobillpay` | `Ooredoo Bill` | `FAHIPAY_OOREDOO_BILL` | Ooredoo bill payment |
| `dhiraagubillpay` | `Dhiraagu Bill` | `FAHIPAY_DHIRAAGU_BILL` | Dhiraagu bill payment |
---
+1 -1
View File
@@ -125,7 +125,7 @@ Client Server
| 3 | [Profile](03-profile.md) | Fetch user profile and linked bank accounts |
| 4 | [Balance](04-balance.md) | Fetch wallet balance |
| 5 | [Transaction History](05-history.md) | Paginated activity/transaction history |
| 6 | [Profile Picture](06-profile-picture.md) | Fetch user profile picture |
| 6 | [Profile Picture](06-profile-picture.md) | Local-only profile picture storage (no Fahipay endpoint) |
| 7 | [Saved Favourites](07-contacts.md) | Fetch saved contacts per payment service |
---
+4 -2
View File
@@ -8,7 +8,7 @@ All traffic to the encrypted API (`faisanet.mib.com.mv`) uses Blowfish encryptio
- **Algorithm**: Blowfish, ECB mode, PKCS5 padding
- **Input**: raw UTF-8 bytes of the JSON payload string
- **Key**: raw UTF-8 bytes of the key string
- **Key**: raw ISO-8859-1 bytes of the key string (not UTF-8 — only matters if a key ever contains a non-ASCII character; in Python this is `key.encode('latin-1')`, in Kotlin `Charsets.ISO_8859_1`)
- **Output**: base64-encoded ciphertext (URL-encoded when sent as form data)
```python
@@ -204,8 +204,10 @@ def generate_nonce(nonce_generator: str) -> str:
### Notes
- `nonce` and `sodium` are separate fields. `sodium` is an independent random integer (~2324 bit range).
- `nonce` and `sodium` are separate fields. `sodium` is an independent random integer in `[1_000_000, 16_000_000)` (~24-bit range, 78 decimal digits) — see `MibNonce.kt:76`.
- `xxid` (when randomly generated client-side, e.g. inside the `sfunc=r`/`sfunc=i` inner payload) is a random 40-bit integer: `Random.nextLong(0L, 1L shl 40)` — see `MibNonce.kt:78`. The server replaces this with a real session `xxid` in its response.
- The `nonceGenerator` is returned once by the key exchange response and reused for the entire session.
- All `S` and `C` arithmetic in Phase 2 uses Kotlin `Long` (see `MibNonce.kt:54-60`) — `carry * carry * carry + …` can exceed 32-bit range for `carry ≈ 99`. Reproducing the nonce in another language requires 64-bit (or arbitrary-precision) arithmetic on the intermediate value before taking the last two digits.
---
+25 -6
View File
@@ -14,17 +14,17 @@ MIB uses a two-phase authentication model:
The password is never sent in plaintext. Required by both `C41` (registration) and `A41` (login).
```
pgf03 = SHA256( clientSalt + SHA256( userSalt + SHA256( password ) ) )
pgf03 = SHA256( clientSalt + SHA256( SHA256( password ) + userSalt ) )
```
All SHA-256 values are uppercase hex strings. `clientSalt` is a fresh random 32-character alphanumeric string each time.
All SHA-256 values are uppercase hex strings. `clientSalt` is a fresh random 32-character alphanumeric string each time. Note the inner concat order is `passwordHash + userSalt` — getting this backwards produces a valid-looking but wrong hash.
```python
import hashlib
def compute_pgf03(password: str, user_salt: str, client_salt: str) -> str:
h1 = hashlib.sha256(password.encode()).hexdigest().upper()
h2 = hashlib.sha256((user_salt + h1).encode()).hexdigest().upper()
h2 = hashlib.sha256((h1 + user_salt).encode()).hexdigest().upper()
return hashlib.sha256((client_salt + h2).encode()).hexdigest().upper()
```
@@ -69,7 +69,7 @@ def compute_pgf03(password: str, user_salt: str, client_salt: str) -> str:
"cmod": "<G^A mod P as decimal string>",
"appId": "IOS17.2-<15 random alphanumeric chars>",
"routePath": "S40",
"sodium": "<random 20-bit int as string>",
"sodium": "<random int in [1_000_000, 16_000_000)>",
"xxid": "<random 40-bit int as string>"
}
}
@@ -218,7 +218,7 @@ Form body: `key2=<key2>&sfunc=i&data=<encrypted payload>`
"cmod": "<G^A mod P>",
"appId": "<appId>",
"routePath": "S40",
"sodium": "<random 20-bit int>",
"sodium": "<random int in [1_000_000, 16_000_000)>",
"xxid": "<random 40-bit int>"
}
}
@@ -272,10 +272,29 @@ After: derive new session key, replace `xxid` and `nonceGenerator`.
"otpTypes": [2, 3],
"email": "<masked email>",
"uuid": "<uuid1>",
"uuid2": "<uuid2>"
"uuid2": "<uuid2>",
"operatingProfiles": [
{
"customerProfileId": "...",
"annexId": "...",
"customerId": "...",
"name": "...",
"cifType": "...",
"profileType": "...",
"color": "...",
"customerImage": "<image hash, may be missing/blank>"
}
],
"profileSelected": false,
"selectedProfileId": "",
"accountBalance": []
}
```
**Single-profile fast-path** — if the account has exactly one operating profile, the server returns `profileSelected: true`, populates `selectedProfileId`, and includes a non-empty `accountBalance` array in the A41 response itself. In that case the [P47](03-accounts.md) call is **skipped** and balances are read from this response (see `MibLoginFlow.kt:150-184`).
> **Field naming**: the image hash field on each `operatingProfiles[]` entry is `customerImage`. The same conceptual value is called `customerImgHash` in the contacts API response — the two endpoints disagree on the field name.
---
### [3b] Get Profile Image — `sfunc=n`, `routePath: P41`
+20 -17
View File
@@ -1,6 +1,8 @@
# Accounts & Balances
Account numbers and balances are returned by the **Select Profile** call (`routePath: P47`). The login init call (`A41`) returns an empty `accountBalance` array — balances are only available after `P47`.
Account numbers and balances are returned by the **Select Profile** call (`routePath: P47`). For multi-profile users the `A41` login init call returns an empty `accountBalance` array and `P47` must be called for each profile to enumerate accounts.
> **Single-profile fast-path**: when the account has exactly one operating profile, the server returns `profileSelected: true`, `selectedProfileId`, and a populated `accountBalance` array directly in the `A41` response. In that case the `P47` call is **skipped** — see [02-login.md](02-login.md) and `MibLoginFlow.kt:150-184`.
---
@@ -95,22 +97,23 @@ Each element represents one account:
}
```
| Field | Description |
|---|---|
| `accountNumber` | Full account number |
| `accountBriefName` | Human-readable account label |
| `currencyCode` | ISO 4217 numeric (e.g. `"462"` = MVR, `"840"` = USD) |
| `currencyName` | ISO 4217 alpha (e.g. `"MVR"`, `"USD"`) |
| `accountTypeName` | Account type (e.g. `"Saving Account"`) |
| `availableBalance` | Spendable balance (decimal string) |
| `currentBalance` | Ledger balance (decimal string) |
| `blockedAmount` | Held/blocked funds — negative means funds are held |
| `settlementBalance` | Balance including pending settlements |
| `mvrBalance` | All balances converted to MVR for unified display |
| `transfer` | `"Y"` if usable as transfer source |
| `statusDesc` | Account status (e.g. `"Active"`) |
| `cif` | Customer Information File number |
| `template` | UI template ID |
| Field | Consumed | Description |
|---|---|---|
| `accountNumber` | yes | Full account number |
| `accountBriefName` | yes | Human-readable account label |
| `currencyName` | yes | ISO 4217 alpha (e.g. `"MVR"`, `"USD"`) |
| `accountTypeName` | yes | Account type (e.g. `"Saving Account"`) |
| `availableBalance` | yes | Spendable balance (decimal string) |
| `currentBalance` | yes | Ledger balance (decimal string) |
| `blockedAmount` | yes | Held/blocked funds. Server value is **signed** (negative = held). The app normalizes to a positive magnitude via `absBlockedAmount()` (`MibLoginFlow.kt:172, 194-197`). |
| `mvrBalance` | yes | All balances converted to MVR for unified display |
| `statusDesc` | yes | Account status (e.g. `"Active"`) |
| `currencyCode` | server-only | ISO 4217 numeric (e.g. `"462"` = MVR, `"840"` = USD) — present in payload but not read by the app |
| `transfer` | server-only | `"Y"` if usable as transfer source |
| `cif` | server-only | Customer Information File number |
| `template` | server-only | UI template ID |
| `branchName` | server-only | Branch name |
| `settlementBalance` | server-only | Balance including pending settlements |
> All balance fields are **decimal strings**, not numbers — parse with `Decimal` for precision.
+40
View File
@@ -71,6 +71,8 @@ Referer: https://faisamobilex-wv.mib.com.mv//debitCards?dashurl=1
| `phoneNumber` | `string` | Registered phone number |
| `cardHolderName` | `string` | Name on card |
> The app's `MibCard` model adds two **app-internal** fields not present on the server payload: `loginTag` (e.g. `"mib_<username>"`) and `profileId` (the active profile that fetched the card). They are populated by the client (`MibCardsClient.kt:66-67`) and used by the UI to map a card back to its owning login.
### Failure
```json
@@ -79,6 +81,44 @@ Referer: https://faisamobilex-wv.mib.com.mv//debitCards?dashurl=1
---
## Freeze / Unfreeze Card
```
POST https://faisamobilex-wv.mib.com.mv/ajaxDebitCard/freeze
POST https://faisamobilex-wv.mib.com.mv/ajaxDebitCard/unfreeze
```
Two endpoints — same shape. Pick by intent. Source: `MibCardsClient.kt:74-103`.
```
Referer: https://faisamobilex-wv.mib.com.mv//debitCards/manage?cardId=<cardId>&dashurl=1
```
### Request Body (form-urlencoded)
| Field | Description |
|---|---|
| `cardId` | Card identifier from `fetchCardInfos` |
| `comments` | User-supplied reason (free text; may be empty) |
### Response
```json
{
"success": true,
"reasonText": "Card frozen successfully",
"currentStatusCode": "F"
}
```
| Field | Description |
|---|---|
| `success` | `true` on success |
| `reasonText` | Human-readable message (also returned on failure — show to user) |
| `currentStatusCode` | New card status code after the action (e.g. `"F"` = frozen, `"A"` = active) |
---
&nbsp;
---
+123 -14
View File
@@ -1,18 +1,23 @@
# Personal Profile
Fetch the user's personal profile details. This endpoint returns an HTML page; data is extracted via HTML scraping.
This document covers two unrelated profile-adjacent surfaces:
1. **Personal profile** — an HTML page on the WebView host, scraped for display.
2. **Profile image management** — three encrypted-API endpoints (`P40`/`P41`/`P42`) that fetch, upload, and delete the avatar.
---
## Endpoint
## 1. Personal Profile (HTML scrape)
Fetch the user's personal profile details. This endpoint returns an HTML page; data is extracted via HTML scraping.
### Endpoint
```
GET https://faisamobilex-wv.mib.com.mv/personalProfile
```
---
## Authentication
### Authentication
Session cookies only — no additional AJAX headers required.
@@ -20,9 +25,7 @@ Session cookies only — no additional AJAX headers required.
Cookie: mbmodel=IOS-1.0; xxid=<session_xxid>; IBSID=<session_xxid>; mbnonce=<nonceGenerator>; time-tracker=597
```
---
## Response
### Response
**Content-Type:** `text/html; charset=UTF-8`
@@ -53,9 +56,7 @@ Regex(
)
```
---
## Extracted Fields
### Extracted Fields
| Label in HTML | Field | Description |
|---|---|---|
@@ -76,15 +77,123 @@ data class MibPersonalProfile(
)
```
---
## Notes
### Notes
- Returns `null` if the response cannot be parsed (network error or unexpected HTML structure).
- This endpoint does not have a JSON equivalent — scraping is the only method.
---
## 2. Profile Image Management
The avatar lives on the **encrypted API** (`faisanet.mib.com.mv`), not the WebView host. Three `sfunc=n` route paths cover fetch, upload, and delete. All three follow the standard encrypted-request format (see [01-encryption.md](01-encryption.md) and [02-login.md](02-login.md)) and include the standard `baseData` fields (`nonce`, `appId`, `sodium`, `routePath`, `xxid`).
| `routePath` | Operation | Source |
|---|---|---|
| `P41` | Fetch image by hash | `MibLoginFlow.kt:368-375` |
| `P40` | Upload new image | `MibLoginFlow.kt:382-391` |
| `P42` | Delete current image | `MibLoginFlow.kt:397-403` |
### Image-hash field naming
The same conceptual value — "the hash that identifies a customer's avatar" — is exposed under **two different field names** depending on which endpoint returned it:
| Source endpoint | Field name |
|---|---|
| `A41` login init (`operatingProfiles[]`) | `customerImage` |
| `C41` registration init (root) | `customerImgHash` |
| `ajaxBeneficiary/main` (contacts list) | `customerImgHash` |
Both are non-empty hash strings that can be passed straight into `P41` as `imageHash`. Treat them as the same value with two names.
### Fetch — `routePath: P41`
**Request** (encrypted payload):
```json
{
"sfunc": "n",
"xxid": "<session xxid>",
"data": {
"imageHash": "<customerImage or customerImgHash>",
"nonce": "<computed nonce>",
"appId": "<appId>",
"sodium": "<random>",
"routePath": "P41",
"xxid": "<session xxid>"
}
}
```
**Response**:
```json
{
"success": true,
"reasonCode": "201",
"profileImage": "<base64-encoded JPEG>"
}
```
`profileImage` is raw base64 with no data URI prefix.
### Upload — `routePath: P40`
**Request** (encrypted payload):
```json
{
"sfunc": "n",
"xxid": "<session xxid>",
"data": {
"profileId": "<active profile ID>",
"profileImage": "<base64-encoded JPEG>",
"nonce": "<computed nonce>",
"appId": "<appId>",
"sodium": "<random>",
"routePath": "P40",
"xxid": "<session xxid>"
}
}
```
**Response**:
```json
{
"success": true,
"imageHash": "<new hash>",
"customerImage": "<new hash, alternate field name>"
}
```
The server may populate either `imageHash` or `customerImage` for the new hash — the client reads both (`MibLoginFlow.kt:389-390`) and prefers `imageHash` when present.
### Delete — `routePath: P42`
**Request** (encrypted payload):
```json
{
"sfunc": "n",
"xxid": "<session xxid>",
"data": {
"profileId": "<active profile ID>",
"nonce": "<computed nonce>",
"appId": "<appId>",
"sodium": "<random>",
"routePath": "P42",
"xxid": "<session xxid>"
}
}
```
**Response**:
```json
{ "success": true }
```
After a successful delete, the `customerImage` field on subsequent `A41` responses is blank.
> Note: BML and Fahipay profile images are stored locally on-device only (`util/ProfileImageStore.kt`). Only MIB persists avatars server-side via these endpoints.
---
&nbsp;
---
+26 -2
View File
@@ -48,6 +48,8 @@ Body: `benefAccount=7700000000000`
Use `bankNo=3` and `transferLocal` for the transfer.
> **USD cross-bank accounts**: `getIPSAccount` only succeeds for **MVR** cross-bank accounts. A 13-digit `7…` account that is denominated in USD returns `success: false` here and cannot be resolved via IPS at all. The client hardcodes `currency = "MVR"` for IPS results (`MibTransferClient.kt:135-137`). For USD BML→MIB transfers the user must first save a BML contact (see Notes below).
---
### 1b. MIB Internal Account (17 digits, starts with `9`)
@@ -62,11 +64,13 @@ Body: `accountNo=90100000000000000`
```json
{
"success": true,
"accountName": "ACCOUNT HOLDER NAME"
"accountName": "ACCOUNT HOLDER NAME",
"currencyCode": "462"
}
```
- `accountName` may be at root level or inside a `data` object — check both
- `accountName` may be at root level or inside a `data` object — check both (`MibTransferClient.kt:160-161`)
- `currencyCode` may also be at root level or inside `data` (`MibTransferClient.kt:162-163`). `"462"` = MVR, `"840"` = USD. The client maps this into `MibIpsAccountInfo.currency``{"MVR", "USD", ""}` — this is the **MIB→MIB USD detection** fix from commit `16fd909`.
- Bank is always MIB (`MADVMVMV`)
Use `bankNo=2` and `transferInternal` for the transfer.
@@ -201,6 +205,26 @@ POST https://faisamobilex-wv.mib.com.mv/ajaxTransfer/transferLocal
---
## `MibIpsAccountInfo` (client model)
All three lookups return this unified structure (`MibModels.kt:42-47`):
| Field | Description |
|---|---|
| `accountName` | Account holder name (trimmed) |
| `accountNumber` | Resolved account number |
| `bankId` | Bank BIC (`MADVMVMV` = MIB, `MALBMVMV` = BML, etc.) |
| `currency` | `"MVR"`, `"USD"`, or `""` (unknown). Populated from `currencyCode` for MIB internal lookups; hardcoded `"MVR"` for IPS lookups; default `""` for alias lookups. |
---
## Notes
- **BML → MIB USD transfers** require a saved BML contact first. Because `getIPSAccount` rejects USD accounts (`success: false`), the app cannot validate the BML USD account number directly. The workaround in `TransferFragment.kt` is to call `MibContactsClient.createContact` (see [09-contacts.md](09-contacts.md)) to auto-add the BML account as a beneficiary, then transfer to that beneficiary. Introduced in commit `16fd909`.
- **Session expiry**: HTTP `419` on either lookup or transfer means the session expired. See [README](README.md) for the unified expiry detection rules.
---
&nbsp;
---
+17 -9
View File
@@ -94,15 +94,23 @@ POST https://faisamobilex-wv.mib.com.mv/ajaxBeneficiary/main
}
```
| Field | Description |
|---|---|
| `benefNo` | Unique beneficiary ID — use for delete |
| `benefNickName` | User-assigned nickname (prefer over `benefName` for display) |
| `benefType` | `L`, `I`, or `S` |
| `bankColor` | Hex color for placeholder avatar background |
| `customerImgHash` | Hash for fetching profile photo (`null` if no photo) |
| `benefCategoryID` | Category ID — `"0"` means uncategorized |
| `transferCyDesc` | Currency (e.g. `"MVR"`, `"USD"`) |
| Field | Consumed | Description |
|---|---|---|
| `benefNo` | yes | Unique beneficiary ID — use for delete |
| `benefName` | yes | Legal/full name; used as fallback when nickname is blank |
| `benefNickName` | yes | User-assigned nickname (prefer over `benefName` for display) |
| `benefAccount` | yes | Account number |
| `benefType` | yes | `L`, `I`, or `S` |
| `bankColor` | yes | Hex color for placeholder avatar background |
| `benefBankName` | yes | Bank display name. Shown as transfer subtitle and contact detail (`MibContactParser.kt:23, 26`). |
| `bankCode` | yes | Short bank code (e.g. `"BML"`, `"MIB"`) |
| `benefStatus` | yes | Beneficiary status (`"A"` = active) |
| `transferCyDesc` | yes | Currency (e.g. `"MVR"`, `"USD"`) |
| `customerImgHash` | yes | Hash for fetching profile photo. May be missing, blank, or the literal **string** `"null"` — the client filters all three (`MibContactsClient.kt:120`) so downstream code only sees a real hash or `null`. |
| `benefCategoryID` | yes | Category ID — `"0"` means uncategorized |
| `benefSwiftCode` | server-only | SWIFT BIC of the beneficiary's bank — present in payload, not read by the app |
| `benefBankId` | server-only | Numeric bank ID — not consumed |
| `transferCy` | server-only | Currency numeric code (e.g. `"462"`) — only `transferCyDesc` is consumed |
---
+145
View File
@@ -0,0 +1,145 @@
# Activity History
Fetch the audit/activity log for the authenticated session. This is a separate feed from transaction history ([04-history.md](04-history.md)) — it records login events, profile switches, transfers initiated, beneficiary edits, etc.
Source: `MibActivityHistoryClient.kt`.
---
## Endpoint
```
POST https://faisamobilex-wv.mib.com.mv/aProfile/getPagedActivityHistory
```
---
## Authentication
WebView session cookies (see [README](README.md)) plus `X-Requested-With: XMLHttpRequest`.
Unlike most WebView AJAX calls, this endpoint sends **no `Referer`** and **no `Origin`** header.
```
Cookie: mbmodel=IOS-1.0; xxid=<session_xxid>; IBSID=<session_xxid>; mbnonce=<nonceGenerator>; time-tracker=597
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (Linux; Android <ver>; wv) AppleWebKit/537.36 ...
```
---
## Request Body (form-urlencoded)
| Field | Value | Description |
|---|---|---|
| `start` | `1` | Start record index (1-based, inclusive) |
| `end` | `100` | End record index (inclusive) |
| `includeCount` | `1` | Return `total_count` in the response |
The app uses a default page size of **100** (`MibActivityHistoryClient.kt:120`).
---
## Response
```json
{
"success": true,
"total_count": "248",
"data": [
{
"aid": "A0001",
"activityType": "Local Transfer",
"pa": "You",
"activity": "transferred MVR 100.00 to",
"pb": "Ahmed Ali",
"date": "16 May 2026 15:10"
}
]
}
```
| Field | Description |
|---|---|
| `success` | `true` on success |
| `total_count` | Total entries on the server side (as a string — parse to int) |
| `data` | Array of activity records |
### Record fields
| Field | Description |
|---|---|
| `aid` | Activity ID — used as the notification ID for read-state tracking |
| `activityType` | Category label (e.g. `"Local Transfer"`, `"Beneficiary Added"`, `"Switch Profile"`, `"Log in"`) |
| `pa` | Subject — the actor, typically `"You"` |
| `activity` | Verb phrase describing the action |
| `pb` | Object — counterparty / target of the action |
| `date` | Timestamp formatted `"dd MMM yyyy HH:mm"` in **US locale** (parsed with `SimpleDateFormat("dd MMM yyyy HH:mm", Locale.US)`) |
### Display message
The app concatenates the three text fields with single spaces, skipping blanks:
```
message = "$pa $activity $pb"
```
E.g. `"You transferred MVR 100.00 to Ahmed Ali"`.
---
## Skipped Activity Types
The client hard-filters two `activityType` values out of the UI feed (`MibActivityHistoryClient.kt:13`):
```kotlin
private val SKIP_TYPES = setOf("Switch Profile", "Log in")
```
These records are still counted in `total_count` and still consume their slot in the requested `[start, end]` page. Pagination therefore has to fetch past them.
---
## Pagination — `fetchUntilEnough`
Because hidden types reduce the effective yield of each page, a thin helper repeats `fetchActivity` until enough visible records are collected or all pages are exhausted (`MibActivityHistoryClient.kt:116-134`):
```kotlin
fun fetchUntilEnough(
session: MibSession,
loginId: String,
minCount: Int = 5,
pageSize: Int = 100
): FetchResult
```
Loop logic:
1. Start at `start = 1`.
2. Call `fetchActivity(session, loginId, start, start + pageSize - 1)`.
3. Append filtered items to the accumulator.
4. Stop when **either** the accumulator has at least `minCount` items, **or** the raw page came back empty, **or** `start + pageSize - 1 >= totalCount`.
5. Otherwise advance `start += pageSize` and repeat.
The returned `FetchResult` carries:
| Field | Description |
|---|---|
| `items` | Filtered, ready-to-display notifications |
| `rawCount` | Total raw items consumed from the server (pre-filter) |
| `totalCount` | Server-reported total |
| `nextStart` | Next `start` to use for further pagination |
---
## Failure
Any non-2xx response, JSON parse failure, or `success: false` is mapped to an empty `FetchResult(emptyList(), 0, 0, end + 1)` — failures are silent. The caller distinguishes "no data" from "transient failure" by inspecting `totalCount`.
---
&nbsp;
---
[← Contacts](09-contacts.md)
+20 -1
View File
@@ -48,6 +48,11 @@ Cookie: mbmodel=IOS-1.0; xxid=<session_xxid>; IBSID=<session_xxid>; mbnonce=<non
These values come from the login flow — `xxid` and `nonceGenerator` from the DH key exchange response.
**Cookie notes:**
- `time-tracker=597` is a **hardcoded constant** in the client. Every WebView client (transfers, contacts, cards, activity, etc.) sends the literal value `597` — it is not computed or rotated.
- `mbnonce` is the **unmodified `nonceGenerator` string** from the key-exchange response. It does **not** carry a freshly-computed per-request nonce. The actual per-request nonce (derived via the algorithm in [01-encryption.md](01-encryption.md)) only appears inside the encrypted payloads of `sfunc=n` calls on the encrypted API — WebView endpoints have no nonce field.
### WebView AJAX Headers
All AJAX `POST` calls also require:
@@ -59,7 +64,20 @@ Origin: https://faisamobilex-wv.mib.com.mv
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
```
The `Referer` value varies per endpoint (documented per endpoint).
The `Referer` value varies per endpoint (documented per endpoint). A few endpoints (notably activity history, [10-activity-history.md](10-activity-history.md)) omit both `Referer` and `Origin`.
---
## Session Expiry Detection
A MIB session is considered expired when **either** condition is met (`MibLoginFlow.kt:248-274`):
| Signal | Source |
|---|---|
| HTTP **`419`** status | Encrypted API or any WebView endpoint |
| JSON `reasonCode == "505"` in a decrypted response body | Encrypted API |
On detection the client auto-recovers by re-running the login flow using stored credentials, refreshes `xxid` + `nonceGenerator` in the in-flight payload, and retries the original request once. Callers receive the retried response transparently. If the recovery itself hits expiry again it surfaces a `SessionExpiredException`.
### WebView User-Agent
@@ -82,6 +100,7 @@ Mozilla/5.0 (Linux; Android {version}; wv) AppleWebKit/537.36 (KHTML, like Gecko
| 7 | [07-profile.md](07-profile.md) | Personal profile (HTML scrape) |
| 8 | [08-transfer.md](08-transfer.md) | Account lookup and fund transfer |
| 9 | [09-contacts.md](09-contacts.md) | Beneficiary management |
| 10 | [10-activity-history.md](10-activity-history.md) | Activity / audit log |
---
+62 -35
View File
@@ -17,16 +17,22 @@ Architecture overview of the app's entry point, main container, navigation syste
### Intent Actions
External intents (from NFC, shortcuts, or notifications) are passed through to `HomeActivity` via the same forwarding intent:
External intents (from NFC, shortcuts, or notifications) are forwarded to `HomeActivity` via the same intent (`MainActivity.kt:57-65`):
| Action | Effect |
|---|---|
| `OPEN_TRANSFER` | Opens transfer screen |
| `OPEN_SCAN_QR` | Opens QR scanner |
| `OPEN_PAY_WITH_CARD` | Opens BML card QR payment |
| `TAP_TO_PAY` | Opens BML tap-to-pay NFC flow |
| Action | Destination | Notes |
|---|---|---|
| `sh.sar.basedbank.OPEN_TRANSFER` | `R.id.nav_transfer` | Plain transfer screen |
| `sh.sar.basedbank.OPEN_SCAN_QR` | `R.id.nav_transfer` + `auto_scan=true` | Opens [QR scanner](25-qr-scanner.md) immediately |
| `sh.sar.basedbank.OPEN_PAY_WITH_CARD` | `R.id.nav_pay_with_card` | Opens [Cards](22-cards.md) (`CardsFragment`) |
| `sh.sar.basedbank.TAP_TO_PAY` | `R.id.nav_pay_with_card` + `auto_tap_mode=true` | Enters [Tap to Pay](23-tap-to-pay.md) on the default card |
`BmlTapToPayActivity` is a dedicated NFC entry point registered in the manifest. It immediately re-fires a `TAP_TO_PAY` intent to `MainActivity` and finishes.
### Share-to-Scan (`ACTION_SEND`)
When another app shares an image to the **Scan to Pay** activity-alias declared in `AndroidManifest.xml`, `MainActivity.kt:47-55` decodes the bitmap on the spot using `ZxingCpp` (while it still holds the share URI permission) and forwards the decoded QR text as `share_qr_text` to `HomeActivity`.
### NFC Entry Point
`BmlTapToPayActivity` (manifest-registered, NFC payment service redirect) immediately re-fires a `TAP_TO_PAY` intent to `MainActivity` and finishes — see [Tap to Pay](23-tap-to-pay.md).
---
@@ -48,6 +54,9 @@ External intents (from NFC, shortcuts, or notifications) are passed through to `
|---|---|
| Lock icon | Immediately locks the app → `LockActivity` (animated with scale + alpha) |
| Eye icon | Toggles `hideAmounts` in `HomeViewModel`; all balance displays redact to `••••` |
| Bell icon | Opens `NotificationsSheetFragment` ([Notifications](24-notifications.md)). Shows `ic_bell` when there are unread notifications, `ic_bell_read` otherwise (`HomeActivity.kt:640-684`) |
In Circular nav mode the toolbar collapses to just the app title — the lock, eye, and bell items are all hidden (`HomeActivity.kt:646-650`).
### Auto-refresh
@@ -61,36 +70,46 @@ A persistent banner appears at the top of `HomeActivity` when network connectivi
## Navigation Modes
The user can choose between two navigation modes in Settings → Appearance:
Three modes are selectable in Settings → Appearance (`NavCustomization.kt:10-12`):
### Drawer (default)
### Drawer (default) — `NAV_MODE_DRAWER`
A slide-out navigation drawer containing up to 10 configurable nav items. The hamburger icon in the toolbar opens it.
A slide-out navigation drawer containing all configurable nav items. The hamburger icon in the toolbar opens it.
### Bottom Navigation
### Bottom Navigation — `NAV_MODE_BOTTOM`
A bottom bar with 3 configurable slots plus a fixed **Dashboard** tab (always leftmost) and a **More** tab (always rightmost). Tapping **More** opens `NavMoreSheetFragment` — a bottom sheet listing all items not assigned to the 3 visible slots.
A bottom bar with **3** configurable slots plus a fixed **Dashboard** tab (always leftmost) and a **More** tab (always rightmost). Tapping **More** opens `NavMoreSheetFragment` — a bottom sheet listing all items not assigned to the visible slots.
### Circular — `NAV_MODE_CIRCULAR`
A radial wheel UI with 4 customisable wheel slots + a lock-icon centre — see [Circular Nav](26-circular-nav.md).
---
## Navigation Slots
10 possible navigation destinations can be assigned to slots. The user reorders them via drag-and-drop in Settings → Appearance.
`NavCustomization.ALL_SWAPPABLE` enumerates every reorderable destination. Quick actions and bottom-bar slots persist as individual `SharedPreferences` keys (`NavCustomization.kt:52-93`).
| Destination | Default slot |
| Destination | Nav ID |
|---|---|
| Accounts | 1 |
| Transfer | 2 |
| Activities | 3 |
| Contacts | 4 |
| Financing | 5 |
| OTP | 6 |
| PayMV QR | 7 |
| BML QR Pay | 8 |
| Transfer History | 9 |
| Settings | 10 |
| Accounts | `nav_accounts` |
| Contacts | `nav_contacts` |
| Transfer | `nav_transfer` |
| PayMV QR | `nav_pay_mv_qr` |
| Activities | `nav_activities` |
| Transfer History | `nav_transfer_history` |
| Financing | `nav_finances` |
| Cards | `nav_pay_with_card` |
| OTP | `nav_otp` |
| Settings | `nav_settings` |
Two **Quick Action** slots appear as FAB-style buttons on the dashboard and are independently configurable.
Defaults:
| Slot set | Defaults |
|---|---|
| Bottom-bar (3 slots) | Accounts, Contacts, Transfer |
| Circular wheel (4 slots) | Transfer, Cards, Contacts, Accounts |
| Quick actions (2 FAB slots on dashboard) | Transfer, PayMV QR |
---
@@ -101,10 +120,11 @@ Autolock fires after a configurable period of user inactivity. Any touch event r
| Timeout option |
|---|
| 30 seconds |
| 1 minute |
| 1 minute (default) |
| 3 minutes |
| 5 minutes |
| Never |
There is no "Never" option (`SettingsSecurityFragment.kt:81-86`) — auto-lock cannot be disabled.
When the timeout expires a 10-second countdown warning dialog appears. If dismissed, the timer resets. If ignored, the app calls `LockActivity` and clears `app.isUnlocked`.
@@ -112,17 +132,24 @@ When the timeout expires a 10-second countdown warning dialog appears. If dismis
## Global State — `BasedBankApp`
`BasedBankApp` holds all in-memory session data. Nothing is stored to disk except encrypted credentials.
`BasedBankApp` holds all in-memory session data (`BasedBankApp.kt:27-49`). Nothing is stored to disk except encrypted credentials.
| Field | Description |
|---|---|
| `isUnlocked` | Set to `true` after successful lock-screen auth; guards against process-restart bypass |
| `mibSessions` | Map of MIB profile ID → active session (cookies + DH key) |
| `bmlSessions` | Map of BML profile ID → OAuth token pair |
| `fahipaySessions` | Map of Fahipay login ID → authID + session cookie |
| `mibLoginFlows` | Active `MibLoginFlow` instances per profile |
| `bmlLoginFlows` | Active `BmlLoginFlow` instances per profile |
| `mibMutex` | Coroutine mutex — serializes all MIB API calls to prevent session corruption |
| `accounts: List<BankAccount>` | Combined view of every visible account across all banks |
| `fullName: String` | Account holder name (best available across logins) |
| `mibSessions` | Map of MIB loginId → active session (cookies + DH key) |
| `mibProfilesMap` | Per-loginId list of MIB CIF profiles |
| `mibLoginFlows` | Active `MibLoginFlow` instances per login |
| `mibAccounts: List<BankAccount>` | MIB-only slice of `accounts` |
| `bmlSessions` | Map of BML profileId → OAuth tokens |
| `bmlProfilesMap` | Per-loginId list of `BmlProfile` |
| `bmlLoginFlows` | Active `BmlLoginFlow` instances per login (holds web session cookies for activation) |
| `bmlAccounts: List<BankAccount>` | BML-only slice |
| `fahipaySessions` | Map of Fahipay loginId → authID + session cookie |
| `fahipayAccounts: List<BankAccount>` | Fahipay-only slice |
| `mibMutex` | Coroutine mutex — serializes all MIB profile-switch + request sequences |
### Profile Visibility
+19 -6
View File
@@ -6,18 +6,25 @@ Shown once on first launch. Walks the user through language selection, security
## Activity — `OnboardingActivity`
`OnboardingActivity` hosts three sequential fragments managed by a `ViewPager2` with manual paging (swipe disabled). Progress dots are shown below the pager.
`OnboardingActivity` hosts **4** pages managed by a `ViewPager2` (`OnboardingPagerAdapter.kt:32-38`). The adapter returns `slides.size + 1` items — three text slides plus the security-setup page inserted at position 1. Tap-to-navigate on the page-indicator dots is disabled; the only forward motion is the bottom **Next** / **Get Started** button.
Each fragment has a **Continue** button that is only enabled after the user satisfies a completion requirement. Scrolling to the bottom of a slide is required before Continue activates on content slides.
| Position | Fragment | Purpose |
|---|---|---|
| 0 | `OnboardingFragment` (welcome) | Language pick + welcome copy |
| 1 | `SecuritySetupFragment` | PIN or pattern setup |
| 2 | `OnboardingConfigureFragment` | Theme, accent, nav mode |
| 3 | `OnboardingFragment` (`isLast = true`) | Final screen with **Get Started** |
Each page has a gate that controls when the bottom button activates.
---
## Slide 1 — Language & Welcome (`OnboardingFragment`)
- Displays a welcome illustration and app name
- Language selector chip group (English / Dhivehi)
- Selecting a language immediately updates the app locale
- Continue button becomes active once a language is selected (or immediately if system locale is already supported)
- Language toggle group (`languageToggle` in `OnboardingActivity.kt:86-92`): **English** / **Dhivehi** only
- Selecting a language calls `AppCompatDelegate.setApplicationLocales()` immediately
- The language section is only visible while the user is on page 0
---
@@ -70,9 +77,15 @@ All preferences are written to `CredentialStore` / `SharedPreferences` immediate
---
## Final Slide — Get Started
The last page is another `OnboardingFragment` with `isLast = true`. After the user scrolls it to the bottom, a 5-second `CountDownTimer` (`OnboardingActivity.kt:95-97`, `155-164`) starts — the **Get Started** button is disabled until the timer reaches zero, with the remaining seconds shown in the button label.
---
## Completion
When the user taps Continue on slide 3, `OnboardingActivity` sets the `onboardingDone` flag and finishes. `MainActivity` then routes to `LoginActivity` (no credentials yet) on the next launch or immediately via `startActivity`.
When the user taps **Get Started**, `OnboardingActivity` sets the `onboarding_done` flag, marks `app.isUnlocked = true`, and launches `LoginActivity` directly so the user does not have to re-authenticate immediately after setup.
---
+8 -4
View File
@@ -53,13 +53,17 @@ On mismatch the attempt counter increments and an error shake animation plays.
## Brute-Force Protection
Constants live in `LockActivity.kt:44-45`: `MAX_ATTEMPTS = 5`, `LOCKOUT_MS = 30_000L`.
| Threshold | Behaviour |
|---|---|
| 14 wrong attempts | Error label shown, counter visible |
| 5 wrong attempts | 30-second lockout; keypad/pattern disabled |
| After lockout | Counter resets; user may try again |
| 14 wrong attempts | Error label shows remaining attempts |
| 5 wrong attempts | 30-second lockout; further entries are rejected by `checkAndShowLockout()` until `LOCKOUT_MS` has elapsed since the last fail |
| After 30 s | Lockout expires (purely time-based, via `last_fail_time`) — the counter itself does **not** reset until a successful unlock |
The attempt counter and lockout timestamp are stored in **plain** `SharedPreferences` (not encrypted) — a known limitation documented in the security audit. The app does not wipe credentials after repeated failures.
The fail counter resets to zero only inside `resetFailures()` (`LockActivity.kt:313-315`), which is called from the success paths of biometric, PIN, and pattern entry. A user who repeatedly fails will hit the 30-second wait again on the very first failure after each lockout window expires.
The attempt counter and last-fail timestamp live in a dedicated `lock_attempts` `SharedPreferences` file (`LockActivity.kt:41`) — plain (not encrypted), which is a known limitation documented in the security audit. The app does not wipe credentials after repeated failures.
---
+26 -21
View File
@@ -30,40 +30,45 @@ Tapping a card navigates to `CredentialsFragment` with the selected bank pre-set
## Credentials — `CredentialsFragment`
### Shared Fields
For MIB and BML the form also includes an **OTP seed** field. The user can:
- Paste the raw base32 / `otpauth://` seed directly into `etOtpSeed`
- Tap the QR scan button (`btnScanOtpSeed`) to launch the [QR scanner](25-qr-scanner.md); the result is parsed by `util/OtpauthParser` and written to `etOtpSeed`. If the QR contains multiple entries the user picks one via a dialog (`CredentialsFragment.kt:67-84`).
A live TOTP preview card under the field updates every second so the user can confirm the seed is correct before submitting. The seed is required for MIB and BML login button activation (`updateLoginButtonState()`).
### MIB Login
Fields:
- Username
- Password
Fields: Username, Password, OTP seed.
Flow on submit:
1. `MibLoginFlow.login()` performs Diffie-Hellman key exchange, then authenticates with Blowfish/ECB-encrypted credentials
2. On success, fetches `operatingProfiles` — the list of CIF profiles (Individual, Sole Propr, etc.)
3. Each profile is stored as a `MibAccount` with `bank = "MIB"` and `cifType` from the API
4. Sessions are stored in `BasedBankApp.mibSessions`
1. `MibLoginFlow.login(username, passwordHash, otpSeed)` — Diffie-Hellman key exchange, then Blowfish/ECB-encrypted credentials
2. On success, fetches `operatingProfiles` — the list of CIF profiles
3. Each profile is stored as a `BankAccount` with `bank = "MIB"` and `cifType` from the API
4. `MibProfileClient().fetchPersonalProfile(session)` is called post-login to retrieve and persist the full account-holder name (used by the OTP screen and elsewhere)
5. Sessions are stored in `BasedBankApp.mibSessions`
### BML Login
Fields:
- Username (customer ID)
- Password
Fields: Username (customer ID), Password, OTP seed.
Flow on submit:
1. `BmlLoginFlow.login()` — OAuth password grant → access token + refresh token
2. Fetches dashboard → list of CASA accounts + cards
3. Each account/card stored as `MibAccount` with `bank = "BML"`
4. Tokens stored in `BasedBankApp.bmlSessions`
Flow on submit (`CredentialsFragment.kt:272-326`):
1. `BmlLoginFlow.login(username, password, otpSeed)` — returns a list of `BmlProfile`
2. For each non-business profile, `flow.activateProfile(profile, loginTag)` runs; `BmlActivationResult.Success` populates `bmlAccounts` and stores a per-profile session
3. Business profiles are skipped at login (user can enable them later via [Settings → Logins](14-settings.md#bml-business-profile-activation); that path returns `BmlActivationResult.NeedsBusinessOtp` and runs the OTP-channel flow)
4. Credentials saved via `store.saveBmlCredentials(loginId, username, password, otpSeed)`
5. Tokens stored per profile in `BasedBankApp.bmlSessions`
### Fahipay Login
Fields:
- Mobile number (7-digit local, auto-prefixed with +960)
- Password
Fields: Mobile / ID-card, Password. Two-step TOTP — after the password is accepted the same screen re-uses itself to collect the TOTP, with `fahipayAwaitingTotp = true` (`CredentialsFragment.kt:60`) controlling the UI state.
Flow on submit:
1. `FahipayLoginFlow.login()` — authenticates against Fahipay API
2. On success, stores `authID` + `__Secure-sess` cookie
3. Single wallet account stored with `bank = "FAHIPAY"`
2. Server responds with a TOTP challenge; user enters the code
3. On success, stores `authID` + `__Secure-sess` cookie
4. Single wallet account stored with `bank = "FAHIPAY"`
---
@@ -71,7 +76,7 @@ Flow on submit:
Each MIB login can have multiple CIF profiles (e.g., an individual and a business account under the same username). Each profile appears as a separate entry in the accounts list and can be toggled independently in Settings → Logins.
BML and Fahipay each yield a single profile per login.
BML can yield multiple profiles per login (personal + business). Fahipay yields a single profile.
Adding the same bank login a second time merges its profiles into the existing login rather than creating a duplicate.
+7 -2
View File
@@ -6,7 +6,12 @@ The accounts screen is typically the default home destination. It shows all acti
## Fragment — `AccountsFragment`
Hosts a `RecyclerView` driven by `AccountsAdapter`. Observes `HomeViewModel.accounts` (a `LiveData<List<MibAccount>>`). The list is filtered to only include accounts whose profile visibility flag is enabled.
Hosts a `RecyclerView` driven by `AccountsAdapter`. Observes `HomeViewModel.accounts` (a `LiveData<List<BankAccount>>`). The list is filtered to only include accounts whose profile visibility flag is enabled.
Two image loaders are passed into the adapter (`AccountsFragment.kt:44-78`):
- `profileImageLoader` — MIB profile images fetched on-demand via `MibLoginFlow.fetchProfileImage()` (P41), keyed by hash and cached in memory for the fragment lifetime
- `localProfileImageLoader` — BML and Fahipay avatars loaded via `ProfileImageStore` using `bml_{profileId}` / `fahipay_{loginId}` keys
A **pull-to-refresh** gesture triggers `HomeActivity.autoRefresh()`, which re-fetches all bank dashboards in parallel.
@@ -25,7 +30,7 @@ Accounts are grouped by bank + CIF type (for MIB) or bank name (for BML/Fahipay)
### Account / Card Rows
Each row is bound from an `AccountListDisplay` object produced by `AccountListParser.from(account)`. See [Account Parser Architecture](PARSERS.md) for mapping details.
Each row is bound from an `AccountListDisplay` object produced by `AccountListParser.from(account)`. See [Account Parser Architecture](19-parsers.md) for mapping details.
| Field | Row element |
|---|---|
+4 -2
View File
@@ -6,7 +6,7 @@ Displays the transaction history for a single account. Opened by tapping an acco
## Fragment — `AccountHistoryFragment`
Receives the selected `MibAccount` via navigation arguments.
Receives the selected `BankAccount` via `AccountHistoryFragment.newInstance(account)`.
---
@@ -26,10 +26,12 @@ Results are mapped to a common display model and shown in a `RecyclerView`.
## Infinite Scroll
The list supports **infinite scroll** (pagination). When the user scrolls near the bottom of the loaded items, the next page is automatically fetched and appended. A loading spinner appears at the bottom while a page is in flight.
The list supports **infinite scroll** (pagination) with a page size of 10. When the user scrolls near the bottom of the loaded items, the next page is automatically fetched and appended. A loading spinner appears at the bottom while a page is in flight.
Page state (current page, total pages) is tracked in the fragment's `ViewModel`. If the last page has been reached the spinner is hidden and no further requests are made.
For MIB accounts a per-account encrypted disk cache (`util/mibapi/TransactionCache`) seeds the first page from local storage, so reopening an account history is instant while the live fetch refreshes in the background.
---
## Search / Filter
+24 -2
View File
@@ -10,7 +10,23 @@ Opened via:
- Navigation menu
- Quick-transfer button on an account row (source pre-selected)
- `OPEN_TRANSFER` intent action
- `OPEN_SCAN_QR` intent — opens the QR scanner immediately (`newInstanceWithAutoScan()`)
- QR scan result (recipient and optional amount pre-filled)
- Donate buttons in Settings → About (`newInstance(...)` with pre-filled recipient)
### Factory Methods
All defined in `TransferFragment.kt:179-242`:
| Method | Use case |
|---|---|
| `newInstance(accountNumber, displayName, subtitle, colorHex, imageHash)` | Pre-fill recipient (contact tap, recents, donate) |
| `newInstanceFrom(account)` | Pre-select a source account (quick-transfer from accounts list) |
| `newInstanceFromQr(accountNumber, displayName, amount, remarks, fromAccountNumber?)` | PayMV QR scan |
| `newInstanceFromBmlQr(qrUrl, fromAccountNumber?)` | BML ebanking / pay.bml URL scan — locks recipient |
| `newInstanceWithAutoScan()` | Open the QR scanner on load |
See [Transfer Flows](20-transfer-flows.md) for the full routing logic.
---
@@ -26,7 +42,7 @@ The user can specify a recipient in three ways:
1. **Manual entry** — type an account number directly
2. **Contact picker** — opens `ContactPickerSheetFragment` to select a saved contact
3. **QR scan**opens the camera scanner; a PayMV QR result pre-fills the account number, amount, and remarks
3. **QR scan**launches [QrScannerActivity](25-qr-scanner.md); a PayMV QR result pre-fills the account number, amount, and remarks; a BML ebanking / pay.bml URL switches the form into [BML QR merchant payment](20-transfer-flows.md#bml-qr-merchant-payment-flow) mode
---
@@ -56,7 +72,13 @@ The resolved name is displayed below the account number field for the user to co
## Biometric Gate
If biometric-for-transfers is enabled in Settings → Security, `BiometricPrompt` is shown before the transfer is submitted. A failed or cancelled biometric blocks submission.
If both `biometrics_enabled` and `biometrics_transfer_confirm` are set in Settings → Security (`SettingsSecurityFragment.kt:59`), `BiometricPrompt` is shown before the transfer is submitted. A failed or cancelled biometric blocks submission.
---
## BML USD → MIB Auto-Add Contact
When the source is a BML USD account and the destination is a MIB account but no saved BML contact exists for it, the app shows a "Contact required" dialog with a **Save** button that opens `AddContactSheetFragment.newInstance(bmlProfileId, accountNumber, recipientName, currency)` pre-filled (`TransferFragment.kt:1136-1164`). The user must save the contact through this sheet before BML can issue a USD cross-bank transfer.
---
+2 -2
View File
@@ -32,7 +32,7 @@ Contacts can be assigned to user-defined categories (e.g., "Family", "Business")
## Add Contact — `AddContactSheetFragment`
A bottom sheet for creating or editing a contact.
A bottom sheet for creating or editing a contact. The companion exposes a single `newInstance(bmlProfileId?, accountNumber?, recipientName?, currency?)` factory (`AddContactSheetFragment.kt:572-585`) used by the BML USD → MIB auto-add flow on the [Transfer screen](20-transfer-flows.md#bml-usd--mib-no-saved-contact).
### Fields
@@ -50,7 +50,7 @@ The pencil icon next to the avatar opens a chooser:
- **Gallery** — pick from device gallery
- **Camera** — capture a new photo (temp file in `cacheDir`)
The image is stored locally in `filesDir/profile_images/` via `ProfileImageStore` with key `"contact_{id}"`.
The selected image is held in memory as a base64 string (`selectedImageBase64` in `AddContactSheetFragment`) and uploaded inline with the contact on save. A local copy is also kept in `util/ContactImageCache` (cache directory, PNG, keyed by the image hash) so the avatar can be rendered without a round-trip on next open.
### Save
+21 -16
View File
@@ -8,6 +8,14 @@ The activities screen shows a local log of completed transfers initiated within
Displays a chronological `RecyclerView` of locally stored transfer records. These records are written by the app at transfer completion time — they are not fetched from bank APIs.
A search bar at the top filters loaded entries by recipient label / account / remarks (`ActivitiesFragment.kt:52-58`).
### Storage
Backed by `util/ReceiptStore` — a single encrypted JSON file at `filesDir/activities.json` (via `CacheEncryption`). Each entry is a `TransferReceiptData` plus a `savedAt` timestamp.
Only successful transfers are saved. The save call is guarded by `ok && receipt != null` (`TransferFragment.kt:1201`), so there is no "Failed" status — failures stay as a toast / dialog and never persist.
---
## Activity List
@@ -17,7 +25,6 @@ Each row shows:
- Recipient name and account number
- Transfer amount (hidden as `••••` when hide-amounts is active)
- Date and time
- Status badge (Completed / Failed)
Tapping a row opens `TransferReceiptFragment` for that record.
@@ -29,26 +36,24 @@ A full-screen receipt view shown immediately after a successful transfer and acc
### Receipt Fields
| Field | Notes |
Schema: `TransferReceiptData` in `ui/home/TransferReceiptData.kt`.
| Field | Source |
|---|---|
| Bank | Source bank logo and name |
| From account | Sender account number |
| To account | Recipient account number |
| Recipient name | As resolved at transfer time |
| Amount | Formatted with currency |
| Remarks | Transfer purpose text |
| Reference number | Bank-issued transaction reference |
| Date and time | Transfer timestamp |
| Status | Completed / Failed |
| `bank` | `"MIB"` / `"BML"` / `"FAHIPAY"` |
| `amount` / `currency` | Entered + source-account currency |
| `fromLabel` / `fromColorHex` / `fromProfileImageHash` | Captured source account display |
| `toLabel` / `toAccount` / `toBank` | Resolved recipient |
| `remarks` | Free text the user entered |
| `mibReferenceNo` / `mibTransactionDate` | Populated for MIB transfers |
| `bmlFromName` / `bmlReference` / `bmlTimestamp` / `bmlMessage` | Populated for BML transfers |
### Actions
- **Share** — generates a text or image summary of the receipt and opens the system share sheet
- **Save to Gallery** — renders the receipt as a bitmap and saves it to the device's Pictures folder (requires `WRITE_EXTERNAL_STORAGE` on API < 29, or `MediaStore` on API 29+)
- **Share** — `captureReceiptBitmap()` uses `PixelCopy` over the receipt card, writes a PNG to `cacheDir/receipts/`, exposes it via `FileProvider`, and fires `ACTION_SEND`
- **Save to Gallery** — same capture, then writes to `MediaStore.Images` (`Pictures/`) on API 29+ or directly to `Environment.DIRECTORY_PICTURES` on older releases
### Screenshot Note
If `FLAG_SECURE` is active (user has enabled the screenshots restriction), the Save to Gallery action uses an off-screen rendering path that bypasses the restriction for the explicit save action only.
`PixelCopy` reads from the window surface, so both actions fail while `FLAG_SECURE` is active. There is no off-screen rendering fallback.
---
+8 -4
View File
@@ -30,10 +30,14 @@ Standard RFC 6238 TOTP:
## Supported Banks
| Bank | Seed source |
|---|---|
| BML | Enrolled via BML app setup; seed stored in `CredentialStore` |
| MIB | MIB business/corporate OTP seed (if applicable) |
One card is rendered for every MIB and every BML login that has a stored OTP seed (`OtpFragment.kt:93-98`). Seeds are per-`loginId` in `CredentialStore`.
| Bank | Seed source | Card label |
|---|---|---|
| MIB | `loadMibCredentials(loginId).otpSeed` (entered at login) | `"MIB · {fullName}"` |
| BML | `loadBmlCredentials(loginId).otpSeed` (entered at login) | `"BML · {fullName}"` |
If no full name has been cached the label falls back to plain `"MIB"` / `"BML"` and a background `MibProfileClient.fetchPersonalProfile()` / `BmlAccountClient.fetchUserInfo()` call refreshes it.
---
+17 -40
View File
@@ -1,61 +1,38 @@
# PayMV QR Screen
Handles both sides of PayMV/Favara QR payments: generating a receive-payment QR code and scanning a QR code to initiate a transfer.
Generates a receive-payment PayMV / Favara QR code. **Generation only** — the send/scan side of PayMV lives in `TransferFragment` via `newInstanceWithAutoScan()` and the [QR scanner](25-qr-scanner.md).
---
## Fragment — `PayMvQrFragment`
Two tabs: **Receive** (generate QR) and **Send** (scan QR).
---
## Receive Tab — Generate QR
Generates a static PayMV QR code that others can scan to pay the user.
A single screen (no tabs). Re-renders the QR live as the user edits the form.
### Fields
| Field | Notes |
| Field | Source / behaviour |
|---|---|
| Source account | Dropdown of all visible accounts; determines acquirer BIC |
| Amount | Optional — leave blank for open-amount QR |
| Purpose | Optional free-text payment purpose |
| Name | Auto-filled from the selected account's holder name |
| Source account dropdown | `viewModel.accounts`, filtered to non-card MVR accounts (MIB and BML USD currently excluded — both flagged as TODO in source). Defaults to `CredentialStore.getDefaultAccountNumber()` when set |
| Amount (`etAmount`) | Optional. Blank open-amount QR |
| Reference (`etReference`) | Free-text purpose; defaults to `paymvqr_reference_default` if blank — written to tag 62→08 |
| Include phone (`switchIncludePhone`) | When on, writes the saved BML / Fahipay mobile to sub-tag 26→05 (auto-prefixed `+960` if 7-digit local) |
### Generation
On tap **Generate**, the fragment builds a decimal TLV payload per the [PayMV QR Format](18-paymv-qr-format.md) spec:
`buildQrPayload()` assembles a decimal TLV payload per the [PayMV QR Format](18-paymv-qr-format.md):
1. Assembles all TLV fields (format indicator, point-of-initiation, tag 26 container, MCC, currency, amount if set, country, name, tag 62 container, tag 80 container)
2. Selects the acquirer BIC from the source account's bank (`MALBMVMV` / `MADVMVMV` / `FAHIMVMV`)
3. Appends `"6304"` and computes CRC-16/CCITT-FALSE over the full string
4. Renders the complete string as a QR code bitmap using the ZXing encoder
5. Displays the QR code full-screen with the account number and name below
1. Tag 26 container: GUI (`mv.favara.mpqr`), acquirer BIC, account number, optional mobile, `IPAY`
2. Acquirer BIC is derived from the source account's bank: `MALBMVMV` (BML) / `MADVMVMV` (MIB) / `FAHIMVMV` (Fahipay)
3. Tag 62 container: random 9-char reference + the purpose text
4. Tag 80 container: GUI + ISO timestamp
5. Appends `"6304"` and computes CRC-16/CCITT-FALSE over the full string
### Share
The rendered card image (bank-styled background plus QR) is shown in-place.
A **Share** button exports the QR bitmap via the system share sheet (image/png).
### Actions
---
## Send Tab — Scan QR
Scans a QR code and pre-fills the transfer screen.
### Scanner
Opens the device camera with a QR viewfinder overlay. Supported QR formats:
| QR type | Handling |
|---|---|
| PayMV / Favara decimal TLV | Parse account number (tag 26→03), amount (tag 54), name (tag 59), purpose (tag 62→08); navigate to `TransferFragment` pre-filled |
| BML plain URL (`https://pay.bml.com.mv/app/...`) | Navigate to `BmlQrPayFragment` |
| BML embedded in combined QR (root tag 35 → sub 20 → sub-sub 01) | Extract URL; navigate to `BmlQrPayFragment` |
### Error Handling
If the scanned code is not a recognized format, a `Snackbar` error is shown and the scanner remains open.
- **Share** (`btnShare`) — exports the rendered card via `FileProvider` + `ACTION_SEND`
- **Save** (`btnSave`, `PayMvQrFragment.kt:78`) — writes the PNG to `MediaStore.Images` / `Pictures/`
---
+2 -76
View File
@@ -1,82 +1,8 @@
# BML QR Pay
Handles BML gateway QR payments — scanning a merchant QR code and completing the 3-step TOTP-authenticated payment flow.
> **This flow no longer lives in a dedicated fragment.** `BmlQrPayFragment.kt` still exists as a source file but is unreachable — no callers, no nav graph entry, no intent action routes here. The actual BML gateway QR flow runs inside `TransferFragment` via `TransferFragment.newInstanceFromBmlQr(qrUrl, fromAccountNumber)`. See [Transfer Flows — BML QR Merchant Payment Flow](20-transfer-flows.md).
---
## Fragment — `BmlQrPayFragment`
Opened via:
- Scanning a BML plain URL QR in `PayMvQrFragment`
- Scanning a combined Fahipay/PayMV QR that embeds a BML gateway URL
- The `OPEN_PAY_WITH_CARD` intent action
---
## Step 1 — Resolve QR
The fragment receives a BML gateway URL (e.g., `https://pay.bml.com.mv/app/<base64>`).
It calls the BML `payrequest` lookup API (see [QR Payment](../bmlapi/13-qr-payment.md)) to resolve the URL to merchant details:
- Merchant name (narrative1)
- Merchant address (narrative2, narrative3)
- Amount (`"0.00"` for static QRS, or preset amount for QRR)
- Currency
The resolved details are displayed on screen for the user to review before paying.
---
## Source Account Selection
A dropdown lists all BML accounts. The selected account's internal UUID (`internalId`) is used as `debitAccount` in the payment request.
---
## Amount Entry
- If the QR is a **static QRS** (amount `"0.00"`), the amount field is editable and required
- If the QR is a **dynamic QRR**, the amount is pre-filled and read-only
---
## Step 2 — TOTP Payment
The payment uses the standard 3-step BML TOTP flow:
### 2a — Initiate
POST to `/walletpayments/pay` with `action: "approve"`, `debitAccount`, `requestId` (the `trxn_hash`), `amount`, `currency`. Expected response: `code: 99` (OTP required).
> This step may be skipped if the gateway already indicates OTP is required.
### 2b — Request OTP Channel
Same POST with `channel: "token"` added. Expected response: `code: 22` (OTP generated and sent to the authenticator).
### 2c — Confirm with TOTP
The fragment presents an OTP input dialog. The user opens the [OTP Screen](12-otp-screen.md) or reads the TOTP from their authenticator app, then enters the 6-digit code.
Same POST with `channel: "token"` and `otp: "<code>"`. On success: `code: 0` with merchant and amount in `payload`.
---
## Success
On successful payment a confirmation card is shown:
- Merchant name
- Amount paid
- Currency
A **Done** button dismisses the fragment.
---
## Error Handling
Failed payments (`success: false`) display the `message` field from the API response. The user may retry with a fresh TOTP code.
The on-the-wire payment protocol is unchanged — see [BML QR Payment API](../bmlapi/13-qr-payment.md) for the 3-step TOTP flow (`approve``channel: token``otp`).
---
+7 -6
View File
@@ -33,13 +33,14 @@ Data comes from `HomeViewModel.bmlLoanDetails`, which is fetched from the BML lo
---
## Card Limits Section (BML)
## Foreign Spend Limits (BML)
`HomeViewModel.bmlLimits` provides credit card limit information for BML card accounts. Displayed alongside the loan section:
- Card name
- Total limit
- Available limit
- Used amount
`HomeViewModel.bmlLimits` (`List<BmlForeignLimit>`) provides **foreign-currency spend limits** per BML card — these are MMA-imposed travel/online caps, not credit-card credit limits. Displayed alongside the loan section:
- Card / profile name
- ECOM remaining / cap (always visible)
- General remaining / cap (always visible)
- ATM, POS and Medical remaining / cap (expandable)
- "Disabled" flags for ATM / POS where applicable
---
+11 -5
View File
@@ -6,14 +6,16 @@ The settings hub and the logins management screen.
## Settings Hub — `SettingsFragment`
A simple preference-list screen with navigation links to sub-sections:
A simple preference-list screen with navigation links to sub-sections (order as defined in `SettingsFragment.kt:26-33`):
| Entry | Destination |
|---|---|
| Logins | `SettingsLoginsFragment` |
| Security | `SettingsSecurityFragment` |
| Appearance | `SettingsAppearanceFragment` |
| Privacy & Security | `SettingsSecurityFragment` |
| Notifications | `SettingsNotificationsFragment` |
| Storage | `SettingsStorageFragment` |
| About | `SettingsAboutFragment` |
---
@@ -55,12 +57,16 @@ A **+** (Add) button navigates to `LoginActivity` → `BankSelectionFragment` to
A **Logout** button on each login card shows a confirmation dialog. On confirm:
- Session tokens are revoked where supported
- Credentials are removed from `CredentialStore`
- All associated `MibAccount` objects are removed from `BasedBankApp`
- All associated `BankAccount` objects are removed from `BasedBankApp`
- The accounts list is refreshed
### Business OTP Seed (MIB)
### BML Business Profile Activation
For MIB corporate/business profiles, an **OTP Seed** entry allows importing a TOTP seed string. The seed is stored encrypted in `CredentialStore` and used by the [OTP Screen](12-otp-screen.md).
For inactive BML business profiles, an **Activate** button kicks off the OTP-channel selection (email / SMS) and OTP confirmation flow — see `SettingsLoginsFragment.kt:572, 722 activateBmlBusinessProfile()` and [Business Profile OTP](../bmlapi/02-business-otp.md).
### OTP Seed
OTP seeds (MIB and BML business) are imported **during initial login** in `CredentialsFragment` — either by scanning the `otpauth://` QR or pasting the raw seed. The seed is stored encrypted in `CredentialStore` and used by the [OTP Screen](10-otp-screen.md). To replace a seed, remove the login and re-add it.
---
+10 -3
View File
@@ -27,20 +27,27 @@ Biometric availability is checked via `BiometricManager.canAuthenticate()`. The
## Auto-Lock Timeout
A radio group or dropdown to set the inactivity timeout:
A `MaterialButtonToggleGroup` to set the inactivity timeout (`SettingsSecurityFragment.kt:81-94`). Default: **1 minute**.
| Option | Timeout |
|---|---|
| 30 seconds | 30 s |
| 1 minute | 60 s |
| 1 minute | 60 s (default) |
| 3 minutes | 180 s |
| 5 minutes | 300 s |
| Never | Disabled |
There is no "Never" option — auto-lock cannot be disabled.
The selection is stored in `SharedPreferences` and read by `HomeActivity` on each timer reset.
---
## Auto-Unlock on Correct PIN
When the lock method is **PIN**, an extra `switchAutoUnlockPin` toggle (`SettingsSecurityFragment.kt:45-51`) lets the entered PIN unlock immediately on the last correct digit, skipping the **Confirm** tap. Not available for the Pattern method.
---
## Screenshots
A toggle for `FLAG_SECURE` on `HomeActivity`'s window.
+33 -27
View File
@@ -10,12 +10,13 @@ Controls navigation mode, nav slot assignment, theme, accent colour, and languag
## Navigation Mode
A toggle/radio group:
A `MaterialButtonToggleGroup` with three options (`SettingsAppearanceFragment.kt:65-80`):
| Mode | Description |
|---|---|
| Drawer | Slide-out navigation drawer (default) |
| Bottom Navigation | Bottom bar with 3 visible slots + Dashboard + More |
| Mode | Value | Description |
|---|---|---|
| Drawer (default) | `NAV_MODE_DRAWER` | Slide-out navigation drawer |
| Bottom | `NAV_MODE_BOTTOM` | Bottom bar with 3 visible slots + pinned Dashboard + More |
| Circular | `NAV_MODE_CIRCULAR` | Radial wheel — see [Circular Nav](26-circular-nav.md) |
Changing mode takes effect immediately; `HomeActivity` recreates its navigation structure.
@@ -23,31 +24,33 @@ Changing mode takes effect immediately; `HomeActivity` recreates its navigation
## Navigation Slot Customisation — `NavCustomization`
A drag-and-drop list of all 10 navigation destinations. The user reorders items to assign them to slots.
### Drawer Mode
All 10 items appear in the drawer in the configured order.
Drag-and-reorder lists tailored per mode (`NavCustomization.kt:52-93`). The disabled list is dimmed to 38% opacity.
### Bottom Navigation Mode
- Slots 13 appear in the bottom bar
- Slot 410 appear in the **More** bottom sheet (`NavMoreSheetFragment`)
- Dashboard is always pinned as the first tab and is not part of the 10-item pool
- **3** slots in the bottom bar (defaults: Accounts, Contacts, Transfer)
- Dashboard is always pinned as the leftmost tab and is not part of the 3-item pool
- All non-slot items appear in the **More** bottom sheet (`NavMoreSheetFragment`)
- `switchShowLabels` toggle controls whether the bottom-bar items render text labels
### Circular Navigation Mode
- **4** customisable wheel slots (defaults: Transfer, Cards, Contacts, Accounts)
- Dashboard, More and the wheel-lock are always present at 6 o'clock / 8 o'clock / centre
### Quick Action Slots
Two dedicated quick-action slots are configured separately at the bottom of the customisation screen. These map to FAB-style buttons shown on the dashboard card.
Two dedicated FAB-style slots on the dashboard. Defaults: Transfer + PayMV QR. Hidden when the user is in Bottom mode (the dashboard hides the FAB row when a bottom bar is present).
### Persistence
The ordered list is serialised to `SharedPreferences` as a comma-separated string of destination IDs.
Each slot is stored individually in `SharedPreferences` (`bottom_nav_slot_{n}_key`, `circular_slot_{n}_key`, `quick_action_{n}_key`).
---
## Theme
A three-way selector:
A three-way selector applied via `AppCompatDelegate.setDefaultNightMode()`:
| Option | Behaviour |
|---|---|
@@ -55,27 +58,30 @@ A three-way selector:
| Light | Forces light theme |
| Dark | Forces dark theme |
Applied via `AppCompatDelegate.setDefaultNightMode()`.
### Pitch Black
`switchPitchBlack` (`SettingsAppearanceFragment.kt:162-166`) — only enabled in **Dark** mode. Applies `ThemeOverlay_PitchBlack` for OLED-friendly true-black surfaces.
---
## Accent Colour
A horizontal chip row with several Material colour options. The selected accent is applied to the app's `MaterialTheme` colour scheme (primary / secondary).
A toggle group (`accentToggle`) with four options (`SettingsAppearanceFragment.kt:172-192`, `ThemeHelper.kt:14-17`):
| Option | Seed |
|---|---|
| Blue (default) | `#3F65AD` |
| Red | `#D32F2F` |
| Green | `#4CAF50` |
| Custom | User-picked hex stored in `accent_custom_color` |
On API 31+ the seed is fed through `DynamicColors.applyToActivityIfAvailable(...).setContentBasedSource(seedBitmap)` for a full content-derived palette. On older releases a static `ThemeOverlay_Accent_*` style is applied. Disabled when Theme = System (dynamic colours own the palette).
---
## Language
A dropdown or chip selector:
| Option |
|---|
| System default |
| English |
| Dhivehi |
Applied via `AppCompatDelegate` locale override. Takes effect immediately (activity recreate).
A toggle group (`languageToggle`) with two options — **English** and **Dhivehi**. There is no "System default" entry. Applied via `AppCompatDelegate.setApplicationLocales()`; takes effect immediately.
---
+23 -35
View File
@@ -1,54 +1,42 @@
# Settings — Storage
Manages locally cached data and profile images.
Clears locally cached data.
---
## Fragment — `SettingsStorageFragment`
Displays an overview of locally stored data categories with clear actions for each.
A single **Clear All Caches** button (`SettingsStorageFragment.kt:44-49`). No per-category clear actions, no size readout.
---
## Stored Data Categories
## What gets cleared
| Category | Location | Description |
|---|---|---|
| Profile images | `filesDir/profile_images/` | BML and Fahipay profile photos stored by `ProfileImageStore` |
| Contact images | `filesDir/profile_images/` | Contact avatar photos |
| Transaction image cache | In-memory / HTTP cache | Merchant logos loaded in account history |
| Camera temp file | `cacheDir/profile_photo_tmp.jpg` | Temp file from camera capture; automatically overwritten on next camera use |
| Transfer receipts | Local database | Completed transfer records shown in Activities |
Tapping the button invokes `clearAllCaches()` (`SettingsStorageFragment.kt:62-72`), which clears each of:
| Cache | Backing store |
|---|---|
| `AccountCache` | Encrypted JSON file in `filesDir` |
| `ContactsCache` | Encrypted JSON file in `filesDir` |
| `FinancingCache` | Encrypted JSON file in `filesDir` |
| `ForeignLimitsCache` | Encrypted JSON file in `filesDir` |
| `RecentsCache` | Encrypted JSON file in `filesDir` |
| `CardsCache` | Encrypted JSON file in `filesDir` |
| `TransactionCache` | Per-account encrypted history files (`tx_<key>.json`) |
| `ContactImageCache` | Local contact avatar bitmaps |
It also resets the live `HomeViewModel` flows (`accounts`, `mibCards`, `financing`, `bmlLoanDetails`, `bmlLimits`, `contacts`, `contactCategories`) and triggers a refresh.
---
## Clear Actions
## What is **not** cleared
### Clear Profile Images
- Profile images stored by `ProfileImageStore` (`filesDir/profile_images/`) — BML and Fahipay avatars persist.
- Credentials / sessions in `CredentialStore`.
- Transfer receipts (Activities) — managed by `ReceiptStore`, persists across cache clears.
- Notification read-state in `NotificationsCache`.
Deletes all files in `filesDir/profile_images/` for BML and Fahipay profiles. MIB profile images are stored server-side and are not affected. After clearing, avatars fall back to the initials placeholder.
### Clear Contact Images
Deletes all contact profile images. Contact records are preserved.
### Clear All Caches
Clears:
- `cacheDir` contents (including camera temp file and HTTP response cache)
- In-memory image caches
Does **not** clear credentials, sessions, or transfer records.
### Clear Transfer History
Deletes all locally stored transfer receipt records from the database. This action is irreversible and requires a confirmation dialog.
---
## Storage Usage
The screen may show an approximate size for each category (calculated by summing file sizes in the respective directories).
To remove a login entirely (and the credentials behind it), use [Settings → Logins](14-settings.md).
---
+9 -9
View File
@@ -2,9 +2,9 @@
## Overview
Each bank's API returns account data in different formats and uses different field names for balances, product types, and status. To keep screens bank-agnostic, each bank has a dedicated parser that translates raw `MibAccount` model data into a standard `AccountListDisplay` object. Screens consume only `AccountListDisplay` — they never inspect `bank` or `profileType` or apply bank-specific logic.
Each bank's API returns account data in different formats and uses different field names for balances, product types, and status. To keep screens bank-agnostic, each bank has a dedicated parser that translates raw `BankAccount` (in `api/models/BankModels.kt`) into a standard `AccountListDisplay` object. Screens consume only `AccountListDisplay` — they never inspect `bank` or `profileType` or apply bank-specific logic.
## Bank Discriminator — `MibAccount.bank`
## Bank Discriminator — `BankAccount.bank`
All dispatchers route by `account.bank`, a string set explicitly by each login flow at account creation time:
@@ -14,9 +14,9 @@ All dispatchers route by `account.bank`, a string set explicitly by each login f
| `"BML"` | `BmlLoginFlow` |
| `"FAHIPAY"` | `FahipayLoginFlow`|
`profileType` is a bank-internal value (e.g. MIB's numeric profile ID, or BML's `"BML_PREPAID"`) and is **never** used for bank routing. Card-type checks within BML still use `profileType` (`"BML_PREPAID"` / `"BML_CREDIT"`).
`profileType` is a bank-internal value (e.g. MIB's numeric profile ID, or BML's `"BML_PREPAID"`) and is **never** used for bank routing. Card-type checks within BML still use `profileType` (`"BML_PREPAID"` / `"BML_CREDIT"` / `"BML_DEBIT"`).
`cifType` (MIB only) is the human-readable profile category name returned by the `operatingProfiles` API (e.g. `"Individual"`, `"Sole Propr"`). It is stored on `MibAccount` and surfaced in the accounts list section header and settings. It is **never hardcoded** in the app.
`cifType` (MIB only) is the human-readable profile category name returned by the `operatingProfiles` API (e.g. `"Individual"`, `"Sole Propr"`). It is stored on `BankAccount` and surfaced in the accounts list section header and settings. It is **never hardcoded** in the app.
## Standard Output Model
@@ -37,7 +37,7 @@ data class AccountListDisplay(
```kotlin
// util/AccountListParser.kt
AccountListParser.from(account: MibAccount): AccountListDisplay?
AccountListParser.from(account: BankAccount): AccountListDisplay?
```
Routes to the correct parser based on `account.bank`. Returns `null` for unknown banks — never falls back to a specific bank.
@@ -46,7 +46,7 @@ Routes to the correct parser based on `account.bank`. Returns `null` for unknown
|----------------|-------------------------|
| `"BML"` | `BmlDashboardParser` |
| `"FAHIPAY"` | `FahipayAccountParser` |
| `"MIB"` | `MibAccountParser` |
| `"MIB"` | `MibAccountParser` |
| anything else | `null` |
## Bank Parsers
@@ -64,7 +64,7 @@ Handles both CASA accounts and prepaid/credit cards.
- **Balance**: `availableBalance` from the MIB API directly
- Known product names (`SAVING ACCOUNT`, `CURRENT ACCOUNT`) mapped to short labels
- `cifType` (e.g. `"Individual"`, `"Sole Propr"`) comes from `MibProfile.cifType`, stored on `MibAccount`, displayed in section headers
- `cifType` (e.g. `"Individual"`, `"Sole Propr"`) comes from `MibProfile.cifType`, stored on `BankAccount`, displayed in section headers
### Fahipay — `util/fahipayapi/FahipayAccountParser`
@@ -73,8 +73,8 @@ Handles both CASA accounts and prepaid/credit cards.
## Adding a New Bank
1. Create `util/<bankname>api/<Bank>AccountParser.kt` with a `displayData(account: MibAccount): AccountListDisplay` function
2. Set `bank = "<BANKNAME>"` in the new login flow when creating `MibAccount` objects
1. Create `util/<bankname>api/<Bank>AccountParser.kt` with a `displayData(account: BankAccount): AccountListDisplay` function
2. Set `bank = "<BANKNAME>"` in the new login flow when creating `BankAccount` objects
3. Add a `when` branch in `AccountListParser.from()` (and other dispatchers) for the new bank value
4. No changes needed in any screen or adapter
+11 -7
View File
@@ -10,11 +10,11 @@ The transfer screen (`TransferFragment`) handles all outgoing payments across MI
| Factory method | Behaviour |
|---|---|
| `newInstance(account, name, ...)` | Pre-fills the "To" card from a contact or recents pick |
| `newInstanceFrom(account)` | Pre-selects the given account in the "From" dropdown |
| `newInstanceFromQr(account, name, amount, remarks)` | Pre-fills recipient + optional amount/remarks from a PayMV QR scan |
| `newInstance(accountNumber, displayName, subtitle, colorHex, imageHash)` | Pre-fills the "To" card from a contact, recents pick, or About → Donate |
| `newInstanceFrom(account: BankAccount)` | Pre-selects the given account in the "From" dropdown |
| `newInstanceFromQr(accountNumber, displayName, amount, remarks, fromAccountNumber?)` | Pre-fills recipient + optional amount/remarks from a PayMV QR scan |
| `newInstanceFromBmlQr(qrUrl, fromAccountNumber?)` | BML card/gateway QR merchant payment mode — locks recipient, may pre-fill amount |
| `newInstanceWithAutoScan()` | Opens the QR scanner immediately on load |
| `newInstanceWithAutoScan()` | Opens the [QR scanner](25-qr-scanner.md) immediately on load |
---
@@ -74,6 +74,10 @@ Calls `MibTransferClient.lookup()` directly. Errors from `MibLookupException` ar
Falls back to `BmlValidateClient.validateAccount()` only.
### Destination currency resolution
`resolvedDestCurrency` (`TransferFragment.kt:89`) holds the currency reported by the lookup, defaulting to `""` until verified. When BML returns nothing useful but a MIB session exists, the MIB lookup is used as a fallback to verify the destination currency — this is what allows the BML USD → MIB rejection dialog to know whether it can pre-fill the recipient name and currency into the auto-add contact sheet.
---
## Transfer Type Routing
@@ -125,11 +129,11 @@ These combinations are blocked before a transfer is attempted.
### BML USD → MIB (no saved contact)
**Condition:** source is BML, currency is USD, destination is a MIB account, and no BML contact exists for that account number.
**Condition:** source is BML, currency is USD, destination is a MIB account, and no BML contact exists for that account number (`TransferFragment.kt:1136-1164`).
**Result:** dialog shown — "Contact required". The user must first add the MIB account as a BML contact before a USD cross-bank transfer can proceed.
**Result:** "Contact required" dialog with a **Save** button that opens `AddContactSheetFragment.newInstance(bmlProfileId, accountNumber, recipientName, currency)` pre-filled. If `resolvedDestCurrency` was verified via the MIB fallback lookup the recipient name and currency are passed through; otherwise the user fills them in manually. After saving the contact the user can retry the transfer.
> This is enforced in `initiateTransfer()` before reaching `doBmlTransfer`.
The dialog body text is switched based on whether the destination currency was verified (`R.string.transfer_bml_contact_required_msg_bml_limit` vs. `R.string.transfer_bml_contact_required_msg`).
---
+86
View File
@@ -0,0 +1,86 @@
# Dashboard
The dashboard is the default landing screen when the app opens directly to `HomeActivity` without an external nav target (`HomeActivity.kt:282`). In Bottom mode it is also pinned as the leftmost tab.
---
## Fragment — `DashboardFragment`
A scrollable summary screen that observes `HomeViewModel` flows (`accounts`, `mibCards`, `financing`, `bmlLoanDetails`, `bmlLimits`, `hideAmounts`) and re-renders on every update. Pull-to-refresh delegates to `HomeActivity.triggerRefresh()`.
---
## Aggregate Balance
Sums `availableBalance` across all non-credit, non-loan accounts and displays one figure per currency (MVR, USD).
| Row | Filter |
|---|---|
| `tvMvrBalance` / `tvUsdBalance` | `profileType != "BML_CREDIT"` and `profileType != "BML_LOAN"`, grouped by `currencyName` |
| `tvMvrCredit` / `tvUsdCredit` | `profileType == "BML_CREDIT"` — split per currency, each card hidden when its total is zero |
When `hideAmounts` is on, every balance is replaced with `"MVR ••••••"` / `"USD ••••••"`.
---
## Card Stack — `DashboardCardAdapter`
A horizontal `RecyclerView` with `LinearSnapHelper` that paginates BML cards + MIB cards as a single stack:
- BML cards: visible accounts with `profileType` in (`BML_PREPAID`, `BML_CREDIT`, `BML_DEBIT`) AND `statusDesc == "Active"`
- MIB cards: filtered by `CardsFragment.isMibCardActive(cardStatus)` (i.e. `CHST0`)
- The user's default card (`CredentialStore.getDefaultCardAccountNumber()`) is moved to the front
- Cards hidden via `getHiddenDashboardCardNumbers()` are skipped
Each row renders the masked card number, holder name, status chip and card art (via `CardsFragment.cardImageAsset()` for MIB, `BmlCardParser.cardImageAsset()` for BML), plus two action buttons:
| Button | Behaviour |
|---|---|
| QR Pay | Launches [QrScannerActivity](25-qr-scanner.md); on success routes to `TransferFragment.newInstanceFromBmlQr(...)` for BML / pay.bml URLs, otherwise PayMV pre-fill. Hardcoded to BML — MIB cards show a "not supported" toast |
| NFC Pay | Goes through `NfcPaymentUtil.checkAndProceed()` and opens `CardsFragment` in [Tap to Pay](23-tap-to-pay.md) mode for the chosen card |
---
## Pending Finances
`tvPendingFinances` shows the sum of MIB `outstandingAmount` (across deals) plus BML `|outstandingAmt|` (across loan details). Tapping the card navigates to [Financing](13-financing.md).
---
## Attention Row
`updateAttentionRow()` builds a row of small warning cards when there is something the user should notice:
| Card | Visible when |
|---|---|
| Blocked MVR | Any CASA-style account has `blockedAmount > 0` (MVR) |
| Blocked USD | Same, for USD |
| Overdue total | `financing.overdueAmount` + BML `loanDetails.overdueAmount` is non-zero — tap routes to [Financing](13-financing.md) |
Credit, debit, prepaid, and loan accounts are excluded from the blocked-amount tally — only operating accounts contribute.
---
## Foreign Spend Limits
For each `HomeViewModel.bmlLimits` entry, the dashboard inflates an `ItemForeignLimitBinding`. ECOM and General progress bars are always shown; tapping a card expands ATM / POS / Medical rows. See [Financing](13-financing.md#foreign-spend-limits-bml).
---
## Quick Actions
Below the cards section the dashboard exposes two FAB-style buttons (`btnQuickAction1`, `btnQuickAction2`) bound from `NavCustomization.getQuickActions(prefs)`. Defaults: Transfer and PayMV QR. Hidden when Bottom navigation is active (`refreshQuickActions()`).
---
## Toolbar Logo
While the dashboard is the foreground fragment in Bottom mode, the toolbar shows the launcher icon as a logo plus the app name (drawn into a bitmap with a small gap). `onPause` clears the logo so other fragments are not affected.
---
&nbsp;
---
[← Transfer Flows](20-transfer-flows.md) &nbsp;&nbsp;&nbsp; **Next →** [Cards](22-cards.md)
+100
View File
@@ -0,0 +1,100 @@
# Cards
Unified card management screen for both MIB and BML cards. Supports a horizontally-paged stack, freeze / unfreeze, default-card and hide-from-dashboard toggles, and launches the [Tap to Pay](23-tap-to-pay.md) NFC flow.
---
## Fragment — `CardsFragment`
File: `ui/home/PayWithCardFragment.kt`. Bound to nav id `R.id.nav_pay_with_card`.
Opened via:
- Navigation slot ("Cards")
- `OPEN_PAY_WITH_CARD` intent
- `TAP_TO_PAY` intent — auto-enters tap mode for the user's default BML card
- Dashboard NFC button — auto-enters tap mode for the selected card
`newInstanceWithAutoTapMode(accountNumber? = null)` (`PayWithCardFragment.kt:1203-1208`) is the factory for the auto-tap entry points.
---
## Card Stack
A horizontal `RecyclerView` driven by `CardStackAdapter` with a `PagerSnapHelper`. Off-centre cards are scaled to 82% and faded to 60% via `applyCardScales()`. Page dots beneath the stack are rebuilt on each scroll-idle.
### Sources
`rebuildCards()` (`PayWithCardFragment.kt:894-958`) merges two lists:
- BML: `viewModel.accounts` filtered to `profileType in (BML_PREPAID, BML_CREDIT, BML_DEBIT)`
- MIB: `viewModel.mibCards`
The combined order is `bmlActive + mibActive + bmlInactive + mibInactive`. The user's default card (`CredentialStore.getDefaultCardAccountNumber()`) is moved to position 0. Initial data is seeded from `CardsCache` so the stack appears immediately; a background `HomeActivity.triggerRefreshCards()` refreshes it.
### Card Art
| Source | Asset path |
|---|---|
| MIB `cardType` → asset | `CardsFragment.cardImageAsset(card)``51``cards/mib/faisa_card.png`, `53``visa_black_platinum.png`, `57``visa_blue_everyday.png`, `70``visa_business.png`, `701/702``visa_bingaa_mvr.png` / `visa_bingaa_usd.png` |
| BML | `BmlCardParser.cardImageAsset(account)` |
Status chips use `bindCardStatus(...)`. MIB statuses: `CHST0` = active (no chip), `CHST20` = "Temporary blocked by client", other values shown verbatim.
---
## Pay Buttons
| Button | Behaviour |
|---|---|
| Scan to Pay (`btnScanToPay`) | Launches [QrScannerActivity](25-qr-scanner.md). BML cards route the result to `TransferFragment` (BML QR mode or PayMV pre-fill). MIB cards show "not supported" |
| Tap to Pay (`btnTapToPay`) | `NfcPaymentUtil.checkAndProceed(...)`. If `biometrics_transfer_confirm` is on, shows a `BIOMETRIC_STRONG` prompt first, then enters tap mode. MIB cards show "not supported" |
---
## Manage Mode
Toggle: `btnManageCard`. Animates the focused card up into a single full-width "manage card" panel, hides the carousel, and exposes:
| Control | Effect |
|---|---|
| Default card switch | `CredentialStore.setDefaultCardAccountNumber()` — BML only |
| Hide from dashboard switch | `CredentialStore.setCardHiddenFromDashboard(accountNumber, hidden)` |
| Freeze / Unfreeze | BML: `BmlCardClient.setCardFreezeState(session, internalId, "freeze"/"unfreeze")`. MIB: prompts for a comments string, then `MibCardsClient().setCardFreezeState(session, cardId, action, comments)` serialised through `app.mibMutex` with a profile switch first |
| Change PIN / Block | Wired but currently shows a "work in progress" toast |
Manage mode can be dismissed by tapping the button again or swipe-down on the manage card.
---
## Tap Mode
Entered via `setTapMode(true, item)`. Hides the carousel, animates the selected card to the top, draws an `NfcTapAnimationView` and a Cancel button.
`fetchAndArmToken()`:
1. Generates a TOTP from the BML OTP seed
2. `BmlTapToPayClient().fetchTokens(session, internalId, otp)` returns a single-use `BmlWalletToken`
3. `BmlHostCardEmulatorService.setToken(token)`
4. Sets `BmlHostCardEmulatorService.onTransactionComplete` — on success shows a "Payment complete" toast and refreshes balances
Exiting tap mode clears the token and the callback. See [Tap to Pay](23-tap-to-pay.md) for the HCE protocol details.
---
## Inactive Cards
Inactive cards remain in the stack at 45% alpha. Tap-to-pay and "default card" are disabled for them. Hide-from-dashboard is force-enabled and locked on for inactive cards (`PayWithCardFragment.kt:522-530`).
---
## Back Handling
`onBackPressed()` returns `true` while in manage mode or tap mode so `HomeActivity` does not pop the back stack — instead the fragment exits the current mode.
---
&nbsp;
---
[← Dashboard](21-dashboard.md) &nbsp;&nbsp;&nbsp; **Next →** [Tap to Pay](23-tap-to-pay.md)
+97
View File
@@ -0,0 +1,97 @@
# Tap to Pay
BML contactless payment via Android Host Card Emulation. The app emulates a BML magnetic-stripe contactless card using a single-use wallet token fetched from the [BML Tap-to-Pay API](../bmlapi/12-tap-to-pay.md).
---
## Entry Points
| Trigger | Path |
|---|---|
| Manifest NFC service launches `BmlTapToPayActivity` | Redirects to `MainActivity` with `TAP_TO_PAY` |
| `TAP_TO_PAY` intent | `R.id.nav_pay_with_card` + `auto_tap_mode=true` |
| "Tap to Pay" button on the [Cards](22-cards.md) screen | `CardsFragment.setTapMode(true, item)` |
| Dashboard NFC button on a BML card | Same |
`BmlTapToPayActivity` (`nfc/BmlTapToPayActivity.kt`) is a 7-line trampoline — it sets the `TAP_TO_PAY` action and `FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TASK` and finishes.
---
## Pre-flight — `NfcPaymentUtil`
Located at `util/NfcPaymentUtil.kt`. Every tap-to-pay entry runs `checkAndProceed(context, onReady)` first:
1. `NfcAdapter.getDefaultAdapter()` — null → "NFC unsupported" dialog
2. `nfcAdapter.isEnabled` — false → "Turn on NFC" dialog with a button that opens `Settings.ACTION_NFC_SETTINGS`
3. `CardEmulation.isDefaultServiceForCategory(component, CATEGORY_PAYMENT)` — false → "Set this app as the default payment app" dialog with a button that opens `Settings.ACTION_NFC_PAYMENT_SETTINGS`
4. All three pass → `onReady()` is invoked
If the user has `biometrics_transfer_confirm` enabled, `CardsFragment.showBiometricPromptForTap()` runs a `BIOMETRIC_STRONG` prompt before entering tap mode.
---
## Token Fetch
`fetchAndArmToken()` (in `CardsFragment`):
1. Locate the BML session and OTP seed for the selected card's login
2. Generate a fresh TOTP via `util/Totp`
3. `BmlTapToPayClient().fetchTokens(session, internalId, otp)` returns a list of `BmlWalletToken`; the first is used
4. `BmlHostCardEmulatorService.setToken(token)` arms the HCE service
5. `BmlHostCardEmulatorService.onTransactionComplete = { success -> ... }` — re-enters the carousel and shows a success toast / refresh on success
If any step fails, a toast is shown and tap mode exits.
---
## HCE Service — `BmlHostCardEmulatorService`
Subclass of `HostApduService`. Implements the minimal EMV mag-stripe contactless flow:
```
SELECT PPSE → SELECT AID → GET PROCESSING OPTIONS → READ RECORD
```
### APDU Handling
| INS | Handler | Response |
|---|---|---|
| `A4` SELECT (data = PPSE name `2PAY.SYS.DDF01`) | `handleSelect` | FCI containing app entry: tag 4F = AID, tag 87 = priority `01`, DF Name `84` = PPSE |
| `A4` SELECT (data = AID bytes from token) | `handleSelect` | FCI containing AID, ASCII label (`VISA` / `MASTERCARD` / `AMEX` / `BML`), PDOL `9F38 = 9F6602` (TTQ 2 bytes) |
| `A8` GPO | `handleGpo` | AIP `0080` (mag-stripe mode), AFL `08010100` (SFI=1, record 1-1) |
| `B2` READ RECORD | `handleReadRecord` | Tag `70` containing tag `57` Track 2 — fires `onTransactionComplete(true)` |
| Anything else | — | `6D00` (INS not supported) |
PPSE selection when no active token is set fires `launchPromptActivity()` which starts `BmlTapToPayActivity` to bring the user into the app.
### Track 2
`buildTrack2(token)`: `"${token.token}D${token.expiry}${token.serviceCode}${token.data}"`, padded with `F` to even length.
### Companion State
| Member | Use |
|---|---|
| `activeToken: BmlWalletToken?` | The armed token (volatile) |
| `onTransactionComplete: ((Boolean) -> Unit)?` | UI callback invoked from READ RECORD or `onDeactivated` |
| `setToken(token)` / `clearToken()` | Called from `CardsFragment.fetchAndArmToken()` / `exitTapMode()` |
| `applicationLabel(aidHex)` | Maps Visa/MC/Amex AIDs to ASCII labels |
`onDeactivated()` fires `onTransactionComplete(false)` if the reader dropped before GPO was sent.
---
## Failure Modes
- No NFC, NFC off, or app not the default payment service — handled by `NfcPaymentUtil` with dialogs that deep-link to Settings
- Missing OTP seed or session — toast, tap mode exits
- Token fetch fails — toast, tap mode exits
- Reader removed before READ RECORD — `onDeactivated` reports `success = false`; no toast is shown
---
&nbsp;
---
[← Cards](22-cards.md) &nbsp;&nbsp;&nbsp; **Next →** [Notifications](24-notifications.md)
+75
View File
@@ -0,0 +1,75 @@
# Notifications
Background polling that surfaces new bank activity as system notifications, plus an in-app bottom sheet for reading them. Opt-in only — the polling service is started from [Settings → Notifications](27-settings-notifications.md).
---
## Foreground Service — `NotificationPollingService`
File: `service/NotificationPollingService.kt`. Runs as a foreground service while the user has notifications enabled. Polls every **30 seconds** (`POLL_INTERVAL_MS = 30_000L`) on a `SupervisorJob` IO scope.
### Lifecycle
1. `onCreate`: creates the service channel (low importance, no badge), calls `startForeground(SERVICE_NOTIF_ID, ...)` with a persistent low-priority notification, then starts the polling loop
2. `onStartCommand`: returns `START_STICKY` so Android relaunches the service after kills
3. `onDestroy`: cancels the coroutine scope
### Poll Cycle
Each tick calls `pollBml()` and `pollMib()` sequentially. Both share the same shape:
| Step | Description |
|---|---|
| 1 | For every active session in `app.bmlSessions` / `app.mibSessions`, fetch the latest activity page |
| 2 | Compare with `NotificationsCache.loadBml(loginId)` / `loadMib(loginId, readIds)` |
| 3 | If the cache is empty (first run), persist the page silently — no system notifications are emitted on first sync |
| 4 | Otherwise, find IDs not in the cache, persist the merged list, and post a system notification per new item |
MIB activity is fetched via `MibActivityHistoryClient.fetchActivity(session, loginId, 1, 100)`. BML uses `BmlNotificationsClient.fetchNotifications(session, loginId, page = 1)`. Errors are caught per-loginId so one bad session does not block the rest.
### Channels
`ensureLoginChannel(bank, loginId)` lazily creates a `NotificationChannel` named `bank_{bank}_{loginId}` with `IMPORTANCE_DEFAULT`. The channel display name uses the profile's `name` from `bmlProfilesMap` / `mibProfilesMap` ("BML · Personal Name" etc.), falling back to the bare login id. This gives the user per-bank-per-profile channel control in system settings.
System notifications use `R.drawable.ic_bell` and a content intent that re-launches the app via `getLaunchIntentForPackage(packageName)`.
---
## In-App Sheet — `NotificationsSheetFragment`
Opened from the bell icon in the toolbar (`HomeActivity.openNotificationsSheet()` at line 687). A `BottomSheetDialogFragment` containing:
- A `TabLayout` + `ViewPager2` with one page per active bank login
- Each page is a `RecyclerView` of `AppNotification` grouped by date headers
- An "Mark all read" action
Notifications are loaded from `NotificationsCache` first for instant display, then refreshed from the network. Read state is tracked in `NotificationsCache` (`getMibReadIds()` etc.) so the bell toolbar icon (`HomeActivity.kt:640-684`) can switch between `ic_bell` and `ic_bell_read`.
`onUnreadCountChanged` callback is wired from `HomeActivity` so the bell icon updates whenever the sheet's contents change.
---
## Cache — `util/NotificationsCache`
Encrypted JSON files in `filesDir` (via `CacheEncryption`), one per bank+loginId. Stores the full notifications payload plus a separate "read IDs" set. Cleared along with the rest of the per-account history when the user invokes [Settings → Storage → Clear All Caches](17-settings-storage.md) (read state is excluded from that clear — verified in `17-settings-storage.md`).
---
## Opt-In
The polling service is not started automatically. The user must:
1. Tap Settings → Notifications
2. Toggle the switch on
3. Grant `POST_NOTIFICATIONS` (API 33+)
4. Optionally allow battery-optimisation exemption
See [Settings → Notifications](27-settings-notifications.md) for the full flow.
---
&nbsp;
---
[← Tap to Pay](23-tap-to-pay.md) &nbsp;&nbsp;&nbsp; **Next →** [QR Scanner](25-qr-scanner.md)
+76
View File
@@ -0,0 +1,76 @@
# QR Scanner
Full-screen camera-based QR scanner. Returns the decoded string to the caller via activity result.
---
## Activity — `QrScannerActivity`
File: `ui/home/QrScannerActivity.kt`. Hosts a CameraX preview + analysis pipeline backed by `de.markusfisch.android.zxingcpp.ZxingCpp`.
### Reader Options
```kotlin
ZxingCpp.ReaderOptions(
tryHarder = true,
tryRotate = true,
tryInvert = true,
tryDownscale = true,
maxNumberOfSymbols = 1,
textMode = ZxingCpp.TextMode.PLAIN,
)
```
### Camera Pipeline
`startCamera()` (`QrScannerActivity.kt:186-240`):
1. `ProcessCameraProvider.getInstance()`
2. Builds `Preview` and `ImageAnalysis` with `HIGHEST_AVAILABLE_STRATEGY` + `RATIO_16_9_FALLBACK_AUTO_STRATEGY`
3. Analyser receives `STRATEGY_KEEP_ONLY_LATEST` frames; reads the Y-plane via `ZxingCpp.readYBuffer(...)` with the image rotation applied
4. On first hit, `deliverResult(text)` sets `Activity.RESULT_OK` with `EXTRA_QR_CONTENT` and finishes
### Lock Guard
`onCreate` (`QrScannerActivity.kt:101-106`) checks `CredentialStore.loadSecurityHash()` and `app.isUnlocked`; if a lock exists and the app is locked, it forwards to `LockActivity` and finishes — direct intent launches cannot bypass the lock screen.
---
## UI Controls
| Control | Behaviour |
|---|---|
| Flashlight (`btnFlashlight`) | Toggles `camera.cameraControl.enableTorch()`; icon swaps between `ic_flashlight_to_on` / `ic_flashlight_to_off` with an `Animatable` transition |
| Zoom slider (`zoomSlider`) | Linear zoom via `setLinearZoom()`; updated live from the camera's `zoomState` observer |
| Pinch-to-zoom | `ScaleGestureDetector` on the preview view, mapped through `cameraControl.setZoomRatio()` clamped to the device's `[minZoomRatio, maxZoomRatio]` |
| Pick image (`btnPickImage`) | `GetContent("image/*")`; decodes the picked bitmap with the same reader options |
### Image Decoder Fallback
`Bitmap.decodeQr()` (`QrScannerActivity.kt:243-249`) mirrors BinaryEye: tries `Binarizer.LOCAL_AVERAGE` first, then falls back to `GLOBAL_HISTOGRAM` for tricky lighting.
---
## Callers
Each caller registers an `ActivityResultContracts.StartActivityForResult` launcher and reads `EXTRA_QR_CONTENT` from the result intent.
| Caller | Result handling |
|---|---|
| [TransferFragment](07-transfer.md) | Routes PayMV / BML URL via `PaymvQrParser` + `extractBmlGatewayUrl` |
| [PayMvQrFragment](11-paymv-qr-screen.md) | Generation only — does not call the scanner directly |
| `CredentialsFragment` (login) | `OtpauthParser.parse(raw)` → fills `etOtpSeed` or shows a chooser for multi-entry QRs |
| [DashboardFragment](21-dashboard.md) | BML URL → BML QR pay; PayMV → pre-fill Transfer; otherwise toast |
| [CardsFragment](22-cards.md) | Same routing as Dashboard, scoped to the active BML card |
### Share-to-Scan Fast Path
When another app shares an image directly to the activity-alias `.ScanToPayActivity`, `MainActivity` decodes the QR from the image **before** routing (it holds the URI permission only at that point). The decoded string is then forwarded as the `share_qr_text` extra. This bypasses the camera entirely.
---
&nbsp;
---
[← Notifications](24-notifications.md) &nbsp;&nbsp;&nbsp; **Next →** [Circular Nav](26-circular-nav.md)
+76
View File
@@ -0,0 +1,76 @@
# Circular Nav
Radial wheel-style navigation. Activated by selecting **Circular** in [Settings → Appearance → Navigation Mode](16-settings-appearance.md). Replaces the drawer and bottom bar entirely.
---
## Fragment — `CircularNavFragment`
File: `ui/home/CircularNavFragment.kt`. Becomes the root content fragment in `HomeActivity` when `NavCustomization.getNavMode(prefs) == NAV_MODE_CIRCULAR` (`HomeActivity.kt:279-280`). Inflates a `LinearLayout` with a `CircularWheelView` filling the available space and the app launcher icon at the bottom.
The toolbar collapses to a single centred title view (`"wheel_title"` tag) and the standard lock/eye/bell items are hidden via `onPrepareOptionsMenu` (`HomeActivity.kt:646-650`).
---
## Wheel Layout
Six segments are populated in this order so the four user-configurable slots sit at the top of the wheel and the system items sit at the bottom:
| Clock position | Item |
|---|---|
| 10, 12, 2 | User wheel slots 02 (`circular_slot_1_key``circular_slot_3_key`) |
| 4 | User wheel slot 3 (`circular_slot_4_key`) |
| 6 | **Dashboard** (fixed) |
| 8 | **More** (fixed) — opens `NavMoreSheetFragment` with the remaining items |
Defaults (`NavCustomization.kt:79-84`): Transfer, Cards, Contacts, Accounts.
The wheel angle is persisted to `circular_wheel_angle` in `SharedPreferences` so the user's preferred rotation survives navigation away from the wheel.
---
## Centre Lock
`CircularWheelView` has an `isWheelLocked` boolean that draws either `ic_lock` (locked) or `ic_lock_open` (unlocked) at the centre. The wheel starts unlocked.
| Action | Result |
|---|---|
| Tap a segment while unlocked | Spins the segment down to 6 o'clock, then fires `onItemClick(navId)``HomeActivity.navigateTo()` |
| Tap the centre while unlocked | Locks the wheel |
| Tap the centre while locked | Fires `onWheelCenterLockedTap``HomeActivity.notifyWheelLockTap()` (intended for user feedback / unlock guidance) |
| Tap a segment while locked | Animates the segment but shakes the lock icon and vibrates instead of navigating (`vibrateDevice()` + `shakeLock()`) |
`HomeActivity` exposes `unlockWheelLock()` on the fragment so external code can clear the locked state.
---
## Gestures
`CircularWheelView` handles its own touch events:
| Gesture | Behaviour |
|---|---|
| Drag | Increments `wheelAngle` by the angular delta. Touch slop must be exceeded before drag mode engages |
| Fling | A 6-sample velocity buffer estimates angular velocity; `fling(vel)` decelerates at `0.0008 deg/ms²` then snaps to the nearest segment |
| Release without dragging | Treated as a tap — the segment under the touch is centered and activated |
After every drag/fling settle, `snapToNearest()` aligns the wheel angle to a segment boundary using `DecelerateInterpolator`.
---
## Visuals
- Disc fill: `colorSurface`
- Outer accent ring + inner centre ring: `colorPrimary`
- Icons: tinted with `colorPrimary` (greyscale + 40% alpha while locked)
- Labels: curved text on a per-segment arc (`drawTextOnPath`); colour follows `colorOnSurface` (semi-transparent when locked)
Icon and centre bitmaps are regenerated on every size change via `reloadBitmaps()`.
---
&nbsp;
---
[← QR Scanner](25-qr-scanner.md) &nbsp;&nbsp;&nbsp; **Next →** [Settings — Notifications](27-settings-notifications.md)
@@ -0,0 +1,53 @@
# Settings — Notifications
Opt-in screen that starts / stops the `NotificationPollingService`. See [Notifications](24-notifications.md) for the polling service itself.
---
## Fragment — `SettingsNotificationsFragment`
A programmatically-built `ScrollView` containing one toggle row and one nav row.
### Enable Toggle
A `SwitchCompat` bound to the `notifications_enabled` preference (default `false`). Flipping it on triggers a multi-step setup; flipping it off stops the service and clears the pref.
---
## Enable Flow
`requestEnableNotifications()` runs a permission chain. Each step proceeds to the next on grant; cancellation reverts the switch to off.
| Step | Code path | When triggered |
|---|---|---|
| 1. POST_NOTIFICATIONS permission | `permissionLauncher` | API 33+ only — skipped on older devices |
| 2. Battery optimisation exemption | `batteryOptLauncher` | If `PowerManager.isIgnoringBatteryOptimizations(packageName) == false`, the user is sent to `Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS` |
| 3. Start service | `enableService()` | Sets `notifications_enabled = true`, calls `startForegroundService(NotificationPollingService)`, leaves the switch on |
The battery-opt step proceeds to `enableService()` regardless of the user's choice — the service will still run but Android may throttle it without the exemption.
---
## Disable Flow
`disableService()`:
1. Sets `notifications_enabled = false`
2. `stopService(NotificationPollingService)` — the foreground notification disappears
3. Switch state is forced back to off
Per-channel notification toggles remain in their last state on the system side — re-enabling re-creates the channels if needed.
---
## Open System Channels
A second row labelled "Open system notification settings" launches `Settings.ACTION_APP_NOTIFICATION_SETTINGS` with the app's package extra. This is the only way to silence individual bank+profile channels created by the polling service (`bank_bml_{loginId}`, `bank_mib_{loginId}`).
---
&nbsp;
---
[← Circular Nav](26-circular-nav.md) &nbsp;&nbsp;&nbsp; **Next →** [Settings — About](28-settings-about.md)
+56
View File
@@ -0,0 +1,56 @@
# Settings — About
App version, terms-of-service links per bank, and optional donate buttons.
---
## Fragment — `SettingsAboutFragment`
File: `ui/home/SettingsAboutFragment.kt`. Bound to layout `FragmentSettingsAboutBinding`.
### Header
| View | Source |
|---|---|
| `tvAppName` | `R.string.app_name` |
| `tvVersion` | `R.string.about_version` formatted with `BuildConfig.VERSION_NAME` |
---
## Terms & Conditions
Three rows that open external T&C URLs via `Intent.ACTION_VIEW`:
| Row | URL |
|---|---|
| MIB | `https://faisanet.mib.com.mv/terms` |
| BML | `https://www.bankofmaldives.com.mv/storage/file/121/10289/terms-conditions-online-banking-en.pdf` |
| Fahipay | `https://fahipay.mv/tos/` |
---
## Donate
Optional MVR and USD donate buttons. Visibility is driven by `BuildConfig.ACCOUNT_MVR` and `BuildConfig.ACCOUNT_USD` — if neither is set, the entire **Donate** section is hidden (`SettingsAboutFragment.kt:43-53`).
Tapping a donate button calls:
```kotlin
TransferFragment.newInstance(
accountNumber = BuildConfig.ACCOUNT_MVR or ACCOUNT_USD,
displayName = getString(R.string.app_name),
subtitle = accountNumber,
colorHex = "#607D8B",
imageHash = null,
)
```
and shows it with `HomeActivity.showWithBackStack(fragment)`. The user then enters the amount and proceeds as a normal [Transfer](07-transfer.md).
---
&nbsp;
---
[← Settings — Notifications](27-settings-notifications.md)
+13 -5
View File
@@ -19,13 +19,21 @@ Documentation for app-specific logic — UI flows, routing decisions, and busine
| [08 — Contacts](08-contacts.md) | Contact list, add/edit/delete, categories, contact picker sheet |
| [09 — Activities](09-activities.md) | Local transfer log, TransferReceiptFragment, share/save receipt |
| [10 — OTP Screen](10-otp-screen.md) | TOTP display, real-time countdown, enrolled bank authenticators |
| [11 — PayMV QR Screen](11-paymv-qr-screen.md) | Generate receive-payment QR, scan QR to initiate transfer |
| [12 — BML QR Pay](12-bml-qr-pay.md) | Scan BML merchant QR, merchant lookup, 3-step TOTP payment |
| [13 — Financing](13-financing.md) | MIB promotional deals, BML loans and card limits |
| [14 — Settings](14-settings.md) | Settings hub, logins management, profile images, add/logout accounts |
| [11 — PayMV QR Screen](11-paymv-qr-screen.md) | Generate receive-payment QR (send/scan lives in Transfer) |
| [12 — BML QR Pay](12-bml-qr-pay.md) | (Stub — see Transfer Flows for the live BML QR merchant flow) |
| [13 — Financing](13-financing.md) | MIB promotional deals, BML loans, BML foreign spend limits |
| [14 — Settings](14-settings.md) | Settings hub: Logins, Appearance, Privacy & Security, Notifications, Storage, About |
| [15 — Settings: Security](15-settings-security.md) | Change lock method, biometrics, auto-lock timeout, screenshots |
| [16 — Settings: Appearance](16-settings-appearance.md) | Navigation mode, slot drag-reorder, theme, accent colour, language |
| [17 — Settings: Storage](17-settings-storage.md) | Clear caches, profile images, transfer history |
| [17 — Settings: Storage](17-settings-storage.md) | Single "Clear All Caches" button |
| [21 — Dashboard](21-dashboard.md) | Aggregate balances, card stack, attention row, foreign limits, integrated QR launcher |
| [22 — Cards](22-cards.md) | Unified MIB + BML card stack, tap-to-pay launcher, freeze/unfreeze, manage mode |
| [23 — Tap to Pay](23-tap-to-pay.md) | BML HCE service, single-use wallet token, EMV mag-stripe APDU flow |
| [24 — Notifications](24-notifications.md) | Foreground polling service, per-bank channels, in-app sheet |
| [25 — QR Scanner](25-qr-scanner.md) | Full-screen CameraX scanner, gallery picker, zoom, torch |
| [26 — Circular Nav](26-circular-nav.md) | Radial 4-slot wheel UI with lock centre |
| [27 — Settings: Notifications](27-settings-notifications.md) | Opt-in flow: permission → battery opt → service start |
| [28 — Settings: About](28-settings-about.md) | Version, T&Cs, donate buttons |
## Reference