diff --git a/app/src/main/java/sh/sar/basedbank/api/bml/BmlLoginFlow.kt b/app/src/main/java/sh/sar/basedbank/api/bml/BmlLoginFlow.kt index 5b1c5ff..3d451e5 100644 --- a/app/src/main/java/sh/sar/basedbank/api/bml/BmlLoginFlow.kt +++ b/app/src/main/java/sh/sar/basedbank/api/bml/BmlLoginFlow.kt @@ -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? { 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 eb867e2..03141aa 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 @@ -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("""]*>\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:") + ) + } 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 { 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 80332d7..ae20a0b 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 @@ -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 } 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 ced7279..cfff421 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 @@ -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() - 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() 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 4e10396..ea5d371 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 @@ -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 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 ba324bb..a78c81e 100644 --- a/app/src/main/java/sh/sar/basedbank/util/CredentialStore.kt +++ b/app/src/main/java/sh/sar/basedbank/util/CredentialStore.kt @@ -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 }