# 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.