Skip to content

feat(cashu): C3 — minimal wallet UI (balance, receive, export) - #237

Open
grunch wants to merge 11 commits into
feat/cashu-c4-escrow-primitivesfrom
feat/cashu-c3-wallet-ui
Open

feat(cashu): C3 — minimal wallet UI (balance, receive, export)#237
grunch wants to merge 11 commits into
feat/cashu-c4-escrow-primitivesfrom
feat/cashu-c3-wallet-ui

Conversation

@grunch

@grunch grunch commented Jul 25, 2026

Copy link
Copy Markdown
Member

Phase C3 of docs/cashu/README.md — the user-facing half of the embedded
wallet.

Stacked. Base is feat/cashu-c4-escrow-primitives (#236), which sits on
#235 (C2); this branch also merges #234 (C1b), whose isCashuAvailableProvider
does the gating. Merge order: #234, #235, #236, then this.

Invisible unless the node runs Cashu. The Settings entry is gated on
isCashuAvailableProvider — the node must have advertised Cashu and a usable
mint. On a Lightning node the feature does not exist as far as the user is
concerned, and an entry point leading to a permanently empty wallet would be
worse than no entry point.

Scope

Balance, redeem a token, export a token, and a proof-state check. That is all it
should be: the wallet funds and drains escrows against the node's mint. Melting
to Lightning, multiple mints and backup UX are later phases (C10), and a
"general Cashu wallet" is an explicit non-goal of the plan.

  • cashu_wallet_provider.dart — a stream over C2's status broadcast plus a
    thin command object. Each method is a single Rust call: no crypto, no mint
    traffic and no gating logic on the Dart side.
  • cashu_wallet_screen.dart — balance and mint; receive via
    PlatformAwareQrScanner (camera on device, paste on web, already in the
    repo); export via an amount dialog bounded by the balance, then a QR plus
    copyable token; and a "check for unredeemed tokens" action that reclaims
    proofs left reserved by an interrupted send.

Two details worth their code:

  • Rust markers are mapped to localized messages, and an unknown marker falls
    back to a generic one — an internal string can never surface to a user.
  • The exported token carries a warning that it is bearer money. Someone who
    reads it as a receipt and sends it twice loses the funds.

Tests

test/features/cashu/screens/cashu_wallet_screen_test.dart, with the bridge
faked so nothing calls Rust:

  • balance and mint rendered when connected;
  • the disconnected state stated, rather than shown as an ordinary empty wallet;
  • sending disabled at a zero balance, enabled with funds;
  • a known marker localized, and an unknown one falling back — neither leaking
    the raw string.

Verification

flutter analyze                             no issues
flutter test                                161 passed
cargo test                                  161 passed, 14 ignored
cargo clippy --locked -- -D warnings        clean
cargo check --target wasm32-unknown-unknown clean

Test plan

  • Lightning node: no Cashu entry in Settings, and no way to reach the screen.
  • Cashu node (or the C1b dev override) + local nutshell: open the wallet,
    paste a faucet token, see the balance rise, export part of it, redeem the
    exported token in another wallet.
  • Kill the app mid-export and reopen: "check for unredeemed tokens" returns
    the reserved amount.
  • Web build: the entry appears if the node is Cashu, and the screen reports
    the wallet as unavailable rather than crashing (storage is stubbed, Web: IndexedDB storage backend is a stub — nothing persists across a reload #233).

Manual verification

This is the first PR in the series a reviewer can actually use: with a local
mint and the C1b developer override, the whole wallet is reachable by hand.

Already run on this branch

cargo test                                  168 passed, 14 ignored
cargo clippy --locked -- -D warnings        clean
cargo check --target wasm32-unknown-unknown clean
flutter analyze                             no issues
flutter test                                164 passed

A · Setup — a mint and a token to paste

docker run -d --name nutshell -p 3338:3338 \
  -e MINT_LISTEN_HOST=0.0.0.0 -e MINT_LISTEN_PORT=3338 \
  -e MINT_BACKEND_BOLT11_SAT=FakeWallet \
  -e MINT_PRIVATE_KEY=TEST_PRIVATE_KEY \
  -e MINT_RATE_LIMIT=FALSE \
  cashubtc/nutshell:latest poetry run mint

The image ships a wallet CLI, so you can mint sats and produce a real token to
paste into the app — no second wallet app needed:

# fund the CLI wallet (FakeWallet settles the invoice instantly)
docker exec nutshell poetry run cashu --host http://localhost:3338 invoice 64

# print a 16 sat token — copy the cashuB… string
docker exec nutshell poetry run cashu --host http://localhost:3338 send 16

Both commands are verified working against nutshell 0.20.3.

B · Turn the feature on (debug build)

  1. flutter run -d linux (or your device).
  2. Settings → Escrow backend (developer) → toggle Force Cashu escrow on.
  3. Set Mint URL override to http://localhost:3338 and apply.
  4. The card should now show Effective backend: Cashu and
    Effective mint: http://localhost:3338, with no red warning line.
  5. A Cashu wallet entry appears in Settings. Before step 2 it was not there
    — that is the gate, and it is the single most important thing in this PR.

C · The wallet

  1. Open Cashu wallet. Balance reads 0 Satoshis, mint shown, no error.
  2. Receive → paste the cashuB… token from step A (the scanner sheet takes
    pasted text on desktop/web, camera on mobile). Expect "Received 16 sats"
    and the balance to update.
    • Also try pasting it prefixed with cashu: — it must work identically.
      That prefix is what QR payloads normally carry.
    • Paste the same token a second time → a localized "could not be redeemed"
      message, never a raw marker like CashuReceiveFailed.
  3. Send → enter more than the balance → the dialog refuses with
    "You only have N sats". Enter 8 → a QR plus the token text appears.
  4. Tap outside the token dialog. It must not close — this used to be the
    fastest way to lose an exported token. Close it with Done.
  5. A reminder now sits on the wallet screen: "You exported a token…" with
    Show it again. Tap it — the same token comes back. This is what makes an
    accidental dismissal survivable.
  6. Redeem the exported token in the CLI wallet to confirm it is real money:
    docker exec nutshell poetry run cashu --host http://localhost:3338 receive <token>
  7. Tap I've sent it → the reminder disappears.
  8. Check for unredeemed tokens → with nothing pending, "Nothing to
    reclaim"
    .

D · Lightning regression — the part that must not move

  1. Turn the developer override off.
  2. The Cashu wallet entry disappears from Settings immediately.
  3. Use the app normally against the default node: order book, take, trade,
    About. Everything must behave as on main.
  4. Navigate directly to the wallet route while the override is off (deep link
    or hot-reload) → the screen reports the wallet as not connected and every
    action refuses. It never half-works.

Notes

grunch added 3 commits July 24, 2026 22:31
…end in About

Closes the C1 phase of docs/cashu/README.md. Behaviour on a Lightning node is
unchanged: with no `escrow_mode` tag the resolution stays Unknown, every Cashu
path stays shut, and the About screen renders exactly what it did before.

Rust
- `Storage` gains a generic k/v accessor (`get_setting`/`set_setting`/
  `delete_setting`) over the `settings` table that already existed. SQLite
  implements it for real and the two named active-node accessors become thin
  wrappers over it; IndexedDB follows its existing stub contract, so on web the
  overrides apply for the session only (tracked in #233).
- `escrow_mode` now stores what the node advertised and the overrides
  separately, and resolves on read. Flipping an override therefore takes effect
  immediately instead of waiting for the next relay fetch, and there is no
  second copy of the answer that can go stale. A node switch clears the tags but
  keeps the overrides — forcing Cashu is a statement about this build, not about
  a node. Every mutator emits on a broadcast channel.
- New `api/escrow.rs`: `get_escrow_mode`, `set_escrow_mode_override`,
  `set_cashu_mint_url_override`, `rehydrate_escrow_overrides` and an
  `on_escrow_mode_changed` stream. Mint URLs are validated as http(s) with a
  host, on write and again on rehydrate. `EscrowModeInfo.mode` is a stable
  marker; Dart maps it to a localized string.

Dart
- `MostroInstance.fromTags` parses `escrow_mode` and the three `cashu_*` tags,
  tri-state and parameter-gated exactly like `BondPolicy`.
- About reports what the node advertises, so the backends are mutually exclusive
  on screen: a Cashu node gets a "Cashu escrow" section instead of the Lightning
  Network one. The mint renders in full rather than through the copyable row,
  whose 20-character abbreviation would hide the host.
- `escrowModeProvider` / `isCashuAvailableProvider` expose the resolution that
  gates behaviour; the override controls live in a `kDebugMode`-only settings
  card that shows the effective resolution next to the toggle.
- Strings translated in all five locales.

Tests
- Rust: k/v round-trip, override re-resolution without a fetch, node switch
  keeps the override, every mutator notifies, mint-URL rejection leaves the
  previous value in place.
- Dart: tag parsing incl. gating and malformed values; widget tests asserting no
  trace of Cashu on a Lightning or silent node, and no Lightning section on a
  Cashu one.
…stence' into feat/cashu-c3-wallet-ui

# Conflicts:
#	rust/src/api/types.rs
#	rust/src/frb_generated.rs
Phase C3 of docs/cashu/README.md. Deliberately minimal: the wallet exists to
fund and drain escrows against the node's mint, not to be a general Cashu
wallet. Melt/mint to Lightning, multiple mints and backup UX are later phases.

Invisible unless the node runs Cashu: the Settings entry is gated on
`isCashuAvailableProvider`, which needs the active node to have advertised
Cashu *and* a usable mint. On a Lightning node the feature does not exist as
far as the user is concerned — and an entry point leading to a permanently
empty wallet would be worse than none.

- `lib/features/cashu/providers/cashu_wallet_provider.dart` — a stream over the
  C2 status broadcast plus a thin command object. Every method is one Rust
  call; no crypto, no mint traffic and no gating logic in Dart.
- `lib/features/cashu/screens/cashu_wallet_screen.dart` — balance, mint,
  receive (QR scan on device, paste on web, reusing PlatformAwareQrScanner),
  export (amount dialog bounded by the balance, then QR + copyable token), and
  a proof-state check for tokens nobody redeemed.
- Route + gated Settings entry; strings in all five locales.

Two details worth their code:
- Rust markers are mapped to localized messages and an unknown marker falls
  back to a generic one, so an internal string can never surface to a user.
- The exported token carries a warning that it is bearer money. A user who
  reads it as a receipt and sends it twice loses the funds.

Tests: balance and mint rendered when connected; the disconnected state stated
rather than shown as an empty wallet; sending disabled with a zero balance and
enabled with funds; a known marker localized and an unknown one falling back,
with neither leaking the raw string.

Stacked: this branch also carries C1b (#234) and C2 (#235), whose providers and
bridge it uses.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@grunch, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 52 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 401ce648-71e5-42c2-88fd-6ecbcd51320b

📥 Commits

Reviewing files that changed from the base of the PR and between 60453c2 and e701b4a.

📒 Files selected for processing (35)
  • docs/cashu/README.md
  • lib/core/app_routes.dart
  • lib/features/about/models/mostro_instance.dart
  • lib/features/about/screens/about_screen.dart
  • lib/features/cashu/cashu_error_messages.dart
  • lib/features/cashu/providers/cashu_wallet_provider.dart
  • lib/features/cashu/screens/cashu_wallet_screen.dart
  • lib/features/settings/providers/escrow_mode_provider.dart
  • lib/features/settings/screens/settings_screen.dart
  • lib/features/settings/widgets/escrow_mode_dev_card.dart
  • lib/l10n/app_de.arb
  • lib/l10n/app_en.arb
  • lib/l10n/app_es.arb
  • lib/l10n/app_fr.arb
  • lib/l10n/app_it.arb
  • lib/l10n/app_localizations.dart
  • lib/l10n/app_localizations_de.dart
  • lib/l10n/app_localizations_en.dart
  • lib/l10n/app_localizations_es.dart
  • lib/l10n/app_localizations_fr.dart
  • lib/l10n/app_localizations_it.dart
  • lib/main.dart
  • rust/src/api/escrow.rs
  • rust/src/api/mod.rs
  • rust/src/api/nostr.rs
  • rust/src/api/types.rs
  • rust/src/db/indexeddb.rs
  • rust/src/db/mod.rs
  • rust/src/db/sqlite.rs
  • rust/src/frb_generated.rs
  • rust/src/mostro/escrow_mode.rs
  • test/features/about/models/mostro_instance_test.dart
  • test/features/about/screens/about_screen_test.dart
  • test/features/cashu/screens/cashu_wallet_screen_test.dart
  • test/features/settings/widgets/escrow_mode_dev_card_test.dart
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/cashu-c3-wallet-ui

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@grunch

grunch commented Jul 25, 2026

Copy link
Copy Markdown
Member Author

Review — C3 (strict pass)

Note: GitHub does not allow Request changes on your own PR, so this is posted as a comment. Treat it as 🔴 Request changes — finding 1 lets a user lose an exported token with one stray tap, on a screen whose own copy warns that the token is the money.

Scope reviewed: lib/features/cashu/**, lib/core/app_routes.dart, lib/features/settings/screens/settings_screen.dart, l10n ×5, test/features/cashu/**.


🔴 Major

1. An exported token is shown exactly once, in a dismissible dialogcashu_wallet_screen.dart, _TokenDialog

showDialog defaults to barrierDismissible: true. Tapping outside the dialog — or a back gesture — closes it, and the token is gone from the UI forever. The proofs are already reserved at that point, so the balance has dropped and the user has nothing to show for it. The screen's own warning says "Anyone who redeems this token keeps the funds", which makes losing the string a plausible way to lose money, not a cosmetic annoyance.

Three things, in order of value:

  1. barrierDismissible: false and an explicit close.
  2. Persist the token against the wallet (or at least keep it in state) so it can be re-shown until it is redeemed.
  3. Failing both, make the "check for unredeemed tokens" affordance far more prominent after an export, rather than a TextButton further down the page.

2. No cashu: prefix handling on scan_receive()

The scanner's raw output goes straight to receiveToken. QR payloads commonly carry the cashu: URI scheme, and the user gets "may be from another mint, or already spent" for a token that is neither. Strip it (Rust side is the better place — see #235 finding 3 — but this is the screen that produces the input).

3. The balance renders an unknown value as 0_BalanceCard

status?.balanceSats ?? 0 prints "0 Satoshis" both when the wallet is genuinely empty and when it has not loaded, or when Rust's balance read failed (#235 finding 1). Those are different facts and only one of them is alarming. Render "—" while status == null.


🟡 Minor

4. Amounts are unformatted. '${status?.balanceSats ?? 0}' — the About screen formats sats with _fmt (1,000 Satoshis). A six-figure balance here reads as a wall of digits. Reuse the same helper.

5. _send bounds the amount by the balance but not by the fee. cdk may need a swap to hit an exact amount, and a send of the entire balance can fail at the mint for want of change. Not wrong today (mint fees are usually zero), but "max" is the value a user will type most often.

6. No autoDispose on cashuWalletProvider. The stream and its opaque Rust handle live for the process. Deliberate for the settings entry (the badge should stay live), but worth a line saying so, since mostroNodeProvider next door is autoDispose and the difference looks accidental.


🔵 Nit

7. _run() guards re-entry with _busy, but _receive() opens the sheet before taking the guard, so two sheets can be stacked by a fast double-tap. Harmless (the second returns null) but the guard reads as if it covers the whole action.

8. _message() is a chain of contains on stringified errors. It works, and the fallback is correct — but it is the third copy of this pattern in the codebase after C1b and C5. Worth extracting one CashuErrorMessages.of(error, l10n).


✅ What is right

  • The gating is the right shape. Settings entry behind isCashuAvailableProvider, screen usable but inert elsewhere, and the reasoning — an entry point leading to a permanently empty wallet is worse than none — is the correct framing.
  • The bearer-money warning on the token dialog. Most wallets do not say this; this one should, because the escrow flow will have users exporting tokens who have never handled ecash.
  • Tests assert an unknown marker falls back and that the raw string is absent. That second assertion is the one that actually protects the user, and it is easy to forget.
  • Reusing PlatformAwareQrScanner rather than adding a second scanning path for web.

Test gaps

  • Nothing exercises the export flow: _AmountDialog bounds, _TokenDialog content, or the copy action. Given finding 1 lives there, that is the gap that matters.
  • No test for the Settings entry being hidden when isCashuAvailableProvider is false — the phase's stated "no trace of the feature" acceptance criterion is currently only verified by inspection.

grunch added 4 commits July 25, 2026 08:56
… write

Addresses the strict review on #234.

Major
- `set_escrow_mode_override` / `set_cashu_mint_url_override` read the overrides
  and wrote them back as a whole struct, so two writes from the same screen
  interleaved and the second silently discarded the first. Both now go through
  `escrow_mode::update_overrides`, which mutates under one write lock. Covered
  by `setting_one_override_never_clobbers_the_other`.
- The dev card assigned `TextEditingController.text` during `build()`, which
  notifies listeners in the middle of laying the field out and also wiped
  in-progress typing on any unrelated escrow event. Seeding now happens in a
  post-frame callback and updates in `ref.listen`.

Minor
- `is_overridden` was true whenever the override was on, including when the
  node genuinely advertises Cashu — so the dev card labelled a real Cashu node
  as forced, which is the confusion the flag exists to prevent. Now
  `forcing && from_tags != Cashu`.
- The broadcast carried a `ResolvedEscrowMode` that the stream discarded and
  rebuilt, resolving twice per change. The channel is now `Sender<()>`: a bare
  wake-up, with subscribers reading the globals as before.
- Every mutator emitted unconditionally, so a node whose capability fetch keeps
  failing produced a stream of identical "still unknown" events. Each now
  compares before/after and only notifies on a real change.
- `indexeddb::delete_setting` returned Ok silently while its siblings warned. A
  silent no-op in a storage layer reads like working storage in a log.

Tests
- `overrides_survive_a_restart_through_the_settings_store` — the persist and
  rehydrate halves end to end against a real store, which nothing covered.
- `a_change_that_changes_nothing_does_not_wake_subscribers`.
- `forcing_cashu_on_a_cashu_node_is_not_flagged_as_an_override`.
Addresses the strict review on #237.

Major
- The exported token was shown once in a dismissible dialog. Tapping outside it
  — the fastest gesture on the screen — lost the only copy, with the proofs
  already reserved and the balance already down. The dialog is now
  non-dismissible, the token is kept for the session, and a reminder offers
  "show it again" until the user says they have sent it.
- The balance rendered an unknown value as `0`. Following C2's `Option<u64>`,
  it now renders "—", and sending is disabled while it is unknown rather than
  offering a send we cannot size.
- `cashu:` / `cashu://` scanner payloads are handled in C2's `normalize_token`,
  so the scanner's raw output works as pasted.

Minor
- Amounts are grouped (`1,234 Satoshis`), matching the About screen.
- `_busy` is checked before opening the receive sheet and the amount dialog,
  not only around the call that follows them.
- The marker→message chain moved to `cashu_error_messages.dart`. It was already
  duplicated once and C5 would have made three copies.

Also fixes a genuine layout bug the new test surfaced: `AlertDialog` asks its
content for intrinsic dimensions and `QrImageView` lays out through a
`LayoutBuilder`, which cannot answer. The token dialog now has a bounded width;
without it the dialog throws on a narrow screen.

Tests: unknown balance rendered as unknown; send disabled while unknown; and
the export flow end to end — dialog, dismissal, and the token still reachable
afterwards.
@grunch

grunch commented Jul 25, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

grunch added 4 commits July 25, 2026 12:13
… doc

CodeRabbit raised these against #234, where the files live. They had been fixed
one branch too far down the stack (on the C5 branch) and so never reached this
PR; applied here instead, and the stack will carry them forward.

- `MostroInstance.parseEscrowMode` used `getOptional`, so a *present but blank*
  `escrow_mode` tag read as `unknown` while Rust's `parse_tags` reads the same
  event as `Lightning`. Two parsers, one event, different answers — About and
  the Cashu gate could disagree about the same daemon. Only an absent (or
  value-less) tag is unknown now.
- The dev card's post-frame seed captured `mintUrlOverride` during build. An
  override arriving in that gap is applied by `ref.listen` first, and the
  captured copy then overwrote it with the older value. The callback reads the
  current provider value instead.
- The C1 "Done when" still said flipping the override flips "the About section",
  contradicting the bullet directly above it. About reports what the node
  advertised; the override moves the resolved mode and the dev card's effective
  state, nothing else.

Two more from the same pass, not raised inline but the same class:
- `api::escrow::snapshot` derived `mode` from one `get_resolved()` and
  `is_cashu_available` from `is_cashu_mode()`, which reads the globals again; a
  node switch between the two produced a snapshot whose mode and gate disagreed.
  `ResolvedEscrowMode::is_cashu_usable()` expresses the gate against a value the
  caller already holds.
- `set_from_tags` logged unconditionally while only notifying on a change, so a
  reconnect that confirmed what we already knew still wrote an info line.

Tests: blank / whitespace / value-less / absent `escrow_mode` pinned against the
Rust behaviour, and a widget suite for the dev card covering seeding, a newer
override winning, and in-progress typing surviving a no-op event.
@grunch

grunch commented Jul 25, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai Review

@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@codaMW codaMW left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed C3, code pass over lib/features/cashu/**, the Settings gating, and the
widget tests, plus a full hands-on run against a local nutshell mint with the C1b
dev override. Approving.

The gate verified by hand (the headline property):

  • Lightning / no override: no Cashu entry in Settings.
  • Force Cashu on, no mint: entry stays hidden, "Cashu cannot run without a mint"
    the gate holds on is_cashu_usable, not mode == cashu.
  • Force Cashu + http://localhost:3338: the "Cashu wallet" entry appears; toggle
    off and it's gone.

The money path verified live against a real mint:

  • Received a real 16-sat token -> balance rose to 15 (the mint's 1-sat swap fee,
    matching the note in the PR body, good that it's not hidden).
  • Sent 2 sats -> exported the token -> redeemed it in an independent CLI wallet
    (cashu receive), which credited 1 sat after the fee. A real bearer-token
    round-trip out of the app and back into another wallet.
  • The export dialog carries the "anyone who redeems this keeps the funds" warning
    and offers explicit Copy / Done rather than relying on dismissal.

Your three Majors verified (live + code + tests):

  • Token-loss _TokenDialog is barrierDismissible: false, and _lastToken keeps
    the token re-showable via the "show it again" reminder (visible on the wallet
    screen after an export). Covered by an exported token stays retrievable until dismissed.
  • cashu: prefix C3 passes the raw string to Rust's receiveToken, which
    normalizes it (C2) the right layer.
  • Unknown balance _BalanceCard renders " " when balanceSats == null, never
    "0 Satoshis"; _fmtSats formats larger amounts. Covered by an unreadable balance renders as unknown, never as zero.

One finding, non-blocking, pre-existing: on Linux desktop, tapping Receive dims
the screen and does nothing. PlatformAwareQrScanner routes every non-web platform
to MobileScanner, but mobile_scanner has no Linux/Windows plugin (confirmed
it's absent from the Linux plugin registrant), so it throws an unhandled
MissingPluginException instead of falling back to paste. It's shared code C3
didn't introduce and Cashu's real target is mobile, but C3 is the first screen to
hit it on desktop, and the failure is a silent dim rather than a graceful "paste
instead". Worth extending the paste fallback to desktop (kIsWeb || isDesktop) or
handling the missing plugin. (I verified the receive/send/export flow above by
locally enabling the paste fallback on Linux reverted, not part of any commit.)

cargo test 169 passed / flutter analyze clean. (The escrow_mode_dev_card
ListTile assertions locally are the known Flutter 3.44-vs-CI-3.38.2 gap from C1b,
not C3.)

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.

2 participants