diff --git a/docs/bmlapi/01-login.md b/docs/bmlapi/01-login.md index 0c1fd26..d24d8d5 100644 --- a/docs/bmlapi/01-login.md +++ b/docs/bmlapi/01-login.md @@ -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 diff --git a/docs/bmlapi/03-oauth-token.md b/docs/bmlapi/03-oauth-token.md index b19f26e..97e4ee4 100644 --- a/docs/bmlapi/03-oauth-token.md +++ b/docs/bmlapi/03-oauth-token.md @@ -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' \ diff --git a/docs/bmlapi/04-dashboard.md b/docs/bmlapi/04-dashboard.md index 7074524..6772e36 100644 --- a/docs/bmlapi/04-dashboard.md +++ b/docs/bmlapi/04-dashboard.md @@ -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` | diff --git a/docs/bmlapi/06-account-history.md b/docs/bmlapi/06-account-history.md index 7fdc076..96e47c0 100644 --- a/docs/bmlapi/06-account-history.md +++ b/docs/bmlapi/06-account-history.md @@ -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 ` | +| `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 ' \ + --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. + +--- +   --- diff --git a/docs/bmlapi/08-transfer.md b/docs/bmlapi/08-transfer.md index 5b9a6d3..ab09918 100644 --- a/docs/bmlapi/08-transfer.md +++ b/docs/bmlapi/08-transfer.md @@ -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. --- diff --git a/docs/bmlapi/10-validate.md b/docs/bmlapi/10-validate.md index d31db30..35363fd 100644 --- a/docs/bmlapi/10-validate.md +++ b/docs/bmlapi/10-validate.md @@ -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 diff --git a/docs/bmlapi/12-tap-to-pay.md b/docs/bmlapi/12-tap-to-pay.md index a31a84a..3ad0402 100644 --- a/docs/bmlapi/12-tap-to-pay.md +++ b/docs/bmlapi/12-tap-to-pay.md @@ -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) diff --git a/docs/bmlapi/13-qr-payment.md b/docs/bmlapi/13-qr-payment.md index 99ffc4c..667988e 100644 --- a/docs/bmlapi/13-qr-payment.md +++ b/docs/bmlapi/13-qr-payment.md @@ -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)     **Next →** [Notifications](14-notifications.md) diff --git a/docs/bmlapi/14-notifications.md b/docs/bmlapi/14-notifications.md new file mode 100644 index 0000000..00b52c9 --- /dev/null +++ b/docs/bmlapi/14-notifications.md @@ -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 ` | +| `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 ' \ + --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 ` | +| `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 ' \ + --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. + +--- + +  + +--- + +[← QR Payment](13-qr-payment.md)     **Next →** [Card Freeze](15-card-freeze.md) diff --git a/docs/bmlapi/15-card-freeze.md b/docs/bmlapi/15-card-freeze.md new file mode 100644 index 0000000..9830c8b --- /dev/null +++ b/docs/bmlapi/15-card-freeze.md @@ -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": "", + "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 ` | +| `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 ' \ + --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 | + +--- + +  + +--- + +[← Notifications](14-notifications.md) diff --git a/docs/bmlapi/README.md b/docs/bmlapi/README.md index 7106e9e..decdb69 100644 --- a/docs/bmlapi/README.md +++ b/docs/bmlapi/README.md @@ -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 | --- diff --git a/docs/fahipayapi/06-profile-picture.md b/docs/fahipayapi/06-profile-picture.md index a0c9c9e..1735018 100644 --- a/docs/fahipayapi/06-profile-picture.md +++ b/docs/fahipayapi/06-profile-picture.md @@ -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: - → GET - → 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`. --- diff --git a/docs/fahipayapi/07-contacts.md b/docs/fahipayapi/07-contacts.md index 65308c7..b70e60f 100644 --- a/docs/fahipayapi/07-contacts.md +++ b/docs/fahipayapi/07-contacts.md @@ -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 | --- diff --git a/docs/fahipayapi/README.md b/docs/fahipayapi/README.md index 3cc12b7..42aecf6 100644 --- a/docs/fahipayapi/README.md +++ b/docs/fahipayapi/README.md @@ -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 | --- diff --git a/docs/mibapi/01-encryption.md b/docs/mibapi/01-encryption.md index f4200d0..8943786 100644 --- a/docs/mibapi/01-encryption.md +++ b/docs/mibapi/01-encryption.md @@ -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 (~23–24 bit range). +- `nonce` and `sodium` are separate fields. `sodium` is an independent random integer in `[1_000_000, 16_000_000)` (~24-bit range, 7–8 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. --- diff --git a/docs/mibapi/02-login.md b/docs/mibapi/02-login.md index 3e02ffe..9c1bd77 100644 --- a/docs/mibapi/02-login.md +++ b/docs/mibapi/02-login.md @@ -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": "", "appId": "IOS17.2-<15 random alphanumeric chars>", "routePath": "S40", - "sodium": "", + "sodium": "", "xxid": "" } } @@ -218,7 +218,7 @@ Form body: `key2=&sfunc=i&data=` "cmod": "", "appId": "", "routePath": "S40", - "sodium": "", + "sodium": "", "xxid": "" } } @@ -272,10 +272,29 @@ After: derive new session key, replace `xxid` and `nonceGenerator`. "otpTypes": [2, 3], "email": "", "uuid": "", - "uuid2": "" + "uuid2": "", + "operatingProfiles": [ + { + "customerProfileId": "...", + "annexId": "...", + "customerId": "...", + "name": "...", + "cifType": "...", + "profileType": "...", + "color": "...", + "customerImage": "" + } + ], + "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` diff --git a/docs/mibapi/03-accounts.md b/docs/mibapi/03-accounts.md index c57f9c7..8cc8dd0 100644 --- a/docs/mibapi/03-accounts.md +++ b/docs/mibapi/03-accounts.md @@ -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. diff --git a/docs/mibapi/05-cards.md b/docs/mibapi/05-cards.md index 184c554..1e13623 100644 --- a/docs/mibapi/05-cards.md +++ b/docs/mibapi/05-cards.md @@ -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_"`) 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=&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) | + +--- +   --- diff --git a/docs/mibapi/07-profile.md b/docs/mibapi/07-profile.md index de63a11..8ee633b 100644 --- a/docs/mibapi/07-profile.md +++ b/docs/mibapi/07-profile.md @@ -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=; IBSID=; mbnonce=; 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": "", + "data": { + "imageHash": "", + "nonce": "", + "appId": "", + "sodium": "", + "routePath": "P41", + "xxid": "" + } +} +``` + +**Response**: +```json +{ + "success": true, + "reasonCode": "201", + "profileImage": "" +} +``` + +`profileImage` is raw base64 with no data URI prefix. + +### Upload — `routePath: P40` + +**Request** (encrypted payload): +```json +{ + "sfunc": "n", + "xxid": "", + "data": { + "profileId": "", + "profileImage": "", + "nonce": "", + "appId": "", + "sodium": "", + "routePath": "P40", + "xxid": "" + } +} +``` + +**Response**: +```json +{ + "success": true, + "imageHash": "", + "customerImage": "" +} +``` + +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": "", + "data": { + "profileId": "", + "nonce": "", + "appId": "", + "sodium": "", + "routePath": "P42", + "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. + +--- +   --- diff --git a/docs/mibapi/08-transfer.md b/docs/mibapi/08-transfer.md index 39cda82..7c61d61 100644 --- a/docs/mibapi/08-transfer.md +++ b/docs/mibapi/08-transfer.md @@ -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. + +--- +   --- diff --git a/docs/mibapi/09-contacts.md b/docs/mibapi/09-contacts.md index 854ac0d..5a879fa 100644 --- a/docs/mibapi/09-contacts.md +++ b/docs/mibapi/09-contacts.md @@ -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 | --- diff --git a/docs/mibapi/10-activity-history.md b/docs/mibapi/10-activity-history.md new file mode 100644 index 0000000..572323c --- /dev/null +++ b/docs/mibapi/10-activity-history.md @@ -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=; IBSID=; mbnonce=; time-tracker=597 +X-Requested-With: XMLHttpRequest +User-Agent: Mozilla/5.0 (Linux; Android ; 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`. + +--- + +  + +--- + +[← Contacts](09-contacts.md) diff --git a/docs/mibapi/README.md b/docs/mibapi/README.md index 0548adb..6fde43d 100644 --- a/docs/mibapi/README.md +++ b/docs/mibapi/README.md @@ -48,6 +48,11 @@ Cookie: mbmodel=IOS-1.0; xxid=; IBSID=; mbnonce=