Skip to content

Fix password recovery reminder triggering - #6177

Open
j0ntz wants to merge 3 commits into
developfrom
jon/pw-recovery-reminder-trigger
Open

Fix password recovery reminder triggering#6177
j0ntz wants to merge 3 commits into
developfrom
jon/pw-recovery-reminder-trigger

Conversation

@j0ntz

@j0ntz j0ntz commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Description

The password-recovery reminder is meant to fire once per balance milestone ($20 / $200 / $2,000 / $20,000 / $200,000) for accounts with no recovery key. It rarely did, for these reasons:

  1. It only ran on newTransactions. checkPasswordRecovery had exactly one dispatch site, inside the wallet.on('newTransactions') handler. It now also runs on each exchange-rate refresh, so funds that arrived while the app was closed are noticed.
  2. The rates could be missing. getExchangeRate returns 0 for a rate it has not loaded, so a partly-loaded rate set undercounts the total and credits the wrong milestone. The thunk now defers while a funded wallet's NATIVE currency has no USD rate, and picks the check back up on a later pass. Only native currencies are checked, and the deferral is bounded to 5 minutes from the account's first check: a token the rates server does not price, or a chain it never prices (a funded bitcointestnet wallet is a real example on a test account), never gets a rate, so an unbounded wait on one would suppress the reminder for the life of the account. The window is keyed on the account object, not on process start, so it belongs to the login session: sitting on the login screen, or logging into a second account later in the same session, each get their own full window.
  3. Only the last transaction counted. The guard was !transactions[finalTxIndex].isSend, so a batch whose last element was a send skipped the check even when the batch contained receives. It now uses the already-computed receivedTxs.

Trigger paths, before and after

Before, every path to the check was gated on something that was often false:

sequenceDiagram
    autonumber
    participant Chain
    participant Wallet
    participant ACM as AccountCallbackManager
    participant Check as checkPasswordRecovery

    Chain-->>Wallet: deposit lands while the app is closed
    Note over Wallet,ACM: no listener attached, so no event
    Wallet-->>ACM: app reopens, wallet syncs
    ACM--xCheck: never dispatched, the only call site was newTransactions

    Chain-->>Wallet: deposit while the app is running
    Wallet->>ACM: newTransactions([receive, send])
    ACM--xCheck: skipped, guard read transactions[last].isSend

    Chain-->>Wallet: deposit on a cold start
    Wallet->>ACM: newTransactions([receive])
    ACM->>Check: dispatch
    Note over Check: rates not loaded yet, so the total is 0<br/>lt(0, "20") returns early, and nothing re-runs it
Loading

After, the rate-refresh cycle gives the check a second, unconditional entry point, and the two broken guards are fixed:

sequenceDiagram
    autonumber
    participant Chain
    participant Wallet
    participant ACM as AccountCallbackManager
    participant Check as checkPasswordRecovery

    Chain-->>Wallet: deposit lands while the app is closed
    Wallet-->>ACM: app reopens, wallet syncs

    loop every rate refresh (30s)
        ACM->>Check: dispatch
        alt a funded wallet's native currency has no rate (first 5 min)
            Note over Check: defer to the next refresh
        else rates complete
            Check->>Check: mark every crossed level shown
            Check-->>ACM: show one reminder modal
        end
    end

    Chain-->>Wallet: deposit while the app is running
    Wallet->>ACM: newTransactions([receive, send])
    ACM->>Check: dispatch, guard is now receivedTxs.length > 0
Loading

The above also forces two supporting changes:

  • The level loop marked only the lowest crossed level and returned, so an account that jumped straight to $500 earned two modals back to back. Now every crossed level is marked and a single modal is shown. writePasswordRecoveryReminders takes a level array so the marks are one read-modify-write, not a race.
  • Light accounts (account.username == null) are skipped. They have no password to recover, and they already get the backup modal from the same handler.
  • Because the check now runs on every 30-second rate refresh for the life of the session, it exits before both wallet walks once every level has already been shown.

Asana: https://app.asana.com/0/1215088146871429/1211152484915503

CHANGELOG

Does this branch warrant an entry to the CHANGELOG?

  • Yes
  • No

Dependencies

none

Requirements

If you have made any visual changes to the GUI. Make sure you have:

  • Tested on iOS device
  • Tested on Android device
  • Tested on small-screen device (iPod Touch)
  • Tested on large-screen device (tablet)

Note

Medium Risk
Changes when and how often a user-facing security modal appears and ties logic to exchange-rate timing; scope is localized but affects all funded accounts without recovery.

Overview
Fixes the password recovery reminder so it reliably appears at each USD balance milestone ($20–$200k) when the account has no recovery key.

checkPasswordRecovery now runs after exchange-rate refreshes as well as on incoming receives, so balances that changed while the app was closed are evaluated once rates load. It waits up to five minutes per login for USD rates on funded wallets’ native currencies (tokens/chains the server never prices don’t block forever), skips light accounts, marks all newly crossed milestones in one write, and shows one modal. AccountCallbackManager triggers the check on any receive in a newTransactions batch (not only the last tx) and again after updateExchangeRates. writePasswordRecoveryReminders accepts multiple levels. Adds unit tests.

Reviewed by Cursor Bugbot for commit 09c01b6. Bugbot is set up for automated code reviews on this repo. Configure here.

@j0ntz

j0ntz commented Aug 28, 2026

Copy link
Copy Markdown
Contributor Author

📸🪓 Test evidence

🪓 Hack-forced evidence: commented out the newTransactions dispatch and forced passwordRecoveryRemindersShown to all-false, so the rate-refresh trigger was the only one that could fire; both edits reverted after capture. Temporary uncommitted edit, reverted before commit; the marked frames prove the rendering, not the trigger.

recovery reminder on funded account

recovery reminder on funded account

🪓 HACK-FORCED: rate refresh trigger

🪓 HACK-FORCED: rate refresh trigger

Captured by the agent's in-app test run (build-and-test).

@j0ntz
j0ntz force-pushed the jon/pw-recovery-reminder-trigger branch from a8e87e6 to 4f3fd7c Compare August 28, 2026 01:30
@j0ntz
j0ntz marked this pull request as ready for review August 28, 2026 01:30
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

Comment thread src/actions/RecoveryReminderActions.tsx Outdated
@j0ntz
j0ntz force-pushed the jon/pw-recovery-reminder-trigger branch 2 times, most recently from 4872bc3 to b57b035 Compare September 1, 2026 19:30

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Claude Code Review

Claude Code Review is paused for this repository. To reconnect it, an admin of this repository's GitHub organization (or the account owner, for personal repositories) who can also manage your Claude organization's Code Review settings needs to re-link GitHub in Code Review settings. This is a one-time step.

Tip: disable this comment in your organization's Code Review settings.

@j0ntz

j0ntz commented Sep 1, 2026

Copy link
Copy Markdown
Contributor Author

📸🪓 Test evidence (after review fix)

🪓 Hack-forced evidence: temporary uncommitted ButtonsModal instrumentation inside checkPasswordRecovery that printed the gate inputs on edge-funds; reverted, tree clean. Temporary uncommitted edit, reverted before commit; the marked frames prove the rendering, not the trigger.

recovery reminder after review fix

recovery reminder after review fix

🪓 HACK-FORCED: unpriced funded balances diag

🪓 HACK-FORCED: unpriced funded balances diag

Captured by the agent's in-app test run (build-and-test).

Comment thread src/actions/RecoveryReminderActions.tsx Outdated
Comment thread src/actions/RecoveryReminderActions.tsx Outdated
@j0ntz
j0ntz force-pushed the jon/pw-recovery-reminder-trigger branch from b57b035 to 751f47c Compare September 3, 2026 20:39

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Want higher recall? High effort reviews run extra passes and find more bugs. A team admin can switch effort levels in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 751f47c. Configure here.

Comment thread src/actions/RecoveryReminderActions.tsx
Comment thread src/actions/RecoveryReminderActions.tsx
@j0ntz
j0ntz force-pushed the jon/pw-recovery-reminder-trigger branch from 751f47c to 09c01b6 Compare September 3, 2026 21:16
@j0ntz

j0ntz commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

📸 Test evidence (after review fixes)

recovery reminder second login same session

recovery reminder second login same session

Captured by the agent's in-app test run (build-and-test).

@j0ntz
j0ntz force-pushed the jon/pw-recovery-reminder-trigger branch from 09c01b6 to 6b0c908 Compare September 3, 2026 22:03
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.

2 participants