new nfc icon, hide cards, removed offline nfc payments
Auto Tag on Version Change / check-version (push) Failing after 12m50s

This commit is contained in:
2026-05-29 16:39:58 +05:00
parent 2df162c09e
commit ee5ecdaa18
15 changed files with 252 additions and 329 deletions
+43
View File
@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
<shortcut
android:shortcutId="transfer"
android:enabled="true"
android:icon="@drawable/ic_shortcut_transfer"
android:shortcutShortLabel="@string/transfer"
android:shortcutLongLabel="@string/transfer">
<intent
android:action="sh.sar.basedbank.OPEN_TRANSFER"
android:targetPackage="sh.sar.basedbank.debug"
android:targetClass="sh.sar.basedbank.MainActivity" />
<categories android:name="android.shortcut.conversation" />
</shortcut>
<shortcut
android:shortcutId="scan_qr"
android:enabled="true"
android:icon="@drawable/ic_shortcut_scan_qr"
android:shortcutShortLabel="@string/transfer_scan_qr"
android:shortcutLongLabel="@string/transfer_scan_qr">
<intent
android:action="sh.sar.basedbank.OPEN_SCAN_QR"
android:targetPackage="sh.sar.basedbank.debug"
android:targetClass="sh.sar.basedbank.MainActivity" />
<categories android:name="android.shortcut.conversation" />
</shortcut>
<shortcut
android:shortcutId="tap_to_pay"
android:enabled="true"
android:icon="@drawable/ic_shortcut_pay_card"
android:shortcutShortLabel="@string/card_pay_nfc"
android:shortcutLongLabel="@string/card_pay_nfc">
<intent
android:action="sh.sar.basedbank.TAP_TO_PAY"
android:targetPackage="sh.sar.basedbank.debug"
android:targetClass="sh.sar.basedbank.MainActivity" />
<categories android:name="android.shortcut.conversation" />
</shortcut>
</shortcuts>
@@ -278,9 +278,11 @@ class LockActivity : AppCompatActivity() {
}
val navDest = intent.getIntExtra("nav_destination", -1)
val autoScan = intent.getBooleanExtra("auto_scan", false)
val autoTapMode = intent.getBooleanExtra("auto_tap_mode", false)
startActivity(Intent(this, HomeActivity::class.java).apply {
if (navDest != -1) putExtra("nav_destination", navDest)
if (autoScan) putExtra("auto_scan", true)
if (autoTapMode) putExtra("auto_tap_mode", true)
})
finish()
}
@@ -9,7 +9,6 @@ import sh.sar.basedbank.ui.onboarding.OnboardingActivity
import sh.sar.basedbank.util.CredentialStore
import sh.sar.basedbank.BasedBankApp
import sh.sar.basedbank.R
import sh.sar.basedbank.nfc.BmlTapToPayActivity
class MainActivity : AppCompatActivity() {
@@ -22,20 +21,15 @@ class MainActivity : AppCompatActivity() {
val store = CredentialStore(this)
val hasCredentials = store.hasMibCredentials() || store.hasBmlCredentials() || store.hasFahipayCredentials()
// Tap to Pay shortcut: go straight to the standalone payment activity
if (intent?.action == "sh.sar.basedbank.TAP_TO_PAY") {
startActivity(Intent(this, BmlTapToPayActivity::class.java))
finish()
return
}
val navDestination = when (intent?.action) {
"sh.sar.basedbank.OPEN_TRANSFER" -> R.id.nav_transfer
"sh.sar.basedbank.OPEN_SCAN_QR" -> R.id.nav_transfer
"sh.sar.basedbank.OPEN_PAY_WITH_CARD" -> R.id.nav_pay_with_card
"sh.sar.basedbank.TAP_TO_PAY" -> R.id.nav_pay_with_card
else -> -1
}
val autoScan = intent?.action == "sh.sar.basedbank.OPEN_SCAN_QR"
val autoTapMode = intent?.action == "sh.sar.basedbank.TAP_TO_PAY"
val target = when {
!onboardingDone -> OnboardingActivity::class.java
@@ -51,6 +45,7 @@ class MainActivity : AppCompatActivity() {
startActivity(Intent(this, target).apply {
if (navDestination != -1) putExtra("nav_destination", navDestination)
if (autoScan) putExtra("auto_scan", true)
if (autoTapMode) putExtra("auto_tap_mode", true)
})
finish()
}
@@ -56,8 +56,9 @@ class BmlHostCardEmulatorService : HostApduService() {
}
private fun launchPromptActivity() {
val intent = Intent(applicationContext, BmlTapToPayActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP
val intent = Intent("sh.sar.basedbank.TAP_TO_PAY").apply {
setPackage(applicationContext.packageName)
flags = Intent.FLAG_ACTIVITY_NEW_TASK
}
startActivity(intent)
}
@@ -2,166 +2,19 @@ package sh.sar.basedbank.nfc
import android.content.Intent
import android.os.Bundle
import android.view.Gravity
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.biometric.BiometricManager
import androidx.biometric.BiometricPrompt
import androidx.core.content.ContextCompat
import com.google.android.material.button.MaterialButton
import com.google.android.material.color.MaterialColors
import sh.sar.basedbank.R
import sh.sar.basedbank.MainActivity
/**
* Launched by [BmlHostCardEmulatorService] when the phone is tapped against a POS terminal
* but no payment token is armed. Handles biometric auth and token loading from the local store,
* then arms the service so the user can tap again to complete the payment.
* Fallback entry point — redirects to MainActivity which routes to the in-app tap-to-pay screen.
*/
class BmlTapToPayActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
prepare()
}
// singleTop: if already showing, just ignore the re-launch
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
}
private fun prepare() {
val store = HceTokenStore(this)
if (!store.hasTokens()) {
Toast.makeText(this,
"Open the app and tap Tap to Pay first to load payment tokens",
Toast.LENGTH_LONG).show()
finish()
return
}
val prefs = getSharedPreferences("prefs", MODE_PRIVATE)
val biometricEnabled = prefs.getBoolean("biometrics_transfer_confirm", false)
if (biometricEnabled) {
val bmgr = BiometricManager.from(this)
if (bmgr.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG) == BiometricManager.BIOMETRIC_SUCCESS) {
showBiometricPrompt(store)
return
}
}
armAndShowReady(store)
}
private fun showBiometricPrompt(store: HceTokenStore) {
val prompt = BiometricPrompt(this, ContextCompat.getMainExecutor(this),
object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
armAndShowReady(store)
}
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
finish()
}
})
prompt.authenticate(
BiometricPrompt.PromptInfo.Builder()
.setTitle(getString(R.string.card_pay_nfc))
.setSubtitle(getString(R.string.app_name))
.setNegativeButtonText(getString(R.string.cancel))
.build()
)
}
private fun armAndShowReady(store: HceTokenStore) {
val token = store.popToken() ?: run {
Toast.makeText(this,
"No valid payment tokens — please open the app",
Toast.LENGTH_LONG).show()
finish()
return
}
BmlHostCardEmulatorService.setToken(token)
BmlHostCardEmulatorService.onTransactionComplete = { success ->
runOnUiThread {
BmlHostCardEmulatorService.clearToken()
BmlHostCardEmulatorService.onTransactionComplete = null
if (success) Toast.makeText(this, "Payment complete", Toast.LENGTH_SHORT).show()
finish()
}
}
showReadyLayout()
}
private fun showReadyLayout() {
val dp = resources.displayMetrics.density
val colorSurface = MaterialColors.getColor(window.decorView,
com.google.android.material.R.attr.colorSurface, 0xFF1C1B1F.toInt())
val colorOnSurface = MaterialColors.getColor(window.decorView,
com.google.android.material.R.attr.colorOnSurface, 0xFFFFFFFF.toInt())
val colorOnSurfaceVariant = MaterialColors.getColor(window.decorView,
com.google.android.material.R.attr.colorOnSurfaceVariant, 0xFFCAC4D0.toInt())
val root = LinearLayout(this).apply {
orientation = LinearLayout.VERTICAL
gravity = Gravity.CENTER
setBackgroundColor(colorSurface)
setPadding((32 * dp).toInt(), (64 * dp).toInt(), (32 * dp).toInt(), (64 * dp).toInt())
}
root.addView(ImageView(this).apply {
setImageResource(R.drawable.ic_nfc)
setColorFilter(colorOnSurface)
layoutParams = LinearLayout.LayoutParams((72 * dp).toInt(), (72 * dp).toInt()).apply {
gravity = Gravity.CENTER_HORIZONTAL
bottomMargin = (32 * dp).toInt()
}
startActivity(Intent(this, MainActivity::class.java).apply {
action = "sh.sar.basedbank.TAP_TO_PAY"
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
})
root.addView(TextView(this).apply {
text = getString(R.string.card_pay_nfc)
textSize = 22f
gravity = Gravity.CENTER
setTextColor(colorOnSurface)
layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
).apply { bottomMargin = (8 * dp).toInt() }
})
root.addView(TextView(this).apply {
text = "Hold your phone near the payment terminal"
textSize = 14f
gravity = Gravity.CENTER
setTextColor(colorOnSurfaceVariant)
layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
).apply { bottomMargin = (48 * dp).toInt() }
})
root.addView(MaterialButton(this, null,
com.google.android.material.R.attr.materialButtonOutlinedStyle
).apply {
text = getString(R.string.cancel)
setOnClickListener {
BmlHostCardEmulatorService.clearToken()
BmlHostCardEmulatorService.onTransactionComplete = null
finish()
}
})
setContentView(root)
}
override fun onDestroy() {
super.onDestroy()
// If the activity is killed without completing, clean up the armed token
if (isFinishing && BmlHostCardEmulatorService.activeToken != null) {
BmlHostCardEmulatorService.clearToken()
BmlHostCardEmulatorService.onTransactionComplete = null
}
finish()
}
}
@@ -1,105 +0,0 @@
package sh.sar.basedbank.nfc
import android.content.Context
import android.content.SharedPreferences
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey
import org.json.JSONArray
import org.json.JSONObject
import sh.sar.basedbank.api.bml.BmlWalletToken
/**
* Encrypted local store for a pool of single-use HCE payment tokens.
* Tokens are consumed one per NFC transaction.
*/
class HceTokenStore(context: Context) {
private val prefs: SharedPreferences = try {
val masterKey = MasterKey.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()
EncryptedSharedPreferences.create(
context,
"hce_token_store",
masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
} catch (_: Exception) {
context.getSharedPreferences("hce_token_store_fallback", Context.MODE_PRIVATE)
}
fun saveTokens(tokens: List<BmlWalletToken>) {
val arr = JSONArray()
tokens.forEach { t ->
arr.put(JSONObject().apply {
put("token", t.token)
put("expiry", t.expiry)
put("app_code", t.appCode)
put("service_code", t.serviceCode)
put("data", t.data)
put("valid_until", t.validUntil)
})
}
prefs.edit().putString("tokens", arr.toString()).apply()
}
/** Remove and return the next token, or null if pool is empty / all expired. */
fun popToken(): BmlWalletToken? {
val raw = prefs.getString("tokens", null) ?: return null
val arr = try { JSONArray(raw) } catch (_: Exception) { return null }
if (arr.length() == 0) return null
val now = System.currentTimeMillis()
var found: BmlWalletToken? = null
var foundIndex = -1
for (i in 0 until arr.length()) {
val o = arr.getJSONObject(i)
val validUntil = o.optString("valid_until", "")
if (validUntil.isNotBlank() && isExpired(validUntil, now)) continue
found = BmlWalletToken(
token = o.getString("token"),
expiry = o.getString("expiry"),
appCode = o.getString("app_code"),
serviceCode = o.getString("service_code"),
data = o.optString("data", ""),
validUntil = validUntil
)
foundIndex = i
break
}
if (found == null || foundIndex < 0) {
prefs.edit().remove("tokens").apply()
return null
}
// Remove consumed token
val remaining = JSONArray()
for (i in 0 until arr.length()) {
if (i != foundIndex) remaining.put(arr.getJSONObject(i))
}
prefs.edit().putString("tokens", remaining.toString()).apply()
return found
}
fun remainingCount(): Int {
val raw = prefs.getString("tokens", null) ?: return 0
return try { JSONArray(raw).length() } catch (_: Exception) { 0 }
}
fun hasTokens(): Boolean = remainingCount() > 0
fun clear() {
prefs.edit().remove("tokens").apply()
}
/** "YYYY-MM-DD HH:mm:ss.SSS" → check if past now */
private fun isExpired(validUntil: String, nowMs: Long): Boolean = try {
val fmt = java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", java.util.Locale.US)
fmt.timeZone = java.util.TimeZone.getTimeZone("UTC")
val d = fmt.parse(validUntil) ?: return false
d.time < nowMs
} catch (_: Exception) { false }
}
@@ -98,12 +98,16 @@ class DashboardFragment : Fragment() {
LinearSnapHelper().attachToRecyclerView(binding.rvCards)
val updateCardList = {
val mibItems = (viewModel.mibCards.value ?: emptyList()).map { CardItem.Mib(it) }
val credStore = CredentialStore(requireContext())
val hidden = credStore.getHiddenDashboardCardNumbers()
val mibItems = (viewModel.mibCards.value ?: emptyList())
.filter { !hidden.contains(it.maskedCardNumber) }
.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") && it.statusDesc.equals("Active", ignoreCase = true) && !hidden.contains(it.accountNumber) }
.map { CardItem.Bml(it) }
val all = mibItems + bmlItems
val defaultNum = CredentialStore(requireContext()).getDefaultCardAccountNumber()
val defaultNum = credStore.getDefaultCardAccountNumber()
val ordered = if (defaultNum != null) {
val def = all.filterIsInstance<CardItem.Bml>().firstOrNull { it.account.accountNumber == defaultNum }
if (def != null) listOf(def) + all.filter { it !== def } else all
@@ -386,8 +390,14 @@ class DashboardFragment : Fragment() {
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(requireContext(), msg, Toast.LENGTH_SHORT).show()
if (isMib) {
Toast.makeText(requireContext(), R.string.mib_qr_nfc_not_supported, Toast.LENGTH_SHORT).show()
} else {
(requireActivity() as HomeActivity).navigateTo(
R.id.nav_pay_with_card,
CardsFragment.newInstanceWithAutoTapMode()
)
}
}
}
}
@@ -237,10 +237,13 @@ class HomeActivity : AppCompatActivity() {
if (savedInstanceState == null) {
val navDest = intent.getIntExtra("nav_destination", -1)
val autoScan = intent.getBooleanExtra("auto_scan", false)
val autoTapMode = intent.getBooleanExtra("auto_tap_mode", false)
if (navDest != -1) {
val fragment = if (autoScan && navDest == R.id.nav_transfer)
TransferFragment.newInstanceWithAutoScan()
else null
val fragment = when {
autoScan && navDest == R.id.nav_transfer -> TransferFragment.newInstanceWithAutoScan()
autoTapMode && navDest == R.id.nav_pay_with_card -> CardsFragment.newInstanceWithAutoTapMode()
else -> null
}
navigateTo(navDest, fragment)
} else {
show(DashboardFragment())
@@ -42,7 +42,6 @@ import sh.sar.basedbank.BasedBankApp
import sh.sar.basedbank.R
import sh.sar.basedbank.api.bml.BmlTapToPayClient
import sh.sar.basedbank.nfc.BmlHostCardEmulatorService
import sh.sar.basedbank.nfc.HceTokenStore
import sh.sar.basedbank.api.mib.MibCard
import sh.sar.basedbank.databinding.FragmentCardsBinding
import sh.sar.basedbank.util.CardsCache
@@ -65,6 +64,7 @@ class CardsFragment : Fragment() {
private var isManageMode: Boolean = false
private var isTapMode: Boolean = false
private var tapAnimView: NfcTapAnimationView? = null
private var autoTapModeTriggered = false
// Carousel snapshot captured on enter, used to reverse the exit animation
private var carouselCardLayoutTop = 0f // card layout top relative to contentLayout
@@ -283,6 +283,7 @@ ViewCompat.setOnApplyWindowInsetsListener(binding.contentLayout) { v, insets ->
binding.llPayButtons.visibility = View.GONE
binding.llManageButtons.visibility = View.VISIBLE
binding.llDefaultCardRow.visibility = View.VISIBLE
binding.llHideDashboardRow.visibility = View.VISIBLE
binding.manageCardView.root.visibility = View.VISIBLE
// Set switch state (clear listener first to avoid triggering on programmatic set)
@@ -303,6 +304,17 @@ ViewCompat.setOnApplyWindowInsetsListener(binding.contentLayout) { v, insets ->
}
}
val accountNumber = (item as? CardItem.Bml)?.account?.accountNumber
?: (item as? CardItem.Mib)?.card?.maskedCardNumber
binding.switchHideFromDashboard.setOnCheckedChangeListener(null)
binding.switchHideFromDashboard.isChecked = accountNumber != null &&
store.getHiddenDashboardCardNumbers().contains(accountNumber)
binding.switchHideFromDashboard.setOnCheckedChangeListener { _, isChecked ->
if (accountNumber != null) {
store.setCardHiddenFromDashboard(accountNumber, isChecked)
}
}
// After layout pass, compute offsets, save carousel snapshot, and animate
binding.contentLayout.doOnNextLayout {
val mgr = binding.manageCardView.root
@@ -394,7 +406,9 @@ ViewCompat.setOnApplyWindowInsetsListener(binding.contentLayout) { v, insets ->
binding.llPayButtons.visibility = View.VISIBLE
binding.llManageButtons.visibility = View.GONE
binding.llDefaultCardRow.visibility = View.GONE
binding.llHideDashboardRow.visibility = View.GONE
binding.switchDefaultCard.setOnCheckedChangeListener(null)
binding.switchHideFromDashboard.setOnCheckedChangeListener(null)
buildDots(cards.size, currentCardPosition)
}
.start()
@@ -589,58 +603,42 @@ ViewCompat.setOnApplyWindowInsetsListener(binding.contentLayout) { v, insets ->
private fun fetchAndArmToken(item: CardItem.Bml) {
val app = requireActivity().application as BasedBankApp
val tokenStore = HceTokenStore(requireContext())
viewLifecycleOwner.lifecycleScope.launch {
var token = withContext(Dispatchers.IO) { tokenStore.popToken() }
val loginId = item.account.loginTag.removePrefix("bml_")
val session = app.bmlSessionFor(item.account)
val otpSeed = CredentialStore(requireContext()).loadBmlCredentials(loginId)?.otpSeed
if (token == null) {
val loginId = item.account.loginTag.removePrefix("bml_")
val session = app.bmlSessionFor(item.account)
val otpSeed = CredentialStore(requireContext()).loadBmlCredentials(loginId)?.otpSeed
if (session == null || otpSeed == null) {
if (isTapMode) {
Toast.makeText(requireContext(),
if (session == null) getString(R.string.transfer_session_unavailable)
else "OTP unavailable",
Toast.LENGTH_SHORT).show()
setTapMode(false)
}
return@launch
}
val otp = Totp.generate(otpSeed)
val result = withContext(Dispatchers.IO) {
runCatching { BmlTapToPayClient().fetchTokens(session, item.account.internalId, otp) }
}
val fetched = result.getOrNull()
if (fetched.isNullOrEmpty()) {
if (isTapMode) {
Toast.makeText(requireContext(),
result.exceptionOrNull()?.message ?: "Failed to get payment token",
Toast.LENGTH_SHORT).show()
setTapMode(false)
}
return@launch
}
withContext(Dispatchers.IO) {
tokenStore.saveTokens(fetched)
token = tokenStore.popToken()
}
}
if (!isTapMode) return@launch // user cancelled while we were fetching
val t = token ?: run {
if (session == null || otpSeed == null) {
if (isTapMode) {
Toast.makeText(requireContext(), "No valid payment tokens", Toast.LENGTH_SHORT).show()
Toast.makeText(requireContext(),
if (session == null) getString(R.string.transfer_session_unavailable)
else "OTP unavailable",
Toast.LENGTH_SHORT).show()
setTapMode(false)
}
return@launch
}
BmlHostCardEmulatorService.setToken(t)
val otp = Totp.generate(otpSeed)
val result = withContext(Dispatchers.IO) {
runCatching { BmlTapToPayClient().fetchTokens(session, item.account.internalId, otp) }
}
val token = result.getOrNull()?.firstOrNull()
if (!isTapMode) return@launch // user cancelled while we were fetching
if (token == null) {
if (isTapMode) {
Toast.makeText(requireContext(),
result.exceptionOrNull()?.message ?: "Failed to get payment token",
Toast.LENGTH_SHORT).show()
setTapMode(false)
}
return@launch
}
BmlHostCardEmulatorService.setToken(token)
BmlHostCardEmulatorService.onTransactionComplete = { success ->
view?.post {
if (!isTapMode) return@post
@@ -685,6 +683,20 @@ ViewCompat.setOnApplyWindowInsetsListener(binding.contentLayout) { v, insets ->
buildDots(cards.size, currentCardPosition)
updateCardInfo(currentCardPosition)
}
// Auto-enter tap mode when launched from shortcut or NFC prompt
if (!autoTapModeTriggered && arguments?.getBoolean(ARG_AUTO_TAP_MODE) == true) {
val defaultCard = cards.filterIsInstance<CardItem.Bml>().firstOrNull()
if (defaultCard != null) {
autoTapModeTriggered = true
val prefs = requireContext().getSharedPreferences("prefs", Context.MODE_PRIVATE)
if (prefs.getBoolean("biometrics_transfer_confirm", false)) {
showBiometricPromptForTap(defaultCard)
} else {
setTapMode(true, defaultCard)
}
}
}
}
private fun applyCardScales() {
@@ -931,6 +943,11 @@ ViewCompat.setOnApplyWindowInsetsListener(binding.contentLayout) { v, insets ->
}
companion object {
private const val ARG_AUTO_TAP_MODE = "auto_tap_mode"
fun newInstanceWithAutoTapMode() = CardsFragment().apply {
arguments = Bundle().apply { putBoolean(ARG_AUTO_TAP_MODE, true) }
}
fun cardImageAsset(card: MibCard): String? = when (card.cardType) {
"51" -> "cards/mib/faisa_card.png"
"53" -> "cards/mib/visa_black_platinum.png"
@@ -627,6 +627,17 @@ class CredentialStore(context: Context) {
editor.apply()
}
// ── Dashboard card visibility ─────────────────────────────────────────────
fun getHiddenDashboardCardNumbers(): Set<String> =
prefs.getStringSet("hidden_dashboard_cards", emptySet()) ?: emptySet()
fun setCardHiddenFromDashboard(accountNumber: String, hidden: Boolean) {
val current = getHiddenDashboardCardNumbers().toMutableSet()
if (hidden) current.add(accountNumber) else current.remove(accountNumber)
prefs.edit().putStringSet("hidden_dashboard_cards", current).apply()
}
// ── MIB profile visibility (per loginId) ─────────────────────────────────
/** Returns the set of MIB profile IDs the user has chosen to hide (for a given loginId). */
+36 -4
View File
@@ -2,9 +2,41 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
android:viewportWidth="48"
android:viewportHeight="48">
<!-- Phone outline -->
<path
android:fillColor="?attr/colorOnSurface"
android:pathData="M20,2L4,2C2.9,2 2,2.9 2,4l0,16c0,1.1 0.9,2 2,2l16,0c1.1,0 2,-0.9 2,-2L22,4C22,2.9 21.1,2 20,2zM13,18l-2,0 0,-1c-2.76,0 -5,-2.24 -5,-5s2.24,-5 5,-5 5,2.24 5,5l-2,0c0,-1.65 -1.35,-3 -3,-3s-3,1.35 -3,3 1.35,3 3,3l0,-2 2,3zM19,12l-2,0c0,-2.76 -2.24,-5 -5,-5l0,-2C15.87,5 19,8.13 19,12z"/>
android:fillColor="#00000000"
android:strokeColor="?attr/colorOnSurface"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M6.08,8.6 L20.45,8.6 A1.58,1.58,0,0,1,22.03,10.18 L22.03,37.81 A1.58,1.58,0,0,1,20.45,39.39 L6.08,39.39 A1.58,1.58,0,0,1,4.5,37.81 L4.5,10.18 A1.58,1.58,0,0,1,6.08,8.6 Z"/>
<!-- Top notch line -->
<path
android:fillColor="#00000000"
android:strokeColor="?attr/colorOnSurface"
android:strokeWidth="2"
android:strokeLineCap="round"
android:pathData="M4.5,12.55 L22.03,12.55"/>
<!-- Bottom notch line -->
<path
android:fillColor="#00000000"
android:strokeColor="?attr/colorOnSurface"
android:strokeWidth="2"
android:strokeLineCap="round"
android:pathData="M4.5,35.45 L22.03,35.45"/>
<!-- NFC waves (outer, mid, inner) -->
<path
android:fillColor="#00000000"
android:strokeColor="?attr/colorOnSurface"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M36.07,9.29 a18.28,18.28,0,0,1,0,29.42 m-4,-24.16 a11.84,11.84,0,0,1,5,9.45 11.84,11.84,0,0,1,-5,9.45 m-3.68,-14 a5.67,5.67,0,0,1,0,9.1"/>
</vector>
@@ -4,13 +4,47 @@
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<group
android:translateX="30"
android:translateY="30"
android:scaleX="2"
android:scaleY="2">
android:scaleX="1.0"
android:scaleY="1.0">
<!-- Phone outline -->
<path
android:fillColor="#FFFFFF"
android:pathData="M20,4H4C2.89,4 2.01,4.89 2.01,6L2,18c0,1.11 0.89,2 2,2h16c1.11,0 2,-0.89 2,-2V6C22,4.89 21.11,4 20,4zM20,18H4v-6h16V18zM20,8H4V6h16V8z" />
android:fillColor="#00000000"
android:strokeColor="#FFFFFF"
android:strokeWidth="2.5"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M6.08,8.6 L20.45,8.6 A1.58,1.58,0,0,1,22.03,10.18 L22.03,37.81 A1.58,1.58,0,0,1,20.45,39.39 L6.08,39.39 A1.58,1.58,0,0,1,4.5,37.81 L4.5,10.18 A1.58,1.58,0,0,1,6.08,8.6 Z"/>
<!-- Top notch line -->
<path
android:fillColor="#00000000"
android:strokeColor="#FFFFFF"
android:strokeWidth="2.5"
android:strokeLineCap="round"
android:pathData="M4.5,12.55 L22.03,12.55"/>
<!-- Bottom notch line -->
<path
android:fillColor="#00000000"
android:strokeColor="#FFFFFF"
android:strokeWidth="2.5"
android:strokeLineCap="round"
android:pathData="M4.5,35.45 L22.03,35.45"/>
<!-- NFC waves -->
<path
android:fillColor="#00000000"
android:strokeColor="#FFFFFF"
android:strokeWidth="2.5"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M36.07,9.29 a18.28,18.28,0,0,1,0,29.42 m-4,-24.16 a11.84,11.84,0,0,1,5,9.45 11.84,11.84,0,0,1,-5,9.45 m-3.68,-14 a5.67,5.67,0,0,1,0,9.1"/>
</group>
</vector>
@@ -174,6 +174,32 @@
</LinearLayout>
<!-- Hide from dashboard toggle (manage mode only) -->
<LinearLayout
android:id="@+id/llHideDashboardRow"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:paddingHorizontal="20dp"
android:paddingTop="4dp"
android:paddingBottom="4dp"
android:visibility="gone">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/card_hide_from_dashboard"
android:textAppearance="?attr/textAppearanceBodyMedium" />
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/switchHideFromDashboard"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
<!-- Card management actions (manage mode only) -->
<LinearLayout
android:id="@+id/llManageButtons"
+1
View File
@@ -332,6 +332,7 @@
<string name="mib_qr_nfc_not_supported">Skill issue on MIB side, Not supported</string>
<string name="card_manage">Manage Card</string>
<string name="card_set_as_default">Set as Default Card</string>
<string name="card_hide_from_dashboard">Hide from Dashboard</string>
<string name="card_action_change_pin">Change PIN</string>
<string name="card_action_freeze">Freeze</string>
<string name="card_action_block">Block</string>
+1 -1
View File
@@ -30,7 +30,7 @@
<shortcut
android:shortcutId="tap_to_pay"
android:enabled="true"
android:icon="@drawable/ic_nfc"
android:icon="@drawable/ic_shortcut_pay_card"
android:shortcutShortLabel="@string/card_pay_nfc"
android:shortcutLongLabel="@string/card_pay_nfc">
<intent