feat(identity-hub): unified Identities hub (Home · Contacts · Activity · Settings)#842
feat(identity-hub): unified Identities hub (Home · Contacts · Activity · Settings)#842
Conversation
Collapse today's separate Identities and Dashpay nav entries into a unified Identities section with Home / Contacts / Activity / Settings tabs, a two-pill Wallet + Identity switcher, and an identity-first naming model where DashPay profile is an optional social-profile overlay. Adds wireframe.html (8 frames, persona + theme toggles), design-spec.md (IA, screen-by-screen, wording audit, tooltip catalog), and README.md. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Fixes Diziet and Adams audit findings:
- Gate F4/F8 Advanced blocks so Alex never sees raw IDs (.adv class)
- Wire aria-describedby on ~60 tooltip triggers (screen-reader access)
- Add tooltips to identity-type, network, and connection badges
- Add .alex-only CSS gate + class on insight banner (P3)
- Drop protx: jargon prefix from Masternode ID chips in F4 and F8
- Adopt spec wallet-pill phrase "Funded by {wallet_name}" across all frames
- Add topbar Refresh icon-button to F3/F4/F5/F6/F7/F8
- Fix F3/F4 switcher pills: tooltip wrappers + aria-haspopup parity
- Add secondary actions row (Add funds / Send to wallet / Send to another identity) on F3 and F4
- Remove dead enabled-Review markup (FG4)
- Wording: "No social profile yet", skip-link, funding subtitle, activity subtitle
- Nits: dead white-space rule, redundant inline style, redundant font-weight load
- Spec: Shadow alpha intentional deviation documented in §E and §F
- Spec: §B.9 Add-funds wizard with all 4 funding methods and persona gating
- Spec: §B.10 Create-identity wizard flow
- Spec: §B.11 Load-existing-identity with all 3 load modes
- Spec: §B.8 Voter-identity keys (Masternode/Evonode), Local nickname, Auto-accept-proof
- Spec: §B.2/B.3 secondary actions row authoritative with entry-point rationale
- Spec: §B.5 Add-by-username accepts raw Identity ID
- Spec: §B.13 Pick-a-username with contested detection, fee preview, vote explanation
- Spec: Onboarding checklist steps enumerated with per-step visibility rules
- Spec: Identity pill dropdown ordering rule + inline search threshold in §A.3
- Spec: §G closed questions G6-G9 for deferred and decided items
- README: design-decisions section for shadow alphas, secondary actions, local nickname
- Catalog §D: entries #83-86 for secondary Home actions and topbar refresh
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Shorten wallet-pill copy from "Funded by Main Wallet" to "Main Wallet" - Collapse wallet + identity switcher into a single horizontal row - Add F3 Identity picker grid as default landing when ≥2 identities exist with per-identity cards (avatar/monogram, name, balance, type pill) and an "Add a new identity" card - Spec §A.4 default-landing rules (0/1/≥2 identities); §B.14 picker screen Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Fold wallet + identity switcher into the breadcrumb itself; placeholders "(no wallet yet)", "(no identity yet)", "(choose an identity)" when empty - Remove the separate switcher row under the breadcrumb from all frames - Move App chrome zoom to the last frame (F8) and rename to "App chrome reference" - Consolidate F3 Identity Home: one canonical frame covering both social-profile-set and no-social-profile states via annotation - Replace Contacts gated-state frame with a populated Contacts page showing received requests (2), active contacts (5), and sent requests (2) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Requirements, UX plan, test-case spec, and dev plan for the new Identities hub UI section (4-tab hub: Home/Contacts/Activity/Settings) derived from docs/ai-design/2026-04-22-identity-dashpay-redesign/. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the foundation for the unified Identities hub UI section (docs/ai-design/2026-04-23-identity-hub-impl/). This commit only introduces the compile-time scaffold; subsequent commits wire up the left-nav entry, AppState registration, and per-tab content. Changes: - New Cargo features `identity-hub` (default on) and `identity-hub-activity-feed` (default off, gates the unified activity timeline backend aggregator that does not exist yet). - New `RootScreenType::RootScreenIdentityHub` variant with stable on-disk encoding `27` and round-trip tests. - New `ScreenType::IdentityHub` and `Screen::IdentityHubScreen` variants; all `ScreenLike` dispatch arms plus `change_context` extended. Macro `set_ctx!` receives the new variant via the `skip` list since the explicit match arm in `change_context` already handles it. - New `src/ui/identity/` module with a `ScreenLike` implementation that dispatches by loaded-identity count (onboarding/home/picker) and renders a placeholder tab bar plus per-tab stubs. The module is unconditionally compiled so the enum dispatch stays exhaustive; the `identity-hub` feature only controls nav visibility, which lands in a follow-up commit. - Eight unit tests covering tab ordering, labels, accessible descriptions, default variant, `HubLanding` state transitions, and `RootScreenType` round-trip. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Completes the coexistence wiring for the new Identities hub. Users now see three identity-related entries in the sidebar: - `Dashpay` (legacy, feature-gated via existing FeatureGate) - `Identities` (legacy identities screen, unchanged) - `Identity Hub` (new, only when the `identity-hub` feature is on) Feature gating at the Cargo level — not a runtime FeatureGate — because the new hub doesn't share a predicate with the other entries. The entry is inserted in the button array immediately after the legacy `Identities` entry so the three identity-related items cluster together. AppState::new() inserts an `IdentityHubScreen` into `main_screens` via an iterator chain that is empty when the feature is disabled, so the screen map never contains an unreachable entry. No existing screen is modified. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Introduces two reusable components for the Identities hub breadcrumb switcher (design-spec §A.3): - `BreadcrumbPill` — label + optional icon + chevron, three visual modes (Interactive / Subdued / Placeholder). Self-contained theming for light + dark mode. Builder methods for icon, tooltip, accessible name, and mode override. - `IdentityPill` — thin wrapper that resolves the identity label via the priority rule: Local nickname → DPNS username → shortened Identity ID (design-spec §G6). Label resolution is a pure function (`display_label`) so it is unit-testable without egui context. Both components follow `docs/COMPONENT_DESIGN_PATTERN.md`: private fields with builder methods, a `ComponentResponse`-implementing response struct, no direct egui state leakage. 16 unit tests added: mode toggling, label priority ordering (nickname/DPNS/id, empty, whitespace), raw-id shortening (head 5 + "…" + tail 3), and response round-trip. No existing code paths modified; the components are exposed via `src/ui/components/mod.rs` and can now be consumed by the hub (breadcrumb switcher composition in a follow-up). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three integration tests register the new hub with AppState and verify: 1. `identity_hub_mounts_and_renders` — AppState with the hub selected as the active root screen renders ten frames without panicking on the default empty database (onboarding path). 2. `legacy_nav_entries_coexist_with_hub` — verifies the enum contains all three coexisting variants and the on-disk encoding for the new hub variant round-trips through `from_int` / `to_int`. 3. `identity_hub_screen_type_creates_hub_screen` — guards against a refactor that drops the hub case from the `create_screen` dispatch. These are the minimum acceptance tests for the current scaffold. The per-tab assertions (IT-HOME-01, IT-CONTACTS-01, IT-ACTIVITY-01, IT-SETTINGS-01 from the test-case spec) arrive alongside each tab's content in follow-up commits. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- `src/ui/components/README.md`: new section documenting `BreadcrumbPill` and `IdentityPill` with their label priority rule and modes. - `docs/user-stories.md`: new IDH section with six stories covering first-time setup, identity home, multi-identity switching, optional social profile, dev-mode bulk creation, and the gated unified activity timeline. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
📝 WalkthroughWalkthroughThis PR adds a unified Identities Hub UI (feature-gated), extensive design/implementation documentation, many new UI components (breadcrumb/identity pills, hero, picker, rows, cards, tab bar, checklist, etc.), a multi-tab IdentityHubScreen (Home/Contacts/Activity/Settings) with onboarding/picker flows, app routing integration, and kittest suites. Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
Note
Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
docs/user-stories.md (1)
7-20:⚠️ Potential issue | 🟡 MinorAdd the new IDH section to the table of contents.
The
Identities Hub (IDH)section is added at Line 1043 but is not linked from the TOC.Proposed docs fix
- [Identity Operations (IDN)](`#identity-operations-idn`) - [DPNS (DPN)](`#dpns-dpn`) - [DashPay (DPY)](`#dashpay-dpy`) +- [Identities Hub (IDH)](`#identities-hub-idh`) - [Token Operations (TOK)](`#token-operations-tok`)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/user-stories.md` around lines 7 - 20, The TOC is missing an entry for the new "Identities Hub (IDH)" section; add a new bullet linking to it (use the anchor format consistent with other entries) by inserting "- [Identities Hub (IDH)](`#identities-hub-idh`)" into the Table of Contents (place it near "Identity Operations (IDN)" for logical grouping) so the "Identities Hub (IDH)" section is reachable from the TOC.
🟡 Minor comments (17)
docs/ai-design/2026-04-22-identity-dashpay-redesign/design-spec.md-352-352 (1)
352-352:⚠️ Potential issue | 🟡 MinorReserve or restore section B.5.
The document jumps from
B.4.1toB.6; add aB.5 — reservedheading or renumber the following sections to keep cross-references stable.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/ai-design/2026-04-22-identity-dashpay-redesign/design-spec.md` at line 352, The document skips B.5 and jumps from B.4.1 to the "B.6 Activity tab (Frame 5)" heading, so add a placeholder heading "B.5 — reserved" immediately before the existing B.6 heading (or alternatively renumber the subsequent headings to restore a continuous sequence); update any internal cross-references that reference B.6/B.4.1 as needed so numbering remains stable and consistent.docs/ai-design/2026-04-22-identity-dashpay-redesign/design-spec.md-12-13 (1)
12-13:⚠️ Potential issue | 🟡 MinorClarify final IA versus Phase 1 coexistence.
These lines describe replacing Dashpay and Identities with a single
Identitiesnav entry, but this PR currently addsIdentity Hubalongside the existing entries. Add an explicit phased-rollout note so follow-up implementation and tests do not treat the scaffold label as a spec violation.Suggested wording
-This spec collapses the current two left-nav entries — **Dashpay** and **Identities** — into -one unified section called **Identities**. +This spec describes the final target state: the current **Dashpay** and **Identities** +left-nav entries collapse into one unified section called **Identities**. During Phase 1, +the implementation may expose a separate **Identity Hub** entry alongside the legacy entries +so the scaffold can be reviewed without removing existing flows.Also applies to: 40-45
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/ai-design/2026-04-22-identity-dashpay-redesign/design-spec.md` around lines 12 - 13, Add a short phased-rollout note to clarify that the spec's final IA replaces the two nav entries ("Dashpay" and "Identities") with a single "Identities" section, while the current PR may temporarily introduce the scaffold label "Identity Hub" alongside existing entries; update the document near the existing sentence and the similar block at lines referenced as also applies to (the other paragraph) to explicitly state that "Identity Hub" is a temporary scaffold for Phase 1 and will be removed/merged in the final rollout so implementations and tests should treat its presence as non-normative.docs/ai-design/2026-04-22-identity-dashpay-redesign/design-spec.md-532-533 (1)
532-533:⚠️ Potential issue | 🟡 MinorTighten the incomplete sentence.
Minimum required shown dynamicallyreads like a missing noun; make it explicit.Suggested wording
- `UseWalletBalance`. Minimum required shown dynamically. + `UseWalletBalance`. The minimum required amount is shown dynamically.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/ai-design/2026-04-22-identity-dashpay-redesign/design-spec.md` around lines 532 - 533, The sentence fragment "Minimum required shown dynamically" is incomplete; update the wording in the "Fund the identity" step to include an explicit noun (e.g., "amount" or "funds"). Locate the line that mentions UseWalletBalance and replace the fragment with a complete phrase such as "Minimum required amount shown dynamically" (or "Minimum required funds shown dynamically") so the instruction reads: "UseWalletBalance. Minimum required amount shown dynamically." Ensure the change appears inline within the Add funds wizard (§B.9) description.src/ui/identity/mod.rs-15-17 (1)
15-17:⚠️ Potential issue | 🟡 MinorUpdate this feature-gate note to match routing behavior.
identity-hubdoes more than control the left-nav entry:src/app.rsalso omitsRootScreenIdentityHubfrommain_screenswhen the feature is disabled. This note should mention both, or it will mislead future changes around persisted root-screen handling.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/ui/identity/mod.rs` around lines 15 - 17, Update the module-level feature-gate note in the identity module to accurately state that the `identity-hub` feature not only controls the left-nav entry but also causes `app.rs` to omit the `RootScreenIdentityHub` variant from the `main_screens` collection, so toggling the feature won’t produce unreachable variants in the root screen enum; modify the comment at the top of the identity module (where the current note lives) to mention both behaviors and reference `RootScreenIdentityHub` and `main_screens` to prevent future confusion when persisting root-screen state.src/ui/components/identity_pill.rs-14-37 (1)
14-37:⚠️ Potential issue | 🟡 MinorKeep
display_label()from returning an empty label.The doc contract says the resolver never returns an empty string, but Line 153 proves an empty ID produces
"". Since this is public UI label logic, use a defensive fallback instead of rendering an invisible pill.Proposed defensive fallback
pub fn display_label( local_nickname: Option<&str>, dpns_handle: Option<&str>, identity_id_base58: &str, ) -> String { @@ if let Some(handle) = dpns_handle.map(str::trim).filter(|s| !s.is_empty()) { return handle.to_string(); } - shorten_id(identity_id_base58) + let identity_id_base58 = identity_id_base58.trim(); + if identity_id_base58.is_empty() { + return "Unknown identity".to_string(); + } + shorten_id(identity_id_base58) }- fn all_empty_collapses_to_empty_id() { - // Defensive: the id is the guaranteed-non-optional fallback, so an - // empty id yields an empty label. Callers must not pass an empty id. + fn all_empty_uses_unknown_identity_fallback() { let label = display_label(None, None, ""); - assert_eq!(label, ""); + assert_eq!(label, "Unknown identity"); }Also applies to: 149-155
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/ui/components/identity_pill.rs` around lines 14 - 37, The display_label function can still return an empty string when identity_id_base58 is empty; update display_label to defensively handle an empty/whitespace identity_id_base58 before calling shorten_id (or make shorten_id return a non-empty fallback). Specifically, in display_label (and similarly used sites), if identity_id_base58.trim().is_empty() return a stable non-empty placeholder (e.g. "Unknown identity" or similar), otherwise call shorten_id(identity_id_base58); alternatively ensure shorten_id never yields "" and returns the raw id or a placeholder when given empty input.docs/ai-design/2026-04-23-identity-hub-impl/03-test-case-spec.md-117-181 (1)
117-181:⚠️ Potential issue | 🟡 MinorSeparate planned per-tab kittests from the tests actually added.
This section names per-tab files and expectations that are not present in the PR’s actual
tests/kittest/identity_hub.rsscaffold coverage. Mark these as planned follow-ups or update the file references so the traceability matrix does not overstate current coverage.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/ai-design/2026-04-23-identity-hub-impl/03-test-case-spec.md` around lines 117 - 181, The doc lists per-tab kittests (IT-ONBOARD-01, IT-HOME-01, IT-CONTACTS-01, IT-ACTIVITY-01, IT-SETTINGS-01, IT-NAV-01) and specific file paths (e.g., tests/kittest/identity_hub_onboarding.rs) that are not actually added to the PR (only tests/kittest/identity_hub.rs scaffold exists); either mark each of those per-tab test entries as "planned" follow-ups or update the file references to point to the actual scaffold (tests/kittest/identity_hub.rs) and adjust the traceability matrix accordingly so it does not overstate coverage — update the headings or add TODO tags next to each IT-XXXX identifier in 03-test-case-spec.md to reflect the true status.tests/kittest/identity_hub.rs-56-70 (1)
56-70:⚠️ Potential issue | 🟡 MinorMake this test exercise the dispatcher it claims to guard.
The current assertion is tautological, so removing the
ScreenType::create_screenarm would not fail this test as long as the enum variant remains. Either construct the existing kittestAppContextfixture and assert the createdScreen::IdentityHubScreen(_), or rename this as a compile-only enum guard.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/kittest/identity_hub.rs` around lines 56 - 70, The test identity_hub_screen_type_creates_hub_screen is currently tautological; change it to actually call ScreenType::create_screen with the real kittest AppContext fixture and assert the returned Screen is the IdentityHub variant (e.g., verify Screen::IdentityHubScreen(_) via matches! or an if let), or if you intentionally want a compile-only guard rename the test accordingly; specifically locate the ScreenType::create_screen call and the test function and replace the enum-only assert with constructing the existing AppContext test fixture (the same one used by identity_hub_mounts_and_renders), invoke ScreenType::create_screen(app_ctx) and assert the result is Screen::IdentityHubScreen(_).docs/ai-design/2026-04-23-identity-hub-impl/03-test-case-spec.md-153-160 (1)
153-160:⚠️ Potential issue | 🟡 MinorUse the hyphenated activity-feed feature name.
Line 156 uses
identity_hub_activity_feed; the Cargo feature documented for the PR isidentity-hub-activity-feed.Proposed docs fix
-**Preconditions**: one identity, `identity_hub_activity_feed` flag off. +**Preconditions**: one identity, `identity-hub-activity-feed` flag off.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/ai-design/2026-04-23-identity-hub-impl/03-test-case-spec.md` around lines 153 - 160, Replace the incorrect underscored Cargo feature name used in the test spec—change the string "identity_hub_activity_feed" to the hyphenated feature name "identity-hub-activity-feed" in the IT-ACTIVITY-01 test documentation (the Activity tab shell renders test in identity_hub_activity), ensuring the documented feature matches the PR's actual Cargo feature.docs/ai-design/2026-04-23-identity-hub-impl/02-ux-plan.md-92-95 (1)
92-95:⚠️ Potential issue | 🟡 MinorUse the Cargo feature name here.
Line 95 documents
identity_hub_activity_feed, but the PR’s Cargo feature isidentity-hub-activity-feed. Keeping the hyphenated name avoids invalid feature flags in follow-up work.Proposed docs fix
- screen." Feature flag: `identity_hub_activity_feed`, off by default. + screen." Feature flag: `identity-hub-activity-feed`, off by default.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/ai-design/2026-04-23-identity-hub-impl/02-ux-plan.md` around lines 92 - 95, The doc uses the underscored feature name identity_hub_activity_feed but the Cargo feature is identity-hub-activity-feed; update the text so the feature name matches the Cargo feature (use identity-hub-activity-feed everywhere the feature is referenced, e.g., in the MVP constraint sentence and any follow-up mentions) and verify this exactly matches the feature key in Cargo.toml to avoid invalid feature flag references.tests/kittest/identity_hub.rs-44-48 (1)
44-48:⚠️ Potential issue | 🟡 MinorUse the actual legacy Dashpay root variant.
Line 45 checks
RootScreenDashPayProfile, which is a DashPay profile screen, not the legacy root nav entry. This test should guard coexistence withRootScreenDashpay.Proposed test fix
- let legacy_dashpay = RootScreenType::RootScreenDashPayProfile; + let legacy_dashpay = RootScreenType::RootScreenDashpay;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/kittest/identity_hub.rs` around lines 44 - 48, The test uses the wrong DashPay variant; replace RootScreenType::RootScreenDashPayProfile with the true legacy root nav variant RootScreenType::RootScreenDashpay so the assertions compare RootScreenIdentityHub against both RootScreenIdentities and RootScreenDashpay; update the variable name (e.g., legacy_dashpay) accordingly and keep the two assert_ne! checks to ensure the new hub variant does not equal the legacy Dashpay root.docs/user-stories.md-1053-1084 (1)
1053-1084:⚠️ Potential issue | 🟡 MinorDo not mark planned hub flows as implemented yet.
The header says
[Implemented]means present in the current codebase, but these stories describe Home tab content, social-profile cards, Contacts gating, and bulk identity creation paths that the PR summary calls out as follow-up scaffold work. Mark them[Gap]or split out smaller scaffold-only implemented stories.Minimal status correction
-### IDH-002: Identity home at a glance [Implemented] +### IDH-002: Identity home at a glance [Gap]-### IDH-004: Opt in to DashPay social profile [Implemented] +### IDH-004: Opt in to DashPay social profile [Gap]-### IDH-005: Developer bulk identity creation [Implemented] +### IDH-005: Developer bulk identity creation [Gap]🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/user-stories.md` around lines 1053 - 1084, Update the status annotations for the identity-hub stories that were incorrectly marked as implemented: change the headers for IDH-002, IDH-003, IDH-004, and IDH-005 in docs/user-stories.md from “[Implemented]” to “[Gap]” (or split each into a scaffold-only sub-story and leave a small implemented stub), and ensure any PR summary or checklist references these stories as follow-up scaffold work rather than completed features so Home tab content, social-profile cards, Contacts gating, and developer bulk-creation paths are not claimed as implemented.docs/ai-design/2026-04-23-identity-hub-impl/04-dev-plan.md-40-41 (1)
40-41:⚠️ Potential issue | 🟡 MinorPoint the enum mapping step at the module that owns it.
RootScreenType::to_int/from_intlive insrc/ui/mod.rsin the provided implementation context, notsrc/database/settings.rs. This step should reference the owning module and mention settings only if a persistence test is being updated there.Proposed docs fix
-6. **`src/database/settings.rs`** — extend `RootScreenType::from_int` / `to_int` mapping - (next free integer). No schema change. +6. **`src/ui/mod.rs`** — extend `RootScreenType::from_int` / `to_int` mapping + (next free integer). No schema change.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/ai-design/2026-04-23-identity-hub-impl/04-dev-plan.md` around lines 40 - 41, Update the docs step to point at the actual owner of the enum mapping: change the reference from src/database/settings.rs to src/ui/mod.rs and state that RootScreenType::to_int and RootScreenType::from_int are implemented there; only mention updating settings persistence or tests in src/database/settings.rs if you also alter saved values or persistence tests — otherwise limit the instruction to editing src/ui/mod.rs where the RootScreenType mapping lives.docs/ai-design/2026-04-22-identity-dashpay-redesign/README.md-3-12 (1)
3-12:⚠️ Potential issue | 🟡 MinorAlign the README with the coexistence rollout.
This says Dashpay and Identities are collapsed into a single
Identitiesnav entry, but this PR preserves both legacy entries and adds the new hub alongside them. Future implementers may remove the wrong nav item if this stays stale.Proposed wording update
-Identities section of Dash Evo Tool 2. The redesign collapses the current two left-nav -entries — Dashpay and Identities — into a single **Identities** section with four tabs: -Home, Contacts, Activity, and Settings. +Identities section of Dash Evo Tool 2. The scaffold introduces a new **Identity Hub** +entry alongside the existing Dashpay and Identities entries, with the hub organized into +four tabs: Home, Contacts, Activity, and Settings.-profile existing. The nav label remains `Identities` (plural, unchanged from the codebase). +profile existing. During the coexistence rollout, the legacy `Identities` nav entry remains +available and the new hub uses its own nav entry.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/ai-design/2026-04-22-identity-dashpay-redesign/README.md` around lines 3 - 12, Update the README to reflect the coexistence rollout: change the statement that Dashpay and Identities are collapsed into a single "Identities" nav entry to instead explain that the redesign introduces a new unified Identities hub while preserving the legacy "Dashpay" and "Identities" left-nav entries (both remain present alongside the new hub), and add a note about future removal being optional; reference the nav labels "Dashpay", "Identities", and the new "Identities" hub (or "Identities" section with tabs Home, Contacts, Activity, Settings) so implementers know the current rollout preserves legacy entries.docs/ai-design/2026-04-23-identity-hub-impl/04-dev-plan.md-66-69 (1)
66-69:⚠️ Potential issue | 🟡 MinorCorrect feature flag names to match Cargo.toml throughout the document.
Feature names in
Cargo.tomlareidentity-hubandidentity-hub-activity-feed(hyphens, no prefix). Update all references:
- Lines 66, 68:
feat-identity-hub→identity-hub;identity_hub_activity_feed→identity-hub-activity-feed- Line 171:
identity_hub_activity_feed→identity-hub-activity-feed- Lines 234–235 (feature table): same corrections
Line 66–69 diff
-- Add `feat-identity-hub` feature to `Cargo.toml` (default-enabled so the hub is visible +- Add `identity-hub` feature to `Cargo.toml` (default-enabled so the hub is visible by default; can be disabled for quick compile). -- Add `identity_hub_activity_feed` feature (default off) — gates the unified activity +- Add `identity-hub-activity-feed` feature (default off) — gates the unified activity aggregator (stub tab content when off).Line 234–235 diff
-| `feat-identity-hub` | Cargo feature | on | entire hub module compilation + nav entry | -| `identity_hub_activity_feed` | Cargo feature | off | unified activity aggregator rendering | +| `identity-hub` | Cargo feature | on | entire hub module compilation + nav entry | +| `identity-hub-activity-feed` | Cargo feature | off | unified activity aggregator rendering |🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/ai-design/2026-04-23-identity-hub-impl/04-dev-plan.md` around lines 66 - 69, The document uses incorrect feature-flag identifiers; update every occurrence of the wrong names to match Cargo.toml by replacing `feat-identity-hub` with `identity-hub` and `identity_hub_activity_feed` (or any underscore/feat-prefixed variants) with `identity-hub-activity-feed`; specifically fix the occurrences called out in the review (the feature mentions around the identity hub description, the unified activity aggregator reference, and the feature table entries) so all references use the exact hyphenated names `identity-hub` and `identity-hub-activity-feed`.docs/ai-design/2026-04-23-identity-hub-impl/01-requirements.md-26-27 (1)
26-27:⚠️ Potential issue | 🟡 MinorAlign the new left-nav label with the implementation plan.
This document says the new entry is also labeled
Identities, while the PR summary calls itIdentity Hub. Keeping two visibleIdentitiesentries during coexistence is ambiguous for users and kittest assertions; please make the requirement and acceptance criteria use one unambiguous label.Also applies to: 150-152
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/ai-design/2026-04-23-identity-hub-impl/01-requirements.md` around lines 26 - 27, Update FR-1 and related acceptance criteria to use a single unambiguous label: change the new left-nav entry text from `Identities` to `Identity Hub` (or vice versa to match the PR summary) throughout the document including references at lines noted (e.g., FR-1 and the acceptance criteria around lines 150-152), and explicitly state that during coexistence the old `Identities` and `Dashpay` entries remain visible while the new `Identity Hub` entry is shown to avoid duplicate `Identities` labels.src/ui/identity/hub_screen.rs-155-164 (1)
155-164:⚠️ Potential issue | 🟡 MinorMake this test exercise
IdentityHubScreen, not just the enum.The test can pass even if
IdentityHubScreen::selected_tab()orIdentityHubScreen::select_tab()is broken, because it only reassigns a localIdentityHubTab. Please instantiate the screen with the existing app-context test harness, or move the state transition into a helper that can be tested withoutAppContext.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/ui/identity/hub_screen.rs` around lines 155 - 164, The test currently only manipulates the IdentityHubTab enum and therefore doesn't validate IdentityHubScreen's state methods; update the test to instantiate an IdentityHubScreen via the existing test harness (or a minimal AppContext-free constructor) and call IdentityHubScreen::selected_tab() and IdentityHubScreen::select_tab() to verify transitions between IdentityHubTab::Home and IdentityHubTab::Settings; if creating a full AppContext is heavy, extract the tab state logic into a small helper (e.g., a TabState struct or impl on IdentityHubScreen that can be new() in tests) and exercise that helper instead so the test actually invokes the same code paths used by IdentityHubScreen.docs/ai-design/2026-04-23-identity-hub-impl/01-requirements.md-126-146 (1)
126-146:⚠️ Potential issue | 🟡 MinorDon’t mark full tab flows as implemented while they are still scaffolded.
US-IDH-002throughUS-IDH-006describe balances, Send actions, picker switching, DashPay opt-in, Developer Mode bulk creation, and a unified activity timeline as implemented. The provided hub/tab code still renders under-construction placeholders, so these should be downgraded to scaffolded / gap-follow-up status or split into separate shipped-vs-follow-up stories.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/ai-design/2026-04-23-identity-hub-impl/01-requirements.md` around lines 126 - 146, Update the status tags and split shipped vs follow-up work for the listed requirements: change US-IDH-002, US-IDH-003, US-IDH-004, and US-IDH-005 from `[Implemented]` to a scaffolded status like `[Scaffolded]` (or `[Gap-follow-up]` where appropriate) and set US-IDH-006 explicitly to `[Gap-follow-up]`; also split each item into two lines or paired stories (a short “UI shell shipped” story and a separate “backend/aggregation follow-up” story) so the README accurately reflects that the hub/tab UI is a placeholder and the full flows (balances, Send action, identity picker behavior, DashPay opt-in backend, Dev Mode bulk creation, unified Activity aggregation) are pending backend work.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/app.rs`:
- Around line 556-575: The persisted selected_main_screen may refer to a screen
that's not registered when the identity-hub feature is disabled, causing
active_root_screen_mut() to panic; modify the initialization to first build the
map of registered screens (the chain that produces (RootScreenType, Screen)
entries including the conditional RootScreenType::RootScreenIdentityHub /
Screen::IdentityHubScreen) and then resolve selected_main_screen by checking the
built map and falling back to a safe default registered key if the persisted
value is missing. Ensure the lookup uses the constructed map rather than
assuming persisted selected_main_screen is valid.
In `@src/ui/components/breadcrumb_pill.rs`:
- Around line 233-263: The click detection bug comes from reading the outer
frame's Response (frame.show(...).response) instead of the inner Label response
that actually carries Sense::click(); update the code to capture the
InnerResponse returned by frame.show (use the value with .inner from the
closure) and use that inner Response for tooltip selection, widget_info and
clicked() logic (keep identifiers: frame.show, frame_response/frame_inner,
ui.add(egui::Label::new(rich).sense(sense)), BreadcrumbPillMode::Interactive and
BreadcrumbPillResponse::new) so interactive pills check inner.clicked() rather
than the frame's response.
In `@src/ui/components/identity_pill.rs`:
- Around line 57-95: IdentityPill currently eagerly stores a BreadcrumbPill and
returns BreadcrumbPillResponse; change it to the lazy pattern by storing the
domain/config fields (local_nickname: Option<String>, dpns_handle:
Option<String>, identity_id_base58: String) plus any builder-set options
(tooltip, accessible_name, mode) instead of BreadcrumbPill, keep the builder
methods (with_tooltip, with_accessible_name, with_mode) to mutate those stored
fields, construct the BreadcrumbPill inside show() using display_label(...) and
the stored options, and return a new IdentityPillResponse type that implements
ComponentResponse (wrapping whatever data you need from the inner
BreadcrumbPillResponse) rather than exposing BreadcrumbPillResponse directly;
update new(), show(), and the public API to use IdentityPillResponse and ensure
no eager UI objects are stored on the struct.
In `@src/ui/identity/hub_screen.rs`:
- Around line 46-52: The current landing() treats any load error as zero
identities by using unwrap_or(0); instead, change landing() to match the result
of app_context.load_local_qualified_identities() so successful Ok(vec) maps to
HubLanding::from_identity_count(vec.len()), while Err(e) surfaces a
user-friendly MessageBanner (use MessageBanner API) and attach the technical
error via BannerHandle::with_details(e) so users see a calm onboarding message
distinct from a real zero count and developers can inspect the attached details;
update code around the landing function and remove unwrap_or(0), referencing
load_local_qualified_identities, HubLanding::from_identity_count, MessageBanner,
and BannerHandle::with_details to implement this behavior.
- Around line 66-144: The ScreenLike impl for IdentityHubScreen currently only
implements ui() so add the missing trait methods: implement
display_task_result(&mut self, result: BackendTaskSuccessResult) to forward or
handle backend results (update state or route to banner system),
display_message(&mut self, msg: &str, ty: MessageType) to enqueue/route messages
to the hub banner/state, refresh(&mut self) and refresh_on_arrival(&mut self) to
reload any cached data or reset UI state (call existing refresh helpers if
present), and change_context(&mut self, app_context: &Arc<AppContext>) to update
self.app_context and trigger a refresh; add all these methods to the impl
ScreenLike for IdentityHubScreen so the hub participates in task result routing,
context changes, and banner lifecycle as required by the project guidelines.
---
Outside diff comments:
In `@docs/user-stories.md`:
- Around line 7-20: The TOC is missing an entry for the new "Identities Hub
(IDH)" section; add a new bullet linking to it (use the anchor format consistent
with other entries) by inserting "- [Identities Hub (IDH)](`#identities-hub-idh`)"
into the Table of Contents (place it near "Identity Operations (IDN)" for
logical grouping) so the "Identities Hub (IDH)" section is reachable from the
TOC.
---
Minor comments:
In `@docs/ai-design/2026-04-22-identity-dashpay-redesign/design-spec.md`:
- Line 352: The document skips B.5 and jumps from B.4.1 to the "B.6 Activity tab
(Frame 5)" heading, so add a placeholder heading "B.5 — reserved" immediately
before the existing B.6 heading (or alternatively renumber the subsequent
headings to restore a continuous sequence); update any internal cross-references
that reference B.6/B.4.1 as needed so numbering remains stable and consistent.
- Around line 12-13: Add a short phased-rollout note to clarify that the spec's
final IA replaces the two nav entries ("Dashpay" and "Identities") with a single
"Identities" section, while the current PR may temporarily introduce the
scaffold label "Identity Hub" alongside existing entries; update the document
near the existing sentence and the similar block at lines referenced as also
applies to (the other paragraph) to explicitly state that "Identity Hub" is a
temporary scaffold for Phase 1 and will be removed/merged in the final rollout
so implementations and tests should treat its presence as non-normative.
- Around line 532-533: The sentence fragment "Minimum required shown
dynamically" is incomplete; update the wording in the "Fund the identity" step
to include an explicit noun (e.g., "amount" or "funds"). Locate the line that
mentions UseWalletBalance and replace the fragment with a complete phrase such
as "Minimum required amount shown dynamically" (or "Minimum required funds shown
dynamically") so the instruction reads: "UseWalletBalance. Minimum required
amount shown dynamically." Ensure the change appears inline within the Add funds
wizard (§B.9) description.
In `@docs/ai-design/2026-04-22-identity-dashpay-redesign/README.md`:
- Around line 3-12: Update the README to reflect the coexistence rollout: change
the statement that Dashpay and Identities are collapsed into a single
"Identities" nav entry to instead explain that the redesign introduces a new
unified Identities hub while preserving the legacy "Dashpay" and "Identities"
left-nav entries (both remain present alongside the new hub), and add a note
about future removal being optional; reference the nav labels "Dashpay",
"Identities", and the new "Identities" hub (or "Identities" section with tabs
Home, Contacts, Activity, Settings) so implementers know the current rollout
preserves legacy entries.
In `@docs/ai-design/2026-04-23-identity-hub-impl/01-requirements.md`:
- Around line 26-27: Update FR-1 and related acceptance criteria to use a single
unambiguous label: change the new left-nav entry text from `Identities` to
`Identity Hub` (or vice versa to match the PR summary) throughout the document
including references at lines noted (e.g., FR-1 and the acceptance criteria
around lines 150-152), and explicitly state that during coexistence the old
`Identities` and `Dashpay` entries remain visible while the new `Identity Hub`
entry is shown to avoid duplicate `Identities` labels.
- Around line 126-146: Update the status tags and split shipped vs follow-up
work for the listed requirements: change US-IDH-002, US-IDH-003, US-IDH-004, and
US-IDH-005 from `[Implemented]` to a scaffolded status like `[Scaffolded]` (or
`[Gap-follow-up]` where appropriate) and set US-IDH-006 explicitly to
`[Gap-follow-up]`; also split each item into two lines or paired stories (a
short “UI shell shipped” story and a separate “backend/aggregation follow-up”
story) so the README accurately reflects that the hub/tab UI is a placeholder
and the full flows (balances, Send action, identity picker behavior, DashPay
opt-in backend, Dev Mode bulk creation, unified Activity aggregation) are
pending backend work.
In `@docs/ai-design/2026-04-23-identity-hub-impl/02-ux-plan.md`:
- Around line 92-95: The doc uses the underscored feature name
identity_hub_activity_feed but the Cargo feature is identity-hub-activity-feed;
update the text so the feature name matches the Cargo feature (use
identity-hub-activity-feed everywhere the feature is referenced, e.g., in the
MVP constraint sentence and any follow-up mentions) and verify this exactly
matches the feature key in Cargo.toml to avoid invalid feature flag references.
In `@docs/ai-design/2026-04-23-identity-hub-impl/03-test-case-spec.md`:
- Around line 117-181: The doc lists per-tab kittests (IT-ONBOARD-01,
IT-HOME-01, IT-CONTACTS-01, IT-ACTIVITY-01, IT-SETTINGS-01, IT-NAV-01) and
specific file paths (e.g., tests/kittest/identity_hub_onboarding.rs) that are
not actually added to the PR (only tests/kittest/identity_hub.rs scaffold
exists); either mark each of those per-tab test entries as "planned" follow-ups
or update the file references to point to the actual scaffold
(tests/kittest/identity_hub.rs) and adjust the traceability matrix accordingly
so it does not overstate coverage — update the headings or add TODO tags next to
each IT-XXXX identifier in 03-test-case-spec.md to reflect the true status.
- Around line 153-160: Replace the incorrect underscored Cargo feature name used
in the test spec—change the string "identity_hub_activity_feed" to the
hyphenated feature name "identity-hub-activity-feed" in the IT-ACTIVITY-01 test
documentation (the Activity tab shell renders test in identity_hub_activity),
ensuring the documented feature matches the PR's actual Cargo feature.
In `@docs/ai-design/2026-04-23-identity-hub-impl/04-dev-plan.md`:
- Around line 40-41: Update the docs step to point at the actual owner of the
enum mapping: change the reference from src/database/settings.rs to
src/ui/mod.rs and state that RootScreenType::to_int and RootScreenType::from_int
are implemented there; only mention updating settings persistence or tests in
src/database/settings.rs if you also alter saved values or persistence tests —
otherwise limit the instruction to editing src/ui/mod.rs where the
RootScreenType mapping lives.
- Around line 66-69: The document uses incorrect feature-flag identifiers;
update every occurrence of the wrong names to match Cargo.toml by replacing
`feat-identity-hub` with `identity-hub` and `identity_hub_activity_feed` (or any
underscore/feat-prefixed variants) with `identity-hub-activity-feed`;
specifically fix the occurrences called out in the review (the feature mentions
around the identity hub description, the unified activity aggregator reference,
and the feature table entries) so all references use the exact hyphenated names
`identity-hub` and `identity-hub-activity-feed`.
In `@docs/user-stories.md`:
- Around line 1053-1084: Update the status annotations for the identity-hub
stories that were incorrectly marked as implemented: change the headers for
IDH-002, IDH-003, IDH-004, and IDH-005 in docs/user-stories.md from
“[Implemented]” to “[Gap]” (or split each into a scaffold-only sub-story and
leave a small implemented stub), and ensure any PR summary or checklist
references these stories as follow-up scaffold work rather than completed
features so Home tab content, social-profile cards, Contacts gating, and
developer bulk-creation paths are not claimed as implemented.
In `@src/ui/components/identity_pill.rs`:
- Around line 14-37: The display_label function can still return an empty string
when identity_id_base58 is empty; update display_label to defensively handle an
empty/whitespace identity_id_base58 before calling shorten_id (or make
shorten_id return a non-empty fallback). Specifically, in display_label (and
similarly used sites), if identity_id_base58.trim().is_empty() return a stable
non-empty placeholder (e.g. "Unknown identity" or similar), otherwise call
shorten_id(identity_id_base58); alternatively ensure shorten_id never yields ""
and returns the raw id or a placeholder when given empty input.
In `@src/ui/identity/hub_screen.rs`:
- Around line 155-164: The test currently only manipulates the IdentityHubTab
enum and therefore doesn't validate IdentityHubScreen's state methods; update
the test to instantiate an IdentityHubScreen via the existing test harness (or a
minimal AppContext-free constructor) and call IdentityHubScreen::selected_tab()
and IdentityHubScreen::select_tab() to verify transitions between
IdentityHubTab::Home and IdentityHubTab::Settings; if creating a full AppContext
is heavy, extract the tab state logic into a small helper (e.g., a TabState
struct or impl on IdentityHubScreen that can be new() in tests) and exercise
that helper instead so the test actually invokes the same code paths used by
IdentityHubScreen.
In `@src/ui/identity/mod.rs`:
- Around line 15-17: Update the module-level feature-gate note in the identity
module to accurately state that the `identity-hub` feature not only controls the
left-nav entry but also causes `app.rs` to omit the `RootScreenIdentityHub`
variant from the `main_screens` collection, so toggling the feature won’t
produce unreachable variants in the root screen enum; modify the comment at the
top of the identity module (where the current note lives) to mention both
behaviors and reference `RootScreenIdentityHub` and `main_screens` to prevent
future confusion when persisting root-screen state.
In `@tests/kittest/identity_hub.rs`:
- Around line 56-70: The test identity_hub_screen_type_creates_hub_screen is
currently tautological; change it to actually call ScreenType::create_screen
with the real kittest AppContext fixture and assert the returned Screen is the
IdentityHub variant (e.g., verify Screen::IdentityHubScreen(_) via matches! or
an if let), or if you intentionally want a compile-only guard rename the test
accordingly; specifically locate the ScreenType::create_screen call and the test
function and replace the enum-only assert with constructing the existing
AppContext test fixture (the same one used by identity_hub_mounts_and_renders),
invoke ScreenType::create_screen(app_ctx) and assert the result is
Screen::IdentityHubScreen(_).
- Around line 44-48: The test uses the wrong DashPay variant; replace
RootScreenType::RootScreenDashPayProfile with the true legacy root nav variant
RootScreenType::RootScreenDashpay so the assertions compare
RootScreenIdentityHub against both RootScreenIdentities and RootScreenDashpay;
update the variable name (e.g., legacy_dashpay) accordingly and keep the two
assert_ne! checks to ensure the new hub variant does not equal the legacy
Dashpay root.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 2948ac86-677c-4a26-8e51-6efc8109f48a
📒 Files selected for processing (28)
Cargo.tomldocs/ai-design/2026-04-22-identity-dashpay-redesign/README.mddocs/ai-design/2026-04-22-identity-dashpay-redesign/design-spec.mddocs/ai-design/2026-04-22-identity-dashpay-redesign/wireframe.htmldocs/ai-design/2026-04-23-identity-hub-impl/01-requirements.mddocs/ai-design/2026-04-23-identity-hub-impl/02-ux-plan.mddocs/ai-design/2026-04-23-identity-hub-impl/03-test-case-spec.mddocs/ai-design/2026-04-23-identity-hub-impl/04-dev-plan.mddocs/user-stories.mdsrc/app.rssrc/ui/components/README.mdsrc/ui/components/breadcrumb_pill.rssrc/ui/components/identity_pill.rssrc/ui/components/left_panel.rssrc/ui/components/mod.rssrc/ui/identity/activity.rssrc/ui/identity/contacts.rssrc/ui/identity/home.rssrc/ui/identity/hub_screen.rssrc/ui/identity/landing.rssrc/ui/identity/mod.rssrc/ui/identity/onboarding.rssrc/ui/identity/picker.rssrc/ui/identity/settings.rssrc/ui/identity/tabs.rssrc/ui/mod.rstests/kittest/identity_hub.rstests/kittest/main.rs
Five Major findings + several Minor docs fixes.
Majors:
- `src/app.rs` — build the `main_screens` BTreeMap first, then resolve
`selected_main_screen` by checking the built map. Falls back to
`RootScreenIdentities` if the persisted value is not registered.
This prevents `active_root_screen_mut()` from panicking when a user
previously selected the hub and the `identity-hub` feature is later
disabled.
- `src/ui/components/breadcrumb_pill.rs` — use `.inner` (the Label's
Response) instead of `.response` (the Frame's outer Response) to
capture clicks. In egui, `Frame::show(...).response` only senses
`Sense::hover` — child-widget click sensing is not inherited, so
the previous code silently broke click detection on all interactive
pills. The inner Label has `.sense(Sense::click())` applied; reading
from it makes the click fire as intended.
- `src/ui/components/identity_pill.rs` — refactor to the lazy-init
component pattern. Previously the struct eagerly stored a
`BreadcrumbPill`; now it stores the domain fields (local_nickname,
dpns_handle, identity_id_base58) plus builder-set options (tooltip,
accessible_name, mode) and constructs the inner `BreadcrumbPill`
inside `show()`. Returns a new `IdentityPillResponse` implementing
`ComponentResponse` instead of leaking `BreadcrumbPillResponse`.
Also: `display_label` now falls back to a stable `Unknown identity`
placeholder when given an empty id, so the pill is never invisible.
- `src/ui/identity/hub_screen.rs::landing()` — stops swallowing load
errors via `unwrap_or(0)`. On failure it surfaces a calm
`MessageBanner` ("Could not load your identities from this device.
Try refreshing or reopening the app.") with the error details
attached via `BannerHandle::with_details`, and reuses the last-known-
good landing so a real zero-identity account is still distinguishable
from a broken one.
- `src/ui/identity/hub_screen.rs::impl ScreenLike` — adds explicit
`refresh`, `refresh_on_arrival`, `display_message`,
`display_task_result`, and `display_task_error` implementations.
`refresh` clears any stale load-error banner so the next `landing()`
attempt can try again cleanly. The others are scaffold no-ops with
comments explaining why.
Minors:
- `tests/kittest/identity_hub.rs`: guard against the correct legacy
DashPay root variant (`RootScreenDashpay`), not the sub-screen
`RootScreenDashPayProfile`.
- `src/ui/identity/mod.rs`: feature-gate note now documents BOTH
integration sites (`left_panel.rs` nav entry + `app.rs main_screens`
registration) so future changes cannot accidentally produce
unreachable variants.
- `src/ui/components/breadcrumb_pill.rs`: `BreadcrumbPillResponse::new`
promoted to `pub(crate)` so the `IdentityPill` wrapper and tests can
fabricate responses without running egui.
- `docs/ai-design/2026-04-23-identity-hub-impl/`: corrected feature
names (`identity-hub` / `identity-hub-activity-feed`, not
underscored) across ux-plan, test-case-spec, and dev-plan. Fixed
dev-plan step 6 to point at `src/ui/mod.rs` (where `RootScreenType`
lives) rather than `src/database/settings.rs`.
- `docs/user-stories.md`: added Identities Hub TOC entry; downgraded
IDH-002..005 from `[Implemented]` to `[Gap]` since only the
scaffolds ship in this PR (follow-up work does the tab content).
Tests: 485 lib + 75 integration + 3 kittest passing. `cargo clippy
--all-features --all-targets -- -D warnings` clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
✅ Review complete (commit b71e1fe) |
Introduce the four-tab horizontal bar for the Identities hub and wire the
onboarding empty state as the landing view when no identities are loaded.
The new `IdentityHubTabBar` component follows the project component
pattern (private fields, builder methods, `ComponentResponse`-based
response) and reuses existing theme tokens only — `DashColors::DASH_BLUE`
for the selected fill, `border_light` outline for unselected tabs, and
`Typography::SCALE_SM` / `Shape::RADIUS_MD` / `Spacing::SM` for layout.
Each tab surfaces `IdentityHubTab::accessible_description()` as its
clickable tooltip for screen-reader parity.
`IdentityHubScreen` now renders the new bar in place of the scaffold's
inline `selectable_label` preview. Selection still lives on the screen,
mirroring the existing controlled-component pattern.
Onboarding copy was already in place per T3 scaffolding; this change
keeps the strings verbatim from design-spec §B.1 and confirms the
developer-mode footer is gated on `AppContext::is_developer_mode()`.
Tests:
- UT-TABS-01 — unit test asserts the bar's selection contract through
its `ComponentResponse`, plus builder / default-state coverage.
- IT-ONBOARD-01 — new kittest under `tests/kittest/identity_hub_onboarding.rs`
mounts `AppState`, forces `RootScreenIdentityHub`, and asserts the
heading + both CTAs render while the developer-mode footer stays
hidden on the default persona.
Refs: docs/ai-design/2026-04-23-identity-hub-impl/04-dev-plan.md (T5),
docs/ai-design/2026-04-23-identity-hub-impl/03-test-case-spec.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Implements T7 from the identity-hub dev plan. - New IdentityPickerCard component: avatar/monogram, identity-type badge, heading (display_name -> DPNS -> shortened id), sub-line, tabular balance, wireframe "Opens Identity Home" hint. Full card is a single click target; response carries the identity id. - New IdentityPickerAddCard component: dashed border default, solid Dash-blue on hover, fixed design-spec strings, click reports add_requested. - Picker grid now renders a responsive flow of identity cards followed by the add card, auto-fitting columns based on available width (design-spec rule minmax(260px, 1fr)). Add card click routes to the existing AddNewIdentityScreen via AppAction::AddScreen - no new screen introduced. Unit tests: UT-PICKER-01/02/03 plus heading/sub-line edge cases, response round-trip, column-count monotonicity. 17 new tests, all passing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Implements T11 of the identity-hub impl plan. Renders a two-column layout inside the central island: social profile (left) and username + aliases (right), with a full-width Advanced expander below that contains identity type + raw ID, keys summary, refresh action, and a danger-zone card. Backend integration is strictly additive — the save path dispatches the existing DashPayTask::UpdateProfile, the refresh path uses IdentityTask::RefreshIdentity, and the register-username CTA routes to the existing RegisterDpnsNameScreen. Controls without a matching backend task (Delete social profile, Add / Remove / Make-primary alias, Unload identity from this device) are rendered as non-interactive affordances with disabled_tooltips explaining that the action is coming, and marked with TODO(identity-hub) comments so the backend follow-up can search for them. Copy comes verbatim from the design spec (§B.8 and §D tooltip catalog). The hub screen now owns a SettingsTab and dispatches through its stateful render so edit drafts persist across frames. Tests: - 10 unit tests in src/ui/identity/settings.rs covering validation thresholds, dirty tracking, string helpers, and the identity-type badge. - One kittest in the same module asserts the three required section headings (Social profile / Username / Aliases / Advanced) render via a build_ui harness — this covers the IT-SETTINGS-01 label assertions without bootstrapping a full identity fixture. - IT-SETTINGS-01 in tests/kittest/identity_hub_settings.rs exercises the AppState-level mount path on the Settings tab and verifies the hub continues to render without panicking on a fresh database. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add T9 of the identity-hub implementation plan: the Contacts tab renders either a centered social-profile gate card (when the active identity has no DashPay profile) or a three-section populated shell (received · active · sent) per design-spec §B.4 / §B.4.1. New flat components in `src/ui/components/`: - `social_profile_gate_card` — centered card with handle-aware body, `Add a display name` primary CTA, and a toggleable `Why?` panel. - `request_card` — received (amber strip + Accept/Decline) and sent (blue strip + Pending pill + Cancel request) variants. - `contact_row` — clickable list row with avatar monogram, display name, `@handle`, optional last-payment hint, and Send + overflow actions; response carries the contact id for click routing. The populated-state shell dispatches the existing `DashPayTask::LoadContacts` via `AppAction::BackendTask` — no new backend variants are added (explicitly scoped out for T9). Interactive accept / decline / cancel flows are deferred to T10 with inline TODO markers. Tests: - UT-GATE-01, UT-REQUEST-CARD-01, UT-CONTACT-ROW-01 (unit). - IT-CONTACTS-01 (kittest) — mounts `contacts::render_gated` directly and asserts the gate heading + primary button render while the populated section headings stay absent. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
T10 ships the Activity tab shell and its reusable row component: - `src/ui/components/activity_row.rs` — a 48 px compact row with `Payment`, `Funding`, and `PlatformOp` kinds and `Normal`, `Expanded`, and `Failed` statuses. `Failed` rows render a `Retry` small button and a danger-stroke border, matching design-spec §B.6. The component reports `ToggleExpand` or `Retry` actions via `ComponentResponse`. - `src/ui/identity/activity.rs` — filter-chip row (All / Payments / Funding / Platform) with `All` as the default reset, plus a gated empty state. When `identity-hub-activity-feed` is off (default) the tab points users to the legacy DashPay Payments screen; when on, it renders an aggregator placeholder — no new backend aggregator is introduced (additive-only rule). Tests: - UT-ACTIVITY-ROW-01 — covers Normal, Expanded, and Failed render paths with Retry-button presence assertion (plus 6 smaller unit tests over response semantics and builder composition). - IT-ACTIVITY-01 — kittest asserting the three required filter chips and the gated empty-state copy render on the default feature set. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Implements T8 from the identity-hub dev plan: - `IdentityHeroCard` — gradient hero (DASH_BLUE → PLATFORM_PURPLE at 14 %, RADIUS_XL, Shadow::elevated()) with two variants: social-profile-set (96 px avatar + display name + @handle) and no-social- profile (type-glyph monogram + optional `Pick a username` prompt). Renders an identity-type badge pill and optional network pill. Follows `docs/COMPONENT_DESIGN_PATTERN.md` with private fields, builder methods, and a response struct implementing `ComponentResponse`. - `OnboardingChecklist` — three-step strip (Pick a username · Set a display name · Add your first contact) with check-mark / empty-circle bullets and a dismiss button. Response carries activation / dismissal intent. - Identity Home tab (`src/ui/identity/home.rs`) — full wiring: hero card, Send / Receive / Add contact quick actions (Add contact gated behind a social profile per §B.3), Add funds / Send to wallet / Send to another identity secondary ghost actions, inline `Set up your social profile` card in the no-profile variant, the onboarding checklist (hidden once dismissed or complete), a recent-activity preview (empty-state for now; wired to flip to the Activity tab), and an Advanced details expander listing raw Identity ID, revision, and key count. - `IdentityHubScreen` owns a small `HomeState` (dismiss flag, skip-social flag, advanced toggle) so tab switches don't wipe per-tab UX state. Dismissal is ephemeral in memory — no DB schema change. Tests (UT-HERO-01..02, UT-CHECKLIST-01..02): 18 unit tests across the two new components + 7 tests in the home module cover the state machine and credit-to-DASH formatter. Kittest IT-HOME-01 mounts the hub, asserts the Home outcome API surface, and pins the four-tab label order. Strings are copied verbatim from design-spec §B.2 / §B.3 / §C / §D. All design choices documented in module-level comments. No backend_task changes — tab dispatches reuse existing TransferScreen / TopUpIdentity / WithdrawalScreen / RegisterDpnsName screens until the dedicated Send sheet (§B.7) lands. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
# Conflicts: # src/ui/components/mod.rs
# Conflicts: # tests/kittest/main.rs
# Conflicts: # tests/kittest/main.rs
# Conflicts: # src/ui/components/mod.rs # tests/kittest/main.rs
… sections # Conflicts: # src/ui/identity/hub_screen.rs # tests/kittest/main.rs
…r tab entry The Contacts populated shell previously dispatched `DashPayTask::LoadContacts` on every paint — flooding the backend channel and hammering the SDK. Introduce `ContactsState` owned by the hub with a `load_requested` flag set on first dispatch; reset on tab switch or `refresh_on_arrival`. This keeps the dispatch additive (no new backend task variant) while making it safe to paint. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…cts gate deep link Introduce a feature-gated `AppAction::SwitchIdentityHubTab` variant that lets in-hub deep links hop between sub-tabs through the normal action-dispatch channel, rather than coupling sibling tabs to each other. `AppState::update` resolves the currently-visible screen and, when it is the Identity Hub, forwards the tab switch via `IdentityHubScreen::select_tab`. Wire the Contacts tab's social-profile gate card to emit `SwitchIdentityHubTab(Settings)` when the user clicks the primary CTA — that is where display name and avatar editing lives. The T8 Home-tab "See all activity" link already hops tabs synchronously via `HomeOutcome`, so no additional deep-link plumbing is needed there. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
T9 shipped the `RequestCard` component with accept/decline/cancel response flags, but the contacts tab currently renders only empty-state placeholder copy — there is no request data feeding the component yet. Document where the wiring belongs: `AcceptContactRequest` / `RejectContactRequest` backend variants exist; a `CancelContactRequest` variant does not and is explicitly deferred to a later wave per integration constraints. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Add Identity Hub components section to `src/ui/components/README.md` covering the nine new components shipped in Wave 1: `IdentityHubTabBar`, `IdentityHeroCard`, `OnboardingChecklist`, `IdentityPickerCard`, `IdentityPickerAddCard`, `SocialProfileGateCard`, `RequestCard`, `ContactRow`, `ActivityRow`. - Flip IDH-002 (Home at a glance) and IDH-004 (social-profile opt-in) from `[Gap]` to `[Implemented]`. IDH-003 and IDH-006 remain `[Gap]` because the breadcrumb switcher composition and the unified activity aggregator are deferred. IDH-005 (dev bulk creation) unchanged — still gap. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 20
🧹 Nitpick comments (4)
src/ui/components/activity_row.rs (2)
220-238: Route component styling throughComponentStyles.The row handles dark mode, but it computes surface/stroke styling directly with
DashColors. Please align this with the shared component styling layer so component theming stays centralized.As per coding guidelines,
src/ui/components/**/*.rs: “UI components must ... support both light and dark mode viaComponentStyles”.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/ui/components/activity_row.rs` around lines 220 - 238, The component currently computes surface and stroke using DashColors (in functions like accent_color, and the block creating surface, stroke, and Frame) which bypasses the shared theming layer; update the code to take and use the shared ComponentStyles (or an existing self.styles) for color and stroke values instead of DashColors so theming is centralized—use the styles' surface/background color for Frame.fill, use a styles.border or styles.card.stroke for the normal stroke, and use a styles.error-border (or equivalent) when matches!(self.status, ActivityRowStatus::Failed); keep the existing Frame construction (fill, stroke, corner_radius, margins, shadow) but source the color and stroke values from ComponentStyles rather than DashColors or hard-coded calls like DashColors::surface/DashColors::border_light/DashColors::ERROR.
75-82: Keep response flags encapsulated to preserve the action invariant.
clicked_bodyandclicked_retryare public mutable fields, whileactionis derived from them once. Downstream mutation can make these fields disagree withhas_changed()/changed_value(). Prefer private fields plus accessors.♻️ Proposed API tightening
pub struct ActivityRowResponse { /// Whether the user clicked the row body or the expand chevron. - pub clicked_body: bool, + clicked_body: bool, /// Whether the user clicked the `Retry` button (only ever `true` for /// `Failed` rows). - pub clicked_retry: bool, + clicked_retry: bool, /// The resolved action, if any. action: Option<ActivityRowAction>, }impl ActivityRowResponse { + /// Whether the user clicked the row body or the expand chevron. + pub fn clicked_body(&self) -> bool { + self.clicked_body + } + + /// Whether the user clicked the `Retry` button. + pub fn clicked_retry(&self) -> bool { + self.clicked_retry + } + /// The resolved action, if any. pub fn action(&self) -> Option<ActivityRowAction> { self.action } }As per coding guidelines,
src/ui/components/**/*.rs: “UI components must ... avoid public mutable fields and eager initialization”.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/ui/components/activity_row.rs` around lines 75 - 82, Make clicked_body and clicked_retry private on ActivityRowResponse and replace direct field access with accessor methods (e.g., pub fn clicked_body(&self) -> bool and pub fn clicked_retry(&self) -> bool) and controlled setters if mutation is needed; keep action private and stop eager initialization by computing or updating action inside changed_value()/has_changed() or inside the setters so the invariant (action matches flags) cannot be violated by downstream mutation. Update any call sites that read or write clicked_body/clicked_retry to use the new accessors/setters and ensure has_changed() and changed_value() derive their result from the authoritative state (computed or maintained by the setters) rather than stale public fields.tests/kittest/identity_hub.rs (1)
61-72: Make this guard exercisecreate_screen.This test only compares
ScreenType::IdentityHubto itself, so dropping theIdentityHubarm fromScreenType::create_screenwould not fail here. Either callScreenType::IdentityHub.create_screen(...)with a liveAppContextand match the returned screen variant, or rename this as an enum-availability guard.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/kittest/identity_hub.rs` around lines 61 - 72, The current test only self-compares ScreenType::IdentityHub and doesn't exercise ScreenType::create_screen; update the test (identity_hub_screen_type_creates_hub_screen) to construct a minimal/live AppContext, call ScreenType::IdentityHub.create_screen(&app_context) and assert the returned Screen is the IdentityHub variant (or pattern-match to ensure it yields the identity hub Screen), or alternatively rename the test to indicate it only checks enum availability; reference ScreenType::IdentityHub, create_screen, AppContext and the related identity_hub_mounts_and_renders test when implementing the minimal context.src/ui/components/identity_picker_card.rs (1)
241-247: Avoid stringly-typed dispatch betweenbadge_labelanddraw_type_badge.
badge_label()mapsIdentityType→&'static str, and thendraw_type_badgere-matches on those exact literals ("Masternode","Evonode") to pick fill/stroke/text colors. If a newIdentityTypevariant is added, the label text is ever changed, or it gets localized downstream, the color branch will silently fall through to the default (user) palette with no compile-time signal.Pass the
IdentityType(or a small localBadgeKind) through to the drawing helper so the palette is driven by the type, not by stringly comparing the visible label.♻️ Suggested refactor
- // Top row: avatar (left) + badge (right). - ui.horizontal(|ui| { - draw_monogram(ui, &heading, self.display_name.is_some(), dark_mode); - ui.add_space(8.0); - ui.with_layout(egui::Layout::right_to_left(egui::Align::TOP), |ui| { - draw_type_badge(ui, badge_label, dark_mode); - }); - }); + // Top row: avatar (left) + badge (right). + ui.horizontal(|ui| { + draw_monogram(ui, &heading, self.display_name.is_some(), dark_mode); + ui.add_space(8.0); + ui.with_layout(egui::Layout::right_to_left(egui::Align::TOP), |ui| { + draw_type_badge(ui, self.identity_type, badge_label, dark_mode); + }); + }); @@ -fn draw_type_badge(ui: &mut Ui, label: &str, dark_mode: bool) { - let (fill, stroke_color) = match label { - "Masternode" => (DashColors::PLATFORM_PURPLE, DashColors::PLATFORM_PURPLE), - "Evonode" => (DashColors::DASH_BLUE, DashColors::DASH_BLUE), - _ => ( - DashColors::surface_elevated(dark_mode), - DashColors::border(dark_mode), - ), - }; - let text_color = if matches!(label, "Masternode" | "Evonode") { - Color32::WHITE - } else { - DashColors::text_primary(dark_mode) - }; +fn draw_type_badge(ui: &mut Ui, kind: IdentityType, label: &str, dark_mode: bool) { + let (fill, stroke_color, text_color) = match kind { + IdentityType::Masternode => ( + DashColors::PLATFORM_PURPLE, + DashColors::PLATFORM_PURPLE, + Color32::WHITE, + ), + IdentityType::Evonode => ( + DashColors::DASH_BLUE, + DashColors::DASH_BLUE, + Color32::WHITE, + ), + IdentityType::User => ( + DashColors::surface_elevated(dark_mode), + DashColors::border(dark_mode), + DashColors::text_primary(dark_mode), + ), + };Also applies to: 405-428
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/ui/components/identity_picker_card.rs` around lines 241 - 247, badge_label currently returns a & 'static str and draw_type_badge re-matches on those literals; change draw_type_badge to accept the IdentityType (or add a small local enum BadgeKind) instead of a label string and drive color selection by matching on that type (match on IdentityType::User / ::Masternode / ::Evonode) so adding/localizing variants won't break palette selection; keep badge_label solely for the displayed text and update all call sites (including the other occurrence around the 405-428 region) to pass the IdentityType/BadgeKind to draw_type_badge while still using badge_label() for the rendered label.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@docs/user-stories.md`:
- Line 15: The TOC entry "[Identities Hub (IDH)](`#identities-hub-idh`)" is out of
sync with the actual "## Identities Hub (IDH)" section placement; either move
the "## Identities Hub (IDH)" section up so it appears before Token Operations
to match the TOC or move the TOC entry to the bottom so it matches the section
order, and ensure the section is separated with the standard '---' delimiter
consistent with other sections.
In `@src/ui/components/activity_row.rs`:
- Around line 401-458: The test ut_activity_row_01_failed_row_has_retry_button
currently only checks the Retry button and labels; add an assertion that the
Failed variant renders the danger border color by verifying the failed row's
rendered border/stroke equals DashColors::ERROR (inspect the failed_response
render metadata or query the harness for the element matching the failed row
label and assert its border color/stroke/style is DashColors::ERROR). Locate
this check around the existing failed_response/has_retry assertions (references:
ut_activity_row_01_failed_row_has_retry_button, ActivityRowStatus::Failed,
ActivityRow::has_retry, DashColors::ERROR) and fail the test if the border color
differs.
In `@src/ui/components/contact_row.rs`:
- Around line 20-34: Add an impl of the shared ComponentResponse trait for
ContactRowResponse (matching the pattern used by other identity hub components)
and implement its required methods so the component can be routed uniformly;
specifically implement changed_value to return Some(contact_id.clone()) when any
of clicked, send_clicked, or overflow_clicked is true, and return None otherwise
(also ensure any other trait methods follow the same semantics used by existing
components and reference ContactRowResponse and ContactRow::show for where
responses are produced).
In `@src/ui/components/identity_hero_card.rs`:
- Around line 244-268: with_avatar_bytes currently flips
avatar_uses_initials_fallback by setting avatar_bytes although the rendering
pipeline still paints initials; change the state so tests reflect actual
renderability: instead of directly setting avatar_bytes in with_avatar_bytes,
store the incoming bytes in a pending field (e.g., avatar_pending_bytes) or set
a separate avatar_texture_ready flag and only mark avatar_bytes (or
avatar_texture_ready = true) when the texture pipeline reports the image is
ready; update avatar_uses_initials_fallback to check the readiness flag (e.g.,
has_social_profile() && avatar_texture_ready is false) rather than mere presence
of raw bytes; apply the same change to the duplicate logic referenced at lines
441-449 so state and rendering stay consistent.
In `@src/ui/components/onboarding_checklist.rs`:
- Around line 247-304: The row is only clickable via the label; update
paint_step_row so the entire horizontal row area (including the circle and
padding) uses a single Sense::click() and drive clicked from that response
instead of label_resp. Follow the pattern in contact_row.rs: obtain the row
rect/response (e.g., call ui.allocate_rect or use
ui.interact/ui.allocate_exact_size for the full horizontal area or reuse the
rect that contains the circle and extend it to the row width), replace the
label's Sense::click() with a non-click label, and set clicked =
row_response.clicked() (keep the existing circle drawing and visual logic
intact).
In `@src/ui/components/request_card.rs`:
- Around line 49-59: RequestCardResponse currently exposes ad-hoc public
booleans; change it to a private-field struct that implements the
ComponentResponse trait by exposing a single typed action via changed_value()
and accessor for the id; introduce a RequestAction (e.g., Accepted, Declined,
Cancelled, None) enum and replace accepted/declined/cancelled booleans with a
single action field inside RequestCardResponse, make fields private, implement
ComponentResponse for RequestCardResponse with changed_value() returning
Option<RequestAction>, and update RequestCard::show (and any call sites) to
construct and return the new RequestCardResponse so callers consume the action
uniformly like other UI components.
In `@src/ui/components/social_profile_gate_card.rs`:
- Around line 49-56: SocialProfileGateCardResponse must implement the shared
ComponentResponse trait so it can be handled like other hub components; add an
impl ComponentResponse for SocialProfileGateCardResponse that forwards the
trait's primary/auxiliary query methods to this struct’s fields (map the trait's
primary/action accessor to primary_clicked and expose the “Why?” toggle via the
trait’s auxiliary/flag accessor or equivalent), and update any call sites
expecting a ComponentResponse to accept SocialProfileGateCardResponse.
In `@src/ui/identity/contacts.rs`:
- Around line 45-50: ContactsState currently only has load_requested and the
search query is recreated each render causing typed text to disappear; add a
persistent string field (e.g., search or search_query) to ContactsState and
initialize it in the Default/derive so it survives renders, then replace the
ephemeral local `search` variable used in the UI render/handler code with
ContactsState::search (update places that read/write the query — likely in the
contacts view and its input handlers around the code that previously created
`search` each render, and in the tab state usage referenced near the 196-205
region) so the input bindings update and read the stored field instead of a
transient local.
In `@src/ui/identity/home.rs`:
- Around line 96-104: The code currently chooses the hero identity by calling
first_loaded_identity(app_context), which ignores a hub-selected identity;
change the logic to first check the hub-selected identity (the value the picker
sets on the hub via app_context) and use that if present, falling back to
first_loaded_identity(app_context) only when no hub selection exists; update the
same pattern used later (around the block referenced at 417-422) so both the
hero and action handlers use the hub-selected identity from app_context instead
of always using first_loaded_identity, and preserve the render_empty(ui,
dark_mode) fallback when neither is available.
- Around line 215-222: The UI currently opens DPNS username registration when
paint_social_profile_card returns true; change the navigation to push the
profile editor instead so the card (which asks for display name, bio, avatar)
leads to the profile editor. Replace the
AppAction::AddScreen(ScreenType::RegisterDpnsName(RegisterDpnsNameSource::Identities).create_screen(app_context))
call with an AddScreen that creates the profile editor screen (use the existing
ScreenType variant/method for editing profiles — e.g., ScreenType::EditProfile
or ProfileEditor.create_screen(app_context) as appropriate), and apply the same
replacement at the other occurrence referenced (lines ~259-263) so both
no-profile card flows route to the profile editor rather than RegisterDpnsName;
ensure no checklist action (SetDisplayName) is toggled to skipped by this
change.
In `@src/ui/identity/hub_screen.rs`:
- Around line 149-214: The match arm for HubLanding::Home | HubLanding::Picker
is discarding the AppAction returned by the selected tab and also lumps Picker
with Home, making picker unreachable; change the logic so HubLanding::Picker is
handled separately (so the picker grid can be rendered) and when rendering
selected tab (IdentityHubTab::Home/Contacts/Activity/Settings) capture and
return the tab's AppAction instead of always returning AppAction::None, while
preserving existing state-reset behavior when selected_tab changes (references:
HubLanding, self.selected_tab, IdentityHubTabBar::new(...).show,
super::home::render, super::home::apply_outcome, self.contacts_state.reset, and
self.settings_tab.render).
In `@src/ui/identity/picker.rs`:
- Around line 135-138: compute_column_count currently divides available_width by
(CARD_MIN_WIDTH + GRID_GAP) which undercounts when the last card doesn't need a
trailing gap; update the calculation to add GRID_GAP to available_width before
dividing so the last column is allowed without a trailing gap (e.g. compute
columns as ((available_width + GRID_GAP) / (CARD_MIN_WIDTH + GRID_GAP)).floor()
as usize) while keeping the min of 1; adjust the function using the existing
names compute_column_count, CARD_MIN_WIDTH, and GRID_GAP.
- Around line 124-127: The code currently writes captured_selection (which can
be None) into selected_id_out on every render; only set the caller's slot when a
real click produced a non-None selection. Change the logic around
selected_id_out and captured_selection so you only assign when
captured_selection is Some(value) (e.g. if let (Some(slot), Some(selection)) =
(selected_id_out, captured_selection) { *slot = selection; }) or guard with
captured_selection.is_some() before dereferencing selected_id_out, thereby
avoiding overwriting the caller's existing selection with None.
In `@src/ui/identity/settings.rs`:
- Around line 647-651: The ensure_selected method currently always picks the
first local identity from app_context.load_local_qualified_identities(); change
it to prefer the hub-selected (active) identity id threaded into SettingsTab:
add an active_identity_id field to SettingsTab (or accept it as a parameter),
use that id to look up the matching identity from app_context (e.g., resolve via
load_local_qualified_identities() then find(|id| id.id == active_identity_id)),
set that as incoming when present, and only fall back to
identities.first().cloned() if no match is found; update callers that construct
SettingsTab or call ensure_selected to pass the active id.
- Around line 128-130: Several methods on SettingsTab (e.g., render, and the
other methods at the indicated ranges) have their parameters ordered as (&mut
self, ui: &mut Ui, app_context: &Arc<AppContext>) — change these to place
app_context immediately after self (i.e., &mut self, app_context:
&Arc<AppContext>, ui: &mut Ui); update the function signatures for render and
the other affected methods (refer to the SettingsTab methods around the noted
ranges) and then update every internal call sites (including ensure_selected and
any callers) to pass app_context as the first argument after self to keep the
repository convention consistent.
- Around line 287-302: The form is being marked clean immediately after
dispatching AppAction::BackendTask(DashPayTask::UpdateProfile), which prevents
retry if the backend fails; remove the immediate assignments to
self.original_display_name, self.original_bio, and self.original_avatar_url from
the save.clicked() branch and instead update those original_* fields only after
the UpdateProfile task completes successfully (e.g., in the code path that
handles the backend response or profile refresh), so the save button remains
enabled until a successful backend confirmation.
- Around line 232-263: The UI counters currently pass byte lengths (using
.len()) to counter(ui, ...) while validation_error() uses .chars().count(),
causing mismatch for non-ASCII input; update the three counter calls for
self.edit_display_name, self.edit_bio, and self.edit_avatar_url to use
.chars().count() so counter(...) and validation_error() use the same
character-counting logic (keep MAX_DISPLAY_NAME, MAX_BIO, MAX_AVATAR_URL and the
counter function unchanged).
In `@tests/kittest/identity_hub_activity.rs`:
- Around line 56-68: Add an assertion for the missing "Platform" filter chip so
the test verifies all four chips; locate the block that calls
harness.query_by_label("All"), ("Payments"), ("Funding") in the
identity_hub_activity test and add a similar assert for
harness.query_by_label("Platform"). Use the same assert!(...is_some(), "... must
render the `Platform` filter chip") pattern and message to match existing
assertions (refer to harness.query_by_label and the surrounding Activity tab
filter assertions).
In `@tests/kittest/identity_hub_home.rs`:
- Around line 31-44: The helper mount_hub leaves the welcome screen enabled so
AppState::update() renders welcome_screen.ui instead of the selected RootScreen,
preventing IdentityHubScreen from being exercised; fix by disabling the welcome
screen in the harness setup (e.g., set app.show_welcome_screen = false or call
the equivalent setter on dash_evo_tool::app::AppState before returning app in
mount_hub) and apply the same change to the other helper block at 55-63.
In `@tests/kittest/identity_hub_settings.rs`:
- Around line 31-45: mount_hub_on_settings currently creates AppState via
AppState::new which may leave show_welcome_screen true and never selects
IdentityHubTab::Settings, so the test renders the onboarding surface instead of
the Settings tab; fix by mirroring the onboarding test’s welcome bypass after
creating the app (e.g., set app.show_welcome_screen = false or call the same
helper used in onboarding) and explicitly set the hub tab to
IdentityHubTab::Settings (or set app.selected_identity_hub_tab =
IdentityHubTab::Settings) before calling harness.run_steps so the Settings tab
is actually mounted in mount_hub_on_settings.
---
Nitpick comments:
In `@src/ui/components/activity_row.rs`:
- Around line 220-238: The component currently computes surface and stroke using
DashColors (in functions like accent_color, and the block creating surface,
stroke, and Frame) which bypasses the shared theming layer; update the code to
take and use the shared ComponentStyles (or an existing self.styles) for color
and stroke values instead of DashColors so theming is centralized—use the
styles' surface/background color for Frame.fill, use a styles.border or
styles.card.stroke for the normal stroke, and use a styles.error-border (or
equivalent) when matches!(self.status, ActivityRowStatus::Failed); keep the
existing Frame construction (fill, stroke, corner_radius, margins, shadow) but
source the color and stroke values from ComponentStyles rather than DashColors
or hard-coded calls like
DashColors::surface/DashColors::border_light/DashColors::ERROR.
- Around line 75-82: Make clicked_body and clicked_retry private on
ActivityRowResponse and replace direct field access with accessor methods (e.g.,
pub fn clicked_body(&self) -> bool and pub fn clicked_retry(&self) -> bool) and
controlled setters if mutation is needed; keep action private and stop eager
initialization by computing or updating action inside
changed_value()/has_changed() or inside the setters so the invariant (action
matches flags) cannot be violated by downstream mutation. Update any call sites
that read or write clicked_body/clicked_retry to use the new accessors/setters
and ensure has_changed() and changed_value() derive their result from the
authoritative state (computed or maintained by the setters) rather than stale
public fields.
In `@src/ui/components/identity_picker_card.rs`:
- Around line 241-247: badge_label currently returns a & 'static str and
draw_type_badge re-matches on those literals; change draw_type_badge to accept
the IdentityType (or add a small local enum BadgeKind) instead of a label string
and drive color selection by matching on that type (match on IdentityType::User
/ ::Masternode / ::Evonode) so adding/localizing variants won't break palette
selection; keep badge_label solely for the displayed text and update all call
sites (including the other occurrence around the 405-428 region) to pass the
IdentityType/BadgeKind to draw_type_badge while still using badge_label() for
the rendered label.
In `@tests/kittest/identity_hub.rs`:
- Around line 61-72: The current test only self-compares ScreenType::IdentityHub
and doesn't exercise ScreenType::create_screen; update the test
(identity_hub_screen_type_creates_hub_screen) to construct a minimal/live
AppContext, call ScreenType::IdentityHub.create_screen(&app_context) and assert
the returned Screen is the IdentityHub variant (or pattern-match to ensure it
yields the identity hub Screen), or alternatively rename the test to indicate it
only checks enum availability; reference ScreenType::IdentityHub, create_screen,
AppContext and the related identity_hub_mounts_and_renders test when
implementing the minimal context.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 6d381ea3-fb66-4dcc-a1b9-9a8ed9cac583
📒 Files selected for processing (33)
Cargo.tomldocs/ai-design/2026-04-23-identity-hub-impl/02-ux-plan.mddocs/ai-design/2026-04-23-identity-hub-impl/03-test-case-spec.mddocs/ai-design/2026-04-23-identity-hub-impl/04-dev-plan.mddocs/user-stories.mdsrc/app.rssrc/ui/components/README.mdsrc/ui/components/activity_row.rssrc/ui/components/breadcrumb_pill.rssrc/ui/components/contact_row.rssrc/ui/components/identity_hero_card.rssrc/ui/components/identity_hub_tab_bar.rssrc/ui/components/identity_picker_add_card.rssrc/ui/components/identity_picker_card.rssrc/ui/components/identity_pill.rssrc/ui/components/mod.rssrc/ui/components/onboarding_checklist.rssrc/ui/components/request_card.rssrc/ui/components/social_profile_gate_card.rssrc/ui/identity/activity.rssrc/ui/identity/contacts.rssrc/ui/identity/home.rssrc/ui/identity/hub_screen.rssrc/ui/identity/mod.rssrc/ui/identity/picker.rssrc/ui/identity/settings.rstests/kittest/identity_hub.rstests/kittest/identity_hub_activity.rstests/kittest/identity_hub_contacts.rstests/kittest/identity_hub_home.rstests/kittest/identity_hub_onboarding.rstests/kittest/identity_hub_settings.rstests/kittest/main.rs
✅ Files skipped from review due to trivial changes (5)
- tests/kittest/main.rs
- src/ui/components/README.md
- docs/ai-design/2026-04-23-identity-hub-impl/04-dev-plan.md
- src/ui/identity/mod.rs
- docs/ai-design/2026-04-23-identity-hub-impl/03-test-case-spec.md
🚧 Files skipped from review as they are similar to previous changes (1)
- Cargo.toml
| - [Identity Operations (IDN)](#identity-operations-idn) | ||
| - [DPNS (DPN)](#dpns-dpn) | ||
| - [DashPay (DPY)](#dashpay-dpy) | ||
| - [Identities Hub (IDH)](#identities-hub-idh) |
There was a problem hiding this comment.
Keep the TOC order and section placement in sync.
The TOC places “Identities Hub” before Token Operations, but the actual ## Identities Hub (IDH) section is appended after Programmatic Access and is not separated by the usual ---. Move the section to match the TOC order, or move the TOC entry to the bottom with the section.
Also applies to: 1053-1053
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@docs/user-stories.md` at line 15, The TOC entry "[Identities Hub
(IDH)](`#identities-hub-idh`)" is out of sync with the actual "## Identities Hub
(IDH)" section placement; either move the "## Identities Hub (IDH)" section up
so it appears before Token Operations to match the TOC or move the TOC entry to
the bottom so it matches the section order, and ensure the section is separated
with the standard '---' delimiter consistent with other sections.
| /// UT-ACTIVITY-ROW-01 — Failed row renders a Retry button and a | ||
| /// danger-stroke border. | ||
| /// | ||
| /// The test covers three variants in a single harness run so we | ||
| /// exercise the Normal, Expanded, and Failed render paths together, | ||
| /// as called out in the test-case spec. | ||
| #[test] | ||
| fn ut_activity_row_01_failed_row_has_retry_button() { | ||
| let mut harness = Harness::builder() | ||
| .with_size(egui::vec2(480.0, 400.0)) | ||
| .build_ui(|ui| { | ||
| let mut normal = ActivityRow::new(ActivityRowKind::Payment, "Sent 0.1 DASH") | ||
| .with_subtitle("To @alice") | ||
| .with_timestamp("5 min ago"); | ||
| let normal_response = normal.show(ui); | ||
| assert!(normal_response.inner.action().is_none()); | ||
|
|
||
| let mut expanded = ActivityRow::new(ActivityRowKind::Funding, "Added funds") | ||
| .with_subtitle("From wallet") | ||
| .with_timestamp("1 h ago") | ||
| .with_status(ActivityRowStatus::Expanded) | ||
| .with_detail("Advanced details."); | ||
| let expanded_response = expanded.show(ui); | ||
| assert!(expanded_response.inner.action().is_none()); | ||
|
|
||
| let mut failed = | ||
| ActivityRow::new(ActivityRowKind::Payment, "Could not send 0.1 DASH to @bob") | ||
| .with_timestamp("just now") | ||
| .with_status(ActivityRowStatus::Failed) | ||
| .with_detail( | ||
| "The network did not accept this payment. \ | ||
| Your balance is unchanged. Check your connection \ | ||
| and try again, or try a smaller amount.", | ||
| ); | ||
| let failed_response = failed.show(ui); | ||
| assert!(failed.has_retry()); | ||
| // No interaction simulated, so no action yet. | ||
| assert!(failed_response.inner.action().is_none()); | ||
| }); | ||
| harness.run(); | ||
|
|
||
| // The Retry button must be present — it is the distinguishing | ||
| // affordance of the Failed variant. | ||
| assert!( | ||
| harness.query_by_label("Retry").is_some(), | ||
| "Failed row must render a Retry button" | ||
| ); | ||
| // Normal and Expanded titles must render. | ||
| assert!(harness.query_by_label("Sent 0.1 DASH").is_some()); | ||
| assert!(harness.query_by_label("Added funds").is_some()); | ||
| assert!( | ||
| harness | ||
| .query_by_label("Could not send 0.1 DASH to @bob") | ||
| .is_some() | ||
| ); | ||
| // The expanded detail text must be visible. | ||
| assert!(harness.query_by_label("Advanced details.").is_some()); | ||
| } |
There was a problem hiding this comment.
Assert the danger border promised by this test case.
UT-ACTIVITY-ROW-01 requires both the Retry button and danger-colored border, but this test only verifies the Retry button and labels. A regression from the failed-row DashColors::ERROR border to the default border would still pass.
🧪 Proposed coverage improvement
impl ActivityRow {
+ fn border_stroke(&self, dark_mode: bool) -> Stroke {
+ if matches!(self.status, ActivityRowStatus::Failed) {
+ Stroke::new(1.0, DashColors::ERROR)
+ } else {
+ Stroke::new(1.0, DashColors::border_light(dark_mode))
+ }
+ }
+
fn accent_color(&self, _dark_mode: bool) -> Color32 {
match (self.status, self.kind) {
(ActivityRowStatus::Failed, _) => DashColors::ERROR,- let stroke = if matches!(self.status, ActivityRowStatus::Failed) {
- Stroke::new(1.0, DashColors::ERROR)
- } else {
- Stroke::new(1.0, DashColors::border_light(dark_mode))
- };
+ let stroke = self.border_stroke(dark_mode); let mut failed =
ActivityRow::new(ActivityRowKind::Payment, "Could not send 0.1 DASH to `@bob`")
.with_timestamp("just now")
.with_status(ActivityRowStatus::Failed)
.with_detail(
"The network did not accept this payment. \
Your balance is unchanged. Check your connection \
and try again, or try a smaller amount.",
);
+ assert_eq!(failed.border_stroke(false).color, DashColors::ERROR);
let failed_response = failed.show(ui);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/ui/components/activity_row.rs` around lines 401 - 458, The test
ut_activity_row_01_failed_row_has_retry_button currently only checks the Retry
button and labels; add an assertion that the Failed variant renders the danger
border color by verifying the failed row's rendered border/stroke equals
DashColors::ERROR (inspect the failed_response render metadata or query the
harness for the element matching the failed row label and assert its border
color/stroke/style is DashColors::ERROR). Locate this check around the existing
failed_response/has_retry assertions (references:
ut_activity_row_01_failed_row_has_retry_button, ActivityRowStatus::Failed,
ActivityRow::has_retry, DashColors::ERROR) and fail the test if the border color
differs.
| /// Response returned by [`ContactRow::show`]. Carries click state and echoes | ||
| /// the contact identifier so the caller can route without a parallel index. | ||
| #[derive(Clone, Debug, Default, PartialEq, Eq)] | ||
| pub struct ContactRowResponse { | ||
| /// The row body (avatar + name + handle region) was clicked. Routes to | ||
| /// the contact detail drawer in the hub. | ||
| pub clicked: bool, | ||
| /// The inline `Send` button was clicked. | ||
| pub send_clicked: bool, | ||
| /// The `•••` overflow icon button was clicked. | ||
| pub overflow_clicked: bool, | ||
| /// The contact identifier supplied by the caller, echoed back so the | ||
| /// click routing never depends on the list index. | ||
| pub contact_id: Option<String>, | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Implement ComponentResponse for ContactRowResponse.
The row returns a custom response but does not implement the shared component response contract, unlike the other Identity Hub components. Key changed_value to the contact_id when any row action fires.
♻️ Proposed fix
+use crate::ui::components::component_trait::ComponentResponse;
use crate::ui::theme::{ComponentStyles, DashColors, Shape};
use eframe::egui::{CornerRadius, Frame, Margin, RichText, Sense, Stroke, Ui, Vec2};
@@
pub struct ContactRowResponse {
@@
/// click routing never depends on the list index.
pub contact_id: Option<String>,
+ changed_value: Option<String>,
}
+
+impl ComponentResponse for ContactRowResponse {
+ type DomainType = String;
+
+ fn has_changed(&self) -> bool {
+ self.clicked || self.send_clicked || self.overflow_clicked
+ }
+
+ fn is_valid(&self) -> bool {
+ true
+ }
+
+ fn changed_value(&self) -> &Option<Self::DomainType> {
+ &self.changed_value
+ }
+
+ fn error_message(&self) -> Option<&str> {
+ None
+ }
+}
@@
- response
+ if response.has_changed() {
+ response.changed_value = response.contact_id.clone();
+ }
+
+ response
}
}As per coding guidelines, src/ui/components/**/*.rs: “UI components must follow lazy initialization pattern: domain data in fields like amount: Option<Amount>, UI components in fields like amount_widget: Option<AmountInput>, provide builder methods for configuration, implement ComponentResponse trait, support both light and dark mode via ComponentStyles, and avoid public mutable fields and eager initialization”.
Also applies to: 75-144
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/ui/components/contact_row.rs` around lines 20 - 34, Add an impl of the
shared ComponentResponse trait for ContactRowResponse (matching the pattern used
by other identity hub components) and implement its required methods so the
component can be routed uniformly; specifically implement changed_value to
return Some(contact_id.clone()) when any of clicked, send_clicked, or
overflow_clicked is true, and return None otherwise (also ensure any other trait
methods follow the same semantics used by existing components and reference
ContactRowResponse and ContactRow::show for where responses are produced).
| /// Attach raw avatar bytes (PNG / JPEG). When present, they override the | ||
| /// initials fallback. | ||
| pub fn with_avatar_bytes(mut self, bytes: Vec<u8>) -> Self { | ||
| if !bytes.is_empty() { | ||
| self.avatar_bytes = Some(bytes); | ||
| } | ||
| self | ||
| } | ||
|
|
||
| /// True when the hero is in its social-profile-set variant. | ||
| pub fn has_social_profile(&self) -> bool { | ||
| self.display_name.is_some() | ||
| } | ||
|
|
||
| /// True when the hero will render the `No username yet` prompt instead of | ||
| /// a `@handle` line. | ||
| pub fn no_dpns(&self) -> bool { | ||
| self.dpns_handle.is_none() | ||
| } | ||
|
|
||
| /// True when the hero will use the initials fallback instead of an image | ||
| /// (only meaningful in the social-profile-set variant). Exposed for tests. | ||
| pub fn avatar_uses_initials_fallback(&self) -> bool { | ||
| self.has_social_profile() && self.avatar_bytes.is_none() | ||
| } |
There was a problem hiding this comment.
Keep avatar-byte state consistent with rendering.
with_avatar_bytes() makes avatar_uses_initials_fallback() return false, but the social-profile render path always paints initials. Users with avatar bytes will still see initials while tests/helpers report otherwise. Either render the texture here or keep the fallback state truthful until the texture pipeline lands.
Also applies to: 441-449
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/ui/components/identity_hero_card.rs` around lines 244 - 268,
with_avatar_bytes currently flips avatar_uses_initials_fallback by setting
avatar_bytes although the rendering pipeline still paints initials; change the
state so tests reflect actual renderability: instead of directly setting
avatar_bytes in with_avatar_bytes, store the incoming bytes in a pending field
(e.g., avatar_pending_bytes) or set a separate avatar_texture_ready flag and
only mark avatar_bytes (or avatar_texture_ready = true) when the texture
pipeline reports the image is ready; update avatar_uses_initials_fallback to
check the readiness flag (e.g., has_social_profile() && avatar_texture_ready is
false) rather than mere presence of raw bytes; apply the same change to the
duplicate logic referenced at lines 441-449 so state and rendering stay
consistent.
| /// Paint a single step row. Returns `true` when the user clicked the | ||
| /// row (regardless of completion state — callers decide whether to | ||
| /// re-route a completed step to its edit screen). | ||
| fn paint_step_row( | ||
| &self, | ||
| ui: &mut Ui, | ||
| dark_mode: bool, | ||
| step: ChecklistStep, | ||
| complete: bool, | ||
| ) -> bool { | ||
| let mut clicked = false; | ||
| ui.horizontal(|ui| { | ||
| // Circle bullet — filled + check mark for complete, outlined | ||
| // otherwise. | ||
| let size = 20.0; | ||
| let (rect, _resp) = ui.allocate_exact_size(egui::vec2(size, size), Sense::hover()); | ||
| let painter = ui.painter(); | ||
| let center = rect.center(); | ||
| let radius = size * 0.5; | ||
|
|
||
| if complete { | ||
| painter.circle_filled(center, radius, DashColors::DASH_BLUE); | ||
| // Draw a white check mark (two-line polyline). | ||
| let check_color = Color32::WHITE; | ||
| let p1 = egui::pos2(center.x - 4.0, center.y); | ||
| let p2 = egui::pos2(center.x - 1.0, center.y + 3.0); | ||
| let p3 = egui::pos2(center.x + 4.0, center.y - 3.0); | ||
| painter.line_segment([p1, p2], Stroke::new(1.8, check_color)); | ||
| painter.line_segment([p2, p3], Stroke::new(1.8, check_color)); | ||
| } else { | ||
| painter.circle_stroke( | ||
| center, | ||
| radius - 1.0, | ||
| Stroke::new(1.5, DashColors::border(dark_mode)), | ||
| ); | ||
| } | ||
|
|
||
| ui.add_space(Spacing::SM); | ||
|
|
||
| // Label — struck through when complete so the state is obvious | ||
| // even to color-blind users. | ||
| let text_color = if complete { | ||
| DashColors::text_secondary(dark_mode) | ||
| } else { | ||
| DashColors::text_primary(dark_mode) | ||
| }; | ||
| let mut rich = RichText::new(step.label()).color(text_color); | ||
| if complete { | ||
| rich = rich.strikethrough(); | ||
| } | ||
| let label_resp = ui | ||
| .add(egui::Label::new(rich).sense(Sense::click())) | ||
| .clickable_tooltip(step.tooltip()); | ||
| if label_resp.clicked() { | ||
| clicked = true; | ||
| } | ||
| }); | ||
| clicked |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, locate the file and check its existence
find . -type f -name "onboarding_checklist.rs" | head -5Repository: dashpay/dash-evo-tool
Length of output: 108
🏁 Script executed:
# Get the file to understand the context and verify the code location
wc -l src/ui/components/onboarding_checklist.rsRepository: dashpay/dash-evo-tool
Length of output: 110
🏁 Script executed:
# Read the specific section mentioned (lines 247-304) and surrounding context
sed -n '240,310p' src/ui/components/onboarding_checklist.rsRepository: dashpay/dash-evo-tool
Length of output: 2576
🏁 Script executed:
# Check egui version in Cargo.toml
find . -name "Cargo.toml" -type f -exec grep -l "egui" {} \; | head -3Repository: dashpay/dash-evo-tool
Length of output: 77
🏁 Script executed:
# Look for scope_builder usage in the codebase to verify API availability
rg "scope_builder" --type rustRepository: dashpay/dash-evo-tool
Length of output: 309
🏁 Script executed:
# Search for similar row/item click patterns in UI components
rg "ui\.horizontal" src/ui/components/ -A 10 | grep -E "(horizontal|clicked|Sense::click)" | head -30Repository: dashpay/dash-evo-tool
Length of output: 2646
🏁 Script executed:
# Check the contact_row.rs implementation to see how scope_builder is used there
grep -B 5 -A 15 "scope_builder" src/ui/components/contact_row.rs | head -40Repository: dashpay/dash-evo-tool
Length of output: 1379
🏁 Script executed:
# Verify the egui version to ensure scope_builder is available
grep -A 5 "egui" Cargo.tomlRepository: dashpay/dash-evo-tool
Length of output: 451
🏁 Script executed:
# Check if there are any other instances in onboarding_checklist.rs where this pattern should be consistent
grep -n "scope_builder\|ui.horizontal" src/ui/components/onboarding_checklist.rsRepository: dashpay/dash-evo-tool
Length of output: 134
🏁 Script executed:
# Let's look at the full context of the onboarding_checklist paint_step_row method
sed -n '245,310p' src/ui/components/onboarding_checklist.rsRepository: dashpay/dash-evo-tool
Length of output: 2492
🏁 Script executed:
# Check how the paint_step_row method is called to understand the contract better
grep -B 3 -A 3 "paint_step_row" src/ui/components/onboarding_checklist.rs | grep -v "^--$"Repository: dashpay/dash-evo-tool
Length of output: 625
🏁 Script executed:
# Let's look at the full contact_row.rs implementation of this pattern to verify the fix approach
sed -n '100,150p' src/ui/components/contact_row.rsRepository: dashpay/dash-evo-tool
Length of output: 2186
🏁 Script executed:
# Check if there are similar issues in other components (rows that should be fully clickable but aren't)
rg "Sense::click" src/ui/components/ -B 3 -A 1 | grep -E "(Label::new|horizontal)" | head -20Repository: dashpay/dash-evo-tool
Length of output: 458
Make the whole checklist row clickable.
The documentation states the row activates the step, but only the text label has Sense::click(); clicking the circle or row padding does nothing and shrinks the hit target, reducing accessibility and affordance.
Proposed fix
- ui.horizontal(|ui| {
+ let row_response =
+ ui.scope_builder(egui::UiBuilder::new().sense(Sense::click()), |ui| {
+ ui.horizontal(|ui| {
// Circle bullet — filled + check mark for complete, outlined
// otherwise.
@@
if label_resp.clicked() {
clicked = true;
}
- });
+ });
+ })
+ .response;
+ if row_response.clicked() {
+ clicked = true;
+ }
clicked
}
}This pattern is already established in contact_row.rs for the same purpose (WCAG 2.4.11 large click targets).
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| /// Paint a single step row. Returns `true` when the user clicked the | |
| /// row (regardless of completion state — callers decide whether to | |
| /// re-route a completed step to its edit screen). | |
| fn paint_step_row( | |
| &self, | |
| ui: &mut Ui, | |
| dark_mode: bool, | |
| step: ChecklistStep, | |
| complete: bool, | |
| ) -> bool { | |
| let mut clicked = false; | |
| ui.horizontal(|ui| { | |
| // Circle bullet — filled + check mark for complete, outlined | |
| // otherwise. | |
| let size = 20.0; | |
| let (rect, _resp) = ui.allocate_exact_size(egui::vec2(size, size), Sense::hover()); | |
| let painter = ui.painter(); | |
| let center = rect.center(); | |
| let radius = size * 0.5; | |
| if complete { | |
| painter.circle_filled(center, radius, DashColors::DASH_BLUE); | |
| // Draw a white check mark (two-line polyline). | |
| let check_color = Color32::WHITE; | |
| let p1 = egui::pos2(center.x - 4.0, center.y); | |
| let p2 = egui::pos2(center.x - 1.0, center.y + 3.0); | |
| let p3 = egui::pos2(center.x + 4.0, center.y - 3.0); | |
| painter.line_segment([p1, p2], Stroke::new(1.8, check_color)); | |
| painter.line_segment([p2, p3], Stroke::new(1.8, check_color)); | |
| } else { | |
| painter.circle_stroke( | |
| center, | |
| radius - 1.0, | |
| Stroke::new(1.5, DashColors::border(dark_mode)), | |
| ); | |
| } | |
| ui.add_space(Spacing::SM); | |
| // Label — struck through when complete so the state is obvious | |
| // even to color-blind users. | |
| let text_color = if complete { | |
| DashColors::text_secondary(dark_mode) | |
| } else { | |
| DashColors::text_primary(dark_mode) | |
| }; | |
| let mut rich = RichText::new(step.label()).color(text_color); | |
| if complete { | |
| rich = rich.strikethrough(); | |
| } | |
| let label_resp = ui | |
| .add(egui::Label::new(rich).sense(Sense::click())) | |
| .clickable_tooltip(step.tooltip()); | |
| if label_resp.clicked() { | |
| clicked = true; | |
| } | |
| }); | |
| clicked | |
| /// Paint a single step row. Returns `true` when the user clicked the | |
| /// row (regardless of completion state — callers decide whether to | |
| /// re-route a completed step to its edit screen). | |
| fn paint_step_row( | |
| &self, | |
| ui: &mut Ui, | |
| dark_mode: bool, | |
| step: ChecklistStep, | |
| complete: bool, | |
| ) -> bool { | |
| let mut clicked = false; | |
| let row_response = | |
| ui.scope_builder(egui::UiBuilder::new().sense(Sense::click()), |ui| { | |
| ui.horizontal(|ui| { | |
| // Circle bullet — filled + check mark for complete, outlined | |
| // otherwise. | |
| let size = 20.0; | |
| let (rect, _resp) = ui.allocate_exact_size(egui::vec2(size, size), Sense::hover()); | |
| let painter = ui.painter(); | |
| let center = rect.center(); | |
| let radius = size * 0.5; | |
| if complete { | |
| painter.circle_filled(center, radius, DashColors::DASH_BLUE); | |
| // Draw a white check mark (two-line polyline). | |
| let check_color = Color32::WHITE; | |
| let p1 = egui::pos2(center.x - 4.0, center.y); | |
| let p2 = egui::pos2(center.x - 1.0, center.y + 3.0); | |
| let p3 = egui::pos2(center.x + 4.0, center.y - 3.0); | |
| painter.line_segment([p1, p2], Stroke::new(1.8, check_color)); | |
| painter.line_segment([p2, p3], Stroke::new(1.8, check_color)); | |
| } else { | |
| painter.circle_stroke( | |
| center, | |
| radius - 1.0, | |
| Stroke::new(1.5, DashColors::border(dark_mode)), | |
| ); | |
| } | |
| ui.add_space(Spacing::SM); | |
| // Label — struck through when complete so the state is obvious | |
| // even to color-blind users. | |
| let text_color = if complete { | |
| DashColors::text_secondary(dark_mode) | |
| } else { | |
| DashColors::text_primary(dark_mode) | |
| }; | |
| let mut rich = RichText::new(step.label()).color(text_color); | |
| if complete { | |
| rich = rich.strikethrough(); | |
| } | |
| let label_resp = ui | |
| .add(egui::Label::new(rich).sense(Sense::click())) | |
| .clickable_tooltip(step.tooltip()); | |
| if label_resp.clicked() { | |
| clicked = true; | |
| } | |
| }); | |
| }) | |
| .response; | |
| if row_response.clicked() { | |
| clicked = true; | |
| } | |
| clicked | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/ui/components/onboarding_checklist.rs` around lines 247 - 304, The row is
only clickable via the label; update paint_step_row so the entire horizontal row
area (including the circle and padding) uses a single Sense::click() and drive
clicked from that response instead of label_resp. Follow the pattern in
contact_row.rs: obtain the row rect/response (e.g., call ui.allocate_rect or use
ui.interact/ui.allocate_exact_size for the full horizontal area or reuse the
rect that contains the circle and extend it to the row width), replace the
label's Sense::click() with a non-click label, and set clicked =
row_response.clicked() (keep the existing circle drawing and visual logic
intact).
| if save.clicked() && can_save { | ||
| action = AppAction::BackendTask(BackendTask::DashPayTask(Box::new( | ||
| DashPayTask::UpdateProfile { | ||
| identity: identity.clone(), | ||
| display_name: string_if_set(&self.edit_display_name), | ||
| bio: string_if_set(&self.edit_bio), | ||
| avatar_url: string_if_set(&self.edit_avatar_url), | ||
| }, | ||
| ))); | ||
| // Mirror the new state as the baseline so the save button | ||
| // disables again until the next edit. The backend completion | ||
| // round-trip will refresh the profile for real. | ||
| self.original_display_name = self.edit_display_name.clone(); | ||
| self.original_bio = self.edit_bio.clone(); | ||
| self.original_avatar_url = self.edit_avatar_url.clone(); | ||
| } |
There was a problem hiding this comment.
Do not mark the form clean before the backend save succeeds.
If UpdateProfile fails, the button is disabled because original_* was already updated, but the cached profile still contains the old values and the user cannot retry without editing a field again.
🐛 Proposed fix
if save.clicked() && can_save {
action = AppAction::BackendTask(BackendTask::DashPayTask(Box::new(
DashPayTask::UpdateProfile {
identity: identity.clone(),
display_name: string_if_set(&self.edit_display_name),
bio: string_if_set(&self.edit_bio),
avatar_url: string_if_set(&self.edit_avatar_url),
},
)));
- // Mirror the new state as the baseline so the save button
- // disables again until the next edit. The backend completion
- // round-trip will refresh the profile for real.
- self.original_display_name = self.edit_display_name.clone();
- self.original_bio = self.edit_bio.clone();
- self.original_avatar_url = self.edit_avatar_url.clone();
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if save.clicked() && can_save { | |
| action = AppAction::BackendTask(BackendTask::DashPayTask(Box::new( | |
| DashPayTask::UpdateProfile { | |
| identity: identity.clone(), | |
| display_name: string_if_set(&self.edit_display_name), | |
| bio: string_if_set(&self.edit_bio), | |
| avatar_url: string_if_set(&self.edit_avatar_url), | |
| }, | |
| ))); | |
| // Mirror the new state as the baseline so the save button | |
| // disables again until the next edit. The backend completion | |
| // round-trip will refresh the profile for real. | |
| self.original_display_name = self.edit_display_name.clone(); | |
| self.original_bio = self.edit_bio.clone(); | |
| self.original_avatar_url = self.edit_avatar_url.clone(); | |
| } | |
| if save.clicked() && can_save { | |
| action = AppAction::BackendTask(BackendTask::DashPayTask(Box::new( | |
| DashPayTask::UpdateProfile { | |
| identity: identity.clone(), | |
| display_name: string_if_set(&self.edit_display_name), | |
| bio: string_if_set(&self.edit_bio), | |
| avatar_url: string_if_set(&self.edit_avatar_url), | |
| }, | |
| ))); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/ui/identity/settings.rs` around lines 287 - 302, The form is being marked
clean immediately after dispatching
AppAction::BackendTask(DashPayTask::UpdateProfile), which prevents retry if the
backend fails; remove the immediate assignments to self.original_display_name,
self.original_bio, and self.original_avatar_url from the save.clicked() branch
and instead update those original_* fields only after the UpdateProfile task
completes successfully (e.g., in the code path that handles the backend response
or profile refresh), so the save button remains enabled until a successful
backend confirmation.
| fn ensure_selected(&mut self, app_context: &Arc<AppContext>) { | ||
| let identities = app_context | ||
| .load_local_qualified_identities() | ||
| .unwrap_or_default(); | ||
| let incoming = identities.first().cloned(); |
There was a problem hiding this comment.
Use the hub-selected identity instead of always picking the first local identity.
In a multi-identity hub, this can edit identity A’s settings after the user selected identity B in the picker or breadcrumb. Thread the active identity id into SettingsTab and resolve that identity before falling back.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/ui/identity/settings.rs` around lines 647 - 651, The ensure_selected
method currently always picks the first local identity from
app_context.load_local_qualified_identities(); change it to prefer the
hub-selected (active) identity id threaded into SettingsTab: add an
active_identity_id field to SettingsTab (or accept it as a parameter), use that
id to look up the matching identity from app_context (e.g., resolve via
load_local_qualified_identities() then find(|id| id.id == active_identity_id)),
set that as incoming when present, and only fall back to
identities.first().cloned() if no match is found; update callers that construct
SettingsTab or call ensure_selected to pass the active id.
| // Filter chips — called out verbatim in the test-case spec. | ||
| assert!( | ||
| harness.query_by_label("All").is_some(), | ||
| "Activity tab must render the `All` filter chip" | ||
| ); | ||
| assert!( | ||
| harness.query_by_label("Payments").is_some(), | ||
| "Activity tab must render the `Payments` filter chip" | ||
| ); | ||
| assert!( | ||
| harness.query_by_label("Funding").is_some(), | ||
| "Activity tab must render the `Funding` filter chip" | ||
| ); |
There was a problem hiding this comment.
Assert the Platform chip too.
The Activity tab contract includes All, Payments, Funding, and Platform; this test omits Platform, so one filter can disappear while IT-ACTIVITY-01 stays green.
🧪 Proposed test coverage addition
assert!(
harness.query_by_label("Funding").is_some(),
"Activity tab must render the `Funding` filter chip"
);
+ assert!(
+ harness.query_by_label("Platform").is_some(),
+ "Activity tab must render the `Platform` filter chip"
+ );📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Filter chips — called out verbatim in the test-case spec. | |
| assert!( | |
| harness.query_by_label("All").is_some(), | |
| "Activity tab must render the `All` filter chip" | |
| ); | |
| assert!( | |
| harness.query_by_label("Payments").is_some(), | |
| "Activity tab must render the `Payments` filter chip" | |
| ); | |
| assert!( | |
| harness.query_by_label("Funding").is_some(), | |
| "Activity tab must render the `Funding` filter chip" | |
| ); | |
| // Filter chips — called out verbatim in the test-case spec. | |
| assert!( | |
| harness.query_by_label("All").is_some(), | |
| "Activity tab must render the `All` filter chip" | |
| ); | |
| assert!( | |
| harness.query_by_label("Payments").is_some(), | |
| "Activity tab must render the `Payments` filter chip" | |
| ); | |
| assert!( | |
| harness.query_by_label("Funding").is_some(), | |
| "Activity tab must render the `Funding` filter chip" | |
| ); | |
| assert!( | |
| harness.query_by_label("Platform").is_some(), | |
| "Activity tab must render the `Platform` filter chip" | |
| ); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@tests/kittest/identity_hub_activity.rs` around lines 56 - 68, Add an
assertion for the missing "Platform" filter chip so the test verifies all four
chips; locate the block that calls harness.query_by_label("All"), ("Payments"),
("Funding") in the identity_hub_activity test and add a similar assert for
harness.query_by_label("Platform"). Use the same assert!(...is_some(), "... must
render the `Platform` filter chip") pattern and message to match existing
assertions (refer to harness.query_by_label and the surrounding Activity tab
filter assertions).
| fn mount_hub() -> Harness<'static, dash_evo_tool::app::AppState> { | ||
| let rt = tokio::runtime::Runtime::new().expect("Failed to create tokio runtime"); | ||
| let _guard = rt.enter(); | ||
|
|
||
| let mut harness = Harness::builder().with_max_steps(100).build_eframe(|ctx| { | ||
| let mut app = dash_evo_tool::app::AppState::new(ctx.egui_ctx.clone()) | ||
| .expect("Failed to create AppState") | ||
| .with_animations(false); | ||
| app.selected_main_screen = RootScreenType::RootScreenIdentityHub; | ||
| app | ||
| }); | ||
| harness.set_size(egui::vec2(1280.0, 800.0)); | ||
| harness.run_steps(10); | ||
| harness |
There was a problem hiding this comment.
Bypass the welcome screen before claiming hub coverage.
Unlike identity_hub_onboarding.rs, this helper leaves show_welcome_screen enabled. On a fresh test DB, AppState::update() renders welcome_screen.ui(ctx) instead of the selected root screen, so home_tab_mounts_without_panic() may not exercise IdentityHubScreen at all.
Proposed test fix
let mut app = dash_evo_tool::app::AppState::new(ctx.egui_ctx.clone())
.expect("Failed to create AppState")
.with_animations(false);
+ app.show_welcome_screen = false;
+ app.welcome_screen = None;
app.selected_main_screen = RootScreenType::RootScreenIdentityHub;
appAlso applies to: 55-63
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@tests/kittest/identity_hub_home.rs` around lines 31 - 44, The helper
mount_hub leaves the welcome screen enabled so AppState::update() renders
welcome_screen.ui instead of the selected RootScreen, preventing
IdentityHubScreen from being exercised; fix by disabling the welcome screen in
the harness setup (e.g., set app.show_welcome_screen = false or call the
equivalent setter on dash_evo_tool::app::AppState before returning app in
mount_hub) and apply the same change to the other helper block at 55-63.
| fn mount_hub_on_settings() -> Harness<'static, dash_evo_tool::app::AppState> { | ||
| let rt = tokio::runtime::Runtime::new().expect("Failed to create tokio runtime"); | ||
| let _guard = rt.enter(); | ||
|
|
||
| let mut harness = Harness::builder().with_max_steps(100).build_eframe(|ctx| { | ||
| let mut app = dash_evo_tool::app::AppState::new(ctx.egui_ctx.clone()) | ||
| .expect("Failed to create AppState") | ||
| .with_animations(false); | ||
| app.selected_main_screen = RootScreenType::RootScreenIdentityHub; | ||
| app | ||
| }); | ||
| harness.set_size(egui::vec2(1280.0, 800.0)); | ||
| // Run a few frames so the onboarding landing renders first; later steps | ||
| // simulate the user clicking the Settings tab. | ||
| harness.run_steps(5); |
There was a problem hiding this comment.
Make this actually mount the Settings tab.
AppState::new() can start with show_welcome_screen = true, and this helper never selects IdentityHubTab::Settings, so the test can pass while only rendering the welcome/onboarding surface instead of the Settings tab. Mirror the onboarding test’s welcome bypass and explicitly select the hub tab before running frames.
Proposed test fix
-use dash_evo_tool::ui::RootScreenType;
+use dash_evo_tool::ui::{RootScreenType, Screen};
@@
let mut app = dash_evo_tool::app::AppState::new(ctx.egui_ctx.clone())
.expect("Failed to create AppState")
.with_animations(false);
+ app.show_welcome_screen = false;
+ app.welcome_screen = None;
app.selected_main_screen = RootScreenType::RootScreenIdentityHub;
+ if let Some(Screen::IdentityHubScreen(hub)) =
+ app.main_screens.get_mut(&RootScreenType::RootScreenIdentityHub)
+ {
+ hub.select_tab(IdentityHubTab::Settings);
+ } else {
+ panic!("Identity Hub screen must be registered for this test");
+ }
appAlso applies to: 57-63
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@tests/kittest/identity_hub_settings.rs` around lines 31 - 45,
mount_hub_on_settings currently creates AppState via AppState::new which may
leave show_welcome_screen true and never selects IdentityHubTab::Settings, so
the test renders the onboarding surface instead of the Settings tab; fix by
mirroring the onboarding test’s welcome bypass after creating the app (e.g., set
app.show_welcome_screen = false or call the same helper used in onboarding) and
explicitly set the hub tab to IdentityHubTab::Settings (or set
app.selected_identity_hub_tab = IdentityHubTab::Settings) before calling
harness.run_steps so the Settings tab is actually mounted in
mount_hub_on_settings.
Applies the revised placement rule: `src/ui/components/` now hosts only widgets with a plausible second consumer outside the Identities hub. Hub-specific widgets move next to the tab modules that consume them. Moved (git mv) into `src/ui/identity/`: - identity_hub_tab_bar - identity_hero_card - onboarding_checklist - identity_picker_card - identity_picker_add_card - identity_pill - social_profile_gate_card - request_card - contact_row - activity_row `breadcrumb_pill` stays in `components/` — it is a generic label + icon + chevron pill usable anywhere a breadcrumb exists. Import sites updated from `crate::ui::components::X` to the matching `super::X` / `crate::ui::identity::X` path. `components/README.md` rewritten to document the new placement rule; a new `identity/README.md` catalogs the hub-local widgets. No behavioural change — pure module relocation. Co-Authored-By: Claudius the Magnificent <noreply@anthropic.com>
The hub shipped dead-on-arrival in PR #842 Wave 2: every click on the Home tab produced no visible effect because `hub_screen.rs::ui` unconditionally returned `AppAction::None` from the central-island closure. The tab modules computed a real `AppAction` in their match arms but `ui.vertical_centered(|ui| { ... })` discarded the value; the closure then returned `AppAction::None` explicitly. Send, Receive, Add funds, Send to wallet, Send to another identity, Change photo, Save social profile, Register a username, Add a new key, Refresh identity — all produced no action. Also dead: the three Contacts header buttons (`Add by username`, `Scan QR`, `Show my QR`) had no click handling at all. The populated-state `Add by username` in the active-contacts section had the same gap. The Activity tab rendered the DashPay Payments hint as plain text rather than a link. Fixes: - `hub_screen.rs`: route the match's `AppAction` through `vertical_centered(...).inner` so AddScreen / BackendTask reach `AppState`. - `contacts.rs`: thread the three header clicks plus the populated- state `Add by username` through a new pure `contacts_button_kind` dispatcher. `Add by username` opens `DashPayAddContact`, `Scan QR` routes to the same screen (it owns the scan affordance today), and `Show my QR` opens `DashPayQRGenerator`. Gate card CTA continues to emit `AppAction::SwitchIdentityHubTab(Settings)`. - `activity.rs`: render the legacy-payments pointer as a clickable link that emits `AppAction::SetMainScreen(RootScreenDashPayPayments)` via a new `activity_button_kind` dispatcher. - `home.rs`: introduce `HomeButton` + `HomeButtonKind` + `home_button_kind(button)`. Every Home click site now dispatches through the pure resolver. The inline "Set up your social profile" CTA and the onboarding "Set a display name" step now route via a new `HomeOutcome::GoToSettings` — DashPay profile editing lives in §B.8 (Settings tab), not in the DPNS register-a-username flow. The old behaviour pushed RegisterDpnsNameScreen for these paths. Regression coverage: each tab module now owns a `#[test]` suite that enumerates every button variant and asserts the dispatcher returns a non-dead result. That is the ground-truth check we could add without a GUI harness — kittest 0.3 is query-only and cannot simulate clicks. The exhaustive-match pattern also makes adding a new button a compile error if the dispatcher arm is missing. Co-Authored-By: Claudius the Magnificent <noreply@anthropic.com>
thepastaclaw
left a comment
There was a problem hiding this comment.
Code Review
Validated findings against the checked-out code (HEAD b71e1fe). The PR has five genuine correctness issues: the picker branch is dead code so multi-identity users silently operate on the first identity; two quick-action tooltips don't match the flows they route to; network switches leave the Contacts tab in a stale 'already loaded' state; the populated Contacts path never dispatches LoadContactRequests; and every tab independently resolves its own 'current identity' via identities.first(), which will defeat the picker once it is wired. Tests are shallow smoke tests that cannot catch any of these. A few lower-severity issues (per-frame SQLite reads, unreachable!() in a UI path, dispatcher weakness) round out the set.
Reviewed commit: b71e1fe
🔴 5 blocking | 🟡 4 suggestion(s) | 💬 1 nitpick(s)
🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.
In `src/ui/identity/hub_screen.rs`:
- [BLOCKING] lines 149-217: `HubLanding::Picker` is dead code — multi-identity users never see the picker
`landing()` returns `HubLanding::Picker` for 2+ identities, but `ui()` collapses `HubLanding::Home | HubLanding::Picker` into the same match arm and never calls `super::picker::render`. A wallet with multiple identities therefore cannot choose which identity the hub operates on — it silently targets whichever identity is first in storage order. The fully implemented picker grid in `src/ui/identity/picker.rs` is unreachable from production code.
- [SUGGESTION] lines 79-102: `landing()` reloads all qualified identities from SQLite on every frame, plus 3 more per-frame reloads in tabs
`landing()` calls `load_local_qualified_identities()` unconditionally every repaint, and Home (home.rs:582), Contacts (contacts.rs:162), and Settings (settings.rs:649) each make their own independent call in the same frame. At 60 fps that is 4 SQLite reads per frame per hub surface. Cache the identity list on the hub once per refresh (or on explicit invalidation) and pass slices into tab renderers.
In `src/ui/identity/settings.rs`:
- [BLOCKING] lines 647-651: Tabs independently pick the 'current identity' via `identities.first()`
Settings (line 651), Home (home.rs:594), and Contacts (contacts.rs:436) each re-resolve their target identity by calling `load_local_qualified_identities()` and taking `.first()`. Identity selection is not owned by the hub. Even once the picker is wired (see related finding), tab content will not be bound to the identity the user actually selected — each tab re-picks its own 'current' identity from collection order on every frame. The hub screen needs to own a `selected_identity` and pass it (or a slice/index) into tab renderers.
In `src/ui/mod.rs`:
- [BLOCKING] lines 921-924: Network switches leave the hub's Contacts tab in a permanently 'already loaded' state
`change_context` for `Screen::IdentityHubScreen` only swaps `screen.app_context` and returns — it does not call `screen.refresh()` or reset `contacts_state`. Because Contacts gates its `LoadContacts` dispatch on `state_guard.load_requested` (contacts.rs:318), the Contacts tab will never refetch for the new network if it had already loaded once on the previous network. The populated shell will keep showing data for the old network's identity until the user manually refreshes. The screen should call `self.refresh()` (or at least `self.contacts_state.reset()`) when the context changes.
In `src/ui/identity/contacts.rs`:
- [BLOCKING] lines 313-325: Contacts populated path never dispatches `LoadContactRequests`
The module comment at line 240–242 states the populated shell is backed by both `DashPayTask::LoadContacts` *and* `LoadContactRequests`, but only `LoadContacts` is dispatched at line 320. The 'Received requests' (line 255) and 'Sent requests' (line 301) sections therefore cannot hydrate from backend data — they will display placeholder empty-state copy even when real requests exist. Either dispatch `LoadContactRequests` alongside `LoadContacts`, or remove the receive/sent sections from the shell until that task is wired.
- [SUGGESTION] lines 224-228: `unreachable!()` in a UI dispatch path can panic the entire app
`render_gated` uses `unreachable!("GateSetUpProfile should not map to OpenScreen")` as the catch-all arm. It is genuinely unreachable today, but this is a UI click handler — a future dispatcher change that adds `OpenScreen` to this button would turn a click into an app-level panic rather than a dead click. Prefer a no-op branch with a warning log (or an exhaustive match keyed by variant) so the UI fails safely.
In `src/ui/identity/home.rs`:
- [BLOCKING] lines 177-194: `Send` / `Receive` quick-action tooltips do not match the flows they open
`home_button_kind` maps `HomeButton::Send -> OpenScreen(Transfer)` and `HomeButton::Receive -> OpenScreen(TopUp)`. `HomeScreenKind::Transfer` opens `ScreenType::TransferScreen` (identity→identity credits transfer) and `HomeScreenKind::TopUp` opens `TopUpIdentity` (move *wallet* funds *into* the identity — explicitly 'move wallet Dash into the identity' per the doc comment at line 155). The rendered tooltips at home.rs:304 ('Send Dash to a contact, username, or address.') and home.rs:313 ('Show a QR code or your username so someone can pay you.') promise very different behavior. Receive in particular is the opposite of TopUp — TopUp sends wallet funds into the identity, not an inbound payment surface. Either correct the routing or change the tooltip copy to reflect what actually happens.
- [SUGGESTION] lines 163-170: `HomeButtonKind::is_dead` cannot detect wrong-screen routing
`is_dead` only flags `Outcome(HomeOutcome::None)` and treats every `OpenScreen(_)` as live. Because of that, the dispatcher unit tests (described in the enum doc comment as the 'ground-truth check' after Wave 2's dead-on-arrival buttons) actively pin the wrong `Send -> Transfer` / `Receive -> TopUp` mapping in place. Consider asserting the expected `HomeScreenKind` per button, not just 'non-None'.
In `tests/kittest/identity_hub.rs`:
- [SUGGESTION] lines 12-73: Hub kittests are smoke tests that cannot catch any of the regressions above
`mount_hub()` uses default `AppState::new`, so the DB has zero identities and `landing()` resolves to `Onboarding` — the tests never render the Home, Picker, Contacts, Activity, or Settings surfaces they purport to cover. `identity_hub_mounts_and_renders` only asserts non-panic, and `legacy_nav_entries_coexist_with_hub` is a pure enum check. None of the dead-picker, wrong-screen-routing, network-switch, or `LoadContactRequests`-missing bugs above would be caught. Add fixtures that seed 1 and 2+ identities in the local DB and assert that the rendered node tree contains the expected tab labels / picker grid / request sections.
| match landing { | ||
| HubLanding::Onboarding => super::onboarding::render(ui, &self.app_context), | ||
| HubLanding::Home | HubLanding::Picker => { | ||
| // `ui.vertical_centered(...).inner` carries the tab's | ||
| // `AppAction` back out — previously this closure returned | ||
| // `AppAction::None` unconditionally, swallowing every | ||
| // `AddScreen` / `BackendTask` produced by the tab content. | ||
| let inner = ui.vertical_centered(|ui| { | ||
| ui.add_space(24.0); | ||
| ui.label( | ||
| RichText::new("Identities hub") | ||
| .size(28.0) | ||
| .color(DashColors::text_primary(dark_mode)), | ||
| ); | ||
| ui.add_space(12.0); | ||
| ui.label( | ||
| RichText::new( | ||
| "This new section is under active development. Use the legacy \ | ||
| Identities and Dashpay entries in the sidebar until the \ | ||
| remaining tabs ship.", | ||
| ) | ||
| .color(DashColors::text_secondary(dark_mode)), | ||
| ); | ||
| ui.add_space(24.0); | ||
|
|
||
| // Hub tab bar (T5). Reuses the project's theme tokens | ||
| // via `IdentityHubTabBar`; selection state lives on | ||
| // the screen so refreshes and deep links can update | ||
| // it from outside the component. | ||
| let tab_response = IdentityHubTabBar::new(self.selected_tab).show(ui); | ||
| if let Some(clicked) = tab_response.clicked() { | ||
| if clicked != self.selected_tab { | ||
| // Tab-entry transition: reset per-tab load | ||
| // guards so the incoming tab re-dispatches | ||
| // its initial backend task once. | ||
| self.contacts_state.reset(); | ||
| } | ||
| self.selected_tab = clicked; | ||
| } | ||
| ui.add_space(16.0); | ||
| match self.selected_tab { | ||
| IdentityHubTab::Home => { | ||
| let (home_action, outcome) = | ||
| super::home::render(ui, &self.app_context, &self.home_state); | ||
| if let Some(next_tab) = | ||
| super::home::apply_outcome(&mut self.home_state, outcome) | ||
| { | ||
| if next_tab != self.selected_tab { | ||
| self.contacts_state.reset(); | ||
| } | ||
| self.selected_tab = next_tab; | ||
| } | ||
| home_action | ||
| } | ||
| IdentityHubTab::Contacts => super::contacts::render( | ||
| ui, | ||
| &self.app_context, | ||
| &mut self.contacts_state, | ||
| ), | ||
| IdentityHubTab::Activity => { | ||
| super::activity::render(ui, &self.app_context) | ||
| } | ||
| IdentityHubTab::Settings => { | ||
| self.settings_tab.render(ui, &self.app_context) | ||
| } | ||
| } | ||
| }); | ||
| inner.inner | ||
| } |
There was a problem hiding this comment.
🔴 Blocking: HubLanding::Picker is dead code — multi-identity users never see the picker
landing() returns HubLanding::Picker for 2+ identities, but ui() collapses HubLanding::Home | HubLanding::Picker into the same match arm and never calls super::picker::render. A wallet with multiple identities therefore cannot choose which identity the hub operates on — it silently targets whichever identity is first in storage order. The fully implemented picker grid in src/ui/identity/picker.rs is unreachable from production code.
source: ['claude-general', 'codex-general', 'claude-rust-quality', 'codex-rust-quality']
🤖 Fix this with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.
In `src/ui/identity/hub_screen.rs`:
- [BLOCKING] lines 149-217: `HubLanding::Picker` is dead code — multi-identity users never see the picker
`landing()` returns `HubLanding::Picker` for 2+ identities, but `ui()` collapses `HubLanding::Home | HubLanding::Picker` into the same match arm and never calls `super::picker::render`. A wallet with multiple identities therefore cannot choose which identity the hub operates on — it silently targets whichever identity is first in storage order. The fully implemented picker grid in `src/ui/identity/picker.rs` is unreachable from production code.
| fn ensure_selected(&mut self, app_context: &Arc<AppContext>) { | ||
| let identities = app_context | ||
| .load_local_qualified_identities() | ||
| .unwrap_or_default(); | ||
| let incoming = identities.first().cloned(); |
There was a problem hiding this comment.
🔴 Blocking: Tabs independently pick the 'current identity' via identities.first()
Settings (line 651), Home (home.rs:594), and Contacts (contacts.rs:436) each re-resolve their target identity by calling load_local_qualified_identities() and taking .first(). Identity selection is not owned by the hub. Even once the picker is wired (see related finding), tab content will not be bound to the identity the user actually selected — each tab re-picks its own 'current' identity from collection order on every frame. The hub screen needs to own a selected_identity and pass it (or a slice/index) into tab renderers.
source: ['claude-rust-quality', 'codex-rust-quality']
🤖 Fix this with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.
In `src/ui/identity/settings.rs`:
- [BLOCKING] lines 647-651: Tabs independently pick the 'current identity' via `identities.first()`
Settings (line 651), Home (home.rs:594), and Contacts (contacts.rs:436) each re-resolve their target identity by calling `load_local_qualified_identities()` and taking `.first()`. Identity selection is not owned by the hub. Even once the picker is wired (see related finding), tab content will not be bound to the identity the user actually selected — each tab re-picks its own 'current' identity from collection order on every frame. The hub screen needs to own a `selected_identity` and pass it (or a slice/index) into tab renderers.
| Screen::IdentityHubScreen(screen) => { | ||
| screen.app_context = app_context; | ||
| return; | ||
| } |
There was a problem hiding this comment.
🔴 Blocking: Network switches leave the hub's Contacts tab in a permanently 'already loaded' state
change_context for Screen::IdentityHubScreen only swaps screen.app_context and returns — it does not call screen.refresh() or reset contacts_state. Because Contacts gates its LoadContacts dispatch on state_guard.load_requested (contacts.rs:318), the Contacts tab will never refetch for the new network if it had already loaded once on the previous network. The populated shell will keep showing data for the old network's identity until the user manually refreshes. The screen should call self.refresh() (or at least self.contacts_state.reset()) when the context changes.
source: ['codex-general']
🤖 Fix this with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.
In `src/ui/mod.rs`:
- [BLOCKING] lines 921-924: Network switches leave the hub's Contacts tab in a permanently 'already loaded' state
`change_context` for `Screen::IdentityHubScreen` only swaps `screen.app_context` and returns — it does not call `screen.refresh()` or reset `contacts_state`. Because Contacts gates its `LoadContacts` dispatch on `state_guard.load_requested` (contacts.rs:318), the Contacts tab will never refetch for the new network if it had already loaded once on the previous network. The populated shell will keep showing data for the old network's identity until the user manually refreshes. The screen should call `self.refresh()` (or at least `self.contacts_state.reset()`) when the context changes.
| // Fire the existing backend task to populate the list — but only once per | ||
| // tab entry. Without this guard the populated shell re-dispatches every | ||
| // frame, which floods the backend channel and hammers the SDK. The hub | ||
| // resets the guard in `refresh_on_arrival()` so a fresh tab switch or | ||
| // explicit refresh will trigger another load. | ||
| if !state_guard.load_requested { | ||
| state_guard.load_requested = true; | ||
| action |= AppAction::BackendTask(BackendTask::DashPayTask(Box::new( | ||
| DashPayTask::LoadContacts { | ||
| identity: identity.clone(), | ||
| }, | ||
| ))); | ||
| } |
There was a problem hiding this comment.
🔴 Blocking: Contacts populated path never dispatches LoadContactRequests
The module comment at line 240–242 states the populated shell is backed by both DashPayTask::LoadContacts and LoadContactRequests, but only LoadContacts is dispatched at line 320. The 'Received requests' (line 255) and 'Sent requests' (line 301) sections therefore cannot hydrate from backend data — they will display placeholder empty-state copy even when real requests exist. Either dispatch LoadContactRequests alongside LoadContacts, or remove the receive/sent sections from the shell until that task is wired.
source: ['claude-rust-quality', 'codex-rust-quality']
🤖 Fix this with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.
In `src/ui/identity/contacts.rs`:
- [BLOCKING] lines 313-325: Contacts populated path never dispatches `LoadContactRequests`
The module comment at line 240–242 states the populated shell is backed by both `DashPayTask::LoadContacts` *and* `LoadContactRequests`, but only `LoadContacts` is dispatched at line 320. The 'Received requests' (line 255) and 'Sent requests' (line 301) sections therefore cannot hydrate from backend data — they will display placeholder empty-state copy even when real requests exist. Either dispatch `LoadContactRequests` alongside `LoadContacts`, or remove the receive/sent sections from the shell until that task is wired.
| match button { | ||
| HomeButton::Send | HomeButton::SendToAnotherIdentity => OpenScreen(Transfer), | ||
| HomeButton::Receive | HomeButton::AddFunds => OpenScreen(TopUp), | ||
| HomeButton::SendToWallet => OpenScreen(Withdrawal), | ||
| HomeButton::PickUsernameHero | HomeButton::ChecklistPickUsername => { | ||
| OpenScreen(RegisterDpnsName) | ||
| } | ||
| HomeButton::AddContact | HomeButton::ChecklistAddFirstContact => { | ||
| Outcome(HomeOutcome::GoToContacts) | ||
| } | ||
| HomeButton::SetUpSocialProfile | HomeButton::ChecklistSetDisplayName => { | ||
| Outcome(HomeOutcome::GoToSettings) | ||
| } | ||
| HomeButton::SeeAllActivity => Outcome(HomeOutcome::GoToActivity), | ||
| HomeButton::DismissChecklist => Outcome(HomeOutcome::DismissChecklist), | ||
| HomeButton::ToggleAdvanced => Outcome(HomeOutcome::ToggleAdvanced), | ||
| } | ||
| } |
There was a problem hiding this comment.
🔴 Blocking: Send / Receive quick-action tooltips do not match the flows they open
home_button_kind maps HomeButton::Send -> OpenScreen(Transfer) and HomeButton::Receive -> OpenScreen(TopUp). HomeScreenKind::Transfer opens ScreenType::TransferScreen (identity→identity credits transfer) and HomeScreenKind::TopUp opens TopUpIdentity (move wallet funds into the identity — explicitly 'move wallet Dash into the identity' per the doc comment at line 155). The rendered tooltips at home.rs:304 ('Send Dash to a contact, username, or address.') and home.rs:313 ('Show a QR code or your username so someone can pay you.') promise very different behavior. Receive in particular is the opposite of TopUp — TopUp sends wallet funds into the identity, not an inbound payment surface. Either correct the routing or change the tooltip copy to reflect what actually happens.
source: ['claude-general']
🤖 Fix this with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.
In `src/ui/identity/home.rs`:
- [BLOCKING] lines 177-194: `Send` / `Receive` quick-action tooltips do not match the flows they open
`home_button_kind` maps `HomeButton::Send -> OpenScreen(Transfer)` and `HomeButton::Receive -> OpenScreen(TopUp)`. `HomeScreenKind::Transfer` opens `ScreenType::TransferScreen` (identity→identity credits transfer) and `HomeScreenKind::TopUp` opens `TopUpIdentity` (move *wallet* funds *into* the identity — explicitly 'move wallet Dash into the identity' per the doc comment at line 155). The rendered tooltips at home.rs:304 ('Send Dash to a contact, username, or address.') and home.rs:313 ('Show a QR code or your username so someone can pay you.') promise very different behavior. Receive in particular is the opposite of TopUp — TopUp sends wallet funds into the identity, not an inbound payment surface. Either correct the routing or change the tooltip copy to reflect what actually happens.
| fn mount_hub() -> Harness<'static, dash_evo_tool::app::AppState> { | ||
| let rt = tokio::runtime::Runtime::new().expect("Failed to create tokio runtime"); | ||
| let _guard = rt.enter(); | ||
|
|
||
| let mut harness = Harness::builder().with_max_steps(100).build_eframe(|ctx| { | ||
| let mut app = dash_evo_tool::app::AppState::new(ctx.egui_ctx.clone()) | ||
| .expect("Failed to create AppState") | ||
| .with_animations(false); | ||
| app.selected_main_screen = RootScreenType::RootScreenIdentityHub; | ||
| app | ||
| }); | ||
| harness.set_size(egui::vec2(1280.0, 800.0)); | ||
| harness.run_steps(10); | ||
| harness | ||
| } | ||
|
|
||
| /// IT-ONBOARD-01 / IT-HOME-01 combined smoke: the hub renders without | ||
| /// panicking on the default first-run database (no identities loaded → should | ||
| /// render the onboarding empty state). | ||
| #[test] | ||
| fn identity_hub_mounts_and_renders() { | ||
| let _harness = mount_hub(); | ||
| // If `mount_hub` returned without panicking, the hub compiled-in and | ||
| // rendered. More detailed assertions land with the per-tab content work. | ||
| } | ||
|
|
||
| /// IT-NAV-01: The nav must keep the legacy `Identities` and `Dashpay` entries | ||
| /// alongside the new hub so users can toggle between old and new. | ||
| #[test] | ||
| fn legacy_nav_entries_coexist_with_hub() { | ||
| // We don't need to drive the UI for this one — it's a pure enum check. | ||
| // The `RootScreenType` enum must contain all three coexisting variants. | ||
| let legacy_identities = RootScreenType::RootScreenIdentities; | ||
| // `RootScreenDashpay` is the legacy root nav entry for DashPay; the other | ||
| // `RootScreenDashPay*` variants are sub-screens within that section. | ||
| let legacy_dashpay_root = RootScreenType::RootScreenDashpay; | ||
| let new_hub = RootScreenType::RootScreenIdentityHub; | ||
| assert_ne!(legacy_identities, new_hub); | ||
| assert_ne!(legacy_dashpay_root, new_hub); | ||
| // Round-trip the new variant through on-disk encoding to verify the | ||
| // persistence contract is stable. | ||
| let encoded = new_hub.to_int(); | ||
| let decoded = RootScreenType::from_int(encoded).expect("hub variant must decode"); | ||
| assert_eq!(new_hub, decoded); | ||
| } | ||
|
|
||
| /// The hub screen must be reachable from the existing `create_screen` | ||
| /// dispatch table. This asserts the wiring that AppState::new relies on. | ||
| #[test] | ||
| fn identity_hub_screen_type_creates_hub_screen() { | ||
| // Guard against a future refactor that silently drops the hub case from | ||
| // `ScreenType::create_screen`. If that happens, this test regresses. | ||
| use dash_evo_tool::ui::ScreenType; | ||
| // The assert-that-it-compiles-and-matches is enough; the dispatcher | ||
| // expects a Screen with IdentityHub variant. | ||
| let screen_type = ScreenType::IdentityHub; | ||
| assert_eq!(screen_type, ScreenType::IdentityHub); | ||
| // `ScreenType::create_screen` requires a live AppContext; instead of | ||
| // constructing one here we verify through the enum discriminant. The end- | ||
| // to-end wiring is exercised by `identity_hub_mounts_and_renders`. | ||
| let _ = screen_type; | ||
| } |
There was a problem hiding this comment.
🟡 Suggestion: Hub kittests are smoke tests that cannot catch any of the regressions above
mount_hub() uses default AppState::new, so the DB has zero identities and landing() resolves to Onboarding — the tests never render the Home, Picker, Contacts, Activity, or Settings surfaces they purport to cover. identity_hub_mounts_and_renders only asserts non-panic, and legacy_nav_entries_coexist_with_hub is a pure enum check. None of the dead-picker, wrong-screen-routing, network-switch, or LoadContactRequests-missing bugs above would be caught. Add fixtures that seed 1 and 2+ identities in the local DB and assert that the rendered node tree contains the expected tab labels / picker grid / request sections.
source: ['claude-general', 'codex-general', 'claude-rust-quality']
🤖 Fix this with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.
In `tests/kittest/identity_hub.rs`:
- [SUGGESTION] lines 12-73: Hub kittests are smoke tests that cannot catch any of the regressions above
`mount_hub()` uses default `AppState::new`, so the DB has zero identities and `landing()` resolves to `Onboarding` — the tests never render the Home, Picker, Contacts, Activity, or Settings surfaces they purport to cover. `identity_hub_mounts_and_renders` only asserts non-panic, and `legacy_nav_entries_coexist_with_hub` is a pure enum check. None of the dead-picker, wrong-screen-routing, network-switch, or `LoadContactRequests`-missing bugs above would be caught. Add fixtures that seed 1 and 2+ identities in the local DB and assert that the rendered node tree contains the expected tab labels / picker grid / request sections.
| impl HomeButtonKind { | ||
| /// A button is "dead" when pressing it would produce neither an | ||
| /// `AppAction::AddScreen` nor a meaningful [`HomeOutcome`]. This is the | ||
| /// invariant checked by the dead-button unit test. | ||
| pub fn is_dead(self) -> bool { | ||
| matches!(self, HomeButtonKind::Outcome(HomeOutcome::None)) | ||
| } | ||
| } |
There was a problem hiding this comment.
🟡 Suggestion: HomeButtonKind::is_dead cannot detect wrong-screen routing
is_dead only flags Outcome(HomeOutcome::None) and treats every OpenScreen(_) as live. Because of that, the dispatcher unit tests (described in the enum doc comment as the 'ground-truth check' after Wave 2's dead-on-arrival buttons) actively pin the wrong Send -> Transfer / Receive -> TopUp mapping in place. Consider asserting the expected HomeScreenKind per button, not just 'non-None'.
source: ['claude-general']
🤖 Fix this with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.
In `src/ui/identity/home.rs`:
- [SUGGESTION] lines 163-170: `HomeButtonKind::is_dead` cannot detect wrong-screen routing
`is_dead` only flags `Outcome(HomeOutcome::None)` and treats every `OpenScreen(_)` as live. Because of that, the dispatcher unit tests (described in the enum doc comment as the 'ground-truth check' after Wave 2's dead-on-arrival buttons) actively pin the wrong `Send -> Transfer` / `Receive -> TopUp` mapping in place. Consider asserting the expected `HomeScreenKind` per button, not just 'non-None'.
| pub(crate) fn landing(&mut self, ctx: &Context) -> HubLanding { | ||
| match self.app_context.load_local_qualified_identities() { | ||
| Ok(identities) => { | ||
| // Clear any previously-shown error banner now that loading works. | ||
| self.load_error_banner.take_and_clear(); | ||
| let landing = HubLanding::from_identity_count(identities.len()); | ||
| self.last_good_landing = landing; | ||
| landing | ||
| } | ||
| Err(e) => { | ||
| // Idempotent: set_global de-duplicates by text, so repainting | ||
| // this frame after frame does not spam banners. | ||
| let handle = MessageBanner::set_global( | ||
| ctx, | ||
| "Could not load your identities from this device. Try refreshing or \ | ||
| reopening the app.", | ||
| MessageType::Error, | ||
| ); | ||
| handle.with_details(&e); | ||
| self.load_error_banner = Some(handle); | ||
| self.last_good_landing | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
🟡 Suggestion: landing() reloads all qualified identities from SQLite on every frame, plus 3 more per-frame reloads in tabs
landing() calls load_local_qualified_identities() unconditionally every repaint, and Home (home.rs:582), Contacts (contacts.rs:162), and Settings (settings.rs:649) each make their own independent call in the same frame. At 60 fps that is 4 SQLite reads per frame per hub surface. Cache the identity list on the hub once per refresh (or on explicit invalidation) and pass slices into tab renderers.
source: ['claude-general', 'claude-rust-quality']
🤖 Fix this with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.
In `src/ui/identity/hub_screen.rs`:
- [SUGGESTION] lines 79-102: `landing()` reloads all qualified identities from SQLite on every frame, plus 3 more per-frame reloads in tabs
`landing()` calls `load_local_qualified_identities()` unconditionally every repaint, and Home (home.rs:582), Contacts (contacts.rs:162), and Settings (settings.rs:649) each make their own independent call in the same frame. At 60 fps that is 4 SQLite reads per frame per hub surface. Cache the identity list on the hub once per refresh (or on explicit invalidation) and pass slices into tab renderers.
| ContactsButtonKind::OpenScreen(_) => { | ||
| // Not possible today (dispatcher returns SwitchHubTab), but | ||
| // exhaustive match future-proofs the gate CTA. | ||
| unreachable!("GateSetUpProfile should not map to OpenScreen"); | ||
| } |
There was a problem hiding this comment.
🟡 Suggestion: unreachable!() in a UI dispatch path can panic the entire app
render_gated uses unreachable!("GateSetUpProfile should not map to OpenScreen") as the catch-all arm. It is genuinely unreachable today, but this is a UI click handler — a future dispatcher change that adds OpenScreen to this button would turn a click into an app-level panic rather than a dead click. Prefer a no-op branch with a warning log (or an exhaustive match keyed by variant) so the UI fails safely.
source: ['claude-rust-quality']
🤖 Fix this with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.
In `src/ui/identity/contacts.rs`:
- [SUGGESTION] lines 224-228: `unreachable!()` in a UI dispatch path can panic the entire app
`render_gated` uses `unreachable!("GateSetUpProfile should not map to OpenScreen")` as the catch-all arm. It is genuinely unreachable today, but this is a UI click handler — a future dispatcher change that adds `OpenScreen` to this button would turn a click into an app-level panic rather than a dead click. Prefer a no-op branch with a warning log (or an exhaustive match keyed by variant) so the UI fails safely.
| let mut action = AppAction::None; | ||
| let dark_mode = ui.ctx().style().visuals.dark_mode; | ||
|
|
||
| section_heading(ui, "Social profile", dark_mode); | ||
| ui.label( | ||
| RichText::new("This information is visible to everyone on Dash Platform.") | ||
| .small() | ||
| .color(DashColors::text_secondary(dark_mode)), | ||
| ); | ||
| ui.add_space(8.0); | ||
|
|
||
| // Avatar block — placeholder glyph + "Change photo" ghost button. | ||
| // The actual file-picker wiring lives in `ProfileScreen`; we surface | ||
| // the button and let the user click through to the legacy edit path | ||
| // in a follow-up (no backend task needed yet). | ||
| ui.horizontal(|ui| { | ||
| ui.label(RichText::new("👤").size(48.0).color(DashColors::DEEP_BLUE)); | ||
| ui.vertical(|ui| { | ||
| let btn = ComponentStyles::add_secondary_button(ui, "Change photo", dark_mode) | ||
| .clickable_tooltip(TIP_CHANGE_PHOTO); | ||
| if btn.clicked() { | ||
| // Route to legacy DashPay Profile screen for the full | ||
| // image-upload flow. This is NOT a backend task and does | ||
| // not violate the "additive only" rule. | ||
| action = AppAction::SetMainScreen( | ||
| crate::ui::RootScreenType::RootScreenDashPayProfile, | ||
| ); | ||
| } | ||
| }); | ||
| }); | ||
|
|
||
| ui.add_space(8.0); | ||
|
|
||
| // Display name input. | ||
| ui.label(RichText::new("Display name").color(DashColors::text_primary(dark_mode))); | ||
| let display_name = ui.add( | ||
| TextEdit::singleline(&mut self.edit_display_name) | ||
| .hint_text("How should people see your name?") | ||
| .desired_width(f32::INFINITY), | ||
| ); | ||
| counter( | ||
| ui, | ||
| self.edit_display_name.len(), | ||
| MAX_DISPLAY_NAME, | ||
| dark_mode, | ||
| ); | ||
| let _ = display_name; // response not needed beyond widget side-effects | ||
|
|
||
| ui.add_space(8.0); | ||
|
|
||
| // Bio textarea. | ||
| ui.label(RichText::new("About").color(DashColors::text_primary(dark_mode))); | ||
| ui.add( | ||
| TextEdit::multiline(&mut self.edit_bio) | ||
| .hint_text(format!("A short description, up to {MAX_BIO} characters.")) | ||
| .desired_width(f32::INFINITY) | ||
| .desired_rows(4), | ||
| ); | ||
| counter(ui, self.edit_bio.len(), MAX_BIO, dark_mode); | ||
|
|
||
| ui.add_space(8.0); | ||
|
|
||
| // Avatar URL — we include it so users can still set the avatar when | ||
| // the file-picker flow is not yet available from this tab. Kept under | ||
| // the visual avatar block so it is clearly secondary. | ||
| ui.label(RichText::new("Avatar URL").color(DashColors::text_primary(dark_mode))); | ||
| ui.add( | ||
| TextEdit::singleline(&mut self.edit_avatar_url) | ||
| .hint_text("https://example.com/avatar.jpg") | ||
| .desired_width(f32::INFINITY), | ||
| ); | ||
| counter(ui, self.edit_avatar_url.len(), MAX_AVATAR_URL, dark_mode); | ||
|
|
||
| ui.add_space(12.0); | ||
|
|
||
| // Save / Delete buttons row. | ||
| let invalid = self.validation_error().is_some(); | ||
| let dirty = self.has_changes(); | ||
| let can_save = !invalid && dirty; | ||
| let save_tooltip = if !dirty { | ||
| TIP_SAVE_NO_CHANGES.to_string() | ||
| } else if invalid { | ||
| TIP_SAVE_INVALID.to_string() | ||
| } else { | ||
| "Save your social profile to DashPay.".to_string() | ||
| }; | ||
|
|
||
| ui.horizontal(|ui| { | ||
| let save = | ||
| ComponentStyles::add_primary_button_enabled(ui, can_save, "Save social profile"); | ||
| let save = if can_save { | ||
| save.clickable_tooltip(save_tooltip) | ||
| } else { | ||
| save.disabled_tooltip(save_tooltip) | ||
| }; | ||
| if save.clicked() && can_save { | ||
| action = AppAction::BackendTask(BackendTask::DashPayTask(Box::new( | ||
| DashPayTask::UpdateProfile { | ||
| identity: identity.clone(), | ||
| display_name: string_if_set(&self.edit_display_name), | ||
| bio: string_if_set(&self.edit_bio), | ||
| avatar_url: string_if_set(&self.edit_avatar_url), | ||
| }, | ||
| ))); | ||
| // Mirror the new state as the baseline so the save button | ||
| // disables again until the next edit. The backend completion | ||
| // round-trip will refresh the profile for real. | ||
| self.original_display_name = self.edit_display_name.clone(); | ||
| self.original_bio = self.edit_bio.clone(); | ||
| self.original_avatar_url = self.edit_avatar_url.clone(); | ||
| } | ||
|
|
||
| ui.add_space(12.0); | ||
|
|
||
| // GATED: DashPayTask::DeleteProfile does not exist (2026-04-23). | ||
| // Render as a non-interactive danger-style link so Alex can see | ||
| // the affordance and knows it is planned. | ||
| // TODO(identity-hub): wire once DashPayTask::DeleteProfile lands. | ||
| let delete = ui | ||
| .add_enabled( | ||
| false, | ||
| egui::Button::new( | ||
| RichText::new("Delete social profile").color(DashColors::ERROR), | ||
| ) | ||
| .fill(egui::Color32::TRANSPARENT) | ||
| .stroke(egui::Stroke::NONE), | ||
| ) | ||
| .disabled_tooltip(format!("{TIP_DELETE_PROFILE} {GATED_COMING_SOON}")); | ||
| if delete.clicked() { | ||
| // Unreachable while disabled; defensive — open the confirm | ||
| // dialog so, once the backend exists, this path activates | ||
| // with a single-line change (remove `add_enabled(false, …)`). | ||
| self.confirm_delete_profile = Some( | ||
| ConfirmationDialog::new( | ||
| "Delete social profile", | ||
| "Remove the display name, bio, and avatar from DashPay. Your \ | ||
| identity, usernames, and balance stay intact. Are you sure?", | ||
| ) | ||
| .confirm_text(Some("Delete")) | ||
| .cancel_text(Some("Keep")) | ||
| .danger_mode(true), | ||
| ); | ||
| } | ||
| }); |
There was a problem hiding this comment.
💬 Nitpick: Several settings sub-renderers assign action = ... instead of action |= ...
Some settings sub-renderers use plain assignment when building AppAction, where sibling code (and the rest of the hub) uses |=. Current call sites happen to tolerate this, but it is a footgun if a sub-renderer ever produces more than one action in a frame — the earlier action will be silently dropped. Switch to |= for consistency with the rest of the hub.
source: ['claude-general']
Summary
Delivers the full unified Identities hub UI described in
docs/ai-design/2026-04-22-identity-dashpay-redesign/(design spec + wireframealready merged on the base branch
feat/identity-dashpay-redesign-wireframe).This PR started as a scaffold and, after a Wave 1 of six parallel per-tab
implementations (T5, T7, T8, T9, T10, T11), a Wave 2 integration pass, and
a Wave 3 dead-button fix (see below), now ships the complete hub:
RootScreenType::RootScreenIdentityHub,IdentityHubScreen, left-nav entry,BreadcrumbPill+IdentityPill, planning artifacts underdocs/ai-design/2026-04-23-identity-hub-impl/.IdentityHubTabBarcomponent driving the four-tab switcher on top of the hub.IdentityPickerCard+IdentityPickerAddCard, rendered on the picker landing when the account holds multiple identities.IdentityHeroCard, quick actions, secondary actions,OnboardingChecklist, recent-activity preview, "See all activity" deep link to the Activity tab viaHomeOutcome::GoToActivity.SocialProfileGateCardfor identities without a DashPay profile, populated shell with three sections (received / active / sent),RequestCard(received + sent variants) andContactRowcomponents wired up with response flags for future accept / decline / cancel handling.ActivityRowcomponent. The unified aggregator is still gated behind theidentity-hub-activity-feedCargo feature until the backend aggregator lands.SettingsTabstate holder owned by the hub so drafts survive tab switches.Wave 3 — dead-button fix + hub-local component relocation
The hub shipped dead-on-arrival in Wave 2: every button on every tab produced no visible effect. Root cause:
hub_screen.rs::uiunconditionally returnedAppAction::Nonefrom the central-island closure. Each tab module computed a realAppActionin its match arm, but the closure discarded the value (ui.vertical_centered(|ui| { ... });then explicitAppAction::Nonereturn). Send, Receive, Add funds, Send to wallet, Send to another identity, Change photo, Save social profile, Register a username, Add a new key, Refresh identity — all produced no action. The Contacts header buttons (Add by username,Scan QR,Show my QR) had no click handling at all. The Activity tab rendered the DashPay Payments hint as plain text rather than a link.Wave 3 fixes the lot:
AppActionthroughvertical_centered(...).innersoAddScreen/BackendTaskreachAppState.HomeButton+HomeButtonKind+ purehome_button_kind(button)dispatcher. Every click site now dispatches through the resolver. The inline "Set up your social profile" CTA and the onboarding "Set a display name" step route via a newHomeOutcome::GoToSettings— DashPay profile editing lives in §B.8 (Settings tab), not in the DPNS register-a-username flow.Add by usernamethrough a new purecontacts_button_kinddispatcher.Add by usernameopensDashPayAddContact,Scan QRroutes there too (it owns the scan affordance today),Show my QRopensDashPayQRGenerator. Gate card CTA continues to emitAppAction::SwitchIdentityHubTab(Settings).AppAction::SetMainScreen(RootScreenDashPayPayments)via a newactivity_button_kinddispatcher.Regression coverage: each tab module now owns a
#[test]suite that enumerates every button variant and asserts the dispatcher returns a non-dead result. The exhaustive-match pattern also makes adding a new button a compile error if the dispatcher arm is missing. This is the ground-truth check we could add without a GUI harness —egui_kittest0.3 is query-only and cannot simulate clicks.Additionally, Wave 3 applies the revised component-placement rule:
src/ui/components/now hosts only widgets with a plausible second consumer outside the Identities hub. Hub-specific widgets (IdentityHubTabBar,IdentityHeroCard,OnboardingChecklist,IdentityPickerCard,IdentityPickerAddCard,IdentityPill,SocialProfileGateCard,RequestCard,ContactRow,ActivityRow) moved intosrc/ui/identity/next to the tab modules that consume them.BreadcrumbPillstays incomponents/— it is a generic label-plus-icon-plus-chevron pill usable anywhere a breadcrumb exists.Wave 2 integration fixes (kept)
Beyond the tab merges, Wave 2 applied the cross-cutting fixes Wave 1 surfaced:
DashPayTask::LoadContactson every paint. A hub-ownedContactsStatewith aload_requestedflag guards the dispatch and is reset onrefresh_on_arrivaland on tab switch.AppAction::SwitchIdentityHubTab: new feature-gated action variant for in-hub deep links. Routed byAppState::updateto the visibleIdentityHubScreen. The Contacts gate card's primary CTA uses it to hop to the Settings tab where the social profile is edited.RequestCard's response flags are exposed but no populated data feeds it yet, so the buttons are unreachable (not inert). Documented the wiring plan (reuse existingAcceptContactRequest/RejectContactRequest; deferCancelContactRequestto a later wave).Imagine you are opening Dash Evo Tool with no identity on a fresh profile: the left sidebar shows the new Identity Hub entry, clicking it takes you straight to a welcome card with two buttons,
Create my first identityandI already have an identity — load it. With one identity loaded, the hub lands on Home — a hero card with balance and quick actions, an onboarding checklist, and a recent-activity preview. Every button produces a side effect: Send opens the Transfer screen, Receive opens Top Up, the secondary actions route to Withdraw / Top Up / Transfer, the onboarding steps route to Register DPNS / Settings / Contacts. Switching to Contacts without a DashPay profile shows a calm gate card whose primary button drops you into Settings to set it up; with a profile, the header'sAdd by username/Scan QR/Show my QRbuttons open the corresponding legacy DashPay screens. Activity shows filter chips plus an "Open DashPay Payments" link for now. Settings hosts your social profile, advanced toggles, and danger-zone actions.Feature-gated capabilities inventory
identity-hubAppAction::SwitchIdentityHubTabvariant + handleridentity-hub-activity-feedActivityRowship unconditionally; the aggregated data source is gated)developer_modeAppContext::is_developer_mode)Deferred items (explicit)
RequestCardcomponent surfaces response flags; no real request data is rendered yet, so the buttons are unreachable rather than inert. Wiring needs a cached contact-request list onAppContextplus reuse ofAcceptContactRequest/RejectContactRequest. ACancelContactRequestbackend variant does not exist and is intentionally deferred to a parallel wallet-refactor wave (per the "no new backend-task variants" integration rule).identity-hub-activity-feed. Backend work lands separately.AddNewIdentityScreenbulk path is pending (IDH-005).disabled_tooltipbecause the backingDashPayTask/IdentityTaskvariants do not exist yet. Every such site carries aTODO(identity-hub)comment naming the missing backend variant.Scan QRdedicated scan screen: currently routes toDashPayAddContact(which owns the scan affordance today). TODO(identity-hub): swap to a dedicated scan screen if / when one ships.Test coverage
cargo test --lib --all-features→ 609 passed · 2 ignoredcargo test --test kittest --all-features→ 85 passed (mounts hub, onboarding, home, contacts, activity, settings through AppState)cargo test --test e2e --all-features→ (unchanged from Wave 2)cargo test --doc --all-features --workspace→ 3 passed · 14 ignored#[ignore](59) — unchanged scopeNew dead-button regression suite (Wave 3):
src/ui/identity/home.rs::tests— 14 tests, one perHomeButtonvariant plus stable-mapping pins.src/ui/identity/contacts.rs::tests— 5 tests covering everyContactsButton.src/ui/identity/activity.rs::tests— 2 tests covering the legacy-payments link.Feature-gate build matrix:
cargo build --all-features— greencargo build --no-default-features— green (hub module elided, legacy path only)cargo clippy --all-features --all-targets -- -D warnings— greencargo +nightly fmt --all --check— greenTest plan
cargo build --no-default-features— compile without the hub (legacy path only)cargo build --all-features— compile with everythingcargo clippy --all-features --all-targets -- -D warningscargo +nightly fmt --all --checkcargo test --all-features --workspaceHomeButton/ContactsButton/ActivityButtonvariant resolves to a non-None action via its pure dispatcherKnown follow-ups
RequestCardresponse flags reachDashPayTask::AcceptContactRequest/RejectContactRequest.DashPayTask::CancelContactRequest(parallel wave) and wire sent-requests cancel button.identity-hub-activity-feeddefault-on.AddNewIdentityScreenbulk path (IDH-005).DashPayTask::DeleteProfile(Settings tab danger zone) once the backend variant lands.IdentityTask::AddAlias/RemoveAlias/MakePrimaryAliasonce those backend variants land.Scan QRto a dedicated scan screen if one ships.🤖 Co-authored by Claudius the Magnificent AI Agent
Summary by CodeRabbit
New Features
Documentation
Tests