Add saved payout destination UX and service contracts (#54)
This commit is contained in:
1 parent
a5689876ff
commit
24af794112
49 files changed
+4687
-62
No files matched your search
@@ -968,3 +968,31 @@ remain separate capabilities. Unrelated notifications runtime changes are exclud
|
||||
Delivered in [PR #51](https://git.sendico.io/tech/sxchnge/pulls/51), targeting
|
||||
`identity-verification-48`. Merge order is #47 → #49 → #51; deploy Sendico #2897
|
||||
before SXCHNGE #51. Both implementation branches are pushed and remain unmerged.
|
||||
|
||||
## Saved payout destinations UX — #54
|
||||
|
||||
Branch `payout-destinations-54`, based on `local-stand-52`. Implements Settings
|
||||
creation, details, rename, replacement and removal; explicit payout selection;
|
||||
optional saving and one-time preparation for crypto, cards and Russian personal
|
||||
accounts. Shared typed service/model/provider layers separate destination identity,
|
||||
metadata/execution revisions, ownership, provider bindings, contextual eligibility,
|
||||
save outcomes and financial outcomes. The customer-scoped demo replays stable
|
||||
created/duplicate results, preserves immutable quote/history snapshots, rechecks at
|
||||
acceptance, and demonstrates card-binding restoration without creating a second
|
||||
card. Bank directory/paste handling follows the reviewed Sendico interface.
|
||||
|
||||
EN/RU and 320 px/1.6× layouts are tested. Formatting (44 changed Dart files),
|
||||
analysis of app/wallet/analytics, `make test-frontend` (including 89 app tests), and
|
||||
`flutter build web` with Wasm dry run passed. Browser smoke inspection confirmed
|
||||
bank form rendering, labeled controls and keyboard focus. Full assistive-technology
|
||||
and real-provider audits were not performed. No security scans were requested/run.
|
||||
|
||||
Persistence and bindings in this delivery are simulated and reset on reload.
|
||||
Authenticated live mode retains explicit use of its provisioned payout destination;
|
||||
new types fail closed until the backend supplies the documented capabilities and
|
||||
registry/atomic-acceptance contracts. No routing, ownership-verification system,
|
||||
provider integration or financial engine was added. See
|
||||
[the specification and integration boundary](ux/payout-destinations.md).
|
||||
|
||||
Stacked merge order: #47 → #49 → #51 → #53 → this contribution. The unrelated
|
||||
notifications runtime edit is preserved outside task commits. Delivery links follow.
|
||||
@@ -0,0 +1,252 @@
|
||||
# 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.
|
||||
Reference in new issue
Block a user