diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ee4b363..d470482d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Option to lock app when entering background (iOS). [#270](https://github.com/passepartoutvpn/passepartout-apple/pull/270) +- Option to lock app when entering background (iOS). [#270](https://github.com/passepartoutvpn/passepartout-apple/pull/270), [#271](https://github.com/passepartoutvpn/passepartout-apple/pull/271), [#273](https://github.com/passepartoutvpn/passepartout-apple/pull/273) - 3D Touch items (iOS). [#267](https://github.com/passepartoutvpn/passepartout-apple/pull/267) - Ukranian translations (Dmitry Chirkin). [#243](https://github.com/passepartoutvpn/passepartout-apple/pull/243) - Restore DNS "Domain" setting. [#260](https://github.com/passepartoutvpn/passepartout-apple/pull/260) diff --git a/Passepartout/App/Constants/Theme.swift b/Passepartout/App/Constants/Theme.swift index 790f4035..2e1cdf5b 100644 --- a/Passepartout/App/Constants/Theme.swift +++ b/Passepartout/App/Constants/Theme.swift @@ -25,6 +25,7 @@ import SwiftUI import PassepartoutLibrary +import LocalAuthentication extension View { var themeIdiom: UIUserInterfaceIdiom { @@ -57,6 +58,9 @@ extension View { extension View { func themeGlobal() -> some View { themeNavigationViewStyle() + #if !targetEnvironment(macCatalyst) + .themeLockScreen() + #endif .themeTint() .listStyle(themeListStyleValue()) .toggleStyle(themeToggleStyleValue()) @@ -490,6 +494,42 @@ extension View { } } +// MARK: Lock screen + +extension View { + func themeLockScreen() -> some View { + @AppStorage(AppPreference.locksInBackground.rawValue) var locksInBackground = false + return LockableView( + locksInBackground: $locksInBackground, + content: { + self + }, + lockedContent: LogoView.init, + unlockBlock: Self.themeUnlockScreenBlock + ) + } + + private static func themeUnlockScreenBlock(isLocked: Binding) { + let context = LAContext() + let policy: LAPolicy = .deviceOwnerAuthentication + var error: NSError? + guard context.canEvaluatePolicy(policy, error: &error) else { + isLocked.wrappedValue = false + return + } + Task { @MainActor in + do { + let isAuthorized = try await context.evaluatePolicy( + policy, + localizedReason: L10n.Global.Messages.unlockApp + ) + isLocked.wrappedValue = !isAuthorized + } catch { + } + } + } +} + // MARK: Validation extension View { diff --git a/Passepartout/App/PassepartoutApp.swift b/Passepartout/App/PassepartoutApp.swift index ba65210a..db0865a5 100644 --- a/Passepartout/App/PassepartoutApp.swift +++ b/Passepartout/App/PassepartoutApp.swift @@ -30,11 +30,9 @@ import PassepartoutLibrary struct PassepartoutApp: App { @UIApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate - @AppStorage(AppPreference.locksInBackground.rawValue) private var locksInBackground = false - @SceneBuilder var body: some Scene { WindowGroup { - mainView + MainView() .withoutTitleBar() .onIntentActivity(IntentDispatcher.connectVPN) .onIntentActivity(IntentDispatcher.disableVPN) @@ -46,19 +44,6 @@ struct PassepartoutApp: App { .onIntentActivity(IntentDispatcher.untrustCurrentNetwork) } } - - private var mainView: some View { - #if targetEnvironment(macCatalyst) - MainView() - #else - LockableView( - reason: L10n.Global.Messages.unlockApp, - locksInBackground: $locksInBackground, - content: MainView.init, - lockedContent: LogoView.init - ) - #endif - } } extension View { diff --git a/Passepartout/App/Reusable/LockableView.swift b/Passepartout/App/Reusable/LockableView.swift index 0ff45292..7144f422 100644 --- a/Passepartout/App/Reusable/LockableView.swift +++ b/Passepartout/App/Reusable/LockableView.swift @@ -24,37 +24,35 @@ // import SwiftUI -import LocalAuthentication struct LockableView: View { - let reason: String - @Binding var locksInBackground: Bool let content: () -> Content let lockedContent: () -> LockedContent + let unlockBlock: (Binding) -> Void + @Environment(\.scenePhase) private var scenePhase - @State private var didAppear = false + @ObservedObject private var lock: Lock = .shared - @State private var isLocked = false + private var isLocked: Binding { + .init { + Lock.shared.isActive + } set: { + Lock.shared.isActive = $0 + } + } var body: some View { - Group { - if !isLocked { - content() - } else { + ZStack { + content() + if isLocked.wrappedValue { lockedContent() } }.onChange(of: scenePhase, perform: onScenePhase) - .onAppear { - if !didAppear && locksInBackground { - didAppear = true - isLocked = true - } - } } private func onScenePhase(_ scenePhase: ScenePhase) { @@ -74,30 +72,26 @@ struct LockableView: View { guard locksInBackground else { return } - isLocked = true + isLocked.wrappedValue = true } func unlockIfNeeded() { guard locksInBackground else { - isLocked = false + isLocked.wrappedValue = false return } - guard isLocked else { + guard isLocked.wrappedValue else { return } - let context = LAContext() - let policy: LAPolicy = .deviceOwnerAuthentication - var error: NSError? - guard context.canEvaluatePolicy(policy, error: &error) else { - isLocked = false - return - } - Task { @MainActor in - do { - let isAuthorized = try await context.evaluatePolicy(policy, localizedReason: reason) - isLocked = !isAuthorized - } catch { - } - } + unlockBlock(isLocked) + } +} + +private class Lock: ObservableObject { + static let shared = Lock() + + @Published var isActive = true + + private init() { } }