new nfc icon, hide cards, removed offline nfc payments
Auto Tag on Version Change / check-version (push) Failing after 12m50s
Auto Tag on Version Change / check-version (push) Failing after 12m50s
This commit is contained in:
@@ -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). */
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user