Optmize business profile logins.. save privacy mode settings on launch
This commit is contained in:
@@ -194,8 +194,7 @@ class HomeActivity : AppCompatActivity() {
|
||||
autoRefresh(store)
|
||||
}
|
||||
|
||||
// hideAmounts is always false on launch; eye feature just needs to be enabled
|
||||
viewModel.hideAmounts.value = false
|
||||
viewModel.hideAmounts.value = getSharedPreferences("prefs", MODE_PRIVATE).getBoolean("hide_amounts", false)
|
||||
|
||||
// Show dashboard on first create
|
||||
if (savedInstanceState == null) {
|
||||
@@ -434,6 +433,7 @@ fun applyNavLabelVisibility() {
|
||||
if (item.itemId == R.id.action_hide_amounts) {
|
||||
val newHidden = !(viewModel.hideAmounts.value ?: false)
|
||||
viewModel.hideAmounts.value = newHidden
|
||||
getSharedPreferences("prefs", MODE_PRIVATE).edit().putBoolean("hide_amounts", newHidden).apply()
|
||||
invalidateOptionsMenu()
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.materialswitch.MaterialSwitch
|
||||
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.ForeignLimitsCache
|
||||
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() {
|
||||
|
||||
private var _binding: FragmentSettingsLoginsBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
private val viewModel: HomeViewModel by activityViewModels()
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
_binding = FragmentSettingsLoginsBinding.inflate(inflater, container, false)
|
||||
@@ -89,13 +100,15 @@ class SettingsLoginsFragment : Fragment() {
|
||||
val profile = store.loadFahipayUserProfile(loginId)
|
||||
val displayName = profile?.fullName?.takeIf { it.isNotBlank() } ?: getString(R.string.fahipay_name)
|
||||
addLoginRow(container, R.drawable.fahipay_logo, displayName) {
|
||||
val hide = viewModel.hideAmounts.value ?: false
|
||||
val masked = "••••••"
|
||||
showLoginDetails(
|
||||
title = getString(R.string.fahipay_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?.nid.isNullOrBlank()) appendLine("${getString(R.string.login_detail_id_card)}: ${profile!!.nid}")
|
||||
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)}: ${if (hide) masked else profile!!.mobile}")
|
||||
if (!profile?.nid.isNullOrBlank()) appendLine("${getString(R.string.login_detail_id_card)}: ${if (hide) masked else profile!!.nid}")
|
||||
}.trim(),
|
||||
onLogout = { confirmLogout(getString(R.string.fahipay_name)) { logoutFahipay(store, loginId) } }
|
||||
)
|
||||
@@ -258,8 +271,16 @@ class SettingsLoginsFragment : Fragment() {
|
||||
) {
|
||||
val ctx = requireContext()
|
||||
val dp = ctx.resources.displayMetrics.density
|
||||
val originalHidden = store.getHiddenBmlProfileIds(loginId)
|
||||
val hidden = originalHidden.toMutableSet()
|
||||
val hidden = store.getHiddenBmlProfileIds(loginId).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 container = LinearLayout(ctx).apply {
|
||||
@@ -269,12 +290,14 @@ class SettingsLoginsFragment : Fragment() {
|
||||
}
|
||||
scroll.addView(container)
|
||||
|
||||
val hide = viewModel.hideAmounts.value ?: false
|
||||
val masked = "••••••"
|
||||
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" }
|
||||
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)}: ${if (hide) masked else 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)}: ${if (hide) masked else it}" }
|
||||
).forEach { line ->
|
||||
container.addView(TextView(ctx).apply {
|
||||
text = line
|
||||
@@ -357,8 +380,19 @@ class SettingsLoginsFragment : Fragment() {
|
||||
|
||||
toggleRows.forEach { (p, toggle) ->
|
||||
toggle.setOnCheckedChangeListener { _, checked ->
|
||||
if (checked) hidden.remove(p.profileId) else hidden.add(p.profileId)
|
||||
updateToggleStates(saveBtn)
|
||||
if (checked && p.profileId in needsActivation) {
|
||||
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)
|
||||
clearAllCaches(ctx)
|
||||
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.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.FahipayAccountClient
|
||||
import sh.sar.basedbank.api.fahipay.FahipayLoginFlow
|
||||
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.ui.home.HomeActivity
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import kotlin.coroutines.resume
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
|
||||
class CredentialsFragment : Fragment() {
|
||||
|
||||
@@ -60,10 +56,6 @@ class CredentialsFragment : Fragment() {
|
||||
private var bmlFlow: BmlLoginFlow? = null
|
||||
private var bmlLoginId: String = ""
|
||||
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 {
|
||||
_binding = FragmentCredentialsBinding.inflate(inflater, container, false)
|
||||
@@ -170,11 +162,7 @@ class CredentialsFragment : Fragment() {
|
||||
|
||||
private fun attemptLogin() {
|
||||
when (bankType) {
|
||||
"BML" -> {
|
||||
if (bmlAwaitingBusinessOtp) submitBmlBusinessOtp()
|
||||
else attemptBmlLogin()
|
||||
return
|
||||
}
|
||||
"BML" -> { attemptBmlLogin(); return }
|
||||
"FAHIPAY" -> { attemptFahipayLogin(); return }
|
||||
}
|
||||
|
||||
@@ -257,10 +245,6 @@ class CredentialsFragment : Fragment() {
|
||||
|
||||
bmlLoginId = username
|
||||
bmlAccumulatedAccounts.clear()
|
||||
bmlPendingBusinessProfiles.clear()
|
||||
bmlCurrentBusinessProfile = null
|
||||
bmlSelectedChannel = null
|
||||
bmlAwaitingBusinessOtp = false
|
||||
|
||||
val flow = BmlLoginFlow().also { bmlFlow = it }
|
||||
val loginTag = "bml_$username"
|
||||
@@ -272,24 +256,22 @@ class CredentialsFragment : Fragment() {
|
||||
}
|
||||
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) {
|
||||
if (profile.profileType == "business") {
|
||||
hasBusinessProfiles = true
|
||||
continue // skip — user can enable in Settings → Logins
|
||||
}
|
||||
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))
|
||||
}
|
||||
if (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
|
||||
}
|
||||
}
|
||||
|
||||
// Save credentials and profile list now (before business OTP prompts)
|
||||
val store = CredentialStore(requireContext())
|
||||
store.saveBmlCredentials(bmlLoginId, username, password, otpSeed)
|
||||
store.saveBmlProfiles(bmlLoginId, profiles)
|
||||
@@ -300,11 +282,7 @@ class CredentialsFragment : Fragment() {
|
||||
binding.progressBar.visibility = View.GONE
|
||||
binding.btnLogin.isEnabled = true
|
||||
|
||||
if (bmlPendingBusinessProfiles.isNotEmpty()) {
|
||||
processNextBmlBusinessProfile()
|
||||
} else {
|
||||
finishBmlLogin()
|
||||
}
|
||||
finishBmlLogin(hasBusinessProfiles)
|
||||
} catch (e: Exception) {
|
||||
binding.tvError.text = e.message ?: "Login failed"
|
||||
binding.tvError.visibility = View.VISIBLE
|
||||
@@ -314,115 +292,7 @@ class CredentialsFragment : Fragment() {
|
||||
}
|
||||
}
|
||||
|
||||
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() {
|
||||
private suspend fun finishBmlLogin(hasBusinessProfiles: Boolean = false) {
|
||||
val store = CredentialStore(requireContext())
|
||||
val accounts = bmlAccumulatedAccounts.toList()
|
||||
|
||||
@@ -479,6 +349,9 @@ class CredentialsFragment : Fragment() {
|
||||
app.bmlAccounts = app.bmlAccounts.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)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
startActivity(intent)
|
||||
|
||||
10
app/src/main/res/drawable/ic_channel_email.xml
Normal file
10
app/src/main/res/drawable/ic_channel_email.xml
Normal file
@@ -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>
|
||||
10
app/src/main/res/drawable/ic_channel_sms.xml
Normal file
10
app/src/main/res/drawable/ic_channel_sms.xml
Normal file
@@ -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>
|
||||
10
app/src/main/res/drawable/ic_channel_whatsapp.xml
Normal file
10
app/src/main/res/drawable/ic_channel_whatsapp.xml
Normal file
@@ -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>
|
||||
@@ -175,10 +175,7 @@
|
||||
<string name="verify">Verify</string>
|
||||
|
||||
<!-- 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_skip">Skip</string>
|
||||
|
||||
<!-- Home -->
|
||||
<string name="transfer_same_account">This is your source account</string>
|
||||
|
||||
6
whatsapp-svgrepo-com.svg
Normal file
6
whatsapp-svgrepo-com.svg
Normal 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
whatsapp.svg
Normal file
1
whatsapp.svg
Normal 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 |
Reference in New Issue
Block a user