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:
parent
325e10845d
commit
5f991d9cc2
|
@ -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]
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -24,7 +24,6 @@
|
|||
//
|
||||
|
||||
import SwiftUI
|
||||
import LocalAuthentication
|
||||
import PassepartoutLibrary
|
||||
|
||||
@main
|
||||
|
@ -35,12 +34,8 @@ struct PassepartoutApp: App {
|
|||
|
||||
@SceneBuilder var body: some Scene {
|
||||
WindowGroup {
|
||||
LockableView(
|
||||
reason: L10n.Global.Messages.unlockApp,
|
||||
locksInBackground: $locksInBackground,
|
||||
content: MainView.init,
|
||||
lockedContent: LogoView.init
|
||||
).withoutTitleBar()
|
||||
mainView
|
||||
.withoutTitleBar()
|
||||
.onIntentActivity(IntentDispatcher.connectVPN)
|
||||
.onIntentActivity(IntentDispatcher.disableVPN)
|
||||
.onIntentActivity(IntentDispatcher.enableVPN)
|
||||
|
@ -51,6 +46,19 @@ 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 {
|
||||
|
|
|
@ -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 {
|
||||
}
|
||||
|
|
|
@ -44,7 +44,9 @@ struct SettingsView: View {
|
|||
|
||||
var body: some View {
|
||||
List {
|
||||
#if !targetEnvironment(macCatalyst)
|
||||
preferencesSection
|
||||
#endif
|
||||
aboutSection
|
||||
}.toolbar {
|
||||
themeCloseItem(presentationMode: presentationMode)
|
||||
|
|
|
@ -24,3 +24,5 @@
|
|||
//
|
||||
|
||||
"NSLocationWhenInUseUsageDescription" = "Access name of current Wi-Fi";
|
||||
|
||||
"NSFaceIDUsageDescription" = "Unlock app with Face ID";
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 */
|
||||
|
|
Loading…
Reference in New Issue