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
This commit is contained in:
Davide De Rosa 2023-03-20 14:12:42 +01:00 committed by GitHub
parent 325e10845d
commit 5f991d9cc2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 50 additions and 37 deletions

View File

@ -9,8 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ### Added
- 3D Touch items. [#267](https://github.com/passepartoutvpn/passepartout-apple/pull/267) - Option to lock app when entering background (iOS). [#270](https://github.com/passepartoutvpn/passepartout-apple/pull/270)
- Prompt for password interactively. [#3](https://github.com/passepartoutvpn/passepartout-apple/issues/3) - 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) - 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) - 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] - 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]

View File

@ -46,6 +46,8 @@
</array> </array>
<key>LSRequiresIPhoneOS</key> <key>LSRequiresIPhoneOS</key>
<true/> <true/>
<key>NSFaceIDUsageDescription</key>
<string>dummy</string>
<key>NSHumanReadableCopyright</key> <key>NSHumanReadableCopyright</key>
<string>$(CFG_COPYRIGHT)</string> <string>$(CFG_COPYRIGHT)</string>
<key>NSLocationWhenInUseUsageDescription</key> <key>NSLocationWhenInUseUsageDescription</key>

View File

@ -24,7 +24,6 @@
// //
import SwiftUI import SwiftUI
import LocalAuthentication
import PassepartoutLibrary import PassepartoutLibrary
@main @main
@ -35,22 +34,31 @@ struct PassepartoutApp: App {
@SceneBuilder var body: some Scene { @SceneBuilder var body: some Scene {
WindowGroup { WindowGroup {
LockableView( mainView
reason: L10n.Global.Messages.unlockApp, .withoutTitleBar()
locksInBackground: $locksInBackground, .onIntentActivity(IntentDispatcher.connectVPN)
content: MainView.init, .onIntentActivity(IntentDispatcher.disableVPN)
lockedContent: LogoView.init .onIntentActivity(IntentDispatcher.enableVPN)
).withoutTitleBar() .onIntentActivity(IntentDispatcher.moveToLocation)
.onIntentActivity(IntentDispatcher.connectVPN) .onIntentActivity(IntentDispatcher.trustCellularNetwork)
.onIntentActivity(IntentDispatcher.disableVPN) .onIntentActivity(IntentDispatcher.trustCurrentNetwork)
.onIntentActivity(IntentDispatcher.enableVPN) .onIntentActivity(IntentDispatcher.untrustCellularNetwork)
.onIntentActivity(IntentDispatcher.moveToLocation) .onIntentActivity(IntentDispatcher.untrustCurrentNetwork)
.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 { extension View {

View File

@ -49,29 +49,21 @@ struct LockableView<Content: View, LockedContent: View>: View {
lockedContent() lockedContent()
} }
}.onChange(of: scenePhase, perform: onScenePhase) }.onChange(of: scenePhase, perform: onScenePhase)
.onAppear {
if !didAppear && locksInBackground {
didAppear = true
isLocked = true
}
}
} }
private func onScenePhase(_ scenePhase: ScenePhase) { private func onScenePhase(_ scenePhase: ScenePhase) {
switch scenePhase { switch scenePhase {
case .active: case .active:
#if targetEnvironment(macCatalyst)
break
#else
if !didAppear {
didAppear = true
if locksInBackground {
isLocked = true
}
}
unlockIfNeeded() unlockIfNeeded()
#endif
case .inactive: case .inactive:
#if targetEnvironment(macCatalyst)
break
#else
lockIfNeeded() lockIfNeeded()
#endif
default: default:
break break
@ -86,17 +78,23 @@ struct LockableView<Content: View, LockedContent: View>: View {
} }
func unlockIfNeeded() { func unlockIfNeeded() {
guard locksInBackground else {
isLocked = false
return
}
guard isLocked else { guard isLocked else {
return return
} }
let context = LAContext() let context = LAContext()
let policy: LAPolicy = .deviceOwnerAuthentication
var error: NSError? var error: NSError?
guard context.canEvaluatePolicy(.deviceOwnerAuthentication, error: &error) else { guard context.canEvaluatePolicy(policy, error: &error) else {
isLocked = false
return return
} }
Task { Task { @MainActor in
do { do {
let isAuthorized = try await context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: reason) let isAuthorized = try await context.evaluatePolicy(policy, localizedReason: reason)
isLocked = !isAuthorized isLocked = !isAuthorized
} catch { } catch {
} }

View File

@ -44,7 +44,9 @@ struct SettingsView: View {
var body: some View { var body: some View {
List { List {
#if !targetEnvironment(macCatalyst)
preferencesSection preferencesSection
#endif
aboutSection aboutSection
}.toolbar { }.toolbar {
themeCloseItem(presentationMode: presentationMode) themeCloseItem(presentationMode: presentationMode)

View File

@ -24,3 +24,5 @@
// //
"NSLocationWhenInUseUsageDescription" = "Access name of current Wi-Fi"; "NSLocationWhenInUseUsageDescription" = "Access name of current Wi-Fi";
"NSFaceIDUsageDescription" = "Unlock app with Face ID";

View File

@ -898,8 +898,8 @@ internal enum L10n {
internal static let caption = L10n.tr("Localizable", "settings.items.donate.caption", fallback: "Make a donation") internal static let caption = L10n.tr("Localizable", "settings.items.donate.caption", fallback: "Make a donation")
} }
internal enum LocksInBackground { internal enum LocksInBackground {
/// Lock app in background /// Lock app access
internal static let caption = L10n.tr("Localizable", "settings.items.locks_in_background.caption", fallback: "Lock app in background") internal static let caption = L10n.tr("Localizable", "settings.items.locks_in_background.caption", fallback: "Lock app access")
} }
} }
} }

View File

@ -321,7 +321,7 @@
/* MARK: SettingsView */ /* MARK: SettingsView */
"settings.title" = "Settings"; "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"; "settings.items.donate.caption" = "Make a donation";
/* MARK: AboutView */ /* MARK: AboutView */