Skip to content

Memo inbox + receiving-address labeling & filtering - #134

Draft
zmanian wants to merge 37 commits into
chainapsis:mainfrom
zmanian:feature/received-memo-inbox
Draft

Memo inbox + receiving-address labeling & filtering#134
zmanian wants to merge 37 commits into
chainapsis:mainfrom
zmanian:feature/received-memo-inbox

Conversation

@zmanian

@zmanian zmanian commented May 24, 2026

Copy link
Copy Markdown
Contributor

This branch ships three stacked features building on each other. Specs/plans under docs/superpowers/.


1. Received memo inbox (read-only, with hide/restore)

A Memos tab on the Activity screen listing received Zcash text memos for the active account, with per-memo hide/restore for spam/abuse.

  • All · Memos segmented control; received text memos only (sent/change/self excluded).
  • Rust-backed case-insensitive search (decodes via decode_text_memo, never LIKE on the raw memo BLOB).
  • No sender shown (Zcash shielded memos have no authenticated sender — deliberate).
  • Per-memo hide/restore: local device-only hidden set (AppSecureStore plain KV, wiped on reset); a Hidden view restores; hidden memos redacted (with Restore) in the transaction detail screen.
  • Lock-aware: list empty while locked; only txid:pool:index identifiers persisted.

2. Receiving-address labeling

Name your own receiving addresses, stored locally per-account; managed inline on the Receive screen and in a new My Addresses screen (Settings → My addresses).

  • Rust list_account_addresses: external, exposed receiving UAs only (key_scope = 0 AND exposed_at_height IS NOT NULL), excluding the gap-limit lookahead pool; newest-first.
  • Dart addressLabelsProvider: local per-account labels keyed by address string (mirrors hiddenMemosProvider); wiped on reset, not seed-recoverable.
  • Inline naming on Receive (optional, never blocks renew); My Addresses lists addresses with Rename.

3. Filter memos by receiving address

The Memos tab gains a receiving-address dropdown that narrows the inbox to memos received at a chosen one of your addresses — shown by label when named (from feature 2).

  • to_address surfaced on ReceivedMemo (Rust → FRB → Dart); same string as the labels, so it both labels and filters with zero translation.
  • Dropdown options = the distinct receiving addresses present in the loaded memo set (labeled, else truncated); shown only at ≥2 addresses.
  • Client-side narrow composing with the Rust-backed search and the Inbox/Hidden partition (all AND together).
  • Resets to "All addresses" when the selection leaves the set; "No memos for this address" empty state. NULL-to_address memos appear only under "All".

Test Plan

  • Rust: cd rust && cargo test — 94 lib tests pass.
  • Dart: 53 targeted tests pass across memo inbox, hide/restore, address labeling, and the address filter (fvm flutter test test/features/activity/ test/features/receive/ test/features/settings/my_addresses_screen_test.dart test/providers/hidden_memos_provider_test.dart test/providers/address_labels_provider_test.dart).
  • fvm flutter analyze — no issues in changed files (vendored rust_builder/cargokit errors are pre-existing/unrelated).
  • Manual smoke (needs a device wallet with received memos at ≥2 addresses): memo list/search/hide/restore + detail redaction; name addresses inline + via My Addresses, rename, relaunch to confirm persistence; address dropdown narrows, composes with search, applies in Hidden view, hidden at <2 addresses.

Known follow-ups (non-blocking)

  • Memo empty-state copy when a search query is active and all matches are hidden.
  • My Addresses rename input styling vs AppTextField; empty-active-account edge in the address list.
  • truncateAddress boundary + hidden-view-empty test coverage (robustness nits).

🤖 Generated with Claude Code

zmanian and others added 29 commits May 24, 2026 11:20
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extract the received-output test out of detail_includes_output so the
inbox query (Task 2+) and the detail path share one definition that
cannot drift. Add #[derive(Clone)] to TxOutput to support test spread
syntax.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds `get_received_memos` (with optional substring query wired for Task 3)
and `ReceivedMemo` struct to the wallet sync layer. Only external inbound
shielded outputs with non-empty `Memo::Text` are returned; change outputs
(internal key scope) and empty/arbitrary memos are excluded by combining
`is_received_output` with `decode_text_memo`. Re-exported from
`wallet::sync` at the correct visibility levels for the FRB layer in Task 5.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add get_received_memos_search to exercise query filtering: two external
inbound txs with memos "Invoice 1042 paid" and "thanks for lunch" verify
that query=None returns 2, "invoice" matches 1 (case-insensitive),
"LUNCH" matches 1, and "zzz" returns 0.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add `memo_output_key: Option<String>` to `pub(crate) struct TransactionDetail`
in `rust/src/wallet/sync/transactions.rs`. In `get_transaction_detail`, the
existing `find_map` over `visible_outputs` is extended to also capture which
output carried the text memo, producing a key formatted as `"<pool>:<index>"`
(e.g. `"3:0"` for Orchard output 0). `None` when no text memo is found.

Two focused unit tests are added:
- `detail_memo_output_key_set_when_memo_present` — asserts the key matches the
  pool and index of the memo-bearing output.
- `detail_memo_output_key_none_when_no_text_memo` — asserts `None` for a tx
  with no text memo.

The FRB-facing `TransactionDetail` in `api/sync.rs` is not touched; it maps
fields explicitly and is unchanged (Task 5 handles the FRB layer).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add ReceivedMemo FRB struct and get_received_memos wrapper to api/sync.rs
that delegates to wallet_sync::get_received_memos. Add memo_output_key field
to FRB TransactionDetail struct and its get_transaction_detail mapping.
Regenerate all FRB bindings (frb_generated.rs and Dart generated files).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pure-function helper that builds the canonical hide-key string
("txidHex:pool:index") from either the ReceivedMemo field set or
the pre-formatted memoOutputKey from TransactionDetail, ensuring
both sources produce identical strings for the same output.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds `kHiddenMemosKey` constant, a `HiddenMemosNotifier` Riverpod
`Notifier` backed by `AppSecureStore`, and a TDD test suite covering
hide, restore, account isolation, JSON round-trip persistence, and
self-initialization.

The provider self-initializes: `build()` kicks off `load()` fire-and-
forget so watchers automatically receive persisted state without an
explicit call. A mutation-generation guard prevents a late initial load
from clobbering newer in-memory state. Persistence goes through
`HiddenMemosState.toSerializable()` rather than reaching into private
fields. Tests inject `AppSecureStore.testing` with an in-memory
secure-storage fake.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Add MemoRepository abstraction and memoRepositoryProvider seam so
  widget tests can override the data source without FFI.
- Add receivedMemosProvider (FutureProvider.family<MemoQuery>) that
  returns [] when appSecurityProvider.requiresUnlock is true (clears on
  lock).
- Add MemosTab and MemoRow widgets in
  lib/src/features/activity/widgets/memos_tab.dart with debounced search
  (350ms), empty states ("No memos yet" vs "No memos match"), and
  navigation to the existing ActivityTransactionStatusScreen.
- Add "All | Memos" segmented control to ActivityScreen; All tab
  behaviour is unchanged.
- 7 widget tests in test/features/activity/memos_tab_test.dart covering
  list rendering, search narrowing, distinct empty states, lock-clear,
  and memo truncation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds hide/restore actions on inbox memo rows via hiddenMemosProvider.
Each inbox row shows a hide icon button; a Hidden segment in the tab
header lists hidden memos with a restore action. When all memos are
hidden the inbox shows a distinct "All memos hidden — see Hidden" state.
Strengthens the existing amount assertion in memos_tab_test.dart to
check the exact ZecAmount.activity string instead of a loose textContaining.

Extracts the shared InMemorySecureStorage test fake to
test/helpers/in_memory_secure_storage.dart (used by both activity test
files) to prevent drift. Keeps the tappable memo content/amount areas
showing the desktop pointer cursor even when a trailing Hide/Restore
button is present.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add MemoDetailSection ConsumerWidget that checks hiddenMemosProvider and
renders a "Memo hidden" placeholder with a Restore affordance when the memo's
hide-key is in the hidden set; otherwise renders the memo text. When not
hidden (or memoOutputKey is null), the non-hidden display is unchanged: the
expand/collapse toggle stays in the message block's titleTrailing (beside the
"Message" heading) exactly as before, with isMemoHidden computed reactively in
the screen build via ref.watch(hiddenMemosProvider).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Harden per-memo ValueKeys (hide/restore buttons, memo text/amount) to use
the full txid:pool:index identifier instead of txid alone, so keys stay
unique when a single transaction carries multiple received memo outputs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… addresses

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Lists external (key_scope=0) receiving addresses the user has actually been
shown (exposed_at_height IS NOT NULL), excluding the gap-limit lookahead pool,
newest-first by diversifier_index_be. Marks the account default address.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add AccountAddress struct and list_account_addresses wrapper to
rust/src/api/wallet.rs, mirroring the existing get_unified_address
idiom (catch + keys::parse_network). Regenerate all FRB bindings;
lib/src/rust/api/wallet.dart now exposes listAccountAddresses and
class AccountAddress with address (String) and isDefault (bool).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add kAddressLabelsKey to app_secure_store.dart, implement
AddressLabelsNotifier/AddressLabelsState mirroring HiddenMemosNotifier
(mutation-generation guard, build-time microtask load, persist via
writePlain), and full test suite covering set, blank-clears, remove,
per-account isolation, JSON round-trip, and self-init without explicit
load().

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extracts AddressNameField (ConsumerStatefulWidget) that binds to
addressLabelsProvider for pre-fill, persist on submit/blur, and
clear-to-remove semantics. Places the field on the Receive screen
below the shielded address block, guarded to the shielded type and
a non-null address. Address renewal re-keys the widget so the
controller resets to the new address's label automatically.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Add addressRepositoryProvider + addressListProvider mirroring the
  memoRepositoryProvider seam pattern for testability without FFI
- Add MyAddressesScreen listing per-account addresses with italic
  "Unnamed" for unlabeled addresses and an inline Rename action
  persisting via addressLabelsProvider
- Add "My Addresses" row to the Account section of SettingsScreen
  following the onMyAddresses callback threading pattern
- Register /settings/my-addresses GoRoute in app.dart
- Widget tests cover: two-row render + italic Unnamed, save-persists,
  cancel-reverts

The rename field's FocusNode is owned by _AddressRowState (created in
initState, requestFocus on edit entry, disposed in dispose) rather than
allocated inline in build, avoiding a leaked FocusNode per rename session.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@zmanian zmanian changed the title Received memo inbox (read-only, with hide/restore) Memo inbox + receiving-address labeling May 25, 2026
zmanian and others added 8 commits May 24, 2026 17:42
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ter plan

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add `to_address: Option<String>` to `ReceivedMemo` and populate it from
`output.to_address` in `get_received_memos`. Extend the existing test to
assert the Some path ("u-my-receiver") and add a fixture row for the None
path (inbound output with no to_address stored).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add pub to_address: Option<String> to the FRB ReceivedMemo struct and
map it from the wallet-layer struct in get_received_memos. Regenerated
frb_generated.rs and Dart bindings so ReceivedMemo.toAddress (String?)
is available to Dart callers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Move the private _truncateAddress logic from my_addresses_screen.dart into a
shared lib/src/features/activity/address_display.dart module so the Memos tab
address filter (Task 3b) can reuse it without duplication.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add a DropdownButton address filter inside the memos data branch. The
filter derives distinct toAddress values from the current (search-filtered)
memo set, applies to both inbox and hidden views, resets via
post-frame callback when the selected address leaves the set, and shows
"No memos for this address" when the filtered list is empty. Dropdown
is hidden when fewer than two distinct addresses are present.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@zmanian zmanian changed the title Memo inbox + receiving-address labeling Memo inbox + receiving-address labeling & filtering May 25, 2026
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