diff --git a/app/src/main/java/sh/sar/basedbank/ui/home/DashboardFragment.kt b/app/src/main/java/sh/sar/basedbank/ui/home/DashboardFragment.kt index ac020e3..3664459 100644 --- a/app/src/main/java/sh/sar/basedbank/ui/home/DashboardFragment.kt +++ b/app/src/main/java/sh/sar/basedbank/ui/home/DashboardFragment.kt @@ -25,6 +25,7 @@ import sh.sar.basedbank.api.models.BankAccount import sh.sar.basedbank.api.mib.MibCard import sh.sar.basedbank.api.mib.MibFinanceDeal import sh.sar.basedbank.util.bmlapi.BmlCardParser +import sh.sar.basedbank.util.CredentialStore import kotlin.math.abs import sh.sar.basedbank.databinding.FragmentDashboardBinding import sh.sar.basedbank.databinding.ItemForeignLimitBinding @@ -101,8 +102,13 @@ class DashboardFragment : Fragment() { .filter { (it.profileType == "BML_PREPAID" || it.profileType == "BML_CREDIT" || it.profileType == "BML_DEBIT") && it.statusDesc.equals("Active", ignoreCase = true) } .map { CardItem.Bml(it) } val all = mibItems + bmlItems - cardAdapter.update(all) - binding.sectionCards.visibility = if (all.isNotEmpty()) View.VISIBLE else View.GONE + val defaultNum = CredentialStore(requireContext()).getDefaultCardAccountNumber() + val ordered = if (defaultNum != null) { + val def = all.filterIsInstance().firstOrNull { it.account.accountNumber == defaultNum } + if (def != null) listOf(def) + all.filter { it !== def } else all + } else all + cardAdapter.update(ordered) + binding.sectionCards.visibility = if (ordered.isNotEmpty()) View.VISIBLE else View.GONE } viewModel.mibCards.observe(viewLifecycleOwner) { updateCardList() } viewModel.accounts.observe(viewLifecycleOwner) { updateCardList() } diff --git a/app/src/main/java/sh/sar/basedbank/ui/home/PayWithCardFragment.kt b/app/src/main/java/sh/sar/basedbank/ui/home/PayWithCardFragment.kt index 1369e4a..c452720 100644 --- a/app/src/main/java/sh/sar/basedbank/ui/home/PayWithCardFragment.kt +++ b/app/src/main/java/sh/sar/basedbank/ui/home/PayWithCardFragment.kt @@ -29,6 +29,7 @@ import sh.sar.basedbank.R import sh.sar.basedbank.api.mib.MibCard import sh.sar.basedbank.databinding.FragmentCardsBinding import sh.sar.basedbank.util.CardsCache +import sh.sar.basedbank.util.CredentialStore import sh.sar.basedbank.util.bmlapi.BmlCardParser import kotlin.math.abs @@ -53,6 +54,9 @@ class CardsFragment : Fragment() { private var swipeDragStartRawY = 0f private var swipeIsDragging = false + private lateinit var stackAdapter: CardStackAdapter + private val store by lazy { CredentialStore(requireContext()) } + 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 @@ -77,7 +81,7 @@ class CardsFragment : Fragment() { val peekPx = screenW / 8 cardWidth = screenW - 2 * peekPx - val stackAdapter = CardStackAdapter(cardWidth) + stackAdapter = CardStackAdapter(cardWidth) binding.rvCards.layoutManager = LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false) binding.rvCards.adapter = stackAdapter binding.rvCards.setPadding(peekPx, 0, peekPx, 0) @@ -114,24 +118,8 @@ ViewCompat.setOnApplyWindowInsetsListener(binding.contentLayout) { v, insets -> insets } - val updateCardList = { - val mibItems = (viewModel.mibCards.value ?: emptyList()).map { CardItem.Mib(it) } - val bmlItems = (viewModel.accounts.value ?: emptyList()) - .filter { it.profileType == "BML_PREPAID" || it.profileType == "BML_CREDIT" || it.profileType == "BML_DEBIT" } - .map { CardItem.Bml(it) } - cards = mibItems + bmlItems - stackAdapter.update(cards) - binding.loadingView.visibility = View.GONE - val empty = cards.isEmpty() - binding.emptyView.visibility = if (empty) View.VISIBLE else View.GONE - binding.contentLayout.visibility = if (empty) View.GONE else View.VISIBLE - if (!empty) { - buildDots(cards.size, currentCardPosition) - updateCardInfo(currentCardPosition) - } - } - viewModel.mibCards.observe(viewLifecycleOwner) { updateCardList() } - viewModel.accounts.observe(viewLifecycleOwner) { updateCardList() } + viewModel.mibCards.observe(viewLifecycleOwner) { rebuildCards() } + viewModel.accounts.observe(viewLifecycleOwner) { rebuildCards() } val cached = CardsCache.load(requireContext()) if (cached.isNotEmpty()) { @@ -264,8 +252,27 @@ ViewCompat.setOnApplyWindowInsetsListener(binding.contentLayout) { v, insets -> binding.pageIndicator.visibility = View.GONE binding.llPayButtons.visibility = View.GONE binding.llManageButtons.visibility = View.VISIBLE + binding.llDefaultCardRow.visibility = View.VISIBLE binding.manageCardView.root.visibility = View.VISIBLE + // Set switch state (clear listener first to avoid triggering on programmatic set) + val isBml = item is CardItem.Bml + binding.switchDefaultCard.setOnCheckedChangeListener(null) + binding.switchDefaultCard.isChecked = isBml && store.getDefaultCardAccountNumber() == (item as? CardItem.Bml)?.account?.accountNumber + binding.switchDefaultCard.setOnCheckedChangeListener { _, isChecked -> + if (item is CardItem.Mib) { + // MIB doesn't support NFC/QR pay — same toast as scan/tap to pay + binding.switchDefaultCard.setOnCheckedChangeListener(null) + binding.switchDefaultCard.isChecked = false + binding.switchDefaultCard.setOnCheckedChangeListener { _, c -> + handleDefaultCardToggle(c) + } + Toast.makeText(requireContext(), R.string.mib_qr_nfc_not_supported, Toast.LENGTH_SHORT).show() + } else { + handleDefaultCardToggle(isChecked) + } + } + // After layout pass, compute offsets, save carousel snapshot, and animate binding.contentLayout.doOnNextLayout { val mgr = binding.manageCardView.root @@ -308,6 +315,12 @@ ViewCompat.setOnApplyWindowInsetsListener(binding.contentLayout) { v, insets -> } } + private fun handleDefaultCardToggle(isChecked: Boolean) { + val item = cards.getOrNull(currentCardPosition) as? CardItem.Bml ?: return + store.setDefaultCardAccountNumber(if (isChecked) item.account.accountNumber else null) + rebuildCards() + } + private fun exitManageMode() { binding.manageCardView.root.animate().cancel() binding.tvSelectedCardType.animate().cancel() @@ -350,6 +363,8 @@ ViewCompat.setOnApplyWindowInsetsListener(binding.contentLayout) { v, insets -> binding.rvCards.visibility = View.VISIBLE binding.llPayButtons.visibility = View.VISIBLE binding.llManageButtons.visibility = View.GONE + binding.llDefaultCardRow.visibility = View.GONE + binding.switchDefaultCard.setOnCheckedChangeListener(null) buildDots(cards.size, currentCardPosition) } .start() @@ -362,6 +377,42 @@ ViewCompat.setOnApplyWindowInsetsListener(binding.contentLayout) { v, insets -> .start() } + private fun rebuildCards() { + // Remember which card is currently selected by identity so we can restore position after reorder + val currentCard = cards.getOrNull(currentCardPosition) + + val defaultNum = store.getDefaultCardAccountNumber() + val mibItems = (viewModel.mibCards.value ?: emptyList()).map { CardItem.Mib(it) } + val bmlItems = (viewModel.accounts.value ?: emptyList()) + .filter { it.profileType == "BML_PREPAID" || it.profileType == "BML_CREDIT" || it.profileType == "BML_DEBIT" } + .map { CardItem.Bml(it) } + val all: List = mibItems + bmlItems + // Move default BML card to front + cards = if (defaultNum != null) { + val def = all.filterIsInstance().firstOrNull { it.account.accountNumber == defaultNum } + if (def != null) listOf(def) + all.filter { it !== def } else all + } else all + + // Restore position to follow the same card after reorder + if (currentCard != null) { + val newPos = cards.indexOf(currentCard) + if (newPos >= 0 && newPos != currentCardPosition) { + currentCardPosition = newPos + binding.rvCards.scrollToPosition(newPos) + } + } + + stackAdapter.update(cards) + binding.loadingView.visibility = View.GONE + val empty = cards.isEmpty() + binding.emptyView.visibility = if (empty) View.VISIBLE else View.GONE + binding.contentLayout.visibility = if (empty) View.GONE else View.VISIBLE + if (!empty) { + buildDots(cards.size, currentCardPosition) + updateCardInfo(currentCardPosition) + } + } + private fun applyCardScales() { val rv = binding.rvCards val rvCenter = rv.paddingStart + (rv.width - rv.paddingStart - rv.paddingEnd) / 2f diff --git a/app/src/main/java/sh/sar/basedbank/ui/home/TransferFragment.kt b/app/src/main/java/sh/sar/basedbank/ui/home/TransferFragment.kt index f4a0db5..3a2309c 100644 --- a/app/src/main/java/sh/sar/basedbank/ui/home/TransferFragment.kt +++ b/app/src/main/java/sh/sar/basedbank/ui/home/TransferFragment.kt @@ -293,6 +293,25 @@ class TransferFragment : Fragment() { } bmlQrInfo = info + // Auto-select the user's default BML card if no card was pre-selected + if (selectedAccount == null) { + val defaultNum = CredentialStore(requireContext()).getDefaultCardAccountNumber() + if (defaultNum != null) { + val allAccounts = viewModel.accounts.value ?: emptyList() + val defaultCard = allAccounts.firstOrNull { + it.accountNumber == defaultNum && + (it.profileType == "BML_PREPAID" || it.profileType == "BML_CREDIT" || it.profileType == "BML_DEBIT") && + it.statusDesc.equals("Active", ignoreCase = true) + } + if (defaultCard != null) { + selectedAccount = defaultCard + updateAmountPrefix(defaultCard) + showFromCard(defaultCard) + updateTransferButton() + } + } + } + // Show merchant in the "To" card — clear button hidden (can't change recipient for QR) binding.tvToAccountName.text = info.merchantName binding.tvToBankBic.text = info.merchantAddress.ifBlank { "BML Merchant" } diff --git a/app/src/main/java/sh/sar/basedbank/util/CredentialStore.kt b/app/src/main/java/sh/sar/basedbank/util/CredentialStore.kt index 3f19102..29ccd00 100644 --- a/app/src/main/java/sh/sar/basedbank/util/CredentialStore.kt +++ b/app/src/main/java/sh/sar/basedbank/util/CredentialStore.kt @@ -615,6 +615,18 @@ class CredentialStore(context: Context) { } catch (_: Exception) { null } } + // ── Default payment card ────────────────────────────────────────────────── + + /** BML card account number the user has pinned as their default payment card, or null. */ + fun getDefaultCardAccountNumber(): String? = prefs.getString("default_card_account_number", null) + + fun setDefaultCardAccountNumber(accountNumber: String?) { + val editor = prefs.edit() + if (accountNumber == null) editor.remove("default_card_account_number") + else editor.putString("default_card_account_number", accountNumber) + editor.apply() + } + // ── MIB profile visibility (per loginId) ───────────────────────────────── /** Returns the set of MIB profile IDs the user has chosen to hide (for a given loginId). */ diff --git a/app/src/main/res/layout/fragment_cards.xml b/app/src/main/res/layout/fragment_cards.xml index 76e4139..0cea026 100644 --- a/app/src/main/res/layout/fragment_cards.xml +++ b/app/src/main/res/layout/fragment_cards.xml @@ -147,6 +147,32 @@ + + + + + + + + + Tap to Pay Skill issue on MIB side, Not supported Manage Card + Set as Default Card Change PIN Freeze Block