Skip to content

feat(stakes): add confirm-following#392

Draft
yhabib wants to merge 11 commits into
mainfrom
feat/confirm-following
Draft

feat(stakes): add confirm-following#392
yhabib wants to merge 11 commits into
mainfrom
feat/confirm-following

Conversation

@yhabib

@yhabib yhabib commented May 20, 2026

Copy link
Copy Markdown
Collaborator

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.

Type Clean state Warning Decaying Expired
Card Screenshot 2026-05-22 at 11 22 40 Screenshot 2026-05-22 at 11 25 22 Screenshot 2026-05-22 at 11 25 04 Screenshot 2026-05-22 at 11 39 23
Modal Screenshot 2026-05-22 at 11 22 59 Screenshot 2026-05-22 at 11 25 57 Screenshot 2026-05-22 at 11 41 05 Screenshot 2026-05-22 at 11 40 17

Changes

  • Added getFollowingHealth, getSecondsUntilFollowingCleared, and supporting utilities classifying a neuron as ok / warning / expired based on votingPowerRefreshedTimestampSeconds and VotingPowerEconomics.
  • Added a 30-day proactive warning window (FOLLOWING_WARNING_WINDOW_SECONDS) so the warning fires before voting power actually starts decaying, matching nns-dapp's NOTIFICATION_PERIOD_BEFORE_REWARD_LOSS_STARTS_DAYS.
  • Added FollowingStatusBadge (renders on NeuronCard only when warning/expired) and FollowingStatusAlert (renders in all three states on the neuron detail summary view).
  • Added useRefreshVotingPower hook that calls refreshVotingPower and invalidates the neurons query; works for both controllers and hotkeys.
  • Added StakingRefreshVotingPower and StakingRefreshVotingPowerError analytics events.

yhabib added 3 commits May 20, 2026 15:33
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`.
Copilot AI review requested due to automatic review settings May 20, 2026 13:34
@yhabib yhabib requested a review from a team as a code owner May 20, 2026 13:34
@github-actions

github-actions Bot commented May 20, 2026

Copy link
Copy Markdown

📊 Build Bundle Stats

The latest build generated the following assets:

dist/index.html                                           1.92 kB │ gzip:   0.69 kB
dist/assets/index-DNsPKyiF.css                          132.23 kB │ gzip:  20.84 kB
dist/assets/externalServices-B9hWFV-7.js                  0.21 kB │ gzip:   0.17 kB
dist/assets/Spinner-De6BDKeW.js                           0.58 kB │ gzip:   0.40 kB
dist/assets/useTvlValue-We9rPk2F.js                       0.58 kB │ gzip:   0.39 kB
dist/assets/service-CTRArNXA.js                           0.60 kB │ gzip:   0.39 kB
dist/assets/useHideBalances-D_mhKfQc.js                   0.61 kB │ gzip:   0.40 kB
dist/assets/numbers-Dob7jT-c.js                           0.61 kB │ gzip:   0.37 kB
dist/assets/PageHeader-Cr3cEbbs.js                        0.75 kB │ gzip:   0.44 kB
dist/assets/Separator-DrNStHeW.js                         0.77 kB │ gzip:   0.46 kB
dist/assets/CertifiedBadge-Bo471Udu.js                    0.80 kB │ gzip:   0.47 kB
dist/assets/useIcpIndex-DkFHh6j8.js                       1.08 kB │ gzip:   0.63 kB
dist/assets/addressBook-6v-6fON7.js                       1.10 kB │ gzip:   0.70 kB
dist/assets/useCommandPaletteSettings-BIdZ6-Z6.js         1.64 kB │ gzip:   0.80 kB
dist/assets/Switch-DfXh-TPt.js                            1.65 kB │ gzip:   0.83 kB
dist/assets/CopyButton-Dc3pRCSF.js                        1.86 kB │ gzip:   0.95 kB
dist/assets/AnimatedNumber-yF9wffRb.js                    1.86 kB │ gzip:   1.04 kB
dist/assets/useGovernanceAppCanister-BWe4Aab_.js          1.92 kB │ gzip:   0.97 kB
dist/assets/useInfiniteQueryThenUpdateCall-Ox6qZELL.js    1.93 kB │ gzip:   0.96 kB
dist/assets/ToggleGroup-9_kdrkuy.js                       3.05 kB │ gzip:   1.33 kB
dist/assets/useTickerPrices-lbOBaRRY.js                   3.17 kB │ gzip:   1.51 kB
dist/assets/AmountInput-CUcZ0fz5.js                       6.37 kB │ gzip:   2.91 kB
dist/assets/useSpamFilterCanister-C1AkUmIM.js             7.20 kB │ gzip:   3.25 kB
dist/assets/QueryStates-CdZwa082.js                       7.95 kB │ gzip:   2.28 kB
dist/assets/index-5WtHqWO6.js                             8.76 kB │ gzip:   3.40 kB
dist/assets/Input-h98dU_cv.js                             9.40 kB │ gzip:   3.35 kB
dist/assets/types--dFrSfG8.js                             9.96 kB │ gzip:   4.32 kB
dist/assets/TopicFollowingAccordion-DuvyFj3c.js          10.28 kB │ gzip:   4.08 kB
dist/assets/index-D0WYLfd9.js                            14.68 kB │ gzip:   4.68 kB
dist/assets/_auth-DfJRvEAx.js                            17.10 kB │ gzip:   6.51 kB
dist/assets/index-CQCAb5k-.js                            23.48 kB │ gzip:   8.06 kB
dist/assets/DepositICPModal-Dg_yYpVs.js                  39.67 kB │ gzip:  13.88 kB
dist/assets/index-DANXjWW6.js                            41.83 kB │ gzip:  11.86 kB
dist/assets/index-DY6Yw63X.js                            49.75 kB │ gzip:  15.01 kB
dist/assets/index-LppGrDS5.js                            67.21 kB │ gzip:  20.66 kB
dist/assets/vendor-md-Dg18DQ9i.js                        89.32 kB │ gzip:  25.51 kB
dist/assets/index-B676Mwub.js                           126.05 kB │ gzip:  40.73 kB
dist/assets/vendor-tanstack-BhrjORQR.js                 126.65 kB │ gzip:  39.34 kB
dist/assets/index-W2Sg_2dG.js                           130.44 kB │ gzip:  36.97 kB
dist/assets/vendor-core-react-By6K7kM9.js               193.24 kB │ gzip:  60.69 kB
dist/assets/vendor-recharts-dOmDmOZa.js                 228.10 kB │ gzip:  65.93 kB
dist/assets/vendor-icp-CyQy9GS2.js                      404.22 kB │ gzip: 100.21 kB
dist/assets/vendor-libs-BNXP9DYS.js                     554.25 kB │ gzip: 183.46 kB

@zeropath-ai

zeropath-ai Bot commented May 20, 2026

Copy link
Copy Markdown

No security or compliance issues detected. Reviewed everything up to 3e587c2.

Security Overview
Detected Code Changes
Change Type Relevant files
Enhancement ► src/governance-app-frontend/src/common/utils/neuron.test.ts
    Add tests for new neuron utility functions
► src/governance-app-frontend/src/common/utils/neuron.ts
    Implement formatRemainingTime utility
    Implement getSecondsSinceVotingPowerRefresh utility
    Implement getSecondsUntilFollowingCleared utility
    Implement getSecondsUntilDecayStarts utility
    Implement getFollowingHealth utility
    Add FollowingHealth type
    Add VotingPowerEconomicsThresholds type
    Add FOLLOWING_WARNING_WINDOW_SECONDS constant
    Add getVotingPowerRefreshedTimestampSeconds utility
► src/governance-app-frontend/src/features/analytics/events.ts
    Add StakingRefreshVotingPower and StakingRefreshVotingPowerError events
► src/governance-app-frontend/src/features/stakes/components/FollowingStatusAlert.tsx
    Create FollowingStatusAlert component
► src/governance-app-frontend/src/features/stakes/components/FollowingStatusBadge.tsx
    Create FollowingStatusBadge component
► src/governance-app-frontend/src/features/stakes/components/FollowingStatusInline.tsx
    Create FollowingStatusInline component
► src/governance-app-frontend/src/features/stakes/components/NeuronCard.tsx
    Integrate FollowingStatusBadge component
► src/governance-app-frontend/src/features/stakes/components/neuronDetail/NeuronDetailSummaryView.tsx
    Integrate FollowingStatusAlert and FollowingStatusInline components
► src/governance-app-frontend/src/features/stakes/hooks/useRefreshVotingPower.ts
    Implement useRefreshVotingPower hook
► src/governance-app-frontend/src/i18n/en/neuron.json
    Add translations for following status components and new utility functions
Refactor ► src/governance-app-frontend/src/common/utils/neuron.ts
    Improve formatDissolveDelay logic for remaining time
Other ► src/governance-app-frontend/tests/mocks/analytics-service.ts
    Create mock for analytics service
► src/governance-app-frontend/vite.config.js
    Alias analytics service mock in vite config

Copilot AI left a comment

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.

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 on VotingPowerEconomics.
  • Adds UI surfaces: a badge on NeuronCard and 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.

Comment thread src/governance-app-frontend/src/common/utils/neuron.test.ts
yhabib added 3 commits May 20, 2026 15:42
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.
yhabib added 3 commits May 22, 2026 12:11
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.
@yhabib yhabib marked this pull request as draft May 22, 2026 10:35
…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.
@yhabib yhabib changed the title feat(stakes): add confirm-following UX to refresh voting power feat(stakes): add confirm-following May 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants