forked from shihaam/thijooree
new nfc icon, hide cards, removed offline nfc payments
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 navDest = intent.getIntExtra("nav_destination", -1)
|
||||||
val autoScan = intent.getBooleanExtra("auto_scan", false)
|
val autoScan = intent.getBooleanExtra("auto_scan", false)
|
||||||
|
val autoTapMode = intent.getBooleanExtra("auto_tap_mode", false)
|
||||||
startActivity(Intent(this, HomeActivity::class.java).apply {
|
startActivity(Intent(this, HomeActivity::class.java).apply {
|
||||||
if (navDest != -1) putExtra("nav_destination", navDest)
|
if (navDest != -1) putExtra("nav_destination", navDest)
|
||||||
if (autoScan) putExtra("auto_scan", true)
|
if (autoScan) putExtra("auto_scan", true)
|
||||||
|
if (autoTapMode) putExtra("auto_tap_mode", true)
|
||||||
})
|
})
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import sh.sar.basedbank.ui.onboarding.OnboardingActivity
|
|||||||
import sh.sar.basedbank.util.CredentialStore
|
import sh.sar.basedbank.util.CredentialStore
|
||||||
import sh.sar.basedbank.BasedBankApp
|
import sh.sar.basedbank.BasedBankApp
|
||||||
import sh.sar.basedbank.R
|
import sh.sar.basedbank.R
|
||||||
import sh.sar.basedbank.nfc.BmlTapToPayActivity
|
|
||||||
|
|
||||||
class MainActivity : AppCompatActivity() {
|
class MainActivity : AppCompatActivity() {
|
||||||
|
|
||||||
@@ -22,20 +21,15 @@ class MainActivity : AppCompatActivity() {
|
|||||||
val store = CredentialStore(this)
|
val store = CredentialStore(this)
|
||||||
val hasCredentials = store.hasMibCredentials() || store.hasBmlCredentials() || store.hasFahipayCredentials()
|
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) {
|
val navDestination = when (intent?.action) {
|
||||||
"sh.sar.basedbank.OPEN_TRANSFER" -> R.id.nav_transfer
|
"sh.sar.basedbank.OPEN_TRANSFER" -> R.id.nav_transfer
|
||||||
"sh.sar.basedbank.OPEN_SCAN_QR" -> 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.OPEN_PAY_WITH_CARD" -> R.id.nav_pay_with_card
|
||||||
|
"sh.sar.basedbank.TAP_TO_PAY" -> R.id.nav_pay_with_card
|
||||||
else -> -1
|
else -> -1
|
||||||
}
|
}
|
||||||
val autoScan = intent?.action == "sh.sar.basedbank.OPEN_SCAN_QR"
|
val autoScan = intent?.action == "sh.sar.basedbank.OPEN_SCAN_QR"
|
||||||
|
val autoTapMode = intent?.action == "sh.sar.basedbank.TAP_TO_PAY"
|
||||||
|
|
||||||
val target = when {
|
val target = when {
|
||||||
!onboardingDone -> OnboardingActivity::class.java
|
!onboardingDone -> OnboardingActivity::class.java
|
||||||
@@ -51,6 +45,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
startActivity(Intent(this, target).apply {
|
startActivity(Intent(this, target).apply {
|
||||||
if (navDestination != -1) putExtra("nav_destination", navDestination)
|
if (navDestination != -1) putExtra("nav_destination", navDestination)
|
||||||
if (autoScan) putExtra("auto_scan", true)
|
if (autoScan) putExtra("auto_scan", true)
|
||||||
|
if (autoTapMode) putExtra("auto_tap_mode", true)
|
||||||
})
|
})
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,8 +56,9 @@ class BmlHostCardEmulatorService : HostApduService() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun launchPromptActivity() {
|
private fun launchPromptActivity() {
|
||||||
val intent = Intent(applicationContext, BmlTapToPayActivity::class.java).apply {
|
val intent = Intent("sh.sar.basedbank.TAP_TO_PAY").apply {
|
||||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP
|
setPackage(applicationContext.packageName)
|
||||||
|
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||||
}
|
}
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,166 +2,19 @@ package sh.sar.basedbank.nfc
|
|||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
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.appcompat.app.AppCompatActivity
|
||||||
import androidx.biometric.BiometricManager
|
import sh.sar.basedbank.MainActivity
|
||||||
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
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Launched by [BmlHostCardEmulatorService] when the phone is tapped against a POS terminal
|
* Fallback entry point — redirects to MainActivity which routes to the in-app tap-to-pay screen.
|
||||||
* 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.
|
|
||||||
*/
|
*/
|
||||||
class BmlTapToPayActivity : AppCompatActivity() {
|
class BmlTapToPayActivity : AppCompatActivity() {
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
prepare()
|
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
|
||||||
// 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()
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
finish()
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
LinearSnapHelper().attachToRecyclerView(binding.rvCards)
|
||||||
|
|
||||||
val updateCardList = {
|
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())
|
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) }
|
.map { CardItem.Bml(it) }
|
||||||
val all = mibItems + bmlItems
|
val all = mibItems + bmlItems
|
||||||
val defaultNum = CredentialStore(requireContext()).getDefaultCardAccountNumber()
|
val defaultNum = credStore.getDefaultCardAccountNumber()
|
||||||
val ordered = if (defaultNum != null) {
|
val ordered = if (defaultNum != null) {
|
||||||
val def = all.filterIsInstance<CardItem.Bml>().firstOrNull { it.account.accountNumber == defaultNum }
|
val def = all.filterIsInstance<CardItem.Bml>().firstOrNull { it.account.accountNumber == defaultNum }
|
||||||
if (def != null) listOf(def) + all.filter { it !== def } else all
|
if (def != null) listOf(def) + all.filter { it !== def } else all
|
||||||
@@ -386,8 +390,14 @@ class DashboardFragment : Fragment() {
|
|||||||
val nfcSupported = nfcAdapter != null
|
val nfcSupported = nfcAdapter != null
|
||||||
btnPayNfc.isEnabled = nfcSupported
|
btnPayNfc.isEnabled = nfcSupported
|
||||||
btnPayNfc.setOnClickListener {
|
btnPayNfc.setOnClickListener {
|
||||||
val msg = if (isMib) R.string.mib_qr_nfc_not_supported else R.string.work_in_progress
|
if (isMib) {
|
||||||
Toast.makeText(requireContext(), msg, Toast.LENGTH_SHORT).show()
|
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) {
|
if (savedInstanceState == null) {
|
||||||
val navDest = intent.getIntExtra("nav_destination", -1)
|
val navDest = intent.getIntExtra("nav_destination", -1)
|
||||||
val autoScan = intent.getBooleanExtra("auto_scan", false)
|
val autoScan = intent.getBooleanExtra("auto_scan", false)
|
||||||
|
val autoTapMode = intent.getBooleanExtra("auto_tap_mode", false)
|
||||||
if (navDest != -1) {
|
if (navDest != -1) {
|
||||||
val fragment = if (autoScan && navDest == R.id.nav_transfer)
|
val fragment = when {
|
||||||
TransferFragment.newInstanceWithAutoScan()
|
autoScan && navDest == R.id.nav_transfer -> TransferFragment.newInstanceWithAutoScan()
|
||||||
else null
|
autoTapMode && navDest == R.id.nav_pay_with_card -> CardsFragment.newInstanceWithAutoTapMode()
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
navigateTo(navDest, fragment)
|
navigateTo(navDest, fragment)
|
||||||
} else {
|
} else {
|
||||||
show(DashboardFragment())
|
show(DashboardFragment())
|
||||||
|
|||||||
@@ -42,7 +42,6 @@ import sh.sar.basedbank.BasedBankApp
|
|||||||
import sh.sar.basedbank.R
|
import sh.sar.basedbank.R
|
||||||
import sh.sar.basedbank.api.bml.BmlTapToPayClient
|
import sh.sar.basedbank.api.bml.BmlTapToPayClient
|
||||||
import sh.sar.basedbank.nfc.BmlHostCardEmulatorService
|
import sh.sar.basedbank.nfc.BmlHostCardEmulatorService
|
||||||
import sh.sar.basedbank.nfc.HceTokenStore
|
|
||||||
import sh.sar.basedbank.api.mib.MibCard
|
import sh.sar.basedbank.api.mib.MibCard
|
||||||
import sh.sar.basedbank.databinding.FragmentCardsBinding
|
import sh.sar.basedbank.databinding.FragmentCardsBinding
|
||||||
import sh.sar.basedbank.util.CardsCache
|
import sh.sar.basedbank.util.CardsCache
|
||||||
@@ -65,6 +64,7 @@ class CardsFragment : Fragment() {
|
|||||||
private var isManageMode: Boolean = false
|
private var isManageMode: Boolean = false
|
||||||
private var isTapMode: Boolean = false
|
private var isTapMode: Boolean = false
|
||||||
private var tapAnimView: NfcTapAnimationView? = null
|
private var tapAnimView: NfcTapAnimationView? = null
|
||||||
|
private var autoTapModeTriggered = false
|
||||||
|
|
||||||
// Carousel snapshot captured on enter, used to reverse the exit animation
|
// Carousel snapshot captured on enter, used to reverse the exit animation
|
||||||
private var carouselCardLayoutTop = 0f // card layout top relative to contentLayout
|
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.llPayButtons.visibility = View.GONE
|
||||||
binding.llManageButtons.visibility = View.VISIBLE
|
binding.llManageButtons.visibility = View.VISIBLE
|
||||||
binding.llDefaultCardRow.visibility = View.VISIBLE
|
binding.llDefaultCardRow.visibility = View.VISIBLE
|
||||||
|
binding.llHideDashboardRow.visibility = View.VISIBLE
|
||||||
binding.manageCardView.root.visibility = View.VISIBLE
|
binding.manageCardView.root.visibility = View.VISIBLE
|
||||||
|
|
||||||
// Set switch state (clear listener first to avoid triggering on programmatic set)
|
// 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
|
// After layout pass, compute offsets, save carousel snapshot, and animate
|
||||||
binding.contentLayout.doOnNextLayout {
|
binding.contentLayout.doOnNextLayout {
|
||||||
val mgr = binding.manageCardView.root
|
val mgr = binding.manageCardView.root
|
||||||
@@ -394,7 +406,9 @@ ViewCompat.setOnApplyWindowInsetsListener(binding.contentLayout) { v, insets ->
|
|||||||
binding.llPayButtons.visibility = View.VISIBLE
|
binding.llPayButtons.visibility = View.VISIBLE
|
||||||
binding.llManageButtons.visibility = View.GONE
|
binding.llManageButtons.visibility = View.GONE
|
||||||
binding.llDefaultCardRow.visibility = View.GONE
|
binding.llDefaultCardRow.visibility = View.GONE
|
||||||
|
binding.llHideDashboardRow.visibility = View.GONE
|
||||||
binding.switchDefaultCard.setOnCheckedChangeListener(null)
|
binding.switchDefaultCard.setOnCheckedChangeListener(null)
|
||||||
|
binding.switchHideFromDashboard.setOnCheckedChangeListener(null)
|
||||||
buildDots(cards.size, currentCardPosition)
|
buildDots(cards.size, currentCardPosition)
|
||||||
}
|
}
|
||||||
.start()
|
.start()
|
||||||
@@ -589,58 +603,42 @@ ViewCompat.setOnApplyWindowInsetsListener(binding.contentLayout) { v, insets ->
|
|||||||
|
|
||||||
private fun fetchAndArmToken(item: CardItem.Bml) {
|
private fun fetchAndArmToken(item: CardItem.Bml) {
|
||||||
val app = requireActivity().application as BasedBankApp
|
val app = requireActivity().application as BasedBankApp
|
||||||
val tokenStore = HceTokenStore(requireContext())
|
|
||||||
|
|
||||||
viewLifecycleOwner.lifecycleScope.launch {
|
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) {
|
if (session == null || otpSeed == 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 (isTapMode) {
|
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)
|
setTapMode(false)
|
||||||
}
|
}
|
||||||
return@launch
|
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 ->
|
BmlHostCardEmulatorService.onTransactionComplete = { success ->
|
||||||
view?.post {
|
view?.post {
|
||||||
if (!isTapMode) return@post
|
if (!isTapMode) return@post
|
||||||
@@ -685,6 +683,20 @@ ViewCompat.setOnApplyWindowInsetsListener(binding.contentLayout) { v, insets ->
|
|||||||
buildDots(cards.size, currentCardPosition)
|
buildDots(cards.size, currentCardPosition)
|
||||||
updateCardInfo(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() {
|
private fun applyCardScales() {
|
||||||
@@ -931,6 +943,11 @@ ViewCompat.setOnApplyWindowInsetsListener(binding.contentLayout) { v, insets ->
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
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) {
|
fun cardImageAsset(card: MibCard): String? = when (card.cardType) {
|
||||||
"51" -> "cards/mib/faisa_card.png"
|
"51" -> "cards/mib/faisa_card.png"
|
||||||
"53" -> "cards/mib/visa_black_platinum.png"
|
"53" -> "cards/mib/visa_black_platinum.png"
|
||||||
|
|||||||
@@ -627,6 +627,17 @@ class CredentialStore(context: Context) {
|
|||||||
editor.apply()
|
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) ─────────────────────────────────
|
// ── MIB profile visibility (per loginId) ─────────────────────────────────
|
||||||
|
|
||||||
/** Returns the set of MIB profile IDs the user has chosen to hide (for a given 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"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:width="24dp"
|
android:width="24dp"
|
||||||
android:height="24dp"
|
android:height="24dp"
|
||||||
android:viewportWidth="24"
|
android:viewportWidth="48"
|
||||||
android:viewportHeight="24">
|
android:viewportHeight="48">
|
||||||
|
|
||||||
|
<!-- Phone outline -->
|
||||||
<path
|
<path
|
||||||
android:fillColor="?attr/colorOnSurface"
|
android:fillColor="#00000000"
|
||||||
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: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>
|
</vector>
|
||||||
|
|||||||
@@ -4,13 +4,47 @@
|
|||||||
android:height="108dp"
|
android:height="108dp"
|
||||||
android:viewportWidth="108"
|
android:viewportWidth="108"
|
||||||
android:viewportHeight="108">
|
android:viewportHeight="108">
|
||||||
|
|
||||||
<group
|
<group
|
||||||
android:translateX="30"
|
android:translateX="30"
|
||||||
android:translateY="30"
|
android:translateY="30"
|
||||||
android:scaleX="2"
|
android:scaleX="1.0"
|
||||||
android:scaleY="2">
|
android:scaleY="1.0">
|
||||||
|
|
||||||
|
<!-- Phone outline -->
|
||||||
<path
|
<path
|
||||||
android:fillColor="#FFFFFF"
|
android:fillColor="#00000000"
|
||||||
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: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>
|
</group>
|
||||||
|
|
||||||
</vector>
|
</vector>
|
||||||
|
|||||||
@@ -174,6 +174,32 @@
|
|||||||
|
|
||||||
</LinearLayout>
|
</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) -->
|
<!-- Card management actions (manage mode only) -->
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/llManageButtons"
|
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="mib_qr_nfc_not_supported">Skill issue on MIB side, Not supported</string>
|
||||||
<string name="card_manage">Manage Card</string>
|
<string name="card_manage">Manage Card</string>
|
||||||
<string name="card_set_as_default">Set as Default 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_change_pin">Change PIN</string>
|
||||||
<string name="card_action_freeze">Freeze</string>
|
<string name="card_action_freeze">Freeze</string>
|
||||||
<string name="card_action_block">Block</string>
|
<string name="card_action_block">Block</string>
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
<shortcut
|
<shortcut
|
||||||
android:shortcutId="tap_to_pay"
|
android:shortcutId="tap_to_pay"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:icon="@drawable/ic_nfc"
|
android:icon="@drawable/ic_shortcut_pay_card"
|
||||||
android:shortcutShortLabel="@string/card_pay_nfc"
|
android:shortcutShortLabel="@string/card_pay_nfc"
|
||||||
android:shortcutLongLabel="@string/card_pay_nfc">
|
android:shortcutLongLabel="@string/card_pay_nfc">
|
||||||
<intent
|
<intent
|
||||||
|
|||||||
Reference in New Issue
Block a user