From 6b9ac5532b371e1c15afb7479704c224d2bfb635 Mon Sep 17 00:00:00 2001 From: Shihaam Abdul Rahman Date: Fri, 23 Oct 2026 05:27:09 +0500 Subject: [PATCH] MIB now fetch users info (idcard, phone num, email) to display in settings --- .../sh/sar/basedbank/api/mib/MibLoginFlow.kt | 8 ++ .../sar/basedbank/api/mib/MibProfileClient.kt | 91 +++++++++++++------ .../sh/sar/basedbank/ui/home/HomeActivity.kt | 3 + .../sh/sar/basedbank/ui/home/OtpFragment.kt | 7 +- .../ui/home/SettingsLoginsFragment.kt | 7 +- .../basedbank/ui/login/CredentialsFragment.kt | 12 +-- .../sh/sar/basedbank/util/CredentialStore.kt | 7 +- docs/mibapi/upgradev2.md | 12 ++- 8 files changed, 99 insertions(+), 48 deletions(-) diff --git a/app/src/main/java/sh/sar/basedbank/api/mib/MibLoginFlow.kt b/app/src/main/java/sh/sar/basedbank/api/mib/MibLoginFlow.kt index bc87bac..3b57677 100644 --- a/app/src/main/java/sh/sar/basedbank/api/mib/MibLoginFlow.kt +++ b/app/src/main/java/sh/sar/basedbank/api/mib/MibLoginFlow.kt @@ -30,6 +30,12 @@ class MibLoginFlow(private val credentialStore: CredentialStore) { var lastProfiles: List = emptyList() private set + /** Full name and email from the last A41 login response. */ + var lastFullName: String = "" + private set + var lastEmail: String = "" + private set + /** Called after automatic session recovery so callers can update their session reference. */ var onSessionRefreshed: ((MibSession, List) -> Unit)? = null @@ -149,6 +155,8 @@ class MibLoginFlow(private val credentialStore: CredentialStore) { } val profiles = parseProfiles(loginResp) + lastFullName = loginResp.optString("fullName").takeIf { it != "null" } ?: "" + lastEmail = loginResp.optString("email").takeIf { it != "null" } ?: "" lastSession = session2 lastProfiles = profiles // keep ALL profiles so settings can show them all diff --git a/app/src/main/java/sh/sar/basedbank/api/mib/MibProfileClient.kt b/app/src/main/java/sh/sar/basedbank/api/mib/MibProfileClient.kt index d3c2953..5c351ef 100644 --- a/app/src/main/java/sh/sar/basedbank/api/mib/MibProfileClient.kt +++ b/app/src/main/java/sh/sar/basedbank/api/mib/MibProfileClient.kt @@ -1,50 +1,87 @@ package sh.sar.basedbank.api.mib +import android.os.Build +import okhttp3.FormBody import okhttp3.OkHttpClient import okhttp3.Request +import org.json.JSONObject +import sh.sar.basedbank.util.CredentialStore import java.util.concurrent.TimeUnit data class MibPersonalProfile( val fullName: String, - val username: String, val email: String, val mobile: String, - val enrolled: String + val idCard: String ) -class MibProfileClient { +class MibProfileClient(private val flow: MibLoginFlow) { + + private val BASE_WV_URL = "https://faisamobilex-wv-v2.mib.com.mv" private val client = OkHttpClient.Builder() .connectTimeout(30, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS) .build() + /** + * Name comes from the A41 login response; email, mobile and National ID from the + * Favara alias suggestions (`/ajaxAlias/getAliasTypes`), which belong to the active + * profile — so the personal profile is selected first. + */ fun fetchPersonalProfile(session: MibSession): MibPersonalProfile? { - val cookieHeader = "mbmodel=AND-1.0; xxid=${session.xxid}; " + - "IBSID=${session.xxid}; mbnonce=${session.nonceGenerator}; time-tracker=597" + val personal = flow.lastProfiles.firstOrNull { it.profileType == "0" } + if (personal != null) flow.switchProfile(session, personal) + val request = Request.Builder() - .url("https://faisamobilex-wv-v2.mib.com.mv/personalProfile") - .get() - .header("Cookie", cookieHeader) - .build() - return try { - val resp = client.newCall(request).execute() - val html = resp.body?.string() ?: return null - resp.close() - fun scrape(label: String): String { - val r = Regex("""]*>\s*]*>\s*$label\s*]*>.*?]*>([^<]+)""", - setOf(RegexOption.DOT_MATCHES_ALL, RegexOption.IGNORE_CASE)) - return r.find(html)?.groupValues?.get(1)?.trim() ?: "" - } - val nameRegex = Regex("""
\s*([^<]+)\s*
""") - val fullName = nameRegex.find(html)?.groupValues?.get(1)?.trim() ?: return null - MibPersonalProfile( - fullName = fullName, - username = scrape("Username:"), - email = scrape("Email:"), - mobile = scrape("Mobile no:"), - enrolled = scrape("Enrolled:") + .url("$BASE_WV_URL/ajaxAlias/getAliasTypes") + .post(FormBody.Builder().build()) + .header("Cookie", "mbmodel=AND-1.0; xxid=${session.xxid}; IBSID=${session.xxid}; mbnonce=${session.nonceGenerator}") + .header( + "User-Agent", + "Mozilla/5.0 (Linux; Android ${Build.VERSION.RELEASE}; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/129.0.6668.70 Mobile Safari/537.36" ) - } catch (_: Exception) { null } + .header("X-Requested-With", "XMLHttpRequest") + .header("Accept", "*/*") + .header("Origin", BASE_WV_URL) + .header("Referer", "$BASE_WV_URL/Alias/Alias?dashurl=1") + .build() + + val json = client.newCall(request).execute().use { response -> + val body = response.body?.string() ?: return null + try { JSONObject(body) } catch (_: Exception) { return null } + } + if (!json.optBoolean("success")) return null + + // Alias type ids: 0 = mobile number, 1 = email, 3 = National ID + val aliases = json.optJSONArray("data") + fun alias(id: Int): String { + if (aliases == null) return "" + for (i in 0 until aliases.length()) { + val o = aliases.getJSONObject(i) + if (o.optInt("id", -1) == id) return o.optString("suggestedAlias").takeIf { it != "null" } ?: "" + } + return "" + } + return MibPersonalProfile( + fullName = flow.lastFullName.ifBlank { personal?.name ?: "" }, + email = alias(1).ifBlank { flow.lastEmail }, + mobile = alias(0), + idCard = alias(3) + ) + } + + /** Fetches and caches the user's details for [loginId] unless a v2 fetch is already cached. */ + fun cacheIfMissing(store: CredentialStore, loginId: String, session: MibSession) { + if (store.loadMibUserProfile(loginId)?.idCard != null) return + val p = fetchPersonalProfile(session) ?: return + store.saveMibUserProfile(loginId, CredentialStore.MibUserProfile( + fullName = p.fullName, + username = loginId, + email = p.email, + mobile = p.mobile, + enrolled = "", + idCard = p.idCard + )) } } diff --git a/app/src/main/java/sh/sar/basedbank/ui/home/HomeActivity.kt b/app/src/main/java/sh/sar/basedbank/ui/home/HomeActivity.kt index f65963b..d4efdb1 100644 --- a/app/src/main/java/sh/sar/basedbank/ui/home/HomeActivity.kt +++ b/app/src/main/java/sh/sar/basedbank/ui/home/HomeActivity.kt @@ -63,6 +63,7 @@ import sh.sar.basedbank.api.models.BankContactCategory import sh.sar.basedbank.api.mib.MibContactsClient import sh.sar.basedbank.api.mib.MibCardsClient import sh.sar.basedbank.api.mib.MibFinancingClient +import sh.sar.basedbank.api.mib.MibProfileClient import sh.sar.basedbank.api.mib.MibProfile import sh.sar.basedbank.api.mib.MibSession import sh.sar.basedbank.api.mib.MibFinanceDeal @@ -821,6 +822,8 @@ fun applyNavLabelVisibility() { app.mibProfilesMap[loginId] = flow.lastProfiles app.mibLoginFlows[loginId] = flow store.saveMibProfiles(loginId, flow.lastProfiles) + // Logins saved before v2 have no cached name/email/mobile/ID card yet + try { MibProfileClient(flow).cacheIfMissing(store, loginId, flow.lastSession!!) } catch (_: Exception) { } accounts } catch (e: java.io.IOException) { refreshErrors.add("NO_INTERNET") diff --git a/app/src/main/java/sh/sar/basedbank/ui/home/OtpFragment.kt b/app/src/main/java/sh/sar/basedbank/ui/home/OtpFragment.kt index 95564ca..c11aeb2 100644 --- a/app/src/main/java/sh/sar/basedbank/ui/home/OtpFragment.kt +++ b/app/src/main/java/sh/sar/basedbank/ui/home/OtpFragment.kt @@ -353,15 +353,16 @@ class OtpFragment : Fragment() { val session = app.mibSessions[loginId] ?: continue val flow = app.mibFlowFor(loginId) val profile = withContext(Dispatchers.IO) { - try { MibProfileClient().fetchPersonalProfile(session) } catch (_: Exception) { null } + try { MibProfileClient(flow).fetchPersonalProfile(session) } catch (_: Exception) { null } } if (profile != null) { store.saveMibUserProfile(loginId, CredentialStore.MibUserProfile( fullName = profile.fullName, - username = profile.username, + username = loginId, email = profile.email, mobile = profile.mobile, - enrolled = profile.enrolled + enrolled = "", + idCard = profile.idCard )) val idx = entries.indexOfFirst { it.bank == "MIB" && it.loginId == loginId } if (idx >= 0) { entries[idx] = entries[idx].copy(name = profile.fullName); changed = true } diff --git a/app/src/main/java/sh/sar/basedbank/ui/home/SettingsLoginsFragment.kt b/app/src/main/java/sh/sar/basedbank/ui/home/SettingsLoginsFragment.kt index d7c8566..d119230 100644 --- a/app/src/main/java/sh/sar/basedbank/ui/home/SettingsLoginsFragment.kt +++ b/app/src/main/java/sh/sar/basedbank/ui/home/SettingsLoginsFragment.kt @@ -668,10 +668,13 @@ class SettingsLoginsFragment : Fragment() { scroll.addView(container) // Account info lines + val hide = viewModel.hideAmounts.value ?: false + val masked = "••••••" listOfNotNull( profile?.fullName?.takeIf { it.isNotBlank() }?.let { "${getString(R.string.login_detail_name)}: $it" }, - profile?.email?.takeIf { it.isNotBlank() }?.let { "${getString(R.string.login_detail_email)}: $it" }, - profile?.mobile?.takeIf { it.isNotBlank() }?.let { "${getString(R.string.login_detail_mobile)}: $it" } + profile?.email?.takeIf { it.isNotBlank() }?.let { "${getString(R.string.login_detail_email)}: ${if (hide) masked else it}" }, + profile?.mobile?.takeIf { it.isNotBlank() }?.let { "${getString(R.string.login_detail_mobile)}: ${if (hide) masked else it}" }, + profile?.idCard?.takeIf { it.isNotBlank() }?.let { "${getString(R.string.login_detail_id_card)}: ${if (hide) masked else it}" } ).forEach { line -> container.addView(TextView(ctx).apply { text = line diff --git a/app/src/main/java/sh/sar/basedbank/ui/login/CredentialsFragment.kt b/app/src/main/java/sh/sar/basedbank/ui/login/CredentialsFragment.kt index 274f226..95b685c 100644 --- a/app/src/main/java/sh/sar/basedbank/ui/login/CredentialsFragment.kt +++ b/app/src/main/java/sh/sar/basedbank/ui/login/CredentialsFragment.kt @@ -255,17 +255,7 @@ class CredentialsFragment : Fragment() { store.saveMibCredentials(loginId, username, passwordHash, otpSeed) withContext(Dispatchers.IO) { flow.lastSession?.let { s -> - val profile = MibProfileClient().fetchPersonalProfile(s) - if (profile != null) store.saveMibUserProfile( - loginId, - CredentialStore.MibUserProfile( - fullName = profile.fullName, - username = profile.username, - email = profile.email, - mobile = profile.mobile, - enrolled = profile.enrolled - ) - ) + try { MibProfileClient(flow).cacheIfMissing(store, loginId, s) } catch (_: Exception) { } } } store.saveMibProfiles(loginId, flow.lastProfiles) diff --git a/app/src/main/java/sh/sar/basedbank/util/CredentialStore.kt b/app/src/main/java/sh/sar/basedbank/util/CredentialStore.kt index 5384691..514e12b 100644 --- a/app/src/main/java/sh/sar/basedbank/util/CredentialStore.kt +++ b/app/src/main/java/sh/sar/basedbank/util/CredentialStore.kt @@ -669,7 +669,8 @@ class CredentialStore(context: Context) { val username: String, val email: String, val mobile: String, - val enrolled: String + val enrolled: String, + val idCard: String? = null // null = not yet fetched from the v2 API ) data class BmlUserProfile( @@ -736,6 +737,7 @@ class CredentialStore(context: Context) { put("email", p.email) put("mobile", p.mobile) put("enrolled", p.enrolled) + if (p.idCard != null) put("idCard", p.idCard) }.toString() val key = getOrCreateKey() prefs.edit().putString("mib_${loginId}_enc_profile", encrypt(json, key)).apply() @@ -752,7 +754,8 @@ class CredentialStore(context: Context) { username = o.optString("username"), email = o.optString("email"), mobile = o.optString("mobile"), - enrolled = o.optString("enrolled") + enrolled = o.optString("enrolled"), + idCard = if (o.has("idCard")) o.optString("idCard") else null ) } catch (_: Exception) { null } } diff --git a/docs/mibapi/upgradev2.md b/docs/mibapi/upgradev2.md index d3c5b46..d021c39 100644 --- a/docs/mibapi/upgradev2.md +++ b/docs/mibapi/upgradev2.md @@ -112,8 +112,13 @@ v2 moved transfers and all destination lookups to the encrypted API (`sfunc=n`). ### 7. User Profile -| Endpoint | Purpose | Used By | Status | -|----------|---------|---------|--------| +| v1 endpoint | v2 source | Notes | Used By | Status | +|-------------|-----------|-------|---------|--------| +| `GET /personalProfile` (HTML scrape) | `A41` login response | `fullName`, `email` | `MibLoginFlow.kt` | 🔧 implemented — needs device test | +| — | `POST /ajaxAlias/getAliasTypes` (`-wv-v2`, empty body) | `data[].{id,aliasTypeName,suggestedAlias}`: `0` mobile, `1` email, `3` National ID. Tied to the active profile, so the personal profile (`profileType 0`) is selected first. Fetched at login and cached. | `MibProfileClient.kt`, `SettingsLoginsFragment.kt` | 🔧 implemented — needs device test | +| — | `POST /ajaxAlias/main` | Registered Favara aliases (`AlsBfy`, `CdtrAcct`, `AdrLine`, …) | — (not used) | ℹ️ | + +----------|---------|---------|--------| | `GET /personalProfile` | HTML scrape: fullName, username, email, mobile, enrolled date | `MibProfileClient.kt` | ☐ | --- @@ -215,7 +220,8 @@ pgf03 = SHA256(clientSalt + SHA256(SHA256(password) + userSalt)) - [x] Capture transaction history — moved to encrypted `A84`, implemented - [x] Test transaction history on device (incl. page 2) - [ ] Test activity history (`/aProfile/getPagedActivityHistory`) -- [ ] Test user profile scraping (`/personalProfile`) +- [x] Capture user profile — `/personalProfile` scrape replaced by A41 + `getAliasTypes` +- [ ] Test login details (name, email, mobile, ID card) on device - [x] Test financing (`/financing?dashurl=1`) — works on `-wv-v2` - [ ] Test session keep-alive (`/aProfile/keepAlive`)