forked from shihaam/thijooree
OTP page to show real name
This commit is contained in:
@@ -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 }
|
||||||
|
|||||||
Reference in New Issue
Block a user