feat(stakes): add confirm-following#392
Draft
yhabib wants to merge 11 commits into
Draft
Conversation
Introduces pure helpers that classify a neuron's following health based on its `votingPowerRefreshedTimestampSeconds` and the protocol thresholds from `VotingPowerEconomics`: - `getVotingPowerRefreshedTimestampSeconds` - `getSecondsSinceVotingPowerRefresh` - `getSecondsUntilFollowingCleared` - `getFollowingHealth` returning 'ok' | 'warning' | 'expired' Per governance.proto:1596-1604, `clearFollowingAfterSeconds` is the duration of the voting-power-reduction phase, not the absolute deadline from refresh. The clear-following deadline is therefore `refresh + startReducing + clearFollowing`. nns-dapp uses the same interpretation in `isNeuronFollowingReset`. Adds `FOLLOWING_WARNING_WINDOW_SECONDS` (30 days) so the 'warning' state fires proactively, before voting power actually starts to decay — matching nns-dapp's `NOTIFICATION_PERIOD_BEFORE_REWARD_LOSS_STARTS_DAYS`. Covered by 29 unit tests including the proactive-window boundary and the in-decay window.
Adds a "confirm following" flow so users can call `refreshVotingPower` before the protocol starts decaying their voting power and clears their followees: - `FollowingStatusBadge` — compact pill on `NeuronCard`, only renders when the neuron is in 'warning' or 'expired' state. - `FollowingStatusAlert` — full alert in `NeuronDetailSummaryView` for all three states, with a CTA in the warning/expired states. Uses the Alert `success` variant for the 'ok' state. - `useRefreshVotingPower` — react-query mutation that calls `refreshVotingPower` on the governance canister and invalidates the neurons query. Works for both controllers and hotkeys (the protocol permits both). - Analytics: `StakingRefreshVotingPower` and `StakingRefreshVotingPowerError`. - i18n: all status + CTA copy lives under `neuron.followingStatus.*`. Warning copy is worded to cover both the pre-decay notice window and the active-decay window.
The Plausible tracker package's package.json doesn't expose a clean main/exports entry, so any test that transitively imports `@features/analytics/service` fails to resolve under vite/vitest. Adds a stub at `tests/mocks/analytics-service.ts` and aliases it via `vite.config.js` so test chains can import the service without hitting the real tracker. Also mocks `useGovernanceEconomics` in NeuronCard's test, now needed by the new `FollowingStatusBadge`.
📊 Build Bundle StatsThe latest build generated the following assets: |
|
✅ No security or compliance issues detected. Reviewed everything up to 3e587c2. Security Overview
Detected Code Changes
|
Contributor
There was a problem hiding this comment.
Pull request overview
Adds UX to surface “following health” for neurons (warning before decay and expired after protocol-clearing), with a one-click “Confirm following” action that calls refreshVotingPower, plus supporting utilities, analytics events, and test scaffolding for vitest.
Changes:
- Introduces following-health classification utilities (
ok/warning/expired) and time-to-clear computations based onVotingPowerEconomics. - Adds UI surfaces: a badge on
NeuronCardand a status alert on the neuron detail summary with a “Confirm following” CTA. - Adds
useRefreshVotingPower, new analytics events, a vitest-only alias + stub for analytics, and unit tests for the new neuron utilities.
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| src/governance-app-frontend/vitest.setup.ts | Minor setup tweak (whitespace). |
| src/governance-app-frontend/vite.config.js | Adds vitest-only alias to stub analytics service for tests. |
| src/governance-app-frontend/tests/mocks/analytics-service.ts | Provides a no-op analytics stub for the vitest alias. |
| src/governance-app-frontend/src/i18n/en/neuron.json | Adds copy for following status badge/alert, CTA, and errors. |
| src/governance-app-frontend/src/features/stakes/hooks/useRefreshVotingPower.ts | New mutation hook calling refreshVotingPower + invalidating neurons query. |
| src/governance-app-frontend/src/features/stakes/components/neuronDetail/NeuronDetailSummaryView.tsx | Renders FollowingStatusAlert in neuron detail summary. |
| src/governance-app-frontend/src/features/stakes/components/NeuronCard.tsx | Renders FollowingStatusBadge on neuron cards. |
| src/governance-app-frontend/src/features/stakes/components/NeuronCard.test.tsx | Mocks governance economics hook for new badge dependency. |
| src/governance-app-frontend/src/features/stakes/components/FollowingStatusBadge.tsx | New badge component for warning/expired states with tooltip. |
| src/governance-app-frontend/src/features/stakes/components/FollowingStatusAlert.tsx | New alert component with countdown + confirm action + analytics/toasts. |
| src/governance-app-frontend/src/features/analytics/events.ts | Adds refresh-voting-power analytics events. |
| src/governance-app-frontend/src/common/utils/neuron.ts | Adds following-health + time-until-cleared utilities and constants. |
| src/governance-app-frontend/src/common/utils/neuron.test.ts | Adds unit tests for the new following-health utilities. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Prettier fix.
The "Following is active" banner sat at the top of the neuron detail modal for every healthy neuron — eating prime real estate to tell users nothing was wrong. Moves the healthy state into a compact "Following status" row inside the details table (icon + "Active", with the duration in a tooltip). The prominent alert with the CTA stays only for the 'warning' and 'expired' states, where action is actually needed. - Added `FollowingStatusInline` rendering icon + label in all three states, with the full description in a tooltip. - Added a "Following status" row in `NeuronDetailSummaryView`, always visible (independent of advanced-following). - Simplified `FollowingStatusAlert` to early-return on 'ok' and drop the ok-state branches. - Removed the now-unused `alertTitleOk` i18n key; added `rowLabel` and `inlineOk` / `inlineWarning` / `inlineExpired`.
Reorders the details table so "Following" comes first and "Following status" sits right under it. Status is now always the last row of the table.
The previous 3-state model (`ok | warning | expired`) collapsed two
genuinely different protocol phases into the same "warning" state: the
proactive notice window (voting power still full, followees intact)
and the active-decay window (voting power decreasing linearly,
followees still intact). Tooltips had to say "will start decaying soon
(or already is)" to be true in both.
Splits the model into four states matching the protocol's lifecycle:
Time since refresh: 0 ── (startReducing − warningWindow) ── startReducing ── (startReducing + clearFollowing) ──►
Health value: | ok | warning | decaying | expired
- `ok` — full voting power, followees intact.
- `warning` — proactive notice; voting power still full.
- `decaying` — voting power actively dropping; followees intact.
- `expired` — followees cleared, voting power at zero.
Each state gets its own icon (ShieldCheck → Clock → TrendingDown →
AlertTriangle), color (green → amber → orange → red), label, tooltip,
and alert copy. Tooltips/descriptions now carry state-specific
duration ("time until decay starts" for warning, "time until following
cleared" for decaying), so they can be precise instead of hedging.
Adds `getSecondsUntilDecayStarts` to compute the new boundary; tests
cover all four transitions and the helper. Also switches the
remaining null/undefined checks in the public helpers to use
`isNullish` from `@dfinity/utils` to match the rest of the file.
When the modal opens on a neuron whose remaining time happens to land right on a clean day boundary, the duration label flickers between "10 days" and "9 days, 23 hours" between renders. The cause: `now = new Date()` advances by a few hundred ms across renders, and `formatDissolveDelay` is sharp at day boundaries — `864000n` renders as `"10 days"` but `863999n` renders as `"9 days, 23 hours"`. Adds `formatRemainingTime` which snaps to the nearest whole day once the duration is ≥ 1 day; sub-day values stay precise. Both sides of a second boundary now collapse to the same label, so the flicker can't appear. All FollowingStatus components switch to it for their "time remaining" labels.
In the expired state, the protocol has already cleared the neuron's followees. Calling `refreshVotingPower` alone restores voting power but does not bring the followees back — the user still has nothing to follow. The previous CTA labeled "Confirm following" was therefore misleading: there was no following to confirm. Swaps the expired-state CTA for a Link that navigates to `/voting?manageFollowing=true`, opening the follow-setup modal. That flow lets the user re-set followees and also refreshes the voting power timestamp as a side effect, fixing both halves of the problem in one motion. Matches nns-dapp's pattern: `ConfirmFollowingActionButton` for warning/decaying, `FollowNeuronsButton` for the cleared state. Copy updated to acknowledge that following needs to be set up again rather than just confirmed.
artkorotkikh-dfinity
approved these changes
May 22, 2026
…ess guard Per-state configs in the three FollowingStatus components moved from an if/else-if chain with mutable `let` declarations to an IIFE that returns the config from a switch. Each state's intent is now explicit (no trailing `else` standing in for `expired`), and a `never`-typed default case makes the build fail if a future addition to `FollowingHealth` forgets a branch. No behavior changes.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation
After a neuron goes ~6 months without a refresh, the protocol decays its voting power and eventually clears its following. Today there is no surface in the app that warns users about this — they silently stop earning rewards. This adds a status badge on each neuron card and an alert on the neuron detail view, with a one-click "Confirm following" action that calls
refreshVotingPower.Changes
getFollowingHealth,getSecondsUntilFollowingCleared, and supporting utilities classifying a neuron asok/warning/expiredbased onvotingPowerRefreshedTimestampSecondsandVotingPowerEconomics.FOLLOWING_WARNING_WINDOW_SECONDS) so the warning fires before voting power actually starts decaying, matching nns-dapp'sNOTIFICATION_PERIOD_BEFORE_REWARD_LOSS_STARTS_DAYS.FollowingStatusBadge(renders onNeuronCardonly when warning/expired) andFollowingStatusAlert(renders in all three states on the neuron detail summary view).useRefreshVotingPowerhook that callsrefreshVotingPowerand invalidates the neurons query; works for both controllers and hotkeys.StakingRefreshVotingPowerandStakingRefreshVotingPowerErroranalytics events.