forked from shihaam/thijooree
add transfers
This commit is contained in:
@@ -311,6 +311,98 @@ class BmlLoginFlow {
|
|||||||
return parseContacts(json)
|
return parseContacts(json)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Step 1 of BML transfer: POST without OTP. Returns true if server responds code=22 (OTP ready).
|
||||||
|
*/
|
||||||
|
fun initiateTransfer(
|
||||||
|
session: BmlSession,
|
||||||
|
debitAccount: String,
|
||||||
|
creditAccount: String,
|
||||||
|
amount: Double,
|
||||||
|
transferType: String,
|
||||||
|
currency: String,
|
||||||
|
bank: String? = null
|
||||||
|
): Boolean {
|
||||||
|
val jo = JSONObject().apply {
|
||||||
|
put("debitAccount", debitAccount)
|
||||||
|
put("creditAccount", creditAccount)
|
||||||
|
put("debitAmount", amount)
|
||||||
|
put("transfertype", transferType)
|
||||||
|
put("currency", currency)
|
||||||
|
put("channel", "token")
|
||||||
|
if (bank != null) put("bank", bank)
|
||||||
|
}
|
||||||
|
val body = jo.toString().toRequestBody("application/json".toMediaType())
|
||||||
|
val request = Request.Builder()
|
||||||
|
.url("$BASE_URL/api/mobile/transfer")
|
||||||
|
.post(body)
|
||||||
|
.header("Authorization", "Bearer ${session.accessToken}")
|
||||||
|
.header("User-Agent", APP_USER_AGENT)
|
||||||
|
.header("x-app-version", APP_VERSION)
|
||||||
|
.header("accept", "application/json")
|
||||||
|
.build()
|
||||||
|
return apiClient.newCall(request).execute().use { response ->
|
||||||
|
val bodyStr = response.body?.string() ?: return@use false
|
||||||
|
try {
|
||||||
|
val json = JSONObject(bodyStr)
|
||||||
|
json.optBoolean("success") && json.optInt("code") == 22
|
||||||
|
} catch (_: Exception) { false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Step 2 of BML transfer: POST with OTP + remarks. Returns BmlTransferResult.
|
||||||
|
*/
|
||||||
|
fun confirmTransfer(
|
||||||
|
session: BmlSession,
|
||||||
|
debitAccount: String,
|
||||||
|
creditAccount: String,
|
||||||
|
amount: Double,
|
||||||
|
transferType: String,
|
||||||
|
currency: String,
|
||||||
|
otp: String,
|
||||||
|
remarks: String = "",
|
||||||
|
bank: String? = null
|
||||||
|
): BmlTransferResult {
|
||||||
|
val jo = JSONObject().apply {
|
||||||
|
put("debitAccount", debitAccount)
|
||||||
|
put("creditAccount", creditAccount)
|
||||||
|
put("debitAmount", amount)
|
||||||
|
put("transfertype", transferType)
|
||||||
|
put("currency", currency)
|
||||||
|
put("channel", "token")
|
||||||
|
put("otp", otp)
|
||||||
|
if (remarks.isNotBlank()) put("remarks", remarks)
|
||||||
|
if (bank != null) put("bank", bank)
|
||||||
|
}
|
||||||
|
val body = jo.toString().toRequestBody("application/json".toMediaType())
|
||||||
|
val request = Request.Builder()
|
||||||
|
.url("$BASE_URL/api/mobile/transfer")
|
||||||
|
.post(body)
|
||||||
|
.header("Authorization", "Bearer ${session.accessToken}")
|
||||||
|
.header("User-Agent", APP_USER_AGENT)
|
||||||
|
.header("x-app-version", APP_VERSION)
|
||||||
|
.header("accept", "application/json")
|
||||||
|
.build()
|
||||||
|
return apiClient.newCall(request).execute().use { response ->
|
||||||
|
val bodyStr = response.body?.string()
|
||||||
|
?: return@use BmlTransferResult(false, errorMessage = "No response")
|
||||||
|
try {
|
||||||
|
val json = JSONObject(bodyStr)
|
||||||
|
if (!json.optBoolean("success")) {
|
||||||
|
BmlTransferResult(false, errorMessage = json.optString("message").ifBlank { "Transfer failed" })
|
||||||
|
} else {
|
||||||
|
val payload = json.optJSONObject("payload")
|
||||||
|
BmlTransferResult(
|
||||||
|
success = true,
|
||||||
|
reference = payload?.optString("reference") ?: "",
|
||||||
|
timestamp = payload?.optString("timestamp") ?: ""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (_: Exception) { BmlTransferResult(false, errorMessage = "Parse error") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun deleteContact(session: BmlSession, contactId: String): Boolean {
|
fun deleteContact(session: BmlSession, contactId: String): Boolean {
|
||||||
val body = """{"_method":"delete"}""".toRequestBody("application/json".toMediaType())
|
val body = """{"_method":"delete"}""".toRequestBody("application/json".toMediaType())
|
||||||
val request = Request.Builder()
|
val request = Request.Builder()
|
||||||
@@ -350,6 +442,8 @@ class BmlLoginFlow {
|
|||||||
val accountNumber = item.optString("account")
|
val accountNumber = item.optString("account")
|
||||||
val status = item.optString("account_status", "Active")
|
val status = item.optString("account_status", "Active")
|
||||||
|
|
||||||
|
val internalId = item.optString("id", "")
|
||||||
|
|
||||||
if (accountType == "CASA") {
|
if (accountType == "CASA") {
|
||||||
val available = item.optDouble("availableBalance", 0.0)
|
val available = item.optDouble("availableBalance", 0.0)
|
||||||
casaAccounts.add(MibAccount(
|
casaAccounts.add(MibAccount(
|
||||||
@@ -365,7 +459,8 @@ class BmlLoginFlow {
|
|||||||
mvrBalance = if (currency == "MVR") "%.2f".format(available) else "0.00",
|
mvrBalance = if (currency == "MVR") "%.2f".format(available) else "0.00",
|
||||||
statusDesc = status,
|
statusDesc = status,
|
||||||
profileImageHash = null,
|
profileImageHash = null,
|
||||||
loginTag = loginTag
|
loginTag = loginTag,
|
||||||
|
internalId = internalId
|
||||||
))
|
))
|
||||||
} else if (accountType == "Card") {
|
} else if (accountType == "Card") {
|
||||||
val isPrepaid = item.optBoolean("prepaid_card", false)
|
val isPrepaid = item.optBoolean("prepaid_card", false)
|
||||||
@@ -385,7 +480,8 @@ class BmlLoginFlow {
|
|||||||
mvrBalance = if (currency == "MVR") "%.2f".format(available) else "0.00",
|
mvrBalance = if (currency == "MVR") "%.2f".format(available) else "0.00",
|
||||||
statusDesc = status,
|
statusDesc = status,
|
||||||
profileImageHash = null,
|
profileImageHash = null,
|
||||||
loginTag = loginTag
|
loginTag = loginTag,
|
||||||
|
internalId = internalId
|
||||||
))
|
))
|
||||||
} else {
|
} else {
|
||||||
// Linked debit cards have no independent balance or account link — skip
|
// Linked debit cards have no independent balance or account link — skip
|
||||||
|
|||||||
@@ -16,6 +16,13 @@ data class BmlAccountValidation(
|
|||||||
val agnt: String? = null // BIC for DOT (MIB account on BML)
|
val agnt: String? = null // BIC for DOT (MIB account on BML)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
data class BmlTransferResult(
|
||||||
|
val success: Boolean,
|
||||||
|
val reference: String = "",
|
||||||
|
val timestamp: String = "",
|
||||||
|
val errorMessage: String = ""
|
||||||
|
)
|
||||||
|
|
||||||
data class BmlForeignLimit(
|
data class BmlForeignLimit(
|
||||||
val type: String,
|
val type: String,
|
||||||
val used: Double,
|
val used: Double,
|
||||||
|
|||||||
@@ -204,7 +204,7 @@ class MibLoginFlow(private val prefs: android.content.SharedPreferences) {
|
|||||||
doRequest(session, payload, "n")
|
doRequest(session, payload, "n")
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun fetchAllProfiles(session: MibSession, profiles: List<MibProfile>, loginTag: String): List<MibAccount> {
|
fun fetchAllProfiles(session: MibSession, profiles: List<MibProfile>, loginTag: String): List<MibAccount> {
|
||||||
val allAccounts = mutableListOf<MibAccount>()
|
val allAccounts = mutableListOf<MibAccount>()
|
||||||
for (profile in profiles) {
|
for (profile in profiles) {
|
||||||
val payload = baseData(session, "P47").apply {
|
val payload = baseData(session, "P47").apply {
|
||||||
|
|||||||
@@ -33,7 +33,15 @@ data class MibAccount(
|
|||||||
val statusDesc: String,
|
val statusDesc: String,
|
||||||
val profileImageHash: String?,
|
val profileImageHash: String?,
|
||||||
val loginTag: String = "",
|
val loginTag: String = "",
|
||||||
val profileId: String = "" // MIB profile ID; empty for BML accounts
|
val profileId: String = "", // MIB profile ID; empty for BML accounts
|
||||||
|
val internalId: String = "" // BML internal UUID; empty for MIB accounts
|
||||||
|
)
|
||||||
|
|
||||||
|
data class MibTransferResult(
|
||||||
|
val success: Boolean,
|
||||||
|
val trxId: String = "",
|
||||||
|
val date: String = "",
|
||||||
|
val errorMessage: String = ""
|
||||||
)
|
)
|
||||||
|
|
||||||
data class MibBeneficiaryCategory(
|
data class MibBeneficiaryCategory(
|
||||||
|
|||||||
@@ -33,6 +33,59 @@ class MibTransferClient {
|
|||||||
.header("Origin", BASE_WV_URL)
|
.header("Origin", BASE_WV_URL)
|
||||||
.header("Referer", "$BASE_WV_URL/transfer/quick")
|
.header("Referer", "$BASE_WV_URL/transfer/quick")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute a transfer. bankNo=2 → MIB internal (/transferInternal), bankNo=3 → local/BML (/transferLocal).
|
||||||
|
* currencyCode: "462"=MVR, "840"=USD. purpose uses "-" if blank.
|
||||||
|
*/
|
||||||
|
fun transfer(
|
||||||
|
session: MibSession,
|
||||||
|
fromAccount: String,
|
||||||
|
toAccount: String,
|
||||||
|
amount: String,
|
||||||
|
currencyCode: String,
|
||||||
|
benefName: String,
|
||||||
|
bankNo: Int,
|
||||||
|
purpose: String,
|
||||||
|
otp: String
|
||||||
|
): MibTransferResult {
|
||||||
|
val endpoint = if (bankNo == 2) "transferInternal" else "transferLocal"
|
||||||
|
val body = FormBody.Builder()
|
||||||
|
.add("benefName", benefName)
|
||||||
|
.add("benefNo", "0")
|
||||||
|
.add("fromAccountNo", fromAccount)
|
||||||
|
.add("benefAccountNo", toAccount)
|
||||||
|
.add("transferCy", currencyCode)
|
||||||
|
.add("benefCurrencyCode", currencyCode)
|
||||||
|
.add("amount", amount)
|
||||||
|
.add("bankNo", bankNo.toString())
|
||||||
|
.add("purpose", purpose.ifBlank { "-" })
|
||||||
|
.add("otp", otp)
|
||||||
|
.add("otpType", "3")
|
||||||
|
.build()
|
||||||
|
val request = Request.Builder()
|
||||||
|
.url("$BASE_WV_URL/ajaxTransfer/$endpoint")
|
||||||
|
.post(body)
|
||||||
|
.withWvHeaders(session)
|
||||||
|
.build()
|
||||||
|
return client.newCall(request).execute().use { response ->
|
||||||
|
val bodyStr = response.body?.string() ?: ""
|
||||||
|
val json = try { JSONObject(bodyStr) } catch (_: Exception) { null }
|
||||||
|
if (json == null || !json.optBoolean("success")) {
|
||||||
|
MibTransferResult(
|
||||||
|
success = false,
|
||||||
|
errorMessage = json?.optString("reasonText")?.ifBlank { null } ?: "Transfer failed"
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
val data = json.optJSONArray("data")?.optJSONObject(0)
|
||||||
|
MibTransferResult(
|
||||||
|
success = true,
|
||||||
|
trxId = data?.optString("trxId") ?: "",
|
||||||
|
date = data?.optString("date") ?: ""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Routes the lookup to the correct endpoint based on input format:
|
* Routes the lookup to the correct endpoint based on input format:
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -49,13 +49,22 @@ class ContactPickerSheetFragment : BottomSheetDialogFragment() {
|
|||||||
adapter = ContactPickerAdapter(
|
adapter = ContactPickerAdapter(
|
||||||
onItemClick = { accountNumber, label ->
|
onItemClick = { accountNumber, label ->
|
||||||
val contacts = viewModel.contacts.value ?: emptyList()
|
val contacts = viewModel.contacts.value ?: emptyList()
|
||||||
|
val accounts = viewModel.accounts.value ?: emptyList()
|
||||||
val contact = contacts.firstOrNull { it.benefAccount == accountNumber }
|
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)
|
val bundle = bundleOf(KEY_ACCOUNT_NUMBER to accountNumber, KEY_LABEL to label)
|
||||||
if (contact != null && !contact.transferCyDesc.equals("MVR", ignoreCase = true)) {
|
when {
|
||||||
bundle.putBoolean(KEY_SKIP_LOOKUP, true)
|
account?.profileType == "BML_PREPAID" -> {
|
||||||
bundle.putString(KEY_SUBTITLE, "${contact.benefBankName} · ${contact.benefAccount}")
|
bundle.putBoolean(KEY_SKIP_LOOKUP, true)
|
||||||
bundle.putString(KEY_COLOR, contact.bankColor)
|
bundle.putString(KEY_SUBTITLE, "${account.accountNumber} · ${account.currencyName} ${account.availableBalance}")
|
||||||
contact.customerImgHash?.let { bundle.putString(KEY_IMAGE_HASH, it) }
|
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)
|
setFragmentResult(REQUEST_KEY, bundle)
|
||||||
dismiss()
|
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>) {
|
private fun refreshFinancing(session: MibSession?, profiles: List<MibProfile>) {
|
||||||
if (session == null || profiles.isEmpty()) return
|
if (session == null || profiles.isEmpty()) return
|
||||||
val prefs = getSharedPreferences("mib_prefs", MODE_PRIVATE)
|
val prefs = getSharedPreferences("mib_prefs", MODE_PRIVATE)
|
||||||
|
|||||||
@@ -11,8 +11,11 @@ import android.util.Base64
|
|||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
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 android.widget.Toast
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.core.widget.addTextChangedListener
|
import androidx.core.widget.addTextChangedListener
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
@@ -26,16 +29,22 @@ 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.BmlLoginFlow
|
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.MibAccount
|
||||||
|
import sh.sar.basedbank.api.mib.MibBeneficiary
|
||||||
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
|
||||||
import sh.sar.basedbank.api.mib.MibTransferClient
|
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.FragmentTransferBinding
|
||||||
import sh.sar.basedbank.databinding.ItemAccountDropdownBinding
|
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.PaymvQrParser
|
||||||
import sh.sar.basedbank.util.RecentPick
|
import sh.sar.basedbank.util.RecentPick
|
||||||
import sh.sar.basedbank.util.RecentsCache
|
import sh.sar.basedbank.util.RecentsCache
|
||||||
|
import sh.sar.basedbank.util.Totp
|
||||||
|
|
||||||
class TransferFragment : Fragment() {
|
class TransferFragment : Fragment() {
|
||||||
|
|
||||||
@@ -47,6 +56,23 @@ class TransferFragment : Fragment() {
|
|||||||
private val session get() = (requireActivity().application as BasedBankApp).mibSession
|
private val session get() = (requireActivity().application as BasedBankApp).mibSession
|
||||||
private val bmlSession get() = (requireActivity().application as BasedBankApp).bmlSession
|
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 {
|
companion object {
|
||||||
private const val ARG_ACCOUNT = "contact_account"
|
private const val ARG_ACCOUNT = "contact_account"
|
||||||
private const val ARG_NAME = "contact_name"
|
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 {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
_binding = FragmentTransferBinding.inflate(inflater, container, false)
|
_binding = FragmentTransferBinding.inflate(inflater, container, false)
|
||||||
return binding.root
|
return binding.root
|
||||||
@@ -115,9 +128,7 @@ class TransferFragment : Fragment() {
|
|||||||
qrLauncher.launch(Intent(requireContext(), QrScannerActivity::class.java))
|
qrLauncher.launch(Intent(requireContext(), QrScannerActivity::class.java))
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.btnTransfer.setOnClickListener {
|
binding.btnTransfer.setOnClickListener { initiateTransfer() }
|
||||||
Toast.makeText(requireContext(), R.string.work_in_progress, Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pre-select contact if navigated from contacts page
|
// Pre-select contact if navigated from contacts page
|
||||||
arguments?.getString(ARG_ACCOUNT)?.let { account ->
|
arguments?.getString(ARG_ACCOUNT)?.let { account ->
|
||||||
@@ -136,22 +147,27 @@ class TransferFragment : Fragment() {
|
|||||||
val adapter = AccountDropdownAdapter(requireContext(), accounts)
|
val adapter = AccountDropdownAdapter(requireContext(), accounts)
|
||||||
binding.actvFrom.setAdapter(adapter)
|
binding.actvFrom.setAdapter(adapter)
|
||||||
|
|
||||||
if (accounts.isNotEmpty() && selectedAccount == null) {
|
// No default selection — user must explicitly pick a source account
|
||||||
selectedAccount = accounts[0]
|
|
||||||
binding.actvFrom.setText(accounts[0].toDisplayString(), false)
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.actvFrom.setOnItemClickListener { _, _, position, _ ->
|
binding.actvFrom.setOnItemClickListener { _, _, position, _ ->
|
||||||
selectedAccount = accounts[position]
|
val picked = adapter.getAccount(position) ?: return@setOnItemClickListener
|
||||||
binding.actvFrom.setText(accounts[position].toDisplayString(), false)
|
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() {
|
private fun setupAccountLookup() {
|
||||||
binding.tilTo.setEndIconOnClickListener { lookupAccount() }
|
binding.tilTo.setEndIconOnClickListener { lookupAccount() }
|
||||||
|
|
||||||
binding.btnClearToInfo.setOnClickListener {
|
binding.btnClearToInfo.setOnClickListener {
|
||||||
|
resolvedAccountNumber = ""
|
||||||
|
resolvedRecipientName = ""
|
||||||
binding.cardToInfo.visibility = View.GONE
|
binding.cardToInfo.visibility = View.GONE
|
||||||
binding.tilTo.visibility = View.VISIBLE
|
binding.tilTo.visibility = View.VISIBLE
|
||||||
binding.btnPickContact.visibility = View.VISIBLE
|
binding.btnPickContact.visibility = View.VISIBLE
|
||||||
@@ -162,6 +178,8 @@ class TransferFragment : Fragment() {
|
|||||||
binding.etTo.addTextChangedListener {
|
binding.etTo.addTextChangedListener {
|
||||||
binding.tilTo.error = null
|
binding.tilTo.error = null
|
||||||
if (binding.cardToInfo.visibility == View.VISIBLE) {
|
if (binding.cardToInfo.visibility == View.VISIBLE) {
|
||||||
|
resolvedAccountNumber = ""
|
||||||
|
resolvedRecipientName = ""
|
||||||
binding.cardToInfo.visibility = View.GONE
|
binding.cardToInfo.visibility = View.GONE
|
||||||
binding.tilTo.visibility = View.VISIBLE
|
binding.tilTo.visibility = View.VISIBLE
|
||||||
binding.btnPickContact.visibility = View.VISIBLE
|
binding.btnPickContact.visibility = View.VISIBLE
|
||||||
@@ -206,7 +224,6 @@ class TransferFragment : Fragment() {
|
|||||||
var errorMsg: String? = null
|
var errorMsg: String? = null
|
||||||
val info = withContext(Dispatchers.IO) {
|
val info = withContext(Dispatchers.IO) {
|
||||||
if (isBmlSource && bmlSess != null) {
|
if (isBmlSource && bmlSess != null) {
|
||||||
// BML source: prefer BML validate, fall back to MIB IPS
|
|
||||||
val bmlResult = try { BmlLoginFlow().validateAccount(bmlSess, accountNumber) } catch (_: Exception) { null }
|
val bmlResult = try { BmlLoginFlow().validateAccount(bmlSess, accountNumber) } catch (_: Exception) { null }
|
||||||
if (bmlResult != null) {
|
if (bmlResult != null) {
|
||||||
val bankId = when (bmlResult.trnType) {
|
val bankId = when (bmlResult.trnType) {
|
||||||
@@ -222,13 +239,11 @@ class TransferFragment : Fragment() {
|
|||||||
errorMsg = getString(R.string.transfer_account_not_found); null
|
errorMsg = getString(R.string.transfer_account_not_found); null
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// MIB source (or no preference): prefer MIB, fall back to BML validate
|
|
||||||
if (mibSess != null) {
|
if (mibSess != null) {
|
||||||
try { MibTransferClient().lookup(mibSess, accountNumber) }
|
try { MibTransferClient().lookup(mibSess, accountNumber) }
|
||||||
catch (e: MibLookupException) { errorMsg = e.message; null }
|
catch (e: MibLookupException) { errorMsg = e.message; null }
|
||||||
catch (_: Exception) { errorMsg = getString(R.string.transfer_account_not_found); null }
|
catch (_: Exception) { errorMsg = getString(R.string.transfer_account_not_found); null }
|
||||||
} else {
|
} else {
|
||||||
// MIB not available, try BML
|
|
||||||
val bmlResult = try { BmlLoginFlow().validateAccount(bmlSess!!, accountNumber) } catch (_: Exception) { null }
|
val bmlResult = try { BmlLoginFlow().validateAccount(bmlSess!!, accountNumber) } catch (_: Exception) { null }
|
||||||
if (bmlResult != null) {
|
if (bmlResult != null) {
|
||||||
val bankId = when (bmlResult.trnType) {
|
val bankId = when (bmlResult.trnType) {
|
||||||
@@ -245,14 +260,14 @@ class TransferFragment : Fragment() {
|
|||||||
binding.tilTo.isEnabled = true
|
binding.tilTo.isEnabled = true
|
||||||
if (info != null) {
|
if (info != null) {
|
||||||
val accounts = viewModel.accounts.value ?: emptyList()
|
val accounts = viewModel.accounts.value ?: emptyList()
|
||||||
val contacts = viewModel.contacts.value ?: emptyList()
|
|
||||||
val matchedAcc = accounts.firstOrNull { it.accountNumber == info.accountNumber }
|
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
|
val displayName = matchedAcc?.accountBriefName ?: matchedCont?.benefNickName ?: info.accountName
|
||||||
?: matchedContact?.benefNickName
|
val colorHex = if (matchedAcc != null) "#FE860E" else matchedCont?.bankColor ?: "#607D8B"
|
||||||
?: info.accountName
|
|
||||||
val colorHex = if (matchedAcc != null) "#FE860E" else matchedContact?.bankColor ?: "#607D8B"
|
resolvedAccountNumber = info.accountNumber
|
||||||
|
resolvedRecipientName = info.accountName
|
||||||
|
|
||||||
binding.tvToAccountName.text = displayName
|
binding.tvToAccountName.text = displayName
|
||||||
binding.tvToBankBic.text = "${info.accountNumber} · ${info.bankId}"
|
binding.tvToBankBic.text = "${info.accountNumber} · ${info.bankId}"
|
||||||
@@ -266,8 +281,8 @@ class TransferFragment : Fragment() {
|
|||||||
when {
|
when {
|
||||||
matchedAcc?.profileImageHash != null ->
|
matchedAcc?.profileImageHash != null ->
|
||||||
loadToPhoto(matchedAcc.profileImageHash, isProfile = true)
|
loadToPhoto(matchedAcc.profileImageHash, isProfile = true)
|
||||||
matchedContact?.customerImgHash != null ->
|
matchedCont?.customerImgHash != null ->
|
||||||
loadToPhoto(matchedContact.customerImgHash, isProfile = false)
|
loadToPhoto(matchedCont.customerImgHash, isProfile = false)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Toast.makeText(requireContext(), errorMsg, Toast.LENGTH_SHORT).show()
|
Toast.makeText(requireContext(), errorMsg, Toast.LENGTH_SHORT).show()
|
||||||
@@ -282,6 +297,9 @@ class TransferFragment : Fragment() {
|
|||||||
colorHex: String,
|
colorHex: String,
|
||||||
imageHash: String?
|
imageHash: String?
|
||||||
) {
|
) {
|
||||||
|
resolvedAccountNumber = accountNumber
|
||||||
|
resolvedRecipientName = displayName
|
||||||
|
|
||||||
binding.tvToAccountName.text = displayName
|
binding.tvToAccountName.text = displayName
|
||||||
binding.tvToBankBic.text = subtitle
|
binding.tvToBankBic.text = subtitle
|
||||||
binding.ivToPhoto.setImageBitmap(makeInitialsBitmap(displayName, colorHex))
|
binding.ivToPhoto.setImageBitmap(makeInitialsBitmap(displayName, colorHex))
|
||||||
@@ -306,6 +324,8 @@ class TransferFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun prefillToFromContact(accountNumber: String, label: String) {
|
private fun prefillToFromContact(accountNumber: String, label: String) {
|
||||||
|
resolvedAccountNumber = ""
|
||||||
|
resolvedRecipientName = ""
|
||||||
binding.cardToInfo.visibility = View.GONE
|
binding.cardToInfo.visibility = View.GONE
|
||||||
binding.tilTo.visibility = View.VISIBLE
|
binding.tilTo.visibility = View.VISIBLE
|
||||||
binding.btnPickContact.visibility = View.VISIBLE
|
binding.btnPickContact.visibility = View.VISIBLE
|
||||||
@@ -315,6 +335,206 @@ class TransferFragment : Fragment() {
|
|||||||
lookupAccount()
|
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) {
|
private fun loadToPhoto(hash: String, isProfile: Boolean) {
|
||||||
val sess = session ?: return
|
val sess = session ?: return
|
||||||
val app = requireActivity().application as BasedBankApp
|
val app = requireActivity().application as BasedBankApp
|
||||||
@@ -381,7 +601,6 @@ class TransferFragment : Fragment() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Manual entry not in contacts/accounts — save with resolved info
|
|
||||||
RecentsCache.save(requireContext(), RecentPick(
|
RecentsCache.save(requireContext(), RecentPick(
|
||||||
accountNumber = info.accountNumber,
|
accountNumber = info.accountNumber,
|
||||||
displayName = info.accountName,
|
displayName = info.accountName,
|
||||||
@@ -404,29 +623,67 @@ class TransferFragment : Fragment() {
|
|||||||
|
|
||||||
private fun MibAccount.toDisplayString() = "$accountBriefName · $accountNumber"
|
private fun MibAccount.toDisplayString() = "$accountBriefName · $accountNumber"
|
||||||
|
|
||||||
|
// items: String = section header, MibAccount = selectable row
|
||||||
private inner class AccountDropdownAdapter(
|
private inner class AccountDropdownAdapter(
|
||||||
context: Context,
|
private val context: Context,
|
||||||
private val accounts: List<MibAccount>
|
accounts: List<MibAccount>
|
||||||
) : ArrayAdapter<String>(context, android.R.layout.simple_list_item_1, accounts.map { it.toDisplayString() }) {
|
) : BaseAdapter(), Filterable {
|
||||||
|
|
||||||
private fun bindDropdown(convertView: View?, parent: ViewGroup, position: Int): View {
|
private val items: List<Any> = buildList {
|
||||||
val itemBinding = if (convertView?.tag is ItemAccountDropdownBinding) {
|
val regular = accounts.filter { it.profileType != "BML_PREPAID" }
|
||||||
convertView.tag as ItemAccountDropdownBinding
|
val cards = accounts.filter { it.profileType == "BML_PREPAID" }
|
||||||
} else {
|
addAll(regular)
|
||||||
ItemAccountDropdownBinding.inflate(LayoutInflater.from(context), parent, false)
|
if (cards.isNotEmpty()) {
|
||||||
.also { it.root.tag = it }
|
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) =
|
fun getAccount(position: Int): MibAccount? = (items.getOrNull(position) as? MibAccount)
|
||||||
bindDropdown(convertView, parent, position)
|
?.takeUnless { it.profileType == "BML_PREPAID" && !it.statusDesc.equals("Active", ignoreCase = true) }
|
||||||
|
|
||||||
override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup) =
|
override fun getCount() = items.size
|
||||||
bindDropdown(convertView, parent, position)
|
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?) = ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ object AccountCache {
|
|||||||
put("mvrBalance", acc.mvrBalance)
|
put("mvrBalance", acc.mvrBalance)
|
||||||
put("statusDesc", acc.statusDesc)
|
put("statusDesc", acc.statusDesc)
|
||||||
put("loginTag", acc.loginTag)
|
put("loginTag", acc.loginTag)
|
||||||
|
put("internalId", acc.internalId)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||||
@@ -77,7 +78,8 @@ object AccountCache {
|
|||||||
mvrBalance = o.optString("mvrBalance"),
|
mvrBalance = o.optString("mvrBalance"),
|
||||||
statusDesc = o.optString("statusDesc"),
|
statusDesc = o.optString("statusDesc"),
|
||||||
profileImageHash = null,
|
profileImageHash = null,
|
||||||
loginTag = o.optString("loginTag")
|
loginTag = o.optString("loginTag"),
|
||||||
|
internalId = o.optString("internalId", "")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) { emptyList() }
|
} catch (e: Exception) { emptyList() }
|
||||||
|
|||||||
@@ -115,6 +115,11 @@
|
|||||||
<string name="transfer_session_unavailable">Session unavailable — please re-login</string>
|
<string name="transfer_session_unavailable">Session unavailable — please re-login</string>
|
||||||
<string name="transfer_amount">Amount</string>
|
<string name="transfer_amount">Amount</string>
|
||||||
<string name="transfer_remarks">Remarks</string>
|
<string name="transfer_remarks">Remarks</string>
|
||||||
|
<string name="transfer_confirm">Confirm</string>
|
||||||
|
<string name="transfer_success">Transfer Successful</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_missing_internal_id">Account data is incomplete — please re-login to refresh.</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