Optmize business profile logins.. save privacy mode settings on launch

This commit is contained in:
2026-05-21 00:43:05 +05:00
parent 6d48c27391
commit fe507073b1
9 changed files with 304 additions and 161 deletions
@@ -194,8 +194,7 @@ class HomeActivity : AppCompatActivity() {
autoRefresh(store) autoRefresh(store)
} }
// hideAmounts is always false on launch; eye feature just needs to be enabled viewModel.hideAmounts.value = getSharedPreferences("prefs", MODE_PRIVATE).getBoolean("hide_amounts", false)
viewModel.hideAmounts.value = false
// Show dashboard on first create // Show dashboard on first create
if (savedInstanceState == null) { if (savedInstanceState == null) {
@@ -434,6 +433,7 @@ fun applyNavLabelVisibility() {
if (item.itemId == R.id.action_hide_amounts) { if (item.itemId == R.id.action_hide_amounts) {
val newHidden = !(viewModel.hideAmounts.value ?: false) val newHidden = !(viewModel.hideAmounts.value ?: false)
viewModel.hideAmounts.value = newHidden viewModel.hideAmounts.value = newHidden
getSharedPreferences("prefs", MODE_PRIVATE).edit().putBoolean("hide_amounts", newHidden).apply()
invalidateOptionsMenu() invalidateOptionsMenu()
return true return true
} }
@@ -11,6 +11,7 @@ import android.widget.ImageView
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.TextView import android.widget.TextView
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.materialswitch.MaterialSwitch import com.google.android.material.materialswitch.MaterialSwitch
import sh.sar.basedbank.BasedBankApp import sh.sar.basedbank.BasedBankApp
@@ -27,11 +28,21 @@ import sh.sar.basedbank.util.CredentialStore
import sh.sar.basedbank.util.FinancingCache import sh.sar.basedbank.util.FinancingCache
import sh.sar.basedbank.util.ForeignLimitsCache import sh.sar.basedbank.util.ForeignLimitsCache
import sh.sar.basedbank.util.RecentsCache import sh.sar.basedbank.util.RecentsCache
import androidx.lifecycle.lifecycleScope
import kotlin.coroutines.resume
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext
import sh.sar.basedbank.api.bml.BmlActivationResult
import sh.sar.basedbank.api.bml.BmlLoginFlow
import sh.sar.basedbank.api.bml.BmlOtpChannel
class SettingsLoginsFragment : Fragment() { class SettingsLoginsFragment : Fragment() {
private var _binding: FragmentSettingsLoginsBinding? = null private var _binding: FragmentSettingsLoginsBinding? = null
private val binding get() = _binding!! private val binding get() = _binding!!
private val viewModel: HomeViewModel by activityViewModels()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_binding = FragmentSettingsLoginsBinding.inflate(inflater, container, false) _binding = FragmentSettingsLoginsBinding.inflate(inflater, container, false)
@@ -89,13 +100,15 @@ class SettingsLoginsFragment : Fragment() {
val profile = store.loadFahipayUserProfile(loginId) val profile = store.loadFahipayUserProfile(loginId)
val displayName = profile?.fullName?.takeIf { it.isNotBlank() } ?: getString(R.string.fahipay_name) val displayName = profile?.fullName?.takeIf { it.isNotBlank() } ?: getString(R.string.fahipay_name)
addLoginRow(container, R.drawable.fahipay_logo, displayName) { addLoginRow(container, R.drawable.fahipay_logo, displayName) {
val hide = viewModel.hideAmounts.value ?: false
val masked = "••••••"
showLoginDetails( showLoginDetails(
title = getString(R.string.fahipay_name), title = getString(R.string.fahipay_name),
details = buildString { details = buildString {
if (!profile?.fullName.isNullOrBlank()) appendLine("${getString(R.string.login_detail_name)}: ${profile!!.fullName}") 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?.email.isNullOrBlank()) appendLine("${getString(R.string.login_detail_email)}: ${if (hide) masked else profile!!.email}")
if (!profile?.mobile.isNullOrBlank()) appendLine("${getString(R.string.login_detail_mobile)}: ${profile!!.mobile}") if (!profile?.mobile.isNullOrBlank()) appendLine("${getString(R.string.login_detail_mobile)}: ${if (hide) masked else profile!!.mobile}")
if (!profile?.nid.isNullOrBlank()) appendLine("${getString(R.string.login_detail_id_card)}: ${profile!!.nid}") if (!profile?.nid.isNullOrBlank()) appendLine("${getString(R.string.login_detail_id_card)}: ${if (hide) masked else profile!!.nid}")
}.trim(), }.trim(),
onLogout = { confirmLogout(getString(R.string.fahipay_name)) { logoutFahipay(store, loginId) } } onLogout = { confirmLogout(getString(R.string.fahipay_name)) { logoutFahipay(store, loginId) } }
) )
@@ -258,8 +271,16 @@ class SettingsLoginsFragment : Fragment() {
) { ) {
val ctx = requireContext() val ctx = requireContext()
val dp = ctx.resources.displayMetrics.density val dp = ctx.resources.displayMetrics.density
val originalHidden = store.getHiddenBmlProfileIds(loginId) val hidden = store.getHiddenBmlProfileIds(loginId).toMutableSet()
val hidden = originalHidden.toMutableSet() // Business profiles with no saved session were skipped during login — ensure they start hidden
val needsActivation = bmlProfiles
.filter { it.profileType == "business" && store.loadBmlProfileSession(it.profileId) == null }
.map { it.profileId }
.toMutableSet()
for (id in needsActivation) {
if (hidden.add(id)) store.setHiddenBmlProfileIds(loginId, hidden)
}
val originalHidden = hidden.toSet()
val scroll = android.widget.ScrollView(ctx) val scroll = android.widget.ScrollView(ctx)
val container = LinearLayout(ctx).apply { val container = LinearLayout(ctx).apply {
@@ -269,12 +290,14 @@ class SettingsLoginsFragment : Fragment() {
} }
scroll.addView(container) scroll.addView(container)
val hide = viewModel.hideAmounts.value ?: false
val masked = "••••••"
listOfNotNull( listOfNotNull(
profile?.fullName?.takeIf { it.isNotBlank() }?.let { "${getString(R.string.login_detail_name)}: $it" }, 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?.email?.takeIf { it.isNotBlank() }?.let { "${getString(R.string.login_detail_email)}: ${if (hide) masked else it}" },
profile?.mobile?.takeIf { it.isNotBlank() }?.let { "${getString(R.string.login_detail_mobile)}: $it" }, profile?.mobile?.takeIf { it.isNotBlank() }?.let { "${getString(R.string.login_detail_mobile)}: ${if (hide) masked else it}" },
profile?.customerId?.takeIf { it.isNotBlank() }?.let { "${getString(R.string.login_detail_customer_id)}: $it" }, profile?.customerId?.takeIf { it.isNotBlank() }?.let { "${getString(R.string.login_detail_customer_id)}: ${if (hide) masked else it}" },
profile?.idCard?.takeIf { it.isNotBlank() }?.let { "${getString(R.string.login_detail_id_card)}: $it" } profile?.idCard?.takeIf { it.isNotBlank() }?.let { "${getString(R.string.login_detail_id_card)}: ${if (hide) masked else it}" }
).forEach { line -> ).forEach { line ->
container.addView(TextView(ctx).apply { container.addView(TextView(ctx).apply {
text = line text = line
@@ -357,8 +380,19 @@ class SettingsLoginsFragment : Fragment() {
toggleRows.forEach { (p, toggle) -> toggleRows.forEach { (p, toggle) ->
toggle.setOnCheckedChangeListener { _, checked -> toggle.setOnCheckedChangeListener { _, checked ->
if (checked) hidden.remove(p.profileId) else hidden.add(p.profileId) if (checked && p.profileId in needsActivation) {
updateToggleStates(saveBtn) toggle.isChecked = false // revert — enabling requires OTP
viewLifecycleOwner.lifecycleScope.launch {
val success = activateBmlBusinessProfile(store, loginId, p)
if (success) {
needsActivation.remove(p.profileId)
toggle.isChecked = true // listener re-fires, removes from hidden
}
}
} else {
if (checked) hidden.remove(p.profileId) else hidden.add(p.profileId)
updateToggleStates(saveBtn)
}
} }
} }
@@ -366,7 +400,209 @@ class SettingsLoginsFragment : Fragment() {
store.setHiddenBmlProfileIds(loginId, hidden) store.setHiddenBmlProfileIds(loginId, hidden)
clearAllCaches(ctx) clearAllCaches(ctx)
dialog.dismiss() dialog.dismiss()
(activity as? HomeActivity)?.applyProfileVisibility() (activity as? HomeActivity)?.relogin()
}
}
private suspend fun activateBmlBusinessProfile(
store: CredentialStore,
loginId: String,
profile: BmlProfile
): Boolean {
val creds = store.loadBmlCredentials(loginId) ?: run {
showSimpleError("Credentials not found — please log out and log in again")
return false
}
val progressDialog = MaterialAlertDialogBuilder(requireContext())
.setMessage("Connecting to BML\u2026")
.setCancelable(false)
.show()
val flow = BmlLoginFlow()
val loginTag = "bml_$loginId"
val activationResult = try {
withContext(Dispatchers.IO) {
flow.login(creds.username, creds.password, creds.otpSeed)
flow.activateProfile(profile, loginTag)
}
} catch (e: Exception) {
progressDialog.dismiss()
showSimpleError(e.message ?: "Authentication failed")
return false
}
progressDialog.dismiss()
return when (activationResult) {
is BmlActivationResult.Success -> {
store.saveBmlProfileSession(profile.profileId, activationResult.session.accessToken, activationResult.session.deviceId)
true
}
is BmlActivationResult.NeedsBusinessOtp ->
continueBmlBusinessOtpFlow(store, loginId, profile, flow, loginTag, activationResult.channels)
}
}
private suspend fun continueBmlBusinessOtpFlow(
store: CredentialStore,
loginId: String,
profile: BmlProfile,
flow: BmlLoginFlow,
loginTag: String,
channels: List<BmlOtpChannel>
): Boolean {
val selectedChannel = showBmlChannelSelectionDialog(profile.name, channels) ?: return false
val channelObj = channels.first { it.channel == selectedChannel }
val sendProgress = MaterialAlertDialogBuilder(requireContext())
.setMessage("Sending OTP\u2026")
.setCancelable(false)
.show()
try {
withContext(Dispatchers.IO) { flow.requestBusinessOtp(selectedChannel) }
} catch (e: Exception) {
sendProgress.dismiss()
showSimpleError(e.message ?: "Failed to send OTP")
return false
}
sendProgress.dismiss()
var otpError: String? = null
while (true) {
val code = showBmlOtpInputDialog(profile.name, channelObj, otpError) ?: return false
val verifyProgress = MaterialAlertDialogBuilder(requireContext())
.setMessage("Verifying\u2026")
.setCancelable(false)
.show()
try {
val (session, _) = withContext(Dispatchers.IO) {
flow.submitBusinessOtp(selectedChannel, code, profile, loginTag)
}
verifyProgress.dismiss()
store.saveBmlProfileSession(profile.profileId, session.accessToken, session.deviceId)
return true
} catch (e: Exception) {
verifyProgress.dismiss()
if (e.message?.contains("Invalid OTP") == true) {
otpError = e.message
} else {
showSimpleError(e.message ?: "Verification failed")
return false
}
}
}
}
private fun showSimpleError(message: String) {
MaterialAlertDialogBuilder(requireContext())
.setMessage(message)
.setPositiveButton(R.string.close, null)
.show()
}
private suspend fun showBmlChannelSelectionDialog(profileName: String, channels: List<BmlOtpChannel>): String? =
suspendCancellableCoroutine { cont ->
val ctx = requireContext()
val dp = ctx.resources.displayMetrics.density
val list = LinearLayout(ctx).apply {
orientation = LinearLayout.VERTICAL
val vp = (8 * dp).toInt()
setPadding(0, vp, 0, vp)
}
for (channel in channels) {
val iconRes = when (channel.channel) {
"Email" -> R.drawable.ic_channel_email
"Mobile" -> R.drawable.ic_channel_sms
"WhatsApp" -> R.drawable.ic_channel_whatsapp
else -> R.drawable.ic_channel_sms
}
val iconSize = (24 * dp).toInt()
val iconView = ImageView(ctx).apply {
setImageResource(iconRes)
}
val textCol = LinearLayout(ctx).apply {
orientation = LinearLayout.VERTICAL
gravity = Gravity.CENTER_VERTICAL
layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f).apply {
marginStart = (12 * dp).toInt()
}
}
textCol.addView(TextView(ctx).apply {
text = channel.description
setTextAppearance(com.google.android.material.R.style.TextAppearance_Material3_BodyLarge)
})
textCol.addView(TextView(ctx).apply {
text = channel.masked
setTextAppearance(com.google.android.material.R.style.TextAppearance_Material3_BodySmall)
alpha = 0.6f
})
val row = LinearLayout(ctx).apply {
orientation = LinearLayout.HORIZONTAL
gravity = Gravity.CENTER_VERTICAL
val ta = ctx.obtainStyledAttributes(intArrayOf(android.R.attr.selectableItemBackground))
background = ta.getDrawable(0); ta.recycle()
isClickable = true; isFocusable = true
val hp = (24 * dp).toInt(); val vp = (12 * dp).toInt()
setPadding(hp, vp, hp, vp)
}
row.addView(iconView, LinearLayout.LayoutParams(iconSize, iconSize))
row.addView(textCol)
list.addView(row)
}
val d = MaterialAlertDialogBuilder(ctx)
.setTitle("Send verification code")
.setView(list)
.setNegativeButton(R.string.cancel) { _, _ -> if (cont.isActive) cont.resume(null) }
.setCancelable(false)
.show()
d.setOnCancelListener { if (cont.isActive) cont.resume(null) }
// Wire up row clicks after dialog is created so we can dismiss it first
val rows = list.run { (0 until childCount).map { getChildAt(it) } }
rows.forEachIndexed { i, row ->
row.setOnClickListener {
d.dismiss()
if (cont.isActive) cont.resume(channels[i].channel)
}
}
}
private suspend fun showBmlOtpInputDialog(
profileName: String,
channel: BmlOtpChannel,
errorMsg: String? = null
): String? = suspendCancellableCoroutine { cont ->
val ctx = requireContext()
val dp = ctx.resources.displayMetrics.density
val input = android.widget.EditText(ctx).apply {
hint = "Enter OTP"
inputType = android.text.InputType.TYPE_CLASS_NUMBER
filters = arrayOf(android.text.InputFilter.LengthFilter(6))
setPadding((24 * dp).toInt(), (8 * dp).toInt(), (24 * dp).toInt(), (8 * dp).toInt())
}
val msg = buildString {
append(getString(R.string.bml_business_otp_sent, channel.description))
append(" (${channel.masked})")
if (errorMsg != null) append("\n\n$errorMsg")
}
val d = MaterialAlertDialogBuilder(ctx)
.setTitle("Enter verification code")
.setMessage(msg)
.setView(input)
.setPositiveButton(R.string.verify, null)
.setNegativeButton(R.string.cancel) { _, _ -> if (cont.isActive) cont.resume(null) }
.setCancelable(false)
.show()
d.setOnCancelListener { if (cont.isActive) cont.resume(null) }
d.getButton(android.app.AlertDialog.BUTTON_POSITIVE).setOnClickListener {
val code = input.text.toString().trim()
if (code.length != 6) {
input.error = "Enter 6 digits"
} else {
d.dismiss()
if (cont.isActive) cont.resume(code)
}
} }
} }
@@ -21,8 +21,6 @@ import sh.sar.basedbank.R
import sh.sar.basedbank.api.bml.BmlAccountClient import sh.sar.basedbank.api.bml.BmlAccountClient
import sh.sar.basedbank.api.bml.BmlActivationResult import sh.sar.basedbank.api.bml.BmlActivationResult
import sh.sar.basedbank.api.bml.BmlLoginFlow 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.FahipayAccountClient import sh.sar.basedbank.api.fahipay.FahipayAccountClient
import sh.sar.basedbank.api.fahipay.FahipayLoginFlow import sh.sar.basedbank.api.fahipay.FahipayLoginFlow
import sh.sar.basedbank.api.fahipay.FahipaySession import sh.sar.basedbank.api.fahipay.FahipaySession
@@ -34,8 +32,6 @@ import sh.sar.basedbank.util.CredentialStore
import sh.sar.basedbank.databinding.FragmentCredentialsBinding import sh.sar.basedbank.databinding.FragmentCredentialsBinding
import sh.sar.basedbank.ui.home.HomeActivity import sh.sar.basedbank.ui.home.HomeActivity
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlin.coroutines.resume
import kotlinx.coroutines.suspendCancellableCoroutine
class CredentialsFragment : Fragment() { class CredentialsFragment : Fragment() {
@@ -60,10 +56,6 @@ class CredentialsFragment : Fragment() {
private var bmlFlow: BmlLoginFlow? = null private var bmlFlow: BmlLoginFlow? = null
private var bmlLoginId: String = "" private var bmlLoginId: String = ""
private var bmlAccumulatedAccounts = mutableListOf<BankAccount>() private var bmlAccumulatedAccounts = mutableListOf<BankAccount>()
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 { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_binding = FragmentCredentialsBinding.inflate(inflater, container, false) _binding = FragmentCredentialsBinding.inflate(inflater, container, false)
@@ -170,11 +162,7 @@ class CredentialsFragment : Fragment() {
private fun attemptLogin() { private fun attemptLogin() {
when (bankType) { when (bankType) {
"BML" -> { "BML" -> { attemptBmlLogin(); return }
if (bmlAwaitingBusinessOtp) submitBmlBusinessOtp()
else attemptBmlLogin()
return
}
"FAHIPAY" -> { attemptFahipayLogin(); return } "FAHIPAY" -> { attemptFahipayLogin(); return }
} }
@@ -257,10 +245,6 @@ class CredentialsFragment : Fragment() {
bmlLoginId = username bmlLoginId = username
bmlAccumulatedAccounts.clear() bmlAccumulatedAccounts.clear()
bmlPendingBusinessProfiles.clear()
bmlCurrentBusinessProfile = null
bmlSelectedChannel = null
bmlAwaitingBusinessOtp = false
val flow = BmlLoginFlow().also { bmlFlow = it } val flow = BmlLoginFlow().also { bmlFlow = it }
val loginTag = "bml_$username" val loginTag = "bml_$username"
@@ -272,24 +256,22 @@ class CredentialsFragment : Fragment() {
} }
if (profiles.isEmpty()) throw Exception("No profiles found for this account") if (profiles.isEmpty()) throw Exception("No profiles found for this account")
// Activate each profile; personal profiles are immediate, business ones need OTP var hasBusinessProfiles = false
for (profile in profiles) { for (profile in profiles) {
if (profile.profileType == "business") {
hasBusinessProfiles = true
continue // skip — user can enable in Settings → Logins
}
val result = withContext(Dispatchers.IO) { flow.activateProfile(profile, loginTag) } val result = withContext(Dispatchers.IO) { flow.activateProfile(profile, loginTag) }
when (result) { if (result is BmlActivationResult.Success) {
is BmlActivationResult.Success -> { bmlAccumulatedAccounts += result.accounts
bmlAccumulatedAccounts += result.accounts val store = CredentialStore(requireContext())
val store = CredentialStore(requireContext()) store.saveBmlProfileSession(profile.profileId, result.session.accessToken, result.session.deviceId)
store.saveBmlProfileSession(profile.profileId, result.session.accessToken, result.session.deviceId) val app = requireActivity().application as BasedBankApp
val app = requireActivity().application as BasedBankApp app.bmlSessions[profile.profileId] = result.session
app.bmlSessions[profile.profileId] = result.session
}
is BmlActivationResult.NeedsBusinessOtp -> {
bmlPendingBusinessProfiles.addLast(Pair(profile, result.channels))
}
} }
} }
// Save credentials and profile list now (before business OTP prompts)
val store = CredentialStore(requireContext()) val store = CredentialStore(requireContext())
store.saveBmlCredentials(bmlLoginId, username, password, otpSeed) store.saveBmlCredentials(bmlLoginId, username, password, otpSeed)
store.saveBmlProfiles(bmlLoginId, profiles) store.saveBmlProfiles(bmlLoginId, profiles)
@@ -300,11 +282,7 @@ class CredentialsFragment : Fragment() {
binding.progressBar.visibility = View.GONE binding.progressBar.visibility = View.GONE
binding.btnLogin.isEnabled = true binding.btnLogin.isEnabled = true
if (bmlPendingBusinessProfiles.isNotEmpty()) { finishBmlLogin(hasBusinessProfiles)
processNextBmlBusinessProfile()
} else {
finishBmlLogin()
}
} catch (e: Exception) { } catch (e: Exception) {
binding.tvError.text = e.message ?: "Login failed" binding.tvError.text = e.message ?: "Login failed"
binding.tvError.visibility = View.VISIBLE binding.tvError.visibility = View.VISIBLE
@@ -314,115 +292,7 @@ class CredentialsFragment : Fragment() {
} }
} }
private suspend fun processNextBmlBusinessProfile() { private suspend fun finishBmlLogin(hasBusinessProfiles: Boolean = false) {
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 store = CredentialStore(requireContext())
val accounts = bmlAccumulatedAccounts.toList() val accounts = bmlAccumulatedAccounts.toList()
@@ -479,6 +349,9 @@ class CredentialsFragment : Fragment() {
app.bmlAccounts = app.bmlAccounts.filter { it.loginTag != "bml_$bmlLoginId" } + accounts app.bmlAccounts = app.bmlAccounts.filter { it.loginTag != "bml_$bmlLoginId" } + accounts
app.accounts = app.accounts.filter { it.loginTag != "bml_$bmlLoginId" } + accounts app.accounts = app.accounts.filter { it.loginTag != "bml_$bmlLoginId" } + accounts
if (hasBusinessProfiles) {
Toast.makeText(requireContext(), "Business profiles can be enabled in Settings → Logins", Toast.LENGTH_LONG).show()
}
val intent = Intent(requireContext(), HomeActivity::class.java) val intent = Intent(requireContext(), HomeActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK
startActivity(intent) startActivity(intent)
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="?attr/colorOnSurfaceVariant"
android:pathData="M20,4H4C2.9,4 2,4.9 2,6v12c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2V6c0,-1.1 -0.9,-2 -2,-2zM20,8l-8,5 -8,-5V6l8,5 8,-5v2z"/>
</vector>
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="?attr/colorOnSurfaceVariant"
android:pathData="M20,2H4C2.9,2 2,2.9 2,4v18l4,-4h14c1.1,0 2,-0.9 2,-2V4c0,-1.1 -0.9,-2 -2,-2zM9,11H7V9h2v2zM13,11h-2V9h2v2zM17,11h-2V9h2v2z"/>
</vector>
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="32"
android:viewportHeight="32">
<path
android:fillColor="?attr/colorOnSurfaceVariant"
android:pathData="M26.576 5.363c-2.69-2.69-6.406-4.354-10.511-4.354-8.209 0-14.865 6.655-14.865 14.865 0 2.732 0.737 5.291 2.022 7.491l-0.038-0.070-2.109 7.702 7.879-2.067c2.051 1.139 4.498 1.809 7.102 1.809h0.006c8.209-0.003 14.862-6.659 14.862-14.868 0-4.103-1.662-7.817-4.349-10.507zM16.062 28.228h-0.005c-2.319 0-4.489-0.64-6.342-1.753l0.056 0.031-0.451-0.267-4.675 1.227 1.247-4.559-0.294-0.467c-1.185-1.862-1.889-4.131-1.889-6.565 0-6.822 5.531-12.353 12.353-12.353s12.353 5.531 12.353 12.353c0 6.822-5.53 12.353-12.353 12.353zM22.838 18.977c-0.371-0.186-2.197-1.083-2.537-1.208-0.341-0.124-0.589-0.185-0.837 0.187-0.246 0.371-0.958 1.207-1.175 1.455-0.216 0.249-0.434 0.279-0.805 0.094-1.15-0.466-2.138-1.087-2.997-1.852l0.010 0.009c-0.799-0.74-1.484-1.587-2.037-2.521l-0.028-0.052c-0.216-0.371-0.023-0.572 0.162-0.757 0.167-0.166 0.372-0.434 0.557-0.65 0.146-0.179 0.271-0.384 0.366-0.604l0.006-0.017c0.043-0.087 0.068-0.188 0.068-0.296 0-0.131-0.037-0.253-0.101-0.357l0.002 0.003c-0.094-0.186-0.836-2.014-1.145-2.758-0.302-0.724-0.609-0.625-0.836-0.637-0.216-0.010-0.464-0.012-0.712-0.012-0.395 0.010-0.746 0.188-0.988 0.463l-0.001 0.002c-0.802 0.761-1.3 1.834-1.3 3.023 0 0.026 0 0.053 0.001 0.079c0.131 1.467 0.681 2.784 1.527 3.857l-0.012-0.015c1.604 2.379 3.742 4.282 6.251 5.564l0.094 0.043c0.548 0.248 1.25 0.513 1.968 0.74l0.149 0.041c0.442 0.14 0.951 0.221 1.479 0.221 0.303 0 0.601-0.027 0.889-0.078l-0.031 0.004c1.069-0.223 1.956-0.868 2.497-1.749l0.009-0.017c0.165-0.366 0.261-0.793 0.261-1.242 0-0.185-0.016-0.366-0.047-0.542l0.003 0.019c-0.092-0.155-0.34-0.247-0.712-0.434z"/>
</vector>
-3
View File
@@ -175,10 +175,7 @@
<string name="verify">Verify</string> <string name="verify">Verify</string>
<!-- BML business OTP --> <!-- BML business OTP -->
<string name="bml_business_otp_title">Activate %s profile</string>
<string name="bml_business_otp_hint">%s OTP code</string>
<string name="bml_business_otp_sent">OTP sent via %s</string> <string name="bml_business_otp_sent">OTP sent via %s</string>
<string name="bml_business_otp_skip">Skip</string>
<!-- Home --> <!-- Home -->
<string name="transfer_same_account">This is your source account</string> <string name="transfer_same_account">This is your source account</string>
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg fill="#000000" width="800px" height="800px" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg">
<title>whatsapp</title>
<path d="M26.576 5.363c-2.69-2.69-6.406-4.354-10.511-4.354-8.209 0-14.865 6.655-14.865 14.865 0 2.732 0.737 5.291 2.022 7.491l-0.038-0.070-2.109 7.702 7.879-2.067c2.051 1.139 4.498 1.809 7.102 1.809h0.006c8.209-0.003 14.862-6.659 14.862-14.868 0-4.103-1.662-7.817-4.349-10.507l0 0zM16.062 28.228h-0.005c-0 0-0.001 0-0.001 0-2.319 0-4.489-0.64-6.342-1.753l0.056 0.031-0.451-0.267-4.675 1.227 1.247-4.559-0.294-0.467c-1.185-1.862-1.889-4.131-1.889-6.565 0-6.822 5.531-12.353 12.353-12.353s12.353 5.531 12.353 12.353c0 6.822-5.53 12.353-12.353 12.353h-0zM22.838 18.977c-0.371-0.186-2.197-1.083-2.537-1.208-0.341-0.124-0.589-0.185-0.837 0.187-0.246 0.371-0.958 1.207-1.175 1.455-0.216 0.249-0.434 0.279-0.805 0.094-1.15-0.466-2.138-1.087-2.997-1.852l0.010 0.009c-0.799-0.74-1.484-1.587-2.037-2.521l-0.028-0.052c-0.216-0.371-0.023-0.572 0.162-0.757 0.167-0.166 0.372-0.434 0.557-0.65 0.146-0.179 0.271-0.384 0.366-0.604l0.006-0.017c0.043-0.087 0.068-0.188 0.068-0.296 0-0.131-0.037-0.253-0.101-0.357l0.002 0.003c-0.094-0.186-0.836-2.014-1.145-2.758-0.302-0.724-0.609-0.625-0.836-0.637-0.216-0.010-0.464-0.012-0.712-0.012-0.395 0.010-0.746 0.188-0.988 0.463l-0.001 0.002c-0.802 0.761-1.3 1.834-1.3 3.023 0 0.026 0 0.053 0.001 0.079l-0-0.004c0.131 1.467 0.681 2.784 1.527 3.857l-0.012-0.015c1.604 2.379 3.742 4.282 6.251 5.564l0.094 0.043c0.548 0.248 1.25 0.513 1.968 0.74l0.149 0.041c0.442 0.14 0.951 0.221 1.479 0.221 0.303 0 0.601-0.027 0.889-0.078l-0.031 0.004c1.069-0.223 1.956-0.868 2.497-1.749l0.009-0.017c0.165-0.366 0.261-0.793 0.261-1.242 0-0.185-0.016-0.366-0.047-0.542l0.003 0.019c-0.092-0.155-0.34-0.247-0.712-0.434z"></path>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

+1
View File
@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>WhatsApp</title><path d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 00-3.48-8.413Z"/></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB