Files
thijooree/docs/bmlapi/12-tap-to-pay.md
T
shihaam a8cd22cbe1
Auto Tag on Version Change / check-version (push) Failing after 13m32s
update docs
2026-06-13 21:30:12 +05:00

292 lines
8.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Tap-to-Pay (NFC / HCE)
BML supports contactless NFC payments via Host Card Emulation (HCE). The app fetches single-use payment tokens from the server, then emulates an EMV mag-stripe contactless card using Android's `HostApduService`.
---
## Overview
```
1. Fetch tokens → POST /api/mobile/walletpayments/gettoken (TOTP-authenticated)
2. HCE exchange → Android NFC subsystem drives the APDU exchange with the POS terminal
```
---
## Step 1 — Fetch Payment Tokens
### Endpoint
```
POST https://www.bankofmaldives.com.mv/internetbanking/api/mobile/walletpayments/gettoken
```
### Headers
| Header | Value |
|---|---|
| `Authorization` | `Bearer <access_token>` |
| `User-Agent` | `bml-mobile-banking/348 ({manufacturer}; Android {version}; {model})` |
| `x-app-version` | `2.1.44.348` |
| `Content-Type` | `application/json` |
### Three-Step OTP Flow
Token retrieval requires TOTP verification and completes in three POSTs to the same endpoint.
#### Step 1a — Initiate
```json
{
"type": "track2",
"cardid": "<cardId>",
"quantity": 3
}
```
Expected response: `{ "code": 99 }` (OTP required)
If `"code": 0` is returned directly the payload contains tokens immediately (skip to parsing).
#### Step 1b — Request OTP Channel
```json
{
"type": "track2",
"cardid": "<cardId>",
"quantity": 3,
"channel": "token"
}
```
Expected response: `{ "code": 22 }` (OTP generated on BML side; TOTP is used locally)
#### Step 1c — Submit TOTP
```json
{
"type": "track2",
"cardid": "<cardId>",
"quantity": 3,
"channel": "token",
"otp": "<TOTP>"
}
```
Expected response: `{ "code": 0, "payload": [...] }`
The OTP is a standard TOTP (RFC 6238, SHA-1, 30-second window, 6 digits) derived from the stored BML authenticator seed.
### Failure Handling
Each of the three POSTs validates the server's `code` field and throws on mismatch (`BmlTapToPayClient.kt:37, 42, 47`). The exception message is the server's `message` field:
| Step | Expected code | Throws if |
|---|---|---|
| 1a | `0` (rare) or `99` | code is neither — `message` propagated |
| 1b | `22` | code is not `22``message` propagated |
| 1c | `0` | code is not `0``message` propagated |
### Token Response
```json
{
"code": 0,
"payload": [
{
"token": "4761360000000000",
"expiry": "2512",
"app_code": "A0000000031010",
"service_code": "000",
"data": "0960919802623742",
"valid_until": "2025-12-01 12:00:00.000"
}
]
}
```
### Token Fields
| Field | Description |
|---|---|
| `token` | PAN-equivalent single-use token (used as Track 2 primary account number) |
| `expiry` | Expiry in `YYMM` format (e.g. `"2512"` = December 2025) |
| `app_code` | AID (Application Identifier) hex string — identifies the card network |
| `service_code` | 3-digit service code for Track 2 |
| `data` | Discretionary data appended to Track 2 |
| `valid_until` | Server-side expiry timestamp for the token |
### AID to Card Network Mapping
| AID prefix | Network |
|---|---|
| `A0000000031010` | Visa |
| `A0000000041010` | Mastercard |
| `A000000025...` | Amex |
| (other) | BML |
---
## Step 2 — HCE APDU Exchange
Once a token is set, Android's NFC subsystem routes contactless commands to the app's `HostApduService`. The flow follows the EMV mag-stripe contactless profile.
### APDU Exchange Flow
```
POS Terminal Android HCE
| |
| SELECT PPSE (INS=A4) |
|--------------------------------------->|
| FCI Template (6F) + 9000 |
|<---------------------------------------|
| |
| SELECT AID (INS=A4) |
|--------------------------------------->|
| FCI Template (6F) + 9000 |
|<---------------------------------------|
| |
| GET PROCESSING OPTIONS (INS=A8) |
|--------------------------------------->|
| Response Message Template (80) + 9000 |
|<---------------------------------------|
| |
| READ RECORD (INS=B2) |
|--------------------------------------->|
| Record Template (70) + 9000 |
|<---------------------------------------|
```
### APDU Command Bytes
| INS | Hex | Command |
|---|---|---|
| `SELECT` | `0xA4` | Select PPSE or AID |
| `GET PROCESSING OPTIONS` | `0xA8` | Request AIP + AFL |
| `READ RECORD` | `0xB2` | Read Track 2 data |
### SELECT PPSE Response
PPSE AID: `2PAY.SYS.DDF01` = `325041592E5359532E4444463031`
```
6F <len>
84 <len> 325041592E5359532E4444463031 ← DF Name (PPSE)
A5 <len>
BF0C <len>
61 <len>
4F <len> <AID> ← ADF Name
87 01 01 ← Application Priority Indicator
9000
```
### SELECT AID Response
```
6F <len>
84 <len> <AID> ← Dedicated File Name
A5 <len>
50 <len> <label-ascii-as-hex> ← Application Label (e.g. "VISA")
9F38 02 9F6602 ← PDOL: TTQ (2 bytes)
9000
```
The application label is derived from the AID prefix (see mapping table above).
### GET PROCESSING OPTIONS Response
```
80 06 0080 08010100
9000
```
| Field | Value | Meaning |
|---|---|---|
| Tag `80` | — | Response Message Template 1 |
| AIP | `0080` | Mag-stripe mode |
| AFL | `08010100` | SFI=1, records 11, 0 offline auth records |
### READ RECORD Response
```
70 <len>
57 <len> <track2-data> ← Track 2 Equivalent Data
9000
```
Track 2 format:
```
{token} D {expiry} {serviceCode} {data} [F]
```
The trailing `F` nibble is appended when the total length is odd (standard Track 2 padding).
Example from a real token:
```
4761360000000000 D 2512 000 0960919802623742
→ 4761360000000000D2512000096091980262374 2F (padded)
```
### Status Words
| SW | Meaning |
|---|---|
| `9000` | Success |
| `6F00` | Generic / unknown error |
| `6D00` | Instruction not supported |
---
## TLV Encoding
All APDU responses use BER-TLV encoding. Tags are 1 or 2 bytes (hex string). Length follows DER short/long form:
| Length range | Encoding |
|---|---|
| 0127 bytes | `LL` (1 byte) |
| 128255 bytes | `81 LL` (2 bytes) |
| 25665535 bytes | `82 HH LL` (3 bytes) |
---
## Lifecycle
The HCE service (`BmlHostCardEmulatorService`) keeps a single active `BmlWalletToken` in a volatile companion-object field. Tokens are single-use — exactly one tap consumes one token.
### Companion API
```kotlin
BmlHostCardEmulatorService.setToken(token: BmlWalletToken)
BmlHostCardEmulatorService.clearToken()
BmlHostCardEmulatorService.onTransactionComplete: (success: Boolean) -> Unit
```
| Call | When |
|---|---|
| `setToken(token)` | After fetching a token, before prompting the user to tap |
| `clearToken()` | After the tap completes, when the prompt is dismissed, or on error |
| `onTransactionComplete(true)` | Fired immediately after the `READ RECORD` response (`BmlHostCardEmulatorService.kt:78`) |
| `onTransactionComplete(false)` | Fired from `onDeactivated` if GPO was never seen (`BmlHostCardEmulatorService.kt:35-38`) — i.e. the reader walked away before completing the EMV exchange |
### State Rules
- A token MUST be installed via `setToken` before the user taps. With no active token, `SELECT PPSE` launches `BmlTapToPayActivity` (a redirector to `MainActivity`) and returns `6F00`.
- The service tracks `gpoSent` to distinguish "user pulled the phone away" from a successful read. A successful `handleReadRecord` resets `gpoSent` to `false` via `onDeactivated` after the success callback has already fired.
- `BmlTapToPayActivity` provides the "Tap your phone…" prompt UI. The activity is responsible for calling `setToken` before showing the prompt and `clearToken` when dismissed.
---
## Prerequisites
- Valid `access_token` from [OAuth Token Exchange](03-oauth-token.md)
- TOTP seed enrolled via BML app (same seed used for login 2FA)
- `cardId` from the dashboard — see [Dashboard](04-dashboard.md)
---
&nbsp;
---
[← Foreign Limits](11-foreign-limits.md) · [Next → QR Payment](13-qr-payment.md)