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
- 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]

View File

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

View File

@ -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 {

View File

@ -49,29 +49,21 @@ struct LockableView<Content: View, LockedContent: View>: 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<Content: View, LockedContent: View>: 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 {
}

View File

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

View File

@ -24,3 +24,5 @@
//
"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 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")
}
}
}

View File

@ -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 */