Skip to content

feat: surface a call to action while no wallet descriptor is loaded - #146

Merged
jvsena42 merged 1 commit into
mainfrom
feat/descriptor-call-to-action
Jul 30, 2026
Merged

feat: surface a call to action while no wallet descriptor is loaded#146
jvsena42 merged 1 commit into
mainfrom
feat/descriptor-call-to-action

Conversation

@jvsena42

@jvsena42 jvsena42 commented Jul 30, 2026

Copy link
Copy Markdown
Owner

Problem

Loading a wallet descriptor is a hard prerequisite — the node only watches addresses it knows about — but the only entry point was a collapsed Descriptors card, item 4 on the 4th tab. Nothing else in the app mentioned it. A first-run user lands on the Node tab, watches it sync, and never learns the step exists, so the app looks broken rather than unconfigured.

Descriptor state also lived solely in SettingsViewModel, which the pager doesn't construct until the user swipes to page 2 (beyondViewportPageCount = 1) — so no other screen could know whether one was loaded.

Approach

New WalletDescriptorRepository singleton polling listdescriptors as the app-wide source of truth. WalletDescriptorStatus.isKnown is the anti-flash guard: it separates "the daemon hasn't answered yet" from "the wallet is genuinely empty", so nothing prompts during the RPC boot window. Non-empty is terminal (Floresta has no unload RPC), so the poll loop exits for good on the happy path and never polls again.

Three surfaces hang off it:

  • Node tab — a manually dismissable snackbar. Rendered as a plain Snackbar beside SnackbarHost in the Scaffold slot, not via showSnackbar(Indefinite), which would hold the single host slot and queue the Utreexo snapshot and clipboard-hint messages behind it. Dismissal is rememberSaveable, so it survives rotation and returns on the next launch while no descriptor is loaded.
  • Settings nav badge — ORed with the existing update badge, preserving markUpdateSeen() semantics. AppNavigationBar/Rail/DestinationIcon are untouched, so the tablet rail is covered for free.
  • Transactions tab — a real empty state replacing the 12sp grey hint. It lives in TransactionLookupCard, which phone and tablet share, so one edit covers both.

All three deep-link to Settings with the Descriptors section expanded and scrolled into view, via a new ExpandDescriptors action (not the toggle, which would collapse an already-open card).

Also fixes a latent bug: the old getDescriptors() only handled onSuccess, so an RPC failure silently read as an empty wallet.

Verification

Verified end-to-end on a wiped x86_64 emulator against mainnet with real peers:

  • no prompt during the splash or the first seconds after; the snackbar appears ~5 s later once listdescriptors has answered twice
  • snackbar, both badges and the Transactions empty state all render
  • "Add descriptor" from either surface lands on Settings with input_descriptor on screen, no manual scrolling
  • ✕ dismisses the snackbar and it stays dismissed across tab switches, returning on the next launch
  • loading a BIP84 zpub cleared all four indicators immediately (via refresh(), not the poll); restart showed no flash

./gradlew test detekt lintDebug passes — 344 unit tests, including a new WalletDescriptorRepositoryImplTest (anti-flash, retry, poll termination, out-of-band refresh) and MainViewModelTest (badge OR truth table).

Notes for review

  • config/detekt/detekt.yml: raised LongParameterList.constructorThreshold to 9. SettingsViewModel hit exactly 7 injected collaborators; a holder object would only hide the dependencies.
  • A testTag on a Snackbar root never surfaces in android layout (the root emits no semantics), so the tag rides the message Text. This keeps the action and ✕ independently targetable — documented in journeys/README.md.
  • DESCRIPTORS_ITEM_INDEX = 3 in ScreenSettings.kt is positional; LazyVerticalStaggeredGrid has no scroll-to-key API. Commented accordingly.
  • The repository's poll settles at one localhost RPC per 30 s for a user who never adds a descriptor — 6× cheaper than NodeViewModel.getInLoop()'s existing 10 s poll.

🤖 Generated with Claude Code

The descriptor is a hard prerequisite — the node only watches addresses it
knows about — but its only entry point was a collapsed Descriptors card on
the 4th tab. First-run users never found it, so the app looked broken rather
than unconfigured.

Descriptor state also lived solely in SettingsViewModel, which the pager does
not construct until the user swipes to page 2, so no other screen could know
whether one was loaded. Lift it into a WalletDescriptorRepository singleton
that polls listdescriptors. Its WalletDescriptorStatus.isKnown flag separates
"the daemon has not answered yet" from "the wallet is empty", so nothing
prompts during the RPC boot window. Non-empty is terminal (there is no unload
RPC), so the poll loop exits for good once a descriptor exists.

Three surfaces hang off that state:

- a manually dismissable snackbar on the Node tab, rendered beside
  SnackbarHost rather than through it — showSnackbar(Indefinite) would hold
  the single host slot and queue the snapshot and clipboard messages behind it
- a Settings nav badge, ORed with the existing update badge so markUpdateSeen
  keeps working
- an empty state on the Transactions tab replacing the 12sp grey hint

All three deep-link to Settings with the Descriptors section expanded and
scrolled into view, via a new ExpandDescriptors action so an already-open
card cannot be collapsed.

Also fixes a latent bug: the old getDescriptors() only handled onSuccess, so
an RPC failure silently read as an empty wallet.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@jvsena42
jvsena42 enabled auto-merge (rebase) July 30, 2026 17:04
@jvsena42
jvsena42 merged commit f04a7d8 into main Jul 30, 2026
2 checks passed
@jvsena42
jvsena42 deleted the feat/descriptor-call-to-action branch July 30, 2026 17:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant