forked from thijooree/android
98 lines
4.1 KiB
Markdown
98 lines
4.1 KiB
Markdown
# Tap to Pay
|
|
|
|
BML contactless payment via Android Host Card Emulation. The app emulates a BML magnetic-stripe contactless card using a single-use wallet token fetched from the [BML Tap-to-Pay API](../bmlapi/12-tap-to-pay.md).
|
|
|
|
---
|
|
|
|
## Entry Points
|
|
|
|
| Trigger | Path |
|
|
|---|---|
|
|
| Manifest NFC service launches `BmlTapToPayActivity` | Redirects to `MainActivity` with `TAP_TO_PAY` |
|
|
| `TAP_TO_PAY` intent | `R.id.nav_pay_with_card` + `auto_tap_mode=true` |
|
|
| "Tap to Pay" button on the [Cards](22-cards.md) screen | `CardsFragment.setTapMode(true, item)` |
|
|
| Dashboard NFC button on a BML card | Same |
|
|
|
|
`BmlTapToPayActivity` (`nfc/BmlTapToPayActivity.kt`) is a 7-line trampoline — it sets the `TAP_TO_PAY` action and `FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TASK` and finishes.
|
|
|
|
---
|
|
|
|
## Pre-flight — `NfcPaymentUtil`
|
|
|
|
Located at `util/NfcPaymentUtil.kt`. Every tap-to-pay entry runs `checkAndProceed(context, onReady)` first:
|
|
|
|
1. `NfcAdapter.getDefaultAdapter()` — null → "NFC unsupported" dialog
|
|
2. `nfcAdapter.isEnabled` — false → "Turn on NFC" dialog with a button that opens `Settings.ACTION_NFC_SETTINGS`
|
|
3. `CardEmulation.isDefaultServiceForCategory(component, CATEGORY_PAYMENT)` — false → "Set this app as the default payment app" dialog with a button that opens `Settings.ACTION_NFC_PAYMENT_SETTINGS`
|
|
4. All three pass → `onReady()` is invoked
|
|
|
|
If the user has `biometrics_transfer_confirm` enabled, `CardsFragment.showBiometricPromptForTap()` runs a `BIOMETRIC_STRONG` prompt before entering tap mode.
|
|
|
|
---
|
|
|
|
## Token Fetch
|
|
|
|
`fetchAndArmToken()` (in `CardsFragment`):
|
|
|
|
1. Locate the BML session and OTP seed for the selected card's login
|
|
2. Generate a fresh TOTP via `util/Totp`
|
|
3. `BmlTapToPayClient().fetchTokens(session, internalId, otp)` returns a list of `BmlWalletToken`; the first is used
|
|
4. `BmlHostCardEmulatorService.setToken(token)` arms the HCE service
|
|
5. `BmlHostCardEmulatorService.onTransactionComplete = { success -> ... }` — re-enters the carousel and shows a success toast / refresh on success
|
|
|
|
If any step fails, a toast is shown and tap mode exits.
|
|
|
|
---
|
|
|
|
## HCE Service — `BmlHostCardEmulatorService`
|
|
|
|
Subclass of `HostApduService`. Implements the minimal EMV mag-stripe contactless flow:
|
|
|
|
```
|
|
SELECT PPSE → SELECT AID → GET PROCESSING OPTIONS → READ RECORD
|
|
```
|
|
|
|
### APDU Handling
|
|
|
|
| INS | Handler | Response |
|
|
|---|---|---|
|
|
| `A4` SELECT (data = PPSE name `2PAY.SYS.DDF01`) | `handleSelect` | FCI containing app entry: tag 4F = AID, tag 87 = priority `01`, DF Name `84` = PPSE |
|
|
| `A4` SELECT (data = AID bytes from token) | `handleSelect` | FCI containing AID, ASCII label (`VISA` / `MASTERCARD` / `AMEX` / `BML`), PDOL `9F38 = 9F6602` (TTQ 2 bytes) |
|
|
| `A8` GPO | `handleGpo` | AIP `0080` (mag-stripe mode), AFL `08010100` (SFI=1, record 1-1) |
|
|
| `B2` READ RECORD | `handleReadRecord` | Tag `70` containing tag `57` Track 2 — fires `onTransactionComplete(true)` |
|
|
| Anything else | — | `6D00` (INS not supported) |
|
|
|
|
PPSE selection when no active token is set fires `launchPromptActivity()` which starts `BmlTapToPayActivity` to bring the user into the app.
|
|
|
|
### Track 2
|
|
|
|
`buildTrack2(token)`: `"${token.token}D${token.expiry}${token.serviceCode}${token.data}"`, padded with `F` to even length.
|
|
|
|
### Companion State
|
|
|
|
| Member | Use |
|
|
|---|---|
|
|
| `activeToken: BmlWalletToken?` | The armed token (volatile) |
|
|
| `onTransactionComplete: ((Boolean) -> Unit)?` | UI callback invoked from READ RECORD or `onDeactivated` |
|
|
| `setToken(token)` / `clearToken()` | Called from `CardsFragment.fetchAndArmToken()` / `exitTapMode()` |
|
|
| `applicationLabel(aidHex)` | Maps Visa/MC/Amex AIDs to ASCII labels |
|
|
|
|
`onDeactivated()` fires `onTransactionComplete(false)` if the reader dropped before GPO was sent.
|
|
|
|
---
|
|
|
|
## Failure Modes
|
|
|
|
- No NFC, NFC off, or app not the default payment service — handled by `NfcPaymentUtil` with dialogs that deep-link to Settings
|
|
- Missing OTP seed or session — toast, tap mode exits
|
|
- Token fetch fails — toast, tap mode exits
|
|
- Reader removed before READ RECORD — `onDeactivated` reports `success = false`; no toast is shown
|
|
|
|
---
|
|
|
|
|
|
|
|
---
|
|
|
|
[← Cards](22-cards.md) **Next →** [Notifications](24-notifications.md)
|