Unified card settings and pay with card into 1 page and redsigned it
This commit is contained in:
@@ -1,139 +1,3 @@
|
||||
package sh.sar.basedbank.ui.home
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import sh.sar.basedbank.R
|
||||
import sh.sar.basedbank.databinding.FragmentCardSettingsBinding
|
||||
import sh.sar.basedbank.util.bmlapi.BmlCardParser
|
||||
|
||||
class CardSettingsFragment : Fragment() {
|
||||
|
||||
private var _binding: FragmentCardSettingsBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
private val viewModel: HomeViewModel by activityViewModels()
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
_binding = FragmentCardSettingsBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
val adapter = CardSettingsAdapter(emptyList(), requireContext())
|
||||
binding.recyclerView.layoutManager = LinearLayoutManager(requireContext())
|
||||
binding.recyclerView.adapter = adapter
|
||||
|
||||
val bottomPaddingBase = (16 * resources.displayMetrics.density).toInt()
|
||||
ViewCompat.setOnApplyWindowInsetsListener(binding.recyclerView) { v, insets ->
|
||||
val isBottomNav = requireContext().getSharedPreferences("prefs", Context.MODE_PRIVATE).getBoolean("bottom_nav", false)
|
||||
val navBar = insets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||
val extraBottom = if (isBottomNav) 0 else navBar.bottom
|
||||
v.setPadding(v.paddingLeft, v.paddingTop, v.paddingRight, bottomPaddingBase + extraBottom)
|
||||
insets
|
||||
}
|
||||
|
||||
binding.swipeRefresh.setOnRefreshListener {
|
||||
(activity as? HomeActivity)?.triggerRefresh()
|
||||
}
|
||||
|
||||
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) }
|
||||
val all = mibItems + bmlItems
|
||||
adapter.update(all)
|
||||
binding.loadingView.visibility = View.GONE
|
||||
binding.swipeRefresh.isRefreshing = false
|
||||
binding.emptyView.visibility = if (all.isEmpty()) View.VISIBLE else View.GONE
|
||||
binding.recyclerView.visibility = if (all.isEmpty()) View.GONE else View.VISIBLE
|
||||
}
|
||||
viewModel.mibCards.observe(viewLifecycleOwner) { updateCardList() }
|
||||
viewModel.accounts.observe(viewLifecycleOwner) { updateCardList() }
|
||||
|
||||
if (viewModel.mibCards.value == null) {
|
||||
binding.loadingView.visibility = View.VISIBLE
|
||||
(activity as? HomeActivity)?.triggerRefreshCards()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
requireActivity().title = getString(R.string.nav_card_settings)
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
_binding = null
|
||||
}
|
||||
|
||||
private inner class CardSettingsAdapter(
|
||||
private var cards: List<CardItem>,
|
||||
private val context: Context
|
||||
) : RecyclerView.Adapter<CardSettingsAdapter.VH>() {
|
||||
|
||||
fun update(newCards: List<CardItem>) {
|
||||
cards = newCards
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH =
|
||||
VH(LayoutInflater.from(context).inflate(R.layout.item_card_settings_entry, parent, false))
|
||||
|
||||
override fun onBindViewHolder(holder: VH, position: Int) = holder.bind(cards[position])
|
||||
override fun getItemCount() = cards.size
|
||||
|
||||
inner class VH(view: View) : RecyclerView.ViewHolder(view) {
|
||||
private val ivCardImage: ImageView = view.findViewById(R.id.ivCardImage)
|
||||
private val tvCardOwner: TextView = view.findViewById(R.id.tvCardOwner)
|
||||
private val tvCardNumber: TextView = view.findViewById(R.id.tvCardNumber)
|
||||
private val tvCardType: TextView = view.findViewById(R.id.tvCardType)
|
||||
private val tvCardStatus: TextView = view.findViewById(R.id.tvCardStatus)
|
||||
private val btnChangePin: View = view.findViewById(R.id.btnChangePin)
|
||||
private val btnFreeze: View = view.findViewById(R.id.btnFreeze)
|
||||
private val btnBlock: View = view.findViewById(R.id.btnBlock)
|
||||
|
||||
fun bind(item: CardItem) {
|
||||
when (item) {
|
||||
is CardItem.Mib -> {
|
||||
tvCardOwner.text = item.card.cardHolderName
|
||||
tvCardNumber.text = PayWithCardFragment.formatMasked(item.card.maskedCardNumber)
|
||||
tvCardType.text = item.card.cardTypeDesc
|
||||
val assetPath = PayWithCardFragment.cardImageAsset(item.card)
|
||||
if (assetPath != null) PayWithCardFragment.loadCardImage(ivCardImage, assetPath)
|
||||
else ivCardImage.setImageDrawable(null)
|
||||
PayWithCardFragment.bindCardStatus(tvCardStatus, PayWithCardFragment.mibCardStatusLabel(item.card.cardStatus))
|
||||
itemView.alpha = 1f
|
||||
}
|
||||
is CardItem.Bml -> {
|
||||
tvCardOwner.text = item.account.accountBriefName
|
||||
tvCardNumber.text = PayWithCardFragment.formatMasked(item.account.accountNumber)
|
||||
tvCardType.text = item.account.accountTypeName
|
||||
PayWithCardFragment.loadCardImage(ivCardImage, BmlCardParser.cardImageAsset(item.account))
|
||||
val isActive = item.account.statusDesc.equals("Active", ignoreCase = true)
|
||||
val bmlStatus = item.account.statusDesc.takeUnless { isActive }
|
||||
PayWithCardFragment.bindCardStatus(tvCardStatus, bmlStatus)
|
||||
itemView.alpha = if (isActive) 1f else 0.45f
|
||||
}
|
||||
}
|
||||
val wip = View.OnClickListener {
|
||||
Toast.makeText(context, R.string.work_in_progress, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
btnChangePin.setOnClickListener(wip)
|
||||
btnFreeze.setOnClickListener(wip)
|
||||
btnBlock.setOnClickListener(wip)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Merged into CardsFragment
|
||||
|
||||
@@ -287,17 +287,17 @@ class DashboardFragment : Fragment() {
|
||||
when (item) {
|
||||
is CardItem.Mib -> {
|
||||
tvCardOwner.text = item.card.cardHolderName
|
||||
tvCardNumber.text = PayWithCardFragment.formatMasked(item.card.maskedCardNumber)
|
||||
val assetPath = PayWithCardFragment.cardImageAsset(item.card)
|
||||
if (assetPath != null) PayWithCardFragment.loadCardImage(ivCardImage, assetPath)
|
||||
tvCardNumber.text = CardsFragment.formatMasked(item.card.maskedCardNumber)
|
||||
val assetPath = CardsFragment.cardImageAsset(item.card)
|
||||
if (assetPath != null) CardsFragment.loadCardImage(ivCardImage, assetPath)
|
||||
else ivCardImage.setImageDrawable(null)
|
||||
PayWithCardFragment.bindCardStatus(tvCardStatus, PayWithCardFragment.mibCardStatusLabel(item.card.cardStatus))
|
||||
CardsFragment.bindCardStatus(tvCardStatus, CardsFragment.mibCardStatusLabel(item.card.cardStatus))
|
||||
}
|
||||
is CardItem.Bml -> {
|
||||
tvCardOwner.text = item.account.accountBriefName
|
||||
tvCardNumber.text = PayWithCardFragment.formatMasked(item.account.accountNumber)
|
||||
PayWithCardFragment.loadCardImage(ivCardImage, BmlCardParser.cardImageAsset(item.account))
|
||||
PayWithCardFragment.bindCardStatus(tvCardStatus, null) // only Active BML cards reach dashboard
|
||||
tvCardNumber.text = CardsFragment.formatMasked(item.account.accountNumber)
|
||||
CardsFragment.loadCardImage(ivCardImage, BmlCardParser.cardImageAsset(item.account))
|
||||
CardsFragment.bindCardStatus(tvCardStatus, null) // only Active BML cards reach dashboard
|
||||
}
|
||||
}
|
||||
val isMib = item is CardItem.Mib
|
||||
|
||||
@@ -159,8 +159,7 @@ class HomeActivity : AppCompatActivity() {
|
||||
R.id.nav_finances -> FinancingFragment()
|
||||
R.id.nav_otp -> OtpFragment()
|
||||
R.id.nav_settings -> SettingsFragment()
|
||||
R.id.nav_pay_with_card -> PayWithCardFragment()
|
||||
R.id.nav_card_settings -> CardSettingsFragment()
|
||||
R.id.nav_pay_with_card -> CardsFragment()
|
||||
else -> null
|
||||
}
|
||||
if (frag != null) show(frag)
|
||||
@@ -379,8 +378,7 @@ fun applyNavLabelVisibility() {
|
||||
R.id.nav_finances -> FinancingFragment()
|
||||
R.id.nav_otp -> OtpFragment()
|
||||
R.id.nav_settings -> SettingsFragment()
|
||||
R.id.nav_pay_with_card -> PayWithCardFragment()
|
||||
R.id.nav_card_settings -> CardSettingsFragment()
|
||||
R.id.nav_pay_with_card -> CardsFragment()
|
||||
else -> { Toast.makeText(this, R.string.work_in_progress, Toast.LENGTH_SHORT).show(); return }
|
||||
}
|
||||
show(dest)
|
||||
|
||||
@@ -25,7 +25,6 @@ object NavCustomization {
|
||||
NavItemDef(R.id.nav_transfer_history, "nav_transfer_history", R.drawable.ic_nav_transfer_history, R.string.nav_transfer_history, R.string.nav_desc_transfer_history),
|
||||
NavItemDef(R.id.nav_finances, "nav_finances", R.drawable.ic_nav_finances, R.string.nav_finances, R.string.nav_desc_finances),
|
||||
NavItemDef(R.id.nav_pay_with_card, "nav_pay_with_card", R.drawable.ic_nav_card, R.string.nav_pay_with_card, R.string.nav_desc_pay_with_card),
|
||||
NavItemDef(R.id.nav_card_settings, "nav_card_settings", R.drawable.ic_nav_card, R.string.nav_card_settings, R.string.nav_desc_card_settings),
|
||||
NavItemDef(R.id.nav_otp, "nav_otp", R.drawable.ic_nav_otp, R.string.nav_otp, R.string.nav_desc_otp),
|
||||
NavItemDef(R.id.nav_settings, "nav_settings", R.drawable.ic_nav_settings, R.string.nav_settings, R.string.nav_desc_settings),
|
||||
)
|
||||
|
||||
@@ -3,13 +3,14 @@ package sh.sar.basedbank.ui.home
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.BitmapFactory
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.GradientDrawable
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
@@ -18,20 +19,25 @@ import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.PagerSnapHelper
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.color.MaterialColors
|
||||
import sh.sar.basedbank.R
|
||||
import sh.sar.basedbank.api.models.BankAccount
|
||||
import sh.sar.basedbank.api.mib.MibCard
|
||||
import sh.sar.basedbank.databinding.FragmentPayWithCardBinding
|
||||
import sh.sar.basedbank.databinding.FragmentCardsBinding
|
||||
import sh.sar.basedbank.util.CardsCache
|
||||
import sh.sar.basedbank.util.bmlapi.BmlCardParser
|
||||
import kotlin.math.abs
|
||||
|
||||
class PayWithCardFragment : Fragment() {
|
||||
class CardsFragment : Fragment() {
|
||||
|
||||
private var _binding: FragmentPayWithCardBinding? = null
|
||||
private var _binding: FragmentCardsBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
private val viewModel: HomeViewModel by activityViewModels()
|
||||
|
||||
private var cards: List<CardItem> = emptyList()
|
||||
private var currentCardPosition: Int = 0
|
||||
private var cardWidth: Int = 0
|
||||
private var pendingQrAccountNumber: String? = null
|
||||
|
||||
private val qrLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
@@ -49,39 +55,67 @@ class PayWithCardFragment : Fragment() {
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
_binding = FragmentPayWithCardBinding.inflate(inflater, container, false)
|
||||
_binding = FragmentCardsBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
val adapter = CardWalletAdapter(emptyList(), requireContext())
|
||||
binding.recyclerView.layoutManager = LinearLayoutManager(requireContext())
|
||||
binding.recyclerView.adapter = adapter
|
||||
val screenW = resources.displayMetrics.widthPixels
|
||||
val peekPx = screenW / 8
|
||||
cardWidth = screenW - 2 * peekPx
|
||||
|
||||
val bottomPaddingBase = (16 * resources.displayMetrics.density).toInt()
|
||||
ViewCompat.setOnApplyWindowInsetsListener(binding.recyclerView) { v, insets ->
|
||||
val isBottomNav = requireContext().getSharedPreferences("prefs", Context.MODE_PRIVATE).getBoolean("bottom_nav", false)
|
||||
val stackAdapter = CardStackAdapter(cardWidth)
|
||||
binding.rvCards.layoutManager = LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false)
|
||||
binding.rvCards.adapter = stackAdapter
|
||||
binding.rvCards.setPadding(peekPx, 0, peekPx, 0)
|
||||
binding.rvCards.clipToPadding = false
|
||||
|
||||
val snapHelper = PagerSnapHelper()
|
||||
snapHelper.attachToRecyclerView(binding.rvCards)
|
||||
|
||||
binding.rvCards.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||
applyCardScales()
|
||||
}
|
||||
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
|
||||
if (newState == RecyclerView.SCROLL_STATE_IDLE) {
|
||||
val lm = recyclerView.layoutManager ?: return
|
||||
val snapView = snapHelper.findSnapView(lm) ?: return
|
||||
val position = lm.getPosition(snapView)
|
||||
if (position >= 0) {
|
||||
currentCardPosition = position
|
||||
buildDots(cards.size, position)
|
||||
updateCardInfo(position)
|
||||
}
|
||||
applyCardScales()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
ViewCompat.setOnApplyWindowInsetsListener(binding.contentLayout) { v, insets ->
|
||||
val isBottomNav = requireContext().getSharedPreferences("prefs", Context.MODE_PRIVATE)
|
||||
.getBoolean("bottom_nav", false)
|
||||
val navBar = insets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||
val extraBottom = if (isBottomNav) 0 else navBar.bottom
|
||||
v.setPadding(v.paddingLeft, v.paddingTop, v.paddingRight, bottomPaddingBase + extraBottom)
|
||||
v.setPadding(0, 0, 0, (16 * resources.displayMetrics.density).toInt() + extraBottom)
|
||||
insets
|
||||
}
|
||||
|
||||
binding.swipeRefresh.setOnRefreshListener {
|
||||
(activity as? HomeActivity)?.triggerRefresh()
|
||||
}
|
||||
|
||||
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") && it.statusDesc.equals("Active", ignoreCase = true) }
|
||||
.filter { it.profileType == "BML_PREPAID" || it.profileType == "BML_CREDIT" || it.profileType == "BML_DEBIT" }
|
||||
.map { CardItem.Bml(it) }
|
||||
val all = mibItems + bmlItems
|
||||
adapter.update(all)
|
||||
cards = mibItems + bmlItems
|
||||
stackAdapter.update(cards)
|
||||
binding.loadingView.visibility = View.GONE
|
||||
binding.swipeRefresh.isRefreshing = false
|
||||
binding.emptyView.visibility = if (all.isEmpty()) View.VISIBLE else View.GONE
|
||||
binding.recyclerView.visibility = if (all.isEmpty()) View.GONE else View.VISIBLE
|
||||
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() }
|
||||
@@ -93,6 +127,84 @@ class PayWithCardFragment : Fragment() {
|
||||
binding.loadingView.visibility = View.VISIBLE
|
||||
}
|
||||
(activity as? HomeActivity)?.triggerRefreshCards()
|
||||
|
||||
binding.btnScanToPay.setOnClickListener {
|
||||
val item = cards.getOrNull(currentCardPosition) ?: return@setOnClickListener
|
||||
if (item is CardItem.Mib) {
|
||||
Toast.makeText(requireContext(), R.string.mib_qr_nfc_not_supported, Toast.LENGTH_SHORT).show()
|
||||
} else {
|
||||
pendingQrAccountNumber = (item as CardItem.Bml).account.accountNumber
|
||||
qrLauncher.launch(Intent(requireContext(), QrScannerActivity::class.java))
|
||||
}
|
||||
}
|
||||
|
||||
val nfcAvailable = android.nfc.NfcAdapter.getDefaultAdapter(requireContext()) != null
|
||||
binding.btnTapToPay.isEnabled = nfcAvailable
|
||||
binding.btnTapToPay.setOnClickListener {
|
||||
val item = cards.getOrNull(currentCardPosition) ?: return@setOnClickListener
|
||||
val msg = if (item is CardItem.Mib) R.string.mib_qr_nfc_not_supported else R.string.work_in_progress
|
||||
Toast.makeText(requireContext(), msg, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
val wip = View.OnClickListener {
|
||||
Toast.makeText(requireContext(), R.string.work_in_progress, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
binding.btnChangePin.setOnClickListener(wip)
|
||||
binding.btnFreeze.setOnClickListener(wip)
|
||||
binding.btnBlock.setOnClickListener(wip)
|
||||
}
|
||||
|
||||
private fun applyCardScales() {
|
||||
val rv = binding.rvCards
|
||||
val rvCenter = rv.paddingStart + (rv.width - rv.paddingStart - rv.paddingEnd) / 2f
|
||||
val lm = rv.layoutManager as? LinearLayoutManager ?: return
|
||||
val first = lm.findFirstVisibleItemPosition()
|
||||
val last = lm.findLastVisibleItemPosition()
|
||||
if (first < 0) return
|
||||
for (i in first..last) {
|
||||
val child = lm.findViewByPosition(i) ?: continue
|
||||
val childCenter = (child.left + child.right) / 2f
|
||||
val fraction = (abs(childCenter - rvCenter) / cardWidth.toFloat()).coerceIn(0f, 1f)
|
||||
val scale = 1f - 0.18f * fraction
|
||||
child.scaleX = scale
|
||||
child.scaleY = scale
|
||||
child.alpha = 1f - 0.4f * fraction
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildDots(count: Int, selected: Int) {
|
||||
binding.pageIndicator.removeAllViews()
|
||||
if (count <= 1) {
|
||||
binding.pageIndicator.visibility = View.GONE
|
||||
return
|
||||
}
|
||||
binding.pageIndicator.visibility = View.VISIBLE
|
||||
val dp = resources.displayMetrics.density
|
||||
val activeColor = MaterialColors.getColor(
|
||||
requireContext(), com.google.android.material.R.attr.colorPrimary, Color.GRAY)
|
||||
val inactiveColor = MaterialColors.getColor(
|
||||
requireContext(), com.google.android.material.R.attr.colorOutlineVariant, Color.LTGRAY)
|
||||
val size = (8 * dp).toInt()
|
||||
val margin = (4 * dp).toInt()
|
||||
repeat(count) { i ->
|
||||
val dot = View(requireContext())
|
||||
dot.layoutParams = LinearLayout.LayoutParams(size, size).apply {
|
||||
setMargins(margin, 0, margin, 0)
|
||||
}
|
||||
dot.background = GradientDrawable().apply {
|
||||
shape = GradientDrawable.OVAL
|
||||
setColor(if (i == selected) activeColor else inactiveColor)
|
||||
}
|
||||
binding.pageIndicator.addView(dot)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateCardInfo(position: Int) {
|
||||
val item = cards.getOrNull(position) ?: return
|
||||
binding.tvSelectedCardType.text = when (item) {
|
||||
is CardItem.Mib -> item.card.cardTypeDesc
|
||||
is CardItem.Bml -> item.account.accountTypeName
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
@@ -105,67 +217,59 @@ class PayWithCardFragment : Fragment() {
|
||||
_binding = null
|
||||
}
|
||||
|
||||
private inner class CardWalletAdapter(
|
||||
private var cards: List<CardItem>,
|
||||
private val context: Context
|
||||
) : RecyclerView.Adapter<CardWalletAdapter.VH>() {
|
||||
private inner class CardStackAdapter(private val cardWidth: Int) : RecyclerView.Adapter<CardStackAdapter.VH>() {
|
||||
private var items: List<CardItem> = emptyList()
|
||||
|
||||
fun update(newCards: List<CardItem>) {
|
||||
cards = newCards
|
||||
fun update(newItems: List<CardItem>) {
|
||||
items = newItems
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH =
|
||||
VH(LayoutInflater.from(context).inflate(R.layout.item_card_wallet, parent, false))
|
||||
override fun getItemCount() = items.size
|
||||
|
||||
override fun onBindViewHolder(holder: VH, position: Int) = holder.bind(cards[position])
|
||||
override fun getItemCount() = cards.size
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH =
|
||||
VH(LayoutInflater.from(parent.context).inflate(R.layout.item_card_stack, parent, false))
|
||||
|
||||
override fun onBindViewHolder(holder: VH, position: Int) {
|
||||
holder.bind(items[position])
|
||||
// Pre-scale based on data position so initial render and off-screen cards are correct
|
||||
val fraction = abs(position - currentCardPosition).toFloat().coerceIn(0f, 1f)
|
||||
val scale = 1f - 0.18f * fraction
|
||||
holder.itemView.scaleX = scale
|
||||
holder.itemView.scaleY = scale
|
||||
holder.itemView.alpha = 1f - 0.4f * fraction
|
||||
}
|
||||
|
||||
inner class VH(view: View) : RecyclerView.ViewHolder(view) {
|
||||
private val ivCardImage: ImageView = view.findViewById(R.id.ivCardImage)
|
||||
private val tvCardOwner: TextView = view.findViewById(R.id.tvCardOwner)
|
||||
private val tvCardNumber: TextView = view.findViewById(R.id.tvCardNumber)
|
||||
private val tvCardType: TextView = view.findViewById(R.id.tvCardType)
|
||||
private val tvCardStatus: TextView = view.findViewById(R.id.tvCardStatus)
|
||||
private val btnPayQr: View = view.findViewById(R.id.btnPayQr)
|
||||
private val btnPayNfc: View = view.findViewById(R.id.btnPayNfc)
|
||||
|
||||
init {
|
||||
itemView.layoutParams = RecyclerView.LayoutParams(cardWidth, RecyclerView.LayoutParams.WRAP_CONTENT)
|
||||
}
|
||||
|
||||
fun bind(item: CardItem) {
|
||||
when (item) {
|
||||
is CardItem.Mib -> {
|
||||
tvCardOwner.text = item.card.cardHolderName
|
||||
tvCardNumber.text = formatMasked(item.card.maskedCardNumber)
|
||||
tvCardType.text = item.card.cardTypeDesc
|
||||
val assetPath = cardImageAsset(item.card)
|
||||
if (assetPath != null) loadCardImage(ivCardImage, assetPath)
|
||||
else ivCardImage.setImageDrawable(null)
|
||||
bindCardStatus(tvCardStatus, mibCardStatusLabel(item.card.cardStatus))
|
||||
itemView.alpha = 1f
|
||||
}
|
||||
is CardItem.Bml -> {
|
||||
tvCardOwner.text = item.account.accountBriefName
|
||||
tvCardNumber.text = formatMasked(item.account.accountNumber)
|
||||
tvCardType.text = item.account.accountTypeName
|
||||
loadCardImage(ivCardImage, BmlCardParser.cardImageAsset(item.account))
|
||||
val bmlStatus = item.account.statusDesc.takeUnless { it.equals("Active", ignoreCase = true) }
|
||||
bindCardStatus(tvCardStatus, bmlStatus)
|
||||
val isActive = item.account.statusDesc.equals("Active", ignoreCase = true)
|
||||
bindCardStatus(tvCardStatus, item.account.statusDesc.takeUnless { isActive })
|
||||
itemView.alpha = if (isActive) 1f else 0.45f
|
||||
}
|
||||
}
|
||||
val isMib = item is CardItem.Mib
|
||||
btnPayQr.setOnClickListener {
|
||||
if (isMib) {
|
||||
Toast.makeText(context, R.string.mib_qr_nfc_not_supported, Toast.LENGTH_SHORT).show()
|
||||
} else {
|
||||
pendingQrAccountNumber = (item as CardItem.Bml).account.accountNumber
|
||||
qrLauncher.launch(Intent(requireContext(), QrScannerActivity::class.java))
|
||||
}
|
||||
}
|
||||
val nfcAdapter = android.nfc.NfcAdapter.getDefaultAdapter(context)
|
||||
val nfcSupported = nfcAdapter != null
|
||||
btnPayNfc.isEnabled = nfcSupported
|
||||
btnPayNfc.setOnClickListener {
|
||||
val msg = if (isMib) R.string.mib_qr_nfc_not_supported else R.string.work_in_progress
|
||||
Toast.makeText(context, msg, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -181,7 +285,7 @@ class PayWithCardFragment : Fragment() {
|
||||
fun loadCardImage(imageView: ImageView, assetPath: String) {
|
||||
try {
|
||||
val bitmap = imageView.context.assets.open(assetPath).use {
|
||||
BitmapFactory.decodeStream(it)
|
||||
android.graphics.BitmapFactory.decodeStream(it)
|
||||
}
|
||||
imageView.setImageBitmap(bitmap)
|
||||
} catch (_: Exception) {
|
||||
@@ -190,8 +294,8 @@ class PayWithCardFragment : Fragment() {
|
||||
}
|
||||
|
||||
fun mibCardStatusLabel(cardStatus: String): String? = when (cardStatus) {
|
||||
"CHST0" -> null // Active — no badge
|
||||
else -> cardStatus
|
||||
"CHST0" -> null
|
||||
else -> cardStatus
|
||||
}
|
||||
|
||||
fun bindCardStatus(tv: TextView, statusLabel: String?) {
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?attr/colorSurface">
|
||||
|
||||
<!-- Main content when cards exist -->
|
||||
<LinearLayout
|
||||
android:id="@+id/contentLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone">
|
||||
|
||||
<!-- Top spacer: pushes card to vertical center -->
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1" />
|
||||
|
||||
<!-- Horizontal card stack. Width/padding set programmatically for centering + peek. -->
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/rvCards"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:clipToPadding="false"
|
||||
android:overScrollMode="never" />
|
||||
|
||||
<!-- Page indicator dots -->
|
||||
<LinearLayout
|
||||
android:id="@+id/pageIndicator"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:orientation="horizontal"
|
||||
android:paddingTop="12dp"
|
||||
android:paddingBottom="2dp" />
|
||||
|
||||
<!-- Selected card type / product name -->
|
||||
<TextView
|
||||
android:id="@+id/tvSelectedCardType"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:paddingTop="6dp"
|
||||
android:paddingBottom="2dp"
|
||||
android:textAppearance="?attr/textAppearanceLabelLarge"
|
||||
android:textColor="?attr/colorOnSurfaceVariant" />
|
||||
|
||||
<!-- Flexible spacer: absorbs remaining space, pushes buttons to bottom -->
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1" />
|
||||
|
||||
<!-- Divider -->
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginHorizontal="24dp"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:background="?attr/colorOutlineVariant" />
|
||||
|
||||
<!-- Primary pay actions -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="4dp">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnScanToPay"
|
||||
style="@style/Widget.Material3.Button.TonalButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:minWidth="0dp"
|
||||
android:minHeight="0dp"
|
||||
android:paddingTop="14dp"
|
||||
android:paddingBottom="14dp"
|
||||
android:text="@string/card_pay_qr"
|
||||
android:textSize="13sp"
|
||||
app:icon="@drawable/ic_qr_scan"
|
||||
app:iconSize="22dp"
|
||||
app:iconGravity="top"
|
||||
app:iconPadding="6dp" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnTapToPay"
|
||||
style="@style/Widget.Material3.Button.TonalButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:minWidth="0dp"
|
||||
android:minHeight="0dp"
|
||||
android:paddingTop="14dp"
|
||||
android:paddingBottom="14dp"
|
||||
android:text="@string/card_pay_nfc"
|
||||
android:textSize="13sp"
|
||||
app:icon="@drawable/ic_nfc"
|
||||
app:iconSize="22dp"
|
||||
app:iconGravity="top"
|
||||
app:iconPadding="6dp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Secondary card management actions -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:paddingHorizontal="8dp"
|
||||
android:paddingTop="4dp"
|
||||
android:paddingBottom="8dp">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnChangePin"
|
||||
style="@style/Widget.Material3.Button.TextButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_height="wrap_content"
|
||||
android:minWidth="0dp"
|
||||
android:minHeight="0dp"
|
||||
android:text="@string/card_action_change_pin"
|
||||
android:textSize="11sp"
|
||||
app:icon="@drawable/ic_edit"
|
||||
app:iconSize="16dp"
|
||||
app:iconPadding="3dp" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnFreeze"
|
||||
style="@style/Widget.Material3.Button.TextButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_height="wrap_content"
|
||||
android:minWidth="0dp"
|
||||
android:minHeight="0dp"
|
||||
android:text="@string/card_action_freeze"
|
||||
android:textSize="11sp"
|
||||
app:icon="@drawable/ic_freeze"
|
||||
app:iconSize="16dp"
|
||||
app:iconPadding="3dp" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnBlock"
|
||||
style="@style/Widget.Material3.Button.TextButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_height="wrap_content"
|
||||
android:minWidth="0dp"
|
||||
android:minHeight="0dp"
|
||||
android:text="@string/card_action_block"
|
||||
android:textSize="11sp"
|
||||
app:icon="@drawable/ic_block"
|
||||
app:iconSize="16dp"
|
||||
app:iconPadding="3dp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Loading state -->
|
||||
<LinearLayout
|
||||
android:id="@+id/loadingView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone">
|
||||
<ProgressBar
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Empty state -->
|
||||
<TextView
|
||||
android:id="@+id/emptyView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center"
|
||||
android:text="@string/cards_empty"
|
||||
android:textAppearance="?attr/textAppearanceBodyMedium"
|
||||
android:textColor="?attr/colorOnSurfaceVariant"
|
||||
android:visibility="gone" />
|
||||
|
||||
|
||||
</FrameLayout>
|
||||
@@ -0,0 +1,79 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="6dp"
|
||||
app:cardCornerRadius="16dp"
|
||||
app:cardElevation="6dp">
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/ivCardImage"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:scaleType="fitCenter"
|
||||
android:adjustViewBounds="true"
|
||||
android:contentDescription="@string/nav_pay_with_card" />
|
||||
|
||||
<!-- Bottom gradient for text legibility -->
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="80dp"
|
||||
android:layout_gravity="bottom"
|
||||
android:background="@drawable/bg_card_overlay_gradient" />
|
||||
|
||||
<!-- Status badge (top-right) -->
|
||||
<TextView
|
||||
android:id="@+id/tvCardStatus"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="top|end"
|
||||
android:layout_margin="8dp"
|
||||
android:paddingStart="6dp"
|
||||
android:paddingEnd="6dp"
|
||||
android:paddingTop="2dp"
|
||||
android:paddingBottom="2dp"
|
||||
android:textSize="9sp"
|
||||
android:textColor="#FFFFFF"
|
||||
android:textStyle="bold"
|
||||
android:visibility="gone" />
|
||||
|
||||
<!-- Card owner name + masked number (bottom-left) -->
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|start"
|
||||
android:orientation="vertical"
|
||||
android:padding="12dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvCardOwner"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="?attr/textAppearanceLabelMedium"
|
||||
android:textColor="#FFFFFF"
|
||||
android:textStyle="bold"
|
||||
android:shadowColor="#80000000"
|
||||
android:shadowDx="1"
|
||||
android:shadowDy="1"
|
||||
android:shadowRadius="3" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvCardNumber"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp"
|
||||
android:textAppearance="?attr/textAppearanceBodySmall"
|
||||
android:textColor="#CCFFFFFF"
|
||||
android:fontFamily="monospace" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
@@ -32,9 +32,6 @@
|
||||
<item android:id="@+id/nav_pay_with_card"
|
||||
android:icon="@drawable/ic_nav_card"
|
||||
android:title="@string/nav_pay_with_card" />
|
||||
<item android:id="@+id/nav_card_settings"
|
||||
android:icon="@drawable/ic_nav_card"
|
||||
android:title="@string/nav_card_settings" />
|
||||
</group>
|
||||
|
||||
<group android:id="@+id/group_system" android:checkableBehavior="single">
|
||||
|
||||
@@ -15,9 +15,6 @@
|
||||
<item android:id="@+id/nav_pay_with_card"
|
||||
android:icon="@drawable/ic_nav_card"
|
||||
android:title="@string/nav_pay_with_card" />
|
||||
<item android:id="@+id/nav_card_settings"
|
||||
android:icon="@drawable/ic_nav_card"
|
||||
android:title="@string/nav_card_settings" />
|
||||
<item android:id="@+id/nav_otp"
|
||||
android:icon="@drawable/ic_nav_otp"
|
||||
android:title="@string/nav_otp" />
|
||||
|
||||
@@ -72,7 +72,8 @@
|
||||
<string name="nav_activities">ހަރަކާތްތައް</string>
|
||||
<string name="nav_transfer_history">ޓްރާންސެކްޝަން ތާރީހް</string>
|
||||
<string name="nav_finances">ފައިނޭންސް</string>
|
||||
<string name="nav_card_settings">ކާޑް ސެޓިންގ</string>
|
||||
<string name="nav_pay_with_card">ކާޑްތައް</string>
|
||||
<string name="nav_desc_pay_with_card">ކާޑް މެނޭޖްކޮށް ފައިސާ ދައްކާ</string>
|
||||
<string name="nav_settings">ސެޓިންގ</string>
|
||||
<string name="nav_desc_accounts">ހުރިހާ ބޭންކް އެކައުންޓްތައް ބަލާ</string>
|
||||
<string name="nav_desc_contacts">ޓްރާންސްފަ ކޮންޓެކްޓްތައް މެނޭޖް ކުރޭ</string>
|
||||
@@ -81,8 +82,6 @@
|
||||
<string name="nav_desc_activities">ފަހުގެ ޓްރާންސްފަތައް ބަލާ</string>
|
||||
<string name="nav_desc_transfer_history">އެކައުންޓް ތަކުގެ ޓްރާންސެކްޝަން ތާރީހް</string>
|
||||
<string name="nav_desc_finances">ލޯން އަދި ފައިނޭންސިންގ</string>
|
||||
<string name="nav_desc_pay_with_card">ކާޑް ބޭނުންކޮށް ފައިސާ ދައްކާ</string>
|
||||
<string name="nav_desc_card_settings">ކާޑް ސެޓިންގ މެނޭޖް ކުރޭ</string>
|
||||
<string name="nav_desc_otp">OTP ކޯޑް ތައްޔާރު ކުރޭ</string>
|
||||
<string name="nav_desc_settings">އެޕްލިކޭޝަންގެ ތަރުތީބު</string>
|
||||
<string name="nav_open_drawer">ނެވިގޭޝަން ހުޅުވާ</string>
|
||||
|
||||
@@ -93,8 +93,7 @@
|
||||
<string name="nav_desc_activities">View your recent transfers</string>
|
||||
<string name="nav_desc_transfer_history">Full transaction history by account</string>
|
||||
<string name="nav_desc_finances">Loans and financing overview</string>
|
||||
<string name="nav_desc_pay_with_card">Make a payment using your card</string>
|
||||
<string name="nav_desc_card_settings">Manage your card preferences</string>
|
||||
<string name="nav_desc_pay_with_card">Manage and pay with your cards</string>
|
||||
<string name="nav_desc_otp">Generate OTP codes for authentication</string>
|
||||
<string name="nav_desc_settings">App preferences and configuration</string>
|
||||
<string name="nav_open_drawer">Open navigation</string>
|
||||
@@ -311,9 +310,9 @@
|
||||
<string name="loan_rate_fmt">%.2f%%</string>
|
||||
|
||||
<!-- Cards -->
|
||||
<string name="nav_pay_with_card">Pay with Card</string>
|
||||
<string name="card_pay_qr">QR Pay</string>
|
||||
<string name="card_pay_nfc">NFC Pay</string>
|
||||
<string name="nav_pay_with_card">Cards</string>
|
||||
<string name="card_pay_qr">Scan to Pay</string>
|
||||
<string name="card_pay_nfc">Tap to Pay</string>
|
||||
<string name="mib_qr_nfc_not_supported">Skill issue on MIB side, Not supported</string>
|
||||
<string name="card_action_change_pin">Change PIN</string>
|
||||
<string name="card_action_freeze">Freeze</string>
|
||||
|
||||
Reference in New Issue
Block a user