optimize contact picker ui to disable forbidden transfers
This commit is contained in:
@@ -164,7 +164,7 @@ class BmlLoginFlow {
|
||||
val resp = apiClient.newCall(apiRequest(session, "$BASE_URL/api/mobile/dashboard")).execute()
|
||||
val json = resp.body?.string() ?: return emptyList()
|
||||
resp.close()
|
||||
return parseDashboard(json)
|
||||
return parseDashboard(json, "bml_${session.deviceId}")
|
||||
}
|
||||
|
||||
fun fetchContacts(session: BmlSession): List<MibBeneficiary> {
|
||||
@@ -181,7 +181,7 @@ class BmlLoginFlow {
|
||||
.header("x-app-version", APP_VERSION)
|
||||
.build()
|
||||
|
||||
private fun parseDashboard(json: String): List<MibAccount> {
|
||||
private fun parseDashboard(json: String, loginTag: String): List<MibAccount> {
|
||||
val root = JSONObject(json)
|
||||
if (!root.optBoolean("success")) return emptyList()
|
||||
val dashboard = root.optJSONObject("payload")?.optJSONArray("dashboard") ?: return emptyList()
|
||||
@@ -211,7 +211,8 @@ class BmlLoginFlow {
|
||||
blockedAmount = "%.2f".format(item.optDouble("lockedAmount", 0.0)),
|
||||
mvrBalance = if (currency == "MVR") "%.2f".format(available) else "0.00",
|
||||
statusDesc = status,
|
||||
profileImageHash = null
|
||||
profileImageHash = null,
|
||||
loginTag = loginTag
|
||||
))
|
||||
} else if (accountType == "Card") {
|
||||
val isPrepaid = item.optBoolean("prepaid_card", false)
|
||||
@@ -230,7 +231,8 @@ class BmlLoginFlow {
|
||||
blockedAmount = "0.00",
|
||||
mvrBalance = if (currency == "MVR") "%.2f".format(available) else "0.00",
|
||||
statusDesc = status,
|
||||
profileImageHash = null
|
||||
profileImageHash = null,
|
||||
loginTag = loginTag
|
||||
))
|
||||
} else {
|
||||
// Linked debit cards have no independent balance or account link — skip
|
||||
|
||||
@@ -125,7 +125,7 @@ class MibLoginFlow(private val prefs: android.content.SharedPreferences) {
|
||||
|
||||
lastSession = session2
|
||||
lastProfiles = profiles
|
||||
return fetchAllProfiles(session2, profiles)
|
||||
return fetchAllProfiles(session2, profiles, "mib_$username")
|
||||
}
|
||||
|
||||
// ─── Helpers ─────────────────────────────────────────────────────────────
|
||||
@@ -204,7 +204,7 @@ class MibLoginFlow(private val prefs: android.content.SharedPreferences) {
|
||||
doRequest(session, payload, "n")
|
||||
}
|
||||
|
||||
private fun fetchAllProfiles(session: MibSession, profiles: List<MibProfile>): List<MibAccount> {
|
||||
private fun fetchAllProfiles(session: MibSession, profiles: List<MibProfile>, loginTag: String): List<MibAccount> {
|
||||
val allAccounts = mutableListOf<MibAccount>()
|
||||
for (profile in profiles) {
|
||||
val payload = baseData(session, "P47").apply {
|
||||
@@ -231,7 +231,8 @@ class MibLoginFlow(private val prefs: android.content.SharedPreferences) {
|
||||
blockedAmount = a.optString("blockedAmount"),
|
||||
mvrBalance = a.optString("mvrBalance"),
|
||||
statusDesc = a.optString("statusDesc"),
|
||||
profileImageHash = profile.customerImage
|
||||
profileImageHash = profile.customerImage,
|
||||
loginTag = loginTag
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -31,7 +31,8 @@ data class MibAccount(
|
||||
val blockedAmount: String,
|
||||
val mvrBalance: String,
|
||||
val statusDesc: String,
|
||||
val profileImageHash: String?
|
||||
val profileImageHash: String?,
|
||||
val loginTag: String = ""
|
||||
)
|
||||
|
||||
data class MibBeneficiaryCategory(
|
||||
|
||||
@@ -7,7 +7,9 @@ import android.graphics.Color
|
||||
import android.graphics.Paint
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import sh.sar.basedbank.R
|
||||
import sh.sar.basedbank.databinding.ItemPickerRowBinding
|
||||
import sh.sar.basedbank.databinding.ItemPickerSectionHeaderBinding
|
||||
|
||||
@@ -27,7 +29,8 @@ class ContactPickerAdapter(
|
||||
val colorHex: String,
|
||||
val isSameAsFrom: Boolean = false,
|
||||
val isManualEntry: Boolean = false,
|
||||
val imageHash: String? = null
|
||||
val imageHash: String? = null,
|
||||
val inactiveReason: String? = null
|
||||
) : PickerItem()
|
||||
}
|
||||
|
||||
@@ -96,10 +99,15 @@ class ContactPickerAdapter(
|
||||
if (item.imageHash != null) onImageNeeded?.invoke(item.imageHash)
|
||||
}
|
||||
|
||||
binding.root.alpha = if (item.isSameAsFrom) 0.4f else 1.0f
|
||||
binding.root.alpha = if (item.isSameAsFrom || item.inactiveReason != null) 0.4f else 1.0f
|
||||
binding.root.setOnClickListener {
|
||||
if (item.isSameAsFrom) onSameAsFrom()
|
||||
else onItemClick(item.accountNumber, item.displayName)
|
||||
when {
|
||||
item.inactiveReason != null ->
|
||||
Toast.makeText(binding.root.context, item.inactiveReason, Toast.LENGTH_SHORT).show()
|
||||
item.isSameAsFrom ->
|
||||
Toast.makeText(binding.root.context, R.string.transfer_same_account, Toast.LENGTH_SHORT).show()
|
||||
else -> onItemClick(item.accountNumber, item.displayName)
|
||||
}
|
||||
}
|
||||
binding.root.setOnLongClickListener { view ->
|
||||
onItemLongClick?.invoke(item.accountNumber, view) ?: false
|
||||
|
||||
@@ -76,10 +76,13 @@ class ContactPickerSheetFragment : BottomSheetDialogFragment() {
|
||||
|
||||
binding.etSheetSearch.addTextChangedListener { rebuildList(selectedAccountNumber) }
|
||||
|
||||
// Tabs: Recents | All | <categories...>
|
||||
// Tabs: Recents | My Accounts | All | <categories...>
|
||||
binding.sheetCategoryTabs.addTab(
|
||||
binding.sheetCategoryTabs.newTab().setText(R.string.contacts_tab_recents).apply { tag = RECENTS_TAG }
|
||||
)
|
||||
binding.sheetCategoryTabs.addTab(
|
||||
binding.sheetCategoryTabs.newTab().setText(R.string.transfer_my_accounts).apply { tag = MY_ACCOUNTS_TAG }
|
||||
)
|
||||
binding.sheetCategoryTabs.addTab(
|
||||
binding.sheetCategoryTabs.newTab().setText(R.string.contacts_tab_all).apply { tag = null }
|
||||
)
|
||||
@@ -93,8 +96,8 @@ class ContactPickerSheetFragment : BottomSheetDialogFragment() {
|
||||
})
|
||||
|
||||
viewModel.contactCategories.observe(viewLifecycleOwner) { cats ->
|
||||
// Remove all tabs after "All" (index 1) and re-add categories
|
||||
while (binding.sheetCategoryTabs.tabCount > 2) binding.sheetCategoryTabs.removeTabAt(2)
|
||||
// Remove all tabs after "All" (index 2) and re-add categories
|
||||
while (binding.sheetCategoryTabs.tabCount > 3) binding.sheetCategoryTabs.removeTabAt(3)
|
||||
for (cat in cats) {
|
||||
binding.sheetCategoryTabs.addTab(
|
||||
binding.sheetCategoryTabs.newTab().setText(cat.categoryName).apply { tag = cat.id }
|
||||
@@ -133,25 +136,63 @@ class ContactPickerSheetFragment : BottomSheetDialogFragment() {
|
||||
|
||||
val accounts = viewModel.accounts.value ?: emptyList()
|
||||
val contacts = viewModel.contacts.value ?: emptyList()
|
||||
val fromAccount = accounts.find { it.accountNumber == fromNumber }
|
||||
val fromCurrency = fromAccount?.currencyName ?: ""
|
||||
val fromLoginTag = fromAccount?.loginTag ?: ""
|
||||
val fromIsCard = fromAccount?.profileType == "BML_PREPAID"
|
||||
|
||||
if (activeCategoryId == null) {
|
||||
val filtered = if (search.isBlank()) accounts else accounts.filter {
|
||||
if (activeCategoryId == MY_ACCOUNTS_TAG) {
|
||||
val regularAccounts = accounts.filter { it.profileType != "BML_PREPAID" }
|
||||
val cards = accounts.filter { it.profileType == "BML_PREPAID" }
|
||||
|
||||
val filteredRegular = if (search.isBlank()) regularAccounts else regularAccounts.filter {
|
||||
it.accountBriefName.contains(search, ignoreCase = true) || it.accountNumber.contains(search)
|
||||
}
|
||||
if (filtered.isNotEmpty()) {
|
||||
items.add(ContactPickerAdapter.PickerItem.Header(getString(R.string.transfer_my_accounts)))
|
||||
for (acc in filtered) {
|
||||
if (filteredRegular.isNotEmpty()) {
|
||||
items.add(ContactPickerAdapter.PickerItem.Header(getString(R.string.accounts)))
|
||||
for (acc in filteredRegular) {
|
||||
if (acc.profileImageHash != null) profileImageHashes.add(acc.profileImageHash)
|
||||
val isSame = acc.accountNumber == fromNumber
|
||||
items.add(ContactPickerAdapter.PickerItem.Row(
|
||||
accountNumber = acc.accountNumber,
|
||||
displayName = acc.accountBriefName,
|
||||
subtitle = "${acc.accountNumber} · ${acc.currencyName} ${acc.availableBalance}",
|
||||
colorHex = "#FE860E",
|
||||
isSameAsFrom = acc.accountNumber == fromNumber,
|
||||
imageHash = acc.profileImageHash
|
||||
isSameAsFrom = isSame,
|
||||
imageHash = acc.profileImageHash,
|
||||
inactiveReason = if (isSame) null
|
||||
else if (fromIsCard && acc.loginTag != fromLoginTag) "Cards can only be used within the same BML account"
|
||||
else currencyMismatchReason(fromCurrency, acc.currencyName)
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
val filteredCards = if (search.isBlank()) cards else cards.filter {
|
||||
it.accountBriefName.contains(search, ignoreCase = true) || it.accountNumber.contains(search)
|
||||
}
|
||||
if (filteredCards.isNotEmpty()) {
|
||||
items.add(ContactPickerAdapter.PickerItem.Header(getString(R.string.cards)))
|
||||
for (acc in filteredCards) {
|
||||
if (acc.profileImageHash != null) profileImageHashes.add(acc.profileImageHash)
|
||||
val isSame = acc.accountNumber == fromNumber
|
||||
val isActive = acc.statusDesc.equals("Active", ignoreCase = true)
|
||||
items.add(ContactPickerAdapter.PickerItem.Row(
|
||||
accountNumber = acc.accountNumber,
|
||||
displayName = acc.accountBriefName,
|
||||
subtitle = "${acc.accountNumber} · ${acc.currencyName} ${acc.availableBalance}",
|
||||
colorHex = "#FE860E",
|
||||
isSameAsFrom = isSame,
|
||||
imageHash = acc.profileImageHash,
|
||||
inactiveReason = if (isSame) null
|
||||
else if (!isActive) acc.statusDesc
|
||||
else if (acc.loginTag != fromLoginTag) "Cards can only be used within the same BML account"
|
||||
else currencyMismatchReason(fromCurrency, acc.currencyName)
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
adapter.submitList(items)
|
||||
return
|
||||
}
|
||||
|
||||
val filteredContacts = contacts.filter { contact ->
|
||||
@@ -164,9 +205,6 @@ class ContactPickerSheetFragment : BottomSheetDialogFragment() {
|
||||
}
|
||||
|
||||
if (filteredContacts.isNotEmpty()) {
|
||||
if (activeCategoryId == null) {
|
||||
items.add(ContactPickerAdapter.PickerItem.Header(getString(R.string.nav_contacts)))
|
||||
}
|
||||
for (contact in filteredContacts) {
|
||||
items.add(ContactPickerAdapter.PickerItem.Row(
|
||||
accountNumber = contact.benefAccount,
|
||||
@@ -174,7 +212,8 @@ class ContactPickerSheetFragment : BottomSheetDialogFragment() {
|
||||
subtitle = "${contact.benefBankName} · ${contact.benefAccount}",
|
||||
colorHex = contact.bankColor,
|
||||
isSameAsFrom = contact.benefAccount == fromNumber,
|
||||
imageHash = contact.customerImgHash
|
||||
imageHash = contact.customerImgHash,
|
||||
inactiveReason = currencyMismatchReason(fromCurrency, contact.transferCyDesc)
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -203,6 +242,9 @@ class ContactPickerSheetFragment : BottomSheetDialogFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun currencyMismatchReason(fromCurrency: String, toCurrency: String): String? =
|
||||
if (fromCurrency == "MVR" && toCurrency == "USD") "Cannot transfer from MVR to USD account" else null
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
_binding = null
|
||||
@@ -214,6 +256,7 @@ class ContactPickerSheetFragment : BottomSheetDialogFragment() {
|
||||
const val KEY_LABEL = "label"
|
||||
private const val ARG_FROM_ACCOUNT = "fromAccount"
|
||||
private const val RECENTS_TAG = "__recents__"
|
||||
private const val MY_ACCOUNTS_TAG = "__my_accounts__"
|
||||
|
||||
fun newInstance(fromAccountNumber: String) = ContactPickerSheetFragment().apply {
|
||||
arguments = bundleOf(ARG_FROM_ACCOUNT to fromAccountNumber)
|
||||
|
||||
@@ -26,6 +26,7 @@ object AccountCache {
|
||||
put("blockedAmount", acc.blockedAmount)
|
||||
put("mvrBalance", acc.mvrBalance)
|
||||
put("statusDesc", acc.statusDesc)
|
||||
put("loginTag", acc.loginTag)
|
||||
if (acc.profileImageHash != null) put("profileImageHash", acc.profileImageHash)
|
||||
})
|
||||
}
|
||||
@@ -48,6 +49,7 @@ object AccountCache {
|
||||
put("blockedAmount", acc.blockedAmount)
|
||||
put("mvrBalance", acc.mvrBalance)
|
||||
put("statusDesc", acc.statusDesc)
|
||||
put("loginTag", acc.loginTag)
|
||||
})
|
||||
}
|
||||
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||
@@ -73,7 +75,8 @@ object AccountCache {
|
||||
blockedAmount = o.optString("blockedAmount"),
|
||||
mvrBalance = o.optString("mvrBalance"),
|
||||
statusDesc = o.optString("statusDesc"),
|
||||
profileImageHash = null
|
||||
profileImageHash = null,
|
||||
loginTag = o.optString("loginTag")
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) { emptyList() }
|
||||
@@ -98,7 +101,8 @@ object AccountCache {
|
||||
blockedAmount = o.optString("blockedAmount"),
|
||||
mvrBalance = o.optString("mvrBalance"),
|
||||
statusDesc = o.optString("statusDesc"),
|
||||
profileImageHash = o.optString("profileImageHash").takeIf { it.isNotBlank() }
|
||||
profileImageHash = o.optString("profileImageHash").takeIf { it.isNotBlank() },
|
||||
loginTag = o.optString("loginTag")
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
|
||||
@@ -92,7 +92,9 @@
|
||||
<string name="lang_dhivehi">ދިވެހި</string>
|
||||
|
||||
<!-- Home -->
|
||||
<string name="transfer_same_account">This is your source account</string>
|
||||
<string name="accounts">Accounts</string>
|
||||
<string name="cards">Cards</string>
|
||||
<string name="available_balance">Available Balance</string>
|
||||
|
||||
<!-- Transfer -->
|
||||
|
||||
Reference in New Issue
Block a user