This commit is contained in:
@@ -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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
+25
-6
@@ -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
@@ -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.
|
||||
|
||||
|
||||
@@ -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) |
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
+123
-14
@@ -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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
@@ -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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
@@ -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 |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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`.
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
[← Contacts](09-contacts.md)
|
||||
+20
-1
@@ -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 |
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user