OTP page to show real name
All checks were successful
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

View File

@@ -190,20 +190,32 @@ class BmlLoginFlow {
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 json = resp.body?.string() ?: return ""
val json = resp.body?.string() ?: return null
resp.close()
return try {
val root = JSONObject(json)
if (!root.optBoolean("success")) return ""
val payload = root.optJSONObject("payload") ?: return ""
payload.optString("name").ifBlank {
payload.optString("fullName").ifBlank {
payload.optString("customer_name")
}
}
} catch (_: Exception) { "" }
if (!root.optBoolean("success")) return null
val user = root.optJSONObject("payload")?.optJSONObject("user") ?: return null
BmlUserInfo(
fullName = user.optString("fullname").trim(),
email = user.optString("email").trim(),
mobile = user.optString("mobile_phone").trim(),
customerId = user.optString("customer_number").trim(),
idCard = user.optString("idcard").trim(),
birthdate = user.optString("birthdate").trim()
)
} catch (_: Exception) { null }
}
fun validateAccount(session: BmlSession, input: String): BmlAccountValidation? {

View File

@@ -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. */
fun fetchProfileImage(session: MibSession, imageHash: String): String? {
val payload = baseData(session, "P41").apply {

View File

@@ -352,7 +352,7 @@ class HomeActivity : AppCompatActivity() {
lifecycleScope.launch {
try {
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 idx = existing.indexOfFirst { it.userName == userName }

View File

@@ -8,9 +8,14 @@ import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
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.ItemOtpCardBinding
import sh.sar.basedbank.util.CredentialStore
@@ -63,14 +68,65 @@ class OtpFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val store = CredentialStore(requireContext())
val app = requireActivity().application as BasedBankApp
val entries = mutableListOf<OtpEntry>()
store.loadMibCredentials()?.let { entries.add(OtpEntry("MIB · ${it.username}", it.otpSeed)) }
store.loadBmlCredentials()?.let { entries.add(OtpEntry("BML · ${it.username}", it.otpSeed)) }
store.loadMibCredentials()?.let { creds ->
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)
binding.recyclerView.layoutManager = LinearLayoutManager(requireContext())
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 {
while (isActive) {
adapter.tick()

View File

@@ -119,7 +119,22 @@ class CredentialsFragment : Fragment() {
val accounts = withContext(Dispatchers.IO) {
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)
val app = requireActivity().application as BasedBankApp
app.accounts = accounts
@@ -162,6 +177,19 @@ class CredentialsFragment : Fragment() {
val store = CredentialStore(requireContext())
store.saveBmlCredentials(username, password, otpSeed)
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)
val app = requireActivity().application as BasedBankApp
app.bmlSession = session

View File

@@ -106,6 +106,71 @@ class CredentialStore(context: Context) {
.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 {
val ks = KeyStore.getInstance("AndroidKeyStore").also { it.load(null) }
ks.getKey(keyAlias, null)?.let { return it as SecretKey }