forked from shihaam/thijooree
prep support for transfers for bml business accounts)
This commit is contained in:
@@ -81,6 +81,27 @@ class BmlAccountClient {
|
|||||||
} catch (_: Exception) { null }
|
} catch (_: Exception) { null }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun fetchTransferChannels(session: BmlSession): List<BmlOtpChannel> {
|
||||||
|
val resp = client.newCall(bmlApiRequest(session, "$BML_BASE_URL/api/mobile/transfer")).execute()
|
||||||
|
val json = resp.body?.string() ?: run { resp.close(); return emptyList() }
|
||||||
|
resp.close()
|
||||||
|
return try {
|
||||||
|
val root = JSONObject(json)
|
||||||
|
if (!root.optBoolean("success")) return emptyList()
|
||||||
|
val arr = root.optJSONObject("payload")
|
||||||
|
?.optJSONObject("transfer")
|
||||||
|
?.optJSONArray("otpChannel") ?: return emptyList()
|
||||||
|
(0 until arr.length()).map { i ->
|
||||||
|
val ch = arr.getJSONObject(i)
|
||||||
|
BmlOtpChannel(
|
||||||
|
channel = ch.optString("channel"),
|
||||||
|
description = ch.optString("description"),
|
||||||
|
masked = ch.optString("masked")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (_: Exception) { emptyList() }
|
||||||
|
}
|
||||||
|
|
||||||
private fun parseDashboard(
|
private fun parseDashboard(
|
||||||
json: String,
|
json: String,
|
||||||
loginTag: String,
|
loginTag: String,
|
||||||
|
|||||||
@@ -17,7 +17,8 @@ class BmlTransferClient {
|
|||||||
amount: Double,
|
amount: Double,
|
||||||
transferType: String,
|
transferType: String,
|
||||||
currency: String,
|
currency: String,
|
||||||
bank: String? = null
|
bank: String? = null,
|
||||||
|
channel: String = "token"
|
||||||
): Boolean {
|
): Boolean {
|
||||||
val jo = JSONObject().apply {
|
val jo = JSONObject().apply {
|
||||||
put("debitAccount", debitAccount)
|
put("debitAccount", debitAccount)
|
||||||
@@ -25,7 +26,7 @@ class BmlTransferClient {
|
|||||||
put("debitAmount", amount)
|
put("debitAmount", amount)
|
||||||
put("transfertype", transferType)
|
put("transfertype", transferType)
|
||||||
put("currency", currency)
|
put("currency", currency)
|
||||||
put("channel", "token")
|
put("channel", channel)
|
||||||
if (bank != null) put("bank", bank)
|
if (bank != null) put("bank", bank)
|
||||||
}
|
}
|
||||||
val request = Request.Builder()
|
val request = Request.Builder()
|
||||||
@@ -55,7 +56,8 @@ class BmlTransferClient {
|
|||||||
currency: String,
|
currency: String,
|
||||||
otp: String,
|
otp: String,
|
||||||
remarks: String = "",
|
remarks: String = "",
|
||||||
bank: String? = null
|
bank: String? = null,
|
||||||
|
channel: String = "token"
|
||||||
): BmlTransferResult {
|
): BmlTransferResult {
|
||||||
val jo = JSONObject().apply {
|
val jo = JSONObject().apply {
|
||||||
put("debitAccount", debitAccount)
|
put("debitAccount", debitAccount)
|
||||||
@@ -63,7 +65,7 @@ class BmlTransferClient {
|
|||||||
put("debitAmount", amount)
|
put("debitAmount", amount)
|
||||||
put("transfertype", transferType)
|
put("transfertype", transferType)
|
||||||
put("currency", currency)
|
put("currency", currency)
|
||||||
put("channel", "token")
|
put("channel", channel)
|
||||||
put("otp", otp)
|
put("otp", otp)
|
||||||
if (remarks.isNotBlank()) put("remarks", remarks)
|
if (remarks.isNotBlank()) put("remarks", remarks)
|
||||||
if (bank != null) put("bank", bank)
|
if (bank != null) put("bank", bank)
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ import android.widget.BaseAdapter
|
|||||||
import android.widget.Filter
|
import android.widget.Filter
|
||||||
import android.widget.Filterable
|
import android.widget.Filterable
|
||||||
import android.graphics.Typeface
|
import android.graphics.Typeface
|
||||||
|
import android.view.Gravity
|
||||||
|
import android.widget.ImageView
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
@@ -36,6 +38,8 @@ 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.BmlTransferClient
|
import sh.sar.basedbank.api.bml.BmlTransferClient
|
||||||
import sh.sar.basedbank.api.bml.BmlTransferResult
|
import sh.sar.basedbank.api.bml.BmlTransferResult
|
||||||
import sh.sar.basedbank.api.bml.BmlValidateClient
|
import sh.sar.basedbank.api.bml.BmlValidateClient
|
||||||
@@ -83,6 +87,28 @@ class TransferFragment : Fragment() {
|
|||||||
// Values: "FAHIPAY_TRANSFER", "RAASTAS", "OOREDOO_BILL"
|
// Values: "FAHIPAY_TRANSFER", "RAASTAS", "OOREDOO_BILL"
|
||||||
private var selectedFahipayService: String? = null
|
private var selectedFahipayService: String? = null
|
||||||
|
|
||||||
|
// 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 val qrLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
private val qrLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||||
if (result.resultCode != Activity.RESULT_OK) return@registerForActivityResult
|
if (result.resultCode != Activity.RESULT_OK) return@registerForActivityResult
|
||||||
val raw = result.data?.getStringExtra(QrScannerActivity.EXTRA_QR_CONTENT) ?: return@registerForActivityResult
|
val raw = result.data?.getStringExtra(QrScannerActivity.EXTRA_QR_CONTENT) ?: return@registerForActivityResult
|
||||||
@@ -171,7 +197,10 @@ class TransferFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
binding.btnTransfer.isEnabled = false
|
binding.btnTransfer.isEnabled = false
|
||||||
binding.btnTransfer.setOnClickListener { initiateTransfer() }
|
binding.btnTransfer.setOnClickListener {
|
||||||
|
if (bmlOtpState == BmlOtpState.AWAITING_OTP) verifyBmlOtp()
|
||||||
|
else initiateTransfer()
|
||||||
|
}
|
||||||
|
|
||||||
binding.etAmount.addTextChangedListener { updateTransferButton() }
|
binding.etAmount.addTextChangedListener { updateTransferButton() }
|
||||||
|
|
||||||
@@ -602,6 +631,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)
|
||||||
val isSrcCard = src.profileType == "BML_PREPAID" || src.profileType == "BML_CREDIT"
|
val isSrcCard = src.profileType == "BML_PREPAID" || src.profileType == "BML_CREDIT"
|
||||||
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" }
|
||||||
@@ -636,26 +666,34 @@ class TransferFragment : Fragment() {
|
|||||||
val mainMsg = "Send $currency $amountStr to $destDisplay?\n\nFrom: ${src.accountBriefName} · ${src.accountNumber}"
|
val mainMsg = "Send $currency $amountStr to $destDisplay?\n\nFrom: ${src.accountBriefName} · ${src.accountNumber}"
|
||||||
|
|
||||||
val doTransfer: () -> Unit = {
|
val doTransfer: () -> Unit = {
|
||||||
binding.btnTransfer.isEnabled = false
|
if (isBmlBusiness) {
|
||||||
(activity as? HomeActivity)?.setRefreshing(true)
|
// Business profile: async OTP channel selection flow
|
||||||
viewLifecycleOwner.lifecycleScope.launch {
|
startBmlBusinessOtpFlow(
|
||||||
val (ok, msg, receipt) = withContext(Dispatchers.IO) {
|
src, resolvedAccountNumber, destDisplay, amount, amountStr, remarks,
|
||||||
if (!isSrcBml) {
|
isSrcCard, isDestMib, currency, allAccounts, allContacts, capturedToAvatar
|
||||||
doMibTransfer(src, resolvedAccountNumber, resolvedRecipientName, destDisplay, amountStr, remarks, bankNameCapture)
|
)
|
||||||
} else {
|
} else {
|
||||||
doBmlTransfer(src, resolvedAccountNumber, destDisplay, amount, amountStr, remarks, isSrcCard, isDestMib, currency, allAccounts, allContacts)
|
binding.btnTransfer.isEnabled = false
|
||||||
|
(activity as? HomeActivity)?.setRefreshing(true)
|
||||||
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
|
val (ok, msg, receipt) = withContext(Dispatchers.IO) {
|
||||||
|
if (!isSrcBml) {
|
||||||
|
doMibTransfer(src, resolvedAccountNumber, resolvedRecipientName, destDisplay, amountStr, remarks, bankNameCapture)
|
||||||
|
} else {
|
||||||
|
doBmlTransfer(src, resolvedAccountNumber, destDisplay, amount, amountStr, remarks, isSrcCard, isDestMib, currency, allAccounts, allContacts)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
binding.btnTransfer.isEnabled = true
|
||||||
|
(activity as? HomeActivity)?.setRefreshing(false)
|
||||||
|
if (ok && receipt != null) {
|
||||||
|
ReceiptStore.save(requireContext(), receipt)
|
||||||
|
clearForm()
|
||||||
|
val activity = requireActivity() as HomeActivity
|
||||||
|
activity.triggerRefresh()
|
||||||
|
activity.showWithBackStack(TransferReceiptFragment.newInstance(receipt, capturedToAvatar))
|
||||||
|
} else if (!ok) {
|
||||||
|
Toast.makeText(requireContext(), msg, Toast.LENGTH_LONG).show()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
binding.btnTransfer.isEnabled = true
|
|
||||||
(activity as? HomeActivity)?.setRefreshing(false)
|
|
||||||
if (ok && receipt != null) {
|
|
||||||
ReceiptStore.save(requireContext(), receipt)
|
|
||||||
clearForm()
|
|
||||||
val activity = requireActivity() as HomeActivity
|
|
||||||
activity.triggerRefresh()
|
|
||||||
activity.showWithBackStack(TransferReceiptFragment.newInstance(receipt, capturedToAvatar))
|
|
||||||
} else if (!ok) {
|
|
||||||
Toast.makeText(requireContext(), msg, Toast.LENGTH_LONG).show()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -884,12 +922,300 @@ class TransferFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── 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.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.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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
disableTransferFields()
|
||||||
|
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, 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
|
||||||
|
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.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
|
||||||
val amount = binding.etAmount.text?.toString()?.trim()?.toDoubleOrNull() ?: 0.0
|
val amount = binding.etAmount.text?.toString()?.trim()?.toDoubleOrNull() ?: 0.0
|
||||||
binding.btnTransfer.isEnabled = selectedAccount != null && resolvedAccountNumber.isNotBlank() && amount > 0
|
binding.btnTransfer.isEnabled = selectedAccount != null && resolvedAccountNumber.isNotBlank() && amount > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun clearForm() {
|
private fun clearForm() {
|
||||||
|
resetBmlOtpState()
|
||||||
selectedAccount = null
|
selectedAccount = null
|
||||||
binding.actvFrom.setText("", false)
|
binding.actvFrom.setText("", false)
|
||||||
binding.cardFromInfo.visibility = View.GONE
|
binding.cardFromInfo.visibility = View.GONE
|
||||||
|
|||||||
@@ -336,6 +336,51 @@
|
|||||||
|
|
||||||
</com.google.android.material.textfield.TextInputLayout>
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<!-- BML business OTP: channel selection (shown after confirmation, before OTP entry) -->
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/layoutBmlChannelSelection"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
android:visibility="gone">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="4dp"
|
||||||
|
android:text="@string/transfer_send_otp_via"
|
||||||
|
android:textAppearance="?attr/textAppearanceLabelMedium"
|
||||||
|
android:textColor="?attr/colorOnSurfaceVariant" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/containerBmlChannels"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- BML business OTP: verification code input (shown after channel selection) -->
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:id="@+id/tilBmlOtp"
|
||||||
|
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:hint="@string/transfer_otp_code_hint"
|
||||||
|
android:layout_marginBottom="16dp"
|
||||||
|
android:visibility="gone">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/etBmlOtp"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:inputType="number"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:maxLength="6" />
|
||||||
|
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
<com.google.android.material.button.MaterialButton
|
||||||
android:id="@+id/btnTransfer"
|
android:id="@+id/btnTransfer"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
|||||||
@@ -232,6 +232,9 @@
|
|||||||
<string name="transfer_bml_contact_required_title">Contact Required</string>
|
<string name="transfer_bml_contact_required_title">Contact Required</string>
|
||||||
<string name="transfer_bml_contact_required_msg">To send USD to a MIB account from BML, the recipient must be saved as a BML contact first. This is required by BML\'s API.\n\nPlease add this account as a BML contact, then try again.</string>
|
<string name="transfer_bml_contact_required_msg">To send USD to a MIB account from BML, the recipient must be saved as a BML contact first. This is required by BML\'s API.\n\nPlease add this account as a BML contact, then try again.</string>
|
||||||
<string name="transfer_missing_internal_id">Account data is incomplete — please re-login to refresh.</string>
|
<string name="transfer_missing_internal_id">Account data is incomplete — please re-login to refresh.</string>
|
||||||
|
<string name="transfer_verify_payment">Verify Payment</string>
|
||||||
|
<string name="transfer_send_otp_via">Send verification code via</string>
|
||||||
|
<string name="transfer_otp_code_hint">Verification code</string>
|
||||||
|
|
||||||
<!-- Contacts -->
|
<!-- Contacts -->
|
||||||
<string name="contacts_empty">No contacts found</string>
|
<string name="contacts_empty">No contacts found</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user