parent
752dc6229f
commit
4124ff5cae
|
@ -11,6 +11,7 @@
|
|||
0E7E3D692B9345FD002BBDB4 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0E7E3D5C2B9345FD002BBDB4 /* Assets.xcassets */; };
|
||||
0E7E3D6B2B9345FD002BBDB4 /* PassepartoutApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E7E3D5F2B9345FD002BBDB4 /* PassepartoutApp.swift */; };
|
||||
0E94EE582B93554B00588243 /* PacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E7E3D672B9345FD002BBDB4 /* PacketTunnelProvider.swift */; };
|
||||
0EB08B982CA46F4900A02591 /* AppPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 0EB08B962CA46F4900A02591 /* AppPlist.strings */; };
|
||||
0EBE80DA2BF55C0E00E36A20 /* AppLibrary in Frameworks */ = {isa = PBXBuildFile; productRef = 0EBE80D92BF55C0E00E36A20 /* AppLibrary */; };
|
||||
0EBE80DC2BF55C0E00E36A20 /* TunnelLibrary in Frameworks */ = {isa = PBXBuildFile; productRef = 0EBE80DB2BF55C0E00E36A20 /* TunnelLibrary */; };
|
||||
0EC066D12C7DC47600D88A94 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 0EC066D02C7DC47600D88A94 /* LaunchScreen.storyboard */; platformFilter = ios; };
|
||||
|
@ -55,6 +56,7 @@
|
|||
0E7E3D672B9345FD002BBDB4 /* PacketTunnelProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PacketTunnelProvider.swift; sourceTree = "<group>"; };
|
||||
0E8D852F2C328CA1005493DE /* Config.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Config.xcconfig; sourceTree = "<group>"; };
|
||||
0E94EE5C2B93570600588243 /* Tunnel.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Tunnel.plist; sourceTree = "<group>"; };
|
||||
0EB08B972CA46F4900A02591 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/AppPlist.strings; sourceTree = "<group>"; };
|
||||
0EBE80DD2BF55C9100E36A20 /* Library */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Library; sourceTree = "<group>"; };
|
||||
0EC066D02C7DC47600D88A94 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||
0EC332C82B8A1808000B9C2F /* PassepartoutTunnel.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = PassepartoutTunnel.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
|
@ -129,6 +131,7 @@
|
|||
0ED1EFDA2C33059600CBD9BD /* App.plist */,
|
||||
0E7E3D5B2B9345FD002BBDB4 /* App.entitlements */,
|
||||
0E7C3CCC2C9AF44600B72E69 /* AppDelegate.swift */,
|
||||
0EB08B962CA46F4900A02591 /* AppPlist.strings */,
|
||||
0EC066D02C7DC47600D88A94 /* LaunchScreen.storyboard */,
|
||||
0E7E3D5F2B9345FD002BBDB4 /* PassepartoutApp.swift */,
|
||||
0E7E3D5C2B9345FD002BBDB4 /* Assets.xcassets */,
|
||||
|
@ -246,6 +249,7 @@
|
|||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
0E7E3D692B9345FD002BBDB4 /* Assets.xcassets in Resources */,
|
||||
0EB08B982CA46F4900A02591 /* AppPlist.strings in Resources */,
|
||||
0EC066D12C7DC47600D88A94 /* LaunchScreen.storyboard in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
@ -319,6 +323,17 @@
|
|||
};
|
||||
/* End PBXTargetDependency section */
|
||||
|
||||
/* Begin PBXVariantGroup section */
|
||||
0EB08B962CA46F4900A02591 /* AppPlist.strings */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
0EB08B972CA46F4900A02591 /* en */,
|
||||
);
|
||||
name = AppPlist.strings;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXVariantGroup section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
0E06D19C2B87629200176E1D /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
|
|
|
@ -59,6 +59,7 @@ struct PassepartoutApp: App {
|
|||
)
|
||||
AppLibrary.configure(with: context)
|
||||
}
|
||||
.themeLockScreen()
|
||||
.environmentObject(theme)
|
||||
.environmentObject(context.iapManager)
|
||||
.environmentObject(context.connectionObserver)
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
"NSLocationWhenInUseUsageDescription" = "Access name of current Wi-Fi";
|
||||
|
||||
"NSFaceIDUsageDescription" = "Unlock app with Face ID";
|
|
@ -245,6 +245,8 @@ internal enum Strings {
|
|||
internal static let server = Strings.tr("Localizable", "global.server", fallback: "Server")
|
||||
/// Servers
|
||||
internal static let servers = Strings.tr("Localizable", "global.servers", fallback: "Servers")
|
||||
/// Settings
|
||||
internal static let settings = Strings.tr("Localizable", "global.settings", fallback: "Settings")
|
||||
/// Status
|
||||
internal static let status = Strings.tr("Localizable", "global.status", fallback: "Status")
|
||||
/// Storage
|
||||
|
@ -396,6 +398,8 @@ internal enum Strings {
|
|||
}
|
||||
internal enum Views {
|
||||
internal enum Advanced {
|
||||
/// Lock app access
|
||||
internal static let lockInBackground = Strings.tr("Localizable", "views.advanced.lock_in_background", fallback: "Lock app access")
|
||||
/// Advanced
|
||||
internal static let title = Strings.tr("Localizable", "views.advanced.title", fallback: "Advanced")
|
||||
internal enum Credits {
|
||||
|
@ -475,6 +479,10 @@ internal enum Strings {
|
|||
/// Make a donation
|
||||
internal static let title = Strings.tr("Localizable", "views.donate.title", fallback: "Make a donation")
|
||||
}
|
||||
internal enum Lockable {
|
||||
/// Passepartout is locked
|
||||
internal static let message = Strings.tr("Localizable", "views.lockable.message", fallback: "Passepartout is locked")
|
||||
}
|
||||
internal enum Profile {
|
||||
internal enum ModuleList {
|
||||
internal enum Section {
|
||||
|
|
|
@ -52,6 +52,7 @@
|
|||
"global.save" = "Save";
|
||||
"global.server" = "Server";
|
||||
"global.servers" = "Servers";
|
||||
"global.settings" = "Settings";
|
||||
"global.status" = "Status";
|
||||
"global.storage" = "Storage";
|
||||
"global.subnet" = "Subnet";
|
||||
|
@ -120,6 +121,7 @@
|
|||
|
||||
"views.advanced.title" = "Advanced";
|
||||
"views.advanced.sections.resources" = "Resources";
|
||||
"views.advanced.lock_in_background" = "Lock app access";
|
||||
|
||||
"views.advanced.links.title" = "Links";
|
||||
"views.advanced.links.sections.support" = "Support";
|
||||
|
@ -150,6 +152,8 @@
|
|||
"views.diagnostics.report_issue.title" = "Report issue";
|
||||
"views.diagnostics.alerts.report_issue.email" = "The device is not configured to send e-mails.";
|
||||
|
||||
"views.lockable.message" = "Passepartout is locked";
|
||||
|
||||
// MARK: - Module views
|
||||
|
||||
"modules.dns.servers.add" = "Add address";
|
||||
|
|
|
@ -29,6 +29,10 @@ import SwiftUI
|
|||
import UtilsLibrary
|
||||
|
||||
struct AdvancedView: View {
|
||||
|
||||
@AppStorage(AppPreference.locksInBackground.key)
|
||||
private var locksInBackground = false
|
||||
|
||||
let identifiers: Constants.Identifiers
|
||||
|
||||
@Binding
|
||||
|
@ -41,6 +45,10 @@ struct AdvancedView: View {
|
|||
}
|
||||
|
||||
extension AdvancedView {
|
||||
var lockInBackgroundToggle: some View {
|
||||
Toggle(Strings.Views.Advanced.lockInBackground, isOn: $locksInBackground)
|
||||
}
|
||||
|
||||
var donateLink: some View {
|
||||
navLink(Strings.Views.Donate.title, to: .donate)
|
||||
}
|
||||
|
|
|
@ -30,6 +30,11 @@ import SwiftUI
|
|||
extension AdvancedView {
|
||||
var listView: some View {
|
||||
List {
|
||||
Section {
|
||||
lockInBackgroundToggle
|
||||
} header: {
|
||||
Text(Strings.Global.settings)
|
||||
}
|
||||
Section {
|
||||
// TODO: donations
|
||||
// donateLink
|
||||
|
|
|
@ -30,11 +30,18 @@ import SwiftUI
|
|||
extension AdvancedView {
|
||||
var listView: some View {
|
||||
List(selection: $navigationRoute) {
|
||||
// TODO: donations
|
||||
// donateLink
|
||||
linksLink
|
||||
creditsLink
|
||||
diagnosticsLink
|
||||
Section {
|
||||
lockInBackgroundToggle
|
||||
} header: {
|
||||
Text(Strings.Global.settings)
|
||||
}
|
||||
Section {
|
||||
// TODO: donations
|
||||
// donateLink
|
||||
linksLink
|
||||
creditsLink
|
||||
diagnosticsLink
|
||||
}
|
||||
}
|
||||
.safeAreaInset(edge: .bottom) {
|
||||
Text(identifiers.versionString)
|
||||
|
|
|
@ -23,6 +23,8 @@
|
|||
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
import CommonLibrary
|
||||
import LocalAuthentication
|
||||
import SwiftUI
|
||||
import UtilsLibrary
|
||||
|
||||
|
@ -62,6 +64,7 @@ struct ThemeBooleanModalModifier<Modal>: ViewModifier where Modal: View {
|
|||
modal()
|
||||
.frame(minWidth: modalSize?.width, minHeight: modalSize?.height)
|
||||
.interactiveDismissDisabled(!isInteractive)
|
||||
.themeLockScreen()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -90,6 +93,7 @@ struct ThemeItemModalModifier<Modal, T>: ViewModifier where Modal: View, T: Iden
|
|||
modal($0)
|
||||
.frame(minWidth: modalSize?.width, minHeight: modalSize?.height)
|
||||
.interactiveDismissDisabled(!isInteractive)
|
||||
.themeLockScreen()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -198,6 +202,41 @@ struct ThemeHoverListRowModifier: ViewModifier {
|
|||
}
|
||||
}
|
||||
|
||||
struct ThemeLockScreenModifier: ViewModifier {
|
||||
|
||||
@AppStorage(AppPreference.locksInBackground.key)
|
||||
private var locksInBackground = false
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
LockableView(
|
||||
locksInBackground: $locksInBackground,
|
||||
content: {
|
||||
content
|
||||
},
|
||||
lockedContent: LogoView.init,
|
||||
unlockBlock: Self.unlockScreenBlock
|
||||
)
|
||||
}
|
||||
|
||||
private static func unlockScreenBlock() async -> Bool {
|
||||
let context = LAContext()
|
||||
let policy: LAPolicy = .deviceOwnerAuthentication
|
||||
var error: NSError?
|
||||
guard context.canEvaluatePolicy(policy, error: &error) else {
|
||||
return true
|
||||
}
|
||||
do {
|
||||
let isAuthorized = try await context.evaluatePolicy(
|
||||
policy,
|
||||
localizedReason: Strings.Views.Lockable.message
|
||||
)
|
||||
return isAuthorized
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Views
|
||||
|
||||
public enum ThemeAnimationCategory: CaseIterable {
|
||||
|
|
|
@ -60,6 +60,8 @@ public final class Theme: ObservableObject {
|
|||
|
||||
var emptyMessageColor: Color = .secondary
|
||||
|
||||
var primaryColor = Color(red: 0.318, green: 0.365, blue: 0.443)
|
||||
|
||||
var activeColor = Color(red: .zero, green: Double(0xAA) / 255.0, blue: .zero)
|
||||
|
||||
var inactiveColor: Color = .gray
|
||||
|
@ -72,6 +74,8 @@ public final class Theme: ObservableObject {
|
|||
|
||||
var animationCategories: Set<ThemeAnimationCategory> = Set(ThemeAnimationCategory.allCases)
|
||||
|
||||
var logoImage = "Logo"
|
||||
|
||||
var systemImage: (ImageName) -> String = {
|
||||
switch $0 {
|
||||
case .add: return "plus"
|
||||
|
@ -207,6 +211,10 @@ extension View {
|
|||
public func themeHoverListRow() -> some View {
|
||||
modifier(ThemeHoverListRowModifier())
|
||||
}
|
||||
|
||||
public func themeLockScreen() -> some View {
|
||||
modifier(ThemeLockScreenModifier())
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Views
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
//
|
||||
// LogoView.swift
|
||||
// Passepartout
|
||||
//
|
||||
// Created by Davide De Rosa on 3/20/23.
|
||||
// Copyright (c) 2024 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 {
|
||||
|
||||
@EnvironmentObject
|
||||
private var theme: Theme
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
theme.primaryColor
|
||||
.ignoresSafeArea()
|
||||
Image(theme.logoImage)
|
||||
}
|
||||
.ignoresSafeArea()
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
LogoView()
|
||||
.environmentObject(Theme())
|
||||
}
|
|
@ -26,10 +26,12 @@
|
|||
import Foundation
|
||||
|
||||
public enum AppPreference: String {
|
||||
case profilesLayout
|
||||
case locksInBackground
|
||||
|
||||
case logsPrivateData
|
||||
|
||||
case profilesLayout
|
||||
|
||||
public var key: String {
|
||||
"App.\(rawValue)"
|
||||
}
|
||||
|
|
|
@ -0,0 +1,144 @@
|
|||
//
|
||||
// LockableView.swift
|
||||
// Passepartout
|
||||
//
|
||||
// Created by Davide De Rosa on 3/20/23.
|
||||
// Copyright (c) 2024 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
|
||||
|
||||
public struct LockableView<Content: View, LockedContent: View>: View {
|
||||
|
||||
@Environment(\.scenePhase)
|
||||
private var scenePhase
|
||||
|
||||
@Binding
|
||||
private var locksInBackground: Bool
|
||||
|
||||
private let content: () -> Content
|
||||
|
||||
private let lockedContent: () -> LockedContent
|
||||
|
||||
private let unlockBlock: () async -> Bool
|
||||
|
||||
@ObservedObject
|
||||
private var lock: Lock = .shared
|
||||
|
||||
@Binding
|
||||
private var state: Lock.State
|
||||
|
||||
public init(
|
||||
locksInBackground: Binding<Bool>,
|
||||
content: @escaping () -> Content,
|
||||
lockedContent: @escaping () -> LockedContent,
|
||||
unlockBlock: @escaping () async -> Bool
|
||||
) {
|
||||
_locksInBackground = locksInBackground
|
||||
self.content = content
|
||||
self.lockedContent = lockedContent
|
||||
self.unlockBlock = unlockBlock
|
||||
|
||||
_state = .init {
|
||||
Lock.shared.state
|
||||
} set: {
|
||||
Lock.shared.state = $0
|
||||
}
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
ZStack {
|
||||
content()
|
||||
if locksInBackground && state != .none {
|
||||
lockedContent()
|
||||
}
|
||||
}.onChange(of: scenePhase, perform: onScenePhase)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
private final class Lock: ObservableObject {
|
||||
enum State {
|
||||
case none
|
||||
|
||||
case covered
|
||||
|
||||
case locked
|
||||
}
|
||||
|
||||
static let shared = Lock()
|
||||
|
||||
@Published var state: State = .locked
|
||||
|
||||
private init() {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
private extension LockableView {
|
||||
func onScenePhase(_ scenePhase: ScenePhase) {
|
||||
switch scenePhase {
|
||||
case .active:
|
||||
unlockIfNeeded()
|
||||
|
||||
case .inactive:
|
||||
if state == .none {
|
||||
state = .covered
|
||||
}
|
||||
|
||||
case .background:
|
||||
lockIfNeeded()
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
func lockIfNeeded() {
|
||||
guard locksInBackground else {
|
||||
return
|
||||
}
|
||||
state = .locked
|
||||
}
|
||||
|
||||
func unlockIfNeeded() {
|
||||
guard locksInBackground else {
|
||||
state = .none
|
||||
return
|
||||
}
|
||||
switch state {
|
||||
case .none:
|
||||
break
|
||||
|
||||
case .covered:
|
||||
state = .none
|
||||
|
||||
case .locked:
|
||||
Task { @MainActor in
|
||||
guard await unlockBlock() else {
|
||||
return
|
||||
}
|
||||
state = .none
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue