257 lines
15 KiB
Markdown
257 lines
15 KiB
Markdown
# Saved payout destinations
|
||
|
||
Task: tech/sxchnge#54. Branch: `payout-destinations-54`.
|
||
|
||
## Delivery and integration boundary
|
||
|
||
The Flutter app implements Settings management and payout selection/new/one-time
|
||
flows with the injectable `DestinationService` in `sxchnge_wallet`. The included
|
||
customer-scoped **demo** service is process-local: reload discards its saved
|
||
records, attempts, directory fixtures and audit. No real card data may be entered.
|
||
The existing live wallet remains connected to provisioned Sendico destinations,
|
||
which now require explicit selection. Its existing financial adapter is unchanged.
|
||
|
||
This is UX and supporting contract implementation, not a production destination
|
||
registry or new payment-provider integration. Live mode never falls back to a
|
||
simulated destination store. It exposes no new destination creation capabilities
|
||
until the BFF implements the contract below. `BackendWalletService` explicitly
|
||
rejects new typed inputs rather than dropping them or using a different destination.
|
||
Actual persistent storage, public Sendico capability/directory adapters, secure
|
||
card collection, binding recovery and atomic financial acceptance are integration
|
||
requirements, not claims established by client tests. The injected service boundary
|
||
lets a production implementation replace the demo without DTO/networking in widgets.
|
||
|
||
## Screen annotations
|
||
|
||
The four primary destinations remain Accounts, Activity, Limits and Settings.
|
||
|
||
```text
|
||
Settings → Payout methods
|
||
Add payout method [primary action]
|
||
Search saved methods [nickname and safe display metadata]
|
||
Crypto addresses [type group; recently used first]
|
||
nickname / asset · network · shortened address
|
||
availability / required action [text, not color alone]
|
||
Bank cards
|
||
nickname / brand · masked suffix
|
||
Russian bank accounts
|
||
nickname / bank · masked suffix
|
||
```
|
||
|
||
```text
|
||
Add payout method
|
||
Type [explicit selection]
|
||
Type-specific destination fields [labels and inline validation]
|
||
Save for future payouts [payout only; unchecked]
|
||
Name for this method (optional) [Settings or saving enabled]
|
||
Validation / eligibility / saving [separate feedback]
|
||
Save payout method | Continue [no ordinary confirmation dialog]
|
||
```
|
||
|
||
```text
|
||
Pay out
|
||
Source account / available funds
|
||
Payout destination [explicit selection every time]
|
||
Saved methods | New destination
|
||
Amount [preserved during navigation/recovery]
|
||
Review
|
||
full destination, source debit, fee, receiving amount/asset
|
||
network and memo/tag for crypto; card remains masked
|
||
Pay out [amount/currency] [explicit consequential confirmation]
|
||
Processing → confirmed settlement [existing separate wallet states]
|
||
```
|
||
|
||
Settings: select type → details → Save. Payout: new destination → details → optional
|
||
saving → Continue. Reuse: select saved method → amount → review → confirm. No
|
||
mandatory nickname, routine ownership warning, automatic selection/substitution,
|
||
or extra verification screen is added. Removal has a consequential confirmation.
|
||
A replacement creates a new record; the original remains until explicitly removed.
|
||
|
||
Lists group by destination type, sort recent use within groups and search safe
|
||
labels. Empty category, search no-match, unavailable data and no eligible methods
|
||
are distinct. Opening creation/management uses transient Navigator state, preserving
|
||
payout input without destination details in URLs or serialized browser history.
|
||
|
||
## Domain and service contract
|
||
|
||
`DestinationInput` is `SavedReference(id, executionRevision)` or
|
||
`OneTimeDestination(details)`. Validation and contextual eligibility apply equally
|
||
to both; quoting/submitting a one-time input must not create a reusable record.
|
||
|
||
A destination has a customer-owned opaque ID independent of provider bindings.
|
||
Ownership declaration, service verification and payout eligibility are separate.
|
||
Unverified details may be saved when saving policy permits. Ownership status is
|
||
shown only when required/actionable. Format checks never establish ownership or
|
||
receiving capability, and successful persistence never establishes payout eligibility.
|
||
|
||
`metadataRevision` protects nickname updates; `executionRevision` protects identity,
|
||
lifecycle and binding changes. Rename cannot invalidate a valid execution quote.
|
||
Removal and binding changes require execution revalidation. Immutable identities
|
||
are changed by replacement. Last-use ordering is not an execution change.
|
||
|
||
Required service operations: capabilities; list/get; typed field validation;
|
||
contextual assessment; create; creation-attempt lookup; duplicate resolution;
|
||
rename with expected metadata revision; replacement; removal with expected
|
||
revisions; bank lookup with cancellation; immutable quote preparation. Remediation
|
||
capabilities must come from the service, never provider-specific widget logic.
|
||
|
||
Eligibility uses current capabilities, available routes, destination requirements,
|
||
ownership/security policy, bindings and payout context including amount when relevant.
|
||
States are eligible, temporarily unavailable, requires action and unsupported.
|
||
Settings retains active unavailable methods. A contextless Settings assessment is
|
||
informational and never proof of eligibility for a particular payout. Selection,
|
||
quote preparation and acceptance revalidate independently. Losing eligibility or
|
||
removal preserves inputs and requires explicit reselection; never substitute.
|
||
|
||
On Continue: validate fields; assess contextual eligibility; optionally persist
|
||
under saving policy; proceed only if eligible. Persistence may succeed while the
|
||
payout is ineligible. Definitive optional save failure allows retry or explicit
|
||
one-time continuation. Unknown outcomes offer status lookup; eligible one-time
|
||
continuation does not cancel/repeat a potentially successful save. Confirmation of
|
||
saving, validation, submission and settlement are separate states.
|
||
|
||
### Canonical identities and duplicate outcomes
|
||
|
||
- Crypto: service-defined asset ID (including token-contract identity), canonical
|
||
network ID, network-normalized address and applicable memo/tag. Identical address
|
||
strings across networks/assets are distinct. Preserve presentation case and
|
||
meaningful memo contents. Display enough metadata to distinguish same-symbol tokens.
|
||
- Russian bank: service-normalized BIK, account number and holder identity. No
|
||
frontend fuzzy matching or inference from an account prefix.
|
||
- Card: reliable service/provider fingerprint where available, never brand/last-four,
|
||
expiry or capture token. Provider-scoped fingerprints are not globally portable.
|
||
Missing reliable fingerprint permits distinct records; never guess/merge.
|
||
|
||
Creation keys identify immutable payloads. Replay returns the same **created** or
|
||
**duplicate** result even after the referenced record is renamed/removed. Attempt
|
||
lookup distinguishes these outcomes. Concurrent creation must serialize duplicate
|
||
resolution within the authenticated customer scope. Use existing method revalidates
|
||
that record and never creates another. Existing nicknames remain unchanged.
|
||
|
||
The demo uses a synchronous customer-scoped mutation boundary; production needs
|
||
persistent unique identity/idempotency constraints with transactions or equivalent.
|
||
No duplicate endpoint may reveal another customer's destination or attempt.
|
||
|
||
### Bank input adapted from Sendico
|
||
|
||
Reviewed local Sendico checkout `c5a3440fa`, especially
|
||
`docs/russian-bank-methods.md` and its form, lookup, parsing and validation tests.
|
||
The form has Account holder, Bank details and optional nickname sections. Structured
|
||
first/last names and optional patronymic preserve compound names; arbitrary full
|
||
names are shown unsplit. Business/tax selectors are excluded from personal-account UX.
|
||
Bank search is debounced 350 ms from two characters, with cancellation and stale
|
||
response rejection. Results identify name, BIK and city. Selected bank is compact,
|
||
with Change bank and expandable correspondent details. Account number accepts
|
||
whitespace and preserves leading zeros; 20-digit/control-key checking is not ownership
|
||
verification. Prefix hints remain advisory.
|
||
|
||
Local paste import recognizes explicit EN/RU labels, previews recognized values,
|
||
flags conflicts, preserves untouched fields and never persists/transmits raw pasted
|
||
text. Directory/name/correspondent mismatches remain visible. Invalid BIK, unknown
|
||
bank, unsupported institution and service failure are distinct. On reuse, cosmetic
|
||
directory labels may refresh; execution-material changes require review/requote.
|
||
Never silently replace BIK, account or holder. Mergers/BIK changes can require a
|
||
replacement; discontinued banks remain saved but ineligible.
|
||
|
||
### Cards and binding lifecycle
|
||
|
||
Card saving requires reusable support or a defined restoration mechanism. An existing
|
||
card remains saved after card/binding expiry, revocation or provider outage.
|
||
Requires action means a supported recovery exists; operational outages are temporary;
|
||
without a reusable binding or supported recovery the UI must not promise reuse.
|
||
Bindings may be multiple, provider-scoped and nonportable. Re-tokenization requires
|
||
trusted same-card identification; otherwise create a replacement. Secure capture is
|
||
transient. Saved card details contain local identity/display metadata, and bindings
|
||
contain separate opaque references; PAN/CVV never enter application storage.
|
||
|
||
## Acceptance, security and retention
|
||
|
||
The BFF's financial acceptance boundary must atomically validate current quote,
|
||
customer authorization, execution revision, lifecycle, eligibility and requirements,
|
||
then persist the accepted immutable snapshot/intent. A preceding API assessment or
|
||
frontend check cannot guarantee financial correctness. Removal committed first rejects
|
||
acceptance; acceptance committed first survives later removal. Rejection preserves
|
||
drafts and returns reselect/review/refresh/requote actions. Expired or materially changed
|
||
quotes need explicit confirmation of recalculated terms. This is a required backend
|
||
contract; the demo's synchronous acceptance guard only demonstrates its semantics.
|
||
|
||
Removal from the active list, binding deactivation and data erasure are separate.
|
||
Removed records cannot serve new payouts; accepted payouts retain immutable snapshots.
|
||
Production retention must assign explicit expiry/cleanup policies to abandoned quotes,
|
||
capture references, idempotency records and required financial history. Do not promise
|
||
complete erasure of retained financial records. The demo stores no browser data and
|
||
expires transient quote leases; all remaining simulation data ends on reload.
|
||
|
||
Authorize every operation server-side and audit actor, timestamp, opaque method/attempt,
|
||
action and outcome, without destination details. Metadata edits use optimistic concurrency.
|
||
Risk-based step-up is service-policy driven; no mandatory MFA is introduced. No raw
|
||
destination, pasted text, nickname, amount or account data enters logs, analytics or replay.
|
||
|
||
## Localization, accessibility and measurement
|
||
|
||
EN/RU copy is maintained in the app ARB sources. Specific states cover saving,
|
||
uncertainty, duplicate reuse, changed/removed methods, directory failures, binding
|
||
authorization and quote recovery. Ordinary success says only "Payout method saved."
|
||
Forms use associated labels, field errors, a focusable live feedback area, text status
|
||
and keyboard controls. Names stack below 480 px in a 720 px maximum form. Review
|
||
320 px layouts at 1.6× text scale, long labels, full-address wrapping and 48 px targets.
|
||
|
||
`JourneyKind.destination` / `destinationSave` measures service-confirmed saving/resolution
|
||
separately from the existing payout quote/submission/settlement signals. Retries share
|
||
recorder correlation; confirmed save closes its journey. Idle/discard attempts use the
|
||
shared 30-minute inactivity/disposal rule. Simulation events are explicitly marked.
|
||
No production persistence, cross-session funnel or financial outcome is inferred from
|
||
the demo recorder. Actual outcome events and backend audit are integration requirements.
|
||
|
||
## Validation scenarios and recorded review
|
||
|
||
Tests exercise one-time nonpersistence, idempotent/repeated/concurrent creation, stable
|
||
duplicate replay after rename/removal, lost-response reconciliation, customer isolation,
|
||
metadata concurrency, execution invalidation at acceptance, asset/network/token identity,
|
||
case/memo handling, fingerprint fallback, card binding recovery states, bank control keys,
|
||
directory changes, paste conflicts and DTO mappings. Widget tests exercise Settings
|
||
save/rename/remove, explicit selection, one-time payout, optional-save failure recovery,
|
||
removal during review, privacy-safe outcomes and EN/RU narrow layouts. Existing auth,
|
||
factor, wallet and workspace regressions are included in the required frontend suite.
|
||
|
||
Research: [Wise recipient management](https://wise.com/help/articles/2491526/how-do-i-add-or-delete-recipients),
|
||
[Coinbase address books](https://help.coinbase.com/en/exchange/managing-my-account/how-does-the-address-book-work),
|
||
[Coinbase memo/tag requirements](https://help.coinbase.com/en/coinbase/trading-and-funding/sending-or-receiving-cryptocurrency/destination-tag-memo-faq),
|
||
[Stripe tokenization](https://docs.stripe.com/api/tokens),
|
||
[W3C form validation](https://www.w3.org/WAI/tutorials/forms/validation/).
|
||
|
||
Final validation commands and PR links are recorded below at delivery.
|
||
|
||
### Completion review — 2026-10-16
|
||
|
||
- Applied `review-frontend-change`, UX Design and UX Observability reviews across
|
||
app, wallet and analytics. Extracted form/search/selection/management providers;
|
||
widgets consume typed models and provider actions. Fixed narrow bank-header
|
||
overflow and bottom feedback overlapping management controls.
|
||
- Added structured immutable destination review, explicit simulated receiving
|
||
asset/amount/timing terms, independent card-binding restoration and a typed
|
||
created/duplicate analytics outcome. Nickname edits do not change execution
|
||
revisions. New typed inputs are explicitly rejected by the legacy live adapter.
|
||
- `dart format --output=none --set-exit-if-changed` over 44 changed Dart files:
|
||
passed, no changes. `git diff --check`: passed.
|
||
- `flutter analyze` in `frontend/app`, `frontend/packages/sxchnge_wallet` and
|
||
`frontend/packages/sxchnge_analytics`: passed, no issues.
|
||
- `make test-frontend`: passed all seven package/application suites, including
|
||
89 application tests and the destination domain/recovery/concurrency scenarios.
|
||
- `flutter build web` in `frontend/app`: passed, including the Wasm dry run.
|
||
- Browser smoke check on a temporary local web preview: method selection and bank
|
||
form rendered; accessibility labels exposed; Tab moved from type to First name.
|
||
Automated EN/RU forms at 320 px and 1.6× scale passed. A full assistive-technology
|
||
audit and production/provider validation were not performed.
|
||
- An exploratory repository-root `flutter analyze` also inspected the unrelated
|
||
standalone `website/flutter/brand_palette.dart`, which lacks Flutter package
|
||
context and reported `dart:ui` errors. The required affected-package analyses
|
||
above passed; no unrelated website code or analyzer settings were changed.
|
||
- No backend code or infrastructure changes, live payments, deployment, merge or
|
||
security scanner runs were performed. Live persistence/atomic acceptance remain
|
||
the explicitly documented integration boundary.
|
||
|
||
Delivery: [task #54](https://git.sendico.io/tech/sxchnge/issues/54),
|
||
[PR #55](https://git.sendico.io/tech/sxchnge/pulls/55), implementation commit
|
||
`24af794`. PR base is `local-stand-52`; merge after #47 → #49 → #51 → #53.
|