diff --git a/app/src/main/java/sh/sar/basedbank/api/bml/BmlLoginFlow.kt b/app/src/main/java/sh/sar/basedbank/api/bml/BmlLoginFlow.kt index 4bfc7dc..2e4c1d1 100644 --- a/app/src/main/java/sh/sar/basedbank/api/bml/BmlLoginFlow.kt +++ b/app/src/main/java/sh/sar/basedbank/api/bml/BmlLoginFlow.kt @@ -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 { @@ -181,7 +181,7 @@ class BmlLoginFlow { .header("x-app-version", APP_VERSION) .build() - private fun parseDashboard(json: String): List { + private fun parseDashboard(json: String, loginTag: String): List { 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 diff --git a/app/src/main/java/sh/sar/basedbank/api/mib/MibLoginFlow.kt b/app/src/main/java/sh/sar/basedbank/api/mib/MibLoginFlow.kt index 94b5940..04ca95f 100644 --- a/app/src/main/java/sh/sar/basedbank/api/mib/MibLoginFlow.kt +++ b/app/src/main/java/sh/sar/basedbank/api/mib/MibLoginFlow.kt @@ -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): List { + private fun fetchAllProfiles(session: MibSession, profiles: List, loginTag: String): List { val allAccounts = mutableListOf() 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 ) ) } diff --git a/app/src/main/java/sh/sar/basedbank/api/mib/MibModels.kt b/app/src/main/java/sh/sar/basedbank/api/mib/MibModels.kt index 4a73572..b8b7062 100644 --- a/app/src/main/java/sh/sar/basedbank/api/mib/MibModels.kt +++ b/app/src/main/java/sh/sar/basedbank/api/mib/MibModels.kt @@ -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( diff --git a/app/src/main/java/sh/sar/basedbank/ui/home/ContactPickerAdapter.kt b/app/src/main/java/sh/sar/basedbank/ui/home/ContactPickerAdapter.kt index 3139f6f..38e64bd 100644 --- a/app/src/main/java/sh/sar/basedbank/ui/home/ContactPickerAdapter.kt +++ b/app/src/main/java/sh/sar/basedbank/ui/home/ContactPickerAdapter.kt @@ -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 diff --git a/app/src/main/java/sh/sar/basedbank/ui/home/ContactPickerSheetFragment.kt b/app/src/main/java/sh/sar/basedbank/ui/home/ContactPickerSheetFragment.kt index 4769f87..74e09d4 100644 --- a/app/src/main/java/sh/sar/basedbank/ui/home/ContactPickerSheetFragment.kt +++ b/app/src/main/java/sh/sar/basedbank/ui/home/ContactPickerSheetFragment.kt @@ -76,10 +76,13 @@ class ContactPickerSheetFragment : BottomSheetDialogFragment() { binding.etSheetSearch.addTextChangedListener { rebuildList(selectedAccountNumber) } - // Tabs: Recents | All | + // Tabs: Recents | My Accounts | All | 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) diff --git a/app/src/main/java/sh/sar/basedbank/util/AccountCache.kt b/app/src/main/java/sh/sar/basedbank/util/AccountCache.kt index a0b639b..7dee68a 100644 --- a/app/src/main/java/sh/sar/basedbank/util/AccountCache.kt +++ b/app/src/main/java/sh/sar/basedbank/util/AccountCache.kt @@ -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) { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c062222..b0e3021 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -92,7 +92,9 @@ ދިވެހި + This is your source account Accounts + Cards Available Balance