MIB now fetch users info (idcard, phone num, email) to display in settings
Auto Tag on Version Change / check-version (push) Successful in 3s

This commit is contained in:
2026-10-23 05:27:09 +05:00
parent 46edefe4d7
commit 6b9ac5532b
8 changed files with 99 additions and 48 deletions
@@ -30,6 +30,12 @@ class MibLoginFlow(private val credentialStore: CredentialStore) {
var lastProfiles: List<MibProfile> = emptyList() var lastProfiles: List<MibProfile> = emptyList()
private set 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. */ /** Called after automatic session recovery so callers can update their session reference. */
var onSessionRefreshed: ((MibSession, List<MibProfile>) -> Unit)? = null var onSessionRefreshed: ((MibSession, List<MibProfile>) -> Unit)? = null
@@ -149,6 +155,8 @@ class MibLoginFlow(private val credentialStore: CredentialStore) {
} }
val profiles = parseProfiles(loginResp) val profiles = parseProfiles(loginResp)
lastFullName = loginResp.optString("fullName").takeIf { it != "null" } ?: ""
lastEmail = loginResp.optString("email").takeIf { it != "null" } ?: ""
lastSession = session2 lastSession = session2
lastProfiles = profiles // keep ALL profiles so settings can show them all lastProfiles = profiles // keep ALL profiles so settings can show them all
@@ -1,50 +1,87 @@
package sh.sar.basedbank.api.mib package sh.sar.basedbank.api.mib
import android.os.Build
import okhttp3.FormBody
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import org.json.JSONObject
import sh.sar.basedbank.util.CredentialStore
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
data class MibPersonalProfile( data class MibPersonalProfile(
val fullName: String, val fullName: String,
val username: String,
val email: String, val email: String,
val mobile: 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() private val client = OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS) .connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS)
.build() .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? { fun fetchPersonalProfile(session: MibSession): MibPersonalProfile? {
val cookieHeader = "mbmodel=AND-1.0; xxid=${session.xxid}; " + val personal = flow.lastProfiles.firstOrNull { it.profileType == "0" }
"IBSID=${session.xxid}; mbnonce=${session.nonceGenerator}; time-tracker=597" if (personal != null) flow.switchProfile(session, personal)
val request = Request.Builder() val request = Request.Builder()
.url("https://faisamobilex-wv-v2.mib.com.mv/personalProfile") .url("$BASE_WV_URL/ajaxAlias/getAliasTypes")
.get() .post(FormBody.Builder().build())
.header("Cookie", cookieHeader) .header("Cookie", "mbmodel=AND-1.0; xxid=${session.xxid}; IBSID=${session.xxid}; mbnonce=${session.nonceGenerator}")
.build() .header(
return try { "User-Agent",
val resp = client.newCall(request).execute() "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"
val html = resp.body?.string() ?: return null
resp.close()
fun scrape(label: String): String {
val r = Regex("""<span[^>]*>\s*<b[^>]*>\s*$label\s*</b[^>]*>.*?<span[^>]*>([^<]+)</span>""",
setOf(RegexOption.DOT_MATCHES_ALL, RegexOption.IGNORE_CASE))
return r.find(html)?.groupValues?.get(1)?.trim() ?: ""
}
val nameRegex = Regex("""<h5 class="mb-1 text-dark fw-semibold">\s*([^<]+)\s*</h5>""")
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:")
) )
} 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
))
} }
} }
@@ -63,6 +63,7 @@ import sh.sar.basedbank.api.models.BankContactCategory
import sh.sar.basedbank.api.mib.MibContactsClient import sh.sar.basedbank.api.mib.MibContactsClient
import sh.sar.basedbank.api.mib.MibCardsClient import sh.sar.basedbank.api.mib.MibCardsClient
import sh.sar.basedbank.api.mib.MibFinancingClient 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.MibProfile
import sh.sar.basedbank.api.mib.MibSession import sh.sar.basedbank.api.mib.MibSession
import sh.sar.basedbank.api.mib.MibFinanceDeal import sh.sar.basedbank.api.mib.MibFinanceDeal
@@ -821,6 +822,8 @@ fun applyNavLabelVisibility() {
app.mibProfilesMap[loginId] = flow.lastProfiles app.mibProfilesMap[loginId] = flow.lastProfiles
app.mibLoginFlows[loginId] = flow app.mibLoginFlows[loginId] = flow
store.saveMibProfiles(loginId, flow.lastProfiles) 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 accounts
} catch (e: java.io.IOException) { } catch (e: java.io.IOException) {
refreshErrors.add("NO_INTERNET") refreshErrors.add("NO_INTERNET")
@@ -353,15 +353,16 @@ class OtpFragment : Fragment() {
val session = app.mibSessions[loginId] ?: continue val session = app.mibSessions[loginId] ?: continue
val flow = app.mibFlowFor(loginId) val flow = app.mibFlowFor(loginId)
val profile = withContext(Dispatchers.IO) { val profile = withContext(Dispatchers.IO) {
try { MibProfileClient().fetchPersonalProfile(session) } catch (_: Exception) { null } try { MibProfileClient(flow).fetchPersonalProfile(session) } catch (_: Exception) { null }
} }
if (profile != null) { if (profile != null) {
store.saveMibUserProfile(loginId, CredentialStore.MibUserProfile( store.saveMibUserProfile(loginId, CredentialStore.MibUserProfile(
fullName = profile.fullName, fullName = profile.fullName,
username = profile.username, username = loginId,
email = profile.email, email = profile.email,
mobile = profile.mobile, mobile = profile.mobile,
enrolled = profile.enrolled enrolled = "",
idCard = profile.idCard
)) ))
val idx = entries.indexOfFirst { it.bank == "MIB" && it.loginId == loginId } val idx = entries.indexOfFirst { it.bank == "MIB" && it.loginId == loginId }
if (idx >= 0) { entries[idx] = entries[idx].copy(name = profile.fullName); changed = true } if (idx >= 0) { entries[idx] = entries[idx].copy(name = profile.fullName); changed = true }
@@ -668,10 +668,13 @@ class SettingsLoginsFragment : Fragment() {
scroll.addView(container) scroll.addView(container)
// Account info lines // Account info lines
val hide = viewModel.hideAmounts.value ?: false
val masked = "••••••"
listOfNotNull( listOfNotNull(
profile?.fullName?.takeIf { it.isNotBlank() }?.let { "${getString(R.string.login_detail_name)}: $it" }, 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?.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)}: $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 -> ).forEach { line ->
container.addView(TextView(ctx).apply { container.addView(TextView(ctx).apply {
text = line text = line
@@ -255,17 +255,7 @@ class CredentialsFragment : Fragment() {
store.saveMibCredentials(loginId, username, passwordHash, otpSeed) store.saveMibCredentials(loginId, username, passwordHash, otpSeed)
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
flow.lastSession?.let { s -> flow.lastSession?.let { s ->
val profile = MibProfileClient().fetchPersonalProfile(s) try { MibProfileClient(flow).cacheIfMissing(store, loginId, s) } catch (_: Exception) { }
if (profile != null) store.saveMibUserProfile(
loginId,
CredentialStore.MibUserProfile(
fullName = profile.fullName,
username = profile.username,
email = profile.email,
mobile = profile.mobile,
enrolled = profile.enrolled
)
)
} }
} }
store.saveMibProfiles(loginId, flow.lastProfiles) store.saveMibProfiles(loginId, flow.lastProfiles)
@@ -669,7 +669,8 @@ class CredentialStore(context: Context) {
val username: String, val username: String,
val email: String, val email: String,
val mobile: 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( data class BmlUserProfile(
@@ -736,6 +737,7 @@ class CredentialStore(context: Context) {
put("email", p.email) put("email", p.email)
put("mobile", p.mobile) put("mobile", p.mobile)
put("enrolled", p.enrolled) put("enrolled", p.enrolled)
if (p.idCard != null) put("idCard", p.idCard)
}.toString() }.toString()
val key = getOrCreateKey() val key = getOrCreateKey()
prefs.edit().putString("mib_${loginId}_enc_profile", encrypt(json, key)).apply() prefs.edit().putString("mib_${loginId}_enc_profile", encrypt(json, key)).apply()
@@ -752,7 +754,8 @@ class CredentialStore(context: Context) {
username = o.optString("username"), username = o.optString("username"),
email = o.optString("email"), email = o.optString("email"),
mobile = o.optString("mobile"), 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 } } catch (_: Exception) { null }
} }
+9 -3
View File
@@ -112,8 +112,13 @@ v2 moved transfers and all destination lookups to the encrypted API (`sfunc=n`).
### 7. User Profile ### 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` | ☐ | | `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] Capture transaction history — moved to encrypted `A84`, implemented
- [x] Test transaction history on device (incl. page 2) - [x] Test transaction history on device (incl. page 2)
- [ ] Test activity history (`/aProfile/getPagedActivityHistory`) - [ ] 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` - [x] Test financing (`/financing?dashurl=1`) — works on `-wv-v2`
- [ ] Test session keep-alive (`/aProfile/keepAlive`) - [ ] Test session keep-alive (`/aProfile/keepAlive`)