fix app lock hanging on macOS and blocking the photo picker
- 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:
1 parent
8b86e33186
commit
6ed5cb3ce9
9 files changed
+86
-78
No files matched your search
@@ -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()
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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 "Подключено"
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user