Replace 24h absolute auto-lock with configurable idle timer#2802
Replace 24h absolute auto-lock with configurable idle timer#2802JakeUrban wants to merge 4 commits into
Conversation
Replaces the hardcoded 24-hour 'session length' alarm with a pure idle-based auto-lock timer. The browser session locks after a configurable number of minutes of user inactivity (1, 5, 15, 30, 60), defaulting to 15. Genuine user interaction in any extension page pings the background via a new USER_ACTIVITY message, which rearms the alarm. - @shared/constants/autoLock.ts: single source of truth for valid timeouts, default, and coercion helpers. - SessionTimer: rewritten as a class that reads the persisted timeout on every reset; same-name browser.alarms.create atomically replaces the in-flight deadline. - saveSettings: validates and persists the new field; when the new timeout is shorter than the elapsed idle time on an unlocked wallet, locks immediately rather than scheduling an alarm in the past. - userActivity handler: gated by isFromExtensionPage in popupMessageListener so dApp content scripts cannot extend a session. Rejects pings when the wallet is locked. - useActivityPing + ActivityTracker: mousedown/keydown/touchstart/wheel listeners on window with 5s leading-edge throttle; only active when the wallet is unlocked. - Preferences UI: dropdown driven by VALID_AUTO_LOCK_TIMEOUT_MINUTES. - Tests cover SessionTimer, userActivity, save/load validation, and the elapsed-idle short-circuit; mock fixtures updated to include the new field. Closes #2082 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
PR Preview build is ready: https://github.com/stellar/freighter/releases/tag/untagged-0b465e898b0c0601b34c (SDF collaborators only — install instructions in the release description) |
There was a problem hiding this comment.
Pull request overview
This PR replaces Freighter’s current hardcoded 24-hour absolute session auto-lock with a configurable idle-based auto-lock timer. It introduces a shared source of truth for valid timeouts, sends throttled “user activity” pings from extension UI surfaces to the MV3 background worker, and reschedules/clears the underlying Chrome alarm as needed (including on settings changes and sign-out).
Changes:
- Add shared auto-lock timeout constants/types and persist
autoLockTimeoutMinutesvia settings load/save. - Implement idle timer reset/stop in the background (
SessionTimer+USER_ACTIVITYhandler) and propagate immediate-lock behavior on shortened timeout. - Add popup-side activity tracking + UI for selecting the timeout, plus tests/mocks updates across background and popup.
Reviewed changes
Copilot reviewed 43 out of 43 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| extension/src/popup/views/Preferences/index.tsx | Adds auto-lock timeout <Select> UI and wires the value into saveSettings. |
| extension/src/popup/views/IntegrationTest.tsx | Updates integration test settings payload to include the new timeout field. |
| extension/src/popup/views/tests/Swap.test.tsx | Updates mocked loadSettings response to include autoLockTimeoutMinutes. |
| extension/src/popup/views/tests/SignTransaction.test.tsx | Updates mocked loadSettings responses to include autoLockTimeoutMinutes. |
| extension/src/popup/views/tests/ManageAssets.test.tsx | Updates mocked loadSettings response to include autoLockTimeoutMinutes. |
| extension/src/popup/views/tests/GrantAccess.test.tsx | Updates mocked loadSettings response to include autoLockTimeoutMinutes. |
| extension/src/popup/views/tests/AddFunds.test.tsx | Updates mocked loadSettings response to include autoLockTimeoutMinutes. |
| extension/src/popup/views/tests/AccountHistory.test.tsx | Updates mocked loadSettings response to include autoLockTimeoutMinutes. |
| extension/src/popup/views/tests/AccountCreator.test.tsx | Updates mocked loadSettings response to include autoLockTimeoutMinutes. |
| extension/src/popup/views/tests/Account.test.tsx | Updates mocked loadSettings responses to include autoLockTimeoutMinutes. |
| extension/src/popup/helpers/hooks/useActivityPing.ts | New hook that throttles and sends USER_ACTIVITY pings to background while unlocked. |
| extension/src/popup/helpers/hooks/tests/useActivityPing.test.ts | Adds unit tests for throttling, event coverage, and cleanup behavior. |
| extension/src/popup/ducks/settings.ts | Plumbs autoLockTimeoutMinutes through state/thunk and propagates wasLocked to auth. |
| extension/src/popup/ducks/accountServices.ts | Adds lockAccount reducer used to flip hasPrivateKey immediately after background lock. |
| extension/src/popup/ducks/tests/settings.test.ts | Adds coverage ensuring saveSettings thunk dispatches lockAccount when wasLocked. |
| extension/src/popup/components/ActivityTracker/index.tsx | New component that mounts useActivityPing based on hasPrivateKeySelector. |
| extension/src/popup/App.tsx | Mounts <ActivityTracker /> once under the Provider to cover all extension surfaces. |
| extension/src/constants/localStorageTypes.ts | Adds AUTO_LOCK_TIMEOUT_MINUTES_ID storage key constant. |
| extension/src/background/messageListener/popupMessageListener.ts | Routes USER_ACTIVITY messages and threads sessionTimer into relevant handlers. |
| extension/src/background/messageListener/helpers/test-helpers.ts | Enhances the session timer test double to read timeout from mock storage and track calls. |
| extension/src/background/messageListener/helpers/login-all-accounts.ts | Awaits sessionTimer.startSession() in the unlock flow. |
| extension/src/background/messageListener/handlers/userActivity.ts | New handler that rearms the timer only when the background session is unlocked. |
| extension/src/background/messageListener/handlers/signOut.ts | Stops the auto-lock alarm on explicit sign-out to avoid stale alarm firing. |
| extension/src/background/messageListener/handlers/saveSettings.ts | Validates/persists timeout, reschedules or immediately locks on shortened-timeout edge cases. |
| extension/src/background/messageListener/handlers/recoverAccount.ts | Awaits sessionTimer.startSession() after account recovery unlock. |
| extension/src/background/messageListener/handlers/migrateAccounts.ts | Awaits sessionTimer.startSession() during migration flow. |
| extension/src/background/messageListener/handlers/loadSettings.ts | Loads timeout from storage via coercion and returns it in settings payload. |
| extension/src/background/messageListener/handlers/handleSignedHwPayload.ts | Treats HW signing completion as activity by resetting the idle timer. |
| extension/src/background/messageListener/handlers/createAccount.ts | Awaits sessionTimer.startSession() after creating an account. |
| extension/src/background/messageListener/handlers/confirmPassword.ts | Dispatches unlockHardwareWallet() on successful password unlock. |
| extension/src/background/messageListener/tests/userActivity.test.ts | Adds unit tests for USER_ACTIVITY behavior across locked/unlocked + HW scenarios. |
| extension/src/background/messageListener/tests/loadSaveSettings.test.ts | Extends tests to cover timeout validation, reschedule/lock behavior, and defaults. |
| extension/src/background/messageListener/tests/handleSignedHwPayload.test.ts | Adds coverage that HW signature handling resets the session timer first. |
| extension/src/background/messageListener/tests/createAccount.test.ts | Updates tests for timer signature changes (casts test timer). |
| extension/src/background/index.ts | Constructs SessionTimer with a storage access instance. |
| extension/src/background/helpers/session.ts | Replaces absolute 24h timer with idle-based SessionTimer and locks HW sessions on clear. |
| extension/src/background/helpers/tests/session.test.ts | Adds tests for SessionTimer reset/stop behavior and default coercion. |
| extension/src/background/ducks/session.ts | Adds HW-lock state, actions, and updates has-private-key selector semantics. |
| @shared/constants/services.ts | Adds SERVICE_TYPES.USER_ACTIVITY. |
| @shared/constants/autoLock.ts | New shared constants/types + coercion/validation helpers for timeout values. |
| @shared/api/types/types.ts | Adds autoLockTimeoutMinutes to Preferences (and thus Settings). |
| @shared/api/types/message-request.ts | Adds timeout field to SaveSettingsMessage and introduces UserActivityMessage. |
| @shared/api/internal.ts | Threads autoLockTimeoutMinutes through saveSettings and updates response typing. |
Comments suppressed due to low confidence (15)
extension/src/popup/views/tests/SignTransaction.test.tsx:466
- Indentation for this newly added field is inconsistent with the surrounding object literal, which makes the test fixture harder to read and may fail formatting checks.
extension/src/popup/views/tests/SignTransaction.test.tsx:606 - Indentation for this newly added field is inconsistent with the surrounding object literal, which makes the test fixture harder to read and may fail formatting checks.
extension/src/popup/views/tests/SignTransaction.test.tsx:721 - Indentation for this newly added field is inconsistent with the surrounding object literal, which makes the test fixture harder to read and may fail formatting checks.
extension/src/popup/views/tests/SignTransaction.test.tsx:844 - Indentation for this newly added field is inconsistent with the surrounding object literal, which makes the test fixture harder to read and may fail formatting checks.
extension/src/popup/views/tests/SignTransaction.test.tsx:967 - Indentation for this newly added field is inconsistent with the surrounding object literal, which makes the test fixture harder to read and may fail formatting checks.
extension/src/popup/views/tests/SignTransaction.test.tsx:1084 - Indentation for this newly added field is inconsistent with the surrounding object literal, which makes the test fixture harder to read and may fail formatting checks.
extension/src/popup/views/tests/SignTransaction.test.tsx:1207 - Indentation for this newly added field is inconsistent with the surrounding object literal, which makes the test fixture harder to read and may fail formatting checks.
extension/src/popup/views/tests/SignTransaction.test.tsx:1347 - Indentation for this newly added field is inconsistent with the surrounding object literal, which makes the test fixture harder to read and may fail formatting checks.
extension/src/popup/views/tests/SignTransaction.test.tsx:1522 - Indentation for this newly added field is inconsistent with the surrounding object literal, which makes the test fixture harder to read and may fail formatting checks.
extension/src/popup/views/tests/SignTransaction.test.tsx:1689 - Indentation for this newly added field is inconsistent with the surrounding object literal, which makes the test fixture harder to read and may fail formatting checks.
extension/src/popup/views/tests/Account.test.tsx:948 - Indentation for this newly added field is inconsistent with the surrounding object literal, which makes the test fixture harder to read and may fail formatting checks.
extension/src/popup/views/tests/Account.test.tsx:1033 - Indentation for this newly added field is inconsistent with the surrounding object literal, which makes the test fixture harder to read and may fail formatting checks.
extension/src/popup/views/tests/Account.test.tsx:1117 - Indentation for this newly added field is inconsistent with the surrounding object literal, which makes the test fixture harder to read and may fail formatting checks.
extension/src/popup/views/tests/Account.test.tsx:1200 - Indentation for this newly added field is inconsistent with the surrounding object literal, which makes the test fixture harder to read and may fail formatting checks.
extension/src/popup/views/tests/Account.test.tsx:1266 - Indentation for this newly added field is inconsistent with the surrounding object literal, which makes the test fixture harder to read and may fail formatting checks.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Preferences: provide grammatically correct English defaultValue fallbacks in formatTimeoutLabel so missing i18n keys don't render "5 minute" / "5 hour" in dev or partial locales. Keys are now scoped (autoLockTimeout.minutes / autoLockTimeout.hours) so the fallback is unambiguous. - Preferences: type autoLockTimeoutMinutesValue as string (the runtime value from Formik/<select>) and make the conversion in handleSubmit explicit rather than relying on the field type being narrower than reality. - useActivityPing: send activePublicKey as an empty string instead of null to match BaseMessage typing. The background's mismatch check (`request.activePublicKey && …`) treats empty-string exactly like the previous null (skip the check). - useActivityPing test: update expectation to match the empty string. - Prettier: reflow newly added autoLockTimeoutMinutes fields in SignTransaction.test.tsx / Account.test.tsx test fixtures (and three other lightly drifted spots prettier picked up at the same time). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… simplify userActivity Two reviewer questions on PR #2802: 1. saveSettings.ts: when the user shortens the auto-lock timeout, the save itself is a user action — we should rearm the timer with the new timeout, not synthesize an immediate lock just because the elapsed-idle time of the in-flight alarm exceeds the new threshold. Remove the elapsed-idle / immediate-lock branch entirely along with the previousAutoLockTimeoutMinutes capture, the existingAlarm inspection, and the wasLocked round-trip. 2. userActivity.ts: a USER_ACTIVITY ping cannot arrive on a locked wallet by design — the popup-side useActivityPing hook only attaches event listeners while unlocked, and popupMessageListener gates the message behind isFromExtensionPage. Drop the redundant in-handler unlocked check (and its dependencies on sessionStore / buildHasPrivateKeySelector / localStore); the handler now just delegates to sessionTimer.resetSession. Knock-on cleanups: - Remove the wasLocked field from saveSettings's return type, from the @shared/api/internal saveSettings response, and from the popup settings thunk; drop the lockAccount dispatch in that thunk. - Remove the now-unused lockAccount reducer / action from popup/ducks/accountServices. - Update tests: loadSaveSettings.test.ts now asserts that shortening the timeout rearms (rather than locking); userActivity.test.ts collapses to a single "always rearms" case; settings.test.ts verifies the popup auth slice is left untouched after save. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Internal review-fix flagged the new "rearms when timeout is shortened" test as too weak — without an alarmsGet stub the parent implementation resetSession(), so the test could not distinguish parent from current behavior. Mock alarmsGet to return an in-flight alarm whose elapsed idle time (59 min) far exceeds the new 5 min timeout: that is the exact scenario where the parent code would have synthesized an immediate lock. The current implementation must just rearm. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Summary
Today Freighter locks the wallet on a fixed 24-hour timer that starts the moment you unlock and never moves. That means two bad outcomes depending on how you use the extension:
This PR replaces that absolute timer with a pure idle timer, configurable from Settings. The wallet locks after a chosen period of inactivity, and active use keeps it unlocked indefinitely — matching the model used by MetaMask, Rabby, Phantom, Trust Wallet, and Coinbase Wallet.
What changes for users
A new Auto-lock timer control in Settings → Preferences with five presets: 1, 5, 15, 30, 60 minutes. The default is 15 minutes, which is applied to existing installs on first read (the migration is implicit — a missing or invalid stored value coerces to the default).
Behavior:
"Lock immediately on blur / window-close" is intentionally out of scope for v1. We can layer it on later as an additional toggle without redoing the timer.
How it works
The Chrome alarms API is the single source of truth for the lock deadline, which means the timer survives MV3 service-worker teardown without us having to persist anything to storage. The popup and other UI surfaces are pure activity reporters; they never hold the timer themselves.
The mechanism, end to end:
useActivityPinghook is mounted once inApp.tsx, which is the entry point for every Freighter UI surface (verified via the v2/v3 manifests and thegetURL('/index.html#…')calls that open the standalone windows). The hook listens formousemove,keydown,click,scroll, andtouchstartand sends oneUSER_ACTIVITYmessage to the background per 5 seconds (leading-edge throttle). 5 s is short enough that the 1-minute preset doesn't visibly drift; longer windows allowed >25 % drift on that preset.userActivityhandler does the minimal correct thing: if the wallet is unlocked (or unlocked-with-an-active-hardware-wallet, which is the only "unlocked but no private key" state), it callssessionTimer.resetSession(). If the wallet is locked, it returnsok: falseand does nothing — a locked wallet never has its deadline extended by a stray ping.SessionTimeris now a thin object around a single named Chrome alarm.resetSession()reads the configured timeout from storage on every call (so a settings change takes effect on the very next activity tick without any "reschedule on save" plumbing) and re-creates the alarm —alarms.createwith the same name atomically replaces any pending alarm.stopSession()clears it. The alarm handler runs the existingclearSessionpath, so the lock semantics themselves are unchanged.wasLocked: trueto the popup, which dispatches a newlockAccountreducer so the router flips to the unlock screen without waiting for the nextuseGetAppDatapoll.signOutnow also callssessionTimer.stopSession()after its existinglogOutwork, so a stale alarm can't fireclearSessionafter the user has fully signed out.Notable design choices
@shared/constants/autoLock.tsexportsVALID_AUTO_LOCK_TIMEOUT_MINUTES, the type, the default, a type guard, and a coercer. The background handlers, theSessionTimer, the Preferences UI<Select>, and the message-request types all import from this one place — there is no second list of allowed values anywhere.lastActivityAt. The alarm's scheduled time is the deadline. We rely onbrowser.alarms.get(...)when we need to compute remaining time. This avoids a class of clock-skew and stale-write bugs.resetSession()on service-worker wake. The worker wakes for non-activity reasons (network alarms, runtime messages, etc.) and rescheduling on wake would silently extend the deadline — exactly the bug we're fixing. Stale 24-hour alarms from previous builds either fire (harmless —clearSessionis idempotent on an already-locked wallet) or get overwritten by the first real activity ping.Files of interest
@shared/constants/autoLock.ts— new, the single source of truth for valid values, type, default, and coercer.extension/src/background/helpers/session.ts—SessionTimerrewritten around the storage-backed timeout;clearSessionunchanged.extension/src/background/messageListener/handlers/userActivity.ts— new, the activity-ping handler.extension/src/background/messageListener/handlers/saveSettings.ts— adds validation, the elapsed-idle short-circuit, and thewasLockedresponse.extension/src/popup/helpers/useActivityPing.ts— new, the activity hook mounted inApp.tsx.extension/src/popup/views/Preferences/index.tsx— adds the auto-lock timer<Select>.extension/src/popup/ducks/accountServices.ts— newlockAccountreducer so the popup state flips immediately on the elapsed-idle short-circuit.Testing
yarn test:ci: 960 passed, 72 skipped, 0 failed.New / updated coverage:
session.test.ts—SessionTimerreads the timeout per-call, falls back to the default on missing/invalid values, and clears the alarm onstopSession.userActivity.test.ts— new file covering the locked / unlocked / hardware-wallet-active / hardware-wallet-active-while-locked cases.loadSaveSettings.test.ts— extended withautoLockTimeoutMinutesvalidation (invalid number, non-numeric), reschedule semantics (unlocked vs locked), the elapsed-idle immediate-lock path, and rearm-with-remaining.useActivityPing.test.ts,handleSignedHwPayload.test.ts, andpopup/ducks/__tests__/settings.test.ts— new tests for the activity hook, the hardware-wallet payload path that now passes throughsessionTimer, and the popup-sidelockAccountreducer.The full
yarn build:extensionwas skipped for this PR; the existing CI run on the branch covers it.Closes #2082