Skip to content

refactor: replace the lock screen with a native privacy cover - #37256

Open
Cal-L wants to merge 10 commits into
mainfrom
refactor/940-privacy-screen
Open

Cal-L wants to merge 10 commits into
mainfrom
refactor/940-privacy-screen

Conversation

@Cal-L

@Cal-L Cal-L commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

Description

The lock screen route is gone. Backgrounding the app now shows a native privacy cover (theme fill and the splash fox) and keeps it up until resume authentication finishes or the wallet is still unlocked and can continue.

Auto-lock is decided on return from wall-clock time. Immediate lock (0) locks on background. Timed locks lock only if enough time has passed when the user comes back. Never (-1) does not lock. Deeplinks that arrive during that decision wait until it finishes.

On Android, the biometric or device-credential prompt starts only after onPostResume. Leaving again before the prompt appears cancels that attempt, and the next resume starts a new one. iOS starts the prompt when the app becomes active. The cover is not shown for iOS inactive (app switcher, Control Center, Face ID).

Android 13+ blanks the Recents card with setRecentsScreenshotEnabled(false). Android 12 and older hold FLAG_SECURE from login until logout.

Card and Ramp do not pause auto-lock. Resume unlock keeps the current screen, so a verification handoff stays on that flow after authentication instead of resetting to Home. The cover still shows, and the user's lock timeout still applies.

Changelog

CHANGELOG entry: Added a privacy cover while the app is in the background and prompt unlock on return

Related issues

Fixes:

Refs: https://consensyssoftware.atlassian.net/browse/MCWP-940

Manual testing steps

Feature: privacy cover and resume unlock

  Background:
    Given the user is logged in
    And auto-lock is set to Immediately

  Scenario: cover appears only after the app is fully backgrounded
    When the user opens the app switcher or Control Center on iOS
    Then the privacy cover is not shown
    When the user backgrounds the app
    Then the privacy cover is shown
    And the wallet contents are not visible

  Scenario: resume prompts unlock and then dismisses the cover
    Given the app is backgrounded and the privacy cover is showing
    When the user returns to the app
    Then a biometric or device-credential prompt is shown
    When authentication succeeds
    Then the privacy cover is dismissed
    And the user is back on the screen they left

  Scenario: leaving again before the prompt appears still unlocks on the next return
    Given auto-lock is Immediate on Android
    When the user backgrounds the app, returns, and backgrounds again before the prompt appears
    And the user returns once more
    Then a new authentication prompt is shown
    And the privacy cover stays up until authentication finishes

  Scenario: timed lock does not lock before the timeout
    Given auto-lock is set to 30 seconds
    When the user backgrounds the app and returns before 30 seconds
    Then the wallet stays unlocked
    And the privacy cover is dismissed
    When the user backgrounds the app and returns after 30 seconds
    Then an authentication prompt is shown

  Scenario: never lock still covers the app
    Given auto-lock is set to Never
    When the user backgrounds the app and returns
    Then the privacy cover was shown while backgrounded
    And the wallet stays unlocked without an authentication prompt

  Scenario: a deeplink waits until unlock finishes
    Given auto-lock is Immediate
    And the app is backgrounded
    When a deeplink is delivered and the user returns and unlocks
    Then the deeplink is handled after unlock

Screenshots/Recordings

Before

N/A

After

iOS

ios-privacy-screen.mov

Android

android-privacy-screen.mov

Pre-merge author checklist

Performance checks (if applicable)

  • I've tested on Android
    • Ideally on a mid-range device; emulator is acceptable
  • I've tested with a power user scenario
    • Use these power-user SRPs to import wallets with many accounts and tokens
  • I've instrumented key operations with Sentry traces for production performance metrics

For performance guidelines and tooling, see the Performance Guide.

Pre-merge reviewer checklist

  • I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed).
  • I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots.

Note

High Risk
Changes wallet security UX (lock timing, resume auth, deeplink ordering) and native lifecycle integration on both platforms; regressions could leak UI in Recents or strand users behind the cover or wrong navigation.

Overview
Replaces the LockScreen navigation route with a native privacy overlay (splash-themed cover on Android onPause / iOS applicationDidEnterBackground) that JS dismisses via PrivacyCoverModule after resume handling completes.

Introduces AppLockService as the single owner of background/foreground behavior: privacy cover timing (minimum visible duration), auto-lock from settings.lockTime (immediate lock on background, timed lock evaluated on resume, never off), biometric unlock with navigationBehavior: 'preserve' so users return to the same screen, deeplink holds until lock/unlock settles, and Android waitUntilAuthenticationReady tied to onPostResume. LockManagerService, AppStateService, and lock-screen sagas are removed; auth saga only calls AppLockService.initialize/start/stop on login lifecycle.

Authentication.tryBiometricUnlock centralizes cold start and resume prompts. Card and Ramp no longer pause auto-lock. Android 13+ uses setRecentsScreenshotEnabled(false); older Android holds FLAG_SECURE for the logged-in session via PreventScreenshot.

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

Backgrounding should hide the wallet immediately, and resume authentication has to wait until Android will actually show the prompt.

Co-authored-by: Cursor <cursoragent@cursor.com>
@Cal-L Cal-L self-assigned this Oct 7, 2026
@github-actions

github-actions Bot commented Oct 7, 2026

Copy link
Copy Markdown
Contributor

CLA Signature Action: All authors have signed the CLA. You may need to manually re-run the blocking PR check if it doesn't pass in a few minutes.

@github-actions

github-actions Bot commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

Smart E2E Test Selection

Selected E2E tags: ALL

Selected Performance tags

@PerformanceLogin
@PerformanceLaunch

AI Confidence: 100

E2E reasoning

Expand to read

This PR fundamentally rewrites the app lock, privacy cover, and authentication resume system — one of the most security-critical flows in the wallet. Key changes:

  1. Authentication core (Authentication.ts): New tryBiometricUnlock() method and navigationBehavior: 'preserve' option change how unlock/navigation works on resume. This affects every flow that requires authentication.

  2. AppLockService (new, 514 lines): Replaces both LockManagerService and AppStateService. Handles app state transitions, privacy cover show/hide, auto-lock timing, biometric unlock on resume, and deeplink hold. This is the central coordinator for the lock lifecycle.

  3. LockScreen route removed: Routes.LOCK_SCREEN is deleted from Routes.ts, App.tsx removes the screen from the navigator, and LockScreen/index.tsx is deleted. The lock UX is now handled by a native privacy cover overlay instead of a React Native screen. This changes how the app appears when locked and how navigation behaves.

  4. Native modules added (iOS Swift + Android Kotlin): New PrivacyCover native modules show a native overlay when the app backgrounds. This is a new native integration that affects every platform.

  5. index.js: AppLockService.initialize() is called at app startup — this is the cold-start path.

  6. store/sagas/index.ts: ~140 lines of app state/lock saga logic removed and replaced by AppLockService. The saga no longer manages the lock state machine.

  7. SDKConnect files: Routes.LOCK_SCREEN removed from skip/wait route lists — SDK connect behavior during lock state changes.

  8. Card/Ramp routes: LockManagerService.stopListening/startListening calls removed — auto-lock is no longer paused during Card/Ramp flows (noted as an open PR comment concern).

  9. Performance test: Android warm-start timer threshold increased from 3000ms to 3500ms — acknowledges the new lock flow takes longer on Android.

Impact on E2E tests: This change affects:

  • Login/unlock flows (SmokeAccounts, SmokeWalletPlatform)
  • App startup and cold/warm start (all flows)
  • Authentication after backgrounding (all flows)
  • SDK Connect flows (SmokeMMConnect, SmokeMultiChainAPI, SmokeNetworkExpansion)
  • Card and Ramp flows (SmokeMoney)
  • Deeplink handling (SmokeSwap, SmokeWalletPlatform)
  • Confirmations after lock/unlock (SmokeConfirmations)
  • Seedless onboarding (SmokeSeedlessOnboarding)

The hard rule seed already selected ALL tags, which is correct given the critical nature of these changes. The lock/authentication system is foundational — any regression could break every user flow that requires the app to be unlocked. Running ALL E2E tags is the only safe approach.

Performance reasoning

Expand to read

Two performance areas are directly impacted: 1) @PerformanceLogin — the unlock/biometric flow is completely rewritten. The new AppLockService handles biometric unlock on resume differently (native privacy cover + waitUntilAuthenticationReady on Android), and the performance test warm-start-to-login.spec.ts already has its Android threshold increased from 3000ms to 3500ms, confirming the team expects measurable timing changes. 2) @PerformanceLaunch — AppLockService.initialize() is now called in index.js at cold start, adding a new AppState listener subscription to the startup path. The privacy cover native module initialization also happens earlier. These changes could affect cold-start and warm-start times. Other performance tags (@PerformanceAccountList, @PerformanceSwaps, @PerformanceAssetLoading, etc.) are not directly impacted by the lock/privacy cover changes.

View run

@Cal-L
Cal-L deployed to build-e2e October 7, 2026 03:44 — with GitHub Actions Active
@Cal-L
Cal-L deployed to build-e2e October 7, 2026 03:44 — with GitHub Actions Active
@Cal-L
Cal-L deployed to build-e2e October 7, 2026 05:42 — with GitHub Actions Active
@Cal-L
Cal-L deployed to build-e2e October 7, 2026 05:42 — with GitHub Actions Active
@Cal-L
Cal-L marked this pull request as ready for review October 7, 2026 21:42
@Cal-L
Cal-L requested review from a team as code owners October 7, 2026 21:42
@Cal-L Cal-L added needs-dev-review PR needs reviews from other engineers (in order to receive required approvals) team-mobile-platform Mobile Platform team No QA Needed Apply this label when your PR does not need any QA effort. labels Oct 7, 2026

@cursor cursor Bot 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.

Stale Bugbot comment from a previous run.

}}
/>
</RootStack.Navigator>
);

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.

Card and Ramp drop auto-lock pause

Medium Severity

Card and Ramp no longer pause auto-lock during verification handoff. The old stopListening / startListening calls were removed, and AppLockService never gained the dangerousPauseAutoLock / dangerousResumeAutoLock API the PR says still exists. Leaving the app for email, SMS, camera, or bank verification now locks on Immediate (and on timed lock after the timeout), and the biometric prompt on return can interrupt the handoff.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 7f86533. Configure here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This removal is intentional. Resume unlock uses navigationBehavior: 'preserve', so returning from a verification handoff stays on the Card or Ramp screen after authentication instead of resetting to Home. The privacy cover still shows, and the user's lock timeout still applies. The PR description no longer says dangerousPauseAutoLock / dangerousResumeAutoLock exist.

Comment thread ios/MetaMask/NativeModules/PrivacyCover/PrivacyCover.swift
@Cal-L
Cal-L deployed to build-e2e October 7, 2026 21:51 — with GitHub Actions Active
@Cal-L
Cal-L deployed to build-e2e October 7, 2026 21:51 — with GitHub Actions Active
@metamask-ci

metamask-ci Bot commented Oct 7, 2026

Copy link
Copy Markdown
Contributor

PR template — items to address before "Ready for review"

Warnings — informational, address before merging:

  • Pre-merge author checklist has unchecked items (e.g. "I've tested with a power user scenario"). Every box must be consciously checked — see docs/readme/ready-for-review.md.

See docs/readme/ready-for-review.md for the full Definition of Ready for Review.

@Cal-L
Cal-L deployed to build-e2e October 8, 2026 16:47 — with GitHub Actions Active
@Cal-L
Cal-L deployed to build-e2e October 8, 2026 16:48 — with GitHub Actions Active

@cursor cursor Bot 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.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 2 total unresolved issues (including 1 from previous review).

Fix All in Cursor

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

Reviewed by Cursor Bugbot for commit 930749e. Configure here.

Comment thread android/app/src/main/java/io/metamask/nativeModules/PrivacyCover.kt
@Cal-L
Cal-L deployed to build-e2e October 8, 2026 18:05 — with GitHub Actions Active
@Cal-L
Cal-L deployed to build-e2e October 8, 2026 18:05 — with GitHub Actions Active
@Cal-L
Cal-L deployed to build-e2e October 8, 2026 20:20 — with GitHub Actions Active
@Cal-L Cal-L added the DO-NOT-MERGE Pull requests that should not be merged label Oct 8, 2026
@Cal-L
Cal-L deployed to build-e2e October 8, 2026 20:20 — with GitHub Actions Active
@github-actions

github-actions Bot commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

⚡ Performance Test Results

ℹ️ Performance test results are currently non-blocking and will not block this PR.

⚠️ Results incomplete — No test results found


Branch: refactor/940-privacy-screen · Build: E2E · Commit: 60039a4 · View full run

@jasonculbertson

Copy link
Copy Markdown
Contributor

Accessibility

Native privacy cover replaces the old LockScreen fox loader — nice direction. One TalkBack gap on Android vs what iOS already does:

Android cover doesn't block AT on the app underneath
PrivacyCover.kt sets importantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS on the overlay. That hides the cover itself from TalkBack, so the screen reader can still reach (and act on) the wallet UI sitting under the splash while the vault is locked. iOS already avoids this with accessibilityViewIsModal = true on the cover window and root.

While the cover is shown, mark the underlying content unavailable to TalkBack (or treat the cover as a modal layer the way iOS does), then restore a11y when hide() runs. Optionally give the cover a short locked-state announcement so VoiceOver/TalkBack users know why the UI is blocked.

android/app/src/main/java/io/metamask/nativeModules/PrivacyCover.kt
ios/MetaMask/NativeModules/PrivacyCover/PrivacyCover.swift (reference)

Happy to take another look.

@sonarqubecloud

sonarqubecloud Bot commented Oct 8, 2026

Copy link
Copy Markdown

This branch had an error being deployed

1 failed deployment
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

DO-NOT-MERGE Pull requests that should not be merged needs-dev-review PR needs reviews from other engineers (in order to receive required approvals) No QA Needed Apply this label when your PR does not need any QA effort. size-XL team-mobile-platform Mobile Platform team

Projects

Status: Needs dev review

Development

Successfully merging this pull request may close these issues.

2 participants