Skip to content

Replace 24h absolute auto-lock with configurable idle timer#2802

Open
JakeUrban wants to merge 4 commits into
masterfrom
plan-do-review/issue-2082
Open

Replace 24h absolute auto-lock with configurable idle timer#2802
JakeUrban wants to merge 4 commits into
masterfrom
plan-do-review/issue-2082

Conversation

@JakeUrban
Copy link
Copy Markdown
Contributor

@JakeUrban JakeUrban commented May 21, 2026

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:

  • Active users get bounced. Someone using Freighter steadily through a workday can hit the 24-hour cutoff mid-flow — typically while signing a transaction — and suddenly see a password prompt with no obvious cause.
  • Idle sessions stay unlocked. Someone who unlocks Freighter and then walks away keeps a fully unlocked wallet for the rest of the day. There is no idle protection.

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:

  • Any interaction with a Freighter UI surface — popup, sidebar, the standalone signing window, the grant-access window — counts as activity and pushes the lock deadline forward.
  • While no Freighter surface is open, no activity is reported and the timer runs down naturally. This is what makes it a true idle timer rather than a "while-the-popup-is-open" timer.
  • Lowering the timeout in Settings takes effect immediately. If the new timeout is shorter than the time you've already been idle, the wallet locks right then and the UI flips to the unlock screen without a poll delay.
  • Signing out (explicit lock) and the idle timer firing remain distinct paths — signing out still fully resets temp state; the idle lock keeps the existing partial-reset semantics.

"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:

  1. Activity capture. A small useActivityPing hook is mounted once in App.tsx, which is the entry point for every Freighter UI surface (verified via the v2/v3 manifests and the getURL('/index.html#…') calls that open the standalone windows). The hook listens for mousemove, keydown, click, scroll, and touchstart and sends one USER_ACTIVITY message 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.
  2. Background handling. The new userActivity handler 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 calls sessionTimer.resetSession(). If the wallet is locked, it returns ok: false and does nothing — a locked wallet never has its deadline extended by a stray ping.
  3. The timer itself. SessionTimer is 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.create with the same name atomically replaces any pending alarm. stopSession() clears it. The alarm handler runs the existing clearSession path, so the lock semantics themselves are unchanged.
  4. Settings save edge case. When the user shrinks the timeout in Settings, we compare elapsed-idle-time against the new timeout. If they've already been idle longer than the new threshold, we lock immediately and return wasLocked: true to the popup, which dispatches a new lockAccount reducer so the router flips to the unlock screen without waiting for the next useGetAppData poll.
  5. Sign-out. signOut now also calls sessionTimer.stopSession() after its existing logOut work, so a stale alarm can't fire clearSession after the user has fully signed out.

Notable design choices

  • Single source of truth for valid timeout values. A new @shared/constants/autoLock.ts exports VALID_AUTO_LOCK_TIMEOUT_MINUTES, the type, the default, a type guard, and a coercer. The background handlers, the SessionTimer, the Preferences UI <Select>, and the message-request types all import from this one place — there is no second list of allowed values anywhere.
  • No persisted lastActivityAt. The alarm's scheduled time is the deadline. We rely on browser.alarms.get(...) when we need to compute remaining time. This avoids a class of clock-skew and stale-write bugs.
  • MV3 bootstrap behavior. We deliberately do not call 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 — clearSession is idempotent on an already-locked wallet) or get overwritten by the first real activity ping.
  • 1-minute preset is not gated. Anyone who genuinely wants 1-minute auto-lock is a legitimate (security-conscious) user, not a misconfiguration to defend against.

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.tsSessionTimer rewritten around the storage-backed timeout; clearSession unchanged.
  • 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 the wasLocked response.
  • extension/src/popup/helpers/useActivityPing.ts — new, the activity hook mounted in App.tsx.
  • extension/src/popup/views/Preferences/index.tsx — adds the auto-lock timer <Select>.
  • extension/src/popup/ducks/accountServices.ts — new lockAccount reducer 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.tsSessionTimer reads the timeout per-call, falls back to the default on missing/invalid values, and clears the alarm on stopSession.
  • userActivity.test.ts — new file covering the locked / unlocked / hardware-wallet-active / hardware-wallet-active-while-locked cases.
  • loadSaveSettings.test.ts — extended with autoLockTimeoutMinutes validation (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, and popup/ducks/__tests__/settings.test.ts — new tests for the activity hook, the hardware-wallet payload path that now passes through sessionTimer, and the popup-side lockAccount reducer.

The full yarn build:extension was skipped for this PR; the existing CI run on the branch covers it.


Closes #2082

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>
Copilot AI review requested due to automatic review settings May 21, 2026 21:29
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 21, 2026

PR Preview build is ready: https://github.com/stellar/freighter/releases/tag/untagged-0b465e898b0c0601b34c (SDF collaborators only — install instructions in the release description)

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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 autoLockTimeoutMinutes via settings load/save.
  • Implement idle timer reset/stop in the background (SessionTimer + USER_ACTIVITY handler) 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.

Comment thread extension/src/popup/views/Preferences/index.tsx
Comment thread extension/src/popup/views/Preferences/index.tsx
Comment thread extension/src/popup/helpers/hooks/useActivityPing.ts
Comment thread extension/src/popup/helpers/hooks/__tests__/useActivityPing.test.ts
Comment thread extension/src/popup/views/__tests__/SignTransaction.test.tsx Outdated
Comment thread extension/src/popup/views/__tests__/Account.test.tsx Outdated
- 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>
Copilot AI added 2 commits May 21, 2026 17:21
… 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>
Comment thread extension/src/background/messageListener/handlers/saveSettings.ts Outdated
Comment thread extension/src/background/messageListener/handlers/userActivity.ts Outdated
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.

Use idle-based timer for auto-lock behavior

3 participants