From 2f70444071b191f19f2ad04a9903490bf18bbf5f Mon Sep 17 00:00:00 2001 From: "Leon.Schmidt" Date: Sat, 15 Aug 2026 22:20:06 +0200 Subject: [PATCH] FIX - modernize lock screen passcode flow Rebuild device unlock around the current iOS-style wallpaper, outlined keypad, telephone letters, status bar, and keyboard support. Require the passcode only for the first unlock of each open device session so a soft lock from the status bar returns directly after the session is authenticated. --- frontend/src/App.vue | 9 +- .../AppDevelopmentPreview.contract.test.ts | 13 + .../components/PhonePasscode.contract.test.ts | 34 ++ frontend/src/components/PhonePasscode.vue | 393 +++++++++++++----- 4 files changed, 353 insertions(+), 96 deletions(-) create mode 100644 frontend/src/components/PhonePasscode.contract.test.ts diff --git a/frontend/src/App.vue b/frontend/src/App.vue index d5e4968..040d242 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -321,6 +321,7 @@ const passcodeError = ref('') const passcodeResetKey = ref(0) const passcodeRetrySeconds = ref(0) const passcodeVisible = ref(false) +const passcodeRequired = ref(false) const setupPreviewDismissed = ref(false) const pendingUnlockRoute = ref(null) const unlockedServicesLoaded = ref(false) @@ -521,6 +522,7 @@ function completePhoneSetup(): void { isLocked.value = false isUnlocking.value = false passcodeVisible.value = false + passcodeRequired.value = false controlCenterOpened.value = false void router.replace('/') loadUnlockedPhoneData() @@ -1116,6 +1118,7 @@ function finishUnlock(): void { isUnlocking.value = true isLocked.value = false passcodeVisible.value = false + passcodeRequired.value = false passcodeError.value = '' unlockTimer = window.setTimeout(() => { @@ -1132,7 +1135,7 @@ function finishUnlock(): void { function unlockPhone(): void { if (!isLocked.value) return - if (phone.security.enabled) { + if (phone.security.enabled && passcodeRequired.value) { passcodeError.value = '' passcodeVisible.value = true return @@ -1219,6 +1222,7 @@ function lockPhone(): void { passcodeVisible.value = false passcodeBusy.value = false passcodeError.value = '' + passcodeRequired.value = false pendingUnlockRoute.value = null isLocked.value = true } @@ -1336,6 +1340,7 @@ watch( passcodeVisible.value = false passcodeBusy.value = false passcodeError.value = '' + passcodeRequired.value = false pendingUnlockRoute.value = null unlockedServicesLoaded.value = false if (passcodeLockTimer !== undefined) { @@ -1347,6 +1352,7 @@ watch( isLocked.value = setupRequired.value ? false : !isDevelopment || developmentLockScreenPreview + passcodeRequired.value = isLocked.value && phone.security.enabled unlockedServicesLoaded.value = false controlCenterOpened.value = false weather.start() @@ -1519,6 +1525,7 @@ onBeforeUnmount(() => { :disabled="passcodeRetrySeconds > 0" :error="passcodeError" :length="phone.security.length ?? 6" + lock-screen :reset-key="passcodeResetKey" :subtitle=" passcodeRetrySeconds > 0 diff --git a/frontend/src/AppDevelopmentPreview.contract.test.ts b/frontend/src/AppDevelopmentPreview.contract.test.ts index 130b8cd..06a3c69 100644 --- a/frontend/src/AppDevelopmentPreview.contract.test.ts +++ b/frontend/src/AppDevelopmentPreview.contract.test.ts @@ -22,6 +22,19 @@ describe('browser development preview contract', () => { expect(source).toContain('else loadUnlockedPhoneData()') }) + it('requires the passcode only for the first unlock of a device session', () => { + expect(source).toContain('const passcodeRequired = ref(false)') + expect(source).toContain( + 'if (phone.security.enabled && passcodeRequired.value)', + ) + expect(source).toContain( + 'passcodeRequired.value = isLocked.value && phone.security.enabled', + ) + expect(source).toMatch( + /function lockPhone\(\): void \{[\s\S]*?passcodeRequired\.value = false[\s\S]*?isLocked\.value = true/, + ) + }) + it('keeps hairlines at one rendered device pixel through phone zoom', () => { expect(source).toContain( '...getHairlinePixelStyle(phoneZoom.value, browserDevicePixelRatio.value)', diff --git a/frontend/src/components/PhonePasscode.contract.test.ts b/frontend/src/components/PhonePasscode.contract.test.ts new file mode 100644 index 0000000..59c6b88 --- /dev/null +++ b/frontend/src/components/PhonePasscode.contract.test.ts @@ -0,0 +1,34 @@ +import { readFileSync } from 'node:fs' + +import { describe, expect, it } from 'vitest' + +const source = readFileSync( + new URL('./PhonePasscode.vue', import.meta.url), + 'utf8', +) + +describe('PhonePasscode iOS presentation contract', () => { + it('uses the Lock Screen wallpaper and status bar for device unlocks', () => { + expect(source).toContain('lockScreen?: boolean') + expect(source).toContain('settings.lockWallpaperImageUrl') + expect(source).toContain( + 'wallpaper--${phone.preferences.settings.lockWallpaper}', + ) + expect(source).toContain(' { + expect(source).toContain("{ digit: 2, letters: 'ABC' }") + expect(source).toContain("{ digit: 7, letters: 'PQRS' }") + expect(source).toContain('min-height: var(--sky-touch-target)') + expect(source).toContain('passcode-screen__footer') + expect(source).toContain('--passcode-key-background: transparent') + expect(source).toContain('@keydown="handleKeydown"') + }) + + it('keeps light, dark and reduced-motion treatments explicit', () => { + expect(source).toContain(':global(.phone-app--light)') + expect(source).toContain('@media (prefers-reduced-motion: reduce)') + expect(source).toContain('passcode-screen__backdrop') + }) +}) diff --git a/frontend/src/components/PhonePasscode.vue b/frontend/src/components/PhonePasscode.vue index 3f09d7c..f78fd55 100644 --- a/frontend/src/components/PhonePasscode.vue +++ b/frontend/src/components/PhonePasscode.vue @@ -1,7 +1,8 @@