ui: wallet hero card, node card and the P4/P5 verification sweep #199

Merged
sirius merged 5 commits from ui/refactor-wallet-node into main 2026-10-29 06:19:57 +00:00
Owner

Plan P4 + P5: the wallet/node screen, and the verification sweep for both halves of the plan.

Base is ui/refactor-shell-messenger (@dc45d9c), not main: PR #198 is unmerged and this card needs its ui/components/ (OwButton / OwSegmentedControl / OwAlertBanner / OwCard) and the new Theme.kt tokens. Merge order is parent first.

P4.2 hero wallet card

  • No wallet open: No Wallet Active + muted dot, the plan's subtitle, Create Wallet (primary) / Import Wallet (secondary) visible in the top card, Saved wallets in the card header.
  • Wallet open: balance at 28sp bold + sync-state pill, gated receive address + QR, Receive / Invoice (primary) / Send / Tip (secondary), Lock Wallet (destructive).
  • Every capability the alpha screen had is still reachable in both states. Creating a further wallet and importing one stay in the open-wallet card, and invoicing stays reachable with no wallet: the core resolves a payee address from the profile (no receive address — set profile xmr or ONIONWIRE_WALLET_RPC is the both-missing case), so hiding the action there would have removed a path that works. A tip needs Wallet::transfer, so it appears only with a wallet open.

P4.3 node card

  • Status row (dot + Syncing Node (Mainnet Pruned)) + percentage, 6dp track #1C2228/#2EE59D, and Block 485,253 of 3,794,383 in mono with 305.2 GiB free on the right.
  • The raw connecting · in-process mainnet pruned · node starting · sync 485253/3794383 string is gone from the surface. It is still the header's contentDescription — a screen reader is told no less than the eye.
  • Network mode: one OwSegmentedControl [Pruned | Full | Testnet], replacing the three stacked buttons.
  • Connection type: [Remote Node | On-Device Node]. Remote shows the host:port field, the one-line LAN caption, and compact Save Daemon / Receive-Only; On-Device shows the amber banner titled Experimental On-Device Node (Cuprate) with the #278 body.
  • Lifecycle: one context-aware control — Start Node & Wallet (primary) when stopped, Stop Node (destructive) when running. The permanently-disabled twin is gone.
  • All raw red/grey paragraphs (no binary, no wallet open, start blocked, core error) are OwAlertBanners.

Fail-closed rules, unchanged and now tested

target_height == 0 prints no percentage and no fraction — it is "the daemon reported no target", never "caught up", and it is what Cuprate reports at the tip. A height past the target clamps at 100.0%. An unrecognised state is never green (NodeStatusTone.Bad). The off / err sentinels are still returned by the core untouched; only their display changed (No Wallet Loaded / Balance unavailable).

P4.1 copy

The four replacements serve as written; WALLET_NOT_OPEN_TEXT now renders as an info banner (a state, not a crash) and ErrorText picks its variant from the text so the same string cannot be red on one screen and informational on the other. The two remaining user-facing mentions of the daemon process are reworded (the wallet is pointed at <file>, the wallet is not running), plus ProfileScreen's empty-address notice.

Behaviour

No change to WireViewModel, RPC calls, the seed gate, shouldReloadWalletReads, openWalletChild lifetime, or any predicate's branches. Only strings and layout moved.

Deliberate deviations, called out

  1. WalletScreen drops its onBack parameter. Wallet is a top-level tab after P2, so a back chevron there is wrong; Chats and Settings already have no back affordance. MainActivity's Wallet destination is the one-line seam update.
  2. OwSegmentedControl gains an optional testTags: List<String?> = emptyList(). Additive, defaulted, so no existing call site changes — a merged track otherwise has no per-value handle.
  3. CUPRATE_PREPRODUCTION_WARNING is replaced by CUPRATE_ON_DEVICE_TITLE + CUPRATE_ON_DEVICE_BODY (the plan's title and body, plus the mainnet-unvalidated / "do NOT use cuprated" sentences that the repo's honesty bounds require). One copy of the caveat, never two that can disagree.

Verification

  • :app:compileDebugKotlin (non-incremental, -Pkotlin.incremental=false, outputs wiped first): BUILD SUCCESSFUL, 0 w: lines.
  • :app:testDebugUnitTest --no-build-cache: 229 tests, 0 failures, 0 errors, 0 skipped, 16 classes. CI's android-ci.yml does not run Kotlin tests, so this local run is the only evidence.
  • No APK is claimed from this PR; android-ci runs the build on the merge.
Plan P4 + P5: the wallet/node screen, and the verification sweep for both halves of the plan. Base is `ui/refactor-shell-messenger` (@dc45d9c), not `main`: PR #198 is unmerged and this card needs its `ui/components/` (OwButton / OwSegmentedControl / OwAlertBanner / OwCard) and the new `Theme.kt` tokens. Merge order is parent first. ## P4.2 hero wallet card - No wallet open: `No Wallet Active` + muted dot, the plan's subtitle, `Create Wallet` (primary) / `Import Wallet` (secondary) visible in the top card, `Saved wallets` in the card header. - Wallet open: balance at 28sp bold + sync-state pill, gated receive address + QR, `Receive / Invoice` (primary) / `Send / Tip` (secondary), `Lock Wallet` (destructive). - Every capability the alpha screen had is still reachable in both states. Creating a further wallet and importing one stay in the open-wallet card, and invoicing stays reachable with no wallet: the core resolves a payee address from the profile (`no receive address — set profile xmr or ONIONWIRE_WALLET_RPC` is the both-missing case), so hiding the action there would have removed a path that works. A tip needs `Wallet::transfer`, so it appears only with a wallet open. ## P4.3 node card - Status row (dot + `Syncing Node (Mainnet Pruned)`) + percentage, 6dp track `#1C2228`/`#2EE59D`, and `Block 485,253 of 3,794,383` in mono with `305.2 GiB free` on the right. - The raw `connecting · in-process mainnet pruned · node starting · sync 485253/3794383` string is gone from the surface. It is still the header's `contentDescription` — a screen reader is told no less than the eye. - Network mode: one `OwSegmentedControl` `[Pruned | Full | Testnet]`, replacing the three stacked buttons. - Connection type: `[Remote Node | On-Device Node]`. Remote shows the `host:port` field, the one-line LAN caption, and compact `Save Daemon` / `Receive-Only`; On-Device shows the amber banner titled `Experimental On-Device Node (Cuprate)` with the #278 body. - Lifecycle: one context-aware control — `Start Node & Wallet` (primary) when stopped, `Stop Node` (destructive) when running. The permanently-disabled twin is gone. - All raw red/grey paragraphs (no binary, no wallet open, start blocked, core error) are `OwAlertBanner`s. ## Fail-closed rules, unchanged and now tested `target_height == 0` prints **no** percentage and no fraction — it is "the daemon reported no target", never "caught up", and it is what Cuprate reports at the tip. A height past the target clamps at `100.0%`. An unrecognised state is never green (`NodeStatusTone.Bad`). The `off` / `err` sentinels are still returned by the core untouched; only their display changed (`No Wallet Loaded` / `Balance unavailable`). ## P4.1 copy The four replacements serve as written; `WALLET_NOT_OPEN_TEXT` now renders as an **info** banner (a state, not a crash) and `ErrorText` picks its variant from the text so the same string cannot be red on one screen and informational on the other. The two remaining user-facing mentions of the daemon process are reworded (`the wallet is pointed at <file>`, `the wallet is not running`), plus ProfileScreen's empty-address notice. ## Behaviour No change to `WireViewModel`, RPC calls, the seed gate, `shouldReloadWalletReads`, `openWalletChild` lifetime, or any predicate's branches. Only strings and layout moved. ## Deliberate deviations, called out 1. `WalletScreen` drops its `onBack` parameter. Wallet is a top-level tab after P2, so a back chevron there is wrong; Chats and Settings already have no back affordance. `MainActivity`'s Wallet destination is the one-line seam update. 2. `OwSegmentedControl` gains an optional `testTags: List<String?> = emptyList()`. Additive, defaulted, so no existing call site changes — a merged track otherwise has no per-value handle. 3. `CUPRATE_PREPRODUCTION_WARNING` is replaced by `CUPRATE_ON_DEVICE_TITLE` + `CUPRATE_ON_DEVICE_BODY` (the plan's title and body, plus the mainnet-unvalidated / "do NOT use cuprated" sentences that the repo's honesty bounds require). One copy of the caveat, never two that can disagree. ## Verification - `:app:compileDebugKotlin` (non-incremental, `-Pkotlin.incremental=false`, outputs wiped first): BUILD SUCCESSFUL, **0** `w:` lines. - `:app:testDebugUnitTest --no-build-cache`: **229 tests, 0 failures, 0 errors, 0 skipped, 16 classes**. CI's `android-ci.yml` does not run Kotlin tests, so this local run is the only evidence. - No APK is claimed from this PR; `android-ci` runs the build on the merge.
The wallet tab was nine equal boxes: a balance card, a daemon card, and then
four outlined twins (Invoice/Tip, Create/Import) pushed below the fold under a
wall of grey helper text. This rebuilds it as two cards.

Hero card. While no wallet file is open the card says "No Wallet Active" with
the plan's subtitle and puts Create Wallet (primary) / Import Wallet
(secondary) first, with Saved wallets in the card header. Once a wallet is
open the balance is the headline at 28sp bold with a sync-state pill, then the
gated receive address and its QR, then Receive / Invoice and Send / Tip, and a
destructive Lock Wallet. Creating and importing a further wallet stay reachable
in both states, and so does invoicing in the no-wallet state: the core resolves
a payee address from the profile, so hiding the action there would drop a path
that works ("no receive address - set profile xmr or ONIONWIRE_WALLET_RPC" is
the both-missing case). A tip really does need the wallet, so it appears only
when one is open.

Node card. The raw "connecting . in-process mainnet pruned . node starting .
sync 485253/3794383" line is replaced by a status sentence, a percentage, a 6dp
progress track and "Block 485,253 of 3,794,383" with the free disk on the
right; the full line is still the header's contentDescription, so a screen
reader loses nothing. The three stacked mode buttons become one segmented
control, Remote vs On-Device is a second one, and the lifecycle is a single
context-aware control (Start Node and Wallet, or Stop Node while it runs)
instead of a permanently disabled twin. Every raw paragraph (no binary, no
wallet open, start blocked, core error) is now an AlertBanner.

Fail-closed rules are unchanged and now tested: target_height == 0 prints no
percentage (it is "no target reported", never "caught up", which is what
Cuprate reports at the tip), a height past the target clamps at 100.0%, and an
unrecognised state is never green. The wallet off / err sentinels still come
back from the core untouched; only their display changed, to "No Wallet Loaded"
and "Balance unavailable".

Deliberate, and named in the handoff: WalletScreen drops its onBack parameter
because Wallet is a top-level tab now (MainActivity updated; Chats and Settings
already have no back affordance), and OwSegmentedControl gains an optional
testTags list - additive, defaulting to empty, so no existing call site
changes.
ui: saved-wallets screen and profile copy on the shared components (plan P4.1)
All checks were successful
android-ci / android (pull_request) Successful in 14m2s
0d0b0b975d
The catalogue screen was still the alpha build: a TopAppBar with a text "Back"
and a text "Refresh", eight hand-rolled Cards on Material's surfaceVariant,
outlined twins for the two secret actions, and every failure — including "no
wallet is open", which is a state the app is in, not a failure — printed as a
raw red Text. It now uses the shared components: a chevron top bar, OwCard rows
with an "In use" pill, and OwButton tiers.

Copy, verbatim from the plan:

  "No wallet is open in the wallet child yet. Open a saved wallet on the Saved
   wallets screen, or create or import one here."
      -> "Select a saved wallet or create a new one to view your balance and
         transact."

  "No wallet is open in the wallet child yet. Open one from the list below — a
   wallet that needs a passphrase asks for it here — and the balance, address
   and backups work again."
      -> "Open or create a wallet to enable balance, address, and backup
         controls."

WALLET_NOT_OPEN_TEXT is now rendered as an info banner with its own title, not
as an error; ErrorText picks the variant from the text, so the same string
cannot read as a crash on one screen and a state on the other. The "no wallet
open" hint, the passphrase prompt's blocked reasons and its "wallet is not
running" line, and the catalogue/keys failures are banners too.

The two remaining user-facing mentions of the daemon process are gone: the
passphrase card says "the wallet is pointed at <file>" and the secrets block
reason says "the wallet is not running". ProfileScreen's empty-address notice
loses its copy of the same phrase ("then comes from the running wallet"), and
its test now asserts the consequence instead of the internal name.

No behaviour change: every predicate (walletSecretsEnabled,
walletPassphraseSubmitEnabled, walletChildReadable, walletFailureText,
displayLastError) is the same function with the same branches; only strings and
layout moved. The four WalletManagerTest assertions that pinned the old strings
are updated here, in the same commit, as the card requires.
ui: one door to the saved-wallet catalogue
All checks were successful
android-ci / android (pull_request) Successful in 15s
ef149ac496
The wallet tab was carrying "Saved wallets" twice: once as a top-bar action
(the alpha screen's "Wallets" text link, which I kept when I moved the screen
onto OwTopBar) and once in the hero card's header, where the plan puts it. Two
controls that do the same thing on one page is how a screen starts to look
unfinished.

The hero header keeps it; the top bar is a title and nothing else.
ui: drop two imports the wallet screen no longer uses
All checks were successful
android-ci / android (pull_request) Successful in 11m57s
9b3a0ad177
sirius changed target branch from ui/refactor-shell-messenger to main 2026-10-29 05:32:22 +00:00
Merge branch 'main' into ui/refactor-wallet-node
All checks were successful
android-ci / android (pull_request) Successful in 11m4s
76f6661171
sirius merged commit 20d2d00e4d into main 2026-10-29 06:19:57 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
sirius/onionwire!199
No description provided.