diff --git a/Passepartout.xcodeproj/project.pbxproj b/Passepartout.xcodeproj/project.pbxproj index f10fbb64..5052032c 100644 --- a/Passepartout.xcodeproj/project.pbxproj +++ b/Passepartout.xcodeproj/project.pbxproj @@ -24,15 +24,13 @@ 0E34A2B627CAA8CC00C73B67 /* Core+L10n.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E34A2B527CAA8CC00C73B67 /* Core+L10n.swift */; }; 0E34A2B927CAA96A00C73B67 /* OpenVPN+L10n.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E34A2AF27CAA84500C73B67 /* OpenVPN+L10n.swift */; }; 0E34A2CF27CADA6300C73B67 /* GenericVersionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E34A2CE27CADA6300C73B67 /* GenericVersionView.swift */; }; - 0E34AC7627F83FE20042F2AB /* OrganizerView+VPN.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E34AC7527F83FE20042F2AB /* OrganizerView+VPN.swift */; }; 0E34AC7827F840890042F2AB /* OrganizerView+Scene.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E34AC7727F840890042F2AB /* OrganizerView+Scene.swift */; }; - 0E34AC7A27F8431D0042F2AB /* OrganizerView+Shortcuts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E34AC7927F8431D0042F2AB /* OrganizerView+Shortcuts.swift */; }; 0E34AC7C27F845510042F2AB /* OrganizerView+Profiles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E34AC7B27F845510042F2AB /* OrganizerView+Profiles.swift */; }; - 0E34AC7E27F849050042F2AB /* OrganizerView+AddProfileMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E34AC7D27F849050042F2AB /* OrganizerView+AddProfileMenu.swift */; }; 0E34AC8227F892C40042F2AB /* OnDemandView+SSID.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E34AC8127F892C40042F2AB /* OnDemandView+SSID.swift */; }; 0E3B7FCD27E47B3700C66F13 /* AddHostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E3B7FCC27E47B3700C66F13 /* AddHostView.swift */; }; 0E3B7FD627E5173A00C66F13 /* ProfileView+VPN.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E3B7FD527E5173A00C66F13 /* ProfileView+VPN.swift */; }; 0E3B7FDA27E51A0200C66F13 /* ProfileView+Provider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E3B7FD927E51A0200C66F13 /* ProfileView+Provider.swift */; }; + 0E3CD47F280DA14B007075C0 /* OrganizerView+AddMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E3CD47E280DA14B007075C0 /* OrganizerView+AddMenu.swift */; }; 0E44689627B051C300A14CE4 /* ProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E44689527B051C300A14CE4 /* ProfileView.swift */; }; 0E44689C27B11B5300A14CE4 /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E44689B27B11B5300A14CE4 /* AboutView.swift */; }; 0E49F6BB27D7638300385834 /* EndpointAdvancedView+OpenVPN.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E49F6BA27D7638300385834 /* EndpointAdvancedView+OpenVPN.swift */; }; @@ -110,6 +108,7 @@ 0ED89C2027DE423B008B36D6 /* ShortcutsView+ConnectTo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ED89C1F27DE423B008B36D6 /* ShortcutsView+ConnectTo.swift */; }; 0ED89C2527DE45A3008B36D6 /* ProfileHeaderRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ED89C2427DE45A3008B36D6 /* ProfileHeaderRow.swift */; }; 0EDE02C227F61C79000FBE3C /* EditableTextList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EDE02C127F61C79000FBE3C /* EditableTextList.swift */; }; + 0EE11CD2280D8317003BE431 /* OrganizerView+SettingsMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EE11CD1280D8317003BE431 /* OrganizerView+SettingsMenu.swift */; }; 0EE8B7E327FF340F00B68621 /* VPNProtocolType+FileExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EE8B7E227FF340F00B68621 /* VPNProtocolType+FileExtensions.swift */; }; 0EED0BB92733CEDA00C9FC68 /* PassepartoutCore in Frameworks */ = {isa = PBXBuildFile; productRef = 0EED0BB82733CEDA00C9FC68 /* PassepartoutCore */; }; 0EF0FAF627DD0211007EB181 /* PaywallView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EF0FAF527DD0211007EB181 /* PaywallView.swift */; }; @@ -205,15 +204,13 @@ 0E34A2AF27CAA84500C73B67 /* OpenVPN+L10n.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OpenVPN+L10n.swift"; sourceTree = ""; }; 0E34A2B527CAA8CC00C73B67 /* Core+L10n.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Core+L10n.swift"; sourceTree = ""; }; 0E34A2CE27CADA6300C73B67 /* GenericVersionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenericVersionView.swift; sourceTree = ""; }; - 0E34AC7527F83FE20042F2AB /* OrganizerView+VPN.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OrganizerView+VPN.swift"; sourceTree = ""; }; 0E34AC7727F840890042F2AB /* OrganizerView+Scene.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OrganizerView+Scene.swift"; sourceTree = ""; }; - 0E34AC7927F8431D0042F2AB /* OrganizerView+Shortcuts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OrganizerView+Shortcuts.swift"; sourceTree = ""; }; 0E34AC7B27F845510042F2AB /* OrganizerView+Profiles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OrganizerView+Profiles.swift"; sourceTree = ""; }; - 0E34AC7D27F849050042F2AB /* OrganizerView+AddProfileMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OrganizerView+AddProfileMenu.swift"; sourceTree = ""; }; 0E34AC8127F892C40042F2AB /* OnDemandView+SSID.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OnDemandView+SSID.swift"; sourceTree = ""; }; 0E3B7FCC27E47B3700C66F13 /* AddHostView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddHostView.swift; sourceTree = ""; }; 0E3B7FD527E5173A00C66F13 /* ProfileView+VPN.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProfileView+VPN.swift"; sourceTree = ""; }; 0E3B7FD927E51A0200C66F13 /* ProfileView+Provider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProfileView+Provider.swift"; sourceTree = ""; }; + 0E3CD47E280DA14B007075C0 /* OrganizerView+AddMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OrganizerView+AddMenu.swift"; sourceTree = ""; }; 0E44689527B051C300A14CE4 /* ProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileView.swift; sourceTree = ""; }; 0E44689B27B11B5300A14CE4 /* AboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = ""; }; 0E49F6BA27D7638300385834 /* EndpointAdvancedView+OpenVPN.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EndpointAdvancedView+OpenVPN.swift"; sourceTree = ""; }; @@ -325,6 +322,7 @@ 0EDE8DC320C86910004C739C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 0EDE8DD220C86978004C739C /* NotificationCenter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = NotificationCenter.framework; path = System/Library/Frameworks/NotificationCenter.framework; sourceTree = SDKROOT; }; 0EDE8DE220C86A13004C739C /* App.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = App.entitlements; sourceTree = ""; }; + 0EE11CD1280D8317003BE431 /* OrganizerView+SettingsMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OrganizerView+SettingsMenu.swift"; sourceTree = ""; }; 0EE8B7E227FF340F00B68621 /* VPNProtocolType+FileExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "VPNProtocolType+FileExtensions.swift"; sourceTree = ""; }; 0EF0FAF527DD0211007EB181 /* PaywallView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaywallView.swift; sourceTree = ""; }; 0EF0FAF827DD212C007EB181 /* IntentActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntentActivity.swift; sourceTree = ""; }; @@ -391,11 +389,10 @@ 0EB34BC927C6A70200B126DA /* OnDemandView.swift */, 0E34AC8127F892C40042F2AB /* OnDemandView+SSID.swift */, 0E2A8D4E27B04BB900207D04 /* OrganizerView.swift */, - 0E34AC7D27F849050042F2AB /* OrganizerView+AddProfileMenu.swift */, + 0E3CD47E280DA14B007075C0 /* OrganizerView+AddMenu.swift */, 0E34AC7B27F845510042F2AB /* OrganizerView+Profiles.swift */, 0E34AC7727F840890042F2AB /* OrganizerView+Scene.swift */, - 0E34AC7927F8431D0042F2AB /* OrganizerView+Shortcuts.swift */, - 0E34AC7527F83FE20042F2AB /* OrganizerView+VPN.swift */, + 0EE11CD1280D8317003BE431 /* OrganizerView+SettingsMenu.swift */, 0ED89C2427DE45A3008B36D6 /* ProfileHeaderRow.swift */, 0E44689527B051C300A14CE4 /* ProfileView.swift */, 0E92D7C527F103300033CB7B /* ProfileView+Configuration.swift */, @@ -937,8 +934,6 @@ 0E34AC7827F840890042F2AB /* OrganizerView+Scene.swift in Sources */, 0E0BD27927B2EBE500583AC5 /* ShortcutsView.swift in Sources */, 0E92D7C627F103300033CB7B /* ProfileView+Configuration.swift in Sources */, - 0E34AC7A27F8431D0042F2AB /* OrganizerView+Shortcuts.swift in Sources */, - 0E34AC7E27F849050042F2AB /* OrganizerView+AddProfileMenu.swift in Sources */, 0E2DE71C27DCCFE80067B9E1 /* TunnelKit+Identifiable.swift in Sources */, 0ED1D6DE27DBA42100983466 /* DiagnosticsView+WireGuard.swift in Sources */, 0EF2213127E674BD001D0BD7 /* AddProviderViewModel.swift in Sources */, @@ -964,6 +959,7 @@ 0E34A2CF27CADA6300C73B67 /* GenericVersionView.swift in Sources */, 0E9C233327F47E95007D5FC7 /* IntentDispatcher+Activities.swift in Sources */, 0EBC075D27EC529000208AD9 /* DebugLog+Constants.swift in Sources */, + 0E3CD47F280DA14B007075C0 /* OrganizerView+AddMenu.swift in Sources */, 0EB17EAA27D226C900D473B5 /* Constants+Extensions.swift in Sources */, 0E53E63727E34FE2001D4902 /* AppContext.swift in Sources */, 0E3B7FD627E5173A00C66F13 /* ProfileView+VPN.swift in Sources */, @@ -986,7 +982,6 @@ 0E49F6BB27D7638300385834 /* EndpointAdvancedView+OpenVPN.swift in Sources */, 0E71ACEF27C106B500F85C4B /* ProviderPresetView.swift in Sources */, 0E0AD49027BD53CB00FBB520 /* ProfileView+Welcome.swift in Sources */, - 0E34AC7627F83FE20042F2AB /* OrganizerView+VPN.swift in Sources */, 0EF2212F27E66F60001D0BD7 /* AddProfileView.swift in Sources */, 0EF0FAF627DD0211007EB181 /* PaywallView.swift in Sources */, 0E5349BE27C16A4500C71BB3 /* StyledPicker.swift in Sources */, @@ -1001,6 +996,7 @@ 0EF0FAF727DD159C007EB181 /* IntentDispatcher.swift in Sources */, 0E12BC8F27F62C8600B2F912 /* Validators.swift in Sources */, 0E9ED48127FD9BAE003B2316 /* CopySavingButton.swift in Sources */, + 0EE11CD2280D8317003BE431 /* OrganizerView+SettingsMenu.swift in Sources */, 0E44689C27B11B5300A14CE4 /* AboutView.swift in Sources */, 0E71ACF927C12E4800F85C4B /* CreditsView.swift in Sources */, 0ED89C1527DE0A0C008B36D6 /* Shortcut.swift in Sources */, diff --git a/Passepartout/App/Shared/Constants/Theme.swift b/Passepartout/App/Shared/Constants/Theme.swift index fb23e8a0..73a2607c 100644 --- a/Passepartout/App/Shared/Constants/Theme.swift +++ b/Passepartout/App/Shared/Constants/Theme.swift @@ -37,28 +37,6 @@ extension Color { } extension View { - - @available(iOS 14, *) - func themeConfigureNavigationBarAppearance() { - let navBackgroundColor = Asset.Assets.primaryColor.color - let titleAttributes: [NSAttributedString.Key: Any] = [ - .foregroundColor: Asset.Assets.lightTextColor.color - ] - - let navBarAppearance = UINavigationBarAppearance() - navBarAppearance.configureWithOpaqueBackground() - navBarAppearance.backgroundColor = navBackgroundColor - navBarAppearance.titleTextAttributes = titleAttributes - navBarAppearance.largeTitleTextAttributes = titleAttributes - - let navBar = UINavigationBar.appearance() - navBar.standardAppearance = navBarAppearance - navBar.compactAppearance = navBarAppearance - navBar.scrollEdgeAppearance = navBarAppearance - -// UITableView.appearance().backgroundColor = .clear - } - @available(iOS 14, *) var themeIdiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom @@ -78,7 +56,7 @@ extension View { @ViewBuilder private func themeNavigationViewStyle() -> some View { - switch UIDevice.current.userInterfaceIdiom { + switch themeIdiom { case .phone: navigationViewStyle(.stack) @@ -87,15 +65,8 @@ extension View { } } - @ViewBuilder func themePrimaryView() -> some View { - switch UIDevice.current.userInterfaceIdiom { - case .phone: - navigationBarTitleDisplayMode(.large) - - default: - themeSecondaryView() - } + navigationBarTitleDisplayMode(.large) } func themeSecondaryView() -> some View { @@ -106,6 +77,11 @@ extension View { lineLimit(1) .truncationMode(.middle) } + + func themeInformativeText() -> some View { + font(.title) + .foregroundColor(themeSecondaryColor) + } } // MARK: Colors @@ -180,10 +156,14 @@ extension View { "externaldrive.connected.to.line.below.fill" } - var themeAddProfileImage: String { + var themeSettingsMenuImage: String { + "ellipsis.circle" + } + + var themeAddMenuImage: String { "plus" } - + var themeCheckmarkImage: String { "checkmark" } diff --git a/Passepartout/App/Shared/InApp/ProductManager.swift b/Passepartout/App/Shared/InApp/ProductManager.swift index a185e16b..d6f48a2e 100644 --- a/Passepartout/App/Shared/InApp/ProductManager.swift +++ b/Passepartout/App/Shared/InApp/ProductManager.swift @@ -103,6 +103,10 @@ class ProductManager: NSObject, ObservableObject { SKPaymentQueue.default().remove(self) } + func canMakePayments() -> Bool { + SKPaymentQueue.canMakePayments() + } + func refreshProducts() { let ids = LocalProduct.all guard !ids.isEmpty else { diff --git a/Passepartout/App/Shared/Info.plist b/Passepartout/App/Shared/Info.plist index b943d576..d938d57e 100644 --- a/Passepartout/App/Shared/Info.plist +++ b/Passepartout/App/Shared/Info.plist @@ -78,8 +78,6 @@ arm64 - UIStatusBarStyle - UIStatusBarStyleLightContent UISupportedInterfaceOrientations UIInterfaceOrientationPortrait @@ -93,8 +91,6 @@ UISupportsDocumentBrowser - UIViewControllerBasedStatusBarAppearance - com.algoritmico.Passepartout.config appstore_id diff --git a/Passepartout/App/Shared/L10n/TunnelKit+L10n.swift b/Passepartout/App/Shared/L10n/TunnelKit+L10n.swift index da4359ef..81fa83c3 100644 --- a/Passepartout/App/Shared/L10n/TunnelKit+L10n.swift +++ b/Passepartout/App/Shared/L10n/TunnelKit+L10n.swift @@ -28,7 +28,7 @@ import TunnelKitManager import TunnelKitOpenVPN import TunnelKitWireGuard import NetworkExtension -import PassepartoutUtils +import PassepartoutCore extension VPNStatus { var localizedDescription: String { diff --git a/Passepartout/App/iOS/Views/AboutView.swift b/Passepartout/App/iOS/Views/AboutView.swift index f3d8f179..f2ce55c2 100644 --- a/Passepartout/App/iOS/Views/AboutView.swift +++ b/Passepartout/App/iOS/Views/AboutView.swift @@ -26,17 +26,6 @@ import SwiftUI struct AboutView: View { - enum ModalType: Identifiable { - case share([Any]) - - // XXX: alert ids - var id: Int { - return 1 - } - } - - @State private var modalType: ModalType? - private let versionString = Constants.Global.appVersionString private let readmeURL = Constants.URLs.readme @@ -51,24 +40,13 @@ struct AboutView: View { private let privacyURL = Constants.URLs.privacyPolicy - private let alternativeToURL = Constants.URLs.alternativeTo - - private let shareMessage = L10n.Global.Messages.share - var body: some View { List { infoSubview githubSubview webSubview - shareSubview }.themeSecondaryView() .navigationTitle(L10n.About.title) - .sheet(item: $modalType) { - switch $0 { - case .share(let items): - ActivityView(activityItems: items) - } - } } private var infoSubview: some View { @@ -116,28 +94,4 @@ struct AboutView: View { } } } - - private var shareSubview: some View { - Section( - header: Text(L10n.About.Sections.Share.header) - ) { - Button(L10n.About.Items.ShareTwitter.caption, action: shareOnTwitter) - Button(L10n.About.Items.ShareGeneric.caption, action: shareWithFriend) - Button(Unlocalized.About.alternativeTo, action: shareAlternativeTo) - } - } - - private func shareOnTwitter() { - let url = Unlocalized.Social.twitterIntent(withMessage: shareMessage) - URL.openURL(url) - } - - private func shareWithFriend() { - let shareMessage = "\(shareMessage) \(Constants.URLs.website)" - modalType = .share([shareMessage]) - } - - private func shareAlternativeTo() { - URL.openURL(alternativeToURL) - } } diff --git a/Passepartout/App/iOS/Views/MainView.swift b/Passepartout/App/iOS/Views/MainView.swift index 6de9604b..7a8f2405 100644 --- a/Passepartout/App/iOS/Views/MainView.swift +++ b/Passepartout/App/iOS/Views/MainView.swift @@ -26,10 +26,6 @@ import SwiftUI struct MainView: View { - init() { - themeConfigureNavigationBarAppearance() - } - var body: some View { NavigationView { OrganizerView() diff --git a/Passepartout/App/iOS/Views/OrganizerView+AddProfileMenu.swift b/Passepartout/App/iOS/Views/OrganizerView+AddMenu.swift similarity index 53% rename from Passepartout/App/iOS/Views/OrganizerView+AddProfileMenu.swift rename to Passepartout/App/iOS/Views/OrganizerView+AddMenu.swift index 10b0c3a1..062655ff 100644 --- a/Passepartout/App/iOS/Views/OrganizerView+AddProfileMenu.swift +++ b/Passepartout/App/iOS/Views/OrganizerView+AddMenu.swift @@ -1,8 +1,8 @@ // -// OrganizerView+AddProfileMenu.swift +// OrganizerView+AddMenu.swift // Passepartout // -// Created by Davide De Rosa on 4/2/22. +// Created by Davide De Rosa on 4/18/22. // Copyright (c) 2022 Davide De Rosa. All rights reserved. // // https://github.com/passepartoutvpn @@ -27,31 +27,21 @@ import SwiftUI import PassepartoutCore extension OrganizerView { - struct AddProfileMenu: View { - struct Bindings { - @Binding var modalType: ModalType? - - @Binding var alertType: AlertType? - - @Binding var isHostFileImporterPresented: Bool - } - - private let withImportedURLs: Bool - - private let bindings: Bindings - - init( - withImportedURLs: Bool, - bindings: Bindings - ) { - self.withImportedURLs = withImportedURLs - self.bindings = bindings + struct AddMenu: View { + @Binding private var modalType: ModalType? + + @Binding private var isHostFileImporterPresented: Bool + + init(modalType: Binding, isHostFileImporterPresented: Binding) { + _modalType = modalType + _isHostFileImporterPresented = isHostFileImporterPresented } + // FIXME: l10n, shorten menu captions var body: some View { - Group { + Menu { Button { - bindings.modalType = .addProvider + modalType = .addProvider } label: { Label(L10n.Organizer.Items.AddProvider.caption, systemImage: themeProviderImage) } @@ -60,12 +50,12 @@ extension OrganizerView { } label: { Label(L10n.Organizer.Items.AddHost.caption, systemImage: themeHostImage) } - if withImportedURLs { + if let urls = importedURLs, !urls.isEmpty { Divider() - importedURLs.map { urls in - ForEach(urls, id: \.absoluteString, content: importedURLRow) - } + ForEach(urls, id: \.absoluteString, content: importedURLRow) } + } label: { + themeAddMenuImage.asSystemImage } } @@ -86,33 +76,31 @@ extension OrganizerView { return nil } } - } -} -extension OrganizerView.AddProfileMenu { - private func presentAddProvider() { - bindings.modalType = .addProvider - } - - private func presentAddHost(withURL url: URL, deletingURLOnSuccess: Bool) { - bindings.modalType = .addHost(url, deletingURLOnSuccess) - } - - private func presentHostFileImporter() { - - // XXX: iOS bug, hack around crappy bug when dismissing by swiping down - // - // https://stackoverflow.com/questions/66965471/swiftui-fileimporter-modifier-not-updating-binding-when-dismissed-by-tapping - bindings.isHostFileImporterPresented = false - Task { - await Task.maybeWait(forMilliseconds: Constants.Delays.xxxPresentFileImporter) - bindings.isHostFileImporterPresented = true + private func presentAddProvider() { + modalType = .addProvider } -// isHostFileImporterPresented = true -// // use this to test hardcoded bundle file -// let url = Bundle.main.url(forResource: "pia", withExtension: "ovpn")! -// importedProfileName = "pia.ovpn" -// modalType = .addHost(url, false) + private func presentAddHost(withURL url: URL, deletingURLOnSuccess: Bool) { + modalType = .addHost(url, deletingURLOnSuccess) + } + + private func presentHostFileImporter() { + + // XXX: iOS bug, hack around crappy bug when dismissing by swiping down + // + // https://stackoverflow.com/questions/66965471/swiftui-fileimporter-modifier-not-updating-binding-when-dismissed-by-tapping + isHostFileImporterPresented = false + Task { + await Task.maybeWait(forMilliseconds: Constants.Delays.xxxPresentFileImporter) + isHostFileImporterPresented = true + } +// isHostFileImporterPresented = true + +// // use this to test hardcoded bundle file +// let url = Bundle.main.url(forResource: "pia", withExtension: "ovpn")! +// importedProfileName = "pia.ovpn" +// modalType = .addHost(url, false) + } } } diff --git a/Passepartout/App/iOS/Views/OrganizerView+Profiles.swift b/Passepartout/App/iOS/Views/OrganizerView+Profiles.swift index a108696a..30a82f68 100644 --- a/Passepartout/App/iOS/Views/OrganizerView+Profiles.swift +++ b/Passepartout/App/iOS/Views/OrganizerView+Profiles.swift @@ -27,7 +27,7 @@ import SwiftUI import PassepartoutCore extension OrganizerView { - struct ProfilesSection: View { + struct ProfilesList: View { @ObservedObject private var appManager: AppManager @ObservedObject private var profileManager: ProfileManager @@ -36,39 +36,32 @@ extension OrganizerView { // just to observe changes in profiles eligibility @ObservedObject private var productManager: ProductManager - - private let addProfileMenuBindings: AddProfileMenu.Bindings + + @Binding private var alertType: AlertType? @State private var isFirstLaunch = true @State private var selectedProfileId: UUID? - init(addProfileMenuBindings: AddProfileMenu.Bindings) { + init(alertType: Binding) { appManager = .shared profileManager = .shared providerManager = .shared productManager = .shared - self.addProfileMenuBindings = addProfileMenuBindings + _alertType = alertType } var body: some View { debugChanges() - return Section { - ReloadingContent( - observing: profileManager.headers, - equality: { - Set($0) == Set($1) - } - ) { - if !$0.isEmpty { - ForEach($0.sorted(), content: navigationLink(forHeader:)) - .onAppear(perform: selectActiveProfile) - } else { - AddProfileMenu( - withImportedURLs: false, - bindings: addProfileMenuBindings - ) - } + return ReloadingContent( + observing: profileManager.headers, + equality: { + Set($0) == Set($1) + } + ) { headers in + mainView(headers) + if headers.isEmpty { + emptyView } }.onAppear(perform: performMigrationsIfNeeded) @@ -80,6 +73,23 @@ extension OrganizerView { selectedProfileId = $0.id } } + + private func mainView(_ headers: [Profile.Header]) -> some View { + List { + Section { + ForEach(headers.sorted(), content: navigationLink(forHeader:)) + .onAppear(perform: selectActiveProfile) + } + } + } + + // FIXME: l10n + private var emptyView: some View { + VStack { + Text("No profiles") + .themeInformativeText() + } + } private func navigationLink(forHeader header: Profile.Header) -> some View { NavigationLink(tag: header.id, selection: $selectedProfileId) { @@ -95,7 +105,7 @@ extension OrganizerView { } } -extension OrganizerView.ProfilesSection { +extension OrganizerView.ProfilesList { struct ActiveProfileHeaderRow: View { @ObservedObject private var currentVPNState: VPNManager.ObservableState @@ -121,7 +131,7 @@ extension OrganizerView.ProfilesSection { } } -extension OrganizerView.ProfilesSection { +extension OrganizerView.ProfilesList { private func selectActiveProfile() { guard isFirstLaunch else { return @@ -133,7 +143,7 @@ extension OrganizerView.ProfilesSection { // - an alert is active, as it would break navigation // - on iPad, as it's already shown // - if addProfileMenuBindings.alertType == nil, + if alertType == nil, themeIdiom != .pad, let activeProfileId = profileManager.activeHeader?.id { @@ -146,7 +156,7 @@ extension OrganizerView.ProfilesSection { await appManager.doMigrations(profileManager) } } - + private func dismissSelectionIfDeleted(headers: [Profile.Header]) { if let selectedProfileId = selectedProfileId, !profileManager.isExistingProfile(withId: selectedProfileId) { diff --git a/Passepartout/App/iOS/Views/OrganizerView+SettingsMenu.swift b/Passepartout/App/iOS/Views/OrganizerView+SettingsMenu.swift new file mode 100644 index 00000000..52c16356 --- /dev/null +++ b/Passepartout/App/iOS/Views/OrganizerView+SettingsMenu.swift @@ -0,0 +1,148 @@ +// +// OrganizerView+SettingsMenu.swift +// Passepartout +// +// Created by Davide De Rosa on 4/18/22. +// Copyright (c) 2022 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 . +// + +import SwiftUI +import PassepartoutCore + +extension OrganizerView { + struct SettingsMenu: View { + @ObservedObject private var productManager: ProductManager + + @Binding var modalType: ModalType? + + @Binding var alertType: AlertType? + + private var isEligibleForSiri: Bool { + productManager.isEligible(forFeature: .siriShortcuts) + } + + private let redditURL = Constants.URLs.subreddit + + private let alternativeToURL = Constants.URLs.alternativeTo + + private let shareMessage = L10n.Global.Messages.share + + private let appName = Unlocalized.appName + + init(modalType: Binding, alertType: Binding) { + productManager = .shared + _modalType = modalType + _alertType = alertType + } + + var body: some View { + Menu { + Menu(L10n.Menu.Support.title) { + supportMenu + } + // FIXME: l10n, refactor string id to "menu.share.title" + Menu(L10n.About.Sections.Share.header) { + shareMenu + } + Divider() + shortcutsButton + aboutButton +// RemoveVPNSection() +// betaSection + } label: { + themeSettingsMenuImage.asSystemImage + } + } + + private var shortcutsButton: some View { + Button { + presentShortcutsOrPaywall() + } label: { + Label(L10n.Organizer.Items.SiriShortcuts.caption, systemImage: themeShortcutsImage) + } + } + + private var supportMenu: some View { + Group { + Button { + modalType = .donate + } label: { + Label(L10n.Organizer.Items.Donate.caption, systemImage: themeDonateImage) + }.disabled(!productManager.canMakePayments()) + + Button { + URL.openURL(redditURL) + } label: { + Label(L10n.Organizer.Items.JoinCommunity.caption, systemImage: themeRedditImage) + } + Button(action: submitReview) { + Label(L10n.Organizer.Items.WriteReview.caption, systemImage: themeWriteReviewImage) + } + } + } + + private var shareMenu: some View { + Group { + Button(L10n.About.Items.ShareTwitter.caption, action: shareOnTwitter) + Button(L10n.About.Items.ShareGeneric.caption, action: shareWithFriend) + Button(Unlocalized.About.alternativeTo, action: shareAlternativeTo) + } + } + + private var aboutButton: some View { + Button(L10n.Organizer.Items.About.caption(appName)) { + presentAbout() + } + } + + private func presentShortcutsOrPaywall() { + + // eligibility: enter Siri shortcuts or present paywall + if isEligibleForSiri { + modalType = .shortcuts + } else { + modalType = .presentPaywallShortcuts + } + } + + private func shareOnTwitter() { + let url = Unlocalized.Social.twitterIntent(withMessage: shareMessage) + URL.openURL(url) + } + + private func shareWithFriend() { + let shareMessage = "\(shareMessage) \(Constants.URLs.website)" + modalType = .share([shareMessage]) + } + + private func shareAlternativeTo() { + URL.openURL(alternativeToURL) + } + + private func submitReview() { + let reviewURL = Reviewer.urlForReview(withAppId: Constants.App.appStoreId) + URL.openURL(reviewURL) + } + + private func presentAbout() { + modalType = .about + } + } +} diff --git a/Passepartout/App/iOS/Views/OrganizerView+Shortcuts.swift b/Passepartout/App/iOS/Views/OrganizerView+Shortcuts.swift deleted file mode 100644 index f94ffc5b..00000000 --- a/Passepartout/App/iOS/Views/OrganizerView+Shortcuts.swift +++ /dev/null @@ -1,70 +0,0 @@ -// -// OrganizerView+Scene.swift -// Passepartout -// -// Created by Davide De Rosa on 4/2/22. -// Copyright (c) 2022 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 . -// - -import SwiftUI - -extension OrganizerView { - struct ShortcutsSection: View { - @ObservedObject private var productManager: ProductManager - - private var isEligibleForSiri: Bool { - productManager.isEligible(forFeature: .siriShortcuts) - } - - @Binding private var modalType: ModalType? - - init(modalType: Binding) { - productManager = .shared - _modalType = modalType - } - - var body: some View { - Section( - header: Text(Unlocalized.Other.siri), - footer: Text(L10n.Organizer.Sections.Siri.footer) - ) { - // eligibility: enter Siri shortcuts or present paywall - if isEligibleForSiri { - NavigationLink { - ShortcutsView() - } label: { - shortcutsRow - } - } else { - Button { - modalType = .presentPaywallShortcuts - } label: { - shortcutsRow - } - } - } - } - - private var shortcutsRow: some View { -// Text(L10n.Organizer.Items.SiriShortcuts.caption) - Label(L10n.Organizer.Items.SiriShortcuts.caption, systemImage: themeShortcutsImage) - } - } -} diff --git a/Passepartout/App/iOS/Views/OrganizerView+VPN.swift b/Passepartout/App/iOS/Views/OrganizerView+VPN.swift deleted file mode 100644 index f44cbded..00000000 --- a/Passepartout/App/iOS/Views/OrganizerView+VPN.swift +++ /dev/null @@ -1,63 +0,0 @@ -// -// OrganizerView+VPN.swift -// Passepartout -// -// Created by Davide De Rosa on 4/2/22. -// Copyright (c) 2022 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 . -// - -import SwiftUI -import PassepartoutCore - -extension OrganizerView { - struct RemoveVPNSection: View { - @ObservedObject private var vpnManager: VPNManager - - @State private var isAskingUninstallVPN = false - - init() { - vpnManager = .shared - } - - var body: some View { - Section { - Button { - isAskingUninstallVPN = true - } label: { - Label(L10n.Organizer.Items.Uninstall.caption, systemImage: themeDeleteImage) - }.foregroundColor(themeErrorColor) - .actionSheet(isPresented: $isAskingUninstallVPN) { - ActionSheet( - title: Text(L10n.Organizer.Alerts.UninstallVpn.message), - message: nil, - buttons: [ - .destructive(Text(L10n.Organizer.Items.Uninstall.caption), action: { - Task { - await vpnManager.uninstall() - } - }), - .cancel(Text(L10n.Global.Strings.cancel)) - ] - ) - } - } - } - } -} diff --git a/Passepartout/App/iOS/Views/OrganizerView.swift b/Passepartout/App/iOS/Views/OrganizerView.swift index d83d1b68..f9b85892 100644 --- a/Passepartout/App/iOS/Views/OrganizerView.swift +++ b/Passepartout/App/iOS/Views/OrganizerView.swift @@ -24,7 +24,6 @@ // import SwiftUI -import StoreKit import PassepartoutCore struct OrganizerView: View { @@ -33,6 +32,14 @@ struct OrganizerView: View { case addHost(URL, Bool) + case shortcuts + + case donate + + case share([Any]) + + case about + case presentPaywallShortcuts // XXX: alert ids @@ -42,7 +49,15 @@ struct OrganizerView: View { case .addHost: return 2 - case .presentPaywallShortcuts: return 3 + case .shortcuts: return 3 + + case .donate: return 4 + + case .share: return 5 + + case .about: return 6 + + case .presentPaywallShortcuts: return 7 } } } @@ -72,18 +87,14 @@ struct OrganizerView: View { @AppStorage(AppManager.DefaultKey.didHandleSubreddit.rawValue) var didHandleSubreddit = false - init() { - appManager = .shared - } - private let hostFileTypes = Constants.URLs.filetypes private let redditURL = Constants.URLs.subreddit - private let appName = Unlocalized.appName - - private let versionString = Constants.Global.appVersionString - + init() { + appManager = .shared + } + var body: some View { debugChanges() return ZStack { @@ -91,22 +102,7 @@ struct OrganizerView: View { alertType: $alertType, didHandleSubreddit: $didHandleSubreddit ) - List { - ProfilesSection( - addProfileMenuBindings: .init( - modalType: $modalType, - alertType: $alertType, - isHostFileImporterPresented: $isHostFileImporterPresented - ) - ) - ShortcutsSection( - modalType: $modalType - ) - supportSection - aboutSection - RemoveVPNSection() -// betaSection - } + ProfilesList(alertType: $alertType) }.navigationTitle(Unlocalized.appName) .toolbar(content: toolbar) .sheet(item: $modalType, content: presentedModal) @@ -119,18 +115,20 @@ struct OrganizerView: View { ).onOpenURL(perform: onOpenURL) } - private func toolbar() -> some View { - Menu { - AddProfileMenu( - withImportedURLs: true, - bindings: .init( - modalType: $modalType, - alertType: $alertType, - isHostFileImporterPresented: $isHostFileImporterPresented - ) + @ToolbarContentBuilder + private func toolbar() -> some ToolbarContent { + ToolbarItem(placement: .primaryAction) { + AddMenu( + modalType: $modalType, + isHostFileImporterPresented: $isHostFileImporterPresented ) - } label: { - themeAddProfileImage.asSystemImage + } + ToolbarItemGroup(placement: .automatic) { + SettingsMenu( + modalType: $modalType, + alertType: $alertType + ) + EditButton() // FIXME: toolbars, this is not shown } } } @@ -161,6 +159,24 @@ extension OrganizerView { ) ) }.themeGlobal() + + case .shortcuts: + NavigationView { + ShortcutsView() + }.themeGlobal() + + case .donate: + NavigationView { + DonateView() + }.themeGlobal() + + case .share(let items): + ActivityView(activityItems: items) + + case .about: + NavigationView { + AboutView() + }.themeGlobal() case .presentPaywallShortcuts: NavigationView { @@ -228,51 +244,10 @@ extension OrganizerView { } } -// MARK: Minor sections - -extension OrganizerView { - private var supportSection: some View { - Section( - header: Text(L10n.Organizer.Sections.Support.header) - ) { - NavigationLink { - DonateView() - } label: { - Label(L10n.Organizer.Items.Donate.caption, systemImage: themeDonateImage) - }.disabled(!SKPaymentQueue.canMakePayments()) - - Button { - URL.openURL(redditURL) - } label: { - Label(L10n.Organizer.Items.JoinCommunity.caption, systemImage: themeRedditImage) - } - Button(action: submitReview) { - Label(L10n.Organizer.Items.WriteReview.caption, systemImage: themeWriteReviewImage) - } - } - } - - private var aboutSection: some View { - Section { - NavigationLink { - AboutView() - } label: { - Text(L10n.Organizer.Items.About.caption(appName)) -// .withTrailingText(versionString) - } - } - } -} - // MARK: Actions extension OrganizerView { private func presentSubscribeReddit() { alertType = .subscribeReddit } - - private func submitReview() { - let reviewURL = Reviewer.urlForReview(withAppId: Constants.App.appStoreId) - URL.openURL(reviewURL) - } } diff --git a/Passepartout/App/iOS/Views/ProfileView+VPN.swift b/Passepartout/App/iOS/Views/ProfileView+VPN.swift index 24627a71..625e0a3d 100644 --- a/Passepartout/App/iOS/Views/ProfileView+VPN.swift +++ b/Passepartout/App/iOS/Views/ProfileView+VPN.swift @@ -124,4 +124,38 @@ extension ProfileView { } } } + + struct UninstallVPNSection: View { + @ObservedObject private var vpnManager: VPNManager + + @State private var isAskingUninstallVPN = false + + init() { + vpnManager = .shared + } + + var body: some View { + Section { + Button { + isAskingUninstallVPN = true + } label: { + Label(L10n.Organizer.Items.Uninstall.caption, systemImage: themeDeleteImage) + }.foregroundColor(themeErrorColor) + .actionSheet(isPresented: $isAskingUninstallVPN) { + ActionSheet( + title: Text(L10n.Organizer.Alerts.UninstallVpn.message), + message: nil, + buttons: [ + .destructive(Text(L10n.Organizer.Items.Uninstall.caption), action: { + Task { + await vpnManager.uninstall() + } + }), + .cancel(Text(L10n.Global.Strings.cancel)) + ] + ) + } + } + } + } } diff --git a/Passepartout/App/iOS/Views/ProfileView+Welcome.swift b/Passepartout/App/iOS/Views/ProfileView+Welcome.swift index 74edc774..92f4d1e5 100644 --- a/Passepartout/App/iOS/Views/ProfileView+Welcome.swift +++ b/Passepartout/App/iOS/Views/ProfileView+Welcome.swift @@ -30,6 +30,7 @@ extension ProfileView { var body: some View { Text(L10n.Profile.Welcome.message) .multilineTextAlignment(.center) + .themeInformativeText() } } } diff --git a/Passepartout/App/iOS/Views/ProfileView.swift b/Passepartout/App/iOS/Views/ProfileView.swift index 6c140743..7d921090 100644 --- a/Passepartout/App/iOS/Views/ProfileView.swift +++ b/Passepartout/App/iOS/Views/ProfileView.swift @@ -99,6 +99,7 @@ struct ProfileView: View { ExtraSection(currentProfile: profileManager.currentProfile) DiagnosticsSection(currentProfile: profileManager.currentProfile) removeProfileSection + UninstallVPNSection() } private var welcomeView: some View { @@ -171,6 +172,27 @@ struct ProfileView: View { } } + private func confirmRemoveProfile() { + withAnimation { + removeProfile() + } + } + + private func removeProfile() { + guard profileManager.isExistingProfile(withId: header.id) else { + assertionFailure("Deleting non-existent profile \(header.name)") + return + } + IntentDispatcher.forgetProfile(withHeader: header) + profileManager.removeProfiles(withIds: [header.id]) + + // XXX: iOS 14, NavigationLink removal via header removal in OrganizerView+Profiles doesn't pop + if #available(iOS 15, *) { + } else { + presentationMode.wrappedValue.dismiss() + } + } + private func loadProfileIfNeeded() { guard !isLoaded else { return @@ -201,27 +223,6 @@ struct ProfileView: View { presentationMode.wrappedValue.dismiss() } } - - private func confirmRemoveProfile() { - withAnimation { - removeProfile() - } - } - - private func removeProfile() { - guard profileManager.isExistingProfile(withId: header.id) else { - assertionFailure("Deleting non-existent profile \(header.name)") - return - } - IntentDispatcher.forgetProfile(withHeader: header) - profileManager.removeProfiles(withIds: [header.id]) - - // XXX: iOS 14, NavigationLink removal via header removal in OrganizerView+Profiles doesn't pop - if #available(iOS 15, *) { - } else { - presentationMode.wrappedValue.dismiss() - } - } private func presentPaywallTrustedNetworks() { modalType = .paywallTrustedNetworks diff --git a/Passepartout/App/iOS/Views/ShortcutsView.swift b/Passepartout/App/iOS/Views/ShortcutsView.swift index 87de4c77..bcbd7804 100644 --- a/Passepartout/App/iOS/Views/ShortcutsView.swift +++ b/Passepartout/App/iOS/Views/ShortcutsView.swift @@ -80,7 +80,10 @@ struct ShortcutsView: View { } private var addSection: some View { - Section { + Section( + // FIXME: l10n, string id + footer: Text(L10n.Organizer.Sections.Siri.footer) + ) { NavigationLink(isActive: $isNavigationPresented) { AddView( pendingShortcut: delegatingPendingShortcut