forked from thijooree/android
patial support for BML business profile accounts
This commit is contained in:
@@ -33,7 +33,9 @@ import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import sh.sar.basedbank.BasedBankApp
|
||||
import sh.sar.basedbank.R
|
||||
import sh.sar.basedbank.api.bml.AuthExpiredException
|
||||
import sh.sar.basedbank.api.bml.BmlActivationResult
|
||||
import sh.sar.basedbank.api.bml.BmlLoginFlow
|
||||
import sh.sar.basedbank.api.bml.BmlProfile
|
||||
import sh.sar.basedbank.api.bml.BmlSession
|
||||
import sh.sar.basedbank.api.fahipay.FahipayLoginFlow
|
||||
import sh.sar.basedbank.api.fahipay.FahipaySession
|
||||
@@ -462,33 +464,83 @@ fun applyNavLabelVisibility() {
|
||||
val bmlJobs = bmlLoginIds.mapNotNull { loginId ->
|
||||
val creds = store.loadBmlCredentials(loginId) ?: return@mapNotNull null
|
||||
loginId to async(Dispatchers.IO) {
|
||||
val bmlFlow = BmlLoginFlow()
|
||||
val loginTag = "bml_$loginId"
|
||||
val savedToken = store.loadBmlSession(loginId)
|
||||
val app = application as BasedBankApp
|
||||
val savedProfiles = store.loadBmlProfiles(loginId)
|
||||
val allAccounts = mutableListOf<MibAccount>()
|
||||
var anyExpired = savedProfiles.isEmpty()
|
||||
|
||||
if (savedToken != null) {
|
||||
try {
|
||||
val session = BmlSession(savedToken.first, savedToken.second)
|
||||
val accounts = bmlFlow.fetchAccounts(session, loginTag)
|
||||
val app = application as BasedBankApp
|
||||
app.bmlSessions[loginId] = session
|
||||
AccountCache.saveBml(this@HomeActivity, loginId, accounts)
|
||||
return@async Pair(session, accounts)
|
||||
} catch (_: AuthExpiredException) {
|
||||
} catch (_: Exception) {
|
||||
// Try each saved profile's cached session
|
||||
for (profile in savedProfiles) {
|
||||
val saved = store.loadBmlProfileSession(profile.profileId)
|
||||
if (saved != null) {
|
||||
try {
|
||||
val session = BmlSession(saved.first, saved.second)
|
||||
val accounts = BmlLoginFlow().fetchAccounts(session, loginTag, profile.name, profile.profileId)
|
||||
app.bmlSessions[profile.profileId] = session
|
||||
allAccounts += accounts
|
||||
} catch (_: AuthExpiredException) { anyExpired = true
|
||||
} catch (_: Exception) { anyExpired = true }
|
||||
} else {
|
||||
anyExpired = true
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
val (session, accounts) = bmlFlow.login(creds.username, creds.password, creds.otpSeed)
|
||||
store.saveBmlSession(loginId, session.accessToken, session.deviceId)
|
||||
val app = application as BasedBankApp
|
||||
app.bmlSessions[loginId] = session
|
||||
AccountCache.saveBml(this@HomeActivity, loginId, accounts)
|
||||
Pair(session, accounts)
|
||||
} catch (_: Exception) {
|
||||
Pair(null, AccountCache.loadBml(this@HomeActivity, loginId))
|
||||
// Also try legacy single-profile session token (pre-multi-profile installs)
|
||||
if (savedProfiles.isEmpty()) {
|
||||
val legacyToken = store.loadBmlSession(loginId)
|
||||
if (legacyToken != null) {
|
||||
try {
|
||||
val session = BmlSession(legacyToken.first, legacyToken.second)
|
||||
val accounts = BmlLoginFlow().fetchAccounts(session, loginTag)
|
||||
app.bmlSessions[loginId] = session
|
||||
allAccounts += accounts
|
||||
anyExpired = false
|
||||
} catch (_: AuthExpiredException) { anyExpired = true
|
||||
} catch (_: Exception) { anyExpired = true }
|
||||
}
|
||||
}
|
||||
|
||||
if (anyExpired || allAccounts.isEmpty()) {
|
||||
// Re-authenticate to refresh personal profile sessions
|
||||
try {
|
||||
val flow = app.bmlFlowFor(loginId)
|
||||
val profiles = flow.login(creds.username, creds.password, creds.otpSeed)
|
||||
store.saveBmlProfiles(loginId, profiles)
|
||||
app.bmlProfilesMap[loginId] = profiles
|
||||
|
||||
for (profile in profiles) {
|
||||
if (profile.profileType == "business") {
|
||||
// Can't activate business profiles without user OTP — use cached
|
||||
val cached = AccountCache.loadBml(this@HomeActivity, loginId)
|
||||
.filter { it.profileId == profile.profileId }
|
||||
if (allAccounts.none { it.profileId == profile.profileId })
|
||||
allAccounts += cached
|
||||
continue
|
||||
}
|
||||
try {
|
||||
val result = flow.activateProfile(profile, loginTag)
|
||||
if (result is BmlActivationResult.Success) {
|
||||
store.saveBmlProfileSession(profile.profileId, result.session.accessToken, result.session.deviceId)
|
||||
app.bmlSessions[profile.profileId] = result.session
|
||||
allAccounts.removeAll { it.profileId == profile.profileId }
|
||||
allAccounts += result.accounts
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
if (allAccounts.none { it.profileId == profile.profileId }) {
|
||||
allAccounts += AccountCache.loadBml(this@HomeActivity, loginId)
|
||||
.filter { it.profileId == profile.profileId }
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
if (allAccounts.isEmpty())
|
||||
allAccounts += AccountCache.loadBml(this@HomeActivity, loginId)
|
||||
}
|
||||
}
|
||||
|
||||
if (allAccounts.isNotEmpty()) AccountCache.saveBml(this@HomeActivity, loginId, allAccounts)
|
||||
allAccounts as List<MibAccount>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -539,8 +591,7 @@ fun applyNavLabelVisibility() {
|
||||
|
||||
val mibResults = mibJobs.map { (loginId, job) -> loginId to job.await() }
|
||||
val mibAccounts = mibResults.flatMap { it.second }
|
||||
val bmlResults = bmlJobs.map { (_, job) -> job.await() }
|
||||
val bmlAccounts = bmlResults.flatMap { it.second }
|
||||
val bmlAccounts = bmlJobs.flatMap { (_, job) -> job.await() }
|
||||
val fahipayAccounts = fahipayJobs.flatMap { (_, job) -> job.await() }
|
||||
|
||||
val app = application as BasedBankApp
|
||||
@@ -559,14 +610,23 @@ fun applyNavLabelVisibility() {
|
||||
}
|
||||
}
|
||||
|
||||
/** Filters MIB accounts whose profileId the user has hidden in settings. */
|
||||
/** Filters accounts whose profileId the user has hidden in settings. */
|
||||
private fun List<MibAccount>.filterVisibleAccounts(): List<MibAccount> {
|
||||
val store = CredentialStore(this@HomeActivity)
|
||||
return filter { acc ->
|
||||
if (acc.bank != "MIB") return@filter true
|
||||
val loginId = acc.loginTag.removePrefix("mib_")
|
||||
val hidden = store.getHiddenMibProfileIds(loginId)
|
||||
hidden.isEmpty() || acc.profileId !in hidden
|
||||
when (acc.bank) {
|
||||
"MIB" -> {
|
||||
val loginId = acc.loginTag.removePrefix("mib_")
|
||||
val hidden = store.getHiddenMibProfileIds(loginId)
|
||||
hidden.isEmpty() || acc.profileId !in hidden
|
||||
}
|
||||
"BML" -> {
|
||||
val loginId = acc.loginTag.removePrefix("bml_")
|
||||
val hidden = store.getHiddenBmlProfileIds(loginId)
|
||||
hidden.isEmpty() || acc.profileId !in hidden
|
||||
}
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -602,10 +662,12 @@ fun applyNavLabelVisibility() {
|
||||
|
||||
private fun refreshBmlContacts(app: BasedBankApp) {
|
||||
if (app.bmlSessions.isEmpty()) return
|
||||
val store = CredentialStore(this)
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
val allBmlContacts = withContext(Dispatchers.IO) {
|
||||
app.bmlSessions.flatMap { (loginId, session) ->
|
||||
store.getBmlLoginIds().flatMap { loginId ->
|
||||
val session = app.anyBmlSessionFor(loginId) ?: return@flatMap emptyList()
|
||||
val contacts = BmlLoginFlow().fetchContacts(session, loginId)
|
||||
if (contacts.isNotEmpty()) ContactsCache.saveBml(this@HomeActivity, loginId, contacts)
|
||||
contacts
|
||||
|
||||
@@ -15,6 +15,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.materialswitch.MaterialSwitch
|
||||
import sh.sar.basedbank.BasedBankApp
|
||||
import sh.sar.basedbank.R
|
||||
import sh.sar.basedbank.api.bml.BmlProfile
|
||||
import sh.sar.basedbank.api.mib.MibProfile
|
||||
import sh.sar.basedbank.api.mib.TransactionCache
|
||||
import sh.sar.basedbank.databinding.FragmentSettingsLoginsBinding
|
||||
@@ -78,24 +79,9 @@ class SettingsLoginsFragment : Fragment() {
|
||||
for (loginId in bmlLoginIds) {
|
||||
val profile = store.loadBmlUserProfile(loginId)
|
||||
val displayName = profile?.fullName?.takeIf { it.isNotBlank() } ?: getString(R.string.bml_name)
|
||||
val profileNames = AccountCache.loadBml(ctx, loginId).map { it.profileName }.filter { it.isNotBlank() }.distinct()
|
||||
val bmlProfiles = store.loadBmlProfiles(loginId)
|
||||
addLoginRow(container, R.drawable.bml_logo_vector, displayName) {
|
||||
showLoginDetails(
|
||||
title = getString(R.string.bml_name),
|
||||
details = buildString {
|
||||
if (!profile?.fullName.isNullOrBlank()) appendLine("${getString(R.string.login_detail_name)}: ${profile!!.fullName}")
|
||||
if (!profile?.email.isNullOrBlank()) appendLine("${getString(R.string.login_detail_email)}: ${profile!!.email}")
|
||||
if (!profile?.mobile.isNullOrBlank()) appendLine("${getString(R.string.login_detail_mobile)}: ${profile!!.mobile}")
|
||||
if (!profile?.customerId.isNullOrBlank()) appendLine("${getString(R.string.login_detail_customer_id)}: ${profile!!.customerId}")
|
||||
if (!profile?.idCard.isNullOrBlank()) appendLine("${getString(R.string.login_detail_id_card)}: ${profile!!.idCard}")
|
||||
if (profileNames.isNotEmpty()) {
|
||||
appendLine()
|
||||
appendLine(getString(R.string.login_detail_profiles))
|
||||
profileNames.forEach { appendLine(" • $it") }
|
||||
}
|
||||
}.trim(),
|
||||
onLogout = { confirmLogout(getString(R.string.bml_name)) { logoutBml(store, loginId) } }
|
||||
)
|
||||
showBmlLoginDetails(store, loginId, profile, bmlProfiles)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -264,6 +250,126 @@ class SettingsLoginsFragment : Fragment() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun showBmlLoginDetails(
|
||||
store: CredentialStore,
|
||||
loginId: String,
|
||||
profile: CredentialStore.BmlUserProfile?,
|
||||
bmlProfiles: List<BmlProfile>
|
||||
) {
|
||||
val ctx = requireContext()
|
||||
val dp = ctx.resources.displayMetrics.density
|
||||
val originalHidden = store.getHiddenBmlProfileIds(loginId)
|
||||
val hidden = originalHidden.toMutableSet()
|
||||
|
||||
val scroll = android.widget.ScrollView(ctx)
|
||||
val container = LinearLayout(ctx).apply {
|
||||
orientation = LinearLayout.VERTICAL
|
||||
val pad = (16 * dp).toInt()
|
||||
setPadding(pad, (8 * dp).toInt(), pad, pad)
|
||||
}
|
||||
scroll.addView(container)
|
||||
|
||||
listOfNotNull(
|
||||
profile?.fullName?.takeIf { it.isNotBlank() }?.let { "${getString(R.string.login_detail_name)}: $it" },
|
||||
profile?.email?.takeIf { it.isNotBlank() }?.let { "${getString(R.string.login_detail_email)}: $it" },
|
||||
profile?.mobile?.takeIf { it.isNotBlank() }?.let { "${getString(R.string.login_detail_mobile)}: $it" },
|
||||
profile?.customerId?.takeIf { it.isNotBlank() }?.let { "${getString(R.string.login_detail_customer_id)}: $it" },
|
||||
profile?.idCard?.takeIf { it.isNotBlank() }?.let { "${getString(R.string.login_detail_id_card)}: $it" }
|
||||
).forEach { line ->
|
||||
container.addView(TextView(ctx).apply {
|
||||
text = line
|
||||
setTextAppearance(com.google.android.material.R.style.TextAppearance_Material3_BodyMedium)
|
||||
layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT).also {
|
||||
it.bottomMargin = (4 * dp).toInt()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (bmlProfiles.isNotEmpty()) {
|
||||
if (profile != null) {
|
||||
container.addView(View(ctx).apply {
|
||||
layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, (1 * dp).toInt()).also {
|
||||
it.topMargin = (12 * dp).toInt(); it.bottomMargin = (12 * dp).toInt()
|
||||
}
|
||||
setBackgroundColor(0x1F000000)
|
||||
})
|
||||
}
|
||||
container.addView(TextView(ctx).apply {
|
||||
text = getString(R.string.login_detail_profiles)
|
||||
setTextAppearance(com.google.android.material.R.style.TextAppearance_Material3_LabelMedium)
|
||||
layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT).also {
|
||||
it.bottomMargin = (8 * dp).toInt()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
val toggleRows = bmlProfiles.map { p ->
|
||||
val row = LinearLayout(ctx).apply {
|
||||
orientation = LinearLayout.HORIZONTAL
|
||||
gravity = Gravity.CENTER_VERTICAL
|
||||
layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT).also {
|
||||
it.bottomMargin = (4 * dp).toInt()
|
||||
}
|
||||
}
|
||||
val textCol = LinearLayout(ctx).apply {
|
||||
orientation = LinearLayout.VERTICAL
|
||||
layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)
|
||||
}
|
||||
textCol.addView(TextView(ctx).apply {
|
||||
text = p.name
|
||||
setTextAppearance(com.google.android.material.R.style.TextAppearance_Material3_BodyMedium)
|
||||
})
|
||||
if (p.type.isNotBlank()) {
|
||||
textCol.addView(TextView(ctx).apply {
|
||||
text = p.type
|
||||
setTextAppearance(com.google.android.material.R.style.TextAppearance_Material3_BodySmall)
|
||||
alpha = 0.6f
|
||||
})
|
||||
}
|
||||
val toggle = MaterialSwitch(ctx).apply { isChecked = p.profileId !in hidden }
|
||||
row.addView(textCol)
|
||||
row.addView(toggle)
|
||||
container.addView(row)
|
||||
p to toggle
|
||||
}
|
||||
|
||||
fun updateToggleStates(saveBtn: android.widget.Button) {
|
||||
val visibleCount = bmlProfiles.count { it.profileId !in hidden }
|
||||
toggleRows.forEach { (_, toggle) ->
|
||||
toggle.isEnabled = !(toggle.isChecked && visibleCount == 1)
|
||||
}
|
||||
saveBtn.isEnabled = hidden != originalHidden && visibleCount >= 1
|
||||
}
|
||||
|
||||
val dialog = MaterialAlertDialogBuilder(ctx)
|
||||
.setTitle(getString(R.string.bml_name))
|
||||
.setView(scroll)
|
||||
.setPositiveButton(R.string.save, null)
|
||||
.setNeutralButton(R.string.close, null)
|
||||
.setNegativeButton(R.string.settings_logout) { _, _ ->
|
||||
confirmLogout(getString(R.string.bml_name)) { logoutBml(store, loginId) }
|
||||
}
|
||||
.show()
|
||||
|
||||
val saveBtn = dialog.getButton(android.app.AlertDialog.BUTTON_POSITIVE)
|
||||
saveBtn.isEnabled = false
|
||||
updateToggleStates(saveBtn)
|
||||
|
||||
toggleRows.forEach { (p, toggle) ->
|
||||
toggle.setOnCheckedChangeListener { _, checked ->
|
||||
if (checked) hidden.remove(p.profileId) else hidden.add(p.profileId)
|
||||
updateToggleStates(saveBtn)
|
||||
}
|
||||
}
|
||||
|
||||
saveBtn.setOnClickListener {
|
||||
store.setHiddenBmlProfileIds(loginId, hidden)
|
||||
clearAllCaches(ctx)
|
||||
dialog.dismiss()
|
||||
(activity as? HomeActivity)?.applyProfileVisibility()
|
||||
}
|
||||
}
|
||||
|
||||
private fun showLoginDetails(title: String, details: String, onLogout: () -> Unit) {
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(title)
|
||||
@@ -299,9 +405,14 @@ class SettingsLoginsFragment : Fragment() {
|
||||
|
||||
private fun logoutBml(store: CredentialStore, loginId: String) {
|
||||
val ctx = requireContext()
|
||||
store.clearBmlCredentials(loginId); store.clearBmlSession(loginId)
|
||||
val app = requireActivity().application as BasedBankApp
|
||||
app.bmlSessions.remove(loginId)
|
||||
// Remove all per-profile sessions for this login from the in-memory map
|
||||
val profiles = app.bmlProfilesMap[loginId] ?: emptyList()
|
||||
profiles.forEach { app.bmlSessions.remove(it.profileId) }
|
||||
// clearBmlCredentials also clears per-profile tokens via loadBmlProfiles internally
|
||||
store.clearBmlCredentials(loginId)
|
||||
app.bmlProfilesMap.remove(loginId)
|
||||
app.bmlLoginFlows.remove(loginId)
|
||||
app.bmlAccounts = app.bmlAccounts.filter { it.loginTag != "bml_$loginId" }
|
||||
clearAllCaches(ctx)
|
||||
(activity as HomeActivity).relogin()
|
||||
|
||||
@@ -18,14 +18,21 @@ import kotlinx.coroutines.withContext
|
||||
import sh.sar.basedbank.util.Totp
|
||||
import sh.sar.basedbank.BasedBankApp
|
||||
import sh.sar.basedbank.R
|
||||
import sh.sar.basedbank.api.bml.BmlActivationResult
|
||||
import sh.sar.basedbank.api.bml.BmlLoginFlow
|
||||
import sh.sar.basedbank.api.bml.BmlOtpChannel
|
||||
import sh.sar.basedbank.api.bml.BmlProfile
|
||||
import sh.sar.basedbank.api.fahipay.FahipayLoginFlow
|
||||
import sh.sar.basedbank.api.fahipay.FahipaySession
|
||||
import sh.sar.basedbank.api.mib.MibAccount
|
||||
import sh.sar.basedbank.api.mib.MibLoginFlow
|
||||
import sh.sar.basedbank.util.AccountCache
|
||||
import sh.sar.basedbank.util.CredentialStore
|
||||
import sh.sar.basedbank.databinding.FragmentCredentialsBinding
|
||||
import sh.sar.basedbank.ui.home.HomeActivity
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import kotlin.coroutines.resume
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
|
||||
class CredentialsFragment : Fragment() {
|
||||
|
||||
@@ -46,6 +53,15 @@ class CredentialsFragment : Fragment() {
|
||||
private var fahipayFlow: FahipayLoginFlow? = null
|
||||
private var fahipayAwaitingTotp = false
|
||||
|
||||
// BML multi-profile state
|
||||
private var bmlFlow: BmlLoginFlow? = null
|
||||
private var bmlLoginId: String = ""
|
||||
private var bmlAccumulatedAccounts = mutableListOf<MibAccount>()
|
||||
private var bmlPendingBusinessProfiles = ArrayDeque<Pair<BmlProfile, List<BmlOtpChannel>>>()
|
||||
private var bmlCurrentBusinessProfile: BmlProfile? = null
|
||||
private var bmlSelectedChannel: String? = null
|
||||
private var bmlAwaitingBusinessOtp = false
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
_binding = FragmentCredentialsBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
@@ -151,7 +167,11 @@ class CredentialsFragment : Fragment() {
|
||||
|
||||
private fun attemptLogin() {
|
||||
when (bankType) {
|
||||
"BML" -> { attemptBmlLogin(); return }
|
||||
"BML" -> {
|
||||
if (bmlAwaitingBusinessOtp) submitBmlBusinessOtp()
|
||||
else attemptBmlLogin()
|
||||
return
|
||||
}
|
||||
"FAHIPAY" -> { attemptFahipayLogin(); return }
|
||||
}
|
||||
|
||||
@@ -232,49 +252,206 @@ class CredentialsFragment : Fragment() {
|
||||
binding.progressBar.visibility = View.VISIBLE
|
||||
binding.btnLogin.isEnabled = false
|
||||
|
||||
val loginId = username
|
||||
val flow = BmlLoginFlow()
|
||||
bmlLoginId = username
|
||||
bmlAccumulatedAccounts.clear()
|
||||
bmlPendingBusinessProfiles.clear()
|
||||
bmlCurrentBusinessProfile = null
|
||||
bmlSelectedChannel = null
|
||||
bmlAwaitingBusinessOtp = false
|
||||
|
||||
val flow = BmlLoginFlow().also { bmlFlow = it }
|
||||
val loginTag = "bml_$username"
|
||||
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
try {
|
||||
val (session, accounts) = withContext(Dispatchers.IO) {
|
||||
val profiles = withContext(Dispatchers.IO) {
|
||||
flow.login(username, password, otpSeed)
|
||||
}
|
||||
val store = CredentialStore(requireContext())
|
||||
store.saveBmlCredentials(loginId, username, password, otpSeed)
|
||||
store.saveBmlSession(loginId, session.accessToken, session.deviceId)
|
||||
withContext(Dispatchers.IO) {
|
||||
val info = flow.fetchUserInfo(session)
|
||||
if (info != null) store.saveBmlUserProfile(
|
||||
loginId,
|
||||
CredentialStore.BmlUserProfile(
|
||||
fullName = info.fullName,
|
||||
email = info.email,
|
||||
mobile = info.mobile,
|
||||
customerId = info.customerId,
|
||||
idCard = info.idCard,
|
||||
birthdate = info.birthdate
|
||||
)
|
||||
)
|
||||
if (profiles.isEmpty()) throw Exception("No profiles found for this account")
|
||||
|
||||
// Activate each profile; personal profiles are immediate, business ones need OTP
|
||||
for (profile in profiles) {
|
||||
val result = withContext(Dispatchers.IO) { flow.activateProfile(profile, loginTag) }
|
||||
when (result) {
|
||||
is BmlActivationResult.Success -> {
|
||||
bmlAccumulatedAccounts += result.accounts
|
||||
val store = CredentialStore(requireContext())
|
||||
store.saveBmlProfileSession(profile.profileId, result.session.accessToken, result.session.deviceId)
|
||||
val app = requireActivity().application as BasedBankApp
|
||||
app.bmlSessions[profile.profileId] = result.session
|
||||
}
|
||||
is BmlActivationResult.NeedsBusinessOtp -> {
|
||||
bmlPendingBusinessProfiles.addLast(Pair(profile, result.channels))
|
||||
}
|
||||
}
|
||||
}
|
||||
AccountCache.saveBml(requireContext(), loginId, accounts)
|
||||
|
||||
// Save credentials and profile list now (before business OTP prompts)
|
||||
val store = CredentialStore(requireContext())
|
||||
store.saveBmlCredentials(bmlLoginId, username, password, otpSeed)
|
||||
store.saveBmlProfiles(bmlLoginId, profiles)
|
||||
val app = requireActivity().application as BasedBankApp
|
||||
app.bmlSessions[loginId] = session
|
||||
// Merge with any existing BML accounts from other logins
|
||||
app.bmlAccounts = app.bmlAccounts.filter { it.loginTag != "bml_$loginId" } + accounts
|
||||
app.accounts = app.accounts + accounts
|
||||
val intent = Intent(requireContext(), HomeActivity::class.java)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
startActivity(intent)
|
||||
app.bmlProfilesMap[bmlLoginId] = profiles
|
||||
app.bmlLoginFlows[bmlLoginId] = flow
|
||||
|
||||
binding.progressBar.visibility = View.GONE
|
||||
binding.btnLogin.isEnabled = true
|
||||
|
||||
if (bmlPendingBusinessProfiles.isNotEmpty()) {
|
||||
processNextBmlBusinessProfile()
|
||||
} else {
|
||||
finishBmlLogin()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
binding.tvError.text = e.message ?: "Login failed"
|
||||
binding.tvError.visibility = View.VISIBLE
|
||||
} finally {
|
||||
binding.progressBar.visibility = View.GONE
|
||||
binding.btnLogin.isEnabled = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun processNextBmlBusinessProfile() {
|
||||
val (profile, channels) = bmlPendingBusinessProfiles.removeFirstOrNull()
|
||||
?: run { finishBmlLogin(); return }
|
||||
|
||||
bmlCurrentBusinessProfile = profile
|
||||
|
||||
// Show channel selection dialog
|
||||
val selectedChannel = showBmlChannelDialog(profile.name, channels) ?: run {
|
||||
// User skipped this profile — move on
|
||||
processNextBmlBusinessProfile()
|
||||
return
|
||||
}
|
||||
bmlSelectedChannel = selectedChannel
|
||||
|
||||
// Request OTP
|
||||
binding.progressBar.visibility = View.VISIBLE
|
||||
binding.btnLogin.isEnabled = false
|
||||
try {
|
||||
withContext(Dispatchers.IO) {
|
||||
bmlFlow!!.requestBusinessOtp(selectedChannel)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
binding.progressBar.visibility = View.GONE
|
||||
binding.btnLogin.isEnabled = true
|
||||
binding.tvError.text = e.message ?: "Failed to send OTP"
|
||||
binding.tvError.visibility = View.VISIBLE
|
||||
processNextBmlBusinessProfile()
|
||||
return
|
||||
}
|
||||
|
||||
// Show OTP input — disable credential fields (same pattern as Fahipay TOTP step)
|
||||
bmlAwaitingBusinessOtp = true
|
||||
binding.etUsername.isEnabled = false
|
||||
binding.etPassword.isEnabled = false
|
||||
binding.etOtpSeed.isEnabled = false
|
||||
binding.progressBar.visibility = View.GONE
|
||||
binding.tilTotpCode.hint = getString(R.string.bml_business_otp_hint, profile.name)
|
||||
binding.tilTotpCode.helperText = getString(R.string.bml_business_otp_sent, selectedChannel)
|
||||
binding.tilTotpCode.visibility = View.VISIBLE
|
||||
binding.etTotpCode.text?.clear()
|
||||
binding.btnLogin.text = getString(R.string.verify)
|
||||
binding.btnLogin.isEnabled = true
|
||||
binding.tvError.visibility = View.GONE
|
||||
}
|
||||
|
||||
private fun submitBmlBusinessOtp() {
|
||||
val code = binding.etTotpCode.text.toString().trim()
|
||||
if (code.length != 6) {
|
||||
binding.tvError.text = getString(R.string.fahipay_totp_hint)
|
||||
binding.tvError.visibility = View.VISIBLE
|
||||
return
|
||||
}
|
||||
|
||||
binding.tvError.visibility = View.GONE
|
||||
binding.progressBar.visibility = View.VISIBLE
|
||||
binding.btnLogin.isEnabled = false
|
||||
|
||||
val profile = bmlCurrentBusinessProfile ?: return
|
||||
val channel = bmlSelectedChannel ?: return
|
||||
val loginTag = "bml_$bmlLoginId"
|
||||
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
try {
|
||||
val (session, accounts) = withContext(Dispatchers.IO) {
|
||||
bmlFlow!!.submitBusinessOtp(channel, code, profile, loginTag)
|
||||
}
|
||||
bmlAccumulatedAccounts += accounts
|
||||
val store = CredentialStore(requireContext())
|
||||
store.saveBmlProfileSession(profile.profileId, session.accessToken, session.deviceId)
|
||||
val app = requireActivity().application as BasedBankApp
|
||||
app.bmlSessions[profile.profileId] = session
|
||||
|
||||
bmlAwaitingBusinessOtp = false
|
||||
binding.tilTotpCode.visibility = View.GONE
|
||||
binding.btnLogin.text = getString(R.string.login)
|
||||
|
||||
binding.progressBar.visibility = View.GONE
|
||||
binding.btnLogin.isEnabled = true
|
||||
|
||||
if (bmlPendingBusinessProfiles.isNotEmpty()) {
|
||||
processNextBmlBusinessProfile()
|
||||
} else {
|
||||
finishBmlLogin()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
binding.tvError.text = e.message ?: "OTP verification failed"
|
||||
binding.tvError.visibility = View.VISIBLE
|
||||
binding.progressBar.visibility = View.GONE
|
||||
binding.btnLogin.isEnabled = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun showBmlChannelDialog(profileName: String, channels: List<BmlOtpChannel>): String? =
|
||||
suspendCancellableCoroutine { cont ->
|
||||
val options = channels.map { "${it.description} (${it.masked})" }.toTypedArray()
|
||||
val dialog = MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(getString(R.string.bml_business_otp_title, profileName))
|
||||
.setItems(options) { _, which ->
|
||||
if (cont.isActive) cont.resume(channels[which].channel)
|
||||
}
|
||||
.setNegativeButton(R.string.bml_business_otp_skip) { _: android.content.DialogInterface, _: Int ->
|
||||
if (cont.isActive) cont.resume(null as String?)
|
||||
}
|
||||
.show()
|
||||
dialog.setOnCancelListener { if (cont.isActive) cont.resume(null as String?) }
|
||||
}
|
||||
|
||||
private suspend fun finishBmlLogin() {
|
||||
val store = CredentialStore(requireContext())
|
||||
val accounts = bmlAccumulatedAccounts.toList()
|
||||
|
||||
// Fetch user profile info from any active session
|
||||
val app = requireActivity().application as BasedBankApp
|
||||
val anySession = app.anyBmlSessionFor(bmlLoginId)
|
||||
if (anySession != null) {
|
||||
withContext(Dispatchers.IO) {
|
||||
val info = BmlLoginFlow().fetchUserInfo(anySession)
|
||||
if (info != null) store.saveBmlUserProfile(
|
||||
bmlLoginId,
|
||||
CredentialStore.BmlUserProfile(
|
||||
fullName = info.fullName,
|
||||
email = info.email,
|
||||
mobile = info.mobile,
|
||||
customerId = info.customerId,
|
||||
idCard = info.idCard,
|
||||
birthdate = info.birthdate
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
AccountCache.saveBml(requireContext(), bmlLoginId, accounts)
|
||||
app.bmlAccounts = app.bmlAccounts.filter { it.loginTag != "bml_$bmlLoginId" } + accounts
|
||||
app.accounts = app.accounts.filter { it.loginTag != "bml_$bmlLoginId" } + accounts
|
||||
|
||||
val intent = Intent(requireContext(), HomeActivity::class.java)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
private fun attemptFahipayLogin() {
|
||||
if (fahipayAwaitingTotp) {
|
||||
submitFahipayTotp()
|
||||
|
||||
Reference in New Issue
Block a user