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
|
### 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]
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 {
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -24,3 +24,5 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
"NSLocationWhenInUseUsageDescription" = "Access name of current Wi-Fi";
|
"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 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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 */
|
||||||
|
|
Loading…
Reference in New Issue