fix app lock hanging on macOS and blocking the photo picker
iOS CI / Build and Test SwiftUI App (push) Canceled after 0s
Build Unsigned iOS and macOS Apps / Build Unsigned iOS IPA (push) Canceled after 0s
Build Unsigned iOS and macOS Apps / Build macOS ZIP (push) Canceled after 0s

- Lock only when the scene really backgrounds: transient inactive
  states (system photo picker, app switcher) no longer swap in the
  lock screen and break gallery selection on iOS.
- Cache biometric availability once per session off the main thread
  instead of calling LAContext.canEvaluatePolicy in view bodies,
  which froze the macOS lock screen, and auto-prompt biometrics on
  iOS only.
- Keep UI tests as an opt-in target and guard both fixes in
  verify.sh.
This commit is contained in:
wt committed 2026-08-30 01:14:23 +07:00
1 parent 8b86e33186
commit 6ed5cb3ce9
9 files changed
+86 -78

No files matched your search

+23
View File
@@ -60,6 +60,11 @@ final class AppModel: ObservableObject {
@Published private(set) var isAppLocked = false
@Published private(set) var appLockIsEnabled = false
@Published private(set) var appLockBiometricIsEnabled = false
/// Cached once per session: LAContext.canEvaluatePolicy performs IPC with
/// the security daemon and must never run in a view body (it froze the
/// macOS lock screen).
@Published private(set) var biometricUnlockAvailable = false
@Published private(set) var biometricUnlockName = ""
@Published private(set) var mediaViewerItem: MediaViewerItem?
@Published private(set) var mediaPreviewURLs: [String: URL] = [:]
@Published private(set) var mediaThumbnailData: [String: Data] = [:]
@@ -140,6 +145,7 @@ final class AppModel: ObservableObject {
appLockIsEnabled = appLock.isEnabled
appLockBiometricIsEnabled = appLock.biometricUnlockEnabled
isAppLocked = appLock.isEnabled && !RuntimeEnvironment.isRunningTests
refreshBiometricAvailability()
xmpp.eventHandler = { [weak self] event in
self?.consume(event)
@@ -535,6 +541,23 @@ final class AppModel: ObservableObject {
return true
}
func refreshBiometricAvailability() {
guard !RuntimeEnvironment.isRunningTests else { return }
Task.detached(priority: .utility) {
let context = LAContext()
var error: NSError?
let available = context.canEvaluatePolicy(
.deviceOwnerAuthenticationWithBiometrics,
error: &error
)
let name = context.biometryType == .faceID ? "Face ID" : "Touch ID"
await MainActor.run { [weak self] in
self?.biometricUnlockAvailable = available
self?.biometricUnlockName = name
}
}
}
func unlockWithBiometrics() async -> Bool {
guard appLockIsEnabled, appLockBiometricIsEnabled else { return false }
let context = LAContext()
+14 -29
View File
@@ -57,12 +57,15 @@ struct AppLockView: View {
.buttonStyle(.borderedProminent)
.disabled(passcode.isEmpty)
if biometricAvailable, model.appLockBiometricIsEnabled {
if model.biometricUnlockAvailable, model.appLockBiometricIsEnabled {
Button {
Task { await biometricUnlock() }
} label: {
Label("Войти по \(biometricName)", systemImage: biometricIcon)
.foregroundStyle(.white)
Label(
"Войти по \(model.biometricUnlockName)",
systemImage: model.biometricUnlockName == "Face ID" ? "faceid" : "touchid"
)
.foregroundStyle(.white)
}
}
}
@@ -73,35 +76,17 @@ struct AppLockView: View {
.onAppear {
isFocused = true
}
#if os(iOS)
.task {
// Auto-prompt biometrics once when the lock screen appears.
guard biometricAvailable, model.appLockBiometricIsEnabled else { return }
// Auto-prompt biometrics once when the lock screen appears. iOS
// only: on macOS the system Touch ID dialog blocks the window
// and freezes the lock screen.
guard model.biometricUnlockAvailable, model.appLockBiometricIsEnabled else {
return
}
await biometricUnlock()
}
}
private var biometricContext: LAContext {
LAContext()
}
private var biometricAvailable: Bool {
var error: NSError?
return biometricContext.canEvaluatePolicy(
.deviceOwnerAuthenticationWithBiometrics,
error: &error
)
}
private var biometryType: LABiometryType {
biometricContext.biometryType
}
private var biometricIcon: String {
biometryType == .faceID ? "faceid" : "touchid"
}
private var biometricName: String {
biometryType == .faceID ? "Face ID" : "Touch ID"
#endif
}
private func submit() {
+2 -18
View File
@@ -112,7 +112,7 @@ struct SettingsView: View {
}
))
if model.appLockIsEnabled {
Toggle("Вход по \(biometricName)", isOn: Binding(
Toggle("Вход по \(model.biometricUnlockName)", isOn: Binding(
get: { model.appLockBiometricIsEnabled },
set: { enabled in
passcodeSheetMode = .verify(
@@ -120,7 +120,7 @@ struct SettingsView: View {
)
}
))
.disabled(!biometricAvailable)
.disabled(!model.biometricUnlockAvailable)
Button("Сменить пароль") {
passcodeSheetMode = .change
}
@@ -194,22 +194,6 @@ struct SettingsView: View {
#endif
}
private var biometricContext: LAContext {
LAContext()
}
private var biometricAvailable: Bool {
var error: NSError?
return biometricContext.canEvaluatePolicy(
.deviceOwnerAuthenticationWithBiometrics,
error: &error
)
}
private var biometricName: String {
biometricContext.biometryType == .faceID ? "Face ID" : "Touch ID"
}
private var connectionTitle: String {
switch model.connectionStatus {
case .connected: return "Подключено"
+6 -4
View File
@@ -428,17 +428,19 @@ final class XMPPService {
throw LumaXMPPError.omemoInitializationFailed
}
// Clean up an invalid self-session left behind by a previous buggy
// build (one-time migration per account), then make sure a working
// self-session exists: encrypt-to-self needs it and deleting it on
// every connect made archived self-copies undecryptable.
// build (one-time migration per account).
omemoStorage.removeSessionWithOwnDeviceOnce()
ensureSelfSession(client: client, omemoStorage: omemoStorage)
configureModules(client: client, signalContext: signalContext, omemoStorage: omemoStorage)
configureConnection(client: client, account: account, password: password)
activePassword = password
subscribe(to: client, omemoStorage: omemoStorage)
// The self-session must be rebuilt after the modules exist: the
// ensureSelfSession lookup goes through the modules manager and
// crashes on a not-yet-registered OMEMO module.
ensureSelfSession(client: client, omemoStorage: omemoStorage)
self.client = client
self.omemoStorage = omemoStorage
callEngine.attach(client: client)