# Exchange workspace — UX plan Task [#24](https://git.sendico.io/tech/sxchnge/issues/24). Approved plan; implementation tracked in task #28. ## Product direction Make it easy to fund a currency account, exchange into another currency, and pay out. Accounts answer “what can I use?”, Activity answers “what happened?”, Limits answers “what can I do next?”, and Settings owns account preferences. A new user should see a clear first step: **Top up an account**. A funded user should be able to exchange or pay out from that account without navigating through settings or a generic dashboard. Unverified is a valid state with basic limits. KYC is offered when an upgrade is useful, not as a forced onboarding detour. Use the existing SXCHNGE theme, rounded surfaces, typography and semantic colors. Keep decoration secondary to balances, amounts, recipients and transaction terms. ## Navigation and screen hierarchy | Destination | What it contains | Main next action | |---|---|---| | Accounts (default) | Currency accounts, available balances, recent activity, actionable pending items | Top up, exchange, pay out | | Currency account | Available balance, relevant activity and applicable limits | Act on this currency | | Activity | All operations, filters, search, processing items | Inspect an operation or resolve an actionable issue | | Limits | Current caps, used and available, period/reset information and upgrade benefits | Verify identity when useful | | Settings | Profile, language, security, notifications and help | Manage the account | Desktop: a persistent sidebar with **Accounts · Activity · Limits · Settings**. Mobile: the same four destinations in bottom navigation. Highlight the active section. Identity verification is reached from Limits and Settings; it does not need a separate primary navigation item. Top up, exchange and pay out are actions, not three competing dashboard tabs. On Accounts and currency-detail screens, put them beside the selected account on wide screens and in a clearly labeled action area beneath its balance on mobile. The transaction flow uses a focused page with Back and Close, a visible source account and a review step. Avoid full-screen wizard chrome on simple settings. Proposed routes, to be finalized during implementation: | Route | Purpose | |---|---| | `/accounts` | Overview | | `/accounts/:accountId` | Currency account detail | | `/top-up` | Account → method → instructions/payment → operation | | `/exchange` | Source/destination/amount → quote review → operation | | `/payout` | Account/method/recipient/amount → review → operation | | `/activity` | Operation list | | `/activity/:operationId` | Canonical operation detail | | `/limits` | Usage, constraints and KYC upgrade | | `/identity` | Existing identity verification flow | | `/settings` | Account settings | | `/settings/preferences` | Language and other supported preferences | | `/settings/security` | Supported security actions | Use opaque IDs in paths. Never put recipient details, document data, amounts, verification tokens or sensitive filter text into route parameters/history. URLs identify screens; providers hold transient input. Deep links to operation status must reload authorized service data, not depend on a previous screen. ## Accounts overview The first viewport should show account balances and useful actions, rather than charts or promotional blocks. ```text ACCOUNTS Account menu Your accounts [Add currency account, if supported] [ EUR · available balance ] [ USD · available balance ] … Selected account: EUR Available: … [Top up] [Exchange] [Pay out] Action needed / in progress (only when there is something useful to show) Recent activity [See all] ``` - Each card shows currency code/name, available amount and a text status when restricted. Do not rely on flags to identify currencies. - Show only available balances. Held/reserved/booked amounts are internal service details, not customer-facing concepts. Backend balances remain authoritative. - Do not sum different currencies directly. A future total in a reporting currency must be labeled estimated, with valuation currency/time and a service-provided conversion. It is not needed for the first interface. - Zero balance: explain that the account is ready and make **Top up** primary. Funded account: make **Exchange** primary, with Top up and Pay out visible. Never rearrange actions while the user is interacting with them. - No accounts: offer account creation only if the backend supports it. Otherwise explain provisioning status and provide Refresh/Help; do not invent accounts. - Partial failure: keep successfully loaded accounts usable. Mark stale balances, offer refresh, and obtain fresh eligibility/quote data before execution. - Context travels into the flow: starting from EUR preselects EUR. Account switching invalidates any quote and refreshes methods and eligibility. ## Top-up journey **Outcome: funds are credited and become available**, not merely instructions shown or a payment button pressed. 1. Choose the receiving account. If there is one eligible account, preselect it. 2. Choose from service-provided methods. Show fees, minimum/maximum, expected timing when supplied, and whether an external step is required. Do not advertise rails that are not actually available. 3. Collect only the fields required by the selected method. Amount is required only when that method needs it. Validate currency precision and current bounds. 4. Review what the user pays, fees, and what is expected to be credited. For bank instructions, show exact destination/reference from the service with labeled copy actions and “Copied” feedback. For redirects, explain the next step before opening the provider. Never collect card details in generic custom fields. 5. Create/continue the operation and show its canonical status. “Awaiting payment” or “Processing” remains pending until the service confirms credit. 6. On credit, refresh balances, usage and activity. Offer **Exchange this balance** with the funded currency preselected, plus View account. Returned from a provider does not mean paid. Reconcile using the operation ID. Handle abandoned redirects, payment decline, expired instructions, wrong/partial amount, delayed credit and service failure with method-specific service responses. Do not invent a completion SLA. Never expose a “Mark paid” control as proof of funds. ## Exchange journey **Outcome: the exchange is executed and both currency accounts reflect the result.** 1. One composition screen: source account, destination currency/account, “You send” and “You receive.” Clearly mark which amount is editable; editing the other side requests a new quote if the backend supports receive-exact quotations. 2. Show available source balance and eligibility near the input. Currency swap exchanges the selection and invalidates the prior quote. Unsupported pairs are explained before the review step. Offer Add account only if supported. 3. Quote response owns the rate, fees, total debit, net credit, quote ID and expiry. Show **1 source currency = … destination currency**, the actual debit and net receipt; never bury fees behind an unexplained estimate. 4. **Review exchange** summarizes both accounts, amounts, fees and rate. The final button says **Exchange [amount/currency]**. Disable final execution while the quote is missing, stale, expired or being refreshed, with a nearby reason. 5. If terms change, show the new quote and require confirmation again. Never silently execute a refreshed rate. Prevent double submission and reuse the idempotency key when recovering the same request. 6. Processing leads to operation detail. Only service-confirmed execution shows **Exchange complete**. Refresh both accounts, activity and limits. Offer **Pay out [destination currency]** and View account. “Max” must use a service-provided spendable amount inclusive of fees and applicable limits; do not assume the displayed balance can all be exchanged. If the amount is above the current permitted limit, show the actual blocker and an upgrade link only when KYC would help. Keep the entered amount when requesting a new quote. ## Payout journey **Outcome: the payout reaches a service-confirmed terminal result.** Distinguish “submitted,” “sent” and “received” when those are separate states in the contract. 1. Preselect the source currency account. Choose a supported payout method and an existing recipient, or enter a new destination. Saved recipients are optional future functionality; do not imply they exist without a service. 2. Method-specific fields and validation come from the service contract. Clearly identify the beneficiary/destination, bank or network when applicable. Do not guess which currencies support which destination types. 3. Enter amount; show fee, total debit, destination amount/currency, available funds, limits and expected timing supplied by the backend. 4. Review the recipient and full destination sufficiently to detect a mistake, alongside the amount and costs. The final button says **Pay out [amount/currency]**. Explain irreversibility where applicable; avoid a redundant confirmation dialog after an already clear review page. Perform step-up authentication when required by the backend, without losing the pending operation context. 5. Show processing/status with a reference. Cancellation is offered only when the service says the operation remains cancellable. If supported, confirm the cancellation and refresh status rather than optimistically removing the payout. 6. On confirmed success, show the outcome and account/activity links. A returned or reversed payout remains visible with its own explanation and linked reversal. Insufficient funds should offer **Top up this account** or **Exchange into this currency**, preserving the draft locally. After funding, revalidate recipient, fees, available balance and limits before allowing submission. Never auto-submit. A transfer destination must never be included in analytics, logs or URLs. ## Shared transaction states and recovery | State | User-facing behavior | |---|---| | Initial/draft | Clear primary action, no invented balances/rates | | Loading balance/methods/quote | Specific progress; keep unrelated navigation usable | | Validation issue | Inline field message and focus on first problem; preserve input | | Not eligible | Reason plus relevant next step: email, limits, account funding or support | | No supported method/pair | Explain unavailability; let user change the selection | | Quote expired | Keep input; request a new quote and require fresh confirmation | | Submission in progress | Prevent duplicate execution and show progress | | Connection lost after submit | “Checking the outcome”; retrieve by operation/idempotency ID, not a fresh submission | | Pending external/human processing | Status and next useful action; pending is not failure or abandonment | | Confirmed failure | Safe reason, support/reference where available; retry only when service permits | | Confirmed success | Authoritative receipt, refreshed balances/limits and useful next action | | Session expired | Reauthenticate and rehydrate operation status; do not retain sensitive fields in browser storage | Close/back preserves an in-memory draft for a clear session-scoped lifetime. Warn before discarding meaningful recipient/payment input. Quotes always expire independently of the draft. Hard reload can discard draft input; submitted operations must survive because they are reloaded from the backend. Logging out clears all account-scoped providers and drafts to prevent cross-account leakage. ## Activity and operation detail Activity is the common destination for the outcome of every transaction flow. Show type, source/destination currencies, meaningful amount(s), time, status and reference. Exchanges are one parent operation showing both debit and credit, not two unrelated rows that look like duplicate transactions. - Filters: all/top-ups/exchanges/payouts, status, currency/account and date range. Preserve filters when returning from a detail page. Provide Clear filters and distinguish “no operations yet” from “no matching results” and a load failure. - Account-detail activity is the same view scoped to that account. Paginate with stable service cursors; refresh without duplicating rows. - Detail includes status timeline, amount breakdown, fees, relevant destination, timestamps, available receipt/reference and service-permitted actions. - Pending items show what is known and whether the user needs to act. Avoid endless spinners or a fake percentage for external settlement. - **Try again** creates a new draft only after confirmed failure and backend permission; never replays a consequential request from a history button. ## Limits and identity Reuse the current KYC and usage work ([PR #21](https://git.sendico.io/tech/sxchnge/pulls/21), [PR #23](https://git.sendico.io/tech/sxchnge/pulls/23)). Limits must show used, reserved, available and caps for the relevant operation/currency/period. Include window/reset information only when supplied; do not imply a calendar reset for a rolling window. Preserve service ordering and applicability. The full page explains active tier and what verification would unlock. Transaction forms show the relevant constraint in context and link to that page. A KYC upgrade must not claim to resolve insufficient funds, an unsupported method or an unrelated restriction. Submission/pending review does not increase limits; refetch the actual backend projection after any decision. ## Account settings and language **Language belongs to the account.** Remove page-level language selectors from the main workspace and identity pages. Settings → Preferences contains English/Russian with one clear selected value. Change language without resetting drafts or filters. - Profile service owns `preferredLocale`; an account provider loads and updates it. Device storage is a cache/guest fallback, not the source of truth for a signed-in account. Use an injectable stub until profile endpoints are available. - On sign-in, the server preference takes precedence. Before account creation, use browser/device locale plus a remembered guest preference. If a guest needs a manual override, expose it once through the auth shell's settings/help entry, not on every login/verification page. - On save failure, clearly state “Not saved to your account,” retain the choice for the current session and offer Retry. Do not report a successful account update. - Server-provided human-readable labels must be fetched for the selected language; preserve stable IDs and in-progress inputs. Avoid mixed-language stale responses. - Security settings show only capabilities actually implemented (password change, sessions, MFA when supported). Do not present decorative toggles as protection. - Identity status is linked here but managed in the existing verification flow. Notification preferences require a real preference service; support has a genuine destination before production. No placeholder action should pretend to complete. ## Service and architecture boundaries Keep shared DTO → mapper → service → model → provider ownership for future mobile reuse. Widgets consume provider state; they do not compute financial outcomes. Proposed domains, not speculative API endpoints: | Domain service | Owns authoritative data | |---|---| | Accounts | Account IDs, currencies, booked/available/held balances, status | | Funding | Eligible methods, instructions, fees, top-up creation/status | | Exchange | Pair eligibility, quotes/expiry, idempotent execution | | Payout | Method/recipient validation, quotes, submission and permissible actions | | Operations | History, canonical status, references, receipts and reconciliation | | Limits | Applicable caps, committed/reserved usage, remaining and upgrade projection | | Profile | Account preferences including language | Use currency-aware decimal/minor-unit values with currency precision metadata; never floating-point money or a universal two-decimal assumption. Balance held amounts and limits reserved usage are different concepts, even when values match. Backend authorization and limit reservations remain necessary at execution time. A visible balance, valid quote or frontend KYC state cannot authorize a payment. For the demo, shared stub state must be coherent across screens: exchange affects both balances; pending payouts hold capacity; failures release simulated holds where appropriate; activity and limits reflect the same simulated operation. Never mix a simulated transaction with a live success claim. Avoid seeded history that contradicts displayed balances or usage without explaining the fixtures. ## Responsiveness, accessibility and trust - Mobile: one-column forms and review summaries; no horizontal table required to complete a task. Wide screens may place quote summary beside input. - Large values and long beneficiary names wrap safely. Use EN/RU, realistic currency precision and text scaling in design/test fixtures. - Monetary fields identify currency and precision; keyboard input accepts the active locale's decimal separator without silently altering value. Avoid formatting the value under the user's cursor on every keystroke. - Provide labeled 48px targets, visible keyboard focus and predictable focus after validation/navigation. Statuses have text/icons, never color alone. - Announce meaningful quote/state changes, not every countdown tick. Honor reduced motion. An expiry countdown accompanies a clear expired state. - Mask sensitive destinations in lists, reveal enough in explicit review/detail to verify them. No session replay/autocapture on money, recipient or identity forms. - Keep unrelated accounts/history accessible during a pending operation. Do not block the whole product behind an exchange spinner. ## Automatic currency accounts (task #72) New customers receive RUB, USDT and USDC accounts through backend-owned setup during signup. Existing customers receive missing defaults on their next wallet load. The UI requires no account-creation form or support contact. During initial loading it announces that the accounts are being prepared. Failure shows one clear load error and a **Try again** action that reruns the same idempotent setup. An unexpected empty snapshot has its own setup-failure fallback, rather than being shown simultaneously with an error. A failed refresh retains existing cards and the stale-data warning. Account-not-found copy requires loaded data. The existing DTO → mapper → service → model → provider → UI boundaries remain intact. Account creation belongs to the backend/provider, and widgets invoke the existing provider refresh action. English and Russian use wrapping text and the existing accessible button pattern; status text uses a live region. Dedicated widget regressions cover loading, failure, retry and successful default accounts on Accounts, Top up, Exchange and Payout at 320px with 1.6× text. Native device and screen-reader QA remain a release check; tests do not claim physical-device QA. Measurement: `sxchnge_currency_accounts_setup_total{outcome="ready|failed"}` and `sxchnge_currency_accounts_setup_duration_seconds` cover the server setup attempt. `ready` is emitted only after all default bindings are confirmed, including idempotent completion of provider wallet/route setup for newly bound ledgers. Each signup/resend/wallet load is a separate attempt; a retry is not a new unique customer conversion. The failure fraction uses all attempts as its denominator; the histogram measures one attempt, not time across multiple browser sessions. No customer, ledger, currency, balance, credentials or form input is a metric label. These server metrics do not measure client abandonment or prove that a balance card was viewed. Existing transaction journey outcomes below are reused; no speculative button-click events are added. Scope and rollout: [task #72](https://git.sendico.io/tech/sxchnge/issues/72), branch `wallet-72`, [task #72](https://git.sendico.io/tech/sxchnge/issues/72). The provider prerequisite and deployment order are documented in [payments](../payments.md). Merge and deployment remain separate. Final validation is recorded in the task and PR, including any transient failures and environmental blockers. ## Journey measurement plan Reuse the typed analytics abstraction and maintain a separate journey per active transaction draft. Random journey IDs correlate steps; opaque operation IDs link accepted requests to server outcomes. Retries reuse the journey and idempotency lineage. Never capture amount, balance, currency-account IDs, recipient, form input, URLs or raw exceptions in client UX events by default. | Journey | Start | Useful states | Actual success | |---|---|---|---| | Top up | User begins funding | Method ready, instructions/payment ready, accepted/pending, failure | Service-confirmed credit | | Exchange | User begins exchange | Valid quote, reviewed, accepted/pending, expired/failure | Service-confirmed execution | | Payout | User begins payout | Valid destination/quote, reviewed, accepted/pending, failure | Contract-defined confirmed payout outcome | Measure start-to-acceptance and acceptance-to-terminal conversion separately. Use one count per journey for draft conversion and one per operation for financial outcomes. Track review-to-submit drop-off, quote expiry, recoverable failure rate, time to acceptance and server time to terminal outcome. Classify failures with safe enums, such as insufficient funds, limits, expired quote, unavailable method, session and service error; emitted events must have a concrete diagnostic use. An explicit draft discard or 30 minutes of settled inactivity is draft abandonment. Changing tabs while preserving a draft is not immediate abandonment. Accepted operations leave the draft abandonment window and remain pending until authoritative status; app closure must not fabricate failure. Multi-session reconciliation and server-side outcome events are required before claiming production funnel coverage. Deduplicate by event/operation identity. Analytics errors must not delay execution. Account/language preference updates reuse profile outcome signals; do not add click events for every setting, card or navigation tab. Simulated events are always flagged and excluded from real transaction/verification conversion dashboards. ## Implementation sequence after approval 1. **Shared workspace foundation:** accounts/operations/profile contracts and coherent stubs, shell/routes, account overview/detail, Activity/detail, account language provider/settings. Move language selectors into settings. Preserve auth/email and existing identity/limits navigation. 2. **Top-up vertical slice:** method/instruction/review/status, balance refresh and an exchange handoff. Include cancellation, pending and failed outcomes. 3. **Exchange vertical slice:** input/quote/review, expiry, idempotent execution, coherent balances/history/usage and payout handoff. 4. **Payout vertical slice:** method/destination/review, step-up contract hooks, reconciliation and confirmed failure/return paths. 5. **Integrated review:** cross-flow drafts, limits/KYC return paths, language changes, responsive/keyboard/screen-reader checks, shared-service tests and semantic event assertions. Complete each slice's tests as it is built, not only at the end. Actual supported currencies/rails, account provisioning, fee/rate policies, recipient requirements, cancellation and support contracts must come from backend capabilities. Demo fixtures can illustrate them but must be labeled and isolated. Real execution is outside this UX-plan task. ## Plan acceptance and validation boundary The plan connects all requested areas and defines optional KYC, usage, language ownership, consequential review, state recovery, privacy and measurement. Before calling implementation complete, walk: new account → top up → exchange → payout → Activity; pending/unknown outcome recovery; exceeded limits → KYC → resume/requote; and locale change with a preserved draft. Test stale/out-of-order responses, repeated submit, expired quote, zero balances, no methods, denied KYC, and logout. This document is a design deliverable, not evidence that these screens exist or that accessibility/payment flows have been tested. Existing demo implementation and pending PRs remain unchanged by this task. ## Implementation simplification — user direction for #28 Only available balances are shown. Customer limits show used and available, with no held/reserved breakdown or explanatory jargon. Keep four navigation destinations and straightforward input → review → result flows. Demo funding/payout methods are explicitly simulated, not representations of live supported rails. ## Implemented demo scope — #28 Four primary destinations and three transaction journeys are implemented. The single demo method needs no method picker; top-up and exchange collect an amount, payout additionally collects sample destination text. Review shows fees, source debit, destination credit and rate. There are no live payment instructions, redirects, saved recipients, security controls or notification preferences. Settings implements account language and links to identity. Unsupported production capabilities remain future work, not decorative controls. Pending top-up: money has not reached available balance. Pending outgoing: total amount plus fee has left available balance, and processing continues. Completed exchange/top-up: destination balance is credited. Failed outgoing: amount plus fee is available again. These explanations appear on canonical operation detail. Pending snapshots refresh automatically across screens; refresh failures mark data potentially stale. The limits service independently supplies usage and remaining. The customer sees aggregate consumed capacity and available capacity, not internal reservations. Activity offers kind/status/currency filters and reference search; operation detail is deep-linkable while the demo session exists. Date filtering, pagination, receipts, persistent history, actual reset timestamps and production support await backend contracts. Transient drafts survive pushed settings/limits screens and locale changes; closing a draft requires discard confirmation. Browser reload resets the entire demo and is not durable recovery. Measurement uses existing random journey IDs and typed quote/submission/settlement steps. Only service-confirmed terminal status closes the transaction outcome. Accepted operations are excluded from draft-abandonment on app disposal. Actual server events, persistent reconciliation and cross-session conversion remain integration gaps. No automatic click tracking, replay or financial data collection is introduced.