OTP page to show real name
Auto Tag on Version Change / check-version (push) Successful in 2s

This commit is contained in:
2026-05-15 11:53:51 +05:00
parent feb5b41f8b
commit 7c0ffece35
6 changed files with 213 additions and 14 deletions
@@ -190,20 +190,32 @@ class BmlLoginFlow {
return parseForeignLimits(json ?: return emptyList()) return parseForeignLimits(json ?: return emptyList())
} }
fun fetchUserInfo(session: BmlSession): String { data class BmlUserInfo(
val fullName: String,
val email: String,
val mobile: String,
val customerId: String,
val idCard: String,
val birthdate: String
)
fun fetchUserInfo(session: BmlSession): BmlUserInfo? {
val resp = apiClient.newCall(apiRequest(session, "$BASE_URL/api/mobile/userinfo")).execute() val resp = apiClient.newCall(apiRequest(session, "$BASE_URL/api/mobile/userinfo")).execute()
val json = resp.body?.string() ?: return "" val json = resp.body?.string() ?: return null
resp.close() resp.close()
return try { return try {
val root = JSONObject(json) val root = JSONObject(json)
if (!root.optBoolean("success")) return "" if (!root.optBoolean("success")) return null
val payload = root.optJSONObject("payload") ?: return "" val user = root.optJSONObject("payload")?.optJSONObject("user") ?: return null
payload.optString("name").ifBlank { BmlUserInfo(
payload.optString("fullName").ifBlank { fullName = user.optString("fullname").trim(),
payload.optString("customer_name") email = user.optString("email").trim(),
} mobile = user.optString("mobile_phone").trim(),
} customerId = user.optString("customer_number").trim(),
} catch (_: Exception) { "" } idCard = user.optString("idcard").trim(),
birthdate = user.optString("birthdate").trim()
)
} catch (_: Exception) { null }
} }
fun validateAccount(session: BmlSession, input: String): BmlAccountValidation? { fun validateAccount(session: BmlSession, input: String): BmlAccountValidation? {
@@ -305,6 +305,44 @@ class MibLoginFlow(private val prefs: android.content.SharedPreferences) {
} }
} }
data class MibPersonalProfile(
val fullName: String,
val username: String,
val email: String,
val mobile: String,
val enrolled: String
)
/** Fetches the customer's profile info from the Faisanet personal profile page. */
fun fetchPersonalProfile(session: MibSession): MibPersonalProfile? {
val cookieHeader = "mbmodel=IOS-1.0; xxid=${session.xxid}; " +
"IBSID=${session.xxid}; mbnonce=${session.nonceGenerator}; time-tracker=597"
val request = Request.Builder()
.url("https://faisamobilex-wv.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:")
)
} catch (_: Exception) { null }
}
/** Fetches a profile image via P41. Returns base64 JPEG string, or null if not found. */ /** Fetches a profile image via P41. Returns base64 JPEG string, or null if not found. */
fun fetchProfileImage(session: MibSession, imageHash: String): String? { fun fetchProfileImage(session: MibSession, imageHash: String): String? {
val payload = baseData(session, "P41").apply { val payload = baseData(session, "P41").apply {
@@ -352,7 +352,7 @@ class HomeActivity : AppCompatActivity() {
lifecycleScope.launch { lifecycleScope.launch {
try { try {
val (userName, limits) = withContext(Dispatchers.IO) { val (userName, limits) = withContext(Dispatchers.IO) {
Pair(bmlFlow.fetchUserInfo(session), bmlFlow.fetchForeignLimits(session)) Pair(bmlFlow.fetchUserInfo(session)?.fullName ?: "", bmlFlow.fetchForeignLimits(session))
} }
val existing = viewModel.bmlLimits.value?.toMutableList() ?: mutableListOf() val existing = viewModel.bmlLimits.value?.toMutableList() ?: mutableListOf()
val idx = existing.indexOfFirst { it.userName == userName } val idx = existing.indexOfFirst { it.userName == userName }
@@ -8,9 +8,14 @@ import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import sh.sar.basedbank.BasedBankApp
import sh.sar.basedbank.api.bml.BmlLoginFlow
import sh.sar.basedbank.api.mib.MibLoginFlow
import sh.sar.basedbank.databinding.FragmentOtpBinding import sh.sar.basedbank.databinding.FragmentOtpBinding
import sh.sar.basedbank.databinding.ItemOtpCardBinding import sh.sar.basedbank.databinding.ItemOtpCardBinding
import sh.sar.basedbank.util.CredentialStore import sh.sar.basedbank.util.CredentialStore
@@ -63,14 +68,65 @@ class OtpFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val store = CredentialStore(requireContext()) val store = CredentialStore(requireContext())
val app = requireActivity().application as BasedBankApp
val entries = mutableListOf<OtpEntry>() val entries = mutableListOf<OtpEntry>()
store.loadMibCredentials()?.let { entries.add(OtpEntry("MIB · ${it.username}", it.otpSeed)) } store.loadMibCredentials()?.let { creds ->
store.loadBmlCredentials()?.let { entries.add(OtpEntry("BML · ${it.username}", it.otpSeed)) } val name = store.loadMibFullName()
entries.add(OtpEntry(if (name != null) "MIB · $name" else "MIB", creds.otpSeed))
}
store.loadBmlCredentials()?.let { creds ->
val name = store.loadBmlFullName()
entries.add(OtpEntry(if (name != null) "BML · $name" else "BML", creds.otpSeed))
}
val adapter = OtpAdapter(entries) val adapter = OtpAdapter(entries)
binding.recyclerView.layoutManager = LinearLayoutManager(requireContext()) binding.recyclerView.layoutManager = LinearLayoutManager(requireContext())
binding.recyclerView.adapter = adapter binding.recyclerView.adapter = adapter
// Fetch real names in background if not yet cached, then refresh labels
viewLifecycleOwner.lifecycleScope.launch {
var changed = false
if (store.loadMibFullName() == null) {
app.mibSession?.let { session ->
val profile = withContext(Dispatchers.IO) {
try { app.mibLoginFlow.fetchPersonalProfile(session) } catch (_: Exception) { null }
}
if (profile != null) {
store.saveMibUserProfile(CredentialStore.MibUserProfile(
fullName = profile.fullName,
username = profile.username,
email = profile.email,
mobile = profile.mobile,
enrolled = profile.enrolled
))
val idx = entries.indexOfFirst { it.seed == store.loadMibCredentials()?.otpSeed }
if (idx >= 0) { entries[idx] = entries[idx].copy(label = "MIB · ${profile.fullName}"); changed = true }
}
}
}
if (store.loadBmlFullName() == null) {
app.bmlSession?.let { session ->
val info = withContext(Dispatchers.IO) {
try { BmlLoginFlow().fetchUserInfo(session) } catch (_: Exception) { null }
}
if (info != null) {
store.saveBmlUserProfile(CredentialStore.BmlUserProfile(
fullName = info.fullName,
email = info.email,
mobile = info.mobile,
customerId = info.customerId,
idCard = info.idCard,
birthdate = info.birthdate
))
val idx = entries.indexOfFirst { it.seed == store.loadBmlCredentials()?.otpSeed }
if (idx >= 0) { entries[idx] = entries[idx].copy(label = "BML · ${info.fullName}"); changed = true }
}
}
}
if (changed) adapter.notifyDataSetChanged()
}
viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.lifecycleScope.launch {
while (isActive) { while (isActive) {
adapter.tick() adapter.tick()
@@ -119,7 +119,22 @@ class CredentialsFragment : Fragment() {
val accounts = withContext(Dispatchers.IO) { val accounts = withContext(Dispatchers.IO) {
flow.login(username, passwordHash, otpSeed) flow.login(username, passwordHash, otpSeed)
} }
CredentialStore(requireContext()).saveMibCredentials(username, passwordHash, otpSeed) val store = CredentialStore(requireContext())
store.saveMibCredentials(username, passwordHash, otpSeed)
withContext(Dispatchers.IO) {
flow.lastSession?.let { s ->
val profile = flow.fetchPersonalProfile(s)
if (profile != null) store.saveMibUserProfile(
CredentialStore.MibUserProfile(
fullName = profile.fullName,
username = profile.username,
email = profile.email,
mobile = profile.mobile,
enrolled = profile.enrolled
)
)
}
}
AccountCache.save(requireContext(), accounts) AccountCache.save(requireContext(), accounts)
val app = requireActivity().application as BasedBankApp val app = requireActivity().application as BasedBankApp
app.accounts = accounts app.accounts = accounts
@@ -162,6 +177,19 @@ class CredentialsFragment : Fragment() {
val store = CredentialStore(requireContext()) val store = CredentialStore(requireContext())
store.saveBmlCredentials(username, password, otpSeed) store.saveBmlCredentials(username, password, otpSeed)
store.saveBmlSession(session.accessToken, session.deviceId) store.saveBmlSession(session.accessToken, session.deviceId)
withContext(Dispatchers.IO) {
val info = flow.fetchUserInfo(session)
if (info != null) store.saveBmlUserProfile(
CredentialStore.BmlUserProfile(
fullName = info.fullName,
email = info.email,
mobile = info.mobile,
customerId = info.customerId,
idCard = info.idCard,
birthdate = info.birthdate
)
)
}
AccountCache.saveBml(requireContext(), accounts) AccountCache.saveBml(requireContext(), accounts)
val app = requireActivity().application as BasedBankApp val app = requireActivity().application as BasedBankApp
app.bmlSession = session app.bmlSession = session
@@ -106,6 +106,71 @@ class CredentialStore(context: Context) {
.apply() .apply()
} }
data class MibUserProfile(
val fullName: String,
val username: String,
val email: String,
val mobile: String,
val enrolled: String
)
data class BmlUserProfile(
val fullName: String,
val email: String,
val mobile: String,
val customerId: String,
val idCard: String,
val birthdate: String
)
fun saveMibFullName(name: String) = prefs.edit().putString("mib_full_name", name).apply()
fun loadMibFullName(): String? = prefs.getString("mib_full_name", null)
fun saveBmlFullName(name: String) = prefs.edit().putString("bml_full_name", name).apply()
fun loadBmlFullName(): String? = prefs.getString("bml_full_name", null)
fun saveMibUserProfile(p: MibUserProfile) {
prefs.edit().putString("mib_full_name", p.fullName)
.putString("mib_profile_username", p.username)
.putString("mib_profile_email", p.email)
.putString("mib_profile_mobile", p.mobile)
.putString("mib_profile_enrolled", p.enrolled)
.apply()
}
fun loadMibUserProfile(): MibUserProfile? {
val name = prefs.getString("mib_full_name", null) ?: return null
return MibUserProfile(
fullName = name,
username = prefs.getString("mib_profile_username", "") ?: "",
email = prefs.getString("mib_profile_email", "") ?: "",
mobile = prefs.getString("mib_profile_mobile", "") ?: "",
enrolled = prefs.getString("mib_profile_enrolled", "") ?: ""
)
}
fun saveBmlUserProfile(p: BmlUserProfile) {
prefs.edit().putString("bml_full_name", p.fullName)
.putString("bml_profile_email", p.email)
.putString("bml_profile_mobile", p.mobile)
.putString("bml_profile_customer_id", p.customerId)
.putString("bml_profile_idcard", p.idCard)
.putString("bml_profile_birthdate", p.birthdate)
.apply()
}
fun loadBmlUserProfile(): BmlUserProfile? {
val name = prefs.getString("bml_full_name", null) ?: return null
return BmlUserProfile(
fullName = name,
email = prefs.getString("bml_profile_email", "") ?: "",
mobile = prefs.getString("bml_profile_mobile", "") ?: "",
customerId = prefs.getString("bml_profile_customer_id", "") ?: "",
idCard = prefs.getString("bml_profile_idcard", "") ?: "",
birthdate = prefs.getString("bml_profile_birthdate", "") ?: ""
)
}
private fun getOrCreateKey(): SecretKey { private fun getOrCreateKey(): SecretKey {
val ks = KeyStore.getInstance("AndroidKeyStore").also { it.load(null) } val ks = KeyStore.getInstance("AndroidKeyStore").also { it.load(null) }
ks.getKey(keyAlias, null)?.let { return it as SecretKey } ks.getKey(keyAlias, null)?.let { return it as SecretKey }