|
| 1 | +# Receive Payment Flow (C12) |
| 2 | + |
| 3 | +The receive flow lets a user request a contactless payment: enter an amount, |
| 4 | +broadcast a payment request over NFC, wait for the payer's on-chain payment, |
| 5 | +and land on a success or failure screen. It is implemented as an explicit |
| 6 | +finite-state machine (FSM) orchestrated by `useReceivePayment` and shared |
| 7 | +across screens via `ReceivePaymentProvider`. |
| 8 | + |
| 9 | +## 1. State diagram |
| 10 | + |
| 11 | +```mermaid |
| 12 | +stateDiagram-v2 |
| 13 | + [*] --> idle |
| 14 | + idle --> preparing: prepare(amount, asset, recipientPublicKey) |
| 15 | + preparing --> failed: trustline check fails (USDC only) |
| 16 | + preparing --> broadcasting: startBroadcast() |
| 17 | + broadcasting --> waiting: NFC writer reports success\n(payload delivered to payer) |
| 18 | + broadcasting --> failed: NFC writer reports error |
| 19 | + broadcasting --> cancelled: cancel() / request expiry |
| 20 | + waiting --> success: confirmSuccess(txHash?) |
| 21 | + waiting --> failed: confirmFailure(reason) / 60s wait timeout |
| 22 | + waiting --> cancelled: cancel() |
| 23 | + success --> idle: reset() ("Receive Another") |
| 24 | + failed --> idle: reset() ("Try Again" / "Change Amount") |
| 25 | + cancelled --> idle: reset() |
| 26 | +``` |
| 27 | + |
| 28 | +Note: NFC delivery success is **not** the same as payment success — it only |
| 29 | +means the request payload reached the payer's device. That's why |
| 30 | +`broadcasting` moves to `waiting` (not `success`) once the NFC writer session |
| 31 | +completes; `waiting` is resolved only by an explicit `confirmSuccess` / |
| 32 | +`confirmFailure` call (today: the 60-second wait timeout in |
| 33 | +`WaitingForPaymentView`; a future iteration can resolve it early via balance |
| 34 | +polling). |
| 35 | + |
| 36 | +## 2. File map |
| 37 | + |
| 38 | +| File | Responsibility | |
| 39 | +| --- | --- | |
| 40 | +| `src/features/receive/schemas/receiveAmount.ts` | Zod validation for the amount + asset pair (CLI-062) | |
| 41 | +| `src/features/receive/services/PaymentRequestBuilder.ts` | Builds a `PaymentRequest` via the shared `createPaymentRequest` (CLI-063) | |
| 42 | +| `src/features/receive/services/receiveSession.ts` | Owns the request-expiry and wait-timeout timers, cancellable and ghost-free (CLI-069) | |
| 43 | +| `src/features/wallet/services/TrustlineService.ts` | Checks whether the receiver has a USDC trustline before a USDC request is broadcast (CLI-070) | |
| 44 | +| `src/features/receive/hooks/useReceivePayment.ts` | FSM orchestrator + `ReceivePaymentProvider`/`useReceivePaymentContext` for cross-screen state (CLI-065) | |
| 45 | +| `src/features/receive/views/ReceiveHomeView.tsx` | Amount entry, asset selector, kicks off `prepare()` (CLI-061) | |
| 46 | +| `src/features/receive/views/ReceiveListeningView.tsx` | Starts the NFC broadcast, shows countdown + NFC status (CLI-064) | |
| 47 | +| `src/features/receive/views/WaitingForPaymentView.tsx` | Waits for payment settlement, 60s timeout (CLI-066) | |
| 48 | +| `src/features/receive/views/ReceiveSuccessView.tsx` | Success summary, reset/home actions (CLI-067) | |
| 49 | +| `src/features/receive/views/ReceiveFailedView.tsx` | Error-specific messaging, retry/change-amount actions (CLI-068) | |
| 50 | +| `src/constants/analytics-events.ts` | Receive funnel event names + sanitized property shape (CLI-071) | |
| 51 | +| `src/app/receive/{listening,waiting,success,failed}.tsx` | Expo Router screens for each non-home view | |
| 52 | +| `src/app/(tabs)/receive.tsx` | Tab entry point, renders `ReceiveHomeView` | |
| 53 | +| `src/app/_layout.tsx` | Mounts `ReceivePaymentProvider` above all routes so orchestrator state survives navigation | |
| 54 | + |
| 55 | +## 3. Timeout model |
| 56 | + |
| 57 | +Two independent timers, both owned by `ReceiveSessionManager` (`receiveSession`): |
| 58 | + |
| 59 | +- **Request expiry** — `startRequestExpiry(expiresAtSeconds, onExpire)`. Started |
| 60 | + in `startBroadcast()` using the `PaymentRequest.expiresAt` timestamp set at |
| 61 | + build time (`DEFAULT_EXPIRY_TTL_SECONDS = 5 * 60`, i.e. 5 minutes from |
| 62 | + `prepare()`). If the broadcast is still active when the request expires, the |
| 63 | + orchestrator cancels the session (`cancel()`). |
| 64 | +- **Wait timeout** — `startWaitTimeout(ms, onTimeout)`. Started by |
| 65 | + `WaitingForPaymentView` when it mounts (`WAIT_TIMEOUT_MS = 60_000`). If no |
| 66 | + success/failure confirmation arrives within 60 seconds, the view calls |
| 67 | + `confirmFailure('timeout')`. |
| 68 | + |
| 69 | +**Interaction**: request expiry only matters while broadcasting (the payer |
| 70 | +hasn't tapped yet); wait timeout only matters after the NFC handoff succeeded |
| 71 | +and we're waiting on settlement. They are mutually exclusive by construction — |
| 72 | +`startBroadcast()` cancels any prior expiry timer before starting a new one, |
| 73 | +and `cancel()` / `reset()` always call `receiveSession.cancelAll()` so no timer |
| 74 | +outlives its screen. |
| 75 | + |
| 76 | +## 4. Error matrix |
| 77 | + |
| 78 | +| Error code | Screen shown | User message | Recovery action | |
| 79 | +| --- | --- | --- | --- | |
| 80 | +| `timeout` | `ReceiveFailedView` | "Payment timed out. Please try again." | Try Again (same amount) or Change Amount | |
| 81 | +| `nfc_error` | `ReceiveFailedView` | "NFC connection was lost." | Try Again (same amount) or Change Amount | |
| 82 | +| `trustline_missing` | `ReceiveFailedView` | "USDC trustline not found. Set up your USDC account first." | Try Again (same amount) or Change Amount | |
| 83 | +| *(anything else)* | `ReceiveFailedView` | "Payment failed. Please try again." | Try Again (same amount) or Change Amount | |
| 84 | + |
| 85 | +"Try Again" preserves the previously entered amount/asset by forwarding them as |
| 86 | +route params back to `ReceiveHomeView`; "Change Amount" resets fully and |
| 87 | +returns to a blank form. |
| 88 | + |
| 89 | +## 5. Analytics event mapping |
| 90 | + |
| 91 | +All receive events live in `AnalyticsEvents` (`src/constants/analytics-events.ts`) |
| 92 | +and carry only sanitized properties — **no public keys, no raw amounts, no |
| 93 | +PII**. Amounts are bucketed via `amount_bucket`: `'<1' | '1-10' | '10-100' | '>100'`. |
| 94 | + |
| 95 | +| State transition | Event | Sanitized properties | |
| 96 | +| --- | --- | --- | |
| 97 | +| `idle` → `preparing` (`prepare()` called) | `RECEIVE_STARTED` | `amount_bucket`, `asset` | |
| 98 | +| `preparing` → `broadcasting` (`startBroadcast()`) | `RECEIVE_BROADCAST` | `amount_bucket`, `asset` | |
| 99 | +| `broadcasting` → `waiting` (NFC write succeeded) | `RECEIVE_WAITING` | `amount_bucket`, `asset` | |
| 100 | +| `waiting`/`broadcasting` → `success` (`confirmSuccess()`) | `RECEIVE_COMPLETED` | `amount_bucket`, `asset` | |
| 101 | +| any → `failed` (`confirmFailure(reason)`) | `RECEIVE_FAILED` | `amount_bucket`, `asset`, `reason` | |
| 102 | +| any → `cancelled` (`cancel()`) | `RECEIVE_CANCELLED` | `amount_bucket`, `asset` | |
| 103 | + |
| 104 | +## 6. Related docs |
| 105 | + |
| 106 | +- [NFC library ADR](adr-nfc-library.md) — payload size/encoding constraints the |
| 107 | + payment request payload must respect. |
| 108 | +- [Product flows & system definition](ding-payments.md) — payment payload |
| 109 | + structure this flow builds on. |
| 110 | +- [Client MVP build plan](build-plan-client-mvp.md) — where C12 sits in the |
| 111 | + overall build. |
0 commit comments