split do bml tranfers
This commit is contained in:
@@ -38,17 +38,10 @@ import kotlinx.coroutines.launch
|
|||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import sh.sar.basedbank.BasedBankApp
|
import sh.sar.basedbank.BasedBankApp
|
||||||
import sh.sar.basedbank.R
|
import sh.sar.basedbank.R
|
||||||
import sh.sar.basedbank.api.bml.BmlAccountClient
|
|
||||||
import sh.sar.basedbank.api.bml.BmlOtpChannel
|
|
||||||
import sh.sar.basedbank.api.bml.BmlQrPayClient
|
|
||||||
import sh.sar.basedbank.api.bml.BmlQrPayInfo
|
|
||||||
import sh.sar.basedbank.api.bml.BmlTransferClient
|
|
||||||
import sh.sar.basedbank.api.bml.BmlTransferResult
|
|
||||||
import sh.sar.basedbank.api.bml.BmlValidateClient
|
import sh.sar.basedbank.api.bml.BmlValidateClient
|
||||||
import sh.sar.basedbank.api.dhiraagu.DhiraaguClient
|
import sh.sar.basedbank.api.dhiraagu.DhiraaguClient
|
||||||
import sh.sar.basedbank.api.fahipay.OoredooClient
|
import sh.sar.basedbank.api.fahipay.OoredooClient
|
||||||
import sh.sar.basedbank.api.models.BankAccount
|
import sh.sar.basedbank.api.models.BankAccount
|
||||||
import sh.sar.basedbank.api.models.BankContact
|
|
||||||
import sh.sar.basedbank.api.mib.MibContactsClient
|
import sh.sar.basedbank.api.mib.MibContactsClient
|
||||||
import sh.sar.basedbank.api.mib.MibIpsAccountInfo
|
import sh.sar.basedbank.api.mib.MibIpsAccountInfo
|
||||||
import sh.sar.basedbank.api.mib.MibLookupException
|
import sh.sar.basedbank.api.mib.MibLookupException
|
||||||
@@ -57,6 +50,7 @@ import sh.sar.basedbank.api.mib.MibTransferResult
|
|||||||
import sh.sar.basedbank.databinding.FragmentTransferBinding
|
import sh.sar.basedbank.databinding.FragmentTransferBinding
|
||||||
import sh.sar.basedbank.databinding.ItemAccountDropdownBinding
|
import sh.sar.basedbank.databinding.ItemAccountDropdownBinding
|
||||||
import sh.sar.basedbank.databinding.ItemPickerSectionHeaderBinding
|
import sh.sar.basedbank.databinding.ItemPickerSectionHeaderBinding
|
||||||
|
import sh.sar.basedbank.ui.home.transfer.BmlTransferHandler
|
||||||
import sh.sar.basedbank.ui.home.transfer.MfaisaTransferHandler
|
import sh.sar.basedbank.ui.home.transfer.MfaisaTransferHandler
|
||||||
import sh.sar.basedbank.util.AccountListParser
|
import sh.sar.basedbank.util.AccountListParser
|
||||||
import sh.sar.basedbank.util.CredentialStore
|
import sh.sar.basedbank.util.CredentialStore
|
||||||
@@ -79,9 +73,7 @@ class TransferFragment : Fragment() {
|
|||||||
private val session get() = selectedAccount
|
private val session get() = selectedAccount
|
||||||
?.let { (requireActivity().application as BasedBankApp).mibSessionFor(it) }
|
?.let { (requireActivity().application as BasedBankApp).mibSessionFor(it) }
|
||||||
?: (requireActivity().application as BasedBankApp).anyMibSession()
|
?: (requireActivity().application as BasedBankApp).anyMibSession()
|
||||||
private fun bmlSessionFor(account: BankAccount?) =
|
private fun bmlSessionFor(account: BankAccount?) = bmlHandler().sessionFor(account)
|
||||||
account?.let { (requireActivity().application as BasedBankApp).bmlSessionFor(it) }
|
|
||||||
?: (requireActivity().application as BasedBankApp).anyBmlSession()
|
|
||||||
|
|
||||||
// Resolved recipient info — set after successful lookup or prefill
|
// Resolved recipient info — set after successful lookup or prefill
|
||||||
private var resolvedAccountNumber = ""
|
private var resolvedAccountNumber = ""
|
||||||
@@ -102,39 +94,41 @@ class TransferFragment : Fragment() {
|
|||||||
private var savedToColorHex = "#607D8B"
|
private var savedToColorHex = "#607D8B"
|
||||||
private var savedToImageHash: String? = null
|
private var savedToImageHash: String? = null
|
||||||
|
|
||||||
// BML QR merchant payment mode (set when navigated from a card QR scan)
|
|
||||||
private var bmlQrInfo: BmlQrPayInfo? = null
|
|
||||||
private var bmlGatewayQr = false // true for pay.bml.com.mv QRs (requires pre-initiate step)
|
|
||||||
private var bmlQrLookupAttempted = false // prevents re-lookup after user clears the merchant
|
|
||||||
|
|
||||||
// M-Faisa QR merchant payment mode (set when the scanned QR is an M-Faisa qrCodeId)
|
// M-Faisa QR merchant payment mode (set when the scanned QR is an M-Faisa qrCodeId)
|
||||||
private var mfaisaQrInfo: sh.sar.basedbank.api.mfaisa.MfaisaQrPayClient.QrMerchant? = null
|
private var mfaisaQrInfo: sh.sar.basedbank.api.mfaisa.MfaisaQrPayClient.QrMerchant? = null
|
||||||
|
|
||||||
private val dropdownProfileImageCache = mutableMapOf<String, Bitmap>()
|
private val dropdownProfileImageCache = mutableMapOf<String, Bitmap>()
|
||||||
|
|
||||||
// BML business profile OTP flow state
|
|
||||||
private enum class BmlOtpState { NONE, SELECTING_CHANNEL, AWAITING_OTP }
|
|
||||||
private var bmlOtpState = BmlOtpState.NONE
|
|
||||||
private var bmlOtpChannel: String? = null
|
|
||||||
|
|
||||||
private data class PendingBmlTransfer(
|
|
||||||
val src: BankAccount,
|
|
||||||
val debitAccount: String,
|
|
||||||
val creditAccount: String,
|
|
||||||
val amount: Double,
|
|
||||||
val amountStr: String,
|
|
||||||
val remarks: String,
|
|
||||||
val transferType: String,
|
|
||||||
val currency: String,
|
|
||||||
val bank: String?,
|
|
||||||
val destDisplay: String,
|
|
||||||
val destAccount: String,
|
|
||||||
val toBank: String,
|
|
||||||
val toAvatar: Bitmap?
|
|
||||||
)
|
|
||||||
private var pendingBmlTransfer: PendingBmlTransfer? = null
|
|
||||||
private var accountDropdownAdapter: AccountDropdownAdapter? = null
|
private var accountDropdownAdapter: AccountDropdownAdapter? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Owns everything BML-specific: sessions, the transfer itself, the business-profile OTP
|
||||||
|
* flow and merchant QR pay. Created lazily on first use and dropped in [onDestroyView]
|
||||||
|
* because it captures the view binding.
|
||||||
|
*/
|
||||||
|
private var bmlHandler: BmlTransferHandler? = null
|
||||||
|
private fun bmlHandler(): BmlTransferHandler =
|
||||||
|
bmlHandler ?: BmlTransferHandler(
|
||||||
|
fragment = this,
|
||||||
|
binding = binding,
|
||||||
|
viewModel = viewModel,
|
||||||
|
currentSource = { selectedAccount },
|
||||||
|
selectSource = { account ->
|
||||||
|
selectedAccount = account
|
||||||
|
updateAmountPrefix(account)
|
||||||
|
showFromCard(account)
|
||||||
|
updateTransferButton()
|
||||||
|
},
|
||||||
|
onStateChanged = { updateTransferButton() },
|
||||||
|
onTransferSuccess = { receipt, avatar ->
|
||||||
|
ReceiptStore.save(requireContext(), receipt)
|
||||||
|
clearForm()
|
||||||
|
val activity = requireActivity() as HomeActivity
|
||||||
|
activity.triggerRefresh()
|
||||||
|
activity.showWithBackStack(TransferReceiptFragment.newInstance(receipt, avatar))
|
||||||
|
}
|
||||||
|
).also { bmlHandler = it }
|
||||||
|
|
||||||
/** Lazy: created the first time the user selects an MFAISA source account. */
|
/** Lazy: created the first time the user selects an MFAISA source account. */
|
||||||
private var mfaisaHandler: MfaisaTransferHandler? = null
|
private var mfaisaHandler: MfaisaTransferHandler? = null
|
||||||
private fun mfaisaHandler(): MfaisaTransferHandler =
|
private fun mfaisaHandler(): MfaisaTransferHandler =
|
||||||
@@ -305,7 +299,7 @@ class TransferFragment : Fragment() {
|
|||||||
childFragmentManager.setFragmentResultListener(ContactPickerSheetFragment.REQUEST_KEY, viewLifecycleOwner) { _, bundle ->
|
childFragmentManager.setFragmentResultListener(ContactPickerSheetFragment.REQUEST_KEY, viewLifecycleOwner) { _, bundle ->
|
||||||
val accountNumber = bundle.getString(ContactPickerSheetFragment.KEY_ACCOUNT_NUMBER) ?: return@setFragmentResultListener
|
val accountNumber = bundle.getString(ContactPickerSheetFragment.KEY_ACCOUNT_NUMBER) ?: return@setFragmentResultListener
|
||||||
if (accountNumber.startsWith("bmlqr:")) {
|
if (accountNumber.startsWith("bmlqr:")) {
|
||||||
lookupBmlQrMerchant(accountNumber.removePrefix("bmlqr:"))
|
bmlHandler().lookupQrMerchant(accountNumber.removePrefix("bmlqr:"))
|
||||||
return@setFragmentResultListener
|
return@setFragmentResultListener
|
||||||
}
|
}
|
||||||
if (accountNumber.startsWith("mfaisaqr:")) {
|
if (accountNumber.startsWith("mfaisaqr:")) {
|
||||||
@@ -353,7 +347,7 @@ class TransferFragment : Fragment() {
|
|||||||
|
|
||||||
binding.btnTransfer.isEnabled = false
|
binding.btnTransfer.isEnabled = false
|
||||||
binding.btnTransfer.setOnClickListener {
|
binding.btnTransfer.setOnClickListener {
|
||||||
if (bmlOtpState == BmlOtpState.AWAITING_OTP) verifyBmlOtp()
|
if (bmlHandler().isAwaitingOtp) bmlHandler().verifyOtp()
|
||||||
else initiateTransfer()
|
else initiateTransfer()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -372,7 +366,7 @@ class TransferFragment : Fragment() {
|
|||||||
arguments?.getString(ARG_AMOUNT_PREFILL)?.let { binding.etAmount.setText(it) }
|
arguments?.getString(ARG_AMOUNT_PREFILL)?.let { binding.etAmount.setText(it) }
|
||||||
arguments?.getString(ARG_REMARKS_PREFILL)?.let { binding.etRemarks.setText(it) }
|
arguments?.getString(ARG_REMARKS_PREFILL)?.let { binding.etRemarks.setText(it) }
|
||||||
|
|
||||||
arguments?.getString(ARG_BML_QR_URL)?.let { lookupBmlQrMerchant(it) }
|
arguments?.getString(ARG_BML_QR_URL)?.let { bmlHandler().lookupQrMerchant(it) }
|
||||||
|
|
||||||
if (arguments?.getBoolean(ARG_AUTO_SCAN, false) == true) {
|
if (arguments?.getBoolean(ARG_AUTO_SCAN, false) == true) {
|
||||||
qrLauncher.launch(Intent(requireContext(), QrScannerActivity::class.java))
|
qrLauncher.launch(Intent(requireContext(), QrScannerActivity::class.java))
|
||||||
@@ -406,85 +400,6 @@ class TransferFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun lookupBmlQrMerchant(qrUrl: String) {
|
|
||||||
bmlQrLookupAttempted = true
|
|
||||||
bmlGatewayQr = qrUrl.startsWith("https://pay.bml.com.mv/app/")
|
|
||||||
val base64Url = android.util.Base64.encodeToString(
|
|
||||||
qrUrl.toByteArray(Charsets.UTF_8), android.util.Base64.NO_WRAP)
|
|
||||||
val app = requireActivity().application as BasedBankApp
|
|
||||||
val session = app.anyBmlSession() ?: return
|
|
||||||
|
|
||||||
// Lock the "To" input row while loading
|
|
||||||
binding.tilTo.visibility = View.GONE
|
|
||||||
binding.btnPickContact.visibility = View.GONE
|
|
||||||
binding.btnScanQr.visibility = View.GONE
|
|
||||||
(activity as? HomeActivity)?.setRefreshing(true)
|
|
||||||
|
|
||||||
viewLifecycleOwner.lifecycleScope.launch {
|
|
||||||
val info = withContext(Dispatchers.IO) {
|
|
||||||
try { BmlQrPayClient().lookupPayRequest(session, base64Url) }
|
|
||||||
catch (_: Exception) { null }
|
|
||||||
}
|
|
||||||
(activity as? HomeActivity)?.setRefreshing(false)
|
|
||||||
if (info == null) {
|
|
||||||
Toast.makeText(requireContext(), R.string.bml_qr_lookup_failed, Toast.LENGTH_LONG).show()
|
|
||||||
requireActivity().onBackPressedDispatcher.onBackPressed()
|
|
||||||
return@launch
|
|
||||||
}
|
|
||||||
bmlQrInfo = info
|
|
||||||
if (info.amount == 0.0) {
|
|
||||||
RecentsCache.save(requireContext(), RecentPick(
|
|
||||||
accountNumber = "bmlqr:$qrUrl",
|
|
||||||
displayName = info.merchantName,
|
|
||||||
subtitle = info.merchantAddress.ifBlank { "BML Merchant" },
|
|
||||||
colorHex = "#0066A1",
|
|
||||||
imageHash = null,
|
|
||||||
isProfileImage = false
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Auto-select the user's default BML card if no card was pre-selected
|
|
||||||
if (selectedAccount == null) {
|
|
||||||
val defaultNum = CredentialStore(requireContext()).getDefaultCardAccountNumber()
|
|
||||||
if (defaultNum != null) {
|
|
||||||
val allAccounts = viewModel.accounts.value ?: emptyList()
|
|
||||||
val defaultCard = allAccounts.firstOrNull {
|
|
||||||
it.accountNumber == defaultNum &&
|
|
||||||
(it.profileType == "BML_PREPAID" || it.profileType == "BML_CREDIT" || it.profileType == "BML_DEBIT") &&
|
|
||||||
it.statusDesc.equals("Active", ignoreCase = true)
|
|
||||||
}
|
|
||||||
if (defaultCard != null) {
|
|
||||||
selectedAccount = defaultCard
|
|
||||||
updateAmountPrefix(defaultCard)
|
|
||||||
showFromCard(defaultCard)
|
|
||||||
updateTransferButton()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show merchant in the "To" card — clear button hidden (can't change recipient for QR)
|
|
||||||
binding.tvToAccountName.text = info.merchantName
|
|
||||||
binding.tvToBankBic.text = info.merchantAddress.ifBlank { "BML Merchant" }
|
|
||||||
binding.tvToAccountDetails.visibility = View.GONE
|
|
||||||
binding.tvToBalance.visibility = View.GONE
|
|
||||||
binding.ivToPhoto.scaleType = android.widget.ImageView.ScaleType.CENTER_CROP
|
|
||||||
binding.ivToPhoto.setImageBitmap(makeInitialsBitmap(info.merchantName, "#0066A1"))
|
|
||||||
binding.cardToInfo.visibility = View.VISIBLE
|
|
||||||
|
|
||||||
// Pre-fill amount if dynamic QR
|
|
||||||
if (info.amount > 0.0) {
|
|
||||||
binding.etAmount.setText("%.2f".format(info.amount))
|
|
||||||
binding.tilAmount.isEnabled = false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remarks not applicable for merchant QR payments
|
|
||||||
binding.tilRemarks.isEnabled = false
|
|
||||||
binding.tilRemarks.alpha = 0.4f
|
|
||||||
|
|
||||||
updateTransferButton()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolves an M-Faisa qrCodeId to a merchant, paints it in the "To" card, auto-selects an
|
* Resolves an M-Faisa qrCodeId to a merchant, paints it in the "To" card, auto-selects an
|
||||||
* M-Faisa source if none is selected (or the current one is the wrong bank), and pre-fills
|
* M-Faisa source if none is selected (or the current one is the wrong bank), and pre-fills
|
||||||
@@ -617,7 +532,7 @@ class TransferFragment : Fragment() {
|
|||||||
|
|
||||||
binding.actvFrom.setOnItemClickListener { _, _, position, _ ->
|
binding.actvFrom.setOnItemClickListener { _, _, position, _ ->
|
||||||
val picked = accountDropdownAdapter?.getAccount(position) ?: return@setOnItemClickListener
|
val picked = accountDropdownAdapter?.getAccount(position) ?: return@setOnItemClickListener
|
||||||
if (bmlQrInfo != null) {
|
if (bmlHandler().hasQrMerchant) {
|
||||||
val isCard = picked.profileType == "BML_PREPAID" || picked.profileType == "BML_CREDIT" || picked.profileType == "BML_DEBIT"
|
val isCard = picked.profileType == "BML_PREPAID" || picked.profileType == "BML_CREDIT" || picked.profileType == "BML_DEBIT"
|
||||||
if (!isCard) {
|
if (!isCard) {
|
||||||
Toast.makeText(requireContext(), "Unsupported for BML QR — select a card", Toast.LENGTH_SHORT).show()
|
Toast.makeText(requireContext(), "Unsupported for BML QR — select a card", Toast.LENGTH_SHORT).show()
|
||||||
@@ -664,9 +579,9 @@ class TransferFragment : Fragment() {
|
|||||||
// On a cold start (e.g. share intent), anyBmlSession() may be null when
|
// On a cold start (e.g. share intent), anyBmlSession() may be null when
|
||||||
// onViewCreated runs. Retry the lookup once sessions are available.
|
// onViewCreated runs. Retry the lookup once sessions are available.
|
||||||
val pendingBmlQrUrl = arguments?.getString(ARG_BML_QR_URL)
|
val pendingBmlQrUrl = arguments?.getString(ARG_BML_QR_URL)
|
||||||
if (pendingBmlQrUrl != null && !bmlQrLookupAttempted) {
|
if (pendingBmlQrUrl != null && !bmlHandler().qrLookupAttempted) {
|
||||||
val app = requireActivity().application as BasedBankApp
|
val app = requireActivity().application as BasedBankApp
|
||||||
if (app.anyBmlSession() != null) lookupBmlQrMerchant(pendingBmlQrUrl)
|
if (app.anyBmlSession() != null) bmlHandler().lookupQrMerchant(pendingBmlQrUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Re-render the from card when the view is recreated on a cached instance
|
// Re-render the from card when the view is recreated on a cached instance
|
||||||
@@ -878,14 +793,7 @@ class TransferFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
binding.btnClearToInfo.setOnClickListener {
|
binding.btnClearToInfo.setOnClickListener {
|
||||||
if (bmlQrInfo != null) {
|
bmlHandler().clearQrMerchant()
|
||||||
bmlQrInfo = null
|
|
||||||
bmlGatewayQr = false
|
|
||||||
binding.tilAmount.isEnabled = true
|
|
||||||
binding.tilRemarks.isEnabled = true
|
|
||||||
binding.tilRemarks.alpha = 1f
|
|
||||||
binding.etAmount.setText("")
|
|
||||||
}
|
|
||||||
if (mfaisaQrInfo != null) {
|
if (mfaisaQrInfo != null) {
|
||||||
mfaisaQrInfo = null
|
mfaisaQrInfo = null
|
||||||
binding.tilAmount.isEnabled = true
|
binding.tilAmount.isEnabled = true
|
||||||
@@ -1265,7 +1173,7 @@ class TransferFragment : Fragment() {
|
|||||||
// ── Transfer ──────────────────────────────────────────────────────────────
|
// ── Transfer ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/** Shared confirm dialog with optional biometric gate, used for both transfers and QR payments. */
|
/** Shared confirm dialog with optional biometric gate, used for both transfers and QR payments. */
|
||||||
private fun showConfirmWithBiometric(
|
internal fun showConfirmWithBiometric(
|
||||||
title: String,
|
title: String,
|
||||||
message: String? = null,
|
message: String? = null,
|
||||||
customView: android.view.View? = null,
|
customView: android.view.View? = null,
|
||||||
@@ -1367,41 +1275,8 @@ class TransferFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// BML QR merchant payment — uses shared confirm dialog, no receipt
|
// BML QR merchant payment — uses shared confirm dialog, no receipt
|
||||||
bmlQrInfo?.let { info ->
|
if (bmlHandler().hasQrMerchant) {
|
||||||
val src = selectedAccount ?: run {
|
bmlHandler().submitQrPayment()
|
||||||
Toast.makeText(requireContext(), R.string.transfer_session_unavailable, Toast.LENGTH_SHORT).show()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
val amountStr = binding.etAmount.text?.toString()?.trim() ?: ""
|
|
||||||
val amount = amountStr.toDoubleOrNull()
|
|
||||||
if (amount == null || amount <= 0) { binding.tilAmount.error = "Enter a valid amount"; return }
|
|
||||||
binding.tilAmount.error = null
|
|
||||||
val debitAccount = src.internalId.ifBlank {
|
|
||||||
Toast.makeText(requireContext(), R.string.transfer_missing_internal_id, Toast.LENGTH_SHORT).show()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
val qrFromTypeLabel = AccountListParser.from(src)?.typeLabel
|
|
||||||
?: BmlDashboardParser.productLabel(src.accountTypeName)
|
|
||||||
val qrFromDetail = listOfNotNull("BML", qrFromTypeLabel.ifBlank { null }).joinToString(" · ")
|
|
||||||
val qrConfirmView = buildTransferConfirmView(
|
|
||||||
amountCurrency = info.currency,
|
|
||||||
amountValue = "%.2f".format(amount),
|
|
||||||
fromName = src.accountBriefName,
|
|
||||||
fromNumber = src.accountNumber,
|
|
||||||
fromDetail = qrFromDetail,
|
|
||||||
toName = info.merchantName,
|
|
||||||
toNumber = "",
|
|
||||||
toDetail = info.merchantAddress.ifBlank { "BML Merchant" }
|
|
||||||
)
|
|
||||||
showConfirmWithBiometric(
|
|
||||||
title = getString(R.string.transfer),
|
|
||||||
customView = qrConfirmView,
|
|
||||||
biometricSubtitle = "${info.currency} ${"%.2f".format(amount)} → ${info.merchantName}",
|
|
||||||
onConfirmed = { dialog, frame ->
|
|
||||||
showProcessingInDialog(dialog, frame)
|
|
||||||
executeBmlQrPayment(src, debitAccount, info, amount, dialog, frame)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1423,7 +1298,7 @@ class TransferFragment : Fragment() {
|
|||||||
val remarks = binding.etRemarks.text?.toString()?.trim() ?: ""
|
val remarks = binding.etRemarks.text?.toString()?.trim() ?: ""
|
||||||
|
|
||||||
val isSrcBml = src.bank == "BML"
|
val isSrcBml = src.bank == "BML"
|
||||||
val isBmlBusiness = isSrcBml && isBusinessProfile(src) // to test on personal accounts: use `isSrcBml`
|
val isBmlBusiness = isSrcBml && bmlHandler().isBusinessProfile(src) // to test on personal accounts: use `isSrcBml`
|
||||||
val isSrcCard = src.profileType == "BML_PREPAID" || src.profileType == "BML_CREDIT" || src.profileType == "BML_DEBIT"
|
val isSrcCard = src.profileType == "BML_PREPAID" || src.profileType == "BML_CREDIT" || src.profileType == "BML_DEBIT"
|
||||||
val isDestMib = AccountInputParser.detect(resolvedAccountNumber) == AccountInputParser.InputType.MIB_ACCOUNT
|
val isDestMib = AccountInputParser.detect(resolvedAccountNumber) == AccountInputParser.InputType.MIB_ACCOUNT
|
||||||
val currency = src.currencyName.ifBlank { "MVR" }
|
val currency = src.currencyName.ifBlank { "MVR" }
|
||||||
@@ -1478,7 +1353,7 @@ class TransferFragment : Fragment() {
|
|||||||
if (isBmlBusiness) {
|
if (isBmlBusiness) {
|
||||||
// Business profile: async OTP channel selection flow — dismiss dialog first
|
// Business profile: async OTP channel selection flow — dismiss dialog first
|
||||||
dialog.dismiss()
|
dialog.dismiss()
|
||||||
startBmlBusinessOtpFlow(
|
bmlHandler().startBusinessOtpFlow(
|
||||||
src, resolvedAccountNumber, destDisplay, amount, amountStr, remarks,
|
src, resolvedAccountNumber, destDisplay, amount, amountStr, remarks,
|
||||||
isSrcCard, isDestMib, currency, allAccounts, allContacts, capturedToAvatar
|
isSrcCard, isDestMib, currency, allAccounts, allContacts, capturedToAvatar
|
||||||
)
|
)
|
||||||
@@ -1490,7 +1365,7 @@ class TransferFragment : Fragment() {
|
|||||||
if (!isSrcBml) {
|
if (!isSrcBml) {
|
||||||
doMibTransfer(src, resolvedAccountNumber, resolvedRecipientName, destDisplay, amountStr, remarks, bankNameCapture)
|
doMibTransfer(src, resolvedAccountNumber, resolvedRecipientName, destDisplay, amountStr, remarks, bankNameCapture)
|
||||||
} else {
|
} else {
|
||||||
doBmlTransfer(src, resolvedAccountNumber, destDisplay, amount, amountStr, remarks, isSrcCard, isDestMib, currency, allAccounts, allContacts)
|
bmlHandler().doTransfer(src, resolvedAccountNumber, destDisplay, amount, amountStr, remarks, isSrcCard, isDestMib, currency, allAccounts, allContacts)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
binding.btnTransfer.isEnabled = true
|
binding.btnTransfer.isEnabled = true
|
||||||
@@ -1689,73 +1564,6 @@ class TransferFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun executeBmlQrPayment(
|
|
||||||
src: BankAccount,
|
|
||||||
debitAccount: String,
|
|
||||||
info: BmlQrPayInfo,
|
|
||||||
amount: Double,
|
|
||||||
dialog: AlertDialog,
|
|
||||||
frame: android.widget.FrameLayout
|
|
||||||
) {
|
|
||||||
val app = requireActivity().application as BasedBankApp
|
|
||||||
val loginId = src.loginTag.removePrefix("bml_")
|
|
||||||
val session = bmlSessionFor(src) ?: run {
|
|
||||||
dialog.dismiss()
|
|
||||||
Toast.makeText(requireContext(), R.string.transfer_session_unavailable, Toast.LENGTH_SHORT).show()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
val otp = CredentialStore(requireContext()).loadBmlCredentials(loginId)?.otpSeed
|
|
||||||
?.let { Totp.generate(it) }
|
|
||||||
?: run { dialog.dismiss(); Toast.makeText(requireContext(), "OTP unavailable", Toast.LENGTH_SHORT).show(); return }
|
|
||||||
|
|
||||||
binding.btnTransfer.isEnabled = false
|
|
||||||
|
|
||||||
viewLifecycleOwner.lifecycleScope.launch {
|
|
||||||
val result = withContext(Dispatchers.IO) {
|
|
||||||
try {
|
|
||||||
if (bmlGatewayQr) {
|
|
||||||
val preOk = BmlQrPayClient().preInitiatePayment(
|
|
||||||
session, debitAccount, info.requestId, amount, info.currency)
|
|
||||||
if (!preOk) return@withContext null
|
|
||||||
}
|
|
||||||
val initiated = BmlQrPayClient().initiatePayment(
|
|
||||||
session, debitAccount, info.requestId, amount, info.currency)
|
|
||||||
if (!initiated) return@withContext null
|
|
||||||
val confirmOtp = CredentialStore(requireContext()).loadBmlCredentials(loginId)
|
|
||||||
?.otpSeed?.let { Totp.generate(it) } ?: otp
|
|
||||||
BmlQrPayClient().confirmPayment(
|
|
||||||
session, debitAccount, info.requestId, amount, info.currency, confirmOtp)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
sh.sar.basedbank.api.bml.BmlQrPayResult(false, errorMessage = e.message ?: "Payment failed")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (_binding == null) return@launch
|
|
||||||
|
|
||||||
if (result == null) {
|
|
||||||
dialog.dismiss()
|
|
||||||
binding.btnTransfer.isEnabled = true
|
|
||||||
Toast.makeText(requireContext(), "Failed to initiate payment", Toast.LENGTH_LONG).show()
|
|
||||||
return@launch
|
|
||||||
}
|
|
||||||
if (result.success) {
|
|
||||||
showSuccessInDialog(
|
|
||||||
dialog, frame,
|
|
||||||
amountCurrency = result.currency.ifBlank { info.currency },
|
|
||||||
amountValue = result.amount.ifBlank { "%.2f".format(amount) },
|
|
||||||
fromName = src.accountBriefName,
|
|
||||||
toName = result.merchant.ifBlank { info.merchantName }
|
|
||||||
) {
|
|
||||||
clearForm()
|
|
||||||
(activity as? HomeActivity)?.triggerRefresh()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
dialog.dismiss()
|
|
||||||
binding.btnTransfer.isEnabled = true
|
|
||||||
Toast.makeText(requireContext(), result.errorMessage, Toast.LENGTH_LONG).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun executeMfaisaQrPayment(
|
private fun executeMfaisaQrPayment(
|
||||||
src: BankAccount,
|
src: BankAccount,
|
||||||
merchant: sh.sar.basedbank.api.mfaisa.MfaisaQrPayClient.QrMerchant,
|
merchant: sh.sar.basedbank.api.mfaisa.MfaisaQrPayClient.QrMerchant,
|
||||||
@@ -1843,7 +1651,7 @@ class TransferFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showProcessingInDialog(dialog: AlertDialog, frame: android.widget.FrameLayout) {
|
internal fun showProcessingInDialog(dialog: AlertDialog, frame: android.widget.FrameLayout) {
|
||||||
dialog.getButton(AlertDialog.BUTTON_POSITIVE)?.visibility = View.GONE
|
dialog.getButton(AlertDialog.BUTTON_POSITIVE)?.visibility = View.GONE
|
||||||
dialog.getButton(AlertDialog.BUTTON_NEGATIVE)?.visibility = View.GONE
|
dialog.getButton(AlertDialog.BUTTON_NEGATIVE)?.visibility = View.GONE
|
||||||
dialog.setCancelable(false)
|
dialog.setCancelable(false)
|
||||||
@@ -1877,7 +1685,7 @@ class TransferFragment : Fragment() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showSuccessInDialog(
|
internal fun showSuccessInDialog(
|
||||||
dialog: AlertDialog,
|
dialog: AlertDialog,
|
||||||
frame: android.widget.FrameLayout,
|
frame: android.widget.FrameLayout,
|
||||||
amountCurrency: String,
|
amountCurrency: String,
|
||||||
@@ -2053,388 +1861,10 @@ class TransferFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun doBmlTransfer(
|
|
||||||
src: BankAccount,
|
|
||||||
destAccount: String,
|
|
||||||
destDisplay: String,
|
|
||||||
amount: Double,
|
|
||||||
amountStr: String,
|
|
||||||
remarks: String,
|
|
||||||
isSrcCard: Boolean,
|
|
||||||
isDestMib: Boolean,
|
|
||||||
currency: String,
|
|
||||||
allAccounts: List<BankAccount>,
|
|
||||||
allContacts: List<BankContact>
|
|
||||||
): Triple<Boolean, String, TransferReceiptData?> {
|
|
||||||
val loginId = src.loginTag.removePrefix("bml_")
|
|
||||||
val sess = bmlSessionFor(src) ?: return Triple(false, getString(R.string.transfer_session_unavailable), null)
|
|
||||||
val otp = CredentialStore(requireContext()).loadBmlCredentials(loginId)?.otpSeed
|
|
||||||
?.let { Totp.generate(it) }
|
|
||||||
?: return Triple(false, "OTP unavailable", null)
|
|
||||||
val debitAccount = src.internalId.ifBlank {
|
|
||||||
return Triple(false, getString(R.string.transfer_missing_internal_id), null)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine type + credit account
|
|
||||||
val isDestMyCard = allAccounts.any { (it.profileType == "BML_PREPAID" || it.profileType == "BML_CREDIT" || it.profileType == "BML_DEBIT") && it.accountNumber == destAccount }
|
|
||||||
val (transferType, creditAccount, bank) = when {
|
|
||||||
isSrcCard -> {
|
|
||||||
// CAD: card → own BML account
|
|
||||||
val destBml = allAccounts.firstOrNull { it.accountNumber == destAccount && it.profileType == "BML" }
|
|
||||||
Triple("CAD", destBml?.internalId?.ifBlank { destAccount } ?: destAccount, null as String?)
|
|
||||||
}
|
|
||||||
isDestMyCard -> {
|
|
||||||
// CPA: BML CASA → own card top-up
|
|
||||||
val card = allAccounts.first { (it.profileType == "BML_PREPAID" || it.profileType == "BML_CREDIT" || it.profileType == "BML_DEBIT") && it.accountNumber == destAccount }
|
|
||||||
Triple("CPA", card.internalId.ifBlank { destAccount }, null as String?)
|
|
||||||
}
|
|
||||||
isDestMib && currency == "MVR" -> Triple("DOT", destAccount, "MIB")
|
|
||||||
isDestMib -> {
|
|
||||||
// USD DOT: requires BML contact numeric ID
|
|
||||||
val contact = allContacts.firstOrNull { it.benefCategoryId == "BML" && it.benefAccount == destAccount }
|
|
||||||
?: return Triple(false, "BML contact not found for this account", null)
|
|
||||||
Triple("DOT", contact.benefNo.removePrefix("bml_"), null as String?)
|
|
||||||
}
|
|
||||||
else -> Triple("IAT", destAccount, null as String?)
|
|
||||||
}
|
|
||||||
val toBank = bank ?: if (isDestMib) "MIB" else "BML"
|
|
||||||
|
|
||||||
// Step 1: initiate
|
|
||||||
val initiated = try {
|
|
||||||
BmlTransferClient().initiateTransfer(sess, debitAccount, creditAccount, amount, transferType, currency, bank)
|
|
||||||
} catch (e: Exception) { return Triple(false, if (e is java.io.IOException) "CONNECTIVITY" else (e.message ?: "Initiation failed"), null) }
|
|
||||||
|
|
||||||
if (!initiated) return Triple(false, "Failed to initiate transfer — check your session", null)
|
|
||||||
|
|
||||||
// Step 2: confirm with fresh OTP
|
|
||||||
val confirmOtp = CredentialStore(requireContext()).loadBmlCredentials(loginId)?.otpSeed
|
|
||||||
?.let { Totp.generate(it) } ?: otp
|
|
||||||
|
|
||||||
return try {
|
|
||||||
val result = BmlTransferClient().confirmTransfer(sess, debitAccount, creditAccount, amount, transferType, currency, confirmOtp, remarks, bank)
|
|
||||||
if (result.success) {
|
|
||||||
val receipt = TransferReceiptData(
|
|
||||||
bank = "BML",
|
|
||||||
amount = "%.2f".format(amount),
|
|
||||||
currency = currency,
|
|
||||||
fromLabel = src.accountBriefName,
|
|
||||||
fromColorHex = "#0066A1",
|
|
||||||
toLabel = destDisplay.ifBlank { destAccount },
|
|
||||||
toAccount = destAccount,
|
|
||||||
toBank = toBank,
|
|
||||||
remarks = remarks,
|
|
||||||
bmlFromName = src.accountBriefName,
|
|
||||||
bmlReference = result.reference,
|
|
||||||
bmlTimestamp = result.timestamp,
|
|
||||||
bmlMessage = result.message
|
|
||||||
)
|
|
||||||
val time = result.timestamp.take(19).replace("T", " ")
|
|
||||||
Triple(true, "Reference: ${result.reference}\n$time", receipt)
|
|
||||||
} else {
|
|
||||||
Triple(false, result.errorMessage.ifBlank { "Transfer failed" }, null)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Triple(false, if (e is java.io.IOException) "CONNECTIVITY" else (e.message ?: "Transfer failed"), null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── BML business profile OTP flow ─────────────────────────────────────────
|
|
||||||
|
|
||||||
private fun isBusinessProfile(account: BankAccount): Boolean {
|
|
||||||
val app = requireActivity().application as BasedBankApp
|
|
||||||
val loginId = account.loginTag.removePrefix("bml_")
|
|
||||||
val profiles = app.bmlProfilesMap[loginId] ?: return false
|
|
||||||
return profiles.firstOrNull { it.profileId == account.profileId }?.profileType == "business"
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun startBmlBusinessOtpFlow(
|
|
||||||
src: BankAccount,
|
|
||||||
destAccount: String,
|
|
||||||
destDisplay: String,
|
|
||||||
amount: Double,
|
|
||||||
amountStr: String,
|
|
||||||
remarks: String,
|
|
||||||
isSrcCard: Boolean,
|
|
||||||
isDestMib: Boolean,
|
|
||||||
currency: String,
|
|
||||||
allAccounts: List<BankAccount>,
|
|
||||||
allContacts: List<BankContact>,
|
|
||||||
toAvatar: Bitmap?
|
|
||||||
) {
|
|
||||||
val debitAccount = src.internalId.ifBlank {
|
|
||||||
Toast.makeText(requireContext(), getString(R.string.transfer_missing_internal_id), Toast.LENGTH_SHORT).show()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
val isDestMyCard = allAccounts.any {
|
|
||||||
(it.profileType == "BML_PREPAID" || it.profileType == "BML_CREDIT" || it.profileType == "BML_DEBIT") && it.accountNumber == destAccount
|
|
||||||
}
|
|
||||||
val (transferType, creditAccount, bank) = when {
|
|
||||||
isSrcCard -> {
|
|
||||||
val destBml = allAccounts.firstOrNull { it.accountNumber == destAccount && it.profileType == "BML" }
|
|
||||||
Triple("CAD", destBml?.internalId?.ifBlank { destAccount } ?: destAccount, null as String?)
|
|
||||||
}
|
|
||||||
isDestMyCard -> {
|
|
||||||
val card = allAccounts.first {
|
|
||||||
(it.profileType == "BML_PREPAID" || it.profileType == "BML_CREDIT" || it.profileType == "BML_DEBIT") && it.accountNumber == destAccount
|
|
||||||
}
|
|
||||||
Triple("CPA", card.internalId.ifBlank { destAccount }, null as String?)
|
|
||||||
}
|
|
||||||
isDestMib && currency == "MVR" -> Triple("DOT", destAccount, "MIB")
|
|
||||||
isDestMib -> {
|
|
||||||
val contact = allContacts.firstOrNull { it.benefCategoryId == "BML" && it.benefAccount == destAccount }
|
|
||||||
if (contact == null) {
|
|
||||||
Toast.makeText(requireContext(), "BML contact not found for this account", Toast.LENGTH_SHORT).show()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
Triple("DOT", contact.benefNo.removePrefix("bml_"), null as String?)
|
|
||||||
}
|
|
||||||
else -> Triple("IAT", destAccount, null as String?)
|
|
||||||
}
|
|
||||||
val toBank = bank ?: if (isDestMib) "MIB" else "BML"
|
|
||||||
|
|
||||||
pendingBmlTransfer = PendingBmlTransfer(
|
|
||||||
src = src,
|
|
||||||
debitAccount = debitAccount,
|
|
||||||
creditAccount = creditAccount,
|
|
||||||
amount = amount,
|
|
||||||
amountStr = amountStr,
|
|
||||||
remarks = remarks,
|
|
||||||
transferType = transferType,
|
|
||||||
currency = currency,
|
|
||||||
bank = bank,
|
|
||||||
destDisplay = destDisplay,
|
|
||||||
destAccount = destAccount,
|
|
||||||
toBank = toBank,
|
|
||||||
toAvatar = toAvatar
|
|
||||||
)
|
|
||||||
|
|
||||||
bmlOtpState = BmlOtpState.SELECTING_CHANNEL
|
|
||||||
binding.btnTransfer.isEnabled = false
|
|
||||||
(activity as? HomeActivity)?.setRefreshing(true)
|
|
||||||
|
|
||||||
viewLifecycleOwner.lifecycleScope.launch {
|
|
||||||
val sess = bmlSessionFor(src)
|
|
||||||
val channels = if (sess != null) {
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
try { BmlAccountClient().fetchTransferChannels(sess) }
|
|
||||||
catch (_: Exception) { emptyList() }
|
|
||||||
}
|
|
||||||
} else emptyList<BmlOtpChannel>()
|
|
||||||
|
|
||||||
(activity as? HomeActivity)?.setRefreshing(false)
|
|
||||||
|
|
||||||
if (channels.isEmpty()) {
|
|
||||||
Toast.makeText(requireContext(), "Could not load OTP channels", Toast.LENGTH_SHORT).show()
|
|
||||||
resetBmlOtpState()
|
|
||||||
updateTransferButton()
|
|
||||||
return@launch
|
|
||||||
}
|
|
||||||
|
|
||||||
showBmlChannelSelection(channels)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showBmlChannelSelection(channels: List<BmlOtpChannel>) {
|
|
||||||
val ctx = requireContext()
|
|
||||||
val dp = ctx.resources.displayMetrics.density
|
|
||||||
binding.containerBmlChannels.removeAllViews()
|
|
||||||
|
|
||||||
for (channel in channels) {
|
|
||||||
val iconRes = when (channel.channel) {
|
|
||||||
"email" -> R.drawable.ic_channel_email
|
|
||||||
"mobile" -> R.drawable.ic_channel_sms
|
|
||||||
else -> R.drawable.ic_channel_sms
|
|
||||||
}
|
|
||||||
val iconSize = (24 * dp).toInt()
|
|
||||||
|
|
||||||
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 = (16 * dp).toInt(); val vp = (12 * dp).toInt()
|
|
||||||
setPadding(hp, vp, hp, vp)
|
|
||||||
}
|
|
||||||
row.addView(ImageView(ctx).apply { setImageResource(iconRes) },
|
|
||||||
LinearLayout.LayoutParams(iconSize, iconSize))
|
|
||||||
row.addView(textCol)
|
|
||||||
row.setOnClickListener { selectBmlOtpChannel(channel) }
|
|
||||||
binding.containerBmlChannels.addView(row)
|
|
||||||
}
|
|
||||||
|
|
||||||
disableTransferFields()
|
|
||||||
binding.layoutBmlChannelSelection.visibility = View.VISIBLE
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun selectBmlOtpChannel(channel: BmlOtpChannel) {
|
|
||||||
bmlOtpChannel = channel.channel
|
|
||||||
binding.layoutBmlChannelSelection.visibility = View.GONE
|
|
||||||
|
|
||||||
val pending = pendingBmlTransfer ?: return
|
|
||||||
val sess = bmlSessionFor(pending.src) ?: run {
|
|
||||||
Toast.makeText(requireContext(), getString(R.string.transfer_session_unavailable), Toast.LENGTH_SHORT).show()
|
|
||||||
resetBmlOtpState()
|
|
||||||
updateTransferButton()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.btnTransfer.isEnabled = false
|
|
||||||
(activity as? HomeActivity)?.setRefreshing(true)
|
|
||||||
|
|
||||||
viewLifecycleOwner.lifecycleScope.launch {
|
|
||||||
val initiated = withContext(Dispatchers.IO) {
|
|
||||||
try {
|
|
||||||
BmlTransferClient().initiateTransfer(
|
|
||||||
sess, pending.debitAccount, pending.creditAccount,
|
|
||||||
pending.amount, pending.transferType, pending.currency,
|
|
||||||
pending.bank, channel.channel
|
|
||||||
)
|
|
||||||
} catch (_: Exception) { false }
|
|
||||||
}
|
|
||||||
(activity as? HomeActivity)?.setRefreshing(false)
|
|
||||||
|
|
||||||
if (!initiated) {
|
|
||||||
Toast.makeText(requireContext(), "Failed to initiate transfer — check your session", Toast.LENGTH_SHORT).show()
|
|
||||||
resetBmlOtpState()
|
|
||||||
updateTransferButton()
|
|
||||||
return@launch
|
|
||||||
}
|
|
||||||
|
|
||||||
bmlOtpState = BmlOtpState.AWAITING_OTP
|
|
||||||
binding.tvBmlOtpSentVia.text = "OTP code sent via: ${channel.description} (${channel.masked})"
|
|
||||||
binding.tvBmlOtpSentVia.visibility = View.VISIBLE
|
|
||||||
binding.tilBmlOtp.visibility = View.VISIBLE
|
|
||||||
binding.etBmlOtp.requestFocus()
|
|
||||||
binding.btnTransfer.text = getString(R.string.transfer_verify_payment)
|
|
||||||
binding.btnTransfer.isEnabled = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun verifyBmlOtp() {
|
|
||||||
val otp = binding.etBmlOtp.text?.toString()?.trim() ?: ""
|
|
||||||
if (otp.isEmpty()) {
|
|
||||||
binding.tilBmlOtp.error = "Enter the verification code"
|
|
||||||
return
|
|
||||||
}
|
|
||||||
binding.tilBmlOtp.error = null
|
|
||||||
val pending = pendingBmlTransfer ?: return
|
|
||||||
val channel = bmlOtpChannel ?: return
|
|
||||||
val sess = bmlSessionFor(pending.src) ?: run {
|
|
||||||
Toast.makeText(requireContext(), getString(R.string.transfer_session_unavailable), Toast.LENGTH_SHORT).show()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.btnTransfer.isEnabled = false
|
|
||||||
(activity as? HomeActivity)?.setRefreshing(true)
|
|
||||||
|
|
||||||
val capturedToAvatar = pending.toAvatar
|
|
||||||
|
|
||||||
viewLifecycleOwner.lifecycleScope.launch {
|
|
||||||
val (ok, msg, receipt) = withContext(Dispatchers.IO) {
|
|
||||||
try {
|
|
||||||
val result = BmlTransferClient().confirmTransfer(
|
|
||||||
sess, pending.debitAccount, pending.creditAccount,
|
|
||||||
pending.amount, pending.transferType, pending.currency,
|
|
||||||
otp, pending.remarks, pending.bank, channel
|
|
||||||
)
|
|
||||||
if (result.success) {
|
|
||||||
val r = TransferReceiptData(
|
|
||||||
bank = "BML",
|
|
||||||
amount = "%.2f".format(pending.amount),
|
|
||||||
currency = pending.currency,
|
|
||||||
fromLabel = pending.src.accountBriefName,
|
|
||||||
fromColorHex = "#0066A1",
|
|
||||||
toLabel = pending.destDisplay.ifBlank { pending.destAccount },
|
|
||||||
toAccount = pending.destAccount,
|
|
||||||
toBank = pending.toBank,
|
|
||||||
remarks = pending.remarks,
|
|
||||||
bmlFromName = pending.src.accountBriefName,
|
|
||||||
bmlReference = result.reference,
|
|
||||||
bmlTimestamp = result.timestamp,
|
|
||||||
bmlMessage = result.message
|
|
||||||
)
|
|
||||||
Triple(true, "", r)
|
|
||||||
} else {
|
|
||||||
Triple(false, result.errorMessage.ifBlank { "Transfer failed" }, null as TransferReceiptData?)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Triple(false, if (e is java.io.IOException) "CONNECTIVITY" else (e.message ?: "Transfer failed"), null as TransferReceiptData?)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
(activity as? HomeActivity)?.setRefreshing(false)
|
|
||||||
|
|
||||||
if (ok && receipt != null) {
|
|
||||||
ReceiptStore.save(requireContext(), receipt)
|
|
||||||
resetBmlOtpState()
|
|
||||||
clearForm()
|
|
||||||
val activity = requireActivity() as HomeActivity
|
|
||||||
activity.triggerRefresh()
|
|
||||||
activity.showWithBackStack(TransferReceiptFragment.newInstance(receipt, capturedToAvatar))
|
|
||||||
} else {
|
|
||||||
binding.btnTransfer.isEnabled = true
|
|
||||||
if (msg == "CONNECTIVITY") {
|
|
||||||
(activity as? HomeActivity)?.showConnectivityBanner(getString(R.string.connectivity_no_internet))
|
|
||||||
} else {
|
|
||||||
binding.tilBmlOtp.error = msg
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun disableTransferFields() {
|
|
||||||
binding.tilAmount.isEnabled = false
|
|
||||||
binding.tilRemarks.isEnabled = false
|
|
||||||
binding.cardFromInfo.alpha = 0.5f
|
|
||||||
binding.btnClearFromInfo.isEnabled = false
|
|
||||||
binding.cardToInfo.alpha = 0.5f
|
|
||||||
binding.btnClearToInfo.isEnabled = false
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun enableTransferFields() {
|
|
||||||
binding.tilAmount.isEnabled = true
|
|
||||||
binding.tilRemarks.isEnabled = true
|
|
||||||
binding.cardFromInfo.alpha = 1f
|
|
||||||
binding.btnClearFromInfo.isEnabled = true
|
|
||||||
binding.cardToInfo.alpha = 1f
|
|
||||||
binding.btnClearToInfo.isEnabled = true
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun resetBmlOtpState() {
|
|
||||||
bmlOtpState = BmlOtpState.NONE
|
|
||||||
bmlOtpChannel = null
|
|
||||||
pendingBmlTransfer = null
|
|
||||||
val b = _binding ?: return
|
|
||||||
b.layoutBmlChannelSelection.visibility = View.GONE
|
|
||||||
b.tvBmlOtpSentVia.visibility = View.GONE
|
|
||||||
b.tilBmlOtp.visibility = View.GONE
|
|
||||||
b.etBmlOtp.setText("")
|
|
||||||
b.tilBmlOtp.error = null
|
|
||||||
enableTransferFields()
|
|
||||||
b.btnTransfer.text = getString(R.string.transfer)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateTransferButton() {
|
private fun updateTransferButton() {
|
||||||
if (bmlOtpState != BmlOtpState.NONE) return
|
if (bmlHandler().isOtpFlowActive) return
|
||||||
val amount = binding.etAmount.text?.toString()?.trim()?.toDoubleOrNull() ?: 0.0
|
val amount = binding.etAmount.text?.toString()?.trim()?.toDoubleOrNull() ?: 0.0
|
||||||
val recipientReady = bmlQrInfo != null || mfaisaQrInfo != null || resolvedAccountNumber.isNotBlank()
|
val recipientReady = bmlHandler().hasQrMerchant || mfaisaQrInfo != null || resolvedAccountNumber.isNotBlank()
|
||||||
val hasAll = selectedAccount != null && recipientReady && amount > 0
|
val hasAll = selectedAccount != null && recipientReady && amount > 0
|
||||||
if (!hasAll) { binding.btnTransfer.isEnabled = false; return }
|
if (!hasAll) { binding.btnTransfer.isEnabled = false; return }
|
||||||
val errors = viewModel.connectivityErrors.value ?: emptySet()
|
val errors = viewModel.connectivityErrors.value ?: emptySet()
|
||||||
@@ -2443,8 +1873,8 @@ class TransferFragment : Fragment() {
|
|||||||
binding.btnTransfer.isEnabled = !bankOffline
|
binding.btnTransfer.isEnabled = !bankOffline
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun clearForm() {
|
internal fun clearForm() {
|
||||||
resetBmlOtpState()
|
bmlHandler().resetOtpState()
|
||||||
mfaisaHandler?.clearState()
|
mfaisaHandler?.clearState()
|
||||||
mfaisaQrInfo = null
|
mfaisaQrInfo = null
|
||||||
selectedAccount = null
|
selectedAccount = null
|
||||||
@@ -2497,7 +1927,7 @@ class TransferFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun makeInitialsBitmap(name: String, colorHex: String): Bitmap {
|
internal fun makeInitialsBitmap(name: String, colorHex: String): Bitmap {
|
||||||
val sizePx = (resources.displayMetrics.density * 40).toInt()
|
val sizePx = (resources.displayMetrics.density * 40).toInt()
|
||||||
val bgColor = try { Color.parseColor(colorHex) } catch (_: Exception) { Color.GRAY }
|
val bgColor = try { Color.parseColor(colorHex) } catch (_: Exception) { Color.GRAY }
|
||||||
val bm = Bitmap.createBitmap(sizePx, sizePx, Bitmap.Config.ARGB_8888)
|
val bm = Bitmap.createBitmap(sizePx, sizePx, Bitmap.Config.ARGB_8888)
|
||||||
@@ -2566,11 +1996,10 @@ class TransferFragment : Fragment() {
|
|||||||
savedAmount = binding.etAmount.text?.toString() ?: ""
|
savedAmount = binding.etAmount.text?.toString() ?: ""
|
||||||
savedRemarks = binding.etRemarks.text?.toString() ?: ""
|
savedRemarks = binding.etRemarks.text?.toString() ?: ""
|
||||||
savedToText = if (resolvedAccountNumber.isEmpty()) binding.etTo.text?.toString() ?: "" else ""
|
savedToText = if (resolvedAccountNumber.isEmpty()) binding.etTo.text?.toString() ?: "" else ""
|
||||||
// Reset in-progress OTP flow — it cannot sensibly resume after the view is gone
|
// The bank handlers hold binding refs; drop them so the next view gets fresh ones.
|
||||||
bmlOtpState = BmlOtpState.NONE
|
// Clearing also resets any in-progress OTP flow, which cannot sensibly resume.
|
||||||
pendingBmlTransfer = null
|
bmlHandler?.clearState()
|
||||||
bmlOtpChannel = null
|
bmlHandler = null
|
||||||
// The M-Faisa handler holds binding refs; drop it so the next view gets a fresh one.
|
|
||||||
mfaisaHandler?.clearState()
|
mfaisaHandler?.clearState()
|
||||||
mfaisaHandler = null
|
mfaisaHandler = null
|
||||||
_binding = null
|
_binding = null
|
||||||
@@ -2647,7 +2076,7 @@ class TransferFragment : Fragment() {
|
|||||||
b.tvDropdownBalance.text = if (hide && balance.isNotBlank()) maskAmount(balance) else balance
|
b.tvDropdownBalance.text = if (hide && balance.isNotBlank()) maskAmount(balance) else balance
|
||||||
b.root.alpha = when {
|
b.root.alpha = when {
|
||||||
inactive -> 0.4f
|
inactive -> 0.4f
|
||||||
bmlQrInfo != null && !isCard -> 0.35f
|
bmlHandler().hasQrMerchant && !isCard -> 0.35f
|
||||||
else -> 1f
|
else -> 1f
|
||||||
}
|
}
|
||||||
val networkIcon = BmlCardParser.cardNetworkIcon(acc)
|
val networkIcon = BmlCardParser.cardNetworkIcon(acc)
|
||||||
|
|||||||
@@ -0,0 +1,697 @@
|
|||||||
|
package sh.sar.basedbank.ui.home.transfer
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.view.Gravity
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.TextView
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import sh.sar.basedbank.BasedBankApp
|
||||||
|
import sh.sar.basedbank.R
|
||||||
|
import sh.sar.basedbank.api.bml.BmlAccountClient
|
||||||
|
import sh.sar.basedbank.api.bml.BmlOtpChannel
|
||||||
|
import sh.sar.basedbank.api.bml.BmlQrPayClient
|
||||||
|
import sh.sar.basedbank.api.bml.BmlQrPayInfo
|
||||||
|
import sh.sar.basedbank.api.bml.BmlQrPayResult
|
||||||
|
import sh.sar.basedbank.api.bml.BmlSession
|
||||||
|
import sh.sar.basedbank.api.bml.BmlTransferClient
|
||||||
|
import sh.sar.basedbank.api.models.BankAccount
|
||||||
|
import sh.sar.basedbank.api.models.BankContact
|
||||||
|
import sh.sar.basedbank.databinding.FragmentTransferBinding
|
||||||
|
import sh.sar.basedbank.ui.home.HomeActivity
|
||||||
|
import sh.sar.basedbank.ui.home.HomeViewModel
|
||||||
|
import sh.sar.basedbank.ui.home.TransferFragment
|
||||||
|
import sh.sar.basedbank.ui.home.TransferReceiptData
|
||||||
|
import sh.sar.basedbank.util.CredentialStore
|
||||||
|
import sh.sar.basedbank.util.RecentPick
|
||||||
|
import sh.sar.basedbank.util.RecentsCache
|
||||||
|
import sh.sar.basedbank.util.Totp
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Owns the BML-only parts of the Transfer screen: session lookup, the CASA/card transfer itself,
|
||||||
|
* the business-profile OTP state machine (channel pick → initiate → verify), and merchant QR
|
||||||
|
* payment (card/gateway QR lookup + pre-initiate/initiate/confirm).
|
||||||
|
*
|
||||||
|
* Lives alongside [TransferFragment], which dispatches to it whenever the selected source's
|
||||||
|
* [BankAccount.bank] is "BML", and mirrors [MfaisaTransferHandler]'s shape: the fragment keeps
|
||||||
|
* the shared confirm dialog, the "To" lookup and the form state; the handler keeps everything
|
||||||
|
* BML-specific.
|
||||||
|
*
|
||||||
|
* Lifetime is bound to the fragment's view: it captures [binding] + [viewModel] + [fragment] (for
|
||||||
|
* [androidx.fragment.app.Fragment.viewLifecycleOwner] and Context) — and must be re-created when
|
||||||
|
* the view is recreated.
|
||||||
|
*/
|
||||||
|
class BmlTransferHandler(
|
||||||
|
private val fragment: TransferFragment,
|
||||||
|
private val binding: FragmentTransferBinding,
|
||||||
|
private val viewModel: HomeViewModel,
|
||||||
|
/** Reads the fragment's currently-selected source account. */
|
||||||
|
private val currentSource: () -> BankAccount?,
|
||||||
|
/** Asks the fragment to make [BankAccount] the source (amount prefix + from-card + Send state). */
|
||||||
|
private val selectSource: (BankAccount) -> Unit,
|
||||||
|
/** Hook called whenever handler state changes in a way that affects the Send button. */
|
||||||
|
private val onStateChanged: () -> Unit,
|
||||||
|
/** Hook called on a successful transfer; fragment navigates to the receipt and refreshes balances. */
|
||||||
|
private val onTransferSuccess: (TransferReceiptData, Bitmap?) -> Unit,
|
||||||
|
) {
|
||||||
|
|
||||||
|
private val app get() = fragment.requireActivity().application as BasedBankApp
|
||||||
|
private val ctx get() = fragment.requireContext()
|
||||||
|
private val host get() = fragment.activity as? HomeActivity
|
||||||
|
|
||||||
|
// ─── State ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Business-profile OTP flow. NONE means the Send button behaves normally. */
|
||||||
|
private enum class OtpState { NONE, SELECTING_CHANNEL, AWAITING_OTP }
|
||||||
|
private var otpState = OtpState.NONE
|
||||||
|
private var otpChannel: String? = null
|
||||||
|
|
||||||
|
private data class PendingTransfer(
|
||||||
|
val src: BankAccount,
|
||||||
|
val debitAccount: String,
|
||||||
|
val creditAccount: String,
|
||||||
|
val amount: Double,
|
||||||
|
val amountStr: String,
|
||||||
|
val remarks: String,
|
||||||
|
val transferType: String,
|
||||||
|
val currency: String,
|
||||||
|
val bank: String?,
|
||||||
|
val destDisplay: String,
|
||||||
|
val destAccount: String,
|
||||||
|
val toBank: String,
|
||||||
|
val toAvatar: Bitmap?
|
||||||
|
)
|
||||||
|
private var pendingTransfer: PendingTransfer? = null
|
||||||
|
|
||||||
|
/** Merchant QR payment mode (set when navigated from a card/gateway QR scan). */
|
||||||
|
var qrInfo: BmlQrPayInfo? = null
|
||||||
|
private set
|
||||||
|
/** True for pay.bml.com.mv QRs, which need an extra pre-initiate step. */
|
||||||
|
private var gatewayQr = false
|
||||||
|
/** Prevents re-running the lookup after the user clears the merchant. */
|
||||||
|
var qrLookupAttempted = false
|
||||||
|
private set
|
||||||
|
|
||||||
|
// ─── Public API the fragment calls ───────────────────────────────────────
|
||||||
|
|
||||||
|
/** Whether the business OTP flow is mid-way — the fragment freezes the Send button while it is. */
|
||||||
|
val isOtpFlowActive: Boolean get() = otpState != OtpState.NONE
|
||||||
|
|
||||||
|
/** Whether the Send button should verify an OTP instead of starting a new transfer. */
|
||||||
|
val isAwaitingOtp: Boolean get() = otpState == OtpState.AWAITING_OTP
|
||||||
|
|
||||||
|
/** Whether a merchant QR is loaded — counts as a resolved recipient for the Send button. */
|
||||||
|
val hasQrMerchant: Boolean get() = qrInfo != null
|
||||||
|
|
||||||
|
fun sessionFor(account: BankAccount?): BmlSession? =
|
||||||
|
account?.let { app.bmlSessionFor(it) } ?: app.anyBmlSession()
|
||||||
|
|
||||||
|
fun isBusinessProfile(account: BankAccount): Boolean {
|
||||||
|
val loginId = account.loginTag.removePrefix("bml_")
|
||||||
|
val profiles = app.bmlProfilesMap[loginId] ?: return false
|
||||||
|
return profiles.firstOrNull { it.profileId == account.profileId }?.profileType == "business"
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Drops the loaded merchant and unlocks the amount/remarks fields the QR mode had frozen. */
|
||||||
|
fun clearQrMerchant() {
|
||||||
|
if (qrInfo == null) return
|
||||||
|
qrInfo = null
|
||||||
|
gatewayQr = false
|
||||||
|
binding.tilAmount.isEnabled = true
|
||||||
|
binding.tilRemarks.isEnabled = true
|
||||||
|
binding.tilRemarks.alpha = 1f
|
||||||
|
binding.etAmount.setText("")
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Called when the view tears down — an in-flight OTP flow cannot sensibly resume. */
|
||||||
|
fun clearState() {
|
||||||
|
otpState = OtpState.NONE
|
||||||
|
otpChannel = null
|
||||||
|
pendingTransfer = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Merchant QR ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
fun lookupQrMerchant(qrUrl: String) {
|
||||||
|
qrLookupAttempted = true
|
||||||
|
gatewayQr = qrUrl.startsWith("https://pay.bml.com.mv/app/")
|
||||||
|
val base64Url = android.util.Base64.encodeToString(
|
||||||
|
qrUrl.toByteArray(Charsets.UTF_8), android.util.Base64.NO_WRAP)
|
||||||
|
val session = app.anyBmlSession() ?: return
|
||||||
|
|
||||||
|
// Lock the "To" input row while loading
|
||||||
|
binding.tilTo.visibility = View.GONE
|
||||||
|
binding.btnPickContact.visibility = View.GONE
|
||||||
|
binding.btnScanQr.visibility = View.GONE
|
||||||
|
host?.setRefreshing(true)
|
||||||
|
|
||||||
|
fragment.viewLifecycleOwner.lifecycleScope.launch {
|
||||||
|
val info = withContext(Dispatchers.IO) {
|
||||||
|
try { BmlQrPayClient().lookupPayRequest(session, base64Url) }
|
||||||
|
catch (_: Exception) { null }
|
||||||
|
}
|
||||||
|
host?.setRefreshing(false)
|
||||||
|
if (info == null) {
|
||||||
|
Toast.makeText(ctx, R.string.bml_qr_lookup_failed, Toast.LENGTH_LONG).show()
|
||||||
|
fragment.requireActivity().onBackPressedDispatcher.onBackPressed()
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
qrInfo = info
|
||||||
|
if (info.amount == 0.0) {
|
||||||
|
RecentsCache.save(ctx, RecentPick(
|
||||||
|
accountNumber = "bmlqr:$qrUrl",
|
||||||
|
displayName = info.merchantName,
|
||||||
|
subtitle = info.merchantAddress.ifBlank { "BML Merchant" },
|
||||||
|
colorHex = "#0066A1",
|
||||||
|
imageHash = null,
|
||||||
|
isProfileImage = false
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-select the user's default BML card if no card was pre-selected
|
||||||
|
if (currentSource() == null) {
|
||||||
|
val defaultNum = CredentialStore(ctx).getDefaultCardAccountNumber()
|
||||||
|
if (defaultNum != null) {
|
||||||
|
val allAccounts = viewModel.accounts.value ?: emptyList()
|
||||||
|
val defaultCard = allAccounts.firstOrNull {
|
||||||
|
it.accountNumber == defaultNum && isCard(it) &&
|
||||||
|
it.statusDesc.equals("Active", ignoreCase = true)
|
||||||
|
}
|
||||||
|
if (defaultCard != null) selectSource(defaultCard)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show merchant in the "To" card — clear button hidden (can't change recipient for QR)
|
||||||
|
binding.tvToAccountName.text = info.merchantName
|
||||||
|
binding.tvToBankBic.text = info.merchantAddress.ifBlank { "BML Merchant" }
|
||||||
|
binding.tvToAccountDetails.visibility = View.GONE
|
||||||
|
binding.tvToBalance.visibility = View.GONE
|
||||||
|
binding.ivToPhoto.scaleType = android.widget.ImageView.ScaleType.CENTER_CROP
|
||||||
|
binding.ivToPhoto.setImageBitmap(fragment.makeInitialsBitmap(info.merchantName, "#0066A1"))
|
||||||
|
binding.cardToInfo.visibility = View.VISIBLE
|
||||||
|
|
||||||
|
// Pre-fill amount if dynamic QR
|
||||||
|
if (info.amount > 0.0) {
|
||||||
|
binding.etAmount.setText("%.2f".format(info.amount))
|
||||||
|
binding.tilAmount.isEnabled = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remarks not applicable for merchant QR payments
|
||||||
|
binding.tilRemarks.isEnabled = false
|
||||||
|
binding.tilRemarks.alpha = 0.4f
|
||||||
|
|
||||||
|
onStateChanged()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Confirm-then-pay for a loaded merchant QR. Uses the fragment's shared confirm dialog and
|
||||||
|
* reports the outcome inside it — there is no receipt screen for merchant payments.
|
||||||
|
*/
|
||||||
|
fun submitQrPayment() {
|
||||||
|
val info = qrInfo ?: return
|
||||||
|
val src = currentSource() ?: run {
|
||||||
|
Toast.makeText(ctx, R.string.transfer_session_unavailable, Toast.LENGTH_SHORT).show()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val amountStr = binding.etAmount.text?.toString()?.trim() ?: ""
|
||||||
|
val amount = amountStr.toDoubleOrNull()
|
||||||
|
if (amount == null || amount <= 0) { binding.tilAmount.error = "Enter a valid amount"; return }
|
||||||
|
binding.tilAmount.error = null
|
||||||
|
val debitAccount = src.internalId.ifBlank {
|
||||||
|
Toast.makeText(ctx, R.string.transfer_missing_internal_id, Toast.LENGTH_SHORT).show()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val fromTypeLabel = sh.sar.basedbank.util.AccountListParser.from(src)?.typeLabel
|
||||||
|
?: sh.sar.basedbank.util.bmlapi.BmlDashboardParser.productLabel(src.accountTypeName)
|
||||||
|
val fromDetail = listOfNotNull("BML", fromTypeLabel.ifBlank { null }).joinToString(" · ")
|
||||||
|
val confirmView = fragment.buildTransferConfirmView(
|
||||||
|
amountCurrency = info.currency,
|
||||||
|
amountValue = "%.2f".format(amount),
|
||||||
|
fromName = src.accountBriefName,
|
||||||
|
fromNumber = src.accountNumber,
|
||||||
|
fromDetail = fromDetail,
|
||||||
|
toName = info.merchantName,
|
||||||
|
toNumber = "",
|
||||||
|
toDetail = info.merchantAddress.ifBlank { "BML Merchant" }
|
||||||
|
)
|
||||||
|
fragment.showConfirmWithBiometric(
|
||||||
|
title = ctx.getString(R.string.transfer),
|
||||||
|
customView = confirmView,
|
||||||
|
biometricSubtitle = "${info.currency} ${"%.2f".format(amount)} → ${info.merchantName}",
|
||||||
|
onConfirmed = { dialog, frame ->
|
||||||
|
fragment.showProcessingInDialog(dialog, frame)
|
||||||
|
executeQrPayment(src, debitAccount, info, amount, dialog, frame)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun executeQrPayment(
|
||||||
|
src: BankAccount,
|
||||||
|
debitAccount: String,
|
||||||
|
info: BmlQrPayInfo,
|
||||||
|
amount: Double,
|
||||||
|
dialog: AlertDialog,
|
||||||
|
frame: android.widget.FrameLayout
|
||||||
|
) {
|
||||||
|
val loginId = src.loginTag.removePrefix("bml_")
|
||||||
|
val session = sessionFor(src) ?: run {
|
||||||
|
dialog.dismiss()
|
||||||
|
Toast.makeText(ctx, R.string.transfer_session_unavailable, Toast.LENGTH_SHORT).show()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val otp = CredentialStore(ctx).loadBmlCredentials(loginId)?.otpSeed
|
||||||
|
?.let { Totp.generate(it) }
|
||||||
|
?: run { dialog.dismiss(); Toast.makeText(ctx, "OTP unavailable", Toast.LENGTH_SHORT).show(); return }
|
||||||
|
|
||||||
|
binding.btnTransfer.isEnabled = false
|
||||||
|
|
||||||
|
fragment.viewLifecycleOwner.lifecycleScope.launch {
|
||||||
|
val result = withContext(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
if (gatewayQr) {
|
||||||
|
val preOk = BmlQrPayClient().preInitiatePayment(
|
||||||
|
session, debitAccount, info.requestId, amount, info.currency)
|
||||||
|
if (!preOk) return@withContext null
|
||||||
|
}
|
||||||
|
val initiated = BmlQrPayClient().initiatePayment(
|
||||||
|
session, debitAccount, info.requestId, amount, info.currency)
|
||||||
|
if (!initiated) return@withContext null
|
||||||
|
val confirmOtp = CredentialStore(ctx).loadBmlCredentials(loginId)
|
||||||
|
?.otpSeed?.let { Totp.generate(it) } ?: otp
|
||||||
|
BmlQrPayClient().confirmPayment(
|
||||||
|
session, debitAccount, info.requestId, amount, info.currency, confirmOtp)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
BmlQrPayResult(false, errorMessage = e.message ?: "Payment failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (fragment.view == null) return@launch
|
||||||
|
|
||||||
|
if (result == null) {
|
||||||
|
dialog.dismiss()
|
||||||
|
binding.btnTransfer.isEnabled = true
|
||||||
|
Toast.makeText(ctx, "Failed to initiate payment", Toast.LENGTH_LONG).show()
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
if (result.success) {
|
||||||
|
fragment.showSuccessInDialog(
|
||||||
|
dialog, frame,
|
||||||
|
amountCurrency = result.currency.ifBlank { info.currency },
|
||||||
|
amountValue = result.amount.ifBlank { "%.2f".format(amount) },
|
||||||
|
fromName = src.accountBriefName,
|
||||||
|
toName = result.merchant.ifBlank { info.merchantName }
|
||||||
|
) {
|
||||||
|
fragment.clearForm()
|
||||||
|
host?.triggerRefresh()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dialog.dismiss()
|
||||||
|
binding.btnTransfer.isEnabled = true
|
||||||
|
Toast.makeText(ctx, result.errorMessage, Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Personal-profile transfer (token OTP, no user interaction) ──────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs initiate + confirm on the calling (IO) thread and returns
|
||||||
|
* `(ok, message, receipt)`. Message is "CONNECTIVITY" when the network was unreachable.
|
||||||
|
*/
|
||||||
|
fun doTransfer(
|
||||||
|
src: BankAccount,
|
||||||
|
destAccount: String,
|
||||||
|
destDisplay: String,
|
||||||
|
amount: Double,
|
||||||
|
amountStr: String,
|
||||||
|
remarks: String,
|
||||||
|
isSrcCard: Boolean,
|
||||||
|
isDestMib: Boolean,
|
||||||
|
currency: String,
|
||||||
|
allAccounts: List<BankAccount>,
|
||||||
|
allContacts: List<BankContact>
|
||||||
|
): Triple<Boolean, String, TransferReceiptData?> {
|
||||||
|
val loginId = src.loginTag.removePrefix("bml_")
|
||||||
|
val sess = sessionFor(src) ?: return Triple(false, ctx.getString(R.string.transfer_session_unavailable), null)
|
||||||
|
val otp = CredentialStore(ctx).loadBmlCredentials(loginId)?.otpSeed
|
||||||
|
?.let { Totp.generate(it) }
|
||||||
|
?: return Triple(false, "OTP unavailable", null)
|
||||||
|
val debitAccount = src.internalId.ifBlank {
|
||||||
|
return Triple(false, ctx.getString(R.string.transfer_missing_internal_id), null)
|
||||||
|
}
|
||||||
|
|
||||||
|
val routed = routeTransfer(destAccount, isSrcCard, isDestMib, currency, allAccounts, allContacts)
|
||||||
|
?: return Triple(false, "BML contact not found for this account", null)
|
||||||
|
val (transferType, creditAccount, bank) = routed
|
||||||
|
val toBank = bank ?: if (isDestMib) "MIB" else "BML"
|
||||||
|
|
||||||
|
// Step 1: initiate
|
||||||
|
val initiated = try {
|
||||||
|
BmlTransferClient().initiateTransfer(sess, debitAccount, creditAccount, amount, transferType, currency, bank)
|
||||||
|
} catch (e: Exception) { return Triple(false, if (e is java.io.IOException) "CONNECTIVITY" else (e.message ?: "Initiation failed"), null) }
|
||||||
|
|
||||||
|
if (!initiated) return Triple(false, "Failed to initiate transfer — check your session", null)
|
||||||
|
|
||||||
|
// Step 2: confirm with fresh OTP
|
||||||
|
val confirmOtp = CredentialStore(ctx).loadBmlCredentials(loginId)?.otpSeed
|
||||||
|
?.let { Totp.generate(it) } ?: otp
|
||||||
|
|
||||||
|
return try {
|
||||||
|
val result = BmlTransferClient().confirmTransfer(sess, debitAccount, creditAccount, amount, transferType, currency, confirmOtp, remarks, bank)
|
||||||
|
if (result.success) {
|
||||||
|
val receipt = TransferReceiptData(
|
||||||
|
bank = "BML",
|
||||||
|
amount = "%.2f".format(amount),
|
||||||
|
currency = currency,
|
||||||
|
fromLabel = src.accountBriefName,
|
||||||
|
fromColorHex = "#0066A1",
|
||||||
|
toLabel = destDisplay.ifBlank { destAccount },
|
||||||
|
toAccount = destAccount,
|
||||||
|
toBank = toBank,
|
||||||
|
remarks = remarks,
|
||||||
|
bmlFromName = src.accountBriefName,
|
||||||
|
bmlReference = result.reference,
|
||||||
|
bmlTimestamp = result.timestamp,
|
||||||
|
bmlMessage = result.message
|
||||||
|
)
|
||||||
|
val time = result.timestamp.take(19).replace("T", " ")
|
||||||
|
Triple(true, "Reference: ${result.reference}\n$time", receipt)
|
||||||
|
} else {
|
||||||
|
Triple(false, result.errorMessage.ifBlank { "Transfer failed" }, null)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Triple(false, if (e is java.io.IOException) "CONNECTIVITY" else (e.message ?: "Transfer failed"), null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Business profile OTP flow ───────────────────────────────────────────
|
||||||
|
|
||||||
|
fun startBusinessOtpFlow(
|
||||||
|
src: BankAccount,
|
||||||
|
destAccount: String,
|
||||||
|
destDisplay: String,
|
||||||
|
amount: Double,
|
||||||
|
amountStr: String,
|
||||||
|
remarks: String,
|
||||||
|
isSrcCard: Boolean,
|
||||||
|
isDestMib: Boolean,
|
||||||
|
currency: String,
|
||||||
|
allAccounts: List<BankAccount>,
|
||||||
|
allContacts: List<BankContact>,
|
||||||
|
toAvatar: Bitmap?
|
||||||
|
) {
|
||||||
|
val debitAccount = src.internalId.ifBlank {
|
||||||
|
Toast.makeText(ctx, ctx.getString(R.string.transfer_missing_internal_id), Toast.LENGTH_SHORT).show()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val routed = routeTransfer(destAccount, isSrcCard, isDestMib, currency, allAccounts, allContacts)
|
||||||
|
if (routed == null) {
|
||||||
|
Toast.makeText(ctx, "BML contact not found for this account", Toast.LENGTH_SHORT).show()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val (transferType, creditAccount, bank) = routed
|
||||||
|
val toBank = bank ?: if (isDestMib) "MIB" else "BML"
|
||||||
|
|
||||||
|
pendingTransfer = PendingTransfer(
|
||||||
|
src = src,
|
||||||
|
debitAccount = debitAccount,
|
||||||
|
creditAccount = creditAccount,
|
||||||
|
amount = amount,
|
||||||
|
amountStr = amountStr,
|
||||||
|
remarks = remarks,
|
||||||
|
transferType = transferType,
|
||||||
|
currency = currency,
|
||||||
|
bank = bank,
|
||||||
|
destDisplay = destDisplay,
|
||||||
|
destAccount = destAccount,
|
||||||
|
toBank = toBank,
|
||||||
|
toAvatar = toAvatar
|
||||||
|
)
|
||||||
|
|
||||||
|
otpState = OtpState.SELECTING_CHANNEL
|
||||||
|
binding.btnTransfer.isEnabled = false
|
||||||
|
host?.setRefreshing(true)
|
||||||
|
|
||||||
|
fragment.viewLifecycleOwner.lifecycleScope.launch {
|
||||||
|
val sess = sessionFor(src)
|
||||||
|
val channels = if (sess != null) {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
try { BmlAccountClient().fetchTransferChannels(sess) }
|
||||||
|
catch (_: Exception) { emptyList() }
|
||||||
|
}
|
||||||
|
} else emptyList<BmlOtpChannel>()
|
||||||
|
|
||||||
|
host?.setRefreshing(false)
|
||||||
|
|
||||||
|
if (channels.isEmpty()) {
|
||||||
|
Toast.makeText(ctx, "Could not load OTP channels", Toast.LENGTH_SHORT).show()
|
||||||
|
resetOtpState()
|
||||||
|
onStateChanged()
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
|
showChannelSelection(channels)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showChannelSelection(channels: List<BmlOtpChannel>) {
|
||||||
|
val dp = ctx.resources.displayMetrics.density
|
||||||
|
binding.containerBmlChannels.removeAllViews()
|
||||||
|
|
||||||
|
for (channel in channels) {
|
||||||
|
val iconRes = when (channel.channel) {
|
||||||
|
"email" -> R.drawable.ic_channel_email
|
||||||
|
"mobile" -> R.drawable.ic_channel_sms
|
||||||
|
else -> R.drawable.ic_channel_sms
|
||||||
|
}
|
||||||
|
val iconSize = (24 * dp).toInt()
|
||||||
|
|
||||||
|
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 = (16 * dp).toInt(); val vp = (12 * dp).toInt()
|
||||||
|
setPadding(hp, vp, hp, vp)
|
||||||
|
}
|
||||||
|
row.addView(ImageView(ctx).apply { setImageResource(iconRes) },
|
||||||
|
LinearLayout.LayoutParams(iconSize, iconSize))
|
||||||
|
row.addView(textCol)
|
||||||
|
row.setOnClickListener { selectOtpChannel(channel) }
|
||||||
|
binding.containerBmlChannels.addView(row)
|
||||||
|
}
|
||||||
|
|
||||||
|
disableTransferFields()
|
||||||
|
binding.layoutBmlChannelSelection.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun selectOtpChannel(channel: BmlOtpChannel) {
|
||||||
|
otpChannel = channel.channel
|
||||||
|
binding.layoutBmlChannelSelection.visibility = View.GONE
|
||||||
|
|
||||||
|
val pending = pendingTransfer ?: return
|
||||||
|
val sess = sessionFor(pending.src) ?: run {
|
||||||
|
Toast.makeText(ctx, ctx.getString(R.string.transfer_session_unavailable), Toast.LENGTH_SHORT).show()
|
||||||
|
resetOtpState()
|
||||||
|
onStateChanged()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.btnTransfer.isEnabled = false
|
||||||
|
host?.setRefreshing(true)
|
||||||
|
|
||||||
|
fragment.viewLifecycleOwner.lifecycleScope.launch {
|
||||||
|
val initiated = withContext(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
BmlTransferClient().initiateTransfer(
|
||||||
|
sess, pending.debitAccount, pending.creditAccount,
|
||||||
|
pending.amount, pending.transferType, pending.currency,
|
||||||
|
pending.bank, channel.channel
|
||||||
|
)
|
||||||
|
} catch (_: Exception) { false }
|
||||||
|
}
|
||||||
|
host?.setRefreshing(false)
|
||||||
|
|
||||||
|
if (!initiated) {
|
||||||
|
Toast.makeText(ctx, "Failed to initiate transfer — check your session", Toast.LENGTH_SHORT).show()
|
||||||
|
resetOtpState()
|
||||||
|
onStateChanged()
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
|
otpState = OtpState.AWAITING_OTP
|
||||||
|
binding.tvBmlOtpSentVia.text = "OTP code sent via: ${channel.description} (${channel.masked})"
|
||||||
|
binding.tvBmlOtpSentVia.visibility = View.VISIBLE
|
||||||
|
binding.tilBmlOtp.visibility = View.VISIBLE
|
||||||
|
binding.etBmlOtp.requestFocus()
|
||||||
|
binding.btnTransfer.text = ctx.getString(R.string.transfer_verify_payment)
|
||||||
|
binding.btnTransfer.isEnabled = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Send-button action while [isAwaitingOtp]: confirms the pending transfer with the typed code. */
|
||||||
|
fun verifyOtp() {
|
||||||
|
val otp = binding.etBmlOtp.text?.toString()?.trim() ?: ""
|
||||||
|
if (otp.isEmpty()) {
|
||||||
|
binding.tilBmlOtp.error = "Enter the verification code"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
binding.tilBmlOtp.error = null
|
||||||
|
val pending = pendingTransfer ?: return
|
||||||
|
val channel = otpChannel ?: return
|
||||||
|
val sess = sessionFor(pending.src) ?: run {
|
||||||
|
Toast.makeText(ctx, ctx.getString(R.string.transfer_session_unavailable), Toast.LENGTH_SHORT).show()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.btnTransfer.isEnabled = false
|
||||||
|
host?.setRefreshing(true)
|
||||||
|
|
||||||
|
val capturedToAvatar = pending.toAvatar
|
||||||
|
|
||||||
|
fragment.viewLifecycleOwner.lifecycleScope.launch {
|
||||||
|
val (ok, msg, receipt) = withContext(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
val result = BmlTransferClient().confirmTransfer(
|
||||||
|
sess, pending.debitAccount, pending.creditAccount,
|
||||||
|
pending.amount, pending.transferType, pending.currency,
|
||||||
|
otp, pending.remarks, pending.bank, channel
|
||||||
|
)
|
||||||
|
if (result.success) {
|
||||||
|
val r = TransferReceiptData(
|
||||||
|
bank = "BML",
|
||||||
|
amount = "%.2f".format(pending.amount),
|
||||||
|
currency = pending.currency,
|
||||||
|
fromLabel = pending.src.accountBriefName,
|
||||||
|
fromColorHex = "#0066A1",
|
||||||
|
toLabel = pending.destDisplay.ifBlank { pending.destAccount },
|
||||||
|
toAccount = pending.destAccount,
|
||||||
|
toBank = pending.toBank,
|
||||||
|
remarks = pending.remarks,
|
||||||
|
bmlFromName = pending.src.accountBriefName,
|
||||||
|
bmlReference = result.reference,
|
||||||
|
bmlTimestamp = result.timestamp,
|
||||||
|
bmlMessage = result.message
|
||||||
|
)
|
||||||
|
Triple(true, "", r)
|
||||||
|
} else {
|
||||||
|
Triple(false, result.errorMessage.ifBlank { "Transfer failed" }, null as TransferReceiptData?)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Triple(false, if (e is java.io.IOException) "CONNECTIVITY" else (e.message ?: "Transfer failed"), null as TransferReceiptData?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
host?.setRefreshing(false)
|
||||||
|
|
||||||
|
if (ok && receipt != null) {
|
||||||
|
resetOtpState()
|
||||||
|
onTransferSuccess(receipt, capturedToAvatar)
|
||||||
|
} else {
|
||||||
|
binding.btnTransfer.isEnabled = true
|
||||||
|
if (msg == "CONNECTIVITY") {
|
||||||
|
host?.showConnectivityBanner(ctx.getString(R.string.connectivity_no_internet))
|
||||||
|
} else {
|
||||||
|
binding.tilBmlOtp.error = msg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Tears down the OTP UI and re-enables the form. Safe to call when no flow is running. */
|
||||||
|
fun resetOtpState() {
|
||||||
|
otpState = OtpState.NONE
|
||||||
|
otpChannel = null
|
||||||
|
pendingTransfer = null
|
||||||
|
if (fragment.view == null) return
|
||||||
|
binding.layoutBmlChannelSelection.visibility = View.GONE
|
||||||
|
binding.tvBmlOtpSentVia.visibility = View.GONE
|
||||||
|
binding.tilBmlOtp.visibility = View.GONE
|
||||||
|
binding.etBmlOtp.setText("")
|
||||||
|
binding.tilBmlOtp.error = null
|
||||||
|
enableTransferFields()
|
||||||
|
binding.btnTransfer.text = ctx.getString(R.string.transfer)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun disableTransferFields() {
|
||||||
|
binding.tilAmount.isEnabled = false
|
||||||
|
binding.tilRemarks.isEnabled = false
|
||||||
|
binding.cardFromInfo.alpha = 0.5f
|
||||||
|
binding.btnClearFromInfo.isEnabled = false
|
||||||
|
binding.cardToInfo.alpha = 0.5f
|
||||||
|
binding.btnClearToInfo.isEnabled = false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun enableTransferFields() {
|
||||||
|
binding.tilAmount.isEnabled = true
|
||||||
|
binding.tilRemarks.isEnabled = true
|
||||||
|
binding.cardFromInfo.alpha = 1f
|
||||||
|
binding.btnClearFromInfo.isEnabled = true
|
||||||
|
binding.cardToInfo.alpha = 1f
|
||||||
|
binding.btnClearToInfo.isEnabled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Shared routing ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Picks the BML transfer type and the credit-account identifier the API expects.
|
||||||
|
* Returns null only for the USD → MIB case with no matching saved BML contact, which both
|
||||||
|
* callers report as "BML contact not found".
|
||||||
|
*/
|
||||||
|
private fun routeTransfer(
|
||||||
|
destAccount: String,
|
||||||
|
isSrcCard: Boolean,
|
||||||
|
isDestMib: Boolean,
|
||||||
|
currency: String,
|
||||||
|
allAccounts: List<BankAccount>,
|
||||||
|
allContacts: List<BankContact>
|
||||||
|
): Triple<String, String, String?>? {
|
||||||
|
val isDestMyCard = allAccounts.any { isCard(it) && it.accountNumber == destAccount }
|
||||||
|
return when {
|
||||||
|
isSrcCard -> {
|
||||||
|
// CAD: card → own BML account
|
||||||
|
val destBml = allAccounts.firstOrNull { it.accountNumber == destAccount && it.profileType == "BML" }
|
||||||
|
Triple("CAD", destBml?.internalId?.ifBlank { destAccount } ?: destAccount, null)
|
||||||
|
}
|
||||||
|
isDestMyCard -> {
|
||||||
|
// CPA: BML CASA → own card top-up
|
||||||
|
val card = allAccounts.first { isCard(it) && it.accountNumber == destAccount }
|
||||||
|
Triple("CPA", card.internalId.ifBlank { destAccount }, null)
|
||||||
|
}
|
||||||
|
isDestMib && currency == "MVR" -> Triple("DOT", destAccount, "MIB")
|
||||||
|
isDestMib -> {
|
||||||
|
// USD DOT: requires BML contact numeric ID
|
||||||
|
val contact = allContacts.firstOrNull { it.benefCategoryId == "BML" && it.benefAccount == destAccount }
|
||||||
|
?: return null
|
||||||
|
Triple("DOT", contact.benefNo.removePrefix("bml_"), null)
|
||||||
|
}
|
||||||
|
else -> Triple("IAT", destAccount, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isCard(account: BankAccount) =
|
||||||
|
account.profileType == "BML_PREPAID" || account.profileType == "BML_CREDIT" || account.profileType == "BML_DEBIT"
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user