From 5f991d9cc290e2170bb1726e7d012e2044c427a6 Mon Sep 17 00:00:00 2001 From: Davide De Rosa Date: Mon, 20 Mar 2023 14:12:42 +0100 Subject: [PATCH] Unlock app with biometrics (#271) * Prevent app lock completely in macOS * Add missing Face ID metadata for biometrics auth * Ensure MainActor * Reword option * Fix lock logic on launch --- CHANGELOG.md | 5 ++- Passepartout/App/Info.plist | 2 + Passepartout/App/PassepartoutApp.swift | 38 +++++++++++-------- Passepartout/App/Reusable/LockableView.swift | 32 ++++++++-------- Passepartout/App/Views/SettingsView.swift | 2 + Passepartout/App/en.lproj/InfoPlist.strings | 2 + .../Constants/SwiftGen+Strings.swift | 4 +- .../AppShared/en.lproj/Localizable.strings | 2 +- 8 files changed, 50 insertions(+), 37 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4bc27399..9bbcd320 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,8 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- 3D Touch items. [#267](https://github.com/passepartoutvpn/passepartout-apple/pull/267) -- Prompt for password interactively. [#3](https://github.com/passepartoutvpn/passepartout-apple/issues/3) +- Option to lock app when entering background (iOS). [#270](https://github.com/passepartoutvpn/passepartout-apple/pull/270) +- 3D Touch items (iOS). [#267](https://github.com/passepartoutvpn/passepartout-apple/pull/267) +- Prompt for password interactively. [#259](https://github.com/passepartoutvpn/passepartout-apple/pull/259) - 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) - OpenVPN: Full implementation of Tunnelblick XOR patch (tmthecoder). [#245](https://github.com/passepartoutvpn/passepartout-apple/pull/245), [tunnelkit#255][https://github.com/passepartoutvpn/tunnelkit/pull/255] diff --git a/Passepartout/App/Info.plist b/Passepartout/App/Info.plist index a82bb931..342965ed 100644 --- a/Passepartout/App/Info.plist +++ b/Passepartout/App/Info.plist @@ -46,6 +46,8 @@ LSRequiresIPhoneOS + NSFaceIDUsageDescription + dummy NSHumanReadableCopyright $(CFG_COPYRIGHT) NSLocationWhenInUseUsageDescription diff --git a/Passepartout/App/PassepartoutApp.swift b/Passepartout/App/PassepartoutApp.swift index 7c6ab2e4..ba65210a 100644 --- a/Passepartout/App/PassepartoutApp.swift +++ b/Passepartout/App/PassepartoutApp.swift @@ -24,7 +24,6 @@ // import SwiftUI -import LocalAuthentication import PassepartoutLibrary @main @@ -35,22 +34,31 @@ struct PassepartoutApp: App { @SceneBuilder var body: some Scene { WindowGroup { - LockableView( - reason: L10n.Global.Messages.unlockApp, - locksInBackground: $locksInBackground, - content: MainView.init, - lockedContent: LogoView.init - ).withoutTitleBar() - .onIntentActivity(IntentDispatcher.connectVPN) - .onIntentActivity(IntentDispatcher.disableVPN) - .onIntentActivity(IntentDispatcher.enableVPN) - .onIntentActivity(IntentDispatcher.moveToLocation) - .onIntentActivity(IntentDispatcher.trustCellularNetwork) - .onIntentActivity(IntentDispatcher.trustCurrentNetwork) - .onIntentActivity(IntentDispatcher.untrustCellularNetwork) - .onIntentActivity(IntentDispatcher.untrustCurrentNetwork) + mainView + .withoutTitleBar() + .onIntentActivity(IntentDispatcher.connectVPN) + .onIntentActivity(IntentDispatcher.disableVPN) + .onIntentActivity(IntentDispatcher.enableVPN) + .onIntentActivity(IntentDispatcher.moveToLocation) + .onIntentActivity(IntentDispatcher.trustCellularNetwork) + .onIntentActivity(IntentDispatcher.trustCurrentNetwork) + .onIntentActivity(IntentDispatcher.untrustCellularNetwork) + .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 c0fe6874..0ff45292 100644 --- a/Passepartout/App/Reusable/LockableView.swift +++ b/Passepartout/App/Reusable/LockableView.swift @@ -49,29 +49,21 @@ struct LockableView: View { lockedContent() } }.onChange(of: scenePhase, perform: onScenePhase) + .onAppear { + if !didAppear && locksInBackground { + didAppear = true + isLocked = true + } + } } private func onScenePhase(_ scenePhase: ScenePhase) { switch scenePhase { case .active: - #if targetEnvironment(macCatalyst) - break - #else - if !didAppear { - didAppear = true - if locksInBackground { - isLocked = true - } - } unlockIfNeeded() - #endif case .inactive: - #if targetEnvironment(macCatalyst) - break - #else lockIfNeeded() - #endif default: break @@ -86,17 +78,23 @@ struct LockableView: View { } func unlockIfNeeded() { + guard locksInBackground else { + isLocked = false + return + } guard isLocked else { return } let context = LAContext() + let policy: LAPolicy = .deviceOwnerAuthentication var error: NSError? - guard context.canEvaluatePolicy(.deviceOwnerAuthentication, error: &error) else { + guard context.canEvaluatePolicy(policy, error: &error) else { + isLocked = false return } - Task { + Task { @MainActor in do { - let isAuthorized = try await context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: reason) + let isAuthorized = try await context.evaluatePolicy(policy, localizedReason: reason) isLocked = !isAuthorized } catch { } diff --git a/Passepartout/App/Views/SettingsView.swift b/Passepartout/App/Views/SettingsView.swift index cffda505..ab11033f 100644 --- a/Passepartout/App/Views/SettingsView.swift +++ b/Passepartout/App/Views/SettingsView.swift @@ -44,7 +44,9 @@ struct SettingsView: View { var body: some View { List { + #if !targetEnvironment(macCatalyst) preferencesSection + #endif aboutSection }.toolbar { themeCloseItem(presentationMode: presentationMode) diff --git a/Passepartout/App/en.lproj/InfoPlist.strings b/Passepartout/App/en.lproj/InfoPlist.strings index 04d31ce1..756012ad 100644 --- a/Passepartout/App/en.lproj/InfoPlist.strings +++ b/Passepartout/App/en.lproj/InfoPlist.strings @@ -24,3 +24,5 @@ // "NSLocationWhenInUseUsageDescription" = "Access name of current Wi-Fi"; + +"NSFaceIDUsageDescription" = "Unlock app with Face ID"; diff --git a/Passepartout/AppShared/Constants/SwiftGen+Strings.swift b/Passepartout/AppShared/Constants/SwiftGen+Strings.swift index 1fe225d9..0f8d3acc 100644 --- a/Passepartout/AppShared/Constants/SwiftGen+Strings.swift +++ b/Passepartout/AppShared/Constants/SwiftGen+Strings.swift @@ -898,8 +898,8 @@ internal enum L10n { internal static let caption = L10n.tr("Localizable", "settings.items.donate.caption", fallback: "Make a donation") } internal enum LocksInBackground { - /// Lock app in background - internal static let caption = L10n.tr("Localizable", "settings.items.locks_in_background.caption", fallback: "Lock app in background") + /// Lock app access + internal static let caption = L10n.tr("Localizable", "settings.items.locks_in_background.caption", fallback: "Lock app access") } } } diff --git a/Passepartout/AppShared/en.lproj/Localizable.strings b/Passepartout/AppShared/en.lproj/Localizable.strings index 4ef72b7f..c0a43f80 100644 --- a/Passepartout/AppShared/en.lproj/Localizable.strings +++ b/Passepartout/AppShared/en.lproj/Localizable.strings @@ -321,7 +321,7 @@ /* MARK: SettingsView */ "settings.title" = "Settings"; -"settings.items.locks_in_background.caption" = "Lock app in background"; +"settings.items.locks_in_background.caption" = "Lock app access"; "settings.items.donate.caption" = "Make a donation"; /* MARK: AboutView */