forked from thijooree/android
update docs: card payments
This commit is contained in:
@@ -0,0 +1,159 @@
|
||||
# Card Verification & Merchant Card Payment
|
||||
|
||||
Two linked features:
|
||||
|
||||
1. **Card verification** — on the [Cards](22-cards.md) manage screen, a **Verify** action reads the
|
||||
physical card over NFC (or takes it by hand), checks it matches the on-screen card, and stores the
|
||||
full card details (PAN, expiry, CVV) encrypted on-device.
|
||||
2. **Merchant card payment** — on [Transfer](07-transfer.md), a BML Merchant Services transaction ID
|
||||
whose merchant has **no BML Pay** is paid with a verified card via the Pomelo + 3-D Secure flow
|
||||
([BML API → Merchant Card Payment](../bmlapi/16-card-payment.md)).
|
||||
|
||||
> ⚠️ The merchant card flow is scraped browser/ACS traffic, not a stable API. Storing the CVV is a
|
||||
> security/PCI liability. See the API doc's
|
||||
> [Fragility](../bmlapi/16-card-payment.md#fragility--what-can-break) section.
|
||||
|
||||
---
|
||||
|
||||
## Card verification
|
||||
|
||||
### Entry — the Verify button
|
||||
|
||||
In manage mode the action row has **Change PIN · Freeze · Block · Verify**
|
||||
(`fragment_cards.xml`, icon `ic_card_verify`). The button reads **Verified** once the selected card
|
||||
has a stored entry (`bindManageCardData` in `PayWithCardFragment.kt`).
|
||||
|
||||
`onVerifyClicked(item)` (`PayWithCardFragment.kt:296`) branches on NFC:
|
||||
|
||||
| Device state | Behaviour |
|
||||
|---|---|
|
||||
| No NFC hardware | Straight to manual entry (`showCardDetailsDialog`) |
|
||||
| NFC off | Dialog: **NFC Settings** / **Manually Verify** / Cancel |
|
||||
| NFC ready | Enter verify mode (tap animation) |
|
||||
|
||||
### Verify mode
|
||||
|
||||
`setVerifyMode(enabled, item)` (`PayWithCardFragment.kt:314`) swaps the manage action buttons for
|
||||
**Cancel Verification** / **Manually Verify**, and draws `CardVerifyAnimationView`
|
||||
(`ui/home/CardVerifyAnimationView.kt`) in the empty area — a flat card tapping a phone with NFC
|
||||
waves, matching the [Tap to Pay](23-tap-to-pay.md) style, with `WAITING / READING / SUCCESS / ERROR`
|
||||
states.
|
||||
|
||||
`startVerifyReader()` (`PayWithCardFragment.kt:346`) uses `NfcAdapter.enableReaderMode` (reader,
|
||||
not HCE). On tap, `EmvCardReader.read(tag)` (`nfc/EmvCardReader.kt:24`) runs a minimal contactless
|
||||
EMV read (PPSE → SELECT AID → GPO → read AFL records) and returns `CardData(pan, expiry)` from tags
|
||||
`5A` / `57` (Track 2) and `5F24`. `onVerifyCardRead` (`:367`) compares the **last 4 digits** against
|
||||
the managed card:
|
||||
|
||||
- **match** → success check mark → `showCardDetailsDialog(item, nfcData)` for the CVV;
|
||||
- **mismatch / unreadable** → error state, then back to waiting.
|
||||
|
||||
### Card details dialog
|
||||
|
||||
`showCardDetailsDialog(item, nfcData?)` (`PayWithCardFragment.kt:406`, layout
|
||||
`dialog_card_manual_verify.xml`):
|
||||
|
||||
- The **name** is always prefilled read-only from the API-provided holder name (`accountBriefName`
|
||||
for BML, `cardHolderName` for MIB) — never read off the chip.
|
||||
- **After an NFC tap** (`nfcData != null`): card number + expiry are prefilled and **locked**; only
|
||||
the CVV is entered. Title shows `Card ending <4>`.
|
||||
- **Manual entry**: number + expiry + CVV entered; validated with a Luhn check (`luhnValid`,
|
||||
`:500`), last-4 match, a not-in-the-past expiry (`normalizeExpiry`, `:489`), and a 3–4 digit CVV.
|
||||
|
||||
`saveVerifiedCard` (`PayWithCardFragment.kt:481`) writes the entry and toggles the button to
|
||||
**Verified**.
|
||||
|
||||
### Storage — `VerifiedCardStore`
|
||||
|
||||
`util/VerifiedCardStore.kt`. Per-card entry keyed by the card's identity (`bml:<accountNumber>` /
|
||||
`mib:<cardId>`), encrypted with the shared `CacheEncryption` AndroidKeyStore key (same as the other
|
||||
caches).
|
||||
|
||||
```
|
||||
VerifiedCard(pan, expiry /*MM/YY*/, cvv, method /*nfc|manual*/, verifiedAt)
|
||||
```
|
||||
|
||||
`save` / `load` / `isVerified` / `keys` / `remove` / `clear`. **Not** wiped by the "clear cache" or
|
||||
"remove login" paths — treated as user data (like profile images).
|
||||
|
||||
---
|
||||
|
||||
## Merchant card payment
|
||||
|
||||
### Routing — card-only vs BML Pay
|
||||
|
||||
A transaction ID / link typed into Transfer's **To** field is parsed by
|
||||
`BmlMerchantTxnClient.parseTransactionId` and resolved in
|
||||
`TransferFragment.lookupBmlMerchantTransaction` (`TransferFragment.kt:882`):
|
||||
|
||||
1. `BmlMerchantTxnClient.fetchPayPage(id)` (`api/bml/BmlMerchantTxnClient.kt:43`) loads `/paynow`
|
||||
(browser UA — the host is Cloudflare-fronted) and parses `window.appData`.
|
||||
2. If `!supportsBmlPay && supportsCard` → `bmlHandler().payCardMerchant(page)` (card flow).
|
||||
3. Otherwise → existing QR path (`fetchQrPayload` → `bmlQrPayTarget` → `openBmlQr`), see
|
||||
[Transfer Flows](20-transfer-flows.md).
|
||||
|
||||
### On-screen, like the QR merchant mode
|
||||
|
||||
`BmlTransferHandler.payCardMerchant(page)` (`ui/home/transfer/BmlTransferHandler.kt:441`) renders
|
||||
into the Transfer screen rather than a one-off dialog, mirroring the BML QR merchant mode:
|
||||
|
||||
- `showCardMerchant(page)` (`:468`) paints the merchant as the **To** card, fills + **locks** the
|
||||
amount (these links carry a fixed amount), and disables remarks.
|
||||
- The **From** picker is limited to BML cards; a verified default card is auto-selected.
|
||||
- State lives in `TransferDraft.bmlCardMerchant`, so it survives tab switches and theme/rotation
|
||||
recreation (repainted via `restoreFromDraft`).
|
||||
- The **✕** on the To card and `clearForm()` both call `clearCardMerchant()` (`:486`), which unlocks
|
||||
and empties the amount and re-enables remarks.
|
||||
|
||||
A card is only offered when it is **both** verified **and** belongs to a BML login the app has an
|
||||
OTP seed for (`verifiedCardCandidates`, `:428`; `isCardVerified`, `:463`) — the 3-D Secure step
|
||||
needs that seed.
|
||||
|
||||
### Send
|
||||
|
||||
`submitCardPayment` (`:496`) → `confirmCardMerchant` (`:506`) shows the shared transfer confirm
|
||||
dialog (biometric-gated), then `executeCardMerchant` (`:534`) runs, off the main thread:
|
||||
|
||||
```
|
||||
BmlMerchantCardPayClient().pay(page, card) { Totp.generate(otpSeed) }
|
||||
```
|
||||
|
||||
where `card` comes from `VerifiedCardStore` (expiry split `MM/YY` → month/year) and `otpSeed` is the
|
||||
card's BML login seed. The client (`api/bml/BmlMerchantCardPayClient.kt`) performs the whole
|
||||
Pomelo + MPGS + Wibmo 3-D Secure sequence — feeding the BML token TOTP into the ACS OTP form
|
||||
automatically, retrying once if the first code expired. Outcome is shown in the shared
|
||||
processing/success dialog; failures surface as a toast.
|
||||
|
||||
### Key assumption
|
||||
|
||||
The 3-D Secure "Authenticator" OTP must be the **same** soft-token TOTP the app already uses for BML
|
||||
transfers (`CredentialStore.loadBmlCredentials(loginId).otpSeed`). This holds for the user's own
|
||||
BML-issued card on a login the app has. It does **not** work for a non-BML card, a card belonging to
|
||||
another login/person, or a card whose 3-D Secure only offers SMS/email OTP.
|
||||
|
||||
---
|
||||
|
||||
## Files
|
||||
|
||||
| File | Role |
|
||||
|---|---|
|
||||
| `ui/home/PayWithCardFragment.kt` | Verify button, verify mode, NFC reader, card details dialog |
|
||||
| `ui/home/CardVerifyAnimationView.kt` | "Tap card to verify" animation |
|
||||
| `nfc/EmvCardReader.kt` | Minimal contactless EMV read (PAN + expiry) |
|
||||
| `util/VerifiedCardStore.kt` | Encrypted per-card store of full details |
|
||||
| `res/layout/dialog_card_manual_verify.xml` | Card details form |
|
||||
| `api/bml/BmlMerchantTxnClient.kt` | `fetchPayPage` (merchant-type detection), `announceBrowser`, QR payload |
|
||||
| `api/bml/BmlMerchantCardPayClient.kt` | Pomelo tokenise + 3-D Secure card payment |
|
||||
| `ui/home/transfer/BmlTransferHandler.kt` | On-screen card merchant mode + payment |
|
||||
| `ui/home/TransferFragment.kt` | Transaction-ID lookup + routing |
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
**Related:** [Cards](22-cards.md) · [Transfer Flows](20-transfer-flows.md) · API side:
|
||||
[Merchant Card Payment](../bmlapi/16-card-payment.md)
|
||||
|
||||
[← Settings — About](28-settings-about.md)
|
||||
@@ -34,6 +34,7 @@ Documentation for app-specific logic — UI flows, routing decisions, and busine
|
||||
| [26 — Circular Nav](26-circular-nav.md) | Radial 4-slot wheel UI with lock centre |
|
||||
| [27 — Settings: Notifications](27-settings-notifications.md) | Opt-in flow: permission → battery opt → service start |
|
||||
| [28 — Settings: About](28-settings-about.md) | Version, T&Cs, donate buttons |
|
||||
| [29 — Card Verification & Merchant Card Pay](29-card-verification-and-merchant-card-pay.md) | NFC/manual card verification + card-only BML merchant payment |
|
||||
|
||||
## Reference
|
||||
|
||||
|
||||
Reference in New Issue
Block a user