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