Skip to content

feat(identity-hub): unified Identities hub (Home · Contacts · Activity · Settings)#842

Draft
lklimek wants to merge 30 commits intov1.0-devfrom
feat/identity-hub-impl
Draft

feat(identity-hub): unified Identities hub (Home · Contacts · Activity · Settings)#842
lklimek wants to merge 30 commits intov1.0-devfrom
feat/identity-hub-impl

Conversation

@lklimek
Copy link
Copy Markdown
Contributor

@lklimek lklimek commented Apr 23, 2026

Summary

Delivers the full unified Identities hub UI described in
docs/ai-design/2026-04-22-identity-dashpay-redesign/ (design spec + wireframe
already 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:

  • Scaffold plumbing (kept from the original scope): feature flags, RootScreenType::RootScreenIdentityHub, IdentityHubScreen, left-nav entry, BreadcrumbPill + IdentityPill, planning artifacts under docs/ai-design/2026-04-23-identity-hub-impl/.
  • Tab bar (T5)IdentityHubTabBar component driving the four-tab switcher on top of the hub.
  • Identity picker grid (T7)IdentityPickerCard + IdentityPickerAddCard, rendered on the picker landing when the account holds multiple identities.
  • Home tab (T8)IdentityHeroCard, quick actions, secondary actions, OnboardingChecklist, recent-activity preview, "See all activity" deep link to the Activity tab via HomeOutcome::GoToActivity.
  • Contacts tab (T9)SocialProfileGateCard for identities without a DashPay profile, populated shell with three sections (received / active / sent), RequestCard (received + sent variants) and ContactRow components wired up with response flags for future accept / decline / cancel handling.
  • Activity tab shell (T10) — filter chips + ActivityRow component. The unified aggregator is still gated behind the identity-hub-activity-feed Cargo feature until the backend aggregator lands.
  • Settings tab (T11) — social-profile block, advanced-settings expander, danger-zone, and a SettingsTab state 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::ui unconditionally returned AppAction::None from the central-island closure. Each tab module computed a real AppAction in its match arm, but the closure discarded the value (ui.vertical_centered(|ui| { ... }); then explicit AppAction::None return). 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:

  • hub_screen.rs: route the match's AppAction through vertical_centered(...).inner so AddScreen / BackendTask reach AppState.
  • home.rs: introduce HomeButton + HomeButtonKind + pure home_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 new HomeOutcome::GoToSettings — DashPay profile editing lives in §B.8 (Settings tab), not in the DPNS register-a-username flow.
  • 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 there too (it owns the scan affordance today), Show my QR opens DashPayQRGenerator. Gate card CTA continues to emit AppAction::SwitchIdentityHubTab(Settings).
  • activity.rs: the legacy-payments pointer is now a clickable link that emits AppAction::SetMainScreen(RootScreenDashPayPayments) via a new activity_button_kind dispatcher.

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_kittest 0.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 into src/ui/identity/ next to the tab modules that consume them. BreadcrumbPill stays in components/ — 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:

  • FIX-1 — Contacts dispatch debounce: the populated Contacts shell no longer dispatches DashPayTask::LoadContacts on every paint. A hub-owned ContactsState with a load_requested flag guards the dispatch and is reset on refresh_on_arrival and on tab switch.
  • FIX-2 — AppAction::SwitchIdentityHubTab: new feature-gated action variant for in-hub deep links. Routed by AppState::update to the visible IdentityHubScreen. The Contacts gate card's primary CTA uses it to hop to the Settings tab where the social profile is edited.
  • FIX-3 — Contacts accept/decline/cancel: 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 existing AcceptContactRequest / RejectContactRequest; defer CancelContactRequest to a later wave).
  • FIX-4 — Wave 1 worktree/branch cleanup: all six worktrees removed, local Wave 1 feature branches deleted. Remote branches kept for traceability.

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 identity and I 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's Add by username / Scan QR / Show my QR buttons 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

Flag Kind Default What it gates
identity-hub Cargo feature on entire hub module compilation, nav entry, AppState registration, new AppAction::SwitchIdentityHubTab variant + handler
identity-hub-activity-feed Cargo feature off unified activity timeline aggregator (the tab shell + filter chips + ActivityRow ship unconditionally; the aggregated data source is gated)
developer_mode runtime (AppContext::is_developer_mode) off dev footer on onboarding state, bulk-creation + load-by-ID affordances (dev-only links still not wired — IDH-005 remains a gap)

Deferred items (explicit)

  • Contact-request accept / decline / cancel wiring: the RequestCard component 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 on AppContext plus reuse of AcceptContactRequest / RejectContactRequest. A CancelContactRequest backend variant does not exist and is intentionally deferred to a parallel wallet-refactor wave (per the "no new backend-task variants" integration rule).
  • Unified activity aggregator: still gated behind identity-hub-activity-feed. Backend work lands separately.
  • Breadcrumb three-segment switcher composition: the pill components exist; the full wallet + identity dropdown composition is not yet wired (IDH-003 still a gap).
  • Developer-mode bulk creation links: onboarding dev footer mentions the entry points as plain text; routing to the existing AddNewIdentityScreen bulk path is pending (IDH-005).
  • Gated Settings controls (unchanged from Wave 2): Delete social profile, Add / Remove / Make-primary alias, and Unload this identity render as visibly disabled with disabled_tooltip because the backing DashPayTask / IdentityTask variants do not exist yet. Every such site carries a TODO(identity-hub) comment naming the missing backend variant.
  • Scan QR dedicated scan screen: currently routes to DashPayAddContact (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-features609 passed · 2 ignored
  • cargo test --test kittest --all-features85 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 --workspace3 passed · 14 ignored
  • Backend-e2e network tests remain #[ignore] (59) — unchanged scope

New dead-button regression suite (Wave 3):

  • src/ui/identity/home.rs::tests — 14 tests, one per HomeButton variant plus stable-mapping pins.
  • src/ui/identity/contacts.rs::tests — 5 tests covering every ContactsButton.
  • src/ui/identity/activity.rs::tests — 2 tests covering the legacy-payments link.

Feature-gate build matrix:

  • cargo build --all-features — green
  • cargo build --no-default-features — green (hub module elided, legacy path only)
  • cargo clippy --all-features --all-targets -- -D warnings — green
  • cargo +nightly fmt --all --check — green

Test plan

  • cargo build --no-default-features — compile without the hub (legacy path only)
  • cargo build --all-features — compile with everything
  • cargo clippy --all-features --all-targets -- -D warnings
  • cargo +nightly fmt --all --check
  • cargo test --all-features --workspace
  • Dead-button regression suite — every HomeButton / ContactsButton / ActivityButton variant resolves to a non-None action via its pure dispatcher
  • Manual smoke: open the app, confirm the three identity-related nav entries coexist, click Identity Hub and verify the onboarding empty state renders
  • Manual smoke: with one identity loaded, verify Home tab renders the hero + checklist + recent activity preview
  • Manual smoke: click Contacts without a DashPay profile, verify the gate card renders and its primary CTA hops to Settings
  • Manual smoke: switch between tabs several times and confirm the Contacts load task fires exactly once per tab entry (check logs)
  • Manual click-through audit of Home / Contacts / Activity / Settings: every non-gated button produces a side effect (user-owned follow-up after this update)
  • Visual review in light + dark mode

Known follow-ups

  • Wire real contact-request data into Contacts so RequestCard response flags reach DashPayTask::AcceptContactRequest / RejectContactRequest.
  • Implement DashPayTask::CancelContactRequest (parallel wave) and wire sent-requests cancel button.
  • Implement the activity aggregator and flip identity-hub-activity-feed default-on.
  • Complete breadcrumb three-segment composition (IDH-003).
  • Wire dev-mode onboarding footer links to AddNewIdentityScreen bulk path (IDH-005).
  • Wire DashPayTask::DeleteProfile (Settings tab danger zone) once the backend variant lands.
  • Wire IdentityTask::AddAlias / RemoveAlias / MakePrimaryAlias once those backend variants land.
  • Swap Contacts Scan QR to a dedicated scan screen if one ships.

🤖 Co-authored by Claudius the Magnificent AI Agent

Summary by CodeRabbit

  • New Features

    • Added a unified Identities hub with Home, Contacts, Activity, and Settings tabs for streamlined identity and profile management.
    • Implemented breadcrumb-based identity switching with wallet/profile pill navigation.
    • Added identity onboarding experience and picker UI for managing multiple identities.
    • Integrated social profile setup gating for contacts access.
  • Documentation

    • Added comprehensive design specifications, requirements, UX plans, and test case specifications for the new hub.
  • Tests

    • Added integration test coverage for hub navigation, onboarding, contacts, activity, and settings flows.

lklimek and others added 10 commits April 22, 2026 13:08
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>
@lklimek lklimek marked this pull request as draft April 23, 2026 11:33
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 23, 2026

📝 Walkthrough

Walkthrough

This 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

Cohort / File(s) Summary
Feature Flags & Build Config
Cargo.toml
Adds identity-hub (enabled by default) and identity-hub-activity-feed (depends on identity-hub); updates unexpected_cfgs allowlist for new cfg(feature) names.
Design / Requirements / Dev Plan
docs/ai-design/.../README.md, .../design-spec.md, .../01-requirements.md, .../02-ux-plan.md, .../03-test-case-spec.md, .../04-dev-plan.md
Adds comprehensive UX/design spec, Phase 1 requirements, UX plan, test-case spec, and dev plan for the Identity Hub.
User Stories & Docs
docs/user-stories.md
Adds Identities Hub user stories and records gaps/feature flags (activity aggregation gated).
App Routing / Root Integration
src/app.rs, src/ui/mod.rs
Refactors main screens initialization to conditionally register Identity Hub when feature enabled; adds RootScreenIdentityHub/ScreenType::IdentityHub mapping and screen construction/dispatch integration.
Identity Hub Module & Routing
src/ui/identity/mod.rs, src/ui/identity/hub_screen.rs, src/ui/identity/landing.rs, src/ui/identity/tabs.rs
Adds new identity hub module, IdentityHubScreen, HubLanding enum, IdentityHubTab enum, tab selection and landing logic, and ScreenLike impl.
Tab Implementations (renderers & states)
src/ui/identity/onboarding.rs, home.rs, contacts.rs, activity.rs, settings.rs, picker.rs
Implements onboarding, Home (HomeState/HomeOutcome), Contacts (ContactsState, gated vs populated), Activity (filter chips, feature-gated messaging), Settings (stateful editor + advanced section), and Picker (grid + add card).
Left-nav and Button Integration
src/ui/components/left_panel.rs
Builds mutable left-panel button list and conditionally inserts "Identity Hub" button (feature-gated) after legacy Identities entry.
Component Module Index & Docs
src/ui/components/mod.rs, src/ui/components/README.md
Exports many new component modules and documents the new breadcrumb/identity pill and identity-hub component catalog.
Breadcrumb & Identity Pills
src/ui/components/breadcrumb_pill.rs, src/ui/components/identity_pill.rs
Adds BreadcrumbPill (Interactive/Subdued/Placeholder), IdentityPill (label resolution: nickname → dpns → shortened id), responses, builders, and tests.
Identity Hub Tab Bar & Rows
src/ui/components/identity_hub_tab_bar.rs, src/ui/components/activity_row.rs
Adds IdentityHubTabBar (stateless, click response) and ActivityRow component (kinds/statuses, retry/expand actions).
Picker & Picker Card Components
src/ui/components/identity_picker_card.rs, src/ui/components/identity_picker_add_card.rs, src/ui/identity/picker.rs
Adds identity picker card, add-card, sizing/helpers, and picker grid renderer with column computation.
Hero, Contact, Request, Contact Row, Gate, Checklist Components
src/ui/components/identity_hero_card.rs, contact_row.rs, request_card.rs, social_profile_gate_card.rs, onboarding_checklist.rs
Introduces IdentityHeroCard, ContactRow, RequestCard, SocialProfileGateCard, OnboardingChecklist with responses, builders, and unit tests.
Other component additions
src/ui/components/activity_row.rs, src/ui/components/...
Multiple other identity-hub components added and exported (e.g., identity_picker_*, identity_hub_tab_bar, activity_row, contact_row, request_card).
Tests / Kittest additions
tests/kittest/*.rs, tests/kittest/main.rs, tests/kittest/identity_hub.rs
Adds kittest harness modules and tests for hub mount, onboarding, home, activity, contacts, settings, enum round-trip, and dispatcher wiring guard.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Poem

🐰 I stitched a hub with tabs that gleam,
Breadcrumbs and pills hop in a stream,
From onboarding carrots to settings bright,
Identities gathered — what a sight! ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The pull request title clearly and specifically describes the main feature delivered: a unified Identities hub with four tabs (Home, Contacts, Activity, Settings). It accurately reflects the primary change in the changeset.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/identity-hub-impl

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

❤️ Share

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

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

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 | 🟡 Minor

Add 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 | 🟡 Minor

Reserve or restore section B.5.

The document jumps from B.4.1 to B.6; add a B.5 — reserved heading 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 | 🟡 Minor

Clarify final IA versus Phase 1 coexistence.

These lines describe replacing Dashpay and Identities with a single Identities nav entry, but this PR currently adds Identity Hub alongside 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 | 🟡 Minor

Tighten the incomplete sentence.

Minimum required shown dynamically reads 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 | 🟡 Minor

Update this feature-gate note to match routing behavior.

identity-hub does more than control the left-nav entry: src/app.rs also omits RootScreenIdentityHub from main_screens when 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 | 🟡 Minor

Keep 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 | 🟡 Minor

Separate 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.rs scaffold 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 | 🟡 Minor

Make this test exercise the dispatcher it claims to guard.

The current assertion is tautological, so removing the ScreenType::create_screen arm would not fail this test as long as the enum variant remains. Either construct the existing kittest AppContext fixture and assert the created Screen::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 | 🟡 Minor

Use the hyphenated activity-feed feature name.

Line 156 uses identity_hub_activity_feed; the Cargo feature documented for the PR is identity-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 | 🟡 Minor

Use the Cargo feature name here.

Line 95 documents identity_hub_activity_feed, but the PR’s Cargo feature is identity-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 | 🟡 Minor

Use 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 with RootScreenDashpay.

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 | 🟡 Minor

Do 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 | 🟡 Minor

Point the enum mapping step at the module that owns it.

RootScreenType::to_int / from_int live in src/ui/mod.rs in the provided implementation context, not src/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 | 🟡 Minor

Align the README with the coexistence rollout.

This says Dashpay and Identities are collapsed into a single Identities nav 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 | 🟡 Minor

Correct feature flag names to match Cargo.toml throughout the document.

Feature names in Cargo.toml are identity-hub and identity-hub-activity-feed (hyphens, no prefix). Update all references:

  • Lines 66, 68: feat-identity-hubidentity-hub; identity_hub_activity_feedidentity-hub-activity-feed
  • Line 171: identity_hub_activity_feedidentity-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 | 🟡 Minor

Align the new left-nav label with the implementation plan.

This document says the new entry is also labeled Identities, while the PR summary calls it Identity Hub. Keeping two visible Identities entries 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 | 🟡 Minor

Make this test exercise IdentityHubScreen, not just the enum.

The test can pass even if IdentityHubScreen::selected_tab() or IdentityHubScreen::select_tab() is broken, because it only reassigns a local IdentityHubTab. Please instantiate the screen with the existing app-context test harness, or move the state transition into a helper that can be tested without AppContext.

🤖 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 | 🟡 Minor

Don’t mark full tab flows as implemented while they are still scaffolded.

US-IDH-002 through US-IDH-006 describe 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

📥 Commits

Reviewing files that changed from the base of the PR and between 5afd79e and 587fe4b.

📒 Files selected for processing (28)
  • Cargo.toml
  • docs/ai-design/2026-04-22-identity-dashpay-redesign/README.md
  • docs/ai-design/2026-04-22-identity-dashpay-redesign/design-spec.md
  • docs/ai-design/2026-04-22-identity-dashpay-redesign/wireframe.html
  • docs/ai-design/2026-04-23-identity-hub-impl/01-requirements.md
  • docs/ai-design/2026-04-23-identity-hub-impl/02-ux-plan.md
  • docs/ai-design/2026-04-23-identity-hub-impl/03-test-case-spec.md
  • docs/ai-design/2026-04-23-identity-hub-impl/04-dev-plan.md
  • docs/user-stories.md
  • src/app.rs
  • src/ui/components/README.md
  • src/ui/components/breadcrumb_pill.rs
  • src/ui/components/identity_pill.rs
  • src/ui/components/left_panel.rs
  • src/ui/components/mod.rs
  • src/ui/identity/activity.rs
  • src/ui/identity/contacts.rs
  • src/ui/identity/home.rs
  • src/ui/identity/hub_screen.rs
  • src/ui/identity/landing.rs
  • src/ui/identity/mod.rs
  • src/ui/identity/onboarding.rs
  • src/ui/identity/picker.rs
  • src/ui/identity/settings.rs
  • src/ui/identity/tabs.rs
  • src/ui/mod.rs
  • tests/kittest/identity_hub.rs
  • tests/kittest/main.rs

Comment thread src/app.rs Outdated
Comment thread src/ui/components/breadcrumb_pill.rs Outdated
Comment thread src/ui/components/identity_pill.rs Outdated
Comment thread src/ui/identity/hub_screen.rs Outdated
Comment thread src/ui/identity/hub_screen.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>
@lklimek lklimek marked this pull request as ready for review April 23, 2026 11:51
@thepastaclaw
Copy link
Copy Markdown
Collaborator

thepastaclaw commented Apr 23, 2026

✅ Review complete (commit b71e1fe)

lklimek and others added 14 commits April 23, 2026 15:19
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:
#	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>
lklimek and others added 2 commits April 23, 2026 15:54
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>
@lklimek lklimek changed the title feat(identity-hub): scaffold unified Identities hub (Home/Contacts/Activity/Settings) feat(identity-hub): unified Identities hub (Home · Contacts · Activity · Settings) Apr 23, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 20

🧹 Nitpick comments (4)
src/ui/components/activity_row.rs (2)

220-238: Route component styling through ComponentStyles.

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 via ComponentStyles”.

🤖 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_body and clicked_retry are public mutable fields, while action is derived from them once. Downstream mutation can make these fields disagree with has_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 exercise create_screen.

This test only compares ScreenType::IdentityHub to itself, so dropping the IdentityHub arm from ScreenType::create_screen would not fail here. Either call ScreenType::IdentityHub.create_screen(...) with a live AppContext and 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 between badge_label and draw_type_badge.

badge_label() maps IdentityType&'static str, and then draw_type_badge re-matches on those exact literals ("Masternode", "Evonode") to pick fill/stroke/text colors. If a new IdentityType variant 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 local BadgeKind) 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

📥 Commits

Reviewing files that changed from the base of the PR and between 587fe4b and 766be8e.

📒 Files selected for processing (33)
  • Cargo.toml
  • docs/ai-design/2026-04-23-identity-hub-impl/02-ux-plan.md
  • docs/ai-design/2026-04-23-identity-hub-impl/03-test-case-spec.md
  • docs/ai-design/2026-04-23-identity-hub-impl/04-dev-plan.md
  • docs/user-stories.md
  • src/app.rs
  • src/ui/components/README.md
  • src/ui/components/activity_row.rs
  • src/ui/components/breadcrumb_pill.rs
  • src/ui/components/contact_row.rs
  • src/ui/components/identity_hero_card.rs
  • src/ui/components/identity_hub_tab_bar.rs
  • src/ui/components/identity_picker_add_card.rs
  • src/ui/components/identity_picker_card.rs
  • src/ui/components/identity_pill.rs
  • src/ui/components/mod.rs
  • src/ui/components/onboarding_checklist.rs
  • src/ui/components/request_card.rs
  • src/ui/components/social_profile_gate_card.rs
  • src/ui/identity/activity.rs
  • src/ui/identity/contacts.rs
  • src/ui/identity/home.rs
  • src/ui/identity/hub_screen.rs
  • src/ui/identity/mod.rs
  • src/ui/identity/picker.rs
  • src/ui/identity/settings.rs
  • tests/kittest/identity_hub.rs
  • tests/kittest/identity_hub_activity.rs
  • tests/kittest/identity_hub_contacts.rs
  • tests/kittest/identity_hub_home.rs
  • tests/kittest/identity_hub_onboarding.rs
  • tests/kittest/identity_hub_settings.rs
  • tests/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

Comment thread docs/user-stories.md
- [Identity Operations (IDN)](#identity-operations-idn)
- [DPNS (DPN)](#dpns-dpn)
- [DashPay (DPY)](#dashpay-dpy)
- [Identities Hub (IDH)](#identities-hub-idh)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

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.

Comment on lines +401 to +458
/// 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());
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

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.

Comment on lines +20 to +34
/// 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>,
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ 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).

Comment on lines +244 to +268
/// 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()
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

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.

Comment on lines +247 to +304
/// 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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, locate the file and check its existence
find . -type f -name "onboarding_checklist.rs" | head -5

Repository: 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.rs

Repository: 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.rs

Repository: 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 -3

Repository: 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 rust

Repository: 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 -30

Repository: 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 -40

Repository: 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.toml

Repository: 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.rs

Repository: 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.rs

Repository: 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.rs

Repository: 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 -20

Repository: 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.

Suggested change
/// 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).

Comment on lines +287 to +302
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();
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

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.

Suggested change
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.

Comment on lines +647 to +651
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();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

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.

Comment on lines +56 to +68
// 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"
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
// 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).

Comment on lines +31 to +44
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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

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;
         app

Also 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.

Comment on lines +31 to +45
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);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

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");
+        }
         app

Also 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.

lklimek and others added 2 commits April 23, 2026 17:38
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>
Copy link
Copy Markdown
Collaborator

@thepastaclaw thepastaclaw left a comment

Choose a reason for hiding this comment

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

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.

Comment on lines +149 to +217
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
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🔴 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.

Comment on lines +647 to +651
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();
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🔴 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.

Comment thread src/ui/mod.rs
Comment on lines +921 to +924
Screen::IdentityHubScreen(screen) => {
screen.app_context = app_context;
return;
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🔴 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.

Comment on lines +313 to +325
// 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(),
},
)));
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🔴 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.

Comment thread src/ui/identity/home.rs
Comment on lines +177 to +194
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),
}
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🔴 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.

Comment on lines +12 to +73
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;
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🟡 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.

Comment thread src/ui/identity/home.rs
Comment on lines +163 to +170
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))
}
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🟡 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'.

Comment on lines +79 to +102
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
}
}
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🟡 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.

Comment on lines +224 to +228
ContactsButtonKind::OpenScreen(_) => {
// Not possible today (dispatcher returns SwitchHubTab), but
// exhaustive match future-proofs the gate CTA.
unreachable!("GateSetUpProfile should not map to OpenScreen");
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🟡 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.

Comment on lines +192 to +335
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),
);
}
});
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

💬 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']

@lklimek lklimek marked this pull request as draft April 27, 2026 08:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants