15 KiB
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.
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
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]
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, Coinbase address books, Coinbase memo/tag requirements, Stripe tokenization, W3C form 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-changedover 44 changed Dart files: passed, no changes.git diff --check: passed.flutter analyzeinfrontend/app,frontend/packages/sxchnge_walletandfrontend/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 webinfrontend/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 analyzealso inspected the unrelated standalonewebsite/flutter/brand_palette.dart, which lacks Flutter package context and reporteddart:uierrors. 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.