Repository navigation
feat: biometric app lock with a native overlay and a read-only bridge - #181
Merged
Merged
Conversation
…only bridge Claude-Session: https://claude.ai/code/session_01QSeKYnW1Xf6HDSWz1eGAqv Co-authored-by: Claude <noreply@anthropic.com>
…ring Claude-Session: https://claude.ai/code/session_01QSeKYnW1Xf6HDSWz1eGAqv Co-authored-by: Claude <noreply@anthropic.com>
…changeset Claude-Session: https://claude.ai/code/session_01QSeKYnW1Xf6HDSWz1eGAqv Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QSeKYnW1Xf6HDSWz1eGAqv Co-authored-by: Claude <noreply@anthropic.com>
Stack both intents with #179 (navigation policy at load start) and #180 (pickImage, share, openFile): BRIDGE_METHODS carries both sets, the app lock also refuses pickImage, share and openFile while locked, every locale key from both sides is in all ten bundles, and the iOS permission texts carry camera, photos and Face ID. pnpm-lock.yaml regenerated by pnpm from main's. READ_EXTERNAL_STORAGE is no longer blocked: the image picker and the file system module declare it too. Claude-Session: https://claude.ai/code/session_01QSeKYnW1Xf6HDSWz1eGAqv Co-authored-by: Claude <noreply@anthropic.com>
Contributor
Author
|
repo:mobile#1 · os-justin · session_01QSeKYnW1Xf6HDSWz1eGAqv Seat verdict: ACCEPT, maintainer merges. The PR is over the 3000-line human-merge threshold (+2930 / −159). Head-sha: 席位意见: 实现符合裁决 A(批 #1 第 2 项)。
席位核对
|
8 of 16 tasks
This was referenced Nov 3, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #145
Clause-②: no
Metadata-protocol: none
维护者速读
lib/app-lock.ts(锁的判定和唯一写入口)、lib/shell/bridge/handler.ts(桥接只读的两个方法与锁屏时拒绝)、app/_layout.tsx(遮罩是覆盖层,不是路由)。npx jest --ci __tests__/components/ConsoleScreen.test.tsx -t "app lock",预期 5 项全过,其中一项把所有桥接方法带着「关锁」参数调用一遍,锁设置不变。What changes
Option A of the ruling on this card (
Ruling: batch #1 item 2 · letter A · maintainer 「同意」): the app-lock setting changes only on a native settings screen, behind biometrics, and the bridge can only read it.stores/security-store.ts,lib/app-lock.ts, reusing the 1.x modules). A pure machine,stepAppLock:unlocked→covered(background with the lock on) →lockedon a return past the timeout, or back tounlockedwithin it;locked→unlocking→unlockedon a successful prompt. A cold start with the lock on startslocked.inactivealone (the Face ID prompt itself, Control Center) never locks. A background seen while a prompt runs is held, because Android's passcode fallback is an activity of its own, and counts only if the app is still in the background when the prompt ends.changeAppLockSettings, asks for Face ID / Touch ID / fingerprint (device passcode as fallback), then stores, then commits. Turning the lock on needs a screen lock on the device. Only the new screenapp/app-lock-settings.tsxcalls it.components/shell/AppLockOverlay.tsx) is an opaque view drawn above the navigation stack inapp/_layout.tsx, as a sibling. It is not a route and nothing reloads, so the Console's load state machine sees no event. While it is up: the stack is hidden from screen readers, Android back does nothing (the WebView's back handler steps aside), the switcher sheet closes, and on iOS the in-app browser is dismissed (both are presented above every view).getAppLock()resolves{ enabled, timeoutSeconds };openAppLockSettings()opens the native screen. Both are announced as capabilities and pass the origin gate and the excursion gate like every method. There is no write method. While locked, methods that bring up native UI (openExternal,scan,showConnectionSwitcher, and feat(bridge): pickImage, share and openFile with bytes #180'spickImage,shareandopenFile, plusopenAppLockSettings) answerfailed("The app is locked"). The rest still answer, so a Console booting under the lock on a cold start still gets its push token.expo-screen-capture(SDK 55, new native dependency): iOS blurs the app-switcher snapshot natively on resign-active. On Android,FLAG_SECUREis set (blank recents thumbnail; it also blocks screenshots and screen recording while the lock is on). Two permissions the module declares for a screenshot listener the shell does not use,READ_MEDIA_IMAGESandDETECT_SCREEN_CAPTURE, are blocked inapp.config.ts. ItsREAD_EXTERNAL_STORAGE(API 32 and below) is not blocked, because feat(bridge): pickImage, share and openFile with bytes #180'sexpo-image-pickerandexpo-file-systemdeclare it too.permissions.faceId, the Face ID usage text written into each language'sInfoPlist.stringslike the camera's.has("openAppLockSettings")) is the follow-up consumer card.docs/HYBRID-SHELL.md: new "App lock" section, "Reading the app lock" under the bridge, the interface, Console-side integration, testing and known limits.Landing points outside the claim's file surface, and why:
app/console.tsx(the bridge capabilities are wired there),components/shell/ConnectionSwitcher.tsx(the settings entry),components/shell/ConsoleWebView.tsx(one line: its Android back handler steps aside while locked),app.config.ts(the Face ID usage string per language, and the blocked permissions) with its test__tests__/lib/app-config.test.tsfrom #180,package.jsonandpnpm-lock.yaml(expo-screen-capture, the only producer of a native app-switcher blur and ofFLAG_SECUREin this build).MDM's
lockServeris unrelated and untouched.Merged with
main(merge commit77ded7b, parents654490fandddcf2d5; no rebase). This brings in #179 (navigation policy at load start) and #180 (pickImage,share,openFile). Both intents are kept:BRIDGE_METHODScarries both sets, the iOS permission texts carry camera, photos and Face ID, and every locale key from both sides is in all ten bundles (three-way merged, key parity checked).pnpm-lock.yamlwas regenerated by pnpm frommain's and differs from it only byexpo-screen-capture. The Android back-handler line sits beside #179's guards.Tests
On merge head
77ded7b:pnpm install --frozen-lockfile: exit 0npx tsc --noEmit: exit 0npx eslint . --ext .ts,.tsx: exit 0npx jest --ci --coverage: exit 0, 49/49 suites, 1048/1048 tests. Coverage:app-lock.ts98.38 % statements,security-store.ts100 %,AppLockOverlay.tsx100 %,handler.ts100 %,protocol.ts100 %.New or extended:
security-store.test.ts(the timeout and every overlay state),app-lock.test.ts(stored record, fail-closed reads, the monitor including the held background, unlock outcomes, the writer, snapshot protection per platform),biometric-auth-level.test.ts,bridge-handler.test.ts(the two methods, the origin and excursion gates, refusals while locked includingpickImage,shareandopenFile, and the refused list pinned exactly),app-config.test.ts(Face ID text in every language, the blocked permissions),bridge-protocol.test.tsandbridge-scripts.test.ts(the lock surface is exactly the two read-only methods),AppLockOverlay.test.tsx,AppLockSettingsScreen.test.tsx,ConnectionSwitcher.test.tsx(the row),ConsoleWebView.test.tsx(back under the lock), andConsoleScreen.test.tsx"the app lock". That last one runs the Console screen's real wiring. It shows that the WebView keeps one mount, is never reloaded and is stillreadyafter a lock and unlock. It also sends every method the handler dispatches on (read from the handler's source), plus write-shaped names, with write-shaped arguments, and checks the setting did not move.Cold variant for the new component tests (no transform cache, run in band, each file alone)
On
77ded7b:The first-render cost is paid in each file's
beforeAllwarm-up (30 s budget), as in the existing component suites. Timings are from a shared container.Ablation: "no bridge method changes the lock"
Run on the pre-merge head
654490f; the merge changed neither the two tests nor the paths they exercise, apart from adding #180's methods to the lists they read. Both legs mutate the committed tree throughablation-replace.mjs(anchor must hit, blob must change, restore proven against the HEAD blob)."setAppLock"inBRIDGE_METHODS(protocol.ts, blob e1ec622 to 241b889).bridge-protocol.test.tsgoes red: "has no app-lock method beyond reading it and opening the native screen", 1 failed / 24 passed. Restored: blob == HEAD,git diff HEADempty.case "updatePreferences"to the handler (blob 5397738 to a500405) and wire it inapp/console.tsxto the store's commit (blob 27eadd2 to adc21da).ConsoleScreen.test.tsxgoes red: "no bridge method changes the lock, whatever it is sent", expected{"enabled": true, "timeoutSeconds": 300}, receivedfalse. Both files restored to their HEAD blobs;git status --porcelainempty.Acceptance notes
getAppLock()when it needs it.Verification
Local gates on
77ded7bas listed under Tests. CI on77ded7b:Lint & Type Check(lint-typecheck.yml:tsc,eslint) success;Code Quality(ci.yml:npx jest --ci --coverage) success.The pull's additions plus deletions now exceed 3,000 lines, so the maintainer merges it.
Generated by Claude Code