Option to lock app when entering background (#270)

This commit is contained in:
Davide De Rosa 2023-03-20 11:00:01 +01:00 committed by GitHub
parent 7346bfc65c
commit 325e10845d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 200 additions and 41 deletions

View File

@ -27,6 +27,8 @@
0E0C0729236087A100155AAC /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 0E0C072B236087A100155AAC /* InfoPlist.strings */; }; 0E0C0729236087A100155AAC /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 0E0C072B236087A100155AAC /* InfoPlist.strings */; };
0E0F4C5A29C761850022E884 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E0F4C5929C761850022E884 /* SceneDelegate.swift */; }; 0E0F4C5A29C761850022E884 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E0F4C5929C761850022E884 /* SceneDelegate.swift */; };
0E0F4C5C29C76B790022E884 /* SceneDelegate+Shortcuts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E0F4C5B29C76B790022E884 /* SceneDelegate+Shortcuts.swift */; }; 0E0F4C5C29C76B790022E884 /* SceneDelegate+Shortcuts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E0F4C5B29C76B790022E884 /* SceneDelegate+Shortcuts.swift */; };
0E0F4C6429C84B5A0022E884 /* LockableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E0F4C6329C84B5A0022E884 /* LockableView.swift */; };
0E0F4C6629C84CF60022E884 /* LogoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E0F4C6529C84CF60022E884 /* LogoView.swift */; };
0E12BC8F27F62C8600B2F912 /* Validators.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E12BC8E27F62C8500B2F912 /* Validators.swift */; }; 0E12BC8F27F62C8600B2F912 /* Validators.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E12BC8E27F62C8500B2F912 /* Validators.swift */; };
0E1B5F5C29C506AD00FE7D18 /* ProfileView+Diagnostics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E1B5F5B29C506AC00FE7D18 /* ProfileView+Diagnostics.swift */; }; 0E1B5F5C29C506AD00FE7D18 /* ProfileView+Diagnostics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E1B5F5B29C506AC00FE7D18 /* ProfileView+Diagnostics.swift */; };
0E1F5628287F0ECB00F8ADD7 /* ProviderProfileItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E1F5627287F0ECB00F8ADD7 /* ProviderProfileItem.swift */; }; 0E1F5628287F0ECB00F8ADD7 /* ProviderProfileItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E1F5627287F0ECB00F8ADD7 /* ProviderProfileItem.swift */; };
@ -308,6 +310,8 @@
0E0C072C236087C800155AAC /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/InfoPlist.strings; sourceTree = "<group>"; }; 0E0C072C236087C800155AAC /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/InfoPlist.strings; sourceTree = "<group>"; };
0E0F4C5929C761850022E884 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; }; 0E0F4C5929C761850022E884 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
0E0F4C5B29C76B790022E884 /* SceneDelegate+Shortcuts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SceneDelegate+Shortcuts.swift"; sourceTree = "<group>"; }; 0E0F4C5B29C76B790022E884 /* SceneDelegate+Shortcuts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SceneDelegate+Shortcuts.swift"; sourceTree = "<group>"; };
0E0F4C6329C84B5A0022E884 /* LockableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockableView.swift; sourceTree = "<group>"; };
0E0F4C6529C84CF60022E884 /* LogoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogoView.swift; sourceTree = "<group>"; };
0E12BC8E27F62C8500B2F912 /* Validators.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Validators.swift; sourceTree = "<group>"; }; 0E12BC8E27F62C8500B2F912 /* Validators.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Validators.swift; sourceTree = "<group>"; };
0E1B5F5B29C506AC00FE7D18 /* ProfileView+Diagnostics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProfileView+Diagnostics.swift"; sourceTree = "<group>"; }; 0E1B5F5B29C506AC00FE7D18 /* ProfileView+Diagnostics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProfileView+Diagnostics.swift"; sourceTree = "<group>"; };
0E1C0A52238FFF97009FC087 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/InfoPlist.strings; sourceTree = "<group>"; }; 0E1C0A52238FFF97009FC087 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/InfoPlist.strings; sourceTree = "<group>"; };
@ -577,9 +581,9 @@
0E29385A285A749E002A6E0E /* Context */ = { 0E29385A285A749E002A6E0E /* Context */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
0E293850285A70AC002A6E0E /* AppPreference.swift */,
0E021D9B284E68580077EF5D /* AppContext.swift */, 0E021D9B284E68580077EF5D /* AppContext.swift */,
0E293856285A73BC002A6E0E /* AppContext+Shared.swift */, 0E293856285A73BC002A6E0E /* AppContext+Shared.swift */,
0E293850285A70AC002A6E0E /* AppPreference.swift */,
); );
path = Context; path = Context;
sourceTree = "<group>"; sourceTree = "<group>";
@ -598,6 +602,7 @@
0EF0FAF827DD212C007EB181 /* IntentActivity.swift */, 0EF0FAF827DD212C007EB181 /* IntentActivity.swift */,
0ED89C1D27DE3F8D008B36D6 /* IntentAddView.swift */, 0ED89C1D27DE3F8D008B36D6 /* IntentAddView.swift */,
0ED89C1627DE0E05008B36D6 /* IntentEditView.swift */, 0ED89C1627DE0E05008B36D6 /* IntentEditView.swift */,
0E0F4C6329C84B5A0022E884 /* LockableView.swift */,
0E5324A827D2AC55002565C3 /* LongContentView.swift */, 0E5324A827D2AC55002565C3 /* LongContentView.swift */,
0EBC075427EBC83800208AD9 /* MailComposerView.swift */, 0EBC075427EBC83800208AD9 /* MailComposerView.swift */,
0ED30DCB27EA197C0057D8A3 /* RevealingSecureField.swift */, 0ED30DCB27EA197C0057D8A3 /* RevealingSecureField.swift */,
@ -648,6 +653,7 @@
0E5349C527C176C200C71BB3 /* EndpointView+OpenVPN.swift */, 0E5349C527C176C200C71BB3 /* EndpointView+OpenVPN.swift */,
0E5349C727C176D100C71BB3 /* EndpointView+WireGuard.swift */, 0E5349C727C176D100C71BB3 /* EndpointView+WireGuard.swift */,
0EB90CC029C25BBD00E64628 /* InteractiveConnectionView.swift */, 0EB90CC029C25BBD00E64628 /* InteractiveConnectionView.swift */,
0E0F4C6529C84CF60022E884 /* LogoView.swift */,
0E0BD27227B2EA2C00583AC5 /* MainView.swift */, 0E0BD27227B2EA2C00583AC5 /* MainView.swift */,
0E71ACE827C1055200F85C4B /* NetworkSettingsView.swift */, 0E71ACE827C1055200F85C4B /* NetworkSettingsView.swift */,
0EB34BC927C6A70200B126DA /* OnDemandView.swift */, 0EB34BC927C6A70200B126DA /* OnDemandView.swift */,
@ -1447,6 +1453,7 @@
0E3B7FD627E5173A00C66F13 /* ProfileView+VPN.swift in Sources */, 0E3B7FD627E5173A00C66F13 /* ProfileView+VPN.swift in Sources */,
0ED89C1E27DE3F8D008B36D6 /* IntentAddView.swift in Sources */, 0ED89C1E27DE3F8D008B36D6 /* IntentAddView.swift in Sources */,
0E5468002867AC9A00F74D1C /* MacUtils.swift in Sources */, 0E5468002867AC9A00F74D1C /* MacUtils.swift in Sources */,
0E0F4C6429C84B5A0022E884 /* LockableView.swift in Sources */,
0E96D3052872010A005EFBCF /* DefaultLightVPNManager.swift in Sources */, 0E96D3052872010A005EFBCF /* DefaultLightVPNManager.swift in Sources */,
0EBE880F281B18DE0090D9E6 /* OrganizerView+ProfileRow.swift in Sources */, 0EBE880F281B18DE0090D9E6 /* OrganizerView+ProfileRow.swift in Sources */,
0ED30DCF27EA1EF80057D8A3 /* PaywallView+Beta.swift in Sources */, 0ED30DCF27EA1EF80057D8A3 /* PaywallView+Beta.swift in Sources */,
@ -1491,6 +1498,7 @@
0EF0FAF727DD159C007EB181 /* IntentDispatcher.swift in Sources */, 0EF0FAF727DD159C007EB181 /* IntentDispatcher.swift in Sources */,
0E12BC8F27F62C8600B2F912 /* Validators.swift in Sources */, 0E12BC8F27F62C8600B2F912 /* Validators.swift in Sources */,
0E0838FD2877334300A34EC0 /* DefaultLightProviderManager.swift in Sources */, 0E0838FD2877334300A34EC0 /* DefaultLightProviderManager.swift in Sources */,
0E0F4C6629C84CF60022E884 /* LogoView.swift in Sources */,
0E039279281890B100827C10 /* AddHostView.swift in Sources */, 0E039279281890B100827C10 /* AddHostView.swift in Sources */,
0E5467F72867A57000F74D1C /* MacBridge.swift in Sources */, 0E5467F72867A57000F74D1C /* MacBridge.swift in Sources */,
0E9ED48127FD9BAE003B2316 /* CopySavingButton.swift in Sources */, 0E9ED48127FD9BAE003B2316 /* CopySavingButton.swift in Sources */,

View File

@ -438,6 +438,17 @@ extension View {
Text(L10n.Global.Strings.save) Text(L10n.Global.Strings.save)
} }
func themeSecureField(_ placeholder: String, text: Binding<String>, contentType: UITextContentType = .password) -> some View {
RevealingSecureField(placeholder, text: text) {
themeConceilImage.asSystemImage
.themeAccentForegroundStyle()
} revealImage: {
themeRevealImage.asSystemImage
.themeAccentForegroundStyle()
}.textContentType(contentType)
.themeRawTextStyle()
}
func themeTextPicker<T: Hashable>(_ title: String, selection: Binding<T>, values: [T], description: @escaping (T) -> String) -> some View { func themeTextPicker<T: Hashable>(_ title: String, selection: Binding<T>, values: [T], description: @escaping (T) -> String) -> some View {
StyledPicker(title: title, selection: selection, values: values) { StyledPicker(title: title, selection: selection, values: values) {
Text(description($0)) Text(description($0))

View File

@ -33,6 +33,8 @@ enum AppPreference: String, KeyStoreDomainLocation {
case didHandleSubreddit case didHandleSubreddit
case locksInBackground
var domain: String { var domain: String {
"Passepartout.App" "Passepartout.App"
} }

View File

@ -24,16 +24,23 @@
// //
import SwiftUI import SwiftUI
import LocalAuthentication
import PassepartoutLibrary import PassepartoutLibrary
@main @main
struct PassepartoutApp: App { struct PassepartoutApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate @UIApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate
@AppStorage(AppPreference.locksInBackground.rawValue) private var locksInBackground = false
@SceneBuilder var body: some Scene { @SceneBuilder var body: some Scene {
WindowGroup { WindowGroup {
MainView() LockableView(
.withoutTitleBar() reason: L10n.Global.Messages.unlockApp,
locksInBackground: $locksInBackground,
content: MainView.init,
lockedContent: LogoView.init
).withoutTitleBar()
.onIntentActivity(IntentDispatcher.connectVPN) .onIntentActivity(IntentDispatcher.connectVPN)
.onIntentActivity(IntentDispatcher.disableVPN) .onIntentActivity(IntentDispatcher.disableVPN)
.onIntentActivity(IntentDispatcher.enableVPN) .onIntentActivity(IntentDispatcher.enableVPN)

View File

@ -0,0 +1,105 @@
//
// LockableView.swift
// Passepartout
//
// Created by Davide De Rosa on 3/20/23.
// Copyright (c) 2023 Davide De Rosa. All rights reserved.
//
// https://github.com/passepartoutvpn
//
// This file is part of Passepartout.
//
// Passepartout is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Passepartout is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
//
import SwiftUI
import LocalAuthentication
struct LockableView<Content: View, LockedContent: View>: View {
let reason: String
@Binding var locksInBackground: Bool
let content: () -> Content
let lockedContent: () -> LockedContent
@Environment(\.scenePhase) private var scenePhase
@State private var didAppear = false
@State private var isLocked = false
var body: some View {
Group {
if !isLocked {
content()
} else {
lockedContent()
}
}.onChange(of: scenePhase, perform: onScenePhase)
}
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
}
}
func lockIfNeeded() {
guard locksInBackground else {
return
}
isLocked = true
}
func unlockIfNeeded() {
guard isLocked else {
return
}
let context = LAContext()
var error: NSError?
guard context.canEvaluatePolicy(.deviceOwnerAuthentication, error: &error) else {
return
}
Task {
do {
let isAuthorized = try await context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: reason)
isLocked = !isAuthorized
} catch {
}
}
}
}

View File

@ -59,6 +59,7 @@ struct AccountView: View {
var body: some View { var body: some View {
List { List {
Section { Section {
// FIXME: l10n
themeTextPicker(L10n.Endpoint.Advanced.Openvpn.Items.Digest.caption, selection: $liveAccount.authenticationMethod ?? .persistent, values: [ themeTextPicker(L10n.Endpoint.Advanced.Openvpn.Items.Digest.caption, selection: $liveAccount.authenticationMethod ?? .persistent, values: [
.persistent, .persistent,
.interactive .interactive
@ -77,27 +78,13 @@ struct AccountView: View {
if liveAccount.authenticationMethod == .interactive { if liveAccount.authenticationMethod == .interactive {
EmptyView() EmptyView()
} else { } else {
RevealingSecureField(L10n.Account.Items.Password.placeholder, text: $liveAccount.password) { themeSecureField(L10n.Account.Items.Password.placeholder, text: $liveAccount.password)
themeConceilImage.asSystemImage
.themeAccentForegroundStyle()
} revealImage: {
themeRevealImage.asSystemImage
.themeAccentForegroundStyle()
}.textContentType(.password)
.themeRawTextStyle()
.withLeadingText(L10n.Account.Items.Password.caption) .withLeadingText(L10n.Account.Items.Password.caption)
} }
// TODO: interactive, scan QR code // TODO: interactive, scan QR code
case .totp: case .totp:
RevealingSecureField(L10n.Account.Items.Password.placeholder, text: $liveAccount.password) { themeSecureField(L10n.Account.Items.Password.placeholder, text: $liveAccount.password, contentType: .oneTimeCode)
themeConceilImage.asSystemImage
.themeAccentForegroundStyle()
} revealImage: {
themeRevealImage.asSystemImage
.themeAccentForegroundStyle()
}.textContentType(.oneTimeCode)
.themeRawTextStyle()
.withLeadingText(L10n.Account.Items.Seed.caption) .withLeadingText(L10n.Account.Items.Seed.caption)
} }
} footer: { } footer: {

View File

@ -50,14 +50,7 @@ struct InteractiveConnectionView: View {
.withLeadingText(L10n.Account.Items.Username.caption) .withLeadingText(L10n.Account.Items.Username.caption)
.disabled(true) .disabled(true)
RevealingSecureField(L10n.Account.Items.Password.placeholder, text: $password) { themeSecureField(L10n.Account.Items.Password.placeholder, text: $password)
themeConceilImage.asSystemImage
.themeAccentForegroundStyle()
} revealImage: {
themeRevealImage.asSystemImage
.themeAccentForegroundStyle()
}.textContentType(.password)
.themeRawTextStyle()
.withLeadingText(L10n.Account.Items.Password.caption) .withLeadingText(L10n.Account.Items.Password.caption)
} header: { } header: {
Text(L10n.Account.title) Text(L10n.Account.title)

View File

@ -0,0 +1,35 @@
//
// LogoView.swift
// Passepartout
//
// Created by Davide De Rosa on 3/20/23.
// Copyright (c) 2023 Davide De Rosa. All rights reserved.
//
// https://github.com/passepartoutvpn
//
// This file is part of Passepartout.
//
// Passepartout is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Passepartout is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
//
import SwiftUI
struct LogoView: View {
var body: some View {
ZStack {
themePrimaryBackground
Image(themeAssetsLogoImage)
}.ignoresSafeArea()
}
}

View File

@ -33,11 +33,7 @@ struct SettingsView: View {
@Environment(\.presentationMode) private var presentationMode @Environment(\.presentationMode) private var presentationMode
// private var isTestBuild: Bool { @AppStorage(AppPreference.locksInBackground.rawValue) private var locksInBackground = false
// Constants.App.isBeta || Constants.InApp.appType == .beta
// }
//
// private let appName = Unlocalized.appName
private let versionString = Constants.Global.appVersionString private let versionString = Constants.Global.appVersionString
@ -48,6 +44,7 @@ struct SettingsView: View {
var body: some View { var body: some View {
List { List {
preferencesSection
aboutSection aboutSection
}.toolbar { }.toolbar {
themeCloseItem(presentationMode: presentationMode) themeCloseItem(presentationMode: presentationMode)
@ -55,6 +52,12 @@ struct SettingsView: View {
.navigationTitle(L10n.Settings.title) .navigationTitle(L10n.Settings.title)
} }
private var preferencesSection: some View {
Section {
Toggle(L10n.Settings.Items.LocksInBackground.caption, isOn: $locksInBackground)
}
}
private var aboutSection: some View { private var aboutSection: some View {
Section { Section {
NavigationLink { NavigationLink {

View File

@ -446,6 +446,8 @@ internal enum L10n {
internal static let emailNotConfigured = L10n.tr("Localizable", "global.messages.email_not_configured", fallback: "No e-mail account is configured.") internal static let emailNotConfigured = L10n.tr("Localizable", "global.messages.email_not_configured", fallback: "No e-mail account is configured.")
/// Passepartout is a user-friendly, open source OpenVPN / WireGuard client for iOS and macOS /// Passepartout is a user-friendly, open source OpenVPN / WireGuard client for iOS and macOS
internal static let share = L10n.tr("Localizable", "global.messages.share", fallback: "Passepartout is a user-friendly, open source OpenVPN / WireGuard client for iOS and macOS") internal static let share = L10n.tr("Localizable", "global.messages.share", fallback: "Passepartout is a user-friendly, open source OpenVPN / WireGuard client for iOS and macOS")
/// Passepartout is locked
internal static let unlockApp = L10n.tr("Localizable", "global.messages.unlock_app", fallback: "Passepartout is locked")
} }
internal enum Placeholders { internal enum Placeholders {
/// My profile /// My profile
@ -895,6 +897,10 @@ internal enum L10n {
/// Make a donation /// Make a donation
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 {
/// Lock app in background
internal static let caption = L10n.tr("Localizable", "settings.items.locks_in_background.caption", fallback: "Lock app in background")
}
} }
} }
internal enum Shortcuts { internal enum Shortcuts {

View File

@ -51,6 +51,7 @@
"global.strings.disconnect" = "Disconnect"; "global.strings.disconnect" = "Disconnect";
"global.strings.download" = "Download"; "global.strings.download" = "Download";
"global.messages.unlock_app" = "Passepartout is locked";
"global.messages.email_not_configured" = "No e-mail account is configured."; "global.messages.email_not_configured" = "No e-mail account is configured.";
"global.messages.share" = "Passepartout is a user-friendly, open source OpenVPN / WireGuard client for iOS and macOS"; "global.messages.share" = "Passepartout is a user-friendly, open source OpenVPN / WireGuard client for iOS and macOS";
@ -320,6 +321,7 @@
/* MARK: SettingsView */ /* MARK: SettingsView */
"settings.title" = "Settings"; "settings.title" = "Settings";
"settings.items.locks_in_background.caption" = "Lock app in background";
"settings.items.donate.caption" = "Make a donation"; "settings.items.donate.caption" = "Make a donation";
/* MARK: AboutView */ /* MARK: AboutView */