diff --git a/app/src/debug/res/xml/shortcuts.xml b/app/src/debug/res/xml/shortcuts.xml
new file mode 100644
index 0000000..1c23d57
--- /dev/null
+++ b/app/src/debug/res/xml/shortcuts.xml
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/java/sh/sar/basedbank/LockActivity.kt b/app/src/main/java/sh/sar/basedbank/LockActivity.kt
index 17aa162..8015a5a 100644
--- a/app/src/main/java/sh/sar/basedbank/LockActivity.kt
+++ b/app/src/main/java/sh/sar/basedbank/LockActivity.kt
@@ -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()
}
diff --git a/app/src/main/java/sh/sar/basedbank/MainActivity.kt b/app/src/main/java/sh/sar/basedbank/MainActivity.kt
index 1e779b7..35e9f4e 100644
--- a/app/src/main/java/sh/sar/basedbank/MainActivity.kt
+++ b/app/src/main/java/sh/sar/basedbank/MainActivity.kt
@@ -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()
}
diff --git a/app/src/main/java/sh/sar/basedbank/nfc/BmlHostCardEmulatorService.kt b/app/src/main/java/sh/sar/basedbank/nfc/BmlHostCardEmulatorService.kt
index 6870ba7..0f22728 100644
--- a/app/src/main/java/sh/sar/basedbank/nfc/BmlHostCardEmulatorService.kt
+++ b/app/src/main/java/sh/sar/basedbank/nfc/BmlHostCardEmulatorService.kt
@@ -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)
}
diff --git a/app/src/main/java/sh/sar/basedbank/nfc/BmlTapToPayActivity.kt b/app/src/main/java/sh/sar/basedbank/nfc/BmlTapToPayActivity.kt
index bc30f70..bae7b3a 100644
--- a/app/src/main/java/sh/sar/basedbank/nfc/BmlTapToPayActivity.kt
+++ b/app/src/main/java/sh/sar/basedbank/nfc/BmlTapToPayActivity.kt
@@ -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()
}
}
diff --git a/app/src/main/java/sh/sar/basedbank/nfc/HceTokenStore.kt b/app/src/main/java/sh/sar/basedbank/nfc/HceTokenStore.kt
deleted file mode 100644
index e9e5a81..0000000
--- a/app/src/main/java/sh/sar/basedbank/nfc/HceTokenStore.kt
+++ /dev/null
@@ -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) {
- 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 }
-}
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 254eab3..df1d560 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
@@ -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().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()
+ )
+ }
}
}
}
diff --git a/app/src/main/java/sh/sar/basedbank/ui/home/HomeActivity.kt b/app/src/main/java/sh/sar/basedbank/ui/home/HomeActivity.kt
index b80a6f7..1d00c8a 100644
--- a/app/src/main/java/sh/sar/basedbank/ui/home/HomeActivity.kt
+++ b/app/src/main/java/sh/sar/basedbank/ui/home/HomeActivity.kt
@@ -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())
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 bd82d90..448bb84 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
@@ -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().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"
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 29ccd00..989284a 100644
--- a/app/src/main/java/sh/sar/basedbank/util/CredentialStore.kt
+++ b/app/src/main/java/sh/sar/basedbank/util/CredentialStore.kt
@@ -627,6 +627,17 @@ class CredentialStore(context: Context) {
editor.apply()
}
+ // ── Dashboard card visibility ─────────────────────────────────────────────
+
+ fun getHiddenDashboardCardNumbers(): Set =
+ 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). */
diff --git a/app/src/main/res/drawable/ic_nfc.xml b/app/src/main/res/drawable/ic_nfc.xml
index 9c5debd..ab94ebb 100644
--- a/app/src/main/res/drawable/ic_nfc.xml
+++ b/app/src/main/res/drawable/ic_nfc.xml
@@ -2,9 +2,41 @@
+ android:viewportWidth="48"
+ android:viewportHeight="48">
+
+
+ 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"/>
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_shortcut_pay_card_fg.xml b/app/src/main/res/drawable/ic_shortcut_pay_card_fg.xml
index 61f4b7e..79063d0 100644
--- a/app/src/main/res/drawable/ic_shortcut_pay_card_fg.xml
+++ b/app/src/main/res/drawable/ic_shortcut_pay_card_fg.xml
@@ -4,13 +4,47 @@
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
+
+ android:scaleX="1.0"
+ android:scaleY="1.0">
+
+
+ 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"/>
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/fragment_cards.xml b/app/src/main/res/layout/fragment_cards.xml
index 3abfdcb..6e912fd 100644
--- a/app/src/main/res/layout/fragment_cards.xml
+++ b/app/src/main/res/layout/fragment_cards.xml
@@ -174,6 +174,32 @@
+
+
+
+
+
+
+
+
+
Skill issue on MIB side, Not supported
Manage Card
Set as Default Card
+ Hide from Dashboard
Change PIN
Freeze
Block
diff --git a/app/src/main/res/xml/shortcuts.xml b/app/src/main/res/xml/shortcuts.xml
index 7c4bba7..162c70e 100644
--- a/app/src/main/res/xml/shortcuts.xml
+++ b/app/src/main/res/xml/shortcuts.xml
@@ -30,7 +30,7 @@