Transfer flow
The core user journey in Senco: sending money or crypto across borders. This pattern combines amount input, method comparison, recipient selection, and cost breakdown into a linear 3-step flow.
Everything else in the design system exists to make this flow work. It is the only irreversible action in the product, the one place where a mistake costs the user money, and the screen most of them will see on a mid-range phone with two bars of signal.
Flow structure
Send then Review then Confirm — a linear 3-step progression.
Each step validates before progressing. Users can go back without losing data.
Why three steps
Three is the smallest number that separates the three decisions a transfer requires: how much, to whom and by which rail, and are you sure. Collapsing review into confirm removes the user's last chance to check the fee against the total. Adding a fourth step means the flow has taken on something that belongs elsewhere.
| Step | The question | What can still change |
|---|---|---|
| 1 · Send | How much, and by which route? | Everything |
| 2 · Review | Is this what I meant? | Everything, by going back |
| 3 · Confirm | Am I sure? | Nothing after submit |
State persistence
- Data survives backward navigation, app backgrounding, and connection loss.
- Returning to an abandoned transfer restores the step and the values: "You have an unfinished transfer to Maria Santos."
- Only the user advances the flow. Nothing auto-progresses, including a successful rate refresh.
- After submission the flow is replaced by the status screen, not layered over it. Leaving the form reachable behind a result is how the same transfer gets sent twice.
Where the rate is locked
The rate is quoted at step 1 and held through the flow. If it expires or moves materially before confirmation, step 3 re-quotes and states the change explicitly rather than charging a different rate silently. A user who reviewed 412,500 NGN and received 408,000 NGN has been given a number that turned out to be decorative.
Transfer comparison
Users choose between bank transfer (fiat FX) and crypto transfer (stablecoin). The comparison card shows:
- Exchange rate or conversion rate
- Service fee
- Delivery speed
- Total received amount
- "Cheaper" / "Faster" badges on the better option
Always display both options side by side so users can make an informed choice.
Comparison, not configuration
Presented as a choice tree, this step asks the user to understand settlement rails before they can send money. Presented as a comparison, it shows the two numbers that actually decide it — total received and arrival time — and the user picks a row. Same capability, no expertise required. See Design principles.
The badges are the shortcut
"Cheapest" and "Fastest" let a user decide without reading either card in full, which is what most people do. Two rules keep them honest:
- A badge is computed, never assigned. If bank transfer is both cheaper and faster, it carries both badges and crypto carries none.
- A badge is part of the option's accessible name, so a screen reader user gets the same shortcut.
Neither option is pre-selected. Pre-selecting a fee-bearing route is a decision made on the user's behalf.
Accessibility
Built on radio card semantics — one Tab stop, arrow keys move and select. Selection is signalled by border weight, background tint, and indicator fill together; under a color vision deficiency the tint is nearly invisible and the weight is what survives.
Cost breakdown
Before confirmation, show the full fee breakdown:
- Amount sent
- Exchange rate or conversion
- FX markup (bank only)
- Service fee
- Delivery time
- Total received (highlighted)
- Savings comparison vs the other method
No hidden fees. No asterisks. The total cost is always visible.
The breakdown is not behind a disclosure
The fee is visible at review, not inside a collapsed Details block. Details may hold the composition of a fee already shown as a total; it may never hold the existence of a cost.
This costs conversion. Some users who see the fee will stop — and a user who would not have sent had they seen the fee should not be sending. That trade is stated in Design principles and this is the screen where it is paid.
FX markup is a line item
The exchange margin is listed separately from the service fee. A single combined "fee" number hides the part most competitors hide, and the entire reason a user is comparing two rails is to see it.
Accessibility
- Rendered as a description list so each label pairs with its value, and announced as structured data rather than two unrelated columns.
- Amounts use
tabular-numsand every one carries its currency code. - The total is bold and larger, not a different color — the emphasis survives greyscale.
Recipient selection
- Surface recent/saved recipients first (smart default)
- Search by name
- Show country, payment method, and currency tag
- Support bank account, mobile money, and wallet address
Selecting a recipient selects a rail
The same person can be a recipient by bank transfer and by mobile money, with different fees and arrival times. The contact card shows the method, because choosing "Maria" without choosing how is not a complete choice.
Verification before commitment
A recipient's account details are displayed for confirmation at review — masked except the last four digits, with the account name as the bank holds it. Name-mismatch is the most common cause of a rejected cross-border payment, and it is cheap to catch before submission and expensive afterwards.
Accessibility
- Each contact card is a
<button>whose accessible name covers everything visible: "Maria Santos, Nigeria, bank transfer." - Search matches name, country, and account fragment; the result count is announced politely.
- The country flag is decorative — the country is already in the name.
Target rate alert
Optional feature allowing users to set a target exchange rate and be notified when the rate is reached. Available on Step 1 (amount entry) for eligible currency pairs.
Eligibility
- Only shown when the currency pair has a variable exchange rate
- Hidden for pegged currencies (e.g., USDT → USD) or same-currency transfers
- Auto-detected: if no variable-rate methods exist for the pair, the toggle is hidden
Flow
- User enters send amount and currency
- "Wait for a better rate?" toggle appears (Switch,
lgsize) - When enabled:
- Send method selector appears (only variable-rate methods)
- Current rate is displayed as reference
- Target rate input: user enters desired rate (must exceed current rate)
- Expiry picker: "24 hours" or "7 days"
- Validation: target rate must be higher than current rate; empty or invalid values disable the submit button
- On submit: redirects to home with success toast confirmation
Interaction with send flow
| Toggle state | Method selector | Rate alert fields | Submit button |
|---|---|---|---|
| OFF | Visible (standard) | Hidden | "Continue to review" |
| ON | Hidden (replaced by alert method) | Visible | "Set rate alert" |
Validation rules
| Rule | Error message |
|---|---|
| Empty target rate | "Enter a target rate" |
| Target ≤ current rate | "Target must be higher than current rate" |
| No method selected | Button disabled (no error shown) |
A rate alert is not a transfer
Enabling the toggle changes what the primary button does — from "Continue to review" to "Set rate alert" — and no money moves. The button label carries that difference, which is why it changes rather than staying "Continue". An expiring alert notifies rather than executing; Senco never sends money on a schedule the user did not confirm in the moment.
Design and accessibility
- Toggle uses
lgSwitch (48×28px) for prominent visibility - Current rate displayed as read-only reference text
- Target input uses standard Input with
inputMode="decimal" - Expiry buttons are a two-option pill selector (not radio cards — too heavy for 2 options)
- Success toast uses DS Sonner success style (green border, check icon)
- Toggling the switch announces the change in available fields, since the form's shape changes underneath the user
- The "no method selected" case disables the button without an error, which is a documented deviation from the forms rule that submit stays enabled — it is listed in the gaps below
Design and accessibility
- Transfer comparison cards are keyboard-navigable (arrow keys)
- Selected method has visible border + checkmark (not color alone)
- Cost breakdown is announced to screen readers as a data table
- Amount input supports both typed and pasted values
- Currency selector is accessible via keyboard
- Target rate toggle uses
lgSwitch with 48×28px touch target - Rate alert form fields validate inline with standard error patterns
Failure paths
The flow has to behave correctly at its worst moment, not its best:
| Failure | Behavior |
|---|---|
| Connection lost mid-flow | Data persists locally; the step is restored on return |
| Submission cannot reach the server | Queued, with "We'll send this when you're back online" — never silently dropped |
| Server rejects the transfer | Error names the cause and states that no money moved and the balance is unchanged |
| Rate expired before confirm | Re-quote, state the change, require re-confirmation |
| Double tap on confirm | The flow sets the confirm button's loading on the first press, before the request starts; the button ignores further presses until the flow clears it |
| Submitted but outcome unknown | Status screen shows Pending with a reference number — never a silent failure |
The "outcome unknown" row is the one that matters most. A user whose confirm request timed out does not know whether their money moved, and the flow must resolve to a reference number they can quote rather than to an error that implies nothing happened.
Known gaps
- Local persistence and offline queueing are specified across this page and Forms but not implemented as a shared mechanism.
- The disabled-submit case in the rate alert contradicts the forms rule that submit stays enabled with an explanatory error.
- No documented recipient verification step for wallet addresses, where a mistyped character is unrecoverable.