OTP page to show real name
All checks were successful
Auto Tag on Version Change / check-version (push) Successful in 2s
All checks were successful
Auto Tag on Version Change / check-version (push) Successful in 2s
This commit is contained in:
@@ -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? {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }
|
||||
|
||||
Reference in New Issue
Block a user