forked from thijooree/android
MIB now fetch users info (idcard, phone num, email) to display in settings
This commit is contained in:
@@ -30,6 +30,12 @@ class MibLoginFlow(private val credentialStore: CredentialStore) {
|
||||
var lastProfiles: List<MibProfile> = 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<MibProfile>) -> 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
|
||||
|
||||
|
||||
@@ -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("""<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:")
|
||||
.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
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
|
||||
@@ -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`)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user