add transfers
This commit is contained in:
@@ -49,13 +49,22 @@ class ContactPickerSheetFragment : BottomSheetDialogFragment() {
|
||||
adapter = ContactPickerAdapter(
|
||||
onItemClick = { accountNumber, label ->
|
||||
val contacts = viewModel.contacts.value ?: emptyList()
|
||||
val accounts = viewModel.accounts.value ?: emptyList()
|
||||
val contact = contacts.firstOrNull { it.benefAccount == accountNumber }
|
||||
val account = accounts.firstOrNull { it.accountNumber == accountNumber }
|
||||
val bundle = bundleOf(KEY_ACCOUNT_NUMBER to accountNumber, KEY_LABEL to label)
|
||||
if (contact != null && !contact.transferCyDesc.equals("MVR", ignoreCase = true)) {
|
||||
bundle.putBoolean(KEY_SKIP_LOOKUP, true)
|
||||
bundle.putString(KEY_SUBTITLE, "${contact.benefBankName} · ${contact.benefAccount}")
|
||||
bundle.putString(KEY_COLOR, contact.bankColor)
|
||||
contact.customerImgHash?.let { bundle.putString(KEY_IMAGE_HASH, it) }
|
||||
when {
|
||||
account?.profileType == "BML_PREPAID" -> {
|
||||
bundle.putBoolean(KEY_SKIP_LOOKUP, true)
|
||||
bundle.putString(KEY_SUBTITLE, "${account.accountNumber} · ${account.currencyName} ${account.availableBalance}")
|
||||
bundle.putString(KEY_COLOR, "#FE860E")
|
||||
}
|
||||
contact != null && !contact.transferCyDesc.equals("MVR", ignoreCase = true) -> {
|
||||
bundle.putBoolean(KEY_SKIP_LOOKUP, true)
|
||||
bundle.putString(KEY_SUBTITLE, "${contact.benefBankName} · ${contact.benefAccount}")
|
||||
bundle.putString(KEY_COLOR, contact.bankColor)
|
||||
contact.customerImgHash?.let { bundle.putString(KEY_IMAGE_HASH, it) }
|
||||
}
|
||||
}
|
||||
setFragmentResult(REQUEST_KEY, bundle)
|
||||
dismiss()
|
||||
|
||||
@@ -289,6 +289,39 @@ class HomeActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
fun refreshBalances(src: MibAccount) {
|
||||
val app = application as BasedBankApp
|
||||
lifecycleScope.launch {
|
||||
val current = viewModel.accounts.value ?: emptyList()
|
||||
if (src.profileType.startsWith("BML")) {
|
||||
val fresh = withContext(Dispatchers.IO) {
|
||||
val sess = app.bmlSession ?: return@withContext null
|
||||
try {
|
||||
val accounts = BmlLoginFlow().fetchAccounts(sess)
|
||||
AccountCache.saveBml(this@HomeActivity, accounts)
|
||||
app.bmlAccounts = accounts
|
||||
accounts
|
||||
} catch (_: Exception) { null }
|
||||
} ?: return@launch
|
||||
val mibOnly = current.filter { !it.profileType.startsWith("BML") }
|
||||
viewModel.accounts.postValue(mibOnly + fresh)
|
||||
} else {
|
||||
val fresh = withContext(Dispatchers.IO) {
|
||||
val sess = app.mibSession ?: return@withContext null
|
||||
val profile = app.mibProfiles.firstOrNull { it.profileId == src.profileId } ?: return@withContext null
|
||||
val prefs = getSharedPreferences("mib_prefs", MODE_PRIVATE)
|
||||
try { MibLoginFlow(prefs).fetchAllProfiles(sess, listOf(profile), src.loginTag) }
|
||||
catch (_: Exception) { null }
|
||||
} ?: return@launch
|
||||
// Replace accounts from this profile only, keep everything else
|
||||
val others = current.filter { it.profileId != src.profileId || it.profileType.startsWith("BML") }
|
||||
val merged = others + fresh
|
||||
AccountCache.save(this@HomeActivity, merged.filter { !it.profileType.startsWith("BML") })
|
||||
viewModel.accounts.postValue(merged)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun refreshFinancing(session: MibSession?, profiles: List<MibProfile>) {
|
||||
if (session == null || profiles.isEmpty()) return
|
||||
val prefs = getSharedPreferences("mib_prefs", MODE_PRIVATE)
|
||||
|
||||
@@ -11,8 +11,11 @@ import android.util.Base64
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.BaseAdapter
|
||||
import android.widget.Filter
|
||||
import android.widget.Filterable
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.widget.addTextChangedListener
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
@@ -26,16 +29,22 @@ import kotlinx.coroutines.withContext
|
||||
import sh.sar.basedbank.BasedBankApp
|
||||
import sh.sar.basedbank.R
|
||||
import sh.sar.basedbank.api.bml.BmlLoginFlow
|
||||
import sh.sar.basedbank.api.bml.BmlTransferResult
|
||||
import sh.sar.basedbank.api.mib.MibAccount
|
||||
import sh.sar.basedbank.api.mib.MibBeneficiary
|
||||
import sh.sar.basedbank.api.mib.MibContactsClient
|
||||
import sh.sar.basedbank.api.mib.MibIpsAccountInfo
|
||||
import sh.sar.basedbank.api.mib.MibLookupException
|
||||
import sh.sar.basedbank.api.mib.MibTransferClient
|
||||
import sh.sar.basedbank.api.mib.MibTransferResult
|
||||
import sh.sar.basedbank.databinding.FragmentTransferBinding
|
||||
import sh.sar.basedbank.databinding.ItemAccountDropdownBinding
|
||||
import sh.sar.basedbank.databinding.ItemPickerSectionHeaderBinding
|
||||
import sh.sar.basedbank.util.CredentialStore
|
||||
import sh.sar.basedbank.util.PaymvQrParser
|
||||
import sh.sar.basedbank.util.RecentPick
|
||||
import sh.sar.basedbank.util.RecentsCache
|
||||
import sh.sar.basedbank.util.Totp
|
||||
|
||||
class TransferFragment : Fragment() {
|
||||
|
||||
@@ -47,6 +56,23 @@ class TransferFragment : Fragment() {
|
||||
private val session get() = (requireActivity().application as BasedBankApp).mibSession
|
||||
private val bmlSession get() = (requireActivity().application as BasedBankApp).bmlSession
|
||||
|
||||
// Resolved recipient info — set after successful lookup or prefill
|
||||
private var resolvedAccountNumber = ""
|
||||
private var resolvedRecipientName = ""
|
||||
|
||||
private val qrLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
if (result.resultCode != Activity.RESULT_OK) return@registerForActivityResult
|
||||
val raw = result.data?.getStringExtra(QrScannerActivity.EXTRA_QR_CONTENT) ?: return@registerForActivityResult
|
||||
val qr = PaymvQrParser.parse(raw)
|
||||
if (qr == null || qr.accountNumber == null) {
|
||||
Toast.makeText(requireContext(), R.string.transfer_qr_invalid, Toast.LENGTH_SHORT).show()
|
||||
return@registerForActivityResult
|
||||
}
|
||||
if (qr.amount != null) binding.etAmount.setText(qr.amount)
|
||||
if (qr.purpose != null) binding.etRemarks.setText(qr.purpose)
|
||||
prefillToFromContact(qr.accountNumber, "")
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val ARG_ACCOUNT = "contact_account"
|
||||
private const val ARG_NAME = "contact_name"
|
||||
@@ -71,19 +97,6 @@ class TransferFragment : Fragment() {
|
||||
}
|
||||
}
|
||||
|
||||
private val qrLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
if (result.resultCode != Activity.RESULT_OK) return@registerForActivityResult
|
||||
val raw = result.data?.getStringExtra(QrScannerActivity.EXTRA_QR_CONTENT) ?: return@registerForActivityResult
|
||||
val qr = PaymvQrParser.parse(raw)
|
||||
if (qr == null || qr.accountNumber == null) {
|
||||
Toast.makeText(requireContext(), R.string.transfer_qr_invalid, Toast.LENGTH_SHORT).show()
|
||||
return@registerForActivityResult
|
||||
}
|
||||
if (qr.amount != null) binding.etAmount.setText(qr.amount)
|
||||
if (qr.purpose != null) binding.etRemarks.setText(qr.purpose)
|
||||
prefillToFromContact(qr.accountNumber, "")
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
_binding = FragmentTransferBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
@@ -115,9 +128,7 @@ class TransferFragment : Fragment() {
|
||||
qrLauncher.launch(Intent(requireContext(), QrScannerActivity::class.java))
|
||||
}
|
||||
|
||||
binding.btnTransfer.setOnClickListener {
|
||||
Toast.makeText(requireContext(), R.string.work_in_progress, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
binding.btnTransfer.setOnClickListener { initiateTransfer() }
|
||||
|
||||
// Pre-select contact if navigated from contacts page
|
||||
arguments?.getString(ARG_ACCOUNT)?.let { account ->
|
||||
@@ -136,22 +147,27 @@ class TransferFragment : Fragment() {
|
||||
val adapter = AccountDropdownAdapter(requireContext(), accounts)
|
||||
binding.actvFrom.setAdapter(adapter)
|
||||
|
||||
if (accounts.isNotEmpty() && selectedAccount == null) {
|
||||
selectedAccount = accounts[0]
|
||||
binding.actvFrom.setText(accounts[0].toDisplayString(), false)
|
||||
}
|
||||
// No default selection — user must explicitly pick a source account
|
||||
|
||||
binding.actvFrom.setOnItemClickListener { _, _, position, _ ->
|
||||
selectedAccount = accounts[position]
|
||||
binding.actvFrom.setText(accounts[position].toDisplayString(), false)
|
||||
val picked = adapter.getAccount(position) ?: return@setOnItemClickListener
|
||||
selectedAccount = picked
|
||||
binding.actvFrom.setText(picked.toDisplayString(), false)
|
||||
updateAmountPrefix(picked)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateAmountPrefix(account: MibAccount) {
|
||||
binding.tilAmount.prefixText = if (account.currencyName == "USD") "USD " else "MVR "
|
||||
}
|
||||
|
||||
private fun setupAccountLookup() {
|
||||
binding.tilTo.setEndIconOnClickListener { lookupAccount() }
|
||||
|
||||
binding.btnClearToInfo.setOnClickListener {
|
||||
resolvedAccountNumber = ""
|
||||
resolvedRecipientName = ""
|
||||
binding.cardToInfo.visibility = View.GONE
|
||||
binding.tilTo.visibility = View.VISIBLE
|
||||
binding.btnPickContact.visibility = View.VISIBLE
|
||||
@@ -162,6 +178,8 @@ class TransferFragment : Fragment() {
|
||||
binding.etTo.addTextChangedListener {
|
||||
binding.tilTo.error = null
|
||||
if (binding.cardToInfo.visibility == View.VISIBLE) {
|
||||
resolvedAccountNumber = ""
|
||||
resolvedRecipientName = ""
|
||||
binding.cardToInfo.visibility = View.GONE
|
||||
binding.tilTo.visibility = View.VISIBLE
|
||||
binding.btnPickContact.visibility = View.VISIBLE
|
||||
@@ -206,7 +224,6 @@ class TransferFragment : Fragment() {
|
||||
var errorMsg: String? = null
|
||||
val info = withContext(Dispatchers.IO) {
|
||||
if (isBmlSource && bmlSess != null) {
|
||||
// BML source: prefer BML validate, fall back to MIB IPS
|
||||
val bmlResult = try { BmlLoginFlow().validateAccount(bmlSess, accountNumber) } catch (_: Exception) { null }
|
||||
if (bmlResult != null) {
|
||||
val bankId = when (bmlResult.trnType) {
|
||||
@@ -222,13 +239,11 @@ class TransferFragment : Fragment() {
|
||||
errorMsg = getString(R.string.transfer_account_not_found); null
|
||||
}
|
||||
} else {
|
||||
// MIB source (or no preference): prefer MIB, fall back to BML validate
|
||||
if (mibSess != null) {
|
||||
try { MibTransferClient().lookup(mibSess, accountNumber) }
|
||||
catch (e: MibLookupException) { errorMsg = e.message; null }
|
||||
catch (_: Exception) { errorMsg = getString(R.string.transfer_account_not_found); null }
|
||||
} else {
|
||||
// MIB not available, try BML
|
||||
val bmlResult = try { BmlLoginFlow().validateAccount(bmlSess!!, accountNumber) } catch (_: Exception) { null }
|
||||
if (bmlResult != null) {
|
||||
val bankId = when (bmlResult.trnType) {
|
||||
@@ -245,14 +260,14 @@ class TransferFragment : Fragment() {
|
||||
binding.tilTo.isEnabled = true
|
||||
if (info != null) {
|
||||
val accounts = viewModel.accounts.value ?: emptyList()
|
||||
val contacts = viewModel.contacts.value ?: emptyList()
|
||||
val matchedAcc = accounts.firstOrNull { it.accountNumber == info.accountNumber }
|
||||
val matchedContact = contacts.firstOrNull { it.benefAccount == info.accountNumber }
|
||||
val matchedCont = contacts.firstOrNull { it.benefAccount == info.accountNumber }
|
||||
|
||||
val displayName = matchedAcc?.accountBriefName
|
||||
?: matchedContact?.benefNickName
|
||||
?: info.accountName
|
||||
val colorHex = if (matchedAcc != null) "#FE860E" else matchedContact?.bankColor ?: "#607D8B"
|
||||
val displayName = matchedAcc?.accountBriefName ?: matchedCont?.benefNickName ?: info.accountName
|
||||
val colorHex = if (matchedAcc != null) "#FE860E" else matchedCont?.bankColor ?: "#607D8B"
|
||||
|
||||
resolvedAccountNumber = info.accountNumber
|
||||
resolvedRecipientName = info.accountName
|
||||
|
||||
binding.tvToAccountName.text = displayName
|
||||
binding.tvToBankBic.text = "${info.accountNumber} · ${info.bankId}"
|
||||
@@ -266,8 +281,8 @@ class TransferFragment : Fragment() {
|
||||
when {
|
||||
matchedAcc?.profileImageHash != null ->
|
||||
loadToPhoto(matchedAcc.profileImageHash, isProfile = true)
|
||||
matchedContact?.customerImgHash != null ->
|
||||
loadToPhoto(matchedContact.customerImgHash, isProfile = false)
|
||||
matchedCont?.customerImgHash != null ->
|
||||
loadToPhoto(matchedCont.customerImgHash, isProfile = false)
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(requireContext(), errorMsg, Toast.LENGTH_SHORT).show()
|
||||
@@ -282,6 +297,9 @@ class TransferFragment : Fragment() {
|
||||
colorHex: String,
|
||||
imageHash: String?
|
||||
) {
|
||||
resolvedAccountNumber = accountNumber
|
||||
resolvedRecipientName = displayName
|
||||
|
||||
binding.tvToAccountName.text = displayName
|
||||
binding.tvToBankBic.text = subtitle
|
||||
binding.ivToPhoto.setImageBitmap(makeInitialsBitmap(displayName, colorHex))
|
||||
@@ -306,6 +324,8 @@ class TransferFragment : Fragment() {
|
||||
}
|
||||
|
||||
private fun prefillToFromContact(accountNumber: String, label: String) {
|
||||
resolvedAccountNumber = ""
|
||||
resolvedRecipientName = ""
|
||||
binding.cardToInfo.visibility = View.GONE
|
||||
binding.tilTo.visibility = View.VISIBLE
|
||||
binding.btnPickContact.visibility = View.VISIBLE
|
||||
@@ -315,6 +335,206 @@ class TransferFragment : Fragment() {
|
||||
lookupAccount()
|
||||
}
|
||||
|
||||
// ── Transfer ──────────────────────────────────────────────────────────────
|
||||
|
||||
private fun initiateTransfer() {
|
||||
val src = selectedAccount ?: run {
|
||||
Toast.makeText(requireContext(), R.string.transfer_session_unavailable, Toast.LENGTH_SHORT).show()
|
||||
return
|
||||
}
|
||||
if (resolvedAccountNumber.isBlank()) {
|
||||
Toast.makeText(requireContext(), R.string.transfer_enter_account_first, 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 remarks = binding.etRemarks.text?.toString()?.trim() ?: ""
|
||||
|
||||
val isSrcBml = src.profileType.startsWith("BML")
|
||||
val isSrcCard = src.profileType == "BML_PREPAID"
|
||||
val isDestMib = resolvedAccountNumber.matches(Regex("^9\\d{16}$"))
|
||||
val currency = src.currencyName.ifBlank { "MVR" }
|
||||
val allAccounts = viewModel.accounts.value ?: emptyList()
|
||||
val allContacts = viewModel.contacts.value ?: emptyList()
|
||||
|
||||
// BML USD → MIB requires a saved BML contact
|
||||
if (isSrcBml && isDestMib && currency == "USD") {
|
||||
val hasBmlContact = allContacts.any { it.benefCategoryId == "BML" && it.benefAccount == resolvedAccountNumber }
|
||||
if (!hasBmlContact) {
|
||||
AlertDialog.Builder(requireContext())
|
||||
.setTitle(R.string.transfer_bml_contact_required_title)
|
||||
.setMessage(R.string.transfer_bml_contact_required_msg)
|
||||
.setPositiveButton("OK", null)
|
||||
.show()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
val destDisplay = binding.tvToAccountName.text?.toString() ?: resolvedAccountNumber
|
||||
AlertDialog.Builder(requireContext())
|
||||
.setTitle(R.string.transfer)
|
||||
.setMessage("Send $currency $amountStr to $destDisplay?\n\nFrom: ${src.accountBriefName} · ${src.accountNumber}")
|
||||
.setPositiveButton(R.string.transfer_confirm) { _, _ ->
|
||||
binding.btnTransfer.isEnabled = false
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
val (ok, msg) = withContext(Dispatchers.IO) {
|
||||
if (!isSrcBml) {
|
||||
doMibTransfer(src, resolvedAccountNumber, resolvedRecipientName, amountStr, remarks)
|
||||
} else {
|
||||
doBmlTransfer(src, resolvedAccountNumber, amount, remarks, isSrcCard, isDestMib, currency, allAccounts, allContacts)
|
||||
}
|
||||
}
|
||||
binding.btnTransfer.isEnabled = true
|
||||
if (ok) {
|
||||
clearForm()
|
||||
(requireActivity() as HomeActivity).refreshBalances(src)
|
||||
AlertDialog.Builder(requireContext())
|
||||
.setTitle(R.string.transfer_success)
|
||||
.setMessage(msg)
|
||||
.setPositiveButton("OK", null)
|
||||
.show()
|
||||
} else {
|
||||
Toast.makeText(requireContext(), msg, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun doMibTransfer(
|
||||
src: MibAccount,
|
||||
destAccount: String,
|
||||
destName: String,
|
||||
amount: String,
|
||||
remarks: String
|
||||
): Pair<Boolean, String> {
|
||||
val sess = session ?: return Pair(false, getString(R.string.transfer_session_unavailable))
|
||||
val app = requireActivity().application as BasedBankApp
|
||||
// Switch to the profile that owns the source account
|
||||
if (src.profileId.isNotBlank()) {
|
||||
val profile = app.mibProfiles.firstOrNull { it.profileId == src.profileId }
|
||||
if (profile != null) app.mibLoginFlow.switchProfile(sess, profile)
|
||||
}
|
||||
val otp = CredentialStore(requireContext()).loadMibCredentials()?.otpSeed
|
||||
?.let { Totp.generate(it) }
|
||||
?: return Pair(false, "OTP unavailable")
|
||||
val currencyCode = if (src.currencyName == "USD") "840" else "462"
|
||||
val isDestMib = destAccount.matches(Regex("^9\\d{16}$"))
|
||||
val bankNo = if (isDestMib) 2 else 3
|
||||
return try {
|
||||
val result = MibTransferClient().transfer(
|
||||
session = sess,
|
||||
fromAccount = src.accountNumber,
|
||||
toAccount = destAccount,
|
||||
amount = amount,
|
||||
currencyCode = currencyCode,
|
||||
benefName = destName.ifBlank { "Recipient" },
|
||||
bankNo = bankNo,
|
||||
purpose = remarks,
|
||||
otp = otp
|
||||
)
|
||||
if (result.success) {
|
||||
Pair(true, "Transaction ID: ${result.trxId}\n${result.date}")
|
||||
} else {
|
||||
Pair(false, result.errorMessage.ifBlank { "Transfer failed" })
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Pair(false, e.message ?: "Transfer failed")
|
||||
}
|
||||
}
|
||||
|
||||
private fun doBmlTransfer(
|
||||
src: MibAccount,
|
||||
destAccount: String,
|
||||
amount: Double,
|
||||
remarks: String,
|
||||
isSrcCard: Boolean,
|
||||
isDestMib: Boolean,
|
||||
currency: String,
|
||||
allAccounts: List<MibAccount>,
|
||||
allContacts: List<MibBeneficiary>
|
||||
): Pair<Boolean, String> {
|
||||
val sess = bmlSession ?: return Pair(false, getString(R.string.transfer_session_unavailable))
|
||||
val otp = CredentialStore(requireContext()).loadBmlCredentials()?.otpSeed
|
||||
?.let { Totp.generate(it) }
|
||||
?: return Pair(false, "OTP unavailable")
|
||||
val debitAccount = src.internalId.ifBlank {
|
||||
return Pair(false, getString(R.string.transfer_missing_internal_id))
|
||||
}
|
||||
|
||||
// Determine type + credit account
|
||||
val isDestMyCard = allAccounts.any { it.profileType == "BML_PREPAID" && 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.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 Pair(false, "BML contact not found for this account")
|
||||
Triple("DOT", contact.benefNo.removePrefix("bml_"), null as String?)
|
||||
}
|
||||
else -> Triple("IAT", destAccount, null as String?)
|
||||
}
|
||||
|
||||
val bmlFlow = BmlLoginFlow()
|
||||
// Step 1: initiate
|
||||
val initiated = try {
|
||||
bmlFlow.initiateTransfer(sess, debitAccount, creditAccount, amount, transferType, currency, bank)
|
||||
} catch (e: Exception) { return Pair(false, e.message ?: "Initiation failed") }
|
||||
|
||||
if (!initiated) return Pair(false, "Failed to initiate transfer — check your session")
|
||||
|
||||
// Step 2: confirm with fresh OTP
|
||||
val confirmOtp = CredentialStore(requireContext()).loadBmlCredentials()?.otpSeed
|
||||
?.let { Totp.generate(it) } ?: otp
|
||||
|
||||
return try {
|
||||
val result = bmlFlow.confirmTransfer(sess, debitAccount, creditAccount, amount, transferType, currency, confirmOtp, remarks, bank)
|
||||
if (result.success) {
|
||||
val time = result.timestamp.take(19).replace("T", " ")
|
||||
Pair(true, "Reference: ${result.reference}\n$time")
|
||||
} else {
|
||||
Pair(false, result.errorMessage.ifBlank { "Transfer failed" })
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Pair(false, e.message ?: "Transfer failed")
|
||||
}
|
||||
}
|
||||
|
||||
private fun clearForm() {
|
||||
selectedAccount = null
|
||||
binding.actvFrom.setText("", false)
|
||||
binding.tilAmount.prefixText = null
|
||||
binding.etAmount.setText("")
|
||||
binding.etRemarks.setText("")
|
||||
resolvedAccountNumber = ""
|
||||
resolvedRecipientName = ""
|
||||
binding.cardToInfo.visibility = View.GONE
|
||||
binding.tilTo.visibility = View.VISIBLE
|
||||
binding.btnPickContact.visibility = View.VISIBLE
|
||||
binding.btnScanQr.visibility = View.VISIBLE
|
||||
binding.etTo.setText("")
|
||||
binding.tilTo.error = null
|
||||
binding.tilAmount.error = null
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
private fun loadToPhoto(hash: String, isProfile: Boolean) {
|
||||
val sess = session ?: return
|
||||
val app = requireActivity().application as BasedBankApp
|
||||
@@ -381,7 +601,6 @@ class TransferFragment : Fragment() {
|
||||
return
|
||||
}
|
||||
|
||||
// Manual entry not in contacts/accounts — save with resolved info
|
||||
RecentsCache.save(requireContext(), RecentPick(
|
||||
accountNumber = info.accountNumber,
|
||||
displayName = info.accountName,
|
||||
@@ -404,29 +623,67 @@ class TransferFragment : Fragment() {
|
||||
|
||||
private fun MibAccount.toDisplayString() = "$accountBriefName · $accountNumber"
|
||||
|
||||
// items: String = section header, MibAccount = selectable row
|
||||
private inner class AccountDropdownAdapter(
|
||||
context: Context,
|
||||
private val accounts: List<MibAccount>
|
||||
) : ArrayAdapter<String>(context, android.R.layout.simple_list_item_1, accounts.map { it.toDisplayString() }) {
|
||||
private val context: Context,
|
||||
accounts: List<MibAccount>
|
||||
) : BaseAdapter(), Filterable {
|
||||
|
||||
private fun bindDropdown(convertView: View?, parent: ViewGroup, position: Int): View {
|
||||
val itemBinding = if (convertView?.tag is ItemAccountDropdownBinding) {
|
||||
convertView.tag as ItemAccountDropdownBinding
|
||||
} else {
|
||||
ItemAccountDropdownBinding.inflate(LayoutInflater.from(context), parent, false)
|
||||
.also { it.root.tag = it }
|
||||
private val items: List<Any> = buildList {
|
||||
val regular = accounts.filter { it.profileType != "BML_PREPAID" }
|
||||
val cards = accounts.filter { it.profileType == "BML_PREPAID" }
|
||||
addAll(regular)
|
||||
if (cards.isNotEmpty()) {
|
||||
add(getString(R.string.cards))
|
||||
addAll(cards)
|
||||
}
|
||||
val account = accounts[position]
|
||||
itemBinding.tvDropdownAccountName.text = account.accountBriefName
|
||||
itemBinding.tvDropdownAccountNumber.text = account.accountNumber
|
||||
itemBinding.tvDropdownBalance.text = "${account.currencyName} ${account.availableBalance}"
|
||||
return itemBinding.root
|
||||
}
|
||||
|
||||
override fun getView(position: Int, convertView: View?, parent: ViewGroup) =
|
||||
bindDropdown(convertView, parent, position)
|
||||
fun getAccount(position: Int): MibAccount? = (items.getOrNull(position) as? MibAccount)
|
||||
?.takeUnless { it.profileType == "BML_PREPAID" && !it.statusDesc.equals("Active", ignoreCase = true) }
|
||||
|
||||
override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup) =
|
||||
bindDropdown(convertView, parent, position)
|
||||
override fun getCount() = items.size
|
||||
override fun getItem(position: Int) = items[position]
|
||||
override fun getItemId(position: Int) = position.toLong()
|
||||
override fun getViewTypeCount() = 2
|
||||
override fun getItemViewType(position: Int) = if (items[position] is String) 0 else 1
|
||||
override fun isEnabled(position: Int) = getAccount(position) != null
|
||||
|
||||
override fun getView(position: Int, convertView: View?, parent: ViewGroup) =
|
||||
getDropDownView(position, convertView, parent)
|
||||
|
||||
override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup): View {
|
||||
val item = items[position]
|
||||
return if (item is String) {
|
||||
val b = if (convertView?.tag is ItemPickerSectionHeaderBinding) {
|
||||
convertView.tag as ItemPickerSectionHeaderBinding
|
||||
} else {
|
||||
ItemPickerSectionHeaderBinding.inflate(LayoutInflater.from(context), parent, false)
|
||||
.also { it.root.tag = it }
|
||||
}
|
||||
b.tvHeader.text = item
|
||||
b.root
|
||||
} else {
|
||||
val acc = item as MibAccount
|
||||
val b = if (convertView?.tag is ItemAccountDropdownBinding) {
|
||||
convertView.tag as ItemAccountDropdownBinding
|
||||
} else {
|
||||
ItemAccountDropdownBinding.inflate(LayoutInflater.from(context), parent, false)
|
||||
.also { it.root.tag = it }
|
||||
}
|
||||
val inactive = acc.profileType == "BML_PREPAID" && !acc.statusDesc.equals("Active", ignoreCase = true)
|
||||
b.tvDropdownAccountName.text = acc.accountBriefName
|
||||
b.tvDropdownAccountNumber.text = if (inactive) "${acc.accountNumber} · ${acc.statusDesc}" else acc.accountNumber
|
||||
b.tvDropdownBalance.text = "${acc.currencyName} ${acc.availableBalance}"
|
||||
b.root.alpha = if (inactive) 0.4f else 1f
|
||||
b.root
|
||||
}
|
||||
}
|
||||
|
||||
override fun getFilter() = object : Filter() {
|
||||
override fun performFiltering(c: CharSequence?) = FilterResults().apply { values = items; count = items.size }
|
||||
override fun publishResults(c: CharSequence?, r: FilterResults?) = notifyDataSetChanged()
|
||||
override fun convertResultToString(r: Any?) = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user