From ee5ecdaa187e4fd1982634be0b28be8b7574bfa8 Mon Sep 17 00:00:00 2001 From: Shihaam Abdul Rahman Date: Fri, 29 May 2026 16:39:58 +0500 Subject: [PATCH] new nfc icon, hide cards, removed offline nfc payments --- app/src/debug/res/xml/shortcuts.xml | 43 +++++ .../java/sh/sar/basedbank/LockActivity.kt | 2 + .../java/sh/sar/basedbank/MainActivity.kt | 11 +- .../nfc/BmlHostCardEmulatorService.kt | 5 +- .../sar/basedbank/nfc/BmlTapToPayActivity.kt | 159 +----------------- .../sh/sar/basedbank/nfc/HceTokenStore.kt | 105 ------------ .../basedbank/ui/home/DashboardFragment.kt | 20 ++- .../sh/sar/basedbank/ui/home/HomeActivity.kt | 9 +- .../basedbank/ui/home/PayWithCardFragment.kt | 105 +++++++----- .../sh/sar/basedbank/util/CredentialStore.kt | 11 ++ app/src/main/res/drawable/ic_nfc.xml | 40 ++++- .../res/drawable/ic_shortcut_pay_card_fg.xml | 42 ++++- app/src/main/res/layout/fragment_cards.xml | 26 +++ app/src/main/res/values/strings.xml | 1 + app/src/main/res/xml/shortcuts.xml | 2 +- 15 files changed, 252 insertions(+), 329 deletions(-) create mode 100644 app/src/debug/res/xml/shortcuts.xml delete mode 100644 app/src/main/java/sh/sar/basedbank/nfc/HceTokenStore.kt 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 @@