tap-to-pay part 3: default wallet and shortcut
Auto Tag on Version Change / check-version (push) Failing after 11m53s
Auto Tag on Version Change / check-version (push) Failing after 11m53s
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
Reference in New Issue
Block a user