tap-to-pay part 3: default wallet and shortcut
Auto Tag on Version Change / check-version (push) Failing after 11m53s

This commit is contained in:
2026-05-29 15:58:05 +05:00
parent 0f77216d2d
commit 2df162c09e
6 changed files with 210 additions and 22 deletions
+6
View File
@@ -62,6 +62,12 @@
android:exported="false"
android:screenOrientation="portrait" />
<activity
android:name=".nfc.BmlTapToPayActivity"
android:exported="false"
android:launchMode="singleTop"
android:theme="@style/Theme.BasedBank" />
<service
android:name=".nfc.BmlHostCardEmulatorService"
android:exported="true"
@@ -9,6 +9,7 @@ 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() {
@@ -21,6 +22,13 @@ 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
@@ -1,5 +1,6 @@
package sh.sar.basedbank.nfc
import android.content.Intent
import android.nfc.cardemulation.HostApduService
import android.os.Bundle
import android.util.Log
@@ -40,21 +41,25 @@ class BmlHostCardEmulatorService : HostApduService() {
private fun handleSelect(apdu: Apdu): ByteArray {
val data = apdu.data ?: return SW_UNKNOWN_ERROR
val token = activeToken ?: return SW_UNKNOWN_ERROR
return when {
data.contentEquals(PPSE_BYTES) -> {
// SELECT PPSE
val resp = buildSelectPpseResponse(token.appCode, applicationLabel(token.appCode), "01")
hexToBytes(resp)
}
data.contentEquals(hexToBytes(token.appCode)) -> {
// SELECT AID
val resp = buildSelectAidResponse(token.appCode, applicationLabel(token.appCode))
hexToBytes(resp)
}
else -> SW_UNKNOWN_ERROR
if (data.contentEquals(PPSE_BYTES)) {
val token = activeToken ?: run { launchPromptActivity(); return SW_UNKNOWN_ERROR }
return hexToBytes(buildSelectPpseResponse(token.appCode, applicationLabel(token.appCode), "01"))
}
val token = activeToken ?: return SW_UNKNOWN_ERROR
return if (data.contentEquals(hexToBytes(token.appCode))) {
hexToBytes(buildSelectAidResponse(token.appCode, applicationLabel(token.appCode)))
} else {
SW_UNKNOWN_ERROR
}
}
private fun launchPromptActivity() {
val intent = Intent(applicationContext, BmlTapToPayActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP
}
startActivity(intent)
}
private fun handleGpo(): ByteArray {
@@ -0,0 +1,167 @@
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
/**
* 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.
*/
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()
}
})
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
}
}
}
+6 -4
View File
@@ -1,8 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<aid-filter xmlns:android="http://schemas.android.com/apk/res/android">
<host-apdu-service xmlns:android="http://schemas.android.com/apk/res/android"
android:description="@string/app_name"
android:requireDeviceUnlock="false">
<!-- Contactless PPSE directory -->
<aid-group android:description="@string/app_name"
<aid-group
android:description="@string/app_name"
android:category="payment">
<!-- PPSE: 2PAY.SYS.DDF01 -->
@@ -19,4 +21,4 @@
</aid-group>
</aid-filter>
</host-apdu-service>
+5 -5
View File
@@ -28,13 +28,13 @@
</shortcut>
<shortcut
android:shortcutId="pay_with_card"
android:shortcutId="tap_to_pay"
android:enabled="true"
android:icon="@drawable/ic_shortcut_pay_card"
android:shortcutShortLabel="@string/nav_pay_with_card"
android:shortcutLongLabel="@string/nav_pay_with_card">
android:icon="@drawable/ic_nfc"
android:shortcutShortLabel="@string/card_pay_nfc"
android:shortcutLongLabel="@string/card_pay_nfc">
<intent
android:action="sh.sar.basedbank.OPEN_PAY_WITH_CARD"
android:action="sh.sar.basedbank.TAP_TO_PAY"
android:targetPackage="sh.sar.basedbank"
android:targetClass="sh.sar.basedbank.MainActivity" />
<categories android:name="android.shortcut.conversation" />