diff --git a/.env.tvos b/.env.tvos new file mode 100644 index 00000000..fa06c784 --- /dev/null +++ b/.env.tvos @@ -0,0 +1,7 @@ +INFO_PLIST_ROOT="Passepartout/App" +MATCH_PLATFORM="tvos" +GYM_SCHEME="Passepartout" +DELIVER_PLATFORM="appletvos" +DELIVER_METADATA_PATH="Passepartout/App/fastlane/tvos/metadata" +DELIVER_SCREENSHOTS_PATH="Passepartout/App/fastlane/tvos/screenshots" +CHANGELOG="CHANGELOG.md" diff --git a/.github/workflows/private_beta.yml b/.github/workflows/private_beta.yml index 963ca6ec..8f3172f0 100644 --- a/.github/workflows/private_beta.yml +++ b/.github/workflows/private_beta.yml @@ -17,7 +17,7 @@ env: jobs: build_upload: name: Distribute Private Beta - runs-on: macos-12 + runs-on: macos-13 environment: name: private_beta strategy: @@ -50,7 +50,7 @@ jobs: go-version: "^1.17" - uses: maxim-lobanov/setup-xcode@v1 with: - xcode-version: latest-stable + xcode-version: "15.1" - name: Create keychain uses: ./.github/actions/create-keychain with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 01ecf45a..746c6320 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,7 +16,7 @@ env: jobs: build_upload: name: Upload to ASC - runs-on: macos-12 + runs-on: macos-13 strategy: fail-fast: true matrix: @@ -48,7 +48,7 @@ jobs: go-version: "^1.17" - uses: maxim-lobanov/setup-xcode@v1 with: - xcode-version: latest-stable + xcode-version: "15.1" - name: Store app version id: app_version if: ${{ matrix.use_version }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 332d2f72..fbbfffd7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,7 +18,7 @@ concurrency: jobs: run_tests: name: Run tests - runs-on: macos-12 + runs-on: macos-13 timeout-minutes: 15 steps: - uses: actions/checkout@v3 @@ -26,7 +26,7 @@ jobs: submodules: true - uses: maxim-lobanov/setup-xcode@v1 with: - xcode-version: latest-stable + xcode-version: "15.1" - uses: ruby/setup-ruby@v1 with: bundler-cache: true diff --git a/CHANGELOG.md b/CHANGELOG.md index d219c63c..596c41d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- App for tvOS. [#315](https://github.com/passepartoutvpn/passepartout-apple/issues/315) - WireGuard: Show data count. [#312](https://github.com/passepartoutvpn/passepartout-apple/issues/312) ### Changed diff --git a/Passepartout.xcodeproj/project.pbxproj b/Passepartout.xcodeproj/project.pbxproj index 54cf3034..06796769 100644 --- a/Passepartout.xcodeproj/project.pbxproj +++ b/Passepartout.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 54; + objectVersion = 55; objects = { /* Begin PBXBuildFile section */ @@ -19,7 +19,7 @@ 0E0838FA2877325A00A34EC0 /* LightProviderManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E0838F92877325A00A34EC0 /* LightProviderManager.swift */; }; 0E0838FB2877325A00A34EC0 /* LightProviderManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E0838F92877325A00A34EC0 /* LightProviderManager.swift */; }; 0E0838FD2877334300A34EC0 /* DefaultLightProviderManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E0838FC2877334300A34EC0 /* DefaultLightProviderManager.swift */; }; - 0E09E35D2834172800BE1BAE /* Credits.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 0E09E35C2834172800BE1BAE /* Credits.rtf */; }; + 0E09E35D2834172800BE1BAE /* Credits.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 0E09E35C2834172800BE1BAE /* Credits.rtf */; platformFilter = maccatalyst; }; 0E0BD27327B2EA2C00583AC5 /* MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E0BD27227B2EA2C00583AC5 /* MainView.swift */; }; 0E0BD27627B2EB2200583AC5 /* DonateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E0BD27527B2EB2200583AC5 /* DonateView.swift */; }; 0E0BD27927B2EBE500583AC5 /* ShortcutsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E0BD27827B2EBE500583AC5 /* ShortcutsView.swift */; }; @@ -30,6 +30,7 @@ 0E0F4C6629C84CF60022E884 /* LogoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E0F4C6529C84CF60022E884 /* LogoView.swift */; }; 0E1AD5CE2A268645002AE6E6 /* Errors+L10n.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E1AD5CD2A268645002AE6E6 /* Errors+L10n.swift */; }; 0E1B5F5C29C506AD00FE7D18 /* DiagnosticsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E1B5F5B29C506AC00FE7D18 /* DiagnosticsSection.swift */; }; + 0E1DC1BF2B3618EE008B755E /* ProfileView+TV.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E1DC1BE2B3618EE008B755E /* ProfileView+TV.swift */; }; 0E1F5628287F0ECB00F8ADD7 /* ProviderProfileItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E1F5627287F0ECB00F8ADD7 /* ProviderProfileItem.swift */; }; 0E1F562B287F0EF100F8ADD7 /* ProviderProfileItem+ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E1F5629287F0EEE00F8ADD7 /* ProviderProfileItem+ViewModel.swift */; }; 0E293857285A73BC002A6E0E /* AppContext+Shared.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E293856285A73BC002A6E0E /* AppContext+Shared.swift */; }; @@ -47,6 +48,9 @@ 0E2E0B762B335AAB00E3204A /* UpgradeManagerStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E2E0B722B335AAB00E3204A /* UpgradeManagerStrategy.swift */; }; 0E2E0B772B335AAB00E3204A /* UpgradeManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E2E0B732B335AAB00E3204A /* UpgradeManager.swift */; }; 0E2E0B782B335AAB00E3204A /* PersistenceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E2E0B742B335AAB00E3204A /* PersistenceManager.swift */; }; + 0E330F532B30469700930C7C /* MockProfileRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E330F522B30469700930C7C /* MockProfileRepository.swift */; }; + 0E330F552B30946600930C7C /* ActiveProfileView+TV.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E330F542B30946600930C7C /* ActiveProfileView+TV.swift */; }; + 0E330F572B30952300930C7C /* ProfilesList+TV.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E330F562B30952300930C7C /* ProfilesList+TV.swift */; }; 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 */; }; @@ -108,6 +112,7 @@ 0E7A8C0C2A1D4A6100780F4B /* PassepartoutLibrary in Frameworks */ = {isa = PBXBuildFile; productRef = 0E7A8C0B2A1D4A6100780F4B /* PassepartoutLibrary */; }; 0E7A8C0F2A1D54DE00780F4B /* Picker+OpenVPN.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E7A8C072A1D40BA00780F4B /* Picker+OpenVPN.swift */; }; 0E7A8C102A1D54DE00780F4B /* Picker+Network.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E7A8C082A1D40BA00780F4B /* Picker+Network.swift */; }; + 0E859B832B2EE08700F80D92 /* OrganizerView+TV.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E859B822B2EE08700F80D92 /* OrganizerView+TV.swift */; }; 0E90DFE627BACC1500EF5078 /* AddHostViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E90DFE527BACC1500EF5078 /* AddHostViewModel.swift */; }; 0E92D7C627F103300033CB7B /* ProfileView+Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E92D7C527F103300033CB7B /* ProfileView+Configuration.swift */; }; 0E92D7C927F1042A0033CB7B /* ProfileView+Extra.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E92D7C827F1042A0033CB7B /* ProfileView+Extra.swift */; }; @@ -184,10 +189,12 @@ 0ED89C1727DE0E05008B36D6 /* IntentEditView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ED89C1627DE0E05008B36D6 /* IntentEditView.swift */; }; 0ED89C1C27DE3ABC008B36D6 /* ShortcutsView+Add.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ED89C1B27DE3ABC008B36D6 /* ShortcutsView+Add.swift */; }; 0ED89C1E27DE3F8D008B36D6 /* IntentAddView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ED89C1D27DE3F8D008B36D6 /* IntentAddView.swift */; }; - 0EDDEC7D28D0DC140017802E /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 0EDDEC7C28D0DC130017802E /* LaunchScreen.storyboard */; }; 0EDE02C227F61C79000FBE3C /* EditableTextList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EDE02C127F61C79000FBE3C /* EditableTextList.swift */; }; 0EE11CD2280D8317003BE431 /* SettingsButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EE11CD1280D8317003BE431 /* SettingsButton.swift */; }; + 0EE562782B2EE3EC000C52F6 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 0EE562772B2EE3EC000C52F6 /* LaunchScreen.storyboard */; platformFilters = (ios, maccatalyst, ); }; + 0EE79B2F2B2ED99500C1220C /* MainView+TV.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EE79B2E2B2ED99500C1220C /* MainView+TV.swift */; }; 0EE8B7E327FF340F00B68621 /* VPNProtocolType+FileExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EE8B7E227FF340F00B68621 /* VPNProtocolType+FileExtensions.swift */; }; + 0EED5B9D2B3700AB009D1E97 /* TunnelError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EF656402B36C00E00CEFC96 /* TunnelError.swift */; }; 0EF0FAF627DD0211007EB181 /* PaywallView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EF0FAF527DD0211007EB181 /* PaywallView.swift */; }; 0EF0FAF727DD159C007EB181 /* IntentDispatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EA591122733DD4E0096F796 /* IntentDispatcher.swift */; }; 0EF0FAF927DD212C007EB181 /* IntentActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EF0FAF827DD212C007EB181 /* IntentActivity.swift */; }; @@ -195,6 +202,10 @@ 0EF2212D27E66EB5001D0BD7 /* AddProviderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EF2212C27E66EB5001D0BD7 /* AddProviderView.swift */; }; 0EF2212F27E66F60001D0BD7 /* AddProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EF2212E27E66F60001D0BD7 /* AddProfileView.swift */; }; 0EF2213127E674BD001D0BD7 /* AddProviderViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EF2213027E674BD001D0BD7 /* AddProviderViewModel.swift */; }; + 0EF6563E2B36BFCD00CEFC96 /* NEPacketTunnelProvider+Expiration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EF6563D2B36BFCD00CEFC96 /* NEPacketTunnelProvider+Expiration.swift */; }; + 0EF6563F2B36BFCD00CEFC96 /* NEPacketTunnelProvider+Expiration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EF6563D2B36BFCD00CEFC96 /* NEPacketTunnelProvider+Expiration.swift */; }; + 0EF656422B36C01200CEFC96 /* TunnelError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EF656402B36C00E00CEFC96 /* TunnelError.swift */; }; + 0EF656432B36C01200CEFC96 /* TunnelError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EF656402B36C00E00CEFC96 /* TunnelError.swift */; }; 0EF8C5A828213C510053CE89 /* OrganizerView+Profiles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EF8C5A728213C510053CE89 /* OrganizerView+Profiles.swift */; }; A38D607728AFCFD20005C271 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A38D607628AFCFD20005C271 /* SettingsView.swift */; }; A3A7CC462878DC8300172D7D /* ProviderServerItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3A7CC452878DC8300172D7D /* ProviderServerItem.swift */; }; @@ -248,6 +259,13 @@ remoteGlobalIDString = 0ECF71F327B6D9CD00CDB528; remoteInfo = WireGuardGo; }; + 0EE79B342B2EDB9C00C1220C /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 0E57F63020C83FC5008323CF /* Project object */; + proxyType = 1; + remoteGlobalIDString = 0EE79B302B2EDB5D00C1220C; + remoteInfo = WireGuardGoTV; + }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -311,6 +329,7 @@ 0E1AD5CD2A268645002AE6E6 /* Errors+L10n.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Errors+L10n.swift"; sourceTree = ""; }; 0E1B5F5B29C506AC00FE7D18 /* DiagnosticsSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiagnosticsSection.swift; sourceTree = ""; }; 0E1C0A52238FFF97009FC087 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/InfoPlist.strings; sourceTree = ""; }; + 0E1DC1BE2B3618EE008B755E /* ProfileView+TV.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProfileView+TV.swift"; sourceTree = ""; }; 0E1F5627287F0ECB00F8ADD7 /* ProviderProfileItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProviderProfileItem.swift; sourceTree = ""; }; 0E1F5629287F0EEE00F8ADD7 /* ProviderProfileItem+ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProviderProfileItem+ViewModel.swift"; sourceTree = ""; }; 0E23B4A12298559800304C30 /* Config.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Config.xcconfig; sourceTree = ""; }; @@ -329,6 +348,9 @@ 0E2E0B722B335AAB00E3204A /* UpgradeManagerStrategy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UpgradeManagerStrategy.swift; sourceTree = ""; }; 0E2E0B732B335AAB00E3204A /* UpgradeManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UpgradeManager.swift; sourceTree = ""; }; 0E2E0B742B335AAB00E3204A /* PersistenceManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PersistenceManager.swift; sourceTree = ""; }; + 0E330F522B30469700930C7C /* MockProfileRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockProfileRepository.swift; sourceTree = ""; }; + 0E330F542B30946600930C7C /* ActiveProfileView+TV.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ActiveProfileView+TV.swift"; sourceTree = ""; }; + 0E330F562B30952300930C7C /* ProfilesList+TV.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProfilesList+TV.swift"; sourceTree = ""; }; 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 = ""; }; @@ -391,6 +413,7 @@ 0E7577DE2817E22C00081CBE /* VPNToggle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNToggle.swift; sourceTree = ""; }; 0E7A8C072A1D40BA00780F4B /* Picker+OpenVPN.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Picker+OpenVPN.swift"; sourceTree = ""; }; 0E7A8C082A1D40BA00780F4B /* Picker+Network.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Picker+Network.swift"; sourceTree = ""; }; + 0E859B822B2EE08700F80D92 /* OrganizerView+TV.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OrganizerView+TV.swift"; sourceTree = ""; }; 0E90DFE527BACC1500EF5078 /* AddHostViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddHostViewModel.swift; sourceTree = ""; }; 0E92D7C527F103300033CB7B /* ProfileView+Configuration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProfileView+Configuration.swift"; sourceTree = ""; }; 0E92D7C827F1042A0033CB7B /* ProfileView+Extra.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProfileView+Extra.swift"; sourceTree = ""; }; @@ -493,13 +516,14 @@ 0ED89C1B27DE3ABC008B36D6 /* ShortcutsView+Add.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ShortcutsView+Add.swift"; sourceTree = ""; }; 0ED89C1D27DE3F8D008B36D6 /* IntentAddView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntentAddView.swift; sourceTree = ""; }; 0EDCEF692B337BEB0023A7FF /* PassepartoutLibrary.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = PassepartoutLibrary.xctestplan; sourceTree = ""; }; - 0EDDEC7C28D0DC130017802E /* LaunchScreen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = ""; }; 0EDE02C127F61C79000FBE3C /* EditableTextList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditableTextList.swift; sourceTree = ""; }; 0EDE8DBF20C86910004C739C /* PassepartoutOpenVPNTunnel.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = PassepartoutOpenVPNTunnel.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 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 /* SettingsButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsButton.swift; sourceTree = ""; }; + 0EE562772B2EE3EC000C52F6 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = ""; }; + 0EE79B2E2B2ED99500C1220C /* MainView+TV.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MainView+TV.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 = ""; }; @@ -507,6 +531,8 @@ 0EF2212C27E66EB5001D0BD7 /* AddProviderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddProviderView.swift; sourceTree = ""; }; 0EF2212E27E66F60001D0BD7 /* AddProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddProfileView.swift; sourceTree = ""; }; 0EF2213027E674BD001D0BD7 /* AddProviderViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddProviderViewModel.swift; sourceTree = ""; }; + 0EF6563D2B36BFCD00CEFC96 /* NEPacketTunnelProvider+Expiration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NEPacketTunnelProvider+Expiration.swift"; sourceTree = ""; }; + 0EF656402B36C00E00CEFC96 /* TunnelError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelError.swift; sourceTree = ""; }; 0EF8C5A728213C510053CE89 /* OrganizerView+Profiles.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "OrganizerView+Profiles.swift"; sourceTree = ""; }; A373484D29DC4F4500D1613C /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/InfoPlist.strings; sourceTree = ""; }; A373484E29DC504000D1613C /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/Localizable.strings; sourceTree = ""; }; @@ -572,6 +598,7 @@ 0E021D9B284E68580077EF5D /* AppContext.swift */, 0E293856285A73BC002A6E0E /* AppContext+Shared.swift */, 0E021D9A284E68580077EF5D /* CoreContext.swift */, + 0E330F522B30469700930C7C /* MockProfileRepository.swift */, ); path = Context; sourceTree = ""; @@ -630,6 +657,7 @@ 0E35C0AE280EF8A80071FA35 /* Views */ = { isa = PBXGroup; children = ( + 0EE79B2D2B2ED96D00C1220C /* TV */, 0E44689B27B11B5300A14CE4 /* AboutView.swift */, 0ECF71ED27B6A99300CDB528 /* AccountView.swift */, 0E039278281890B100827C10 /* AddHostView.swift */, @@ -673,6 +701,7 @@ 0E3CD482280DAE92007075C0 /* ProfileView+MainMenu.swift */, 0E3B7FD927E51A0200C66F13 /* ProfileView+Provider.swift */, 0EBC074B27EB673C00208AD9 /* ProfileView+Rename.swift */, + 0E1DC1BE2B3618EE008B755E /* ProfileView+TV.swift */, 0E3B7FD527E5173A00C66F13 /* ProfileView+VPN.swift */, 0E71ACF027C1073800F85C4B /* ProviderLocationView.swift */, 0E71ACEE27C106B400F85C4B /* ProviderPresetView.swift */, @@ -889,7 +918,7 @@ 0E57F64720C83FC7008323CF /* Info.plist */, 0E0C072B236087A100155AAC /* InfoPlist.strings */, 0E9E5AE227B44CF1008C95DA /* Localizable.strings */, - 0EDDEC7C28D0DC130017802E /* LaunchScreen.storyboard */, + 0EE562772B2EE3EC000C52F6 /* LaunchScreen.storyboard */, 0E3FC6852867A3F9009B851C /* AppDelegate.swift */, 0E2A8D4727ADF87F00207D04 /* PassepartoutApp.swift */, 0E0F4C5929C761850022E884 /* SceneDelegate.swift */, @@ -975,6 +1004,8 @@ 0ED2B33D27D3C53400FD8EA9 /* WireGuard */, 0ED31C3B20CF39510027975F /* Tunnel.entitlements */, 0EDE8DC320C86910004C739C /* Info.plist */, + 0EF6563D2B36BFCD00CEFC96 /* NEPacketTunnelProvider+Expiration.swift */, + 0EF656402B36C00E00CEFC96 /* TunnelError.swift */, ); path = Tunnel; sourceTree = ""; @@ -987,6 +1018,17 @@ name = Packages; sourceTree = ""; }; + 0EE79B2D2B2ED96D00C1220C /* TV */ = { + isa = PBXGroup; + children = ( + 0E330F542B30946600930C7C /* ActiveProfileView+TV.swift */, + 0EE79B2E2B2ED99500C1220C /* MainView+TV.swift */, + 0E859B822B2EE08700F80D92 /* OrganizerView+TV.swift */, + 0E330F562B30952300930C7C /* ProfilesList+TV.swift */, + ); + path = TV; + sourceTree = ""; + }; 374B9F085E1148C37CF9117A /* Frameworks */ = { isa = PBXGroup; children = ( @@ -1014,6 +1056,20 @@ passBuildSettingsInEnvironment = 1; productName = PassepartoutWireGuard; }; + 0EE79B302B2EDB5D00C1220C /* WireGuardGoTV */ = { + isa = PBXLegacyTarget; + buildArgumentsString = "$(ACTION)"; + buildConfigurationList = 0EE79B312B2EDB5D00C1220C /* Build configuration list for PBXLegacyTarget "WireGuardGoTV" */; + buildPhases = ( + ); + buildToolPath = "$(PROJECT_DIR)/Passepartout/App/Scripts/build_wireguard_go_bridge.sh"; + buildWorkingDirectory = ""; + dependencies = ( + ); + name = WireGuardGoTV; + passBuildSettingsInEnvironment = 1; + productName = PassepartoutWireGuard; + }; /* End PBXLegacyTarget section */ /* Begin PBXNativeTarget section */ @@ -1052,9 +1108,10 @@ dependencies = ( 0ECB78E7285F5CC400B0E460 /* PBXTargetDependency */, 0E41BDAB286713F6006346B4 /* PBXTargetDependency */, - 0ECF71FC27B6DA6700CDB528 /* PBXTargetDependency */, 0EB2B14A2733FB6F007705AB /* PBXTargetDependency */, 0ED2B36227D3C99100FD8EA9 /* PBXTargetDependency */, + 0ECF71FC27B6DA6700CDB528 /* PBXTargetDependency */, + 0EE79B352B2EDB9C00C1220C /* PBXTargetDependency */, ); name = Passepartout; packageProductDependencies = ( @@ -1210,8 +1267,9 @@ 0ECB78D9285F52F700B0E460 /* PassepartoutMac */, 0E41BD96286711C3006346B4 /* PassepartoutLauncher */, 0EDE8DBE20C86910004C739C /* OpenVPNTunnel */, - 0ECF71F327B6D9CD00CDB528 /* WireGuardGo */, 0ED2B33E27D3C77800FD8EA9 /* WireGuardTunnel */, + 0ECF71F327B6D9CD00CDB528 /* WireGuardGo */, + 0EE79B302B2EDB5D00C1220C /* WireGuardGoTV */, ); }; /* End PBXProject section */ @@ -1229,8 +1287,8 @@ buildActionMask = 2147483647; files = ( 0E6059CB27FCC5DE003F4063 /* Flags.xcassets in Resources */, + 0EE562782B2EE3EC000C52F6 /* LaunchScreen.storyboard in Resources */, 0E0C0729236087A100155AAC /* InfoPlist.strings in Resources */, - 0EDDEC7D28D0DC140017802E /* LaunchScreen.storyboard in Resources */, 0E6059CC27FCC5DE003F4063 /* Providers.xcassets in Resources */, 0E6059CD27FCC5DE003F4063 /* Assets.xcassets in Resources */, 0E9E5AEF27B44CF1008C95DA /* Localizable.strings in Resources */, @@ -1376,6 +1434,7 @@ 0E2DE71C27DCCFE80067B9E1 /* TunnelKit+Extensions.swift in Sources */, 0ED1D6DE27DBA42100983466 /* DiagnosticsView+WireGuard.swift in Sources */, 0EF2213127E674BD001D0BD7 /* AddProviderViewModel.swift in Sources */, + 0EED5B9D2B3700AB009D1E97 /* TunnelError.swift in Sources */, 0E90DFE627BACC1500EF5078 /* AddHostViewModel.swift in Sources */, 0E3A593C2A50975700B3FE40 /* ErrorHandler.swift in Sources */, 0E34AC8227F892C40042F2AB /* OnDemandView+SSID.swift in Sources */, @@ -1411,6 +1470,7 @@ 0ED89C1E27DE3F8D008B36D6 /* IntentAddView.swift in Sources */, 0E5468002867AC9A00F74D1C /* MacUtils.swift in Sources */, 0E0F4C6429C84B5A0022E884 /* LockableView.swift in Sources */, + 0E330F552B30946600930C7C /* ActiveProfileView+TV.swift in Sources */, 0E96D3052872010A005EFBCF /* DefaultLightVPNManager.swift in Sources */, 0EBE880F281B18DE0090D9E6 /* OrganizerView+ProfileRow.swift in Sources */, 0ED30DCF27EA1EF80057D8A3 /* PaywallView+Restricted.swift in Sources */, @@ -1421,6 +1481,7 @@ 0E96D30228720067005EFBCF /* LightVPNManager.swift in Sources */, 0ED89C1727DE0E05008B36D6 /* IntentEditView.swift in Sources */, 0E70589B28377DC40075D1D2 /* VPNStatusText.swift in Sources */, + 0EE79B2F2B2ED99500C1220C /* MainView+TV.swift in Sources */, 0E71ACE927C1055300F85C4B /* NetworkSettingsView.swift in Sources */, 0EB34BCA27C6A70200B126DA /* OnDemandView.swift in Sources */, 0E0838FA2877325A00A34EC0 /* LightProviderManager.swift in Sources */, @@ -1432,6 +1493,7 @@ 0EB17EBA27D2560300D473B5 /* PassepartoutProviders+Extensions.swift in Sources */, 0E3B7FDA27E51A0200C66F13 /* ProfileView+Provider.swift in Sources */, 0E2E0B6F2B335A8E00E3204A /* AppPreference.swift in Sources */, + 0E859B832B2EE08700F80D92 /* OrganizerView+TV.swift in Sources */, 0E5468062867AEC500F74D1C /* MacMenu.swift in Sources */, 0E71ACE327C0F2E400F85C4B /* Providers+L10n.swift in Sources */, 0E2E0B752B335AAB00E3204A /* IntentsManager.swift in Sources */, @@ -1457,6 +1519,7 @@ 0E2A8D4927ADF87F00207D04 /* PassepartoutApp.swift in Sources */, 0EBC075527EBC83800208AD9 /* MailComposerView.swift in Sources */, 0EF0FAF727DD159C007EB181 /* IntentDispatcher.swift in Sources */, + 0E330F572B30952300930C7C /* ProfilesList+TV.swift in Sources */, 0E0838FD2877334300A34EC0 /* DefaultLightProviderManager.swift in Sources */, 0E2E0B772B335AAB00E3204A /* UpgradeManager.swift in Sources */, 0E0F4C6629C84CF60022E884 /* LogoView.swift in Sources */, @@ -1471,6 +1534,7 @@ 0E34A2B927CAA96A00C73B67 /* OpenVPN+L10n.swift in Sources */, 0E2E0B782B335AAB00E3204A /* PersistenceManager.swift in Sources */, 0EF8C5A828213C510053CE89 /* OrganizerView+Profiles.swift in Sources */, + 0E330F532B30469700930C7C /* MockProfileRepository.swift in Sources */, 0E3CD483280DAE92007075C0 /* ProfileView+MainMenu.swift in Sources */, 0E71ACEB27C1060D00F85C4B /* EndpointView.swift in Sources */, 0E293857285A73BC002A6E0E /* AppContext+Shared.swift in Sources */, @@ -1478,6 +1542,7 @@ 0EB4042E27CA136300378B1A /* AddingTextField.swift in Sources */, 0EE8B7E327FF340F00B68621 /* VPNProtocolType+FileExtensions.swift in Sources */, 0EF2212B27E667EA001D0BD7 /* AddProviderView+Name.swift in Sources */, + 0E1DC1BF2B3618EE008B755E /* ProfileView+TV.swift in Sources */, 0E065F112813269500062CAF /* WelcomeView.swift in Sources */, 0E2DE71F27DCD0290067B9E1 /* TunnelKit+L10n.swift in Sources */, 0E49F6BF27D764AF00385834 /* EndpointAdvancedView.swift in Sources */, @@ -1500,9 +1565,11 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 0EF6563F2B36BFCD00CEFC96 /* NEPacketTunnelProvider+Expiration.swift in Sources */, 0ED2B35B27D3C94F00FD8EA9 /* PacketTunnelProvider.swift in Sources */, 0ED30DDD27EA35230057D8A3 /* Constants.swift in Sources */, 0ED1A5FD2B2B98CC00A0EA90 /* Constants+Tunnel.swift in Sources */, + 0EF656432B36C01200CEFC96 /* TunnelError.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1510,9 +1577,11 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 0EF6563E2B36BFCD00CEFC96 /* NEPacketTunnelProvider+Expiration.swift in Sources */, 0E9AA978259F756A003FAFF1 /* PacketTunnelProvider.swift in Sources */, 0EB17EA727D226B400D473B5 /* Constants.swift in Sources */, 0ED30DDB27EA351C0057D8A3 /* Constants+Tunnel.swift in Sources */, + 0EF656422B36C01200CEFC96 /* TunnelError.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1538,6 +1607,10 @@ }; 0ECF71FC27B6DA6700CDB528 /* PBXTargetDependency */ = { isa = PBXTargetDependency; + platformFilters = ( + ios, + maccatalyst, + ); target = 0ECF71F327B6D9CD00CDB528 /* WireGuardGo */; targetProxy = 0ECF71FB27B6DA6700CDB528 /* PBXContainerItemProxy */; }; @@ -1551,6 +1624,14 @@ target = 0ECF71F327B6D9CD00CDB528 /* WireGuardGo */; targetProxy = 0ED2B36A27D3CAB100FD8EA9 /* PBXContainerItemProxy */; }; + 0EE79B352B2EDB9C00C1220C /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + platformFilters = ( + tvos, + ); + target = 0EE79B302B2EDB5D00C1220C /* WireGuardGoTV */; + targetProxy = 0EE79B342B2EDB9C00C1220C /* PBXContainerItemProxy */; + }; /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ @@ -1730,7 +1811,8 @@ SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2,6"; + TARGETED_DEVICE_FAMILY = "1,2,3"; + TVOS_DEPLOYMENT_TARGET = 17.0; }; name = Debug; }; @@ -1791,7 +1873,8 @@ SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2,6"; + TARGETED_DEVICE_FAMILY = "1,2,3"; + TVOS_DEPLOYMENT_TARGET = 17.0; VALIDATE_PRODUCT = YES; }; name = Release; @@ -1801,6 +1884,7 @@ buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + "ASSETCATALOG_COMPILER_APPICON_NAME[sdk=appletvos*]" = TV; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Passepartout/App/App.entitlements; @@ -1814,11 +1898,12 @@ PRODUCT_BUNDLE_IDENTIFIER = "$(CFG_APP_ID)"; PRODUCT_NAME = Passepartout; PROVISIONING_PROFILE_SPECIFIER = "match Development com.algoritmico.ios.Passepartout"; + "PROVISIONING_PROFILE_SPECIFIER[sdk=appletvos*]" = "match Development com.algoritmico.ios.Passepartout tvos"; "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = "match Development com.algoritmico.ios.Passepartout catalyst"; + SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_STRICT_CONCURRENCY = targeted; - TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; }; @@ -1827,6 +1912,7 @@ buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + "ASSETCATALOG_COMPILER_APPICON_NAME[sdk=appletvos*]" = TV; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Passepartout/App/App.entitlements; @@ -1840,10 +1926,11 @@ PRODUCT_BUNDLE_IDENTIFIER = "$(CFG_APP_ID)"; PRODUCT_NAME = Passepartout; PROVISIONING_PROFILE_SPECIFIER = "match Development com.algoritmico.ios.Passepartout"; + "PROVISIONING_PROFILE_SPECIFIER[sdk=appletvos*]" = "match Development com.algoritmico.ios.Passepartout tvos"; "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = "match Development com.algoritmico.ios.Passepartout catalyst"; + SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = YES; SWIFT_STRICT_CONCURRENCY = targeted; - TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; }; @@ -1959,8 +2046,10 @@ PRODUCT_BUNDLE_IDENTIFIER = "$(CFG_APP_ID).WireGuardTunnel"; PRODUCT_NAME = PassepartoutWireGuardTunnel; PROVISIONING_PROFILE_SPECIFIER = "match Development com.algoritmico.ios.Passepartout.WireGuardTunnel"; + "PROVISIONING_PROFILE_SPECIFIER[sdk=appletvos*]" = "match Development com.algoritmico.ios.Passepartout.WireGuardTunnel tvos"; "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = "match Development com.algoritmico.ios.Passepartout.WireGuardTunnel catalyst"; SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = YES; }; name = Debug; @@ -1979,8 +2068,10 @@ PRODUCT_BUNDLE_IDENTIFIER = "$(CFG_APP_ID).WireGuardTunnel"; PRODUCT_NAME = PassepartoutWireGuardTunnel; PROVISIONING_PROFILE_SPECIFIER = "match Development com.algoritmico.ios.Passepartout.WireGuardTunnel"; + "PROVISIONING_PROFILE_SPECIFIER[sdk=appletvos*]" = "match Development com.algoritmico.ios.Passepartout.WireGuardTunnel tvos"; "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = "match Development com.algoritmico.ios.Passepartout.WireGuardTunnel catalyst"; SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = YES; }; name = Release; @@ -1999,8 +2090,10 @@ PRODUCT_BUNDLE_IDENTIFIER = "$(CFG_APP_ID).OpenVPNTunnel"; PRODUCT_NAME = PassepartoutOpenVPNTunnel; PROVISIONING_PROFILE_SPECIFIER = "match Development com.algoritmico.ios.Passepartout.OpenVPNTunnel"; + "PROVISIONING_PROFILE_SPECIFIER[sdk=appletvos*]" = "match Development com.algoritmico.ios.Passepartout.OpenVPNTunnel tvos"; "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = "match Development com.algoritmico.ios.Passepartout.OpenVPNTunnel catalyst"; SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = YES; }; name = Debug; @@ -2019,12 +2112,51 @@ PRODUCT_BUNDLE_IDENTIFIER = "$(CFG_APP_ID).OpenVPNTunnel"; PRODUCT_NAME = PassepartoutOpenVPNTunnel; PROVISIONING_PROFILE_SPECIFIER = "match Development com.algoritmico.ios.Passepartout.OpenVPNTunnel"; + "PROVISIONING_PROFILE_SPECIFIER[sdk=appletvos*]" = "match Development com.algoritmico.ios.Passepartout.OpenVPNTunnel tvos"; "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = "match Development com.algoritmico.ios.Passepartout.OpenVPNTunnel catalyst"; SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = YES; }; name = Release; }; + 0EE79B322B2EDB5D00C1220C /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + DEBUGGING_SYMBOLS = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = DTDYD63ZX9; + GCC_GENERATE_DEBUGGING_SYMBOLS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + OTHER_CFLAGS = ""; + OTHER_LDFLAGS = ""; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = appletvos; + }; + name = Debug; + }; + 0EE79B332B2EDB5D00C1220C /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = DTDYD63ZX9; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + OTHER_CFLAGS = ""; + OTHER_LDFLAGS = ""; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = appletvos; + }; + name = Release; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -2091,6 +2223,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + 0EE79B312B2EDB5D00C1220C /* Build configuration list for PBXLegacyTarget "WireGuardGoTV" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 0EE79B322B2EDB5D00C1220C /* Debug */, + 0EE79B332B2EDB5D00C1220C /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ diff --git a/Passepartout.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Passepartout.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index ecbe6b60..c0ad5312 100644 --- a/Passepartout.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Passepartout.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,70 +1,67 @@ { - "object": { - "pins": [ - { - "package": "DTFoundation", - "repositoryURL": "https://github.com/Cocoanetics/DTFoundation.git", - "state": { - "branch": null, - "revision": "76062513434421cb6c8a1ae1d4f8368a7ebc2da3", - "version": "1.7.18" - } - }, - { - "package": "GenericJSON", - "repositoryURL": "https://github.com/zoul/generic-json-swift", - "state": { - "branch": null, - "revision": "0a06575f4038b504e78ac330913d920f1630f510", - "version": "2.0.2" - } - }, - { - "package": "Kvitto", - "repositoryURL": "https://github.com/Cocoanetics/Kvitto", - "state": { - "branch": null, - "revision": "88888674d772ddcf19671159ed0022cb0bc37be2", - "version": "1.0.6" - } - }, - { - "package": "openssl-apple", - "repositoryURL": "https://github.com/passepartoutvpn/openssl-apple", - "state": { - "branch": null, - "revision": "026702febcaebcbf9ea68f2fa66b017eba998cdf", - "version": "3.2.105" - } - }, - { - "package": "SwiftyBeaver", - "repositoryURL": "https://github.com/SwiftyBeaver/SwiftyBeaver", - "state": { - "branch": null, - "revision": "12b5acf96d98f91d50de447369bd18df74600f1a", - "version": "1.9.6" - } - }, - { - "package": "TunnelKit", - "repositoryURL": "https://github.com/passepartoutvpn/tunnelkit", - "state": { - "branch": null, - "revision": "bda84bf569792fbb702d0173de3c9c58768f9153", - "version": null - } - }, - { - "package": "WireGuardKit", - "repositoryURL": "https://github.com/passepartoutvpn/wireguard-apple", - "state": { - "branch": null, - "revision": "73d9152fa0cb661db0348a1ac11dbbf998422a50", - "version": "1.0.17" - } + "pins" : [ + { + "identity" : "dtfoundation", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Cocoanetics/DTFoundation.git", + "state" : { + "revision" : "76062513434421cb6c8a1ae1d4f8368a7ebc2da3", + "version" : "1.7.18" } - ] - }, - "version": 1 + }, + { + "identity" : "generic-json-swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/zoul/generic-json-swift", + "state" : { + "revision" : "0a06575f4038b504e78ac330913d920f1630f510", + "version" : "2.0.2" + } + }, + { + "identity" : "kvitto", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Cocoanetics/Kvitto", + "state" : { + "revision" : "88888674d772ddcf19671159ed0022cb0bc37be2", + "version" : "1.0.6" + } + }, + { + "identity" : "openssl-apple", + "kind" : "remoteSourceControl", + "location" : "https://github.com/passepartoutvpn/openssl-apple", + "state" : { + "revision" : "026702febcaebcbf9ea68f2fa66b017eba998cdf", + "version" : "3.2.105" + } + }, + { + "identity" : "swiftybeaver", + "kind" : "remoteSourceControl", + "location" : "https://github.com/SwiftyBeaver/SwiftyBeaver", + "state" : { + "revision" : "12b5acf96d98f91d50de447369bd18df74600f1a", + "version" : "1.9.6" + } + }, + { + "identity" : "tunnelkit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/passepartoutvpn/tunnelkit", + "state" : { + "revision" : "708c785e615f5715ce08386c772c92fb45730a3a" + } + }, + { + "identity" : "wireguard-apple", + "kind" : "remoteSourceControl", + "location" : "https://github.com/passepartoutvpn/wireguard-apple", + "state" : { + "branch" : "develop", + "revision" : "b79f0f150356d8200a64922ecf041dd020140aa0" + } + } + ], + "version" : 2 } diff --git a/Passepartout.xcodeproj/xcshareddata/xcschemes/Passepartout.xcscheme b/Passepartout.xcodeproj/xcshareddata/xcschemes/Passepartout.xcscheme index fe233326..fa0c548e 100644 --- a/Passepartout.xcodeproj/xcshareddata/xcschemes/Passepartout.xcscheme +++ b/Passepartout.xcodeproj/xcshareddata/xcschemes/Passepartout.xcscheme @@ -127,7 +127,7 @@ + isEnabled = "NO"> com.apple.developer.icloud-container-identifiers iCloud.com.algoritmico.Passepartout + iCloud.com.algoritmico.Passepartout.Shared com.apple.developer.icloud-services diff --git a/Passepartout/App/Assets.xcassets/TV.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/AppIcon.png b/Passepartout/App/Assets.xcassets/TV.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/AppIcon.png new file mode 100644 index 00000000..c6a9cf1a Binary files /dev/null and b/Passepartout/App/Assets.xcassets/TV.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/AppIcon.png differ diff --git a/Passepartout/App/Assets.xcassets/TV.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/Contents.json b/Passepartout/App/Assets.xcassets/TV.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/Contents.json new file mode 100644 index 00000000..71d8a3df --- /dev/null +++ b/Passepartout/App/Assets.xcassets/TV.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "AppIcon.png", + "idiom" : "tv" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Passepartout/App/Assets.xcassets/TV.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Contents.json b/Passepartout/App/Assets.xcassets/TV.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/Passepartout/App/Assets.xcassets/TV.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Passepartout/App/Assets.xcassets/TV.brandassets/App Icon - App Store.imagestack/Contents.json b/Passepartout/App/Assets.xcassets/TV.brandassets/App Icon - App Store.imagestack/Contents.json new file mode 100644 index 00000000..3d73e5f8 --- /dev/null +++ b/Passepartout/App/Assets.xcassets/TV.brandassets/App Icon - App Store.imagestack/Contents.json @@ -0,0 +1,14 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "layers" : [ + { + "filename" : "Front.imagestacklayer" + }, + { + "filename" : "Back.imagestacklayer" + } + ] +} diff --git a/Passepartout/App/Assets.xcassets/TV.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Content.imageset/AppIcon.png b/Passepartout/App/Assets.xcassets/TV.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Content.imageset/AppIcon.png new file mode 100644 index 00000000..e021d9cc Binary files /dev/null and b/Passepartout/App/Assets.xcassets/TV.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Content.imageset/AppIcon.png differ diff --git a/Passepartout/App/Assets.xcassets/TV.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Content.imageset/Contents.json b/Passepartout/App/Assets.xcassets/TV.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Content.imageset/Contents.json new file mode 100644 index 00000000..71d8a3df --- /dev/null +++ b/Passepartout/App/Assets.xcassets/TV.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Content.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "AppIcon.png", + "idiom" : "tv" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Passepartout/App/Assets.xcassets/TV.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Contents.json b/Passepartout/App/Assets.xcassets/TV.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/Passepartout/App/Assets.xcassets/TV.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Passepartout/App/Assets.xcassets/TV.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/AppIcon.png b/Passepartout/App/Assets.xcassets/TV.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/AppIcon.png new file mode 100644 index 00000000..4cdccf18 Binary files /dev/null and b/Passepartout/App/Assets.xcassets/TV.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/AppIcon.png differ diff --git a/Passepartout/App/Assets.xcassets/TV.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/AppIcon@2x.png b/Passepartout/App/Assets.xcassets/TV.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/AppIcon@2x.png new file mode 100644 index 00000000..1f5c19e5 Binary files /dev/null and b/Passepartout/App/Assets.xcassets/TV.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/AppIcon@2x.png differ diff --git a/Passepartout/App/Assets.xcassets/TV.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/Contents.json b/Passepartout/App/Assets.xcassets/TV.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/Contents.json new file mode 100644 index 00000000..4ed632e1 --- /dev/null +++ b/Passepartout/App/Assets.xcassets/TV.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/Contents.json @@ -0,0 +1,18 @@ +{ + "images" : [ + { + "filename" : "AppIcon.png", + "idiom" : "tv", + "scale" : "1x" + }, + { + "filename" : "AppIcon@2x.png", + "idiom" : "tv", + "scale" : "2x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Passepartout/App/Assets.xcassets/TV.brandassets/App Icon.imagestack/Back.imagestacklayer/Contents.json b/Passepartout/App/Assets.xcassets/TV.brandassets/App Icon.imagestack/Back.imagestacklayer/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/Passepartout/App/Assets.xcassets/TV.brandassets/App Icon.imagestack/Back.imagestacklayer/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Passepartout/App/Assets.xcassets/TV.brandassets/App Icon.imagestack/Contents.json b/Passepartout/App/Assets.xcassets/TV.brandassets/App Icon.imagestack/Contents.json new file mode 100644 index 00000000..3d73e5f8 --- /dev/null +++ b/Passepartout/App/Assets.xcassets/TV.brandassets/App Icon.imagestack/Contents.json @@ -0,0 +1,14 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "layers" : [ + { + "filename" : "Front.imagestacklayer" + }, + { + "filename" : "Back.imagestacklayer" + } + ] +} diff --git a/Passepartout/App/Assets.xcassets/TV.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/AppIcon.png b/Passepartout/App/Assets.xcassets/TV.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/AppIcon.png new file mode 100644 index 00000000..61480f1b Binary files /dev/null and b/Passepartout/App/Assets.xcassets/TV.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/AppIcon.png differ diff --git a/Passepartout/App/Assets.xcassets/TV.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/AppIcon@2x.png b/Passepartout/App/Assets.xcassets/TV.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/AppIcon@2x.png new file mode 100644 index 00000000..a099ce5f Binary files /dev/null and b/Passepartout/App/Assets.xcassets/TV.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/AppIcon@2x.png differ diff --git a/Passepartout/App/Assets.xcassets/TV.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/Contents.json b/Passepartout/App/Assets.xcassets/TV.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/Contents.json new file mode 100644 index 00000000..4ed632e1 --- /dev/null +++ b/Passepartout/App/Assets.xcassets/TV.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/Contents.json @@ -0,0 +1,18 @@ +{ + "images" : [ + { + "filename" : "AppIcon.png", + "idiom" : "tv", + "scale" : "1x" + }, + { + "filename" : "AppIcon@2x.png", + "idiom" : "tv", + "scale" : "2x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Passepartout/App/Assets.xcassets/TV.brandassets/App Icon.imagestack/Front.imagestacklayer/Contents.json b/Passepartout/App/Assets.xcassets/TV.brandassets/App Icon.imagestack/Front.imagestacklayer/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/Passepartout/App/Assets.xcassets/TV.brandassets/App Icon.imagestack/Front.imagestacklayer/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Passepartout/App/Assets.xcassets/TV.brandassets/Contents.json b/Passepartout/App/Assets.xcassets/TV.brandassets/Contents.json new file mode 100644 index 00000000..f47ba43d --- /dev/null +++ b/Passepartout/App/Assets.xcassets/TV.brandassets/Contents.json @@ -0,0 +1,32 @@ +{ + "assets" : [ + { + "filename" : "App Icon - App Store.imagestack", + "idiom" : "tv", + "role" : "primary-app-icon", + "size" : "1280x768" + }, + { + "filename" : "App Icon.imagestack", + "idiom" : "tv", + "role" : "primary-app-icon", + "size" : "400x240" + }, + { + "filename" : "Top Shelf Image Wide.imageset", + "idiom" : "tv", + "role" : "top-shelf-image-wide", + "size" : "2320x720" + }, + { + "filename" : "Top Shelf Image.imageset", + "idiom" : "tv", + "role" : "top-shelf-image", + "size" : "1920x720" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Passepartout/App/Assets.xcassets/TV.brandassets/Top Shelf Image Wide.imageset/Contents.json b/Passepartout/App/Assets.xcassets/TV.brandassets/Top Shelf Image Wide.imageset/Contents.json new file mode 100644 index 00000000..f5ad0b97 --- /dev/null +++ b/Passepartout/App/Assets.xcassets/TV.brandassets/Top Shelf Image Wide.imageset/Contents.json @@ -0,0 +1,18 @@ +{ + "images" : [ + { + "filename" : "TopShelf.png", + "idiom" : "tv", + "scale" : "1x" + }, + { + "filename" : "TopShelf@2x.png", + "idiom" : "tv", + "scale" : "2x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Passepartout/App/Assets.xcassets/TV.brandassets/Top Shelf Image Wide.imageset/TopShelf.png b/Passepartout/App/Assets.xcassets/TV.brandassets/Top Shelf Image Wide.imageset/TopShelf.png new file mode 100644 index 00000000..ab60d765 Binary files /dev/null and b/Passepartout/App/Assets.xcassets/TV.brandassets/Top Shelf Image Wide.imageset/TopShelf.png differ diff --git a/Passepartout/App/Assets.xcassets/TV.brandassets/Top Shelf Image Wide.imageset/TopShelf@2x.png b/Passepartout/App/Assets.xcassets/TV.brandassets/Top Shelf Image Wide.imageset/TopShelf@2x.png new file mode 100644 index 00000000..8bb176ba Binary files /dev/null and b/Passepartout/App/Assets.xcassets/TV.brandassets/Top Shelf Image Wide.imageset/TopShelf@2x.png differ diff --git a/Passepartout/App/Assets.xcassets/TV.brandassets/Top Shelf Image.imageset/Contents.json b/Passepartout/App/Assets.xcassets/TV.brandassets/Top Shelf Image.imageset/Contents.json new file mode 100644 index 00000000..f5ad0b97 --- /dev/null +++ b/Passepartout/App/Assets.xcassets/TV.brandassets/Top Shelf Image.imageset/Contents.json @@ -0,0 +1,18 @@ +{ + "images" : [ + { + "filename" : "TopShelf.png", + "idiom" : "tv", + "scale" : "1x" + }, + { + "filename" : "TopShelf@2x.png", + "idiom" : "tv", + "scale" : "2x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Passepartout/App/Assets.xcassets/TV.brandassets/Top Shelf Image.imageset/TopShelf.png b/Passepartout/App/Assets.xcassets/TV.brandassets/Top Shelf Image.imageset/TopShelf.png new file mode 100644 index 00000000..6bf162ae Binary files /dev/null and b/Passepartout/App/Assets.xcassets/TV.brandassets/Top Shelf Image.imageset/TopShelf.png differ diff --git a/Passepartout/App/Assets.xcassets/TV.brandassets/Top Shelf Image.imageset/TopShelf@2x.png b/Passepartout/App/Assets.xcassets/TV.brandassets/Top Shelf Image.imageset/TopShelf@2x.png new file mode 100644 index 00000000..08f4fe6f Binary files /dev/null and b/Passepartout/App/Assets.xcassets/TV.brandassets/Top Shelf Image.imageset/TopShelf@2x.png differ diff --git a/Passepartout/App/Constants/Constants+App.swift b/Passepartout/App/Constants/Constants+App.swift index b44b10fe..b9665a20 100644 --- a/Passepartout/App/Constants/Constants+App.swift +++ b/Passepartout/App/Constants/Constants+App.swift @@ -44,6 +44,8 @@ extension Constants { enum CloudKit { static let containerId: String = bundleConfig("cloudkit_id") + static let sharedContainerId: String = bundleConfig("cloudkit_shared_id") + static let coreDataZone = "com.apple.coredata.cloudkit.zone" } @@ -84,6 +86,8 @@ extension Constants { return [] } #endif + + static let tvLimitedMinutes = 10 } } @@ -131,6 +135,8 @@ extension Constants { enum Persistence { static let profilesContainerName = "Profiles" + static let sharedProfilesContainerName = "SharedProfiles" + static let providersContainerName = "Providers" } diff --git a/Passepartout/App/Constants/Theme.swift b/Passepartout/App/Constants/Theme.swift index 3aed7cc0..c79f4422 100644 --- a/Passepartout/App/Constants/Theme.swift +++ b/Passepartout/App/Constants/Theme.swift @@ -23,7 +23,9 @@ // along with Passepartout. If not, see . // +#if !os(tvOS) import LocalAuthentication +#endif import PassepartoutLibrary import SwiftUI @@ -33,20 +35,28 @@ extension View { } var themeIsiPadPortrait: Bool { + #if !os(tvOS) #if targetEnvironment(macCatalyst) false #else let device: UIDevice = .current return device.userInterfaceIdiom == .pad && device.orientation.isPortrait #endif + #else + false + #endif } var themeIsiPadMultitasking: Bool { + #if !os(tvOS) #if targetEnvironment(macCatalyst) false #else UIDevice.current.userInterfaceIdiom == .pad #endif + #else + false + #endif } } @@ -55,6 +65,7 @@ extension View { extension View { func themeGlobal() -> some View { themeNavigationViewStyle() + #if !os(tvOS) #if !targetEnvironment(macCatalyst) .themeLockScreen() #endif @@ -62,22 +73,39 @@ extension View { .listStyle(themeListStyleValue()) .toggleStyle(themeToggleStyleValue()) .menuStyle(.borderlessButton) + #endif .withErrorHandler() } + #if os(tvOS) + func themeTV() -> some View { + GeometryReader { geo in + self + .padding(.horizontal, 0.25 * geo.size.width) + .scrollClipDisabled() + } + } + #endif + func themePrimaryView() -> some View { #if targetEnvironment(macCatalyst) navigationBarTitleDisplayMode(.inline) .themeSidebarListStyle() - #else + #elseif !os(tvOS) navigationBarTitleDisplayMode(.large) .navigationTitle(Unlocalized.appName) .themeSidebarListStyle() + #else + self #endif } func themeSecondaryView() -> some View { + #if !os(tvOS) navigationBarTitleDisplayMode(.inline) + #else + self + #endif } @ViewBuilder @@ -93,6 +121,7 @@ extension View { @ViewBuilder private func themeSidebarListStyle() -> some View { + #if !os(tvOS) switch themeIdiom { case .phone: listStyle(.insetGrouped) @@ -100,6 +129,9 @@ extension View { default: listStyle(.sidebar) } + #else + self + #endif } @ViewBuilder @@ -108,11 +140,19 @@ extension View { } private func themeListStyleValue() -> some ListStyle { + #if !os(tvOS) .insetGrouped + #else + PlainListStyle() + #endif } private func themeToggleStyleValue() -> some ToggleStyle { + #if !os(tvOS) .switch + #else + DefaultToggleStyle() + #endif } } @@ -179,6 +219,10 @@ extension View { "eye" } + var themeAppleTVImage: String { + "tv" + } + // MARK: Organizer func themeAssetsProviderImage(_ providerName: ProviderName) -> String { @@ -387,6 +431,7 @@ extension View { // MARK: Shortcuts +#if !os(tvOS) extension ShortcutType { var themeImageName: String { switch self { @@ -401,6 +446,7 @@ extension ShortcutType { } } } +#endif // MARK: Animations @@ -497,6 +543,7 @@ extension View { // MARK: Lock screen +#if !os(tvOS) extension View { func themeLockScreen() -> some View { @AppStorage(AppPreference.locksInBackground.key) var locksInBackground = false @@ -528,6 +575,7 @@ extension View { } } } +#endif // MARK: Validation diff --git a/Passepartout/App/Context/AppContext.swift b/Passepartout/App/Context/AppContext.swift index 5ffaf14a..cdd85702 100644 --- a/Passepartout/App/Context/AppContext.swift +++ b/Passepartout/App/Context/AppContext.swift @@ -66,6 +66,7 @@ final class AppContext { persistenceManager = PersistenceManager( store: store, ckContainerId: Constants.CloudKit.containerId, + ckSharedContainerId: Constants.CloudKit.sharedContainerId, ckCoreDataZone: Constants.CloudKit.coreDataZone ) @@ -94,6 +95,10 @@ final class AppContext { private extension AppContext { func configureObjects() { + coreContext.profileManager.willSaveSharedProfile = { [unowned self] in + willSaveSharedProfile(withNewProfile: $0, existingProfile: $1) + } + coreContext.vpnManager.isOnDemandRulesSupported = { self.isEligibleForOnDemandRules() } @@ -101,6 +106,13 @@ private extension AppContext { self.isEligibleForNetworkSettings() } + coreContext.vpnManager.userData = { + if let expirationDate = $0.connectionExpirationDate { + return [Constants.Tunnel.expirationTimeIntervalKey: expirationDate.timeIntervalSinceReferenceDate] + } + return nil + } + coreContext.vpnManager.currentState.$vpnStatus .removeDuplicates() .receive(on: DispatchQueue.main) @@ -138,4 +150,41 @@ private extension AppContext { } return true } + + // eligibility: expire restricted TV profiles after N minutes + func willSaveSharedProfile(withNewProfile newProfile: Profile, existingProfile: Profile?) -> Profile { + if let existingProfile { + assert(newProfile.id == existingProfile.id) + } + + guard productManager.isEligible(forFeature: .appleTV) else { + var restricted = newProfile + let remainingMinutes: Int + let expirationDate: Date + + // retain current expiration period if any + if let currentExpirationDate = existingProfile?.connectionExpirationDate { + remainingMinutes = Int(currentExpirationDate.timeIntervalSinceNow / 60.0) + expirationDate = currentExpirationDate + } + // otherwise, expire in N minutes from now + else { + remainingMinutes = Constants.InApp.tvLimitedMinutes + expirationDate = Date() + .addingTimeInterval(TimeInterval(remainingMinutes) * 60.0) + + restricted.connectionExpirationDate = expirationDate + } + + if remainingMinutes > 0 { + pp_log.warning("\(newProfile.logDescription): TV connection expires in \(remainingMinutes) minutes (at \(expirationDate))") + } else { + pp_log.warning("\(newProfile.logDescription): TV connection expired at \(expirationDate)") + } + + return restricted + } + + return newProfile + } } diff --git a/Passepartout/App/Context/CoreContext.swift b/Passepartout/App/Context/CoreContext.swift index 94fbe79c..63f16d0b 100644 --- a/Passepartout/App/Context/CoreContext.swift +++ b/Passepartout/App/Context/CoreContext.swift @@ -44,9 +44,14 @@ final class CoreContext { init(persistenceManager: PersistenceManager) { store = persistenceManager.store + #if !os(tvOS) let vpnPersistence = persistenceManager.loadVPNPersistence( withName: Constants.Persistence.profilesContainerName ) + #endif + let sharedVPNPersistence = persistenceManager.loadSharedVPNPersistence( + withName: Constants.Persistence.sharedProfilesContainerName + ) let providersPersistence = persistenceManager.loadProvidersPersistence( withName: Constants.Persistence.providersContainerName ) @@ -68,10 +73,20 @@ final class CoreContext { remoteProvidersStrategy: remoteProvidersStrategy ) + let tvProfileRepository = sharedVPNPersistence.profileRepository() + #if !os(tvOS) + let profileRepository = vpnPersistence.profileRepository() + let sharedProfileRepository = tvProfileRepository + #else + let profileRepository = tvProfileRepository + let sharedProfileRepository: ProfileRepository? = nil + #endif + profileManager = ProfileManager( store: store, providerManager: providerManager, - profileRepository: vpnPersistence.profileRepository(), + profileRepository: profileRepository, + sharedProfileRepository: sharedProfileRepository, keychain: KeychainSecretRepository(appGroup: Constants.App.appGroupId), keychainEntry: Unlocalized.Keychain.passwordEntry, keychainLabel: Unlocalized.Keychain.passwordLabel diff --git a/Passepartout/App/Context/MockProfileRepository.swift b/Passepartout/App/Context/MockProfileRepository.swift new file mode 100644 index 00000000..b379faed --- /dev/null +++ b/Passepartout/App/Context/MockProfileRepository.swift @@ -0,0 +1,60 @@ +// +// MockProfileRepository.swift +// Passepartout +// +// Created by Davide De Rosa on 12/18/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 . +// + +import Combine +import Foundation +import PassepartoutLibrary + +final class MockProfileRepository: ProfileRepository, ObservableObject { + @Published var profiles: [UUID: Profile] + + init() { + profiles = [:] + } + + func allProfiles() -> [UUID: Profile] { + profiles + } + + func profile(withId id: UUID) -> Profile? { + profiles[id] + } + + func saveProfiles(_ profiles: [Profile]) throws { + profiles.forEach { + self.profiles[$0.id] = $0 + } + } + + func removeProfiles(withIds ids: [UUID]) { + ids.forEach { + profiles.removeValue(forKey: $0) + } + } + + func willUpdateProfiles() -> AnyPublisher<[UUID: Profile], Never> { + $profiles.eraseToAnyPublisher() + } +} diff --git a/Passepartout/App/Domain/AppError.swift b/Passepartout/App/Domain/AppError.swift index 809561d3..a19de46e 100644 --- a/Passepartout/App/Domain/AppError.swift +++ b/Passepartout/App/Domain/AppError.swift @@ -33,6 +33,8 @@ enum AppError: Error { case vpn(Passepartout.VPNError) + case tunnel(TunnelError) + case generic(Error) init(_ error: Error) { diff --git a/Passepartout/App/Domain/IntentDispatcher+Activities.swift b/Passepartout/App/Domain/IntentDispatcher+Activities.swift index c04f80d2..cf653e98 100644 --- a/Passepartout/App/Domain/IntentDispatcher+Activities.swift +++ b/Passepartout/App/Domain/IntentDispatcher+Activities.swift @@ -23,6 +23,7 @@ // along with Passepartout. If not, see . // +#if !os(tvOS) import Foundation import Intents import PassepartoutLibrary @@ -157,3 +158,4 @@ extension IntentDispatcher { } } } +#endif diff --git a/Passepartout/App/Domain/IntentDispatcher.swift b/Passepartout/App/Domain/IntentDispatcher.swift index b470e859..2f662015 100644 --- a/Passepartout/App/Domain/IntentDispatcher.swift +++ b/Passepartout/App/Domain/IntentDispatcher.swift @@ -23,6 +23,7 @@ // along with Passepartout. If not, see . // +#if !os(tvOS) import Foundation import Intents import PassepartoutLibrary @@ -157,3 +158,4 @@ private extension INInteraction { } } } +#endif diff --git a/Passepartout/App/Info.plist b/Passepartout/App/Info.plist index 2e835550..c82104da 100644 --- a/Passepartout/App/Info.plist +++ b/Passepartout/App/Info.plist @@ -24,7 +24,7 @@ CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier - $(CFG_APP_ID) + $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName @@ -80,7 +80,7 @@ LaunchScreen UIRequiredDeviceCapabilities - armv7 + arm64 UISupportedInterfaceOrientations @@ -99,10 +99,12 @@ appstore_id $(CFG_APPSTORE_ID) - group_id - group.$(CFG_GROUP_ID) cloudkit_id iCloud.$(CFG_GROUP_ID) + cloudkit_shared_id + iCloud.$(CFG_GROUP_ID).Shared + group_id + group.$(CFG_GROUP_ID) diff --git a/Passepartout/App/L10n/Errors+L10n.swift b/Passepartout/App/L10n/Errors+L10n.swift index 64c8722c..ae88e2c0 100644 --- a/Passepartout/App/L10n/Errors+L10n.swift +++ b/Passepartout/App/L10n/Errors+L10n.swift @@ -77,6 +77,12 @@ extension AppError: LocalizedError { return nil } + case .tunnel(let tunnelError): + switch tunnelError { + case .expired: + return V.tunnelExpired + } + case .generic(let error): return error.localizedDescription } diff --git a/Passepartout/App/L10n/Unlocalized.swift b/Passepartout/App/L10n/Unlocalized.swift index 9f9fe7ff..3ea97f16 100644 --- a/Passepartout/App/L10n/Unlocalized.swift +++ b/Passepartout/App/L10n/Unlocalized.swift @@ -259,6 +259,8 @@ enum Unlocalized { static let iCloud = "iCloud" + static let appleTV = "Apple TV" + static let totp = "TOTP" } } diff --git a/Passepartout/App/Managers/IntentsManager.swift b/Passepartout/App/Managers/IntentsManager.swift index c1ded1af..eb252ed0 100644 --- a/Passepartout/App/Managers/IntentsManager.swift +++ b/Passepartout/App/Managers/IntentsManager.swift @@ -23,6 +23,7 @@ // along with Passepartout. If not, see . // +#if !os(tvOS) import Combine import Foundation @preconcurrency import Intents @@ -106,3 +107,4 @@ extension IntentsManager: INUIEditVoiceShortcutViewControllerDelegate { shouldDismissIntentView.send() } } +#endif diff --git a/Passepartout/App/Managers/PersistenceManager.swift b/Passepartout/App/Managers/PersistenceManager.swift index e4d2786b..5ac7c509 100644 --- a/Passepartout/App/Managers/PersistenceManager.swift +++ b/Passepartout/App/Managers/PersistenceManager.swift @@ -35,10 +35,14 @@ final class PersistenceManager: ObservableObject { private let ckContainerId: String + private let ckSharedContainerId: String + private let ckCoreDataZone: String private var vpnPersistence: VPNPersistence? + private var sharedVPNPersistence: VPNPersistence? + private var providersPersistence: ProvidersPersistence? private(set) var isCloudSyncingEnabled: Bool { @@ -52,9 +56,13 @@ final class PersistenceManager: ObservableObject { let didChangePersistence = PassthroughSubject() - init(store: KeyValueStore, ckContainerId: String, ckCoreDataZone: String) { + init(store: KeyValueStore, + ckContainerId: String, + ckSharedContainerId: String, + ckCoreDataZone: String) { self.store = store self.ckContainerId = ckContainerId + self.ckSharedContainerId = ckSharedContainerId self.ckCoreDataZone = ckCoreDataZone isCloudSyncingEnabled = store.canEnableCloudSyncing @@ -65,11 +73,23 @@ final class PersistenceManager: ObservableObject { } func loadVPNPersistence(withName containerName: String) -> VPNPersistence { - let persistence = VPNPersistence(withName: containerName, cloudKit: isCloudSyncingEnabled, author: persistenceAuthor) + let persistence = VPNPersistence(withName: containerName, + cloudKit: isCloudSyncingEnabled, + cloudKitIdentifier: nil, + author: persistenceAuthor) vpnPersistence = persistence return persistence } + func loadSharedVPNPersistence(withName containerName: String) -> VPNPersistence { + let persistence = VPNPersistence(withName: containerName, + cloudKit: true, + cloudKitIdentifier: ckSharedContainerId, + author: persistenceAuthor) + sharedVPNPersistence = persistence + return persistence + } + func loadProvidersPersistence(withName containerName: String) -> ProvidersPersistence { let persistence = ProvidersPersistence(withName: containerName, cloudKit: false, author: persistenceAuthor) providersPersistence = persistence @@ -86,6 +106,10 @@ extension PersistenceManager { fromContainerWithId: ckContainerId, zoneId: .init(zoneName: ckCoreDataZone) ) + await Self.eraseCloudKitStore( + fromContainerWithId: ckSharedContainerId, + zoneId: .init(zoneName: ckCoreDataZone) + ) isErasingCloudKitStore = false } @@ -109,7 +133,11 @@ private extension KeyValueStore { } private var isCloudKitSupported: Bool { + #if !os(tvOS) cloudKitToken != nil + #else + true + #endif } var canEnableCloudSyncing: Bool { diff --git a/Passepartout/App/PassepartoutApp.swift b/Passepartout/App/PassepartoutApp.swift index 77ed7b10..9f8bbbc3 100644 --- a/Passepartout/App/PassepartoutApp.swift +++ b/Passepartout/App/PassepartoutApp.swift @@ -33,6 +33,7 @@ struct PassepartoutApp: App { @SceneBuilder var body: some Scene { WindowGroup { MainView() + #if !os(tvOS) .withoutTitleBar() .onIntentActivity(IntentDispatcher.connectVPN) .onIntentActivity(IntentDispatcher.disableVPN) @@ -42,6 +43,7 @@ struct PassepartoutApp: App { .onIntentActivity(IntentDispatcher.trustCurrentNetwork) .onIntentActivity(IntentDispatcher.untrustCellularNetwork) .onIntentActivity(IntentDispatcher.untrustCurrentNetwork) + #endif } } } diff --git a/Passepartout/App/Reusable/ActivityView.swift b/Passepartout/App/Reusable/ActivityView.swift index e6531c25..73e6fd3a 100644 --- a/Passepartout/App/Reusable/ActivityView.swift +++ b/Passepartout/App/Reusable/ActivityView.swift @@ -23,6 +23,7 @@ // along with Passepartout. If not, see . // +#if !os(tvOS) import SwiftUI import UIKit @@ -38,3 +39,4 @@ struct ActivityView: UIViewControllerRepresentable { func updateUIViewController(_ uiViewController: UIActivityViewController, context: UIViewControllerRepresentableContext) { } } +#endif diff --git a/Passepartout/App/Reusable/GenericCreditsView.swift b/Passepartout/App/Reusable/GenericCreditsView.swift index 23622f64..cbf162e9 100644 --- a/Passepartout/App/Reusable/GenericCreditsView.swift +++ b/Passepartout/App/Reusable/GenericCreditsView.swift @@ -180,7 +180,9 @@ private extension GenericCreditsView { .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) .padding() }.navigationTitle(content.name) + #if !os(tvOS) .navigationBarTitleDisplayMode(.inline) + #endif } } diff --git a/Passepartout/App/Reusable/IntentAddView.swift b/Passepartout/App/Reusable/IntentAddView.swift index e4765c85..b78bc908 100644 --- a/Passepartout/App/Reusable/IntentAddView.swift +++ b/Passepartout/App/Reusable/IntentAddView.swift @@ -23,6 +23,7 @@ // along with Passepartout. If not, see . // +#if !os(tvOS) import Intents import IntentsUI import SwiftUI @@ -41,3 +42,4 @@ struct IntentAddView: UIViewControllerRepresentable { func updateUIViewController(_ uiViewController: INUIAddVoiceShortcutViewController, context: UIViewControllerRepresentableContext) { } } +#endif diff --git a/Passepartout/App/Reusable/IntentEditView.swift b/Passepartout/App/Reusable/IntentEditView.swift index 94a03b79..1cc5dc0a 100644 --- a/Passepartout/App/Reusable/IntentEditView.swift +++ b/Passepartout/App/Reusable/IntentEditView.swift @@ -23,6 +23,7 @@ // along with Passepartout. If not, see . // +#if !os(tvOS) import Intents import IntentsUI import SwiftUI @@ -41,3 +42,4 @@ struct IntentEditView: UIViewControllerRepresentable { func updateUIViewController(_ uiViewController: INUIEditVoiceShortcutViewController, context: UIViewControllerRepresentableContext) { } } +#endif diff --git a/Passepartout/App/Reusable/LongContentView.swift b/Passepartout/App/Reusable/LongContentView.swift index 6b337a82..86dbb536 100644 --- a/Passepartout/App/Reusable/LongContentView.swift +++ b/Passepartout/App/Reusable/LongContentView.swift @@ -29,9 +29,15 @@ struct LongContentView: View { @Binding var content: String var body: some View { + #if !os(tvOS) TextEditor(text: $content) .font(.system(.body, design: .monospaced)) // .padding() + #else + Text(content) + .font(.system(.body, design: .monospaced)) +// .padding() + #endif // TODO: layout, add padding an inset, let content extend beyond safe areas } diff --git a/Passepartout/App/Reusable/MailComposerView.swift b/Passepartout/App/Reusable/MailComposerView.swift index df330b3c..5b74b71d 100644 --- a/Passepartout/App/Reusable/MailComposerView.swift +++ b/Passepartout/App/Reusable/MailComposerView.swift @@ -23,6 +23,7 @@ // along with Passepartout. If not, see . // +#if !os(tvOS) import MessageUI import SwiftUI @@ -82,3 +83,4 @@ extension MailComposerView { } } } +#endif diff --git a/Passepartout/App/Reusable/Reviewer.swift b/Passepartout/App/Reusable/Reviewer.swift index 2d70013c..ca5ba8b5 100644 --- a/Passepartout/App/Reusable/Reviewer.swift +++ b/Passepartout/App/Reusable/Reviewer.swift @@ -75,17 +75,21 @@ public final class Reviewer: ObservableObject { defaults.removeObject(forKey: Keys.eventCount) defaults.set(currentVersion, forKey: Keys.lastVersion) + #if !os(tvOS) requestReview() + #endif return true } // may or may not appear + #if !os(tvOS) private func requestReview() { guard let scene = UIApplication.shared.connectedScenes.first(where: { $0 is UIWindowScene }) as? UIWindowScene else { return } SKStoreReviewController.requestReview(in: scene) } + #endif public static func urlForReview(withAppId appId: String) -> URL { URL(string: "https://apps.apple.com/app/id\(appId)?action=write-review")! diff --git a/Passepartout/App/Reusable/Shortcut.swift b/Passepartout/App/Reusable/Shortcut.swift index 813b88ae..7cbe31ea 100644 --- a/Passepartout/App/Reusable/Shortcut.swift +++ b/Passepartout/App/Reusable/Shortcut.swift @@ -23,6 +23,7 @@ // along with Passepartout. If not, see . // +#if !os(tvOS) import Foundation import Intents @@ -53,3 +54,4 @@ struct Shortcut: Identifiable, Hashable, Comparable { native.invocationPhrase.lowercased() } } +#endif diff --git a/Passepartout/App/SceneDelegate+Shortcuts.swift b/Passepartout/App/SceneDelegate+Shortcuts.swift index 40424266..6338ac40 100644 --- a/Passepartout/App/SceneDelegate+Shortcuts.swift +++ b/Passepartout/App/SceneDelegate+Shortcuts.swift @@ -23,6 +23,7 @@ // along with Passepartout. If not, see . // +#if !os(tvOS) import PassepartoutLibrary import SwiftUI @@ -89,3 +90,4 @@ private extension ShortcutType { ) } } +#endif diff --git a/Passepartout/App/SceneDelegate.swift b/Passepartout/App/SceneDelegate.swift index 29777527..e23ede84 100644 --- a/Passepartout/App/SceneDelegate.swift +++ b/Passepartout/App/SceneDelegate.swift @@ -33,10 +33,14 @@ final class SceneDelegate: UIResponder, UIWindowSceneDelegate { #if targetEnvironment(macCatalyst) MacBundle.shared.utils.sendAppToBackground() #endif + #if !os(tvOS) rebuildShortcutItems() + #endif } + #if !os(tvOS) func windowScene(_ windowScene: UIWindowScene, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void) { handleShortcutItem(shortcutItem) } + #endif } diff --git a/Passepartout/App/Views/DebugLogView.swift b/Passepartout/App/Views/DebugLogView.swift index 8aa4f69a..8535274d 100644 --- a/Passepartout/App/Views/DebugLogView.swift +++ b/Passepartout/App/Views/DebugLogView.swift @@ -67,6 +67,7 @@ struct DebugLogView: View { refreshLog(scrollingToLatestWith: scrollProxy) } } + #if !os(tvOS) #if targetEnvironment(macCatalyst) .toolbar { Button(action: copyDebugLog) { @@ -82,7 +83,9 @@ struct DebugLogView: View { } else { ProgressView() } - }.sheet(isPresented: $isSharing, content: sharingActivityView) + } + .sheet(isPresented: $isSharing, content: sharingActivityView) + #endif #endif .edgesIgnoringSafeArea([.leading, .trailing]) .onReceive(timer, perform: refreshLog) @@ -104,9 +107,11 @@ private extension DebugLogView { // TODO: layout, a slight padding would be nice, but it glitches on first touch } + #if !os(tvOS) func sharingActivityView() -> some View { ActivityView(activityItems: sharingItems) } + #endif var sharingItems: [Any] { let raw = logLines.joined(separator: "\n") @@ -140,6 +145,7 @@ private extension DebugLogView { } } + #if !os(tvOS) func shareDebugLog() { guard !logLines.isEmpty else { assertionFailure("Log is empty, why could it share?") @@ -159,6 +165,7 @@ private extension DebugLogView { Utils.copyToPasteboard(content) } + #endif func scrollToLatestUpdate(_ proxy: ScrollViewProxy) { proxy.maybeScrollTo(logLines.count - 1, anchor: .bottomLeading) diff --git a/Passepartout/App/Views/DiagnosticsView+OpenVPN.swift b/Passepartout/App/Views/DiagnosticsView+OpenVPN.swift index dab8a77b..194affc9 100644 --- a/Passepartout/App/Views/DiagnosticsView+OpenVPN.swift +++ b/Passepartout/App/Views/DiagnosticsView+OpenVPN.swift @@ -23,6 +23,7 @@ // along with Passepartout. If not, see . // +#if !os(tvOS) import PassepartoutLibrary import SwiftUI import TunnelKitOpenVPN @@ -200,3 +201,4 @@ private extension DiagnosticsView.OpenVPNView { } } } +#endif diff --git a/Passepartout/App/Views/DiagnosticsView.swift b/Passepartout/App/Views/DiagnosticsView.swift index d47b5fb2..cbdc81de 100644 --- a/Passepartout/App/Views/DiagnosticsView.swift +++ b/Passepartout/App/Views/DiagnosticsView.swift @@ -32,7 +32,9 @@ struct DiagnosticsView: View { var body: some View { Group { if !profile.isPlaceholder { + #if !os(tvOS) vpnView + #endif } else { genericView } @@ -41,6 +43,8 @@ struct DiagnosticsView: View { } private extension DiagnosticsView { + + #if !os(tvOS) var vpnView: some View { Group { switch profile.currentVPNProtocol { @@ -56,6 +60,7 @@ private extension DiagnosticsView { } } } + #endif var genericView: some View { List { diff --git a/Passepartout/App/Views/EndpointView+Add.swift b/Passepartout/App/Views/EndpointView+Add.swift index 79b75850..686e7578 100644 --- a/Passepartout/App/Views/EndpointView+Add.swift +++ b/Passepartout/App/Views/EndpointView+Add.swift @@ -23,6 +23,7 @@ // along with Passepartout. If not, see . // +#if !os(tvOS) import PassepartoutLibrary import SwiftUI import TunnelKitCore @@ -111,3 +112,4 @@ private extension EndpointView.AddView { presentationMode.wrappedValue.dismiss() } } +#endif diff --git a/Passepartout/App/Views/EndpointView+OpenVPN.swift b/Passepartout/App/Views/EndpointView+OpenVPN.swift index 27ace603..5c876286 100644 --- a/Passepartout/App/Views/EndpointView+OpenVPN.swift +++ b/Passepartout/App/Views/EndpointView+OpenVPN.swift @@ -23,6 +23,7 @@ // along with Passepartout. If not, see . // +#if !os(tvOS) import PassepartoutLibrary import SwiftUI import TunnelKitOpenVPN @@ -394,3 +395,4 @@ private extension Profile { } } } +#endif diff --git a/Passepartout/App/Views/EndpointView+WireGuard.swift b/Passepartout/App/Views/EndpointView+WireGuard.swift index e67f0f76..1025a580 100644 --- a/Passepartout/App/Views/EndpointView+WireGuard.swift +++ b/Passepartout/App/Views/EndpointView+WireGuard.swift @@ -23,6 +23,7 @@ // along with Passepartout. If not, see . // +#if !os(tvOS) import PassepartoutLibrary import SwiftUI import TunnelKitWireGuard @@ -155,3 +156,4 @@ private extension ObservableProfile { } } } +#endif diff --git a/Passepartout/App/Views/EndpointView.swift b/Passepartout/App/Views/EndpointView.swift index 3e8d95cf..63f0eadb 100644 --- a/Passepartout/App/Views/EndpointView.swift +++ b/Passepartout/App/Views/EndpointView.swift @@ -23,6 +23,7 @@ // along with Passepartout. If not, see . // +#if !os(tvOS) import PassepartoutLibrary import SwiftUI @@ -48,3 +49,4 @@ struct EndpointView: View { } } } +#endif diff --git a/Passepartout/App/Views/MainView.swift b/Passepartout/App/Views/MainView.swift index f2a624f8..1c28aa00 100644 --- a/Passepartout/App/Views/MainView.swift +++ b/Passepartout/App/Views/MainView.swift @@ -23,6 +23,7 @@ // along with Passepartout. If not, see . // +#if !os(tvOS) import SwiftUI struct MainView: View { @@ -33,3 +34,4 @@ struct MainView: View { }.themeGlobal() } } +#endif diff --git a/Passepartout/App/Views/OnDemandView.swift b/Passepartout/App/Views/OnDemandView.swift index 99e9aa93..0adb79b0 100644 --- a/Passepartout/App/Views/OnDemandView.swift +++ b/Passepartout/App/Views/OnDemandView.swift @@ -137,8 +137,10 @@ private extension OnDemandView { guard isEligibleForSiri else { return } + #if !os(tvOS) IntentDispatcher.donateTrustCellularNetwork() IntentDispatcher.donateUntrustCellularNetwork() + #endif } // eligibility: donate intents if eligible for Siri @@ -146,7 +148,9 @@ private extension OnDemandView { guard isEligibleForSiri else { return } + #if !os(tvOS) IntentDispatcher.donateTrustCurrentNetwork() IntentDispatcher.donateUntrustCurrentNetwork() + #endif } } diff --git a/Passepartout/App/Views/OrganizerView+ProfileRow.swift b/Passepartout/App/Views/OrganizerView+ProfileRow.swift index 1d3b14e9..4c01b2e6 100644 --- a/Passepartout/App/Views/OrganizerView+ProfileRow.swift +++ b/Passepartout/App/Views/OrganizerView+ProfileRow.swift @@ -23,6 +23,7 @@ // along with Passepartout. If not, see . // +#if !os(tvOS) import PassepartoutLibrary import SwiftUI @@ -56,6 +57,7 @@ extension OrganizerView { VPNToggle( profile: profile, interactiveProfile: interactiveProfile, + title: L10n.Global.Strings.enabled, rateLimit: Constants.RateLimit.vpnToggle ).labelsHidden() }.padding([.top, .bottom], 10) @@ -81,3 +83,4 @@ private extension OrganizerView.ProfileRow { } } } +#endif diff --git a/Passepartout/App/Views/OrganizerView+Profiles.swift b/Passepartout/App/Views/OrganizerView+Profiles.swift index 169276e8..97e87e51 100644 --- a/Passepartout/App/Views/OrganizerView+Profiles.swift +++ b/Passepartout/App/Views/OrganizerView+Profiles.swift @@ -23,6 +23,7 @@ // along with Passepartout. If not, see . // +#if !os(tvOS) import PassepartoutLibrary import SwiftUI @@ -187,3 +188,4 @@ private extension OrganizerView.ProfilesList { } } } +#endif diff --git a/Passepartout/App/Views/OrganizerView+Scene.swift b/Passepartout/App/Views/OrganizerView+Scene.swift index 79f360e5..f79fcb2d 100644 --- a/Passepartout/App/Views/OrganizerView+Scene.swift +++ b/Passepartout/App/Views/OrganizerView+Scene.swift @@ -23,6 +23,7 @@ // along with Passepartout. If not, see . // +#if !os(tvOS) import PassepartoutLibrary import SwiftUI @@ -84,3 +85,4 @@ private extension OrganizerView.SceneView { } } } +#endif diff --git a/Passepartout/App/Views/OrganizerView.swift b/Passepartout/App/Views/OrganizerView.swift index ad728686..376e97f7 100644 --- a/Passepartout/App/Views/OrganizerView.swift +++ b/Passepartout/App/Views/OrganizerView.swift @@ -23,6 +23,7 @@ // along with Passepartout. If not, see . // +#if !os(tvOS) import PassepartoutLibrary import SwiftUI @@ -89,12 +90,14 @@ struct OrganizerView: View { presenting: alertType, actions: alertActions, message: alertMessage - ).fileImporter( + ) + .fileImporter( isPresented: $isHostFileImporterPresented, allowedContentTypes: hostFileTypes, allowsMultipleSelection: false, onCompletion: onHostFileImporterResult - ).onOpenURL(perform: onOpenURL) + ) + .onOpenURL(perform: onOpenURL) .themePrimaryView() } } @@ -171,3 +174,4 @@ private extension OrganizerView { addProfileModalType = .addHost(url, false) } } +#endif diff --git a/Passepartout/App/Views/PaywallView+Purchase.swift b/Passepartout/App/Views/PaywallView+Purchase.swift index 84a2fdf0..49287a18 100644 --- a/Passepartout/App/Views/PaywallView+Purchase.swift +++ b/Passepartout/App/Views/PaywallView+Purchase.swift @@ -44,6 +44,8 @@ extension PaywallView { @State private var purchaseState: PurchaseState? + @State private var didPurchaseAppleTV = false + init(isPresented: Binding, feature: LocalProduct? = nil) { productManager = .shared _isPresented = isPresented @@ -52,12 +54,22 @@ extension PaywallView { var body: some View { List { - featuresSection + skFullVersion.map { + fullFeaturesSection(withHeader: $0.localizedTitle) + } purchaseSection .disabled(purchaseState != nil) restoreSection .disabled(purchaseState != nil) - }.navigationTitle(Unlocalized.appName) + } + .navigationTitle(Unlocalized.appName) + .alert(Unlocalized.Other.appleTV, isPresented: $didPurchaseAppleTV) { + Button(L10n.Global.Strings.ok) { + isPresented = false + } + } message: { + Text(L10n.Paywall.Alerts.Purchase.Appletv.Success.message) + } // reloading .task { @@ -121,9 +133,9 @@ private struct PurchaseRow: View { // MARK: - private extension PaywallView.PurchaseView { - var featuresSection: some View { + func fullFeaturesSection(withHeader header: String) -> some View { Section { - ForEach(features) { feature in + ForEach(fullFeatures) { feature in VStack(alignment: .leading) { Text(feature.title) .themeCellTitleStyle() @@ -134,6 +146,8 @@ private extension PaywallView.PurchaseView { } } } + } header: { + Text(header) } } @@ -183,6 +197,9 @@ private extension PaywallView.PurchaseView { // hide full version if already bought the other platform version var skFullVersion: InAppProduct? { + guard !productManager.isFullVersion() else { + return nil + } #if targetEnvironment(macCatalyst) guard !productManager.hasPurchased(.fullVersion_iOS) else { return nil @@ -195,17 +212,25 @@ private extension PaywallView.PurchaseView { return productManager.product(withIdentifier: .fullVersion) } - var features: [FeatureModel] { + var skAppleTV: InAppProduct? { + guard feature == .appleTV else { + return nil + } + return productManager.product(withIdentifier: .appleTV) + } + + var fullFeatures: [FeatureModel] { productManager.featureProducts(excluding: { - $0 == .fullVersion || $0.isPlatformVersion + $0 == .fullVersion || $0 == .appleTV || $0.isLegacyPlatformVersion }) .map { FeatureModel(product: $0) } + .sorted() } var productRowModels: [InAppProduct] { - [skFullVersion] + [skFullVersion, skAppleTV] .compactMap { $0 } } } @@ -249,16 +274,25 @@ private extension PurchaseRow { // MARK: - +// IMPORTANT: resync shared profiles after purchasing Apple TV feature (drop time limit) + private extension PaywallView.PurchaseView { func purchaseProduct(_ product: InAppProduct) { purchaseState = .purchasing(product) Task { do { + let wasEligibleForAppleTV = productManager.isEligible(forFeature: .appleTV) let result = try await productManager.purchase(product) + switch result { case .done: - isPresented = false + if !wasEligibleForAppleTV && productManager.isEligible(forFeature: .appleTV) { + ProfileManager.shared.refreshSharedProfiles() + didPurchaseAppleTV = true + } else { + isPresented = false + } case .cancelled: break @@ -281,8 +315,16 @@ private extension PaywallView.PurchaseView { Task { do { + let wasEligibleForAppleTV = productManager.isEligible(forFeature: .appleTV) try await productManager.restorePurchases() - isPresented = false + + if !wasEligibleForAppleTV && productManager.isEligible(forFeature: .appleTV) { + ProfileManager.shared.refreshSharedProfiles() + didPurchaseAppleTV = true + } else { + isPresented = false + } + purchaseState = nil } catch { pp_log.error("Unable to restore purchases: \(error)") diff --git a/Passepartout/App/Views/ProfileView+Configuration.swift b/Passepartout/App/Views/ProfileView+Configuration.swift index e562e068..3c800faf 100644 --- a/Passepartout/App/Views/ProfileView+Configuration.swift +++ b/Passepartout/App/Views/ProfileView+Configuration.swift @@ -53,11 +53,13 @@ extension ProfileView { Label(L10n.Global.Strings.protocol, systemImage: themeVPNProtocolImage) .withTrailingText(currentProfile.value.currentVPNProtocol.description) } + #if !os(tvOS) NavigationLink { EndpointView(currentProfile: currentProfile) } label: { Label(L10n.Global.Strings.endpoint, systemImage: themeEndpointImage) } + #endif if currentProfile.value.requiresCredentials { NavigationLink { AccountView( diff --git a/Passepartout/App/Views/ProfileView+MainMenu.swift b/Passepartout/App/Views/ProfileView+MainMenu.swift index 622d8e7d..57b4a777 100644 --- a/Passepartout/App/Views/ProfileView+MainMenu.swift +++ b/Passepartout/App/Views/ProfileView+MainMenu.swift @@ -96,6 +96,7 @@ extension ProfileView { } } + #if !os(tvOS) struct ShortcutsButton: View { @ObservedObject private var productManager: ProductManager @@ -114,6 +115,7 @@ extension ProfileView { } } } + #endif struct RenameButton: View { @Binding private var modalType: ModalType? @@ -164,9 +166,11 @@ private extension ProfileView.MainMenu { var mainView: some View { Menu { ProfileView.ReconnectButton() + #if !os(tvOS) ProfileView.ShortcutsButton( modalType: $modalType ) + #endif Divider() ProfileView.RenameButton( modalType: $modalType @@ -238,11 +242,13 @@ private extension ProfileView.MainMenu { } } +#if !os(tvOS) private extension ProfileView.ShortcutsButton { var isEligibleForSiri: Bool { productManager.isEligible(forFeature: .siriShortcuts) } } +#endif // MARK: - @@ -260,6 +266,7 @@ private extension ProfileView.MainMenu { } } +#if !os(tvOS) private extension ProfileView.ShortcutsButton { func presentShortcutsOrPaywall() { @@ -271,6 +278,7 @@ private extension ProfileView.ShortcutsButton { } } } +#endif private extension ProfileView.DuplicateButton { func duplicateProfile(withId id: UUID) { diff --git a/Passepartout/App/Views/ProfileView+TV.swift b/Passepartout/App/Views/ProfileView+TV.swift new file mode 100644 index 00000000..bc107c0d --- /dev/null +++ b/Passepartout/App/Views/ProfileView+TV.swift @@ -0,0 +1,95 @@ +// +// ProfileView+TV.swift +// Passepartout +// +// Created by Davide De Rosa on 12/22/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 . +// + +import PassepartoutLibrary +import SwiftUI + +extension ProfileView { + struct TVSection: View { + @ObservedObject private var profileManager: ProfileManager + + @ObservedObject private var productManager: ProductManager + + @ObservedObject private var currentProfile: ObservableProfile + + @Binding private var isProfileShared: Bool + + @Binding private var modalType: ModalType? + + init(currentProfile: ObservableProfile, modalType: Binding) { + let profileManager: ProfileManager = .shared + + self.profileManager = profileManager + productManager = .shared + self.currentProfile = currentProfile + _isProfileShared = Binding { + profileManager.isSharing(profile: currentProfile.value) + } set: { + profileManager.setSharing($0, profile: currentProfile.value) + } + _modalType = modalType + } + + var body: some View { + Section { + Toggle(isOn: $isProfileShared) { + Label(shareText, systemImage: themeAppleTVImage) + } + + // eligibility: present paywall for full support for Apple TV + if !isEligibleForAppleTV { + Button(L10n.Paywall.title) { + modalType = .paywallAppleTV + } + } + } footer: { + Text(footerText) + } + } + } +} + +private extension ProfileView.TVSection { + var isEligibleForAppleTV: Bool { + productManager.isEligible(forFeature: .appleTV) + } + + var shareText: String { + var sentences: [String] = [Unlocalized.Other.appleTV] + if !isEligibleForAppleTV { + sentences.append(L10n.Profile.Items.TvSharing.Caption.limited(Constants.InApp.tvLimitedMinutes)) + } + return sentences.joined(separator: " — ") + } + + var footerText: String { + var sentences: [String] = [L10n.Profile.Sections.Tv.Footer.encryption] + if !isEligibleForAppleTV { + sentences.append(L10n.Profile.Sections.Tv.Footer.Restricted.p1(Constants.InApp.tvLimitedMinutes)) + sentences.append(L10n.Profile.Sections.Tv.Footer.Restricted.p2) + } + return sentences.joined(separator: " ") + } +} diff --git a/Passepartout/App/Views/ProfileView+VPN.swift b/Passepartout/App/Views/ProfileView+VPN.swift index 63ce5714..01f3745d 100644 --- a/Passepartout/App/Views/ProfileView+VPN.swift +++ b/Passepartout/App/Views/ProfileView+VPN.swift @@ -73,6 +73,7 @@ private extension ProfileView.VPNSection { VPNToggle( profile: profile, interactiveProfile: interactiveProfile, + title: L10n.Global.Strings.enabled, rateLimit: Constants.RateLimit.vpnToggle ) } diff --git a/Passepartout/App/Views/ProfileView.swift b/Passepartout/App/Views/ProfileView.swift index ba6e60ee..984e3c20 100644 --- a/Passepartout/App/Views/ProfileView.swift +++ b/Passepartout/App/Views/ProfileView.swift @@ -30,7 +30,9 @@ struct ProfileView: View { enum ModalType: Int, Identifiable { case interactiveAccount + #if !os(tvOS) case shortcuts + #endif case rename @@ -40,6 +42,8 @@ struct ProfileView: View { case paywallTrustedNetworks + case paywallAppleTV + var id: Int { rawValue } @@ -104,6 +108,10 @@ private extension ProfileView { currentProfile: currentProfile, modalType: $modalType ) + TVSection( + currentProfile: currentProfile, + modalType: $modalType + ) ExtraSection(currentProfile: currentProfile) Section { DiagnosticsRow(currentProfile: currentProfile) @@ -122,10 +130,12 @@ private extension ProfileView { InteractiveConnectionView(profile: currentProfile.value) }.themeGlobal() + #if !os(tvOS) case .shortcuts: NavigationView { ShortcutsView(target: currentProfile.value) }.themeGlobal() + #endif case .rename: NavigationView { @@ -155,6 +165,14 @@ private extension ProfileView { feature: .trustedNetworks ) }.themeGlobal() + + case .paywallAppleTV: + NavigationView { + PaywallView( + modalType: $modalType, + feature: .appleTV + ) + }.themeGlobal() } } } diff --git a/Passepartout/App/Views/ProviderLocationView.swift b/Passepartout/App/Views/ProviderLocationView.swift index 2cfe072c..15f13bac 100644 --- a/Passepartout/App/Views/ProviderLocationView.swift +++ b/Passepartout/App/Views/ProviderLocationView.swift @@ -168,9 +168,11 @@ private extension ProviderLocationView { ForEach(filteredLocations(for: category)) { location in if isEditable { locationRow(location) + #if !os(tvOS) .swipeActions(edge: .trailing, allowsFullSwipe: false) { favoriteActions(location) } + #endif } else { locationRow(location) } diff --git a/Passepartout/App/Views/ReportIssueView.swift b/Passepartout/App/Views/ReportIssueView.swift index 0d7aa40c..2af78a1b 100644 --- a/Passepartout/App/Views/ReportIssueView.swift +++ b/Passepartout/App/Views/ReportIssueView.swift @@ -23,6 +23,7 @@ // along with Passepartout. If not, see . // +#if !os(tvOS) import MessageUI import PassepartoutLibrary import SwiftUI @@ -88,3 +89,4 @@ struct ReportIssueView: View { ) } } +#endif diff --git a/Passepartout/App/Views/ShortcutsView+Add.swift b/Passepartout/App/Views/ShortcutsView+Add.swift index 4d3a8624..5c91d445 100644 --- a/Passepartout/App/Views/ShortcutsView+Add.swift +++ b/Passepartout/App/Views/ShortcutsView+Add.swift @@ -23,6 +23,7 @@ // along with Passepartout. If not, see . // +#if !os(tvOS) import Intents import PassepartoutLibrary import SwiftUI @@ -162,3 +163,4 @@ private extension ShortcutsView.AddView { pendingShortcut = shortcut } } +#endif diff --git a/Passepartout/App/Views/ShortcutsView.swift b/Passepartout/App/Views/ShortcutsView.swift index c2d60937..daf75884 100644 --- a/Passepartout/App/Views/ShortcutsView.swift +++ b/Passepartout/App/Views/ShortcutsView.swift @@ -23,6 +23,7 @@ // along with Passepartout. If not, see . // +#if !os(tvOS) import Intents import PassepartoutLibrary import SwiftUI @@ -176,3 +177,4 @@ private extension ShortcutsView { modalType = .add(shortcut: shortcut) } } +#endif diff --git a/Passepartout/App/Views/TV/ActiveProfileView+TV.swift b/Passepartout/App/Views/TV/ActiveProfileView+TV.swift new file mode 100644 index 00000000..363d930d --- /dev/null +++ b/Passepartout/App/Views/TV/ActiveProfileView+TV.swift @@ -0,0 +1,118 @@ +// +// ActiveProfileView+TV.swift +// Passepartout +// +// Created by Davide De Rosa on 12/18/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 . +// + +#if os(tvOS) +import PassepartoutLibrary +import SwiftUI + +struct ActiveProfileView: View { + @ObservedObject private var profileManager: ProfileManager + + @ObservedObject private var vpnState: ObservableVPNState + + init(profileManager: ProfileManager) { + self.profileManager = profileManager + vpnState = .shared + } + + var body: some View { + Section { + let activeProfile = profileManager.activeProfile + nameView(for: activeProfile) + if let activeProfile { + toggleView(for: activeProfile) + vpnProtocolView(for: activeProfile) + statusView(for: activeProfile) + if let expirationDate = activeProfile.connectionExpirationDate { + expirationView(at: expirationDate) + } + } + } + if let activeProfile = profileManager.activeProfile, + let server = activeProfile.providerServer(.shared) { + providerSection(with: server) + } + } +} + +private extension ActiveProfileView { + func nameView(for profile: Profile?) -> some View { + NavigationLink { + ProfilesListView(profileManager: profileManager) + } label: { + Text(L10n.Global.Placeholders.profileName) + .withTrailingText(profile?.header.name) + } + } + + func vpnProtocolView(for profile: Profile) -> some View { + Text(L10n.Global.Strings.protocol) + .withTrailingText(profile.currentVPNProtocol.description) + } + + func toggleView(for profile: Profile) -> some View { + VPNToggle( + profile: profile, + interactiveProfile: .constant(nil), + title: L10n.Profile.Items.ConnectionStatus.caption, + rateLimit: Constants.RateLimit.vpnToggle + ) + } + + func statusView(for activeProfile: Profile) -> some View { + HStack { + Text(Unlocalized.VPN.vpn) + Spacer() + if vpnState.isEnabled && activeProfile.isExpired { + Text(L10n.Global.Errors.tunnelExpired) + .themeSecondaryTextStyle() + } else { + VPNStatusText(isActiveProfile: true) + .themeSecondaryTextStyle() + } + } + } + + func expirationView(at expirationDate: Date) -> some View { + Text(L10n.Profile.Items.ExpiresAt.caption) + .withTrailingText(expirationDate.timestamp) + } + + func providerSection(with server: ProviderServer) -> some View { + Section { + Text(L10n.Global.Strings.name) + .withTrailingText(server.providerMetadata.fullName) + HStack { + Text(L10n.Provider.Location.title) + Spacer() + Label(server.localizedDescription(style: .country), image: themeAssetsCountryImage(server.countryCode)) + .themeSecondaryTextStyle() + } + } header: { + Text(L10n.Global.Strings.provider) + } + } +} +#endif diff --git a/Passepartout/App/Views/TV/MainView+TV.swift b/Passepartout/App/Views/TV/MainView+TV.swift new file mode 100644 index 00000000..a5fece81 --- /dev/null +++ b/Passepartout/App/Views/TV/MainView+TV.swift @@ -0,0 +1,43 @@ +// +// MainView+TV.swift +// Passepartout +// +// Created by Davide De Rosa on 12/17/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 . +// + +#if os(tvOS) +import SwiftUI + +struct MainView: View { + @State private var path = NavigationPath() + + var body: some View { + NavigationStack(path: $path) { + OrganizerView() + } + .themeGlobal() + } +} + +#Preview { + MainView() +} +#endif diff --git a/Passepartout/App/Views/TV/OrganizerView+TV.swift b/Passepartout/App/Views/TV/OrganizerView+TV.swift new file mode 100644 index 00000000..3fb4efe2 --- /dev/null +++ b/Passepartout/App/Views/TV/OrganizerView+TV.swift @@ -0,0 +1,163 @@ +// +// OrganizerView+TV.swift +// Passepartout +// +// Created by Davide De Rosa on 12/17/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 . +// + +#if os(tvOS) +import PassepartoutLibrary +import SwiftUI + +struct OrganizerView: View { + @ObservedObject private var profileManager: ProfileManager + +#if targetEnvironment(simulator) + @State private var didLoadMockProfiles = false +#endif + + init(profileManager: ProfileManager = .shared) { + self.profileManager = profileManager + } + + var body: some View { + List { + ActiveProfileView(profileManager: profileManager) + aboutSection + } + .navigationTitle(Unlocalized.appName) + .themeTV() + .themeAnimation(on: profileManager.activeProfileId) +#if targetEnvironment(simulator) + .task { + await loadMockProfiles() + } +#endif + } +} + +private extension OrganizerView { + var aboutSection: some View { + Section { + Text(L10n.Version.title) + .withTrailingText(Constants.Global.appVersionString) + } header: { + Text(L10n.About.title) + } + } +} + +// MARK: Mock + +#if targetEnvironment(simulator) +import TunnelKitOpenVPN +import TunnelKitWireGuard + +// poor man's preview: +// +// https://developer.apple.com/forums/thread/719078 + +private let mockHosts: [(String, VPNProtocolType)] = [ + ("My Profile", .wireGuard), + ("Friend's House", .openVPN), + ("At School", .wireGuard) +] + +private let mockProviders: [ProviderName] = [ + .hideme, + .pia, + .protonvpn +] + +@MainActor +private let mockRepository: ProfileRepository = { + let hostProfiles = mockHosts.map { name, vpnType in + let header = Profile.Header(name: name) + switch vpnType { + case .openVPN: + let ovpn = OpenVPN.ConfigurationBuilder() + return Profile(header, configuration: ovpn.build()) + + case .wireGuard: + let wg = WireGuard.ConfigurationBuilder() + return Profile(header, configuration: wg.build()) + } + } + + let providerProfiles = mockProviders.map { providerName in + let manager = ProviderManager.shared + let metadata = manager.provider(withName: providerName)! + let header = Profile.Header(name: metadata.fullName, providerName: providerName) + var provider = Profile.Provider(providerName) + let vpnType: VPNProtocolType = .openVPN // isOpenVPN ? .openVPN : .wireGuard + var settings = Profile.Provider.Settings() + let anyServer = manager.anyDefaultServer(providerName, vpnProtocol: vpnType) + settings.serverId = anyServer?.id + settings.presetId = anyServer?.presetIds.first + settings.account = .init("hello", "world") + provider.vpnSettings[vpnType] = settings + return Profile(header, provider: provider) + } + + var profiles: [Profile] = [] + profiles.append(contentsOf: hostProfiles) + profiles.append(contentsOf: providerProfiles) + + let repo = MockProfileRepository() + try? repo.saveProfiles(profiles.map { + var copy = $0 + copy.connectionExpirationDate = Date().addingTimeInterval(10.0) + return copy + }) + return repo +}() + +private extension OrganizerView { + func loadMockProfiles() async { + guard !didLoadMockProfiles else { + return + } + do { + let providerManager: ProviderManager = .shared + try await providerManager.fetchProvidersIndexPublisher(priority: .bundle).async() + + for name in mockProviders { + try? await providerManager.fetchProviderPublisher(withName: name, vpnProtocol: .openVPN, priority: .bundle).async() + } + + profileManager.swapProfileRepository(mockRepository) + profileManager.activateProfile(mockRepository.allProfiles().first!.value) + + didLoadMockProfiles = true + } catch { + ErrorHandler.shared.handle(AppError(error)) + } + } +} + +#Preview { + NavigationStack { + OrganizerView() + } +} +#endif + +#endif diff --git a/Passepartout/App/Views/TV/ProfilesList+TV.swift b/Passepartout/App/Views/TV/ProfilesList+TV.swift new file mode 100644 index 00000000..81ff41a9 --- /dev/null +++ b/Passepartout/App/Views/TV/ProfilesList+TV.swift @@ -0,0 +1,113 @@ +// +// ProfilesList+TV.swift +// Passepartout +// +// Created by Davide De Rosa on 12/18/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 . +// + +#if os(tvOS) +import PassepartoutLibrary +import SwiftUI + +struct ProfilesListView: View { + @ObservedObject private var profileManager: ProfileManager + + @Environment(\.presentationMode) private var presentationMode + + @State private var profileIdPendingSelection: UUID? + + @FocusState private var focusedProfileId: UUID? + + init(profileManager: ProfileManager) { + self.profileManager = profileManager + } + + var body: some View { + List { + Section { + Text(listHeader) + .font(.footnote) + .themeSecondaryTextStyle() + ForEach(profiles, content: profileRow) + .themeAnimation(on: profiles) + } + } + .onAppear { + focusedProfileId = profileManager.activeProfileId + } + .disabled(profileIdPendingSelection != nil) + .navigationTitle(Text(L10n.Global.Strings.profiles)) + .themeTV() + } +} + +private extension ProfilesListView { + var profiles: [Profile] { + profileManager.profiles.sorted() + } + + var listHeader: String { + [ + L10n.Organizer.Sections.Tv.ProfilesList.Header.p1, + L10n.Profile.Sections.Tv.Footer.encryption + ] + .joined(separator: " ") + } + + func profileRow(for profile: Profile) -> some View { + Button { + activateProfile(profile) + } label: { + HStack { + Text(profile.header.name) + Spacer() + if profile.header.id == profileIdPendingSelection { + ProgressView() + } else if profileManager.isActiveProfile(profile.header.id) { + themeCheckmarkImage.asSystemImage + } + } + } + .focused($focusedProfileId, equals: profile.id) + } + + func activateProfile(_ profile: Profile) { + guard profile.id != profileManager.activeProfileId else { + presentationMode.wrappedValue.dismiss() + return + } + Task { + profileIdPendingSelection = profile.id + do { + try await profileManager.makeProfileReady(profile) + await VPNManager.shared.disable() + profileManager.activateProfile(profile) + presentationMode.wrappedValue.dismiss() + } catch { + ErrorHandler.shared.handle( + title: L10n.Global.Strings.profiles, + message: AppError(error).localizedDescription + ) + } + } + } +} +#endif diff --git a/Passepartout/App/Views/VPNToggle.swift b/Passepartout/App/Views/VPNToggle.swift index 51756e90..d96bc335 100644 --- a/Passepartout/App/Views/VPNToggle.swift +++ b/Passepartout/App/Views/VPNToggle.swift @@ -39,22 +39,25 @@ struct VPNToggle: View { @Binding private var interactiveProfile: Profile? + private let title: String + private let rateLimit: Int @State private var canToggle = true - init(profile: Profile, interactiveProfile: Binding, rateLimit: Int) { + init(profile: Profile, interactiveProfile: Binding, title: String, rateLimit: Int) { profileManager = .shared vpnManager = .shared currentVPNState = .shared productManager = .shared self.profile = profile _interactiveProfile = interactiveProfile + self.title = title self.rateLimit = rateLimit } var body: some View { - Toggle(L10n.Global.Strings.enabled, isOn: isEnabled) + Toggle(title, isOn: isEnabled) .disabled(!canToggle) .themeAnimation(on: currentVPNState.isEnabled) } @@ -129,11 +132,13 @@ private extension VPNToggle { pp_log.debug("Donating connection intents...") + #if !os(tvOS) IntentDispatcher.donateEnableVPN() IntentDispatcher.donateDisableVPN() IntentDispatcher.donateConnection( with: profile, providerManager: ProviderManager.shared ) + #endif } } diff --git a/Passepartout/App/Views/View+Extensions.swift b/Passepartout/App/Views/View+Extensions.swift index 578e4fc0..fd9993bb 100644 --- a/Passepartout/App/Views/View+Extensions.swift +++ b/Passepartout/App/Views/View+Extensions.swift @@ -71,9 +71,11 @@ extension View { .truncationMode(truncationMode) if copyOnTap { trailing + #if !os(tvOS) .onTapGesture { text.map(Utils.copyToPasteboard) } + #endif } else { trailing } diff --git a/Passepartout/App/de.lproj/Localizable.strings b/Passepartout/App/de.lproj/Localizable.strings index c808f508..3bc2ced0 100644 --- a/Passepartout/App/de.lproj/Localizable.strings +++ b/Passepartout/App/de.lproj/Localizable.strings @@ -72,7 +72,7 @@ "global.strings.authentication" = "Authentifizierung"; "global.messages.unlock_app" = "Passepartout ist gesperrt"; "global.messages.email_not_configured" = "Es wurde kein Email-Account konfiguriert."; -"global.messages.share" = "Passepartout ist ein Benutzerfreundlicher, Open Source OpenVPN / WireGuard client für iOS und macOS"; +"global.messages.share" = "Passepartout ist ein Benutzerfreundlicher, Open Source OpenVPN / WireGuard client für iOS und macOS"; // FIXME: l10n, Apple platforms "global.alerts.buttons.remind" = "Später erinnern"; "global.alerts.buttons.never" = "Nicht erneut fragen"; @@ -83,6 +83,7 @@ "global.errors.missing_account" = "Fehlender Account"; "global.errors.missing_provider_server" = "Fehlender Standort"; "global.errors.missing_provider_preset" = "Fehlende Voreinstellung"; +"global.errors.tunnel_expired" = "Verbindung abgelaufen"; /* MARK: Menus */ @@ -132,6 +133,7 @@ /* MARK: OrganizerView */ "organizer.sections.active" = "In Benutzung"; +"organizer.sections.tv.profiles_list.header.p1" = "Öffne Passepartout auf deinem iOS- oder macOS-Gerät und aktiviere den „Apple TV“-Schalter eines Profils, damit es hier angezeigt wird."; /* MARK: OrganizerView */ "organizer.empty.no_profiles" = "Keine Profile"; @@ -163,6 +165,9 @@ "profile.sections.vpn.footer" = "Die Verbindung wird immer aufgebaut wenn notwendig."; "profile.sections.status.header" = "Verbindung"; "profile.sections.provider_infrastructure.footer" = "Zuletzt aktualisiert am %@."; +"profile.sections.tv.footer.encryption" = "Profile sind verschlüsselt und werden deinem Apple TV über iCloud zur Verfügung gestellt."; +"profile.sections.tv.footer.restricted.p1" = "Die Verbindung läuft jedoch nach %d Minuten ab."; +"profile.sections.tv.footer.restricted.p2" = "Bei einem Kauf entfällt diese Beschränkung."; "profile.sections.vpn_survives_sleep.footer" = "Deaktivieren um die Batterielaufzeit zu verbessern, allerdings verzögert sich der Verbindungsaufbau beim Aufwachen."; "profile.sections.vpn_resolves_hostname.footer" = "Bevorzugt in den meisten Netzwerken und benötigt in manchen IPv6 Netzwerken. Deaktivieren wo DNS geblockt ist oder um die Aushandlung zu beschleunigen bei langsam antwortenden DNS."; "profile.sections.feedback.header" = "Feedback"; @@ -178,6 +183,8 @@ "profile.items.only_shows_favorites.caption" = "Nur favorisierte Standorte anzeigen"; "profile.items.vpn_survives_sleep.caption" = "Verbindung aktiv halten trotz Schlafmodus"; "profile.items.vpn_resolves_hostname.caption" = "Server Hostname auflösen"; +"profile.items.tv_sharing.caption.limited" = "Begrenzt auf %d Minuten"; +"profile.items.expires_at.caption" = "Endet"; "profile.alerts.rename.title" = "Profil umbenennen"; "profile.alerts.reconnect_vpn.message" = "Möchtest du erneut zum VPN verbinden?"; @@ -323,6 +330,7 @@ "paywall.items.full_version.extra_description" = "Alle Anbieter (inklusive Zukünftige)\n%@"; "paywall.items.restore.title" = "Einkäufe wiederherstellen"; "paywall.items.restore.description" = "Wenn Sie diese App oder Funktion in der Vergangenheit gekauft haben, können Sie Ihre Einkäufe wiederherstellen und dieser Bildschirm wird nicht mehr angezeigt."; +"paywall.alerts.purchase.appletv.success.message" = "Vielen Dank! Das Zeitlimit entfällt, sobald iCloud auf dem neuesten Stand ist. Warte einen Moment und starte die Verbindung in der TV-App dann neu."; /* MARK: DonateView */ diff --git a/Passepartout/App/el.lproj/Localizable.strings b/Passepartout/App/el.lproj/Localizable.strings index adab4798..b15937cd 100644 --- a/Passepartout/App/el.lproj/Localizable.strings +++ b/Passepartout/App/el.lproj/Localizable.strings @@ -72,7 +72,7 @@ "global.strings.authentication" = "Αυθεντικοποίηση"; "global.messages.unlock_app" = "Το πασπαρτού είναι κλειδωμένο"; "global.messages.email_not_configured" = "Δεν έχει ρυθμιστεί λογαριασμός ηλεκτρονικού ταχυδρομείου."; -"global.messages.share" = "Το Passepartout είναι φιλικό προς το χρήστη, ανοιχτού κώδικα OpenVPN / WireGuard πρόγραμμα για iOS και macOS"; +"global.messages.share" = "Το Passepartout είναι φιλικό προς το χρήστη, ανοιχτού κώδικα OpenVPN / WireGuard πρόγραμμα για iOS και macOS"; // FIXME: l10n, Apple platforms "global.alerts.buttons.remind" = "Υπενθύμιση Αργότερα"; "global.alerts.buttons.never" = "Μη με ρωτήσεις ξανά"; @@ -83,6 +83,7 @@ "global.errors.missing_account" = "Λείπει λογαριασμός"; "global.errors.missing_provider_server" = "Λείπει η τοποθεσία"; "global.errors.missing_provider_preset" = "Λείπει προεπιλογή"; +"global.errors.tunnel_expired" = "H σύνδεση έληξε"; /* MARK: Menus */ @@ -132,6 +133,7 @@ /* MARK: OrganizerView */ "organizer.sections.active" = "Σε χρήση"; +"organizer.sections.tv.profiles_list.header.p1" = "Ανοίξτε το Passepartout στη συσκευή σας iOS ή macOS και ενεργοποιήστε την εναλλαγή \"Apple TV\" ενός προφίλ για να εμφανιστεί εδώ."; /* MARK: OrganizerView */ "organizer.empty.no_profiles" = "Δεν υπάρχουν προφίλ"; @@ -163,6 +165,9 @@ "profile.sections.vpn.footer" = "Η σύνδεση θα πραγματοποιηθεί όποτε είναι απαραίτητο."; "profile.sections.status.header" = "Σύνδεση"; "profile.sections.provider_infrastructure.footer" = "Τελευταία ενημέρωση στις %@."; +"profile.sections.tv.footer.encryption" = "Τα προφίλ κρυπτογραφούνται και διατίθενται στο Apple TV σας μέσω iCloud."; +"profile.sections.tv.footer.restricted.p1" = "Ωστόσο, η σύνδεση θα λήξει μετά από %d λεπτά."; +"profile.sections.tv.footer.restricted.p2" = "Αγορά για την κατάργηση του περιορισμού."; "profile.sections.vpn_survives_sleep.footer" = "Απενεργοποιήστε για να βελτιώσετε τη χρήση της μπαταρίας, εις βάρος των περιστασιακών επιβραδύνσεων που οφείλονται σε επανασύνδεση αφύπνισης."; "profile.sections.vpn_resolves_hostname.footer" = "Προτιμάται στα περισσότερα δίκτυα και απαιτείται σε ορισμένα δίκτυα IPv6. Απενεργοποιήστε το εκεί που μπλοκάρεται το DNS ή για να επιταχύνετε τη επικοινωνία όταν το DNS είναι αργό για να ανταποκριθεί."; "profile.sections.feedback.header" = "Ανατροφοδότηση"; @@ -178,6 +183,8 @@ "profile.items.only_shows_favorites.caption" = "Προβολή αγαπημένων τοποθεσιών μόνο"; "profile.items.vpn_survives_sleep.caption" = "Κρατήστε ζωντανό στον ύπνο"; "profile.items.vpn_resolves_hostname.caption" = "Επίλυση του ονόματος σέρβερ διακομιστή"; +"profile.items.tv_sharing.caption.limited" = "Περιορίστηκε σε %d λεπτά"; +"profile.items.expires_at.caption" = "Λήξη"; "profile.alerts.rename.title" = "Μετονομασία προφίλ"; "profile.alerts.reconnect_vpn.message" = "Θέλετε να συνδεθείτε ξανά με το VPN;"; @@ -323,6 +330,7 @@ "paywall.items.full_version.extra_description" = "Όλοι οι πάροχοι (περιλαμβάνονται και οι μελλοντικοί)\n%@"; "paywall.items.restore.title" = "Επαναφορά Αγορών"; "paywall.items.restore.description" = "Εαν αγοράσατε την εφαρμογή στο παρελθόν, μπορείτε να κάνετε επαναφορά αγορών και αυτή η οθόνη δε θα εμφανιστεί ξανά."; +"paywall.alerts.purchase.appletv.success.message" = "Σας ευχαριστούμε! Το χρονικό όριο θα αφαιρεθεί μόλις ενημερωθεί το iCloud. Περιμένετε μερικά λεπτά και μετά επανεκκινήστε τη σύνδεση στην εφαρμογή TV."; /* MARK: DonateView */ diff --git a/Passepartout/App/en.lproj/Localizable.strings b/Passepartout/App/en.lproj/Localizable.strings index 5f0c9242..3bb917ff 100644 --- a/Passepartout/App/en.lproj/Localizable.strings +++ b/Passepartout/App/en.lproj/Localizable.strings @@ -72,7 +72,7 @@ "global.strings.authentication" = "Authentication"; "global.messages.unlock_app" = "Passepartout is locked"; "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"; // FIXME: l10n, Apple platforms "global.alerts.buttons.remind" = "Remind me later"; "global.alerts.buttons.never" = "Don't ask again"; @@ -83,6 +83,7 @@ "global.errors.missing_account" = "Missing account"; "global.errors.missing_provider_server" = "Missing location"; "global.errors.missing_provider_preset" = "Missing preset"; +"global.errors.tunnel_expired" = "Connection expired"; /* MARK: Menus */ @@ -132,6 +133,7 @@ /* MARK: OrganizerView */ "organizer.sections.active" = "In use"; +"organizer.sections.tv.profiles_list.header.p1" = "Open Passepartout on your iOS or macOS device and enable the \"Apple TV\" toggle of a profile to make it appear here."; /* MARK: OrganizerView */ "organizer.empty.no_profiles" = "No profiles"; @@ -163,6 +165,9 @@ "profile.sections.vpn.footer" = "The connection will be established whenever necessary."; "profile.sections.status.header" = "Connection"; "profile.sections.provider_infrastructure.footer" = "Last updated on %@."; +"profile.sections.tv.footer.encryption" = "Profiles are encrypted and made available to your Apple TV via iCloud."; +"profile.sections.tv.footer.restricted.p1" = "However, the connection will expire after %d minutes."; +"profile.sections.tv.footer.restricted.p2" = "Purchase to drop the restriction."; "profile.sections.vpn_survives_sleep.footer" = "Disable to improve battery usage, at the expense of occasional slowdowns due to wake-up reconnections."; "profile.sections.vpn_resolves_hostname.footer" = "Preferred in most networks and required in some IPv6 networks. Disable where DNS is blocked, or to speed up negotiation when DNS is slow to respond."; "profile.sections.feedback.header" = "Feedback"; @@ -178,6 +183,8 @@ "profile.items.only_shows_favorites.caption" = "Only show favorite locations"; "profile.items.vpn_survives_sleep.caption" = "Keep alive on sleep"; "profile.items.vpn_resolves_hostname.caption" = "Resolve provider hostname"; +"profile.items.tv_sharing.caption.limited" = "Limited to %d minutes"; +"profile.items.expires_at.caption" = "Expiration"; "profile.alerts.rename.title" = "Rename profile"; "profile.alerts.reconnect_vpn.message" = "Do you want to reconnect to the VPN?"; @@ -323,6 +330,7 @@ "paywall.items.full_version.extra_description" = "All providers (including future ones)\n%@"; "paywall.items.restore.title" = "Restore purchases"; "paywall.items.restore.description" = "If you bought this app or feature in the past, you can restore your purchases and this screen won't show again."; +"paywall.alerts.purchase.appletv.success.message" = "Thank you! The time limit will be dropped as soon as iCloud catches up. Wait a few moments, then restart the connection on the TV app."; /* MARK: DonateView */ diff --git a/Passepartout/App/es.lproj/Localizable.strings b/Passepartout/App/es.lproj/Localizable.strings index 0ec7fb89..7d327279 100644 --- a/Passepartout/App/es.lproj/Localizable.strings +++ b/Passepartout/App/es.lproj/Localizable.strings @@ -72,7 +72,7 @@ "global.strings.authentication" = "Autenticación"; "global.messages.unlock_app" = "Passepartout está bloqueada"; "global.messages.email_not_configured" = "Ningún e-mail configurado."; -"global.messages.share" = "Passepartout es un cliente OpenVPN / WireGuard intuitivo, de código abierto para iOS y macOS"; +"global.messages.share" = "Passepartout es un cliente OpenVPN / WireGuard intuitivo, de código abierto para iOS y macOS"; // FIXME: l10n, Apple platforms "global.alerts.buttons.remind" = "Recordar más tarde"; "global.alerts.buttons.never" = "No preguntar más"; @@ -83,6 +83,7 @@ "global.errors.missing_account" = "Sin cuenta"; "global.errors.missing_provider_server" = "Sin ubicación"; "global.errors.missing_provider_preset" = "Sin ajuste"; +"global.errors.tunnel_expired" = "Conexión caducada"; /* MARK: Menus */ @@ -132,6 +133,7 @@ /* MARK: OrganizerView */ "organizer.sections.active" = "En uso"; +"organizer.sections.tv.profiles_list.header.p1" = "Abre Passepartout desde tu dispositivo iOS o macOS y habilita el switch \"Apple TV\" de un perfil para que aparezca aquí abajo."; /* MARK: OrganizerView */ "organizer.empty.no_profiles" = "Ningún perfil"; @@ -163,6 +165,9 @@ "profile.sections.vpn.footer" = "La conexión se establecerá siempre y cuando sea necesario."; "profile.sections.status.header" = "Conexión"; "profile.sections.provider_infrastructure.footer" = "Última actualización: %@."; +"profile.sections.tv.footer.encryption" = "Los perfiles están encriptados y puestos a disposición para tu Apple TV a través de iCloud."; +"profile.sections.tv.footer.restricted.p1" = "Sin embargo, la conexión caducará en %d minutos."; +"profile.sections.tv.footer.restricted.p2" = "Comprar para eliminar la restricción."; "profile.sections.vpn_survives_sleep.footer" = "Deshabilitar para mejorar el uso de la batería, a costa de ralentizaciones ocasionales por las reconexiones al despertar el dispositivo."; "profile.sections.vpn_resolves_hostname.footer" = "Preferido en la mayoría de las redes y necesario en algunas redes IPv6. Deshabilitar donde el DNS esté bloqueado, o para acelerar la negociación cuando el DNS sea lento en responder."; "profile.sections.feedback.header" = "Feedback"; @@ -178,6 +183,8 @@ "profile.items.only_shows_favorites.caption" = "Mostrar solo ubicaciones favoritas"; "profile.items.vpn_survives_sleep.caption" = "Mantener en modo inactivo"; "profile.items.vpn_resolves_hostname.caption" = "Resolver hostname del servidor"; +"profile.items.tv_sharing.caption.limited" = "Limitado a %d minutos"; +"profile.items.expires_at.caption" = "Caducidad"; "profile.alerts.rename.title" = "Renombrar perfil"; "profile.alerts.reconnect_vpn.message" = "Quieres reconectarte al VPN?"; @@ -323,6 +330,7 @@ "paywall.items.full_version.extra_description" = "Todos los proveedores (incluye los futuros)\n%@"; "paywall.items.restore.title" = "Restaurar compras"; "paywall.items.restore.description" = "Si compraste esta aplicación o funcionalidad anteriormente, puedes restaurar tus compras y esta pantalla no volverá a aparecer."; +"paywall.alerts.purchase.appletv.success.message" = "Gracias! El limite de tiempo será eliminado en cuanto iCloud se sincronice. Espera unos momentos, y reinicia la conexión en la app de la TV."; /* MARK: DonateView */ diff --git a/Passepartout/App/fastlane/ios/metadata/de-DE/release_notes.txt b/Passepartout/App/fastlane/ios/metadata/de-DE/release_notes.txt index e0981fac..658b14fb 100644 --- a/Passepartout/App/fastlane/ios/metadata/de-DE/release_notes.txt +++ b/Passepartout/App/fastlane/ios/metadata/de-DE/release_notes.txt @@ -1,4 +1,9 @@ -### Fixed +### Added -- Persisted profile is overwritten with its former value. +- WireGuard: Show data count. + +### Changed + +- Upgrade OpenSSL to 3.2.0. +- Encrypt profiles stored to iCloud. diff --git a/Passepartout/App/fastlane/ios/metadata/el/release_notes.txt b/Passepartout/App/fastlane/ios/metadata/el/release_notes.txt index e0981fac..658b14fb 100644 --- a/Passepartout/App/fastlane/ios/metadata/el/release_notes.txt +++ b/Passepartout/App/fastlane/ios/metadata/el/release_notes.txt @@ -1,4 +1,9 @@ -### Fixed +### Added -- Persisted profile is overwritten with its former value. +- WireGuard: Show data count. + +### Changed + +- Upgrade OpenSSL to 3.2.0. +- Encrypt profiles stored to iCloud. diff --git a/Passepartout/App/fastlane/ios/metadata/en-US/apple_tv_privacy_policy.txt b/Passepartout/App/fastlane/ios/metadata/en-US/apple_tv_privacy_policy.txt index 8b137891..616ee1df 100644 --- a/Passepartout/App/fastlane/ios/metadata/en-US/apple_tv_privacy_policy.txt +++ b/Passepartout/App/fastlane/ios/metadata/en-US/apple_tv_privacy_policy.txt @@ -1 +1,3 @@ +Passepartout is a bare VPN client and, as such, does not store nor forward any personal data. Therefore, you should get in touch with your VPN service provider to learn about its data retention policy. +Passepartout may access GitHub repositories on request. Please refer to GitHub privacy statement for any information about data retention when accessing their website. diff --git a/Passepartout/App/fastlane/ios/metadata/en-US/release_notes.txt b/Passepartout/App/fastlane/ios/metadata/en-US/release_notes.txt index e0981fac..658b14fb 100644 --- a/Passepartout/App/fastlane/ios/metadata/en-US/release_notes.txt +++ b/Passepartout/App/fastlane/ios/metadata/en-US/release_notes.txt @@ -1,4 +1,9 @@ -### Fixed +### Added -- Persisted profile is overwritten with its former value. +- WireGuard: Show data count. + +### Changed + +- Upgrade OpenSSL to 3.2.0. +- Encrypt profiles stored to iCloud. diff --git a/Passepartout/App/fastlane/ios/metadata/es-MX/release_notes.txt b/Passepartout/App/fastlane/ios/metadata/es-MX/release_notes.txt index e0981fac..658b14fb 100644 --- a/Passepartout/App/fastlane/ios/metadata/es-MX/release_notes.txt +++ b/Passepartout/App/fastlane/ios/metadata/es-MX/release_notes.txt @@ -1,4 +1,9 @@ -### Fixed +### Added -- Persisted profile is overwritten with its former value. +- WireGuard: Show data count. + +### Changed + +- Upgrade OpenSSL to 3.2.0. +- Encrypt profiles stored to iCloud. diff --git a/Passepartout/App/fastlane/ios/metadata/fr-FR/release_notes.txt b/Passepartout/App/fastlane/ios/metadata/fr-FR/release_notes.txt index e0981fac..658b14fb 100755 --- a/Passepartout/App/fastlane/ios/metadata/fr-FR/release_notes.txt +++ b/Passepartout/App/fastlane/ios/metadata/fr-FR/release_notes.txt @@ -1,4 +1,9 @@ -### Fixed +### Added -- Persisted profile is overwritten with its former value. +- WireGuard: Show data count. + +### Changed + +- Upgrade OpenSSL to 3.2.0. +- Encrypt profiles stored to iCloud. diff --git a/Passepartout/App/fastlane/ios/metadata/it/release_notes.txt b/Passepartout/App/fastlane/ios/metadata/it/release_notes.txt index e0981fac..658b14fb 100644 --- a/Passepartout/App/fastlane/ios/metadata/it/release_notes.txt +++ b/Passepartout/App/fastlane/ios/metadata/it/release_notes.txt @@ -1,4 +1,9 @@ -### Fixed +### Added -- Persisted profile is overwritten with its former value. +- WireGuard: Show data count. + +### Changed + +- Upgrade OpenSSL to 3.2.0. +- Encrypt profiles stored to iCloud. diff --git a/Passepartout/App/fastlane/ios/metadata/nl-NL/release_notes.txt b/Passepartout/App/fastlane/ios/metadata/nl-NL/release_notes.txt index e0981fac..658b14fb 100644 --- a/Passepartout/App/fastlane/ios/metadata/nl-NL/release_notes.txt +++ b/Passepartout/App/fastlane/ios/metadata/nl-NL/release_notes.txt @@ -1,4 +1,9 @@ -### Fixed +### Added -- Persisted profile is overwritten with its former value. +- WireGuard: Show data count. + +### Changed + +- Upgrade OpenSSL to 3.2.0. +- Encrypt profiles stored to iCloud. diff --git a/Passepartout/App/fastlane/ios/metadata/pl/release_notes.txt b/Passepartout/App/fastlane/ios/metadata/pl/release_notes.txt index e0981fac..658b14fb 100644 --- a/Passepartout/App/fastlane/ios/metadata/pl/release_notes.txt +++ b/Passepartout/App/fastlane/ios/metadata/pl/release_notes.txt @@ -1,4 +1,9 @@ -### Fixed +### Added -- Persisted profile is overwritten with its former value. +- WireGuard: Show data count. + +### Changed + +- Upgrade OpenSSL to 3.2.0. +- Encrypt profiles stored to iCloud. diff --git a/Passepartout/App/fastlane/ios/metadata/pt-BR/release_notes.txt b/Passepartout/App/fastlane/ios/metadata/pt-BR/release_notes.txt index e0981fac..658b14fb 100644 --- a/Passepartout/App/fastlane/ios/metadata/pt-BR/release_notes.txt +++ b/Passepartout/App/fastlane/ios/metadata/pt-BR/release_notes.txt @@ -1,4 +1,9 @@ -### Fixed +### Added -- Persisted profile is overwritten with its former value. +- WireGuard: Show data count. + +### Changed + +- Upgrade OpenSSL to 3.2.0. +- Encrypt profiles stored to iCloud. diff --git a/Passepartout/App/fastlane/ios/metadata/ru/release_notes.txt b/Passepartout/App/fastlane/ios/metadata/ru/release_notes.txt index e0981fac..658b14fb 100644 --- a/Passepartout/App/fastlane/ios/metadata/ru/release_notes.txt +++ b/Passepartout/App/fastlane/ios/metadata/ru/release_notes.txt @@ -1,4 +1,9 @@ -### Fixed +### Added -- Persisted profile is overwritten with its former value. +- WireGuard: Show data count. + +### Changed + +- Upgrade OpenSSL to 3.2.0. +- Encrypt profiles stored to iCloud. diff --git a/Passepartout/App/fastlane/ios/metadata/sv/release_notes.txt b/Passepartout/App/fastlane/ios/metadata/sv/release_notes.txt index e0981fac..658b14fb 100644 --- a/Passepartout/App/fastlane/ios/metadata/sv/release_notes.txt +++ b/Passepartout/App/fastlane/ios/metadata/sv/release_notes.txt @@ -1,4 +1,9 @@ -### Fixed +### Added -- Persisted profile is overwritten with its former value. +- WireGuard: Show data count. + +### Changed + +- Upgrade OpenSSL to 3.2.0. +- Encrypt profiles stored to iCloud. diff --git a/Passepartout/App/fastlane/mac/metadata/de-DE/release_notes.txt b/Passepartout/App/fastlane/mac/metadata/de-DE/release_notes.txt index e0981fac..658b14fb 100644 --- a/Passepartout/App/fastlane/mac/metadata/de-DE/release_notes.txt +++ b/Passepartout/App/fastlane/mac/metadata/de-DE/release_notes.txt @@ -1,4 +1,9 @@ -### Fixed +### Added -- Persisted profile is overwritten with its former value. +- WireGuard: Show data count. + +### Changed + +- Upgrade OpenSSL to 3.2.0. +- Encrypt profiles stored to iCloud. diff --git a/Passepartout/App/fastlane/mac/metadata/el/release_notes.txt b/Passepartout/App/fastlane/mac/metadata/el/release_notes.txt index e0981fac..658b14fb 100644 --- a/Passepartout/App/fastlane/mac/metadata/el/release_notes.txt +++ b/Passepartout/App/fastlane/mac/metadata/el/release_notes.txt @@ -1,4 +1,9 @@ -### Fixed +### Added -- Persisted profile is overwritten with its former value. +- WireGuard: Show data count. + +### Changed + +- Upgrade OpenSSL to 3.2.0. +- Encrypt profiles stored to iCloud. diff --git a/Passepartout/App/fastlane/mac/metadata/en-US/release_notes.txt b/Passepartout/App/fastlane/mac/metadata/en-US/release_notes.txt index e0981fac..658b14fb 100644 --- a/Passepartout/App/fastlane/mac/metadata/en-US/release_notes.txt +++ b/Passepartout/App/fastlane/mac/metadata/en-US/release_notes.txt @@ -1,4 +1,9 @@ -### Fixed +### Added -- Persisted profile is overwritten with its former value. +- WireGuard: Show data count. + +### Changed + +- Upgrade OpenSSL to 3.2.0. +- Encrypt profiles stored to iCloud. diff --git a/Passepartout/App/fastlane/mac/metadata/es-MX/release_notes.txt b/Passepartout/App/fastlane/mac/metadata/es-MX/release_notes.txt index e0981fac..658b14fb 100644 --- a/Passepartout/App/fastlane/mac/metadata/es-MX/release_notes.txt +++ b/Passepartout/App/fastlane/mac/metadata/es-MX/release_notes.txt @@ -1,4 +1,9 @@ -### Fixed +### Added -- Persisted profile is overwritten with its former value. +- WireGuard: Show data count. + +### Changed + +- Upgrade OpenSSL to 3.2.0. +- Encrypt profiles stored to iCloud. diff --git a/Passepartout/App/fastlane/mac/metadata/fr-FR/release_notes.txt b/Passepartout/App/fastlane/mac/metadata/fr-FR/release_notes.txt index e0981fac..658b14fb 100644 --- a/Passepartout/App/fastlane/mac/metadata/fr-FR/release_notes.txt +++ b/Passepartout/App/fastlane/mac/metadata/fr-FR/release_notes.txt @@ -1,4 +1,9 @@ -### Fixed +### Added -- Persisted profile is overwritten with its former value. +- WireGuard: Show data count. + +### Changed + +- Upgrade OpenSSL to 3.2.0. +- Encrypt profiles stored to iCloud. diff --git a/Passepartout/App/fastlane/mac/metadata/it/release_notes.txt b/Passepartout/App/fastlane/mac/metadata/it/release_notes.txt index e0981fac..658b14fb 100644 --- a/Passepartout/App/fastlane/mac/metadata/it/release_notes.txt +++ b/Passepartout/App/fastlane/mac/metadata/it/release_notes.txt @@ -1,4 +1,9 @@ -### Fixed +### Added -- Persisted profile is overwritten with its former value. +- WireGuard: Show data count. + +### Changed + +- Upgrade OpenSSL to 3.2.0. +- Encrypt profiles stored to iCloud. diff --git a/Passepartout/App/fastlane/mac/metadata/nl-NL/release_notes.txt b/Passepartout/App/fastlane/mac/metadata/nl-NL/release_notes.txt index e0981fac..658b14fb 100644 --- a/Passepartout/App/fastlane/mac/metadata/nl-NL/release_notes.txt +++ b/Passepartout/App/fastlane/mac/metadata/nl-NL/release_notes.txt @@ -1,4 +1,9 @@ -### Fixed +### Added -- Persisted profile is overwritten with its former value. +- WireGuard: Show data count. + +### Changed + +- Upgrade OpenSSL to 3.2.0. +- Encrypt profiles stored to iCloud. diff --git a/Passepartout/App/fastlane/mac/metadata/pl/release_notes.txt b/Passepartout/App/fastlane/mac/metadata/pl/release_notes.txt index e0981fac..658b14fb 100644 --- a/Passepartout/App/fastlane/mac/metadata/pl/release_notes.txt +++ b/Passepartout/App/fastlane/mac/metadata/pl/release_notes.txt @@ -1,4 +1,9 @@ -### Fixed +### Added -- Persisted profile is overwritten with its former value. +- WireGuard: Show data count. + +### Changed + +- Upgrade OpenSSL to 3.2.0. +- Encrypt profiles stored to iCloud. diff --git a/Passepartout/App/fastlane/mac/metadata/pt-BR/release_notes.txt b/Passepartout/App/fastlane/mac/metadata/pt-BR/release_notes.txt index e0981fac..658b14fb 100644 --- a/Passepartout/App/fastlane/mac/metadata/pt-BR/release_notes.txt +++ b/Passepartout/App/fastlane/mac/metadata/pt-BR/release_notes.txt @@ -1,4 +1,9 @@ -### Fixed +### Added -- Persisted profile is overwritten with its former value. +- WireGuard: Show data count. + +### Changed + +- Upgrade OpenSSL to 3.2.0. +- Encrypt profiles stored to iCloud. diff --git a/Passepartout/App/fastlane/mac/metadata/ru/release_notes.txt b/Passepartout/App/fastlane/mac/metadata/ru/release_notes.txt index e0981fac..658b14fb 100644 --- a/Passepartout/App/fastlane/mac/metadata/ru/release_notes.txt +++ b/Passepartout/App/fastlane/mac/metadata/ru/release_notes.txt @@ -1,4 +1,9 @@ -### Fixed +### Added -- Persisted profile is overwritten with its former value. +- WireGuard: Show data count. + +### Changed + +- Upgrade OpenSSL to 3.2.0. +- Encrypt profiles stored to iCloud. diff --git a/Passepartout/App/fastlane/mac/metadata/sv/release_notes.txt b/Passepartout/App/fastlane/mac/metadata/sv/release_notes.txt index e0981fac..658b14fb 100644 --- a/Passepartout/App/fastlane/mac/metadata/sv/release_notes.txt +++ b/Passepartout/App/fastlane/mac/metadata/sv/release_notes.txt @@ -1,4 +1,9 @@ -### Fixed +### Added -- Persisted profile is overwritten with its former value. +- WireGuard: Show data count. + +### Changed + +- Upgrade OpenSSL to 3.2.0. +- Encrypt profiles stored to iCloud. diff --git a/Passepartout/App/fastlane/symlink-appletv-privacy.sh b/Passepartout/App/fastlane/symlink-appletv-privacy.sh new file mode 100755 index 00000000..d738c9be --- /dev/null +++ b/Passepartout/App/fastlane/symlink-appletv-privacy.sh @@ -0,0 +1,7 @@ +#!/bin/bash +LANGUAGE=$1 +FILENAME="apple_tv_privacy_policy.txt" +IOS_DIR=../../../ios/metadata/en-US +cd $LANGUAGE +rm -f $FILENAME +ln -s "$IOS_DIR/$FILENAME" diff --git a/Passepartout/App/fastlane/symlink-l10n.sh b/Passepartout/App/fastlane/symlink-l10n.sh new file mode 100755 index 00000000..72234922 --- /dev/null +++ b/Passepartout/App/fastlane/symlink-l10n.sh @@ -0,0 +1,9 @@ +#!/bin/bash +LANGUAGE=$1 +FILELIST="apple_tv_privacy_policy.txt description.txt keywords.txt marketing_url.txt name.txt privacy_url.txt promotional_text.txt subtitle.txt support_url.txt" +IOS_DIR=../../../ios/metadata +cd $LANGUAGE +rm *.txt +for FILENAME in $FILELIST; do + ln -s "$IOS_DIR/$LANGUAGE/$FILENAME" +done diff --git a/Passepartout/App/fastlane/tvos/metadata/copyright.txt b/Passepartout/App/fastlane/tvos/metadata/copyright.txt new file mode 120000 index 00000000..ea7bc17b --- /dev/null +++ b/Passepartout/App/fastlane/tvos/metadata/copyright.txt @@ -0,0 +1 @@ +../../ios/metadata/copyright.txt \ No newline at end of file diff --git a/Passepartout/App/fastlane/tvos/metadata/de-DE/apple_tv_privacy_policy.txt b/Passepartout/App/fastlane/tvos/metadata/de-DE/apple_tv_privacy_policy.txt new file mode 120000 index 00000000..db021f29 --- /dev/null +++ b/Passepartout/App/fastlane/tvos/metadata/de-DE/apple_tv_privacy_policy.txt @@ -0,0 +1 @@ +../../../ios/metadata/en-US/apple_tv_privacy_policy.txt \ No newline at end of file diff --git a/Passepartout/App/fastlane/tvos/metadata/de-DE/description.txt b/Passepartout/App/fastlane/tvos/metadata/de-DE/description.txt new file mode 100644 index 00000000..f63a1185 --- /dev/null +++ b/Passepartout/App/fastlane/tvos/metadata/de-DE/description.txt @@ -0,0 +1,3 @@ +Passepartout ist ein smarter VPN client der perfekt in die Apple-Plattform integriert ist. Dank der Ende-zu-Ende-Verschlüsselung des CloudKits kannst du die VPN-Profile deiner iOS- und macOS-Apps jetzt auch sicher mit deinem Apple TV teilen. + +Passepartout ist Open Source: https://github.com/passepartoutvpn diff --git a/Passepartout/App/fastlane/tvos/metadata/de-DE/keywords.txt b/Passepartout/App/fastlane/tvos/metadata/de-DE/keywords.txt new file mode 120000 index 00000000..4d587f18 --- /dev/null +++ b/Passepartout/App/fastlane/tvos/metadata/de-DE/keywords.txt @@ -0,0 +1 @@ +../../../ios/metadata/de-DE/keywords.txt \ No newline at end of file diff --git a/Passepartout/App/fastlane/tvos/metadata/de-DE/marketing_url.txt b/Passepartout/App/fastlane/tvos/metadata/de-DE/marketing_url.txt new file mode 120000 index 00000000..641438ba --- /dev/null +++ b/Passepartout/App/fastlane/tvos/metadata/de-DE/marketing_url.txt @@ -0,0 +1 @@ +../../../ios/metadata/de-DE/marketing_url.txt \ No newline at end of file diff --git a/Passepartout/App/fastlane/tvos/metadata/de-DE/name.txt b/Passepartout/App/fastlane/tvos/metadata/de-DE/name.txt new file mode 120000 index 00000000..768f56fb --- /dev/null +++ b/Passepartout/App/fastlane/tvos/metadata/de-DE/name.txt @@ -0,0 +1 @@ +../../../ios/metadata/de-DE/name.txt \ No newline at end of file diff --git a/Passepartout/App/fastlane/tvos/metadata/de-DE/privacy_url.txt b/Passepartout/App/fastlane/tvos/metadata/de-DE/privacy_url.txt new file mode 120000 index 00000000..a4a7a52c --- /dev/null +++ b/Passepartout/App/fastlane/tvos/metadata/de-DE/privacy_url.txt @@ -0,0 +1 @@ +../../../ios/metadata/de-DE/privacy_url.txt \ No newline at end of file diff --git a/Passepartout/App/fastlane/tvos/metadata/de-DE/promotional_text.txt b/Passepartout/App/fastlane/tvos/metadata/de-DE/promotional_text.txt new file mode 120000 index 00000000..2dc0ca16 --- /dev/null +++ b/Passepartout/App/fastlane/tvos/metadata/de-DE/promotional_text.txt @@ -0,0 +1 @@ +../../../ios/metadata/de-DE/promotional_text.txt \ No newline at end of file diff --git a/Passepartout/App/fastlane/tvos/metadata/de-DE/subtitle.txt b/Passepartout/App/fastlane/tvos/metadata/de-DE/subtitle.txt new file mode 120000 index 00000000..13ed3c40 --- /dev/null +++ b/Passepartout/App/fastlane/tvos/metadata/de-DE/subtitle.txt @@ -0,0 +1 @@ +../../../ios/metadata/de-DE/subtitle.txt \ No newline at end of file diff --git a/Passepartout/App/fastlane/tvos/metadata/de-DE/support_url.txt b/Passepartout/App/fastlane/tvos/metadata/de-DE/support_url.txt new file mode 120000 index 00000000..adcdde2b --- /dev/null +++ b/Passepartout/App/fastlane/tvos/metadata/de-DE/support_url.txt @@ -0,0 +1 @@ +../../../ios/metadata/de-DE/support_url.txt \ No newline at end of file diff --git a/Passepartout/App/fastlane/tvos/metadata/el/apple_tv_privacy_policy.txt b/Passepartout/App/fastlane/tvos/metadata/el/apple_tv_privacy_policy.txt new file mode 120000 index 00000000..db021f29 --- /dev/null +++ b/Passepartout/App/fastlane/tvos/metadata/el/apple_tv_privacy_policy.txt @@ -0,0 +1 @@ +../../../ios/metadata/en-US/apple_tv_privacy_policy.txt \ No newline at end of file diff --git a/Passepartout/App/fastlane/tvos/metadata/el/description.txt b/Passepartout/App/fastlane/tvos/metadata/el/description.txt new file mode 100644 index 00000000..83832184 --- /dev/null +++ b/Passepartout/App/fastlane/tvos/metadata/el/description.txt @@ -0,0 +1,3 @@ +Το Passepartout είναι ένας έξυπνος πελάτης VPN που είναι απόλυτα ενσωματωμένος στις πλατφόρμες Apple. Μπορείτε πλέον να μοιράζεστε τα προφίλ σας VPN από τις εφαρμογές iOS και macOS με το Apple TV σας, επίσης με ασφάλεια χάρη στην κρυπτογράφηση από άκρο σε άκρο του CloudKit. + +To Passepartout βασίζεται σε ανοιχτό κώδικα: https://github.com/passepartoutvpn diff --git a/Passepartout/App/fastlane/tvos/metadata/el/keywords.txt b/Passepartout/App/fastlane/tvos/metadata/el/keywords.txt new file mode 120000 index 00000000..fcb4a48d --- /dev/null +++ b/Passepartout/App/fastlane/tvos/metadata/el/keywords.txt @@ -0,0 +1 @@ +../../../ios/metadata/el/keywords.txt \ No newline at end of file diff --git a/Passepartout/App/fastlane/tvos/metadata/el/marketing_url.txt b/Passepartout/App/fastlane/tvos/metadata/el/marketing_url.txt new file mode 120000 index 00000000..389a9a0a --- /dev/null +++ b/Passepartout/App/fastlane/tvos/metadata/el/marketing_url.txt @@ -0,0 +1 @@ +../../../ios/metadata/el/marketing_url.txt \ No newline at end of file diff --git a/Passepartout/App/fastlane/tvos/metadata/el/name.txt b/Passepartout/App/fastlane/tvos/metadata/el/name.txt new file mode 120000 index 00000000..f3df0704 --- /dev/null +++ b/Passepartout/App/fastlane/tvos/metadata/el/name.txt @@ -0,0 +1 @@ +../../../ios/metadata/el/name.txt \ No newline at end of file diff --git a/Passepartout/App/fastlane/tvos/metadata/el/privacy_url.txt b/Passepartout/App/fastlane/tvos/metadata/el/privacy_url.txt new file mode 120000 index 00000000..39ac401f --- /dev/null +++ b/Passepartout/App/fastlane/tvos/metadata/el/privacy_url.txt @@ -0,0 +1 @@ +../../../ios/metadata/el/privacy_url.txt \ No newline at end of file diff --git a/Passepartout/App/fastlane/tvos/metadata/el/promotional_text.txt b/Passepartout/App/fastlane/tvos/metadata/el/promotional_text.txt new file mode 120000 index 00000000..1f79def5 --- /dev/null +++ b/Passepartout/App/fastlane/tvos/metadata/el/promotional_text.txt @@ -0,0 +1 @@ +../../../ios/metadata/el/promotional_text.txt \ No newline at end of file diff --git a/Passepartout/App/fastlane/tvos/metadata/el/subtitle.txt b/Passepartout/App/fastlane/tvos/metadata/el/subtitle.txt new file mode 120000 index 00000000..1151d981 --- /dev/null +++ b/Passepartout/App/fastlane/tvos/metadata/el/subtitle.txt @@ -0,0 +1 @@ +../../../ios/metadata/el/subtitle.txt \ No newline at end of file diff --git a/Passepartout/App/fastlane/tvos/metadata/el/support_url.txt b/Passepartout/App/fastlane/tvos/metadata/el/support_url.txt new file mode 120000 index 00000000..7824c415 --- /dev/null +++ b/Passepartout/App/fastlane/tvos/metadata/el/support_url.txt @@ -0,0 +1 @@ +../../../ios/metadata/el/support_url.txt \ No newline at end of file diff --git a/Passepartout/App/fastlane/tvos/metadata/en-US/apple_tv_privacy_policy.txt b/Passepartout/App/fastlane/tvos/metadata/en-US/apple_tv_privacy_policy.txt new file mode 120000 index 00000000..db021f29 --- /dev/null +++ b/Passepartout/App/fastlane/tvos/metadata/en-US/apple_tv_privacy_policy.txt @@ -0,0 +1 @@ +../../../ios/metadata/en-US/apple_tv_privacy_policy.txt \ No newline at end of file diff --git a/Passepartout/App/fastlane/tvos/metadata/en-US/description.txt b/Passepartout/App/fastlane/tvos/metadata/en-US/description.txt new file mode 100644 index 00000000..0bf5a40d --- /dev/null +++ b/Passepartout/App/fastlane/tvos/metadata/en-US/description.txt @@ -0,0 +1,3 @@ +Passepartout is a smart VPN client perfectly integrated with Apple platforms. You may now share your VPN profiles from the iOS and macOS apps with your Apple TV, also safely thanks to CloudKit end-to-end encryption. + +Passepartout is open source: https://github.com/passepartoutvpn diff --git a/Passepartout/App/fastlane/tvos/metadata/en-US/keywords.txt b/Passepartout/App/fastlane/tvos/metadata/en-US/keywords.txt new file mode 120000 index 00000000..d285c5ce --- /dev/null +++ b/Passepartout/App/fastlane/tvos/metadata/en-US/keywords.txt @@ -0,0 +1 @@ +../../../ios/metadata/en-US/keywords.txt \ No newline at end of file diff --git a/Passepartout/App/fastlane/tvos/metadata/en-US/marketing_url.txt b/Passepartout/App/fastlane/tvos/metadata/en-US/marketing_url.txt new file mode 120000 index 00000000..6bfb2aa4 --- /dev/null +++ b/Passepartout/App/fastlane/tvos/metadata/en-US/marketing_url.txt @@ -0,0 +1 @@ +../../../ios/metadata/en-US/marketing_url.txt \ No newline at end of file diff --git a/Passepartout/App/fastlane/tvos/metadata/en-US/name.txt b/Passepartout/App/fastlane/tvos/metadata/en-US/name.txt new file mode 120000 index 00000000..6c01feee --- /dev/null +++ b/Passepartout/App/fastlane/tvos/metadata/en-US/name.txt @@ -0,0 +1 @@ +../../../ios/metadata/en-US/name.txt \ No newline at end of file diff --git a/Passepartout/App/fastlane/tvos/metadata/en-US/privacy_url.txt b/Passepartout/App/fastlane/tvos/metadata/en-US/privacy_url.txt new file mode 120000 index 00000000..631e58f2 --- /dev/null +++ b/Passepartout/App/fastlane/tvos/metadata/en-US/privacy_url.txt @@ -0,0 +1 @@ +../../../ios/metadata/en-US/privacy_url.txt \ No newline at end of file diff --git a/Passepartout/App/fastlane/tvos/metadata/en-US/promotional_text.txt b/Passepartout/App/fastlane/tvos/metadata/en-US/promotional_text.txt new file mode 120000 index 00000000..6d9f7336 --- /dev/null +++ b/Passepartout/App/fastlane/tvos/metadata/en-US/promotional_text.txt @@ -0,0 +1 @@ +../../../ios/metadata/en-US/promotional_text.txt \ No newline at end of file diff --git a/Passepartout/App/fastlane/tvos/metadata/en-US/subtitle.txt b/Passepartout/App/fastlane/tvos/metadata/en-US/subtitle.txt new file mode 120000 index 00000000..7a24133e --- /dev/null +++ b/Passepartout/App/fastlane/tvos/metadata/en-US/subtitle.txt @@ -0,0 +1 @@ +../../../ios/metadata/en-US/subtitle.txt \ No newline at end of file diff --git a/Passepartout/App/fastlane/tvos/metadata/en-US/support_url.txt b/Passepartout/App/fastlane/tvos/metadata/en-US/support_url.txt new file mode 120000 index 00000000..0076fdfd --- /dev/null +++ b/Passepartout/App/fastlane/tvos/metadata/en-US/support_url.txt @@ -0,0 +1 @@ +../../../ios/metadata/en-US/support_url.txt \ No newline at end of file diff --git a/Passepartout/App/fastlane/tvos/metadata/es-MX/apple_tv_privacy_policy.txt b/Passepartout/App/fastlane/tvos/metadata/es-MX/apple_tv_privacy_policy.txt new file mode 120000 index 00000000..db021f29 --- /dev/null +++ b/Passepartout/App/fastlane/tvos/metadata/es-MX/apple_tv_privacy_policy.txt @@ -0,0 +1 @@ +../../../ios/metadata/en-US/apple_tv_privacy_policy.txt \ No newline at end of file diff --git a/Passepartout/App/fastlane/tvos/metadata/es-MX/description.txt b/Passepartout/App/fastlane/tvos/metadata/es-MX/description.txt new file mode 100644 index 00000000..377f9e0d --- /dev/null +++ b/Passepartout/App/fastlane/tvos/metadata/es-MX/description.txt @@ -0,0 +1,3 @@ +Passepartout es un cliente VPN inteligente perfectamente integrado con las plataformas Apple. Ya puedes compartir tus perfiles VPN desde tus apps iOS y macOS con tu Apple TV, también de forma segura gracias a la criptografía end-to-end de CloudKit. + +Passepartout tiene código abierto: https://github.com/passepartoutvpn diff --git a/Passepartout/App/fastlane/tvos/metadata/es-MX/keywords.txt b/Passepartout/App/fastlane/tvos/metadata/es-MX/keywords.txt new file mode 120000 index 00000000..01ac2936 --- /dev/null +++ b/Passepartout/App/fastlane/tvos/metadata/es-MX/keywords.txt @@ -0,0 +1 @@ +../../../ios/metadata/es-MX/keywords.txt \ No newline at end of file diff --git a/Passepartout/App/fastlane/tvos/metadata/es-MX/marketing_url.txt b/Passepartout/App/fastlane/tvos/metadata/es-MX/marketing_url.txt new file mode 120000 index 00000000..e9f02a42 --- /dev/null +++ b/Passepartout/App/fastlane/tvos/metadata/es-MX/marketing_url.txt @@ -0,0 +1 @@ +../../../ios/metadata/es-MX/marketing_url.txt \ No newline at end of file diff --git a/Passepartout/App/fastlane/tvos/metadata/es-MX/name.txt b/Passepartout/App/fastlane/tvos/metadata/es-MX/name.txt new file mode 120000 index 00000000..c4ffda47 --- /dev/null +++ b/Passepartout/App/fastlane/tvos/metadata/es-MX/name.txt @@ -0,0 +1 @@ +../../../ios/metadata/es-MX/name.txt \ No newline at end of file diff --git a/Passepartout/App/fastlane/tvos/metadata/es-MX/privacy_url.txt b/Passepartout/App/fastlane/tvos/metadata/es-MX/privacy_url.txt new file mode 120000 index 00000000..f0b4717c --- /dev/null +++ b/Passepartout/App/fastlane/tvos/metadata/es-MX/privacy_url.txt @@ -0,0 +1 @@ +../../../ios/metadata/es-MX/privacy_url.txt \ No newline at end of file diff --git a/Passepartout/App/fastlane/tvos/metadata/es-MX/promotional_text.txt b/Passepartout/App/fastlane/tvos/metadata/es-MX/promotional_text.txt new file mode 120000 index 00000000..43a77163 --- /dev/null +++ b/Passepartout/App/fastlane/tvos/metadata/es-MX/promotional_text.txt @@ -0,0 +1 @@ +../../../ios/metadata/es-MX/promotional_text.txt \ No newline at end of file diff --git a/Passepartout/App/fastlane/tvos/metadata/es-MX/subtitle.txt b/Passepartout/App/fastlane/tvos/metadata/es-MX/subtitle.txt new file mode 120000 index 00000000..a5f53378 --- /dev/null +++ b/Passepartout/App/fastlane/tvos/metadata/es-MX/subtitle.txt @@ -0,0 +1 @@ +../../../ios/metadata/es-MX/subtitle.txt \ No newline at end of file diff --git a/Passepartout/App/fastlane/tvos/metadata/es-MX/support_url.txt b/Passepartout/App/fastlane/tvos/metadata/es-MX/support_url.txt new file mode 120000 index 00000000..35263621 --- /dev/null +++ b/Passepartout/App/fastlane/tvos/metadata/es-MX/support_url.txt @@ -0,0 +1 @@ +../../../ios/metadata/es-MX/support_url.txt \ No newline at end of file diff --git a/Passepartout/App/fastlane/tvos/metadata/fr-FR/apple_tv_privacy_policy.txt b/Passepartout/App/fastlane/tvos/metadata/fr-FR/apple_tv_privacy_policy.txt new file mode 120000 index 00000000..db021f29 --- /dev/null +++ b/Passepartout/App/fastlane/tvos/metadata/fr-FR/apple_tv_privacy_policy.txt @@ -0,0 +1 @@ +../../../ios/metadata/en-US/apple_tv_privacy_policy.txt \ No newline at end of file diff --git a/Passepartout/App/fastlane/tvos/metadata/fr-FR/description.txt b/Passepartout/App/fastlane/tvos/metadata/fr-FR/description.txt new file mode 100644 index 00000000..acb361f4 --- /dev/null +++ b/Passepartout/App/fastlane/tvos/metadata/fr-FR/description.txt @@ -0,0 +1,3 @@ +Passepartout est un client VPN parfaitement intégré avec les plateformes Apple. Vous pouvez à présent partager vos profils VPN depuis les applications iOS et macOS avec votre Apple TV en toute sécurité grâce au chiffrement de bout en bout CloudKit. + +Passepartout est open source: https://github.com/passepartoutvpn diff --git a/Passepartout/App/fastlane/tvos/metadata/fr-FR/keywords.txt b/Passepartout/App/fastlane/tvos/metadata/fr-FR/keywords.txt new file mode 120000 index 00000000..3257ef14 --- /dev/null +++ b/Passepartout/App/fastlane/tvos/metadata/fr-FR/keywords.txt @@ -0,0 +1 @@ +../../../ios/metadata/fr-FR/keywords.txt \ No newline at end of file diff --git a/Passepartout/App/fastlane/tvos/metadata/fr-FR/marketing_url.txt b/Passepartout/App/fastlane/tvos/metadata/fr-FR/marketing_url.txt new file mode 120000 index 00000000..86e000c3 --- /dev/null +++ b/Passepartout/App/fastlane/tvos/metadata/fr-FR/marketing_url.txt @@ -0,0 +1 @@ +../../../ios/metadata/fr-FR/marketing_url.txt \ No newline at end of file diff --git a/Passepartout/App/fastlane/tvos/metadata/fr-FR/name.txt b/Passepartout/App/fastlane/tvos/metadata/fr-FR/name.txt new file mode 120000 index 00000000..86751c5b --- /dev/null +++ b/Passepartout/App/fastlane/tvos/metadata/fr-FR/name.txt @@ -0,0 +1 @@ +../../../ios/metadata/fr-FR/name.txt \ No newline at end of file diff --git a/Passepartout/App/fastlane/tvos/metadata/fr-FR/privacy_url.txt b/Passepartout/App/fastlane/tvos/metadata/fr-FR/privacy_url.txt new file mode 120000 index 00000000..91460c59 --- /dev/null +++ b/Passepartout/App/fastlane/tvos/metadata/fr-FR/privacy_url.txt @@ -0,0 +1 @@ +../../../ios/metadata/fr-FR/privacy_url.txt \ No newline at end of file diff --git a/Passepartout/App/fastlane/tvos/metadata/fr-FR/promotional_text.txt b/Passepartout/App/fastlane/tvos/metadata/fr-FR/promotional_text.txt new file mode 120000 index 00000000..4b471308 --- /dev/null +++ b/Passepartout/App/fastlane/tvos/metadata/fr-FR/promotional_text.txt @@ -0,0 +1 @@ +../../../ios/metadata/fr-FR/promotional_text.txt \ No newline at end of file diff --git a/Passepartout/App/fastlane/tvos/metadata/fr-FR/subtitle.txt b/Passepartout/App/fastlane/tvos/metadata/fr-FR/subtitle.txt new file mode 120000 index 00000000..91e54eed --- /dev/null +++ b/Passepartout/App/fastlane/tvos/metadata/fr-FR/subtitle.txt @@ -0,0 +1 @@ +../../../ios/metadata/fr-FR/subtitle.txt \ No newline at end of file diff --git a/Passepartout/App/fastlane/tvos/metadata/fr-FR/support_url.txt b/Passepartout/App/fastlane/tvos/metadata/fr-FR/support_url.txt new file mode 120000 index 00000000..e826cede --- /dev/null +++ b/Passepartout/App/fastlane/tvos/metadata/fr-FR/support_url.txt @@ -0,0 +1 @@ +../../../ios/metadata/fr-FR/support_url.txt \ No newline at end of file diff --git a/Passepartout/App/fastlane/tvos/metadata/it/apple_tv_privacy_policy.txt b/Passepartout/App/fastlane/tvos/metadata/it/apple_tv_privacy_policy.txt new file mode 120000 index 00000000..db021f29 --- /dev/null +++ b/Passepartout/App/fastlane/tvos/metadata/it/apple_tv_privacy_policy.txt @@ -0,0 +1 @@ +../../../ios/metadata/en-US/apple_tv_privacy_policy.txt \ No newline at end of file diff --git a/Passepartout/App/fastlane/tvos/metadata/it/description.txt b/Passepartout/App/fastlane/tvos/metadata/it/description.txt new file mode 100644 index 00000000..d60e6698 --- /dev/null +++ b/Passepartout/App/fastlane/tvos/metadata/it/description.txt @@ -0,0 +1,3 @@ +Passepartout è un client VPN intelligente perfettamente integrato con le piattaforme Apple. Ora puoi condividere i tuoi profili VPN dalle app iOS e macOS con la tua Apple TV, e in modo sicuro grazie alla crittografia end-to-end di CloudKit. + +Passepartout è open source: https://github.com/passepartoutvpn diff --git a/Passepartout/App/fastlane/tvos/metadata/it/keywords.txt b/Passepartout/App/fastlane/tvos/metadata/it/keywords.txt new file mode 120000 index 00000000..2fd1fbd0 --- /dev/null +++ b/Passepartout/App/fastlane/tvos/metadata/it/keywords.txt @@ -0,0 +1 @@ +../../../ios/metadata/it/keywords.txt \ No newline at end of file diff --git a/Passepartout/App/fastlane/tvos/metadata/it/marketing_url.txt b/Passepartout/App/fastlane/tvos/metadata/it/marketing_url.txt new file mode 120000 index 00000000..0753c8ea --- /dev/null +++ b/Passepartout/App/fastlane/tvos/metadata/it/marketing_url.txt @@ -0,0 +1 @@ +../../../ios/metadata/it/marketing_url.txt \ No newline at end of file diff --git a/Passepartout/App/fastlane/tvos/metadata/it/name.txt b/Passepartout/App/fastlane/tvos/metadata/it/name.txt new file mode 120000 index 00000000..a5af425b --- /dev/null +++ b/Passepartout/App/fastlane/tvos/metadata/it/name.txt @@ -0,0 +1 @@ +../../../ios/metadata/it/name.txt \ No newline at end of file diff --git a/Passepartout/App/fastlane/tvos/metadata/it/privacy_url.txt b/Passepartout/App/fastlane/tvos/metadata/it/privacy_url.txt new file mode 120000 index 00000000..f978fbf1 --- /dev/null +++ b/Passepartout/App/fastlane/tvos/metadata/it/privacy_url.txt @@ -0,0 +1 @@ +../../../ios/metadata/it/privacy_url.txt \ No newline at end of file diff --git a/Passepartout/App/fastlane/tvos/metadata/it/promotional_text.txt b/Passepartout/App/fastlane/tvos/metadata/it/promotional_text.txt new file mode 120000 index 00000000..6f450d39 --- /dev/null +++ b/Passepartout/App/fastlane/tvos/metadata/it/promotional_text.txt @@ -0,0 +1 @@ +../../../ios/metadata/it/promotional_text.txt \ No newline at end of file diff --git a/Passepartout/App/fastlane/tvos/metadata/it/subtitle.txt b/Passepartout/App/fastlane/tvos/metadata/it/subtitle.txt new file mode 120000 index 00000000..f7672a65 --- /dev/null +++ b/Passepartout/App/fastlane/tvos/metadata/it/subtitle.txt @@ -0,0 +1 @@ +../../../ios/metadata/it/subtitle.txt \ No newline at end of file diff --git a/Passepartout/App/fastlane/tvos/metadata/it/support_url.txt b/Passepartout/App/fastlane/tvos/metadata/it/support_url.txt new file mode 120000 index 00000000..64476e81 --- /dev/null +++ b/Passepartout/App/fastlane/tvos/metadata/it/support_url.txt @@ -0,0 +1 @@ +../../../ios/metadata/it/support_url.txt \ No newline at end of file diff --git a/Passepartout/App/fastlane/tvos/metadata/nl-NL/apple_tv_privacy_policy.txt b/Passepartout/App/fastlane/tvos/metadata/nl-NL/apple_tv_privacy_policy.txt new file mode 120000 index 00000000..db021f29 --- /dev/null +++ b/Passepartout/App/fastlane/tvos/metadata/nl-NL/apple_tv_privacy_policy.txt @@ -0,0 +1 @@ +../../../ios/metadata/en-US/apple_tv_privacy_policy.txt \ No newline at end of file diff --git a/Passepartout/App/fastlane/tvos/metadata/nl-NL/description.txt b/Passepartout/App/fastlane/tvos/metadata/nl-NL/description.txt new file mode 100644 index 00000000..a893d742 --- /dev/null +++ b/Passepartout/App/fastlane/tvos/metadata/nl-NL/description.txt @@ -0,0 +1,3 @@ +Passepartout is een slimme VPN-client die perfect is geïntegreerd in Apple-platformen. Je kunt je VPN-profielen van de iOS- en macOS-apps nu delen met je Apple TV, ook op een veilige manier dankzij de volledige encryptie van CloudKit. + +Passepartout is open source: https://github.com/passepartoutvpn diff --git a/Passepartout/App/fastlane/tvos/metadata/nl-NL/keywords.txt b/Passepartout/App/fastlane/tvos/metadata/nl-NL/keywords.txt new file mode 120000 index 00000000..bcaebd4e --- /dev/null +++ b/Passepartout/App/fastlane/tvos/metadata/nl-NL/keywords.txt @@ -0,0 +1 @@ +../../../ios/metadata/nl-NL/keywords.txt \ No newline at end of file diff --git a/Passepartout/App/fastlane/tvos/metadata/nl-NL/marketing_url.txt b/Passepartout/App/fastlane/tvos/metadata/nl-NL/marketing_url.txt new file mode 120000 index 00000000..1f730d5d --- /dev/null +++ b/Passepartout/App/fastlane/tvos/metadata/nl-NL/marketing_url.txt @@ -0,0 +1 @@ +../../../ios/metadata/nl-NL/marketing_url.txt \ No newline at end of file diff --git a/Passepartout/App/fastlane/tvos/metadata/nl-NL/name.txt b/Passepartout/App/fastlane/tvos/metadata/nl-NL/name.txt new file mode 120000 index 00000000..2876284f --- /dev/null +++ b/Passepartout/App/fastlane/tvos/metadata/nl-NL/name.txt @@ -0,0 +1 @@ +../../../ios/metadata/nl-NL/name.txt \ No newline at end of file diff --git a/Passepartout/App/fastlane/tvos/metadata/nl-NL/privacy_url.txt b/Passepartout/App/fastlane/tvos/metadata/nl-NL/privacy_url.txt new file mode 120000 index 00000000..5f224f28 --- /dev/null +++ b/Passepartout/App/fastlane/tvos/metadata/nl-NL/privacy_url.txt @@ -0,0 +1 @@ +../../../ios/metadata/nl-NL/privacy_url.txt \ No newline at end of file diff --git a/Passepartout/App/fastlane/tvos/metadata/nl-NL/promotional_text.txt b/Passepartout/App/fastlane/tvos/metadata/nl-NL/promotional_text.txt new file mode 120000 index 00000000..efc3e72c --- /dev/null +++ b/Passepartout/App/fastlane/tvos/metadata/nl-NL/promotional_text.txt @@ -0,0 +1 @@ +../../../ios/metadata/nl-NL/promotional_text.txt \ No newline at end of file diff --git a/Passepartout/App/fastlane/tvos/metadata/nl-NL/subtitle.txt b/Passepartout/App/fastlane/tvos/metadata/nl-NL/subtitle.txt new file mode 120000 index 00000000..002950a4 --- /dev/null +++ b/Passepartout/App/fastlane/tvos/metadata/nl-NL/subtitle.txt @@ -0,0 +1 @@ +../../../ios/metadata/nl-NL/subtitle.txt \ No newline at end of file diff --git a/Passepartout/App/fastlane/tvos/metadata/nl-NL/support_url.txt b/Passepartout/App/fastlane/tvos/metadata/nl-NL/support_url.txt new file mode 120000 index 00000000..ed8b9e79 --- /dev/null +++ b/Passepartout/App/fastlane/tvos/metadata/nl-NL/support_url.txt @@ -0,0 +1 @@ +../../../ios/metadata/nl-NL/support_url.txt \ No newline at end of file diff --git a/Passepartout/App/fastlane/tvos/metadata/pl/apple_tv_privacy_policy.txt b/Passepartout/App/fastlane/tvos/metadata/pl/apple_tv_privacy_policy.txt new file mode 120000 index 00000000..db021f29 --- /dev/null +++ b/Passepartout/App/fastlane/tvos/metadata/pl/apple_tv_privacy_policy.txt @@ -0,0 +1 @@ +../../../ios/metadata/en-US/apple_tv_privacy_policy.txt \ No newline at end of file diff --git a/Passepartout/App/fastlane/tvos/metadata/pl/description.txt b/Passepartout/App/fastlane/tvos/metadata/pl/description.txt new file mode 100644 index 00000000..4b892802 --- /dev/null +++ b/Passepartout/App/fastlane/tvos/metadata/pl/description.txt @@ -0,0 +1,3 @@ +Passepartout jest inteligentnym klientem VPN, perfekcyjnie zintegrowanym z platformami systemu iOS. Możesz teraz udostępniać swoje profile VPN z aplikacji iOS i macOS na Apple TV, również bezpiecznie dzięki szyfrowaniu typu end-to-end CloudKit. + +Passepartout jest aplikacją typu open source: https://github.com/passepartoutvpn diff --git a/Passepartout/App/fastlane/tvos/metadata/pl/keywords.txt b/Passepartout/App/fastlane/tvos/metadata/pl/keywords.txt new file mode 120000 index 00000000..071cd410 --- /dev/null +++ b/Passepartout/App/fastlane/tvos/metadata/pl/keywords.txt @@ -0,0 +1 @@ +../../../ios/metadata/pl/keywords.txt \ No newline at end of file diff --git a/Passepartout/App/fastlane/tvos/metadata/pl/marketing_url.txt b/Passepartout/App/fastlane/tvos/metadata/pl/marketing_url.txt new file mode 120000 index 00000000..f09f7b45 --- /dev/null +++ b/Passepartout/App/fastlane/tvos/metadata/pl/marketing_url.txt @@ -0,0 +1 @@ +../../../ios/metadata/pl/marketing_url.txt \ No newline at end of file diff --git a/Passepartout/App/fastlane/tvos/metadata/pl/name.txt b/Passepartout/App/fastlane/tvos/metadata/pl/name.txt new file mode 120000 index 00000000..4cad052e --- /dev/null +++ b/Passepartout/App/fastlane/tvos/metadata/pl/name.txt @@ -0,0 +1 @@ +../../../ios/metadata/pl/name.txt \ No newline at end of file diff --git a/Passepartout/App/fastlane/tvos/metadata/pl/privacy_url.txt b/Passepartout/App/fastlane/tvos/metadata/pl/privacy_url.txt new file mode 120000 index 00000000..6d552b46 --- /dev/null +++ b/Passepartout/App/fastlane/tvos/metadata/pl/privacy_url.txt @@ -0,0 +1 @@ +../../../ios/metadata/pl/privacy_url.txt \ No newline at end of file diff --git a/Passepartout/App/fastlane/tvos/metadata/pl/promotional_text.txt b/Passepartout/App/fastlane/tvos/metadata/pl/promotional_text.txt new file mode 120000 index 00000000..205f560b --- /dev/null +++ b/Passepartout/App/fastlane/tvos/metadata/pl/promotional_text.txt @@ -0,0 +1 @@ +../../../ios/metadata/pl/promotional_text.txt \ No newline at end of file diff --git a/Passepartout/App/fastlane/tvos/metadata/pl/subtitle.txt b/Passepartout/App/fastlane/tvos/metadata/pl/subtitle.txt new file mode 120000 index 00000000..fef06411 --- /dev/null +++ b/Passepartout/App/fastlane/tvos/metadata/pl/subtitle.txt @@ -0,0 +1 @@ +../../../ios/metadata/pl/subtitle.txt \ No newline at end of file diff --git a/Passepartout/App/fastlane/tvos/metadata/pl/support_url.txt b/Passepartout/App/fastlane/tvos/metadata/pl/support_url.txt new file mode 120000 index 00000000..f252f8f9 --- /dev/null +++ b/Passepartout/App/fastlane/tvos/metadata/pl/support_url.txt @@ -0,0 +1 @@ +../../../ios/metadata/pl/support_url.txt \ No newline at end of file diff --git a/Passepartout/App/fastlane/tvos/metadata/primary_category.txt b/Passepartout/App/fastlane/tvos/metadata/primary_category.txt new file mode 120000 index 00000000..53417145 --- /dev/null +++ b/Passepartout/App/fastlane/tvos/metadata/primary_category.txt @@ -0,0 +1 @@ +../../ios/metadata/primary_category.txt \ No newline at end of file diff --git a/Passepartout/App/fastlane/tvos/metadata/primary_first_sub_category.txt b/Passepartout/App/fastlane/tvos/metadata/primary_first_sub_category.txt new file mode 120000 index 00000000..f671236a --- /dev/null +++ b/Passepartout/App/fastlane/tvos/metadata/primary_first_sub_category.txt @@ -0,0 +1 @@ +../../ios/metadata/primary_first_sub_category.txt \ No newline at end of file diff --git a/Passepartout/App/fastlane/tvos/metadata/primary_second_sub_category.txt b/Passepartout/App/fastlane/tvos/metadata/primary_second_sub_category.txt new file mode 120000 index 00000000..a94c03a0 --- /dev/null +++ b/Passepartout/App/fastlane/tvos/metadata/primary_second_sub_category.txt @@ -0,0 +1 @@ +../../ios/metadata/primary_second_sub_category.txt \ No newline at end of file diff --git a/Passepartout/App/fastlane/tvos/metadata/pt-BR/apple_tv_privacy_policy.txt b/Passepartout/App/fastlane/tvos/metadata/pt-BR/apple_tv_privacy_policy.txt new file mode 120000 index 00000000..db021f29 --- /dev/null +++ b/Passepartout/App/fastlane/tvos/metadata/pt-BR/apple_tv_privacy_policy.txt @@ -0,0 +1 @@ +../../../ios/metadata/en-US/apple_tv_privacy_policy.txt \ No newline at end of file diff --git a/Passepartout/App/fastlane/tvos/metadata/pt-BR/description.txt b/Passepartout/App/fastlane/tvos/metadata/pt-BR/description.txt new file mode 100644 index 00000000..518fea21 --- /dev/null +++ b/Passepartout/App/fastlane/tvos/metadata/pt-BR/description.txt @@ -0,0 +1,3 @@ +Passepartout é um cliente inteligente de VPN perfeitamente integrado com Apple. Agora pode partilhar os seus perfis VPN a partir de aplicações iOS e macOS com a sua Apple TV, também de forma segura graças à encriptação completa da CloudKit. + +Passepartout possui código aberto: https://github.com/passepartoutvpn diff --git a/Passepartout/App/fastlane/tvos/metadata/pt-BR/keywords.txt b/Passepartout/App/fastlane/tvos/metadata/pt-BR/keywords.txt new file mode 120000 index 00000000..1320138f --- /dev/null +++ b/Passepartout/App/fastlane/tvos/metadata/pt-BR/keywords.txt @@ -0,0 +1 @@ +../../../ios/metadata/pt-BR/keywords.txt \ No newline at end of file diff --git a/Passepartout/App/fastlane/tvos/metadata/pt-BR/marketing_url.txt b/Passepartout/App/fastlane/tvos/metadata/pt-BR/marketing_url.txt new file mode 120000 index 00000000..cffad23d --- /dev/null +++ b/Passepartout/App/fastlane/tvos/metadata/pt-BR/marketing_url.txt @@ -0,0 +1 @@ +../../../ios/metadata/pt-BR/marketing_url.txt \ No newline at end of file diff --git a/Passepartout/App/fastlane/tvos/metadata/pt-BR/name.txt b/Passepartout/App/fastlane/tvos/metadata/pt-BR/name.txt new file mode 120000 index 00000000..57d9b60e --- /dev/null +++ b/Passepartout/App/fastlane/tvos/metadata/pt-BR/name.txt @@ -0,0 +1 @@ +../../../ios/metadata/pt-BR/name.txt \ No newline at end of file diff --git a/Passepartout/App/fastlane/tvos/metadata/pt-BR/privacy_url.txt b/Passepartout/App/fastlane/tvos/metadata/pt-BR/privacy_url.txt new file mode 120000 index 00000000..8121e236 --- /dev/null +++ b/Passepartout/App/fastlane/tvos/metadata/pt-BR/privacy_url.txt @@ -0,0 +1 @@ +../../../ios/metadata/pt-BR/privacy_url.txt \ No newline at end of file diff --git a/Passepartout/App/fastlane/tvos/metadata/pt-BR/promotional_text.txt b/Passepartout/App/fastlane/tvos/metadata/pt-BR/promotional_text.txt new file mode 120000 index 00000000..0219a69c --- /dev/null +++ b/Passepartout/App/fastlane/tvos/metadata/pt-BR/promotional_text.txt @@ -0,0 +1 @@ +../../../ios/metadata/pt-BR/promotional_text.txt \ No newline at end of file diff --git a/Passepartout/App/fastlane/tvos/metadata/pt-BR/subtitle.txt b/Passepartout/App/fastlane/tvos/metadata/pt-BR/subtitle.txt new file mode 120000 index 00000000..952b8b94 --- /dev/null +++ b/Passepartout/App/fastlane/tvos/metadata/pt-BR/subtitle.txt @@ -0,0 +1 @@ +../../../ios/metadata/pt-BR/subtitle.txt \ No newline at end of file diff --git a/Passepartout/App/fastlane/tvos/metadata/pt-BR/support_url.txt b/Passepartout/App/fastlane/tvos/metadata/pt-BR/support_url.txt new file mode 120000 index 00000000..377fefd6 --- /dev/null +++ b/Passepartout/App/fastlane/tvos/metadata/pt-BR/support_url.txt @@ -0,0 +1 @@ +../../../ios/metadata/pt-BR/support_url.txt \ No newline at end of file diff --git a/Passepartout/App/fastlane/tvos/metadata/ru/apple_tv_privacy_policy.txt b/Passepartout/App/fastlane/tvos/metadata/ru/apple_tv_privacy_policy.txt new file mode 120000 index 00000000..db021f29 --- /dev/null +++ b/Passepartout/App/fastlane/tvos/metadata/ru/apple_tv_privacy_policy.txt @@ -0,0 +1 @@ +../../../ios/metadata/en-US/apple_tv_privacy_policy.txt \ No newline at end of file diff --git a/Passepartout/App/fastlane/tvos/metadata/ru/description.txt b/Passepartout/App/fastlane/tvos/metadata/ru/description.txt new file mode 100644 index 00000000..9652e86f --- /dev/null +++ b/Passepartout/App/fastlane/tvos/metadata/ru/description.txt @@ -0,0 +1,3 @@ +Passepartout — это умный VPN-клиент, идеально интегрированный с платформами Apple. Теперь вы можете безопасно делиться своими профилями VPN из приложений iOS и macOS на Apple TV благодаря сквозному шифрованию CloudKit. + +У Passepartout открытый исходный код: https://github.com/passepartoutvpn diff --git a/Passepartout/App/fastlane/tvos/metadata/ru/keywords.txt b/Passepartout/App/fastlane/tvos/metadata/ru/keywords.txt new file mode 120000 index 00000000..77caf16d --- /dev/null +++ b/Passepartout/App/fastlane/tvos/metadata/ru/keywords.txt @@ -0,0 +1 @@ +../../../ios/metadata/ru/keywords.txt \ No newline at end of file diff --git a/Passepartout/App/fastlane/tvos/metadata/ru/marketing_url.txt b/Passepartout/App/fastlane/tvos/metadata/ru/marketing_url.txt new file mode 120000 index 00000000..8f4a4555 --- /dev/null +++ b/Passepartout/App/fastlane/tvos/metadata/ru/marketing_url.txt @@ -0,0 +1 @@ +../../../ios/metadata/ru/marketing_url.txt \ No newline at end of file diff --git a/Passepartout/App/fastlane/tvos/metadata/ru/name.txt b/Passepartout/App/fastlane/tvos/metadata/ru/name.txt new file mode 120000 index 00000000..95a91e6f --- /dev/null +++ b/Passepartout/App/fastlane/tvos/metadata/ru/name.txt @@ -0,0 +1 @@ +../../../ios/metadata/ru/name.txt \ No newline at end of file diff --git a/Passepartout/App/fastlane/tvos/metadata/ru/privacy_url.txt b/Passepartout/App/fastlane/tvos/metadata/ru/privacy_url.txt new file mode 120000 index 00000000..15aa6090 --- /dev/null +++ b/Passepartout/App/fastlane/tvos/metadata/ru/privacy_url.txt @@ -0,0 +1 @@ +../../../ios/metadata/ru/privacy_url.txt \ No newline at end of file diff --git a/Passepartout/App/fastlane/tvos/metadata/ru/promotional_text.txt b/Passepartout/App/fastlane/tvos/metadata/ru/promotional_text.txt new file mode 120000 index 00000000..4dfc0690 --- /dev/null +++ b/Passepartout/App/fastlane/tvos/metadata/ru/promotional_text.txt @@ -0,0 +1 @@ +../../../ios/metadata/ru/promotional_text.txt \ No newline at end of file diff --git a/Passepartout/App/fastlane/tvos/metadata/ru/subtitle.txt b/Passepartout/App/fastlane/tvos/metadata/ru/subtitle.txt new file mode 120000 index 00000000..04a61e98 --- /dev/null +++ b/Passepartout/App/fastlane/tvos/metadata/ru/subtitle.txt @@ -0,0 +1 @@ +../../../ios/metadata/ru/subtitle.txt \ No newline at end of file diff --git a/Passepartout/App/fastlane/tvos/metadata/ru/support_url.txt b/Passepartout/App/fastlane/tvos/metadata/ru/support_url.txt new file mode 120000 index 00000000..fce8a988 --- /dev/null +++ b/Passepartout/App/fastlane/tvos/metadata/ru/support_url.txt @@ -0,0 +1 @@ +../../../ios/metadata/ru/support_url.txt \ No newline at end of file diff --git a/Passepartout/App/fastlane/tvos/metadata/secondary_category.txt b/Passepartout/App/fastlane/tvos/metadata/secondary_category.txt new file mode 120000 index 00000000..c45a96c9 --- /dev/null +++ b/Passepartout/App/fastlane/tvos/metadata/secondary_category.txt @@ -0,0 +1 @@ +../../ios/metadata/secondary_category.txt \ No newline at end of file diff --git a/Passepartout/App/fastlane/tvos/metadata/secondary_first_sub_category.txt b/Passepartout/App/fastlane/tvos/metadata/secondary_first_sub_category.txt new file mode 120000 index 00000000..99aef6d9 --- /dev/null +++ b/Passepartout/App/fastlane/tvos/metadata/secondary_first_sub_category.txt @@ -0,0 +1 @@ +../../ios/metadata/secondary_first_sub_category.txt \ No newline at end of file diff --git a/Passepartout/App/fastlane/tvos/metadata/secondary_second_sub_category.txt b/Passepartout/App/fastlane/tvos/metadata/secondary_second_sub_category.txt new file mode 120000 index 00000000..289d86cf --- /dev/null +++ b/Passepartout/App/fastlane/tvos/metadata/secondary_second_sub_category.txt @@ -0,0 +1 @@ +../../ios/metadata/secondary_second_sub_category.txt \ No newline at end of file diff --git a/Passepartout/App/fastlane/tvos/metadata/sv/apple_tv_privacy_policy.txt b/Passepartout/App/fastlane/tvos/metadata/sv/apple_tv_privacy_policy.txt new file mode 120000 index 00000000..db021f29 --- /dev/null +++ b/Passepartout/App/fastlane/tvos/metadata/sv/apple_tv_privacy_policy.txt @@ -0,0 +1 @@ +../../../ios/metadata/en-US/apple_tv_privacy_policy.txt \ No newline at end of file diff --git a/Passepartout/App/fastlane/tvos/metadata/sv/description.txt b/Passepartout/App/fastlane/tvos/metadata/sv/description.txt new file mode 100644 index 00000000..50c21dd0 --- /dev/null +++ b/Passepartout/App/fastlane/tvos/metadata/sv/description.txt @@ -0,0 +1,3 @@ +Passepartout är en smart VPN-klient som är helt integrerad med Apples plattformar. Du kan nu dela dina VPN-profiler från iOS- och macOS-apparna med din Apple TV på ett säkert sätt tack vare end-to-end-kryptering med CloudKit. + +Passepartout är öppen källkod: https://github.com/passepartoutvpn diff --git a/Passepartout/App/fastlane/tvos/metadata/sv/keywords.txt b/Passepartout/App/fastlane/tvos/metadata/sv/keywords.txt new file mode 120000 index 00000000..09c86041 --- /dev/null +++ b/Passepartout/App/fastlane/tvos/metadata/sv/keywords.txt @@ -0,0 +1 @@ +../../../ios/metadata/sv/keywords.txt \ No newline at end of file diff --git a/Passepartout/App/fastlane/tvos/metadata/sv/marketing_url.txt b/Passepartout/App/fastlane/tvos/metadata/sv/marketing_url.txt new file mode 120000 index 00000000..b5ad9e05 --- /dev/null +++ b/Passepartout/App/fastlane/tvos/metadata/sv/marketing_url.txt @@ -0,0 +1 @@ +../../../ios/metadata/sv/marketing_url.txt \ No newline at end of file diff --git a/Passepartout/App/fastlane/tvos/metadata/sv/name.txt b/Passepartout/App/fastlane/tvos/metadata/sv/name.txt new file mode 120000 index 00000000..e660818d --- /dev/null +++ b/Passepartout/App/fastlane/tvos/metadata/sv/name.txt @@ -0,0 +1 @@ +../../../ios/metadata/sv/name.txt \ No newline at end of file diff --git a/Passepartout/App/fastlane/tvos/metadata/sv/privacy_url.txt b/Passepartout/App/fastlane/tvos/metadata/sv/privacy_url.txt new file mode 120000 index 00000000..7d9ea64e --- /dev/null +++ b/Passepartout/App/fastlane/tvos/metadata/sv/privacy_url.txt @@ -0,0 +1 @@ +../../../ios/metadata/sv/privacy_url.txt \ No newline at end of file diff --git a/Passepartout/App/fastlane/tvos/metadata/sv/promotional_text.txt b/Passepartout/App/fastlane/tvos/metadata/sv/promotional_text.txt new file mode 120000 index 00000000..b6958df6 --- /dev/null +++ b/Passepartout/App/fastlane/tvos/metadata/sv/promotional_text.txt @@ -0,0 +1 @@ +../../../ios/metadata/sv/promotional_text.txt \ No newline at end of file diff --git a/Passepartout/App/fastlane/tvos/metadata/sv/subtitle.txt b/Passepartout/App/fastlane/tvos/metadata/sv/subtitle.txt new file mode 120000 index 00000000..0c89fc49 --- /dev/null +++ b/Passepartout/App/fastlane/tvos/metadata/sv/subtitle.txt @@ -0,0 +1 @@ +../../../ios/metadata/sv/subtitle.txt \ No newline at end of file diff --git a/Passepartout/App/fastlane/tvos/metadata/sv/support_url.txt b/Passepartout/App/fastlane/tvos/metadata/sv/support_url.txt new file mode 120000 index 00000000..502dfbe9 --- /dev/null +++ b/Passepartout/App/fastlane/tvos/metadata/sv/support_url.txt @@ -0,0 +1 @@ +../../../ios/metadata/sv/support_url.txt \ No newline at end of file diff --git a/Passepartout/App/fastlane/tvos/screenshots/en-US/tv-01.png b/Passepartout/App/fastlane/tvos/screenshots/en-US/tv-01.png new file mode 100644 index 00000000..51f773d2 Binary files /dev/null and b/Passepartout/App/fastlane/tvos/screenshots/en-US/tv-01.png differ diff --git a/Passepartout/App/fastlane/tvos/screenshots/en-US/tv-02.png b/Passepartout/App/fastlane/tvos/screenshots/en-US/tv-02.png new file mode 100644 index 00000000..7f17966b Binary files /dev/null and b/Passepartout/App/fastlane/tvos/screenshots/en-US/tv-02.png differ diff --git a/Passepartout/App/fastlane/tvos/screenshots/en-US/tv-03.png b/Passepartout/App/fastlane/tvos/screenshots/en-US/tv-03.png new file mode 100644 index 00000000..65d4c267 Binary files /dev/null and b/Passepartout/App/fastlane/tvos/screenshots/en-US/tv-03.png differ diff --git a/Passepartout/App/fastlane/tvos/screenshots/en-US/tv-04.png b/Passepartout/App/fastlane/tvos/screenshots/en-US/tv-04.png new file mode 100644 index 00000000..6d84dd78 Binary files /dev/null and b/Passepartout/App/fastlane/tvos/screenshots/en-US/tv-04.png differ diff --git a/Passepartout/App/fr.lproj/Localizable.strings b/Passepartout/App/fr.lproj/Localizable.strings index c45280b0..7e662333 100644 --- a/Passepartout/App/fr.lproj/Localizable.strings +++ b/Passepartout/App/fr.lproj/Localizable.strings @@ -72,7 +72,7 @@ "global.strings.authentication" = "Authentification"; "global.messages.unlock_app" = "Passepartout verrouillé"; "global.messages.email_not_configured" = "Aucun compte courriel n'est configuré."; -"global.messages.share" = "Passepartout est un client OpenVPN / WireGuard simple d'utilisation et open source pour iOS et macOS"; +"global.messages.share" = "Passepartout est un client OpenVPN / WireGuard simple d'utilisation et open source pour iOS et macOS"; // FIXME: l10n, Apple platforms "global.alerts.buttons.remind" = "Me rappeler plus tard"; "global.alerts.buttons.never" = "Ne pas me redemander"; @@ -83,6 +83,7 @@ "global.errors.missing_account" = "Compte manquant"; "global.errors.missing_provider_server" = "Emplacement manquant"; "global.errors.missing_provider_preset" = "Préréglage manquant"; +"global.errors.tunnel_expired" = "Connexion expirée"; /* MARK: Menus */ @@ -132,6 +133,7 @@ /* MARK: OrganizerView */ "organizer.sections.active" = "En utilisation"; +"organizer.sections.tv.profiles_list.header.p1" = "Ouvrez Passepartout sur votre appareil iOS ou macOS et activez la bascule « Apple TV » d'un profil pour le faire apparaître ici."; /* MARK: OrganizerView */ "organizer.empty.no_profiles" = "Aucun profil"; @@ -163,6 +165,9 @@ "profile.sections.vpn.footer" = "La connection sera établie lorsque nécessaire."; "profile.sections.status.header" = "Connection"; "profile.sections.provider_infrastructure.footer" = "Mis à jour : %@."; +"profile.sections.tv.footer.encryption" = "Les profils sont cryptés et rendus disponible pour votre Apple TV via iCloud."; +"profile.sections.tv.footer.restricted.p1" = "Cependant, la connexion expirera après %d minutes."; +"profile.sections.tv.footer.restricted.p2" = "Achetez pour lever la restriction."; "profile.sections.vpn_survives_sleep.footer" = "Désactiver pour augmenter l'autonomie de la batterie, au dépends de la rapidité au réveil pour la reconnection."; "profile.sections.vpn_resolves_hostname.footer" = "Préféré dans la plus part des réseaux et requis dans certains réseaux IPv6. Désactiver lorsque le DNS est bloqué ou pour augmenter la rapidité des négociations lorsque le DNS est lent à répondre."; "profile.sections.feedback.header" = "Commentaires"; @@ -178,6 +183,8 @@ "profile.items.only_shows_favorites.caption" = "Afficher uniquement les emplacements favoris"; "profile.items.vpn_survives_sleep.caption" = "Garder actif lors de la veille"; "profile.items.vpn_resolves_hostname.caption" = "Résoudre le nom d'hôte du serveur"; +"profile.items.tv_sharing.caption.limited" = "Limité à %d minutes."; +"profile.items.expires_at.caption" = "Expiration"; "profile.alerts.rename.title" = "Renommer le profile"; "profile.alerts.reconnect_vpn.message" = "Voulez-vous reconnecter le VPN?"; @@ -323,6 +330,7 @@ "paywall.items.full_version.extra_description" = "Tous les fournisseurs (incluant les prochains)\n%@"; "paywall.items.restore.title" = "Restaurer les achats"; "paywall.items.restore.description" = "Si vous avez acheté l'application ou une fonctionnalité dans le passé, vous pouvez restaurer les achats et ce message ne s'affichera plus."; +"paywall.alerts.purchase.appletv.success.message" = "Merci ! La limite de temps sera levée dès que iCloud se mettra à jour. Patientez quelques instants, puis relancez la connexion sur l'application TV."; /* MARK: DonateView */ diff --git a/Passepartout/App/it.lproj/Localizable.strings b/Passepartout/App/it.lproj/Localizable.strings index 5d0ce0c8..f0aa6462 100644 --- a/Passepartout/App/it.lproj/Localizable.strings +++ b/Passepartout/App/it.lproj/Localizable.strings @@ -72,7 +72,7 @@ "global.strings.authentication" = "Autenticazione"; "global.messages.unlock_app" = "Passepartout è bloccata"; "global.messages.email_not_configured" = "Nessun account e-mail configurato."; -"global.messages.share" = "Passepartout è un client OpenVPN / WireGuard user-friendly ed open source per iOS e macOS"; +"global.messages.share" = "Passepartout è un client OpenVPN / WireGuard user-friendly ed open source per iOS e macOS"; // FIXME: l10n, Apple platforms "global.alerts.buttons.remind" = "Ricordami più tardi"; "global.alerts.buttons.never" = "Non chiedere più"; @@ -83,6 +83,7 @@ "global.errors.missing_account" = "Credenziali mancanti"; "global.errors.missing_provider_server" = "Regione mancante"; "global.errors.missing_provider_preset" = "Preset mancante"; +"global.errors.tunnel_expired" = "Connessione scaduta"; /* MARK: Menus */ @@ -132,6 +133,7 @@ /* MARK: OrganizerView */ "organizer.sections.active" = "In uso"; +"organizer.sections.tv.profiles_list.header.p1" = "Apri Passepartout sul tuo dispositivo iOS o macOS ed abilita lo switch \"Apple TV\" di un profilo per mostrarlo qui in basso."; /* MARK: OrganizerView */ "organizer.empty.no_profiles" = "Nessun profilo"; @@ -163,6 +165,9 @@ "profile.sections.vpn.footer" = "La connessione sarà stabilita ogni volta che è necessario."; "profile.sections.status.header" = "Connessione"; "profile.sections.provider_infrastructure.footer" = "Ultimo aggiornamento: %@."; +"profile.sections.tv.footer.encryption" = "I profili sono criptati e resi disponibili sulla tua Apple TV attraverso iCloud."; +"profile.sections.tv.footer.restricted.p1" = "Tuttavia, la connessione scadrà fra %d minuti."; +"profile.sections.tv.footer.restricted.p2" = "Acquista per rimuovere la restrizione."; "profile.sections.vpn_survives_sleep.footer" = "Disabilita per migliorare il consumo della batteria, a discapito di rallentamenti occasionali causati dalle riconnessioni."; "profile.sections.vpn_resolves_hostname.footer" = "Preferibile nella maggior parte delle reti e necessario in alcune reti IPv6. Disabilita dove il DNS è bloccato, o per velocizzare la negoziazione quando il DNS tarda a rispondere."; "profile.sections.feedback.header" = "Feedback"; @@ -178,6 +183,8 @@ "profile.items.only_shows_favorites.caption" = "Mostra solo le posizioni preferite"; "profile.items.vpn_survives_sleep.caption" = "Mantieni attivo in sleep"; "profile.items.vpn_resolves_hostname.caption" = "Risolvi hostname del server"; +"profile.items.tv_sharing.caption.limited" = "Limitato a %d minuti."; +"profile.items.expires_at.caption" = "Scadenza"; "profile.alerts.rename.title" = "Rinomina profilo"; "profile.alerts.reconnect_vpn.message" = "Vuoi riconnetterti alla VPN?"; @@ -323,6 +330,7 @@ "paywall.items.full_version.extra_description" = "Tutti i provider (inclusi quelli futuri)\n%@"; "paywall.items.restore.title" = "Ripristina acquisti"; "paywall.items.restore.description" = "Se hai comprato quest'applicazione o funzionalità in precedenza, puoi ripristinare i tuoi acquisti in modo che questa schermata non compaia più."; +"paywall.alerts.purchase.appletv.success.message" = "Grazie! Il limite di tempo sarà rimosso non appena iCloud si sarà sincronizzato. Aspetta un momento, dopodiché riavvia la connessione sull'app della TV."; /* MARK: DonateView */ diff --git a/Passepartout/App/nl.lproj/Localizable.strings b/Passepartout/App/nl.lproj/Localizable.strings index ab0b6172..088dc315 100644 --- a/Passepartout/App/nl.lproj/Localizable.strings +++ b/Passepartout/App/nl.lproj/Localizable.strings @@ -72,7 +72,7 @@ "global.strings.authentication" = "Authenticatie"; "global.messages.unlock_app" = "Hoofdsleutel is vergrendeld"; "global.messages.email_not_configured" = "Er is geen email adres geconfigureerd."; -"global.messages.share" = "Passepartout is een gebruiksvriendelijke open source OpenVPN / WireGuard client voor iOS en macOS"; +"global.messages.share" = "Passepartout is een gebruiksvriendelijke open source OpenVPN / WireGuard client voor iOS en macOS"; // FIXME: l10n, Apple platforms "global.alerts.buttons.remind" = "Herinner me later"; "global.alerts.buttons.never" = "Vraag dit niet meer"; @@ -83,6 +83,7 @@ "global.errors.missing_account" = "Ontbrekend account"; "global.errors.missing_provider_server" = "Ontbrekende locatie"; "global.errors.missing_provider_preset" = "Ontbrekende voorkeur"; +"global.errors.tunnel_expired" = "Verbinding verlopen"; /* MARK: Menus */ @@ -132,6 +133,7 @@ /* MARK: OrganizerView */ "organizer.sections.active" = "In gebruik"; +"organizer.sections.tv.profiles_list.header.p1" = "Open Passepartout op je iOS- of macOS-apparaat en activeer de 'Apple TV'-schakelaar van een profiel om dit hier weer te geven."; /* MARK: OrganizerView */ "organizer.empty.no_profiles" = "Geen profielen"; @@ -163,6 +165,9 @@ "profile.sections.vpn.footer" = "De verbinding zal worden gestart wanneer nodig."; "profile.sections.status.header" = "Verbinding"; "profile.sections.provider_infrastructure.footer" = "Laatste update was op %@."; +"profile.sections.tv.footer.encryption" = "Profielen zijn gecodeerd en via iCloud beschikbaar voor je Apple TV."; +"profile.sections.tv.footer.restricted.p1" = "Maar de verbinding verloopt na %d minuten."; +"profile.sections.tv.footer.restricted.p2" = "Doe een aankoop om de beperking op te heffen."; "profile.sections.vpn_survives_sleep.footer" = "Uitschakelen om het batterijverbruik te verbeteren, ten koste van incidentele vertragingen als gevolg van het opnieuw opstarten na wake-up."; "profile.sections.vpn_resolves_hostname.footer" = "Voorkeur om dit aan te zetten voor de meeste netwerken en vereist in sommige IPv6-netwerken. Uitschakelen waar DNS wordt geblokkeerd, of om de onderhandelingen te versnellen wanneer DNS traag reageert."; "profile.sections.feedback.header" = "Terugkoppeling"; @@ -178,6 +183,8 @@ "profile.items.only_shows_favorites.caption" = "Alleen favoriete locaties weergeven"; "profile.items.vpn_survives_sleep.caption" = "Actief tijdens slaapstand"; "profile.items.vpn_resolves_hostname.caption" = "Haal de naam van de host op"; +"profile.items.tv_sharing.caption.limited" = "Beperkt tot %d minuten"; +"profile.items.expires_at.caption" = "Verlooptijd"; "profile.alerts.rename.title" = "Profiel hernoemen"; "profile.alerts.reconnect_vpn.message" = "Opnieuw verbinding maken met de VPN?"; @@ -323,6 +330,7 @@ "paywall.items.full_version.extra_description" = "Alle providers (inclusief toekomstige)\n%@"; "paywall.items.restore.title" = "Herstel Aankopen"; "paywall.items.restore.description" = "Als u deze app of functie in het verleden heeft gekocht, kunt u uw aankopen herstellen en wordt dit scherm niet meer getoond."; +"paywall.alerts.purchase.appletv.success.message" = "Bedankt! De tijdsbeperking wordt opgeheven zodra iCloud dit ziet. Wacht even en herstart dan de verbinding op de tv-app."; /* MARK: DonateView */ diff --git a/Passepartout/App/pl.lproj/Localizable.strings b/Passepartout/App/pl.lproj/Localizable.strings index ba8ff0fa..d2db51a1 100644 --- a/Passepartout/App/pl.lproj/Localizable.strings +++ b/Passepartout/App/pl.lproj/Localizable.strings @@ -72,7 +72,7 @@ "global.strings.authentication" = "Uwierzytelnianie"; "global.messages.unlock_app" = "Passepartout jest zablokowany"; "global.messages.email_not_configured" = "Adres e-mail nie jest skonfigurowany."; -"global.messages.share" = "Passepartout to klient OpenVPN / WireGuard, przyjazny użytkownikowi, open-source, stworzony dla iOS i macOS"; +"global.messages.share" = "Passepartout to klient OpenVPN / WireGuard, przyjazny użytkownikowi, open-source, stworzony dla iOS i macOS"; // FIXME: l10n, Apple platforms "global.alerts.buttons.remind" = "Przypomnij mi później"; "global.alerts.buttons.never" = "Nie przypominaj"; @@ -83,6 +83,7 @@ "global.errors.missing_account" = "Brakujące konto"; "global.errors.missing_provider_server" = "Brakująca lokalizacja"; "global.errors.missing_provider_preset" = "Brakujący preset"; +"global.errors.tunnel_expired" = "Połączenie wygasło"; /* MARK: Menus */ @@ -132,6 +133,7 @@ /* MARK: OrganizerView */ "organizer.sections.active" = "W użyciu"; +"organizer.sections.tv.profiles_list.header.p1" = "Otwórz Passepartout na urządzeniu z systemem iOS lub macOS i włącz przełącznik \"Apple TV\" profilu, aby pojawił się tutaj."; /* MARK: OrganizerView */ "organizer.empty.no_profiles" = "Brak profili"; @@ -163,6 +165,9 @@ "profile.sections.vpn.footer" = "Połączenie zostanie nawiązane zgodnie z ustawieniami."; "profile.sections.status.header" = "Połączenie"; "profile.sections.provider_infrastructure.footer" = "Ostatnio aktualizowane %@."; +"profile.sections.tv.footer.encryption" = "Profile są szyfrowane i udostępniane Apple TV za pośrednictwem iCloud."; +"profile.sections.tv.footer.restricted.p1" = "Połączenie wygaśnie jednak po %d minutach."; +"profile.sections.tv.footer.restricted.p2" = "Zakup w celu zniesienia ograniczenia."; "profile.sections.vpn_survives_sleep.footer" = "Wyłącz dla mniejszego zużycia baterii kosztem wolniejszego działania spowodowanego ponownym połączeniem przy wybudzeniu urządzenia."; "profile.sections.vpn_resolves_hostname.footer" = "Preferowane w większości sieci i potrzebne w niektórych sieciach IPv6. Wyłącz kiedy DNS jest zablokowane, lub żeby przyspieszyć ustanawianie połączenia gdy DNS jest zbyt wolne."; "profile.sections.feedback.header" = "Wyraź opinię"; @@ -178,6 +183,8 @@ "profile.items.only_shows_favorites.caption" = "Pokazuj tylko ulubione lokalizacje"; "profile.items.vpn_survives_sleep.caption" = "Utrzymuj połączenie przy zablokowanym ekranie"; "profile.items.vpn_resolves_hostname.caption" = "Rozwiązuj nazwy hostów usługodawcy"; +"profile.items.tv_sharing.caption.limited" = "Ograniczenie do %d minut"; +"profile.items.expires_at.caption" = "Wygaśnięcie"; "profile.alerts.rename.title" = "Zmień nazwę profilu"; "profile.alerts.reconnect_vpn.message" = "Czy chcesz połączyć się ponownie z VPN?"; @@ -323,6 +330,7 @@ "paywall.items.full_version.extra_description" = "Wszyscy usługodawcy (włączając przyszłych)\n%@"; "paywall.items.restore.title" = "Przywróć zakup"; "paywall.items.restore.description" = "Jeśli kupiłeś tą aplikację lub funkcję wcześniej, możesz przywrócić swoje zakupy i ten ekran nie będzie wyświetlony ponownie."; +"paywall.alerts.purchase.appletv.success.message" = "Dziękujemy! Limit czasu zostanie zniesiony, gdy tylko iCloud zaktualizuje dane. Odczekaj chwilę, a następnie ponownie uruchom połączenie w aplikacji TV."; /* MARK: DonateView */ diff --git a/Passepartout/App/pt.lproj/Localizable.strings b/Passepartout/App/pt.lproj/Localizable.strings index 6716c21d..37ee3505 100644 --- a/Passepartout/App/pt.lproj/Localizable.strings +++ b/Passepartout/App/pt.lproj/Localizable.strings @@ -72,7 +72,7 @@ "global.strings.authentication" = "Autenticação"; "global.messages.unlock_app" = "O Passepartout está bloqueado"; "global.messages.email_not_configured" = "Nenhuma conta de email configurada."; -"global.messages.share" = "Passepartout é um cliente OpenVPN / WireGuard fácil e open-source para iOS e macOS"; +"global.messages.share" = "Passepartout é um cliente OpenVPN / WireGuard fácil e open-source para iOS e macOS"; // FIXME: l10n, Apple platforms "global.alerts.buttons.remind" = "Lembrar-me depois"; "global.alerts.buttons.never" = "Não perguntar novamente"; @@ -83,6 +83,7 @@ "global.errors.missing_account" = "Conta em falta"; "global.errors.missing_provider_server" = "Localização em falta"; "global.errors.missing_provider_preset" = "Pré-definição em falta"; +"global.errors.tunnel_expired" = "A ligação expirou"; /* MARK: Menus */ @@ -132,6 +133,7 @@ /* MARK: OrganizerView */ "organizer.sections.active" = "Ativo"; +"organizer.sections.tv.profiles_list.header.p1" = "Abra o Passepartout no seu dispositivo iOS ou macOS e ative o botão \"Apple TV\" de um perfil para o fazer aparecer aqui."; /* MARK: OrganizerView */ "organizer.empty.no_profiles" = "Sem perfis"; @@ -163,6 +165,9 @@ "profile.sections.vpn.footer" = "A conexão será estabelecida assim que necessária."; "profile.sections.status.header" = "Conexão"; "profile.sections.provider_infrastructure.footer" = "Última atualização em %@."; +"profile.sections.tv.footer.encryption" = "Os perfis estão encriptados e estão disponíveis para a sua Apple TV através da iCloud."; +"profile.sections.tv.footer.restricted.p1" = "Todavia, a ligação vai perder a validade depois de %d minutos."; +"profile.sections.tv.footer.restricted.p2" = "Compre para largar a restrição."; "profile.sections.vpn_survives_sleep.footer" = "Desative para melhorar o consumo de bateria, o que poderá ocasionar queda de performance quando o restabelecimento de conexão for realizado."; "profile.sections.vpn_resolves_hostname.footer" = "Recomendado para maioria das redes e requirido em algumas redes IPv6. Desative se o DNS estiver bloqueado, ou para acelerar o DNS quando o mesmo está devagar."; "profile.sections.feedback.header" = "Feedback"; @@ -178,6 +183,8 @@ "profile.items.only_shows_favorites.caption" = "Mostrar apenas os locais preferidos"; "profile.items.vpn_survives_sleep.caption" = "Manter ativo em modo descanço"; "profile.items.vpn_resolves_hostname.caption" = "Resolver hostname do servidor"; +"profile.items.tv_sharing.caption.limited" = "Limitado a %d minutos"; +"profile.items.expires_at.caption" = "Validade"; "profile.alerts.rename.title" = "Renomear perfil"; "profile.alerts.reconnect_vpn.message" = "Deseja reconectar à VPN?"; @@ -323,6 +330,7 @@ "paywall.items.full_version.extra_description" = "Todos os provedores (incluindo os futuros)\n%@"; "paywall.items.restore.title" = "Restaurar compras"; "paywall.items.restore.description" = "Se você comprou este aplicativo ou recurso no passado, pode restaurar suas compras e essa tela não será exibida novamente."; +"paywall.alerts.purchase.appletv.success.message" = "Obrigado! O limite de tempo irá desaparecer assim que a iCloud seja atualizada. Espere um momento e depois reinicie a ligação na aplicação da TV."; /* MARK: DonateView */ diff --git a/Passepartout/App/ru.lproj/Localizable.strings b/Passepartout/App/ru.lproj/Localizable.strings index 66bd979d..f7bb1c47 100644 --- a/Passepartout/App/ru.lproj/Localizable.strings +++ b/Passepartout/App/ru.lproj/Localizable.strings @@ -72,7 +72,7 @@ "global.strings.authentication" = "Аутентификация"; "global.messages.unlock_app" = "Passepartout заблокирован"; "global.messages.email_not_configured" = "E-mail аккаунт не создан."; -"global.messages.share" = "Passepartout — это удобный в использовании клиент OpenVPN / WireGuard для iOS и macOS с открытым исходным кодом"; +"global.messages.share" = "Passepartout — это удобный в использовании клиент OpenVPN / WireGuard для iOS и macOS с открытым исходным кодом"; // FIXME: l10n, Apple platforms "global.alerts.buttons.remind" = "Напомнить позже"; "global.alerts.buttons.never" = "Больше не спрашивать"; @@ -83,6 +83,7 @@ "global.errors.missing_account" = "Отсутствует учетная запись"; "global.errors.missing_provider_server" = "Отсутствует местоположение"; "global.errors.missing_provider_preset" = "Отсутствует пресет"; +"global.errors.tunnel_expired" = "Срок действия соединения истек"; /* MARK: Menus */ @@ -132,6 +133,7 @@ /* MARK: OrganizerView */ "organizer.sections.active" = "Используется"; +"organizer.sections.tv.profiles_list.header.p1" = "Откройте Passepartout на своем устройстве iOS или macOS и включите Apple TV в профиле, чтобы он появился здесь."; /* MARK: OrganizerView */ "organizer.empty.no_profiles" = "Нет профилей"; @@ -163,6 +165,9 @@ "profile.sections.vpn.footer" = "Соединение будет установлено при необходимости."; "profile.sections.status.header" = "Соединение"; "profile.sections.provider_infrastructure.footer" = "Последнее обновление %@."; +"profile.sections.tv.footer.encryption" = "Профили зашифрованы и доступны на вашем Apple TV через iCloud."; +"profile.sections.tv.footer.restricted.p1" = "Тем не менее, срок действия соединения истечет через %d мин."; +"profile.sections.tv.footer.restricted.p2" = "Совершите покупку, чтобы снять ограничение."; "profile.sections.vpn_survives_sleep.footer" = "Отключите для уменьшения расхода заряда аккумулятора, может привести к временным замедлениям в связи с повторным подключением после \"пробуждения\"."; "profile.sections.vpn_resolves_hostname.footer" = "Предпочтительно в большинстве сетей и необходимо в некоторых IPv6 сетях. Отключите если  DNS заблокирован, или для увеличения скорости в случае медленных ответов DNS."; "profile.sections.feedback.header" = "Отзыв"; @@ -178,6 +183,8 @@ "profile.items.only_shows_favorites.caption" = "Показывать только места из избранного"; "profile.items.vpn_survives_sleep.caption" = "Оставлять включенным во время сна"; "profile.items.vpn_resolves_hostname.caption" = "Разрешить имя хоста сервера"; +"profile.items.tv_sharing.caption.limited" = "Ограничено до %d мин."; +"profile.items.expires_at.caption" = "Срок действия"; "profile.alerts.rename.title" = "Переименовать профиль"; "profile.alerts.reconnect_vpn.message" = "Хотите заново подключиться к VPN?"; @@ -323,6 +330,7 @@ "paywall.items.full_version.extra_description" = "Все провайдеры (включая добавленных в будущем)\n%@"; "paywall.items.restore.title" = "Восстановить покупки"; "paywall.items.restore.description" = "Если Вы купили это приложение или совершили встроенные покупки в прошлом, вы можете восстановить ваши покупки, и этот баннер больше не появится."; +"paywall.alerts.purchase.appletv.success.message" = "Спасибо! Ограничение по времени будет снято, как только iCloud обновит данные. Перезапустите соединение через несколько секунд в приложении TV."; /* MARK: DonateView */ diff --git a/Passepartout/App/sv.lproj/Localizable.strings b/Passepartout/App/sv.lproj/Localizable.strings index efaa5ee2..7e8a845e 100644 --- a/Passepartout/App/sv.lproj/Localizable.strings +++ b/Passepartout/App/sv.lproj/Localizable.strings @@ -72,7 +72,7 @@ "global.strings.authentication" = "Autentisering"; "global.messages.unlock_app" = "Passepartout är låst"; "global.messages.email_not_configured" = "Inget e-postkonto är konfigurerat."; -"global.messages.share" = "Passepartout är en användarvänlig öppen källkod OpenVPN / WireGuard klient för iOS och macOS"; +"global.messages.share" = "Passepartout är en användarvänlig öppen källkod OpenVPN / WireGuard klient för iOS och macOS"; // FIXME: l10n, Apple platforms "global.alerts.buttons.remind" = "Påminn mig senare"; "global.alerts.buttons.never" = "Fråga inte igen"; @@ -83,6 +83,7 @@ "global.errors.missing_account" = "Konto saknas"; "global.errors.missing_provider_server" = "Plats saknas"; "global.errors.missing_provider_preset" = "Förinställning saknas"; +"global.errors.tunnel_expired" = "Anslutningen gick ut"; /* MARK: Menus */ @@ -132,6 +133,7 @@ /* MARK: OrganizerView */ "organizer.sections.active" = "Under användning"; +"organizer.sections.tv.profiles_list.header.p1" = "Öppna Passepartout på en iOS- eller macOS-enhet och slå på reglaget för \"Apple TV\" i en profil för att få den att synas här."; /* MARK: OrganizerView */ "organizer.empty.no_profiles" = "Inga profiler"; @@ -163,6 +165,9 @@ "profile.sections.vpn.footer" = "Anslutningen kommer att upprättas vid behov."; "profile.sections.status.header" = "Koppling"; "profile.sections.provider_infrastructure.footer" = "Senast uppdaterad på %@."; +"profile.sections.tv.footer.encryption" = "Profiler är krypterade och tillgängliggörs på din Apple TV via iCloud."; +"profile.sections.tv.footer.restricted.p1" = "Anslutningen bryts dock efter %d minuter."; +"profile.sections.tv.footer.restricted.p2" = "Köp för att slippa begränsningen."; "profile.sections.vpn_survives_sleep.footer" = "Inaktivera för att förbättra batterianvändningen, på bekostnad av tillfälliga avmattningar på grund av återuppkoppling."; "profile.sections.vpn_resolves_hostname.footer" = "Föredragna i de flesta nätverk och krävs i vissa IPv6-nätverk. Inaktivera var DNS blockeras eller för att påskynda förhandlingar när DNS är långsamt att svara."; "profile.sections.feedback.header" = "Feedback"; @@ -178,6 +183,8 @@ "profile.items.only_shows_favorites.caption" = "Visa endast favoritplatser"; "profile.items.vpn_survives_sleep.caption" = "Håll dig levande i sömnen"; "profile.items.vpn_resolves_hostname.caption" = "Lösa server värdnamn"; +"profile.items.tv_sharing.caption.limited" = "Begränsat till %d minuter"; +"profile.items.expires_at.caption" = "Utgång"; "profile.alerts.rename.title" = "Byt namn på profil"; "profile.alerts.reconnect_vpn.message" = "Vill du återansluta till VPN?"; @@ -323,6 +330,7 @@ "paywall.items.full_version.extra_description" = "Alla leverantörer (inklusive framtida)\n%@"; "paywall.items.restore.title" = "Återställ köp"; "paywall.items.restore.description" = "Om du köpte den här appen eller funktionen tidigare kan du återställa dina inköp och den här skärmen visas inte igen."; +"paywall.alerts.purchase.appletv.success.message" = "Tack! Tidsbegränsningen kommer att försvinna så snart iCloud hinner ikapp. Vänta en liten stund och starta sedan om anslutningen i TV-appen."; /* MARK: DonateView */ diff --git a/Passepartout/App/zh-Hans.lproj/Localizable.strings b/Passepartout/App/zh-Hans.lproj/Localizable.strings index 88010e7f..86db3011 100644 --- a/Passepartout/App/zh-Hans.lproj/Localizable.strings +++ b/Passepartout/App/zh-Hans.lproj/Localizable.strings @@ -72,7 +72,7 @@ "global.strings.authentication" = "身份验证"; "global.messages.unlock_app" = "Passepartout已被锁定"; "global.messages.email_not_configured" = "未配置e-mail账户。"; -"global.messages.share" = "Passepartout是适用于iOS和macOS操作系统的OpenVPN/WireGuard开源客户端,易于使用"; +"global.messages.share" = "Passepartout是适用于iOS和macOS操作系统的OpenVPN/WireGuard开源客户端,易于使用"; // FIXME: l10n, Apple platforms "global.alerts.buttons.remind" = "稍后提醒我"; "global.alerts.buttons.never" = "不要再问"; @@ -83,6 +83,7 @@ "global.errors.missing_account" = "缺少帐户"; "global.errors.missing_provider_server" = "缺少位置"; "global.errors.missing_provider_preset" = "缺少预设"; +"global.errors.tunnel_expired" = "连接已过期"; /* MARK: Menus */ @@ -132,6 +133,7 @@ /* MARK: OrganizerView */ "organizer.sections.active" = "使用中"; +"organizer.sections.tv.profiles_list.header.p1" = "在iOS或macOS设备上打开Passepartout,并启用个人资料下的“Apple TV”,使其显示在此处。"; /* MARK: OrganizerView */ "organizer.empty.no_profiles" = "没有配置"; @@ -163,6 +165,9 @@ "profile.sections.vpn.footer" = "必要时连接将会建立。"; "profile.sections.status.header" = "连接"; "profile.sections.provider_infrastructure.footer" = "最后在%@时更新"; +"profile.sections.tv.footer.encryption" = "用户资料经过加密,并通过iCloud共享至Apple TV。"; +"profile.sections.tv.footer.restricted.p1" = "不过,连接将在%d分钟后过期。"; +"profile.sections.tv.footer.restricted.p2" = "购买后限制即取消。"; "profile.sections.vpn_survives_sleep.footer" = "禁用以减少电池消耗,由于存在可能的唤醒时重连消耗。"; "profile.sections.vpn_resolves_hostname.footer" = "推荐在大部分的网络中打开,并要求在IPv6环境下。当DNS被阻断或相应缓慢时禁用。"; "profile.sections.feedback.header" = "反馈"; @@ -178,6 +183,8 @@ "profile.items.only_shows_favorites.caption" = "仅显示收藏的地点"; "profile.items.vpn_survives_sleep.caption" = "休眠时保持连接"; "profile.items.vpn_resolves_hostname.caption" = "解析服务器主机名"; +"profile.items.tv_sharing.caption.limited" = "最多%d分钟"; +"profile.items.expires_at.caption" = "过期"; "profile.alerts.rename.title" = "重命名配置"; "profile.alerts.reconnect_vpn.message" = "要重连VPN吗?"; @@ -323,6 +330,7 @@ "paywall.items.full_version.extra_description" = "所有的提供商(包括未来添加的)\n%@"; "paywall.items.restore.title" = "恢复购买"; "paywall.items.restore.description" = "如果你购买过此应用或其特征, 你可以恢复购买,此页面将不在显示。"; +"paywall.alerts.purchase.appletv.success.message" = "非常感谢!iCloud同步后,时间限制将立即取消。请稍等片刻,然后重新启动Apple TV应用程序的连接。"; /* MARK: DonateView */ diff --git a/Passepartout/AppShared/Constants/SwiftGen+Strings.swift b/Passepartout/AppShared/Constants/SwiftGen+Strings.swift index 18807c13..acc7e951 100644 --- a/Passepartout/AppShared/Constants/SwiftGen+Strings.swift +++ b/Passepartout/AppShared/Constants/SwiftGen+Strings.swift @@ -414,7 +414,7 @@ internal enum L10n { internal static let caption = L10n.tr("Localizable", "endpoint.wireguard.items.allowed_ip.caption", fallback: "Allowed IP") } internal enum Peer { - /// Peer + /// MARK: ProfileView -> EndpointView internal static let caption = L10n.tr("Localizable", "endpoint.wireguard.items.peer.caption", fallback: "Peer") } internal enum PresharedKey { @@ -442,6 +442,8 @@ internal enum L10n { internal static let missingProviderPreset = L10n.tr("Localizable", "global.errors.missing_provider_preset", fallback: "Missing preset") /// Missing location internal static let missingProviderServer = L10n.tr("Localizable", "global.errors.missing_provider_server", fallback: "Missing location") + /// Connection expired + internal static let tunnelExpired = L10n.tr("Localizable", "global.errors.tunnel_expired", fallback: "Connection expired") } internal enum Messages { /// No e-mail account is configured. @@ -650,6 +652,10 @@ internal enum L10n { /// MARK: ProfileView -> OnDemandView internal static let title = L10n.tr("Localizable", "on_demand.title", fallback: "On demand") internal enum Items { + internal enum Active { + /// Trust + internal static let caption = L10n.tr("Localizable", "on_demand.items.active.caption", fallback: "Trust") + } internal enum AddSsid { /// Add Wi-Fi internal static let caption = L10n.tr("Localizable", "on_demand.items.add_ssid.caption", fallback: "Add Wi-Fi") @@ -662,6 +668,10 @@ internal enum L10n { /// Cellular network internal static let caption = L10n.tr("Localizable", "on_demand.items.mobile.caption", fallback: "Cellular network") } + internal enum Policy { + /// Trust disables VPN + internal static let caption = L10n.tr("Localizable", "on_demand.items.policy.caption", fallback: "Trust disables VPN") + } } internal enum Policy { /// All networks @@ -714,11 +724,29 @@ internal enum L10n { internal enum Sections { /// MARK: OrganizerView internal static let active = L10n.tr("Localizable", "organizer.sections.active", fallback: "In use") + internal enum Tv { + internal enum ProfilesList { + internal enum Header { + /// Open Passepartout on your iOS or macOS device and enable the "Apple TV" toggle of a profile to make it appear here. + internal static let p1 = L10n.tr("Localizable", "organizer.sections.tv.profiles_list.header.p1", fallback: "Open Passepartout on your iOS or macOS device and enable the \"Apple TV\" toggle of a profile to make it appear here.") + } + } + } } } internal enum Paywall { /// MARK: PaywallView internal static let title = L10n.tr("Localizable", "paywall.title", fallback: "Purchase") + internal enum Alerts { + internal enum Purchase { + internal enum Appletv { + internal enum Success { + /// Thank you! The time limit will be dropped as soon as iCloud catches up. Wait a few moments, then restart the connection on the TV app. + internal static let message = L10n.tr("Localizable", "paywall.alerts.purchase.appletv.success.message", fallback: "Thank you! The time limit will be dropped as soon as iCloud catches up. Wait a few moments, then restart the connection on the TV app.") + } + } + } + } internal enum Items { internal enum FullVersion { /// All providers (including future ones) @@ -807,6 +835,10 @@ internal enum L10n { /// Exchanged data internal static let caption = L10n.tr("Localizable", "profile.items.data_count.caption", fallback: "Exchanged data") } + internal enum ExpiresAt { + /// Expiration + internal static let caption = L10n.tr("Localizable", "profile.items.expires_at.caption", fallback: "Expiration") + } internal enum OnlyShowsFavorites { /// Only show favorite locations internal static let caption = L10n.tr("Localizable", "profile.items.only_shows_favorites.caption", fallback: "Only show favorite locations") @@ -821,6 +853,14 @@ internal enum L10n { /// Randomize server internal static let caption = L10n.tr("Localizable", "profile.items.randomizes_server.caption", fallback: "Randomize server") } + internal enum TvSharing { + internal enum Caption { + /// Limited to %d minutes + internal static func limited(_ p1: Int) -> String { + return L10n.tr("Localizable", "profile.items.tv_sharing.caption.limited", p1, fallback: "Limited to %d minutes") + } + } + } internal enum UseProfile { /// Use this profile internal static let caption = L10n.tr("Localizable", "profile.items.use_profile.caption", fallback: "Use this profile") @@ -863,6 +903,20 @@ internal enum L10n { /// Connection internal static let header = L10n.tr("Localizable", "profile.sections.status.header", fallback: "Connection") } + internal enum Tv { + internal enum Footer { + /// Profiles are encrypted and made available to your Apple TV via iCloud. + internal static let encryption = L10n.tr("Localizable", "profile.sections.tv.footer.encryption", fallback: "Profiles are encrypted and made available to your Apple TV via iCloud.") + internal enum Restricted { + /// However, the connection will expire after %d minutes. + internal static func p1(_ p1: Int) -> String { + return L10n.tr("Localizable", "profile.sections.tv.footer.restricted.p1", p1, fallback: "However, the connection will expire after %d minutes.") + } + /// Purchase to drop the restriction. + internal static let p2 = L10n.tr("Localizable", "profile.sections.tv.footer.restricted.p2", fallback: "Purchase to drop the restriction.") + } + } + } internal enum Vpn { /// The connection will be established whenever necessary. internal static let footer = L10n.tr("Localizable", "profile.sections.vpn.footer", fallback: "The connection will be established whenever necessary.") diff --git a/Passepartout/Constants.swift b/Passepartout/Constants.swift index ae430690..7ee5002e 100644 --- a/Passepartout/Constants.swift +++ b/Passepartout/Constants.swift @@ -45,4 +45,8 @@ enum Constants { static let appVersionString = "\(appVersionNumber) (\(appBuildNumber))" } + + enum Tunnel { + static let expirationTimeIntervalKey = "ExpirationTimeInterval" + } } diff --git a/Passepartout/Tunnel/Info.plist b/Passepartout/Tunnel/Info.plist index a6d8988d..213e4c18 100644 --- a/Passepartout/Tunnel/Info.plist +++ b/Passepartout/Tunnel/Info.plist @@ -29,7 +29,9 @@ NSExtensionPrincipalClass $(PRODUCT_MODULE_NAME).PacketTunnelProvider - NSHumanReadableCopyright - $(CFG_COPYRIGHT) + UIRequiredDeviceCapabilities + + arm64 + diff --git a/Passepartout/Tunnel/NEPacketTunnelProvider+Expiration.swift b/Passepartout/Tunnel/NEPacketTunnelProvider+Expiration.swift new file mode 100644 index 00000000..e6260bbd --- /dev/null +++ b/Passepartout/Tunnel/NEPacketTunnelProvider+Expiration.swift @@ -0,0 +1,47 @@ +// +// NEPacketTunnelProvider+Expiration.swift +// Passepartout +// +// Created by Davide De Rosa on 12/23/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 . +// + +import Foundation +import NetworkExtension + +extension NEPacketTunnelProvider { + func tryStartGivenExpirationDate(withTimeIntervalKey key: String) throws { + if let protocolConfiguration = protocolConfiguration as? NETunnelProviderProtocol, + let expirationDateInterval = protocolConfiguration.providerConfiguration?[key] as? TimeInterval { + let expirationDate = Date(timeIntervalSinceReferenceDate: expirationDateInterval) + + // already expired? + let delay = Int(expirationDate.timeIntervalSinceNow) + if delay < .zero { + throw TunnelError.expired + } + + // schedule connection expiration + DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(delay)) { [weak self] in + self?.cancelTunnelWithError(TunnelError.expired) + } + } + } +} diff --git a/Passepartout/Tunnel/OpenVPN/PacketTunnelProvider.swift b/Passepartout/Tunnel/OpenVPN/PacketTunnelProvider.swift index aeff33b7..256204c1 100644 --- a/Passepartout/Tunnel/OpenVPN/PacketTunnelProvider.swift +++ b/Passepartout/Tunnel/OpenVPN/PacketTunnelProvider.swift @@ -24,14 +24,18 @@ // import Foundation +import NetworkExtension import OpenVPNAppExtension final class PacketTunnelProvider: OpenVPNTunnelProvider { - override func startTunnel(options: [String: NSObject]?, completionHandler: @escaping (Error?) -> Void) { + override func startTunnel(options: [String: NSObject]? = nil) async throws { + try tryStartGivenExpirationDate(withTimeIntervalKey: Constants.Tunnel.expirationTimeIntervalKey) + appVersion = "\(Constants.Global.appName) \(Constants.Global.appVersionString)" dnsTimeout = Constants.OpenVPNTunnel.dnsTimeout logSeparator = Constants.OpenVPNTunnel.sessionMarker dataCountInterval = Constants.OpenVPNTunnel.dataCountInterval - super.startTunnel(options: options, completionHandler: completionHandler) + + try await super.startTunnel(options: options) } } diff --git a/Passepartout/Tunnel/TunnelError.swift b/Passepartout/Tunnel/TunnelError.swift new file mode 100644 index 00000000..f9cee8f1 --- /dev/null +++ b/Passepartout/Tunnel/TunnelError.swift @@ -0,0 +1,30 @@ +// +// TunnelError.swift +// Passepartout +// +// Created by Davide De Rosa on 12/23/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 . +// + +import Foundation + +enum TunnelError: Error { + case expired +} diff --git a/Passepartout/Tunnel/WireGuard/PacketTunnelProvider.swift b/Passepartout/Tunnel/WireGuard/PacketTunnelProvider.swift index 99305000..41173de2 100644 --- a/Passepartout/Tunnel/WireGuard/PacketTunnelProvider.swift +++ b/Passepartout/Tunnel/WireGuard/PacketTunnelProvider.swift @@ -27,8 +27,11 @@ import Foundation import WireGuardAppExtension final class PacketTunnelProvider: WireGuardTunnelProvider { - override func startTunnel(options: [String: NSObject]?, completionHandler: @escaping (Error?) -> Void) { + override func startTunnel(options: [String: NSObject]? = nil) async throws { + try tryStartGivenExpirationDate(withTimeIntervalKey: Constants.Tunnel.expirationTimeIntervalKey) + dataCountInterval = Constants.WireGuardTunnel.dataCountInterval - super.startTunnel(options: options, completionHandler: completionHandler) + + try await super.startTunnel(options: options) } } diff --git a/PassepartoutLibrary.xctestplan b/PassepartoutLibrary.xctestplan index dd24e3db..a8bcc6ac 100644 --- a/PassepartoutLibrary.xctestplan +++ b/PassepartoutLibrary.xctestplan @@ -9,7 +9,7 @@ } ], "defaultOptions" : { - "codeCoverage" : false + }, "testTargets" : [ { diff --git a/PassepartoutLibrary/Package.swift b/PassepartoutLibrary/Package.swift index 534f1bf7..5d3e7fd4 100644 --- a/PassepartoutLibrary/Package.swift +++ b/PassepartoutLibrary/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.5 +// swift-tools-version:5.9 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription @@ -6,7 +6,9 @@ import PackageDescription let package = Package( name: "PassepartoutLibrary", platforms: [ - .iOS(.v15), .macOS(.v12) + .iOS(.v15), + .macOS(.v12), + .tvOS(.v17) ], products: [ // Products define the executables and libraries a package produces, and make them visible to other packages. @@ -31,8 +33,8 @@ let package = Package( dependencies: [ // Dependencies declare other packages that this package depends on. // .package(url: /* package url */, from: "1.0.0"), -// .package(name: "TunnelKit", url: "https://github.com/passepartoutvpn/tunnelkit", from: "6.1.1"), - .package(name: "TunnelKit", url: "https://github.com/passepartoutvpn/tunnelkit", .revision("bda84bf569792fbb702d0173de3c9c58768f9153")), +// .package(url: "https://github.com/passepartoutvpn/tunnelkit", from: "6.3.0"), + .package(url: "https://github.com/passepartoutvpn/tunnelkit", revision: "708c785e615f5715ce08386c772c92fb45730a3a"), // .package(name: "TunnelKit", path: "../../tunnelkit"), .package(url: "https://github.com/zoul/generic-json-swift", from: "2.0.0"), .package(url: "https://github.com/SwiftyBeaver/SwiftyBeaver", from: "1.9.0") diff --git a/PassepartoutLibrary/Sources/PassepartoutCore/Reusable/CoreDataPersistentStore.swift b/PassepartoutLibrary/Sources/PassepartoutCore/Reusable/CoreDataPersistentStore.swift index 4b4d85ef..9652f8da 100644 --- a/PassepartoutLibrary/Sources/PassepartoutCore/Reusable/CoreDataPersistentStore.swift +++ b/PassepartoutLibrary/Sources/PassepartoutCore/Reusable/CoreDataPersistentStore.swift @@ -31,17 +31,19 @@ import Foundation public final class CoreDataPersistentStore { private let container: NSPersistentContainer - public convenience init(withName containerName: String, model: NSManagedObjectModel, cloudKit: Bool, author: String?) { + public convenience init(withName containerName: String, model: NSManagedObjectModel, cloudKit: Bool, cloudKitIdentifier: String?, author: String?) { let container: NSPersistentContainer if cloudKit { container = NSPersistentCloudKitContainer(name: containerName, managedObjectModel: model) + pp_log.debug("Setting up CloudKit container: \(containerName)") } else { container = NSPersistentContainer(name: containerName, managedObjectModel: model) + pp_log.debug("Setting up local container: \(containerName)") } - self.init(withContainer: container, author: author) + self.init(withContainer: container, cloudKitIdentifier: cloudKitIdentifier, author: author) } - private init(withContainer container: NSPersistentContainer, author: String?) { + private init(withContainer container: NSPersistentContainer, cloudKitIdentifier: String?, author: String?) { self.container = container guard let desc = container.persistentStoreDescriptions.first else { @@ -49,6 +51,11 @@ public final class CoreDataPersistentStore { } pp_log.debug("Container description: \(desc)") + // optional container identifier for CloudKit, first in entitlements otherwise + if let cloudKitIdentifier { + desc.cloudKitContainerOptions = .init(containerIdentifier: cloudKitIdentifier) + } + // set this even for local container, to avoid readonly mode in case // container was formerly created with CloudKit option desc.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey) diff --git a/PassepartoutLibrary/Sources/PassepartoutCore/Utils/Utils+Network.swift b/PassepartoutLibrary/Sources/PassepartoutCore/Utils/Utils+Network.swift index 31f43ae8..e902b13c 100644 --- a/PassepartoutLibrary/Sources/PassepartoutCore/Utils/Utils+Network.swift +++ b/PassepartoutLibrary/Sources/PassepartoutCore/Utils/Utils+Network.swift @@ -24,7 +24,7 @@ // import Foundation -#if os(iOS) +#if os(iOS) || os(tvOS) import NetworkExtension #else import CoreWLAN @@ -77,8 +77,10 @@ extension Utils { continuation.resume(with: .success(network.ssid)) } } - #else + #elseif os(macOS) CWWiFiClient.shared().interface()?.ssid() + #else + nil #endif } } diff --git a/PassepartoutLibrary/Sources/PassepartoutCore/Utils/Utils+Strings.swift b/PassepartoutLibrary/Sources/PassepartoutCore/Utils/Utils+Strings.swift index 71f01c68..c312e3ee 100644 --- a/PassepartoutLibrary/Sources/PassepartoutCore/Utils/Utils+Strings.swift +++ b/PassepartoutLibrary/Sources/PassepartoutCore/Utils/Utils+Strings.swift @@ -24,23 +24,25 @@ // import Foundation -#if os(iOS) +#if os(iOS) || os(tvOS) import UIKit #else import AppKit #endif extension Utils { + #if !os(tvOS) public static func copyToPasteboard(_ string: String) { #if os(iOS) let pb = UIPasteboard.general pb.string = string - #else + #elseif os(macOS) let pb = NSPasteboard.general pb.clearContents() pb.setString(string, forType: .string) #endif } + #endif } extension String: StrippableContent { diff --git a/PassepartoutLibrary/Sources/PassepartoutCore/Utils/Utils+URL.swift b/PassepartoutLibrary/Sources/PassepartoutCore/Utils/Utils+URL.swift index 3f382ed9..74e7d0d4 100644 --- a/PassepartoutLibrary/Sources/PassepartoutCore/Utils/Utils+URL.swift +++ b/PassepartoutLibrary/Sources/PassepartoutCore/Utils/Utils+URL.swift @@ -25,7 +25,7 @@ import Foundation import StoreKit -#if os(iOS) +#if os(iOS) || os(tvOS) import UIKit #else import AppKit @@ -38,7 +38,7 @@ extension URL { @discardableResult public static func open(_ url: URL) -> Bool { - #if os(iOS) + #if os(iOS) || os(tvOS) guard UIApplication.shared.canOpenURL(url) else { return false } diff --git a/PassepartoutLibrary/Sources/PassepartoutFrontend/Domain/LocalProduct.swift b/PassepartoutLibrary/Sources/PassepartoutFrontend/Domain/LocalProduct.swift index d3173375..f9900794 100644 --- a/PassepartoutLibrary/Sources/PassepartoutFrontend/Domain/LocalProduct.swift +++ b/PassepartoutLibrary/Sources/PassepartoutFrontend/Domain/LocalProduct.swift @@ -75,6 +75,10 @@ public struct LocalProduct: RawRepresentable, Hashable, Sendable { public static let siriShortcuts = LocalProduct(featureId: "siri") + public static let appleTV = LocalProduct(featureId: "appletv") + + // MARK: Full version + public static let fullVersion_iOS = LocalProduct(featureId: "full_version") public static let fullVersion_macOS = LocalProduct(featureId: "full_mac_version") @@ -86,6 +90,7 @@ public struct LocalProduct: RawRepresentable, Hashable, Sendable { .networkSettings, .trustedNetworks, .siriShortcuts, + .appleTV, .fullVersion_iOS, .fullVersion_macOS, .fullVersion @@ -98,7 +103,7 @@ public struct LocalProduct: RawRepresentable, Hashable, Sendable { // MARK: All static var all: [LocalProduct] { - allDonations + allFeatures// + allProviders + allDonations + allFeatures } public var isDonation: Bool { @@ -113,7 +118,7 @@ public struct LocalProduct: RawRepresentable, Hashable, Sendable { rawValue.hasPrefix(LocalProduct.providersBundle) } - public var isPlatformVersion: Bool { + public var isLegacyPlatformVersion: Bool { switch self { case .fullVersion_iOS, .fullVersion_macOS: return true diff --git a/PassepartoutLibrary/Sources/PassepartoutFrontend/Managers/ProductManager.swift b/PassepartoutLibrary/Sources/PassepartoutFrontend/Managers/ProductManager.swift index e2a20a5d..1904c9a4 100644 --- a/PassepartoutLibrary/Sources/PassepartoutFrontend/Managers/ProductManager.swift +++ b/PassepartoutLibrary/Sources/PassepartoutFrontend/Managers/ProductManager.swift @@ -195,10 +195,10 @@ extension ProductManager { return true } } - if feature.isPlatformVersion { - return isActivePurchase(feature) + if isIncludedInFullVersion(feature) { + return isFullVersion() || isActivePurchase(feature) } - return isFullVersion() || isActivePurchase(feature) + return isActivePurchase(feature) } public func isEligible(forProvider providerName: ProviderName) -> Bool { @@ -209,7 +209,7 @@ extension ProductManager { } public func isEligibleForFeedback() -> Bool { - appType == .beta || !purchasedFeatures.isEmpty + appType == .beta || isPayingUser() } } @@ -226,7 +226,11 @@ extension ProductManager { isActivePurchase(isMac ? .fullVersion_macOS : .fullVersion_iOS) } - func isFullVersion() -> Bool { + func isIncludedInFullVersion(_ feature: LocalProduct) -> Bool { + !feature.isLegacyPlatformVersion && feature != .appleTV + } + + public func isFullVersion() -> Bool { if appType == .fullVersion { return true } @@ -235,6 +239,10 @@ extension ProductManager { } return isActivePurchase(.fullVersion) } + + public func isPayingUser() -> Bool { + !purchasedFeatures.subtracting(cancelledPurchases ?? []).isEmpty + } } // MARK: Receipt @@ -299,6 +307,7 @@ private extension ProductManager { #endif } + // FIXME: in-app, this is incomplete func detectRefunds(_ refunds: Set) { let isEligibleForFullVersion = isFullVersion() let hasCancelledFullVersion: Bool diff --git a/PassepartoutLibrary/Sources/PassepartoutProvidersImpl/Data/ProvidersPersistence.swift b/PassepartoutLibrary/Sources/PassepartoutProvidersImpl/Data/ProvidersPersistence.swift index 8a22c292..8f0548d9 100644 --- a/PassepartoutLibrary/Sources/PassepartoutProvidersImpl/Data/ProvidersPersistence.swift +++ b/PassepartoutLibrary/Sources/PassepartoutProvidersImpl/Data/ProvidersPersistence.swift @@ -48,6 +48,7 @@ public final class ProvidersPersistence { withName: containerName, model: Self.dataModel, cloudKit: cloudKit, + cloudKitIdentifier: nil, author: author ) } diff --git a/PassepartoutLibrary/Sources/PassepartoutVPN/Domain/Profile.swift b/PassepartoutLibrary/Sources/PassepartoutVPN/Domain/Profile.swift index f5c21846..536d3f60 100644 --- a/PassepartoutLibrary/Sources/PassepartoutVPN/Domain/Profile.swift +++ b/PassepartoutLibrary/Sources/PassepartoutVPN/Domain/Profile.swift @@ -39,6 +39,8 @@ public struct Profile: Identifiable, Codable, Equatable { public var onDemand = Profile.OnDemand() + public var connectionExpirationDate: Date? + var host: Host? var provider: Provider? @@ -96,3 +98,12 @@ extension Profile { header.id == Self.placeholder.id } } + +extension Profile { + public var isExpired: Bool { + guard let connectionExpirationDate else { + return false + } + return Date().distance(to: connectionExpirationDate) <= .zero + } +} diff --git a/PassepartoutLibrary/Sources/PassepartoutVPN/Domain/VPNConfigurationParameters.swift b/PassepartoutLibrary/Sources/PassepartoutVPN/Domain/VPNConfigurationParameters.swift index bd39428b..aeb02e34 100644 --- a/PassepartoutLibrary/Sources/PassepartoutVPN/Domain/VPNConfigurationParameters.swift +++ b/PassepartoutLibrary/Sources/PassepartoutVPN/Domain/VPNConfigurationParameters.swift @@ -52,13 +52,16 @@ public struct VPNConfigurationParameters { public let withCustomRules: Bool + public let userData: [String: Any]? + init( _ profile: Profile, providerManager: ProviderManager, preferences: VPNPreferences, passwordReference: Data?, withNetworkSettings: Bool, - withCustomRules: Bool + withCustomRules: Bool, + userData: [String: Any]? ) { self.profile = profile self.providerManager = providerManager @@ -66,5 +69,6 @@ public struct VPNConfigurationParameters { self.passwordReference = passwordReference self.withNetworkSettings = withNetworkSettings self.withCustomRules = withCustomRules + self.userData = userData } } diff --git a/PassepartoutLibrary/Sources/PassepartoutVPN/Extensions/DebugLog+Extensions.swift b/PassepartoutLibrary/Sources/PassepartoutVPN/Extensions/DebugLog+Extensions.swift index 05598a81..ef8ef105 100644 --- a/PassepartoutLibrary/Sources/PassepartoutVPN/Extensions/DebugLog+Extensions.swift +++ b/PassepartoutLibrary/Sources/PassepartoutVPN/Extensions/DebugLog+Extensions.swift @@ -24,7 +24,7 @@ // import Foundation -#if os(iOS) +#if os(iOS) || os(tvOS) import UIKit #else import AppKit @@ -35,7 +35,7 @@ extension DebugLog { let osVersion: String let deviceType: String? - #if os(iOS) + #if os(iOS) || os(tvOS) let device: UIDevice = .current osVersion = "\(device.systemName) \(device.systemVersion)" #if targetEnvironment(macCatalyst) diff --git a/PassepartoutLibrary/Sources/PassepartoutVPN/Managers/ProfileManager+Extensions.swift b/PassepartoutLibrary/Sources/PassepartoutVPN/Managers/ProfileManager+Extensions.swift index 785a1b63..864e4823 100644 --- a/PassepartoutLibrary/Sources/PassepartoutVPN/Managers/ProfileManager+Extensions.swift +++ b/PassepartoutLibrary/Sources/PassepartoutVPN/Managers/ProfileManager+Extensions.swift @@ -47,7 +47,7 @@ extension ProfileManager { } public func activateProfile(_ profile: Profile) { - saveProfile(profile, isActive: true, updateIfCurrent: true) + activateProfile(profile, isActive: true) } public func saveProfile(_ profile: Profile, isActive: Bool?) { @@ -65,6 +65,6 @@ extension ProfileManager { } public func activateCurrentProfile() { - saveProfile(currentProfile.value, isActive: true, updateIfCurrent: false) + activateProfile(currentProfile.value) } } diff --git a/PassepartoutLibrary/Sources/PassepartoutVPN/Managers/ProfileManager.swift b/PassepartoutLibrary/Sources/PassepartoutVPN/Managers/ProfileManager.swift index bcb1aedf..66ca5dc3 100644 --- a/PassepartoutLibrary/Sources/PassepartoutVPN/Managers/ProfileManager.swift +++ b/PassepartoutLibrary/Sources/PassepartoutVPN/Managers/ProfileManager.swift @@ -44,12 +44,16 @@ public final class ProfileManager: ObservableObject { private var profileRepository: ProfileRepository + private var sharedProfileRepository: ProfileRepository? + private let keychain: SecretRepository private let keychainEntry: (Profile) -> String private let keychainLabel: (Profile) -> String + public var willSaveSharedProfile: (_ newProfile: Profile, _ existingProfile: Profile?) -> Profile + // MARK: State @Published private var internalActiveProfileId: UUID? { @@ -89,12 +93,19 @@ public final class ProfileManager: ObservableObject { public let didCreateProfile = PassthroughSubject() + @Published private var sharedProfileIds: Set { + didSet { + refreshSharedProfiles() + } + } + private var cancellables: Set = [] public init( store: KeyValueStore, providerManager: ProviderManager, profileRepository: ProfileRepository, + sharedProfileRepository: ProfileRepository? = nil, keychain: SecretRepository, keychainEntry: @escaping KeychainEntry, keychainLabel: @escaping KeychainLabel @@ -102,11 +113,20 @@ public final class ProfileManager: ObservableObject { self.store = store self.providerManager = providerManager self.profileRepository = profileRepository + self.sharedProfileRepository = sharedProfileRepository self.keychain = keychain self.keychainEntry = keychainEntry self.keychainLabel = keychainLabel + willSaveSharedProfile = { newProfile, _ in + newProfile + } currentProfile = ObservableProfile() + if let sharedProfileRepository { + sharedProfileIds = Set(sharedProfileRepository.allProfiles().keys) + } else { + sharedProfileIds = [] + } } } @@ -172,6 +192,21 @@ extension ProfileManager { return profile } + public func activateProfile(_ profile: Profile, isActive: Bool) { + guard !profile.isPlaceholder else { + assertionFailure("Placeholder") + return + } + + if isActive { + pp_log.info("\tActivating profile...") + activeProfileId = profile.id + } else if activeProfileId == profile.id { + pp_log.info("\tDeactivating profile...") + activeProfileId = nil + } + } + public func saveProfile(_ profile: Profile, isActive: Bool?, updateIfCurrent: Bool = true) { guard !profile.isPlaceholder else { assertionFailure("Placeholder") @@ -179,16 +214,10 @@ extension ProfileManager { } pp_log.info("Writing profile \(profile.logDescription) to persistent store") - profileRepository.saveProfilesAndLog([profile]) + saveProfilesAndLog([profile]) - if let isActive = isActive { - if isActive { - pp_log.info("\tActivating profile...") - activeProfileId = profile.id - } else if activeProfileId == profile.id { - pp_log.info("\tDeactivating profile...") - activeProfileId = nil - } + if let isActive { + activateProfile(profile, isActive: isActive) } else if allProfiles.isEmpty { pp_log.info("\tActivating first profile...") activeProfileId = profile.id @@ -235,7 +264,7 @@ extension ProfileManager { // autosaves copy if non-existing in persistent store setCurrentProfile(copy) } else { - profileRepository.saveProfilesAndLog([copy]) + saveProfilesAndLog([copy]) } } @@ -328,7 +357,7 @@ extension ProfileManager { } defer { if !profilesToSave.isEmpty { - profileRepository.saveProfilesAndLog(profilesToSave) + saveProfilesAndLog(profilesToSave) } } @@ -350,13 +379,39 @@ extension ProfileManager { $internalActiveProfileId .sink { [weak self] in self?.didUpdateActiveProfile.send($0) - }.store(in: &cancellables) + } + .store(in: &cancellables) profileRepository.willUpdateProfiles() .dropFirst() .sink { [weak self] in self?.willUpdateProfiles($0) - }.store(in: &cancellables) + } + .store(in: &cancellables) + + if let sharedProfileRepository { + + // persist changes to shared profiles immediately + currentProfile.$value + .dropFirst() + .removeDuplicates() + .filter { [unowned self] in + sharedProfileIds.contains($0.id) + } + .map { [unowned self] in + let existingProfile = sharedProfileRepository.profile(withId: $0.id) + return willSaveSharedProfile($0, existingProfile) + } + .sink { newSharedProfile in + do { + try sharedProfileRepository.saveProfiles([newSharedProfile]) + pp_log.info("Current profile persisted (shared): \(newSharedProfile.logDescription)") + } catch { + pp_log.error("Unable to persist current profile (shared): \(error)") + } + } + .store(in: &cancellables) + } } private func willUpdateProfiles(_ newProfiles: [UUID: Profile]) { @@ -431,20 +486,78 @@ extension ProfileManager { } } if !renamedProfiles.isEmpty { - profileRepository.saveProfilesAndLog(renamedProfiles) + saveProfilesAndLog(renamedProfiles) pp_log.debug("Duplicates successfully renamed!") } } } -private extension ProfileRepository { - func saveProfilesAndLog(_ profiles: [Profile]) { +// MARK: Persistence + +extension ProfileManager { + private func saveProfilesAndLog(_ profiles: [Profile]) { do { - try saveProfiles(profiles) + try profileRepository.saveProfiles(profiles.map { + var copy = $0 + copy.connectionExpirationDate = nil + return copy + }) } catch { pp_log.error("Unable to save profile(s): \(error)") } } + + public func isSharing(profile: Profile) -> Bool { + sharedProfileIds.contains(profile.id) + } + + public func setSharing(_ isShared: Bool, profile: Profile) { + if isShared { + pp_log.debug("Adding shared profile: \(profile.id)") + sharedProfileIds.insert(profile.id) + } else { + pp_log.debug("Removing shared profile: \(profile.id))") + sharedProfileIds.remove(profile.id) + } + } +} + +extension ProfileManager { + public func refreshSharedProfiles() { + guard let sharedProfileRepository else { + return + } + + var toAdd: [Profile] = [] + var toRemove: [UUID] = [] + profiles.forEach { + if sharedProfileIds.contains($0.id) { + toAdd.append($0) + } else { + toRemove.append($0.id) + } + } + + let existingProfiles = sharedProfileRepository + .allProfiles() + .filter { + sharedProfileIds.contains($0.key) + } + + pp_log.debug("Refreshing shared profiles") + pp_log.debug("\tAdding: \(toAdd.map(\.logDescription))") + pp_log.debug("\tExisting: \(existingProfiles.keys)") + pp_log.debug("\tRemoving: \(toRemove)") + + sharedProfileRepository.removeProfiles(withIds: toRemove) + do { + try sharedProfileRepository.saveProfiles(toAdd.map { + willSaveSharedProfile($0, existingProfiles[$0.id]) + }) + } catch { + pp_log.error("Unable to save shared profiles: \(error)") + } + } } // MARK: Readiness diff --git a/PassepartoutLibrary/Sources/PassepartoutVPN/Managers/VPNManager.swift b/PassepartoutLibrary/Sources/PassepartoutVPN/Managers/VPNManager.swift index 3ad7e0c6..07bbe405 100644 --- a/PassepartoutLibrary/Sources/PassepartoutVPN/Managers/VPNManager.swift +++ b/PassepartoutLibrary/Sources/PassepartoutVPN/Managers/VPNManager.swift @@ -45,6 +45,8 @@ public final class VPNManager: ObservableObject { public var isOnDemandRulesSupported: () -> Bool + public var userData: (Profile) -> [String: Any]? + // MARK: State public let currentState: ObservableVPNState @@ -78,6 +80,7 @@ public final class VPNManager: ObservableObject { self.strategy = strategy isNetworkSettingsSupported = { false } isOnDemandRulesSupported = { false } + userData = { _ in nil } currentState = ObservableVPNState() } @@ -290,7 +293,8 @@ private extension VPNManager { preferences: vpnPreferences, passwordReference: profileManager.passwordReference(forProfile: profile), withNetworkSettings: isNetworkSettingsSupported(), - withCustomRules: isOnDemandRulesSupported() + withCustomRules: isOnDemandRulesSupported(), + userData: userData(profile) ) } } diff --git a/PassepartoutLibrary/Sources/PassepartoutVPN/Strategies/ProfileRepository.swift b/PassepartoutLibrary/Sources/PassepartoutVPN/Strategies/ProfileRepository.swift index fefe0707..bc8bb86c 100644 --- a/PassepartoutLibrary/Sources/PassepartoutVPN/Strategies/ProfileRepository.swift +++ b/PassepartoutLibrary/Sources/PassepartoutVPN/Strategies/ProfileRepository.swift @@ -38,3 +38,9 @@ public protocol ProfileRepository { func willUpdateProfiles() -> AnyPublisher<[UUID: Profile], Never> } + +extension ProfileRepository { + public func hasProfile(withId id: UUID) -> Bool { + profile(withId: id) != nil + } +} diff --git a/PassepartoutLibrary/Sources/PassepartoutVPNImpl/Data/VPNPersistence.swift b/PassepartoutLibrary/Sources/PassepartoutVPNImpl/Data/VPNPersistence.swift index 89eeb61c..0e58b445 100644 --- a/PassepartoutLibrary/Sources/PassepartoutVPNImpl/Data/VPNPersistence.swift +++ b/PassepartoutLibrary/Sources/PassepartoutVPNImpl/Data/VPNPersistence.swift @@ -42,11 +42,15 @@ public final class VPNPersistence { store.containerURLs } - public init(withName containerName: String, cloudKit: Bool, author: String?) { + public init(withName containerName: String, + cloudKit: Bool, + cloudKitIdentifier: String?, + author: String?) { store = .init( withName: containerName, model: Self.dataModel, cloudKit: cloudKit, + cloudKitIdentifier: cloudKitIdentifier, author: author ) } diff --git a/PassepartoutLibrary/Sources/PassepartoutVPNImpl/Extensions/OpenVPNSettings+TunnelKit.swift b/PassepartoutLibrary/Sources/PassepartoutVPNImpl/Extensions/OpenVPNSettings+TunnelKit.swift index 11deb7ef..ca48bbd1 100644 --- a/PassepartoutLibrary/Sources/PassepartoutVPNImpl/Extensions/OpenVPNSettings+TunnelKit.swift +++ b/PassepartoutLibrary/Sources/PassepartoutVPNImpl/Extensions/OpenVPNSettings+TunnelKit.swift @@ -68,6 +68,7 @@ extension Profile.OpenVPNSettings: TunnelKitConfigurationProviding { extra.passwordReference = parameters.passwordReference extra.onDemandRules = parameters.onDemandRules extra.disconnectsOnSleep = !parameters.networkSettings.keepsAliveOnSleep + extra.userData = parameters.userData pp_log.verbose("Configuration:") pp_log.verbose(cfg) diff --git a/PassepartoutLibrary/Sources/PassepartoutVPNImpl/Extensions/WireGuardSettings+TunnelKit.swift b/PassepartoutLibrary/Sources/PassepartoutVPNImpl/Extensions/WireGuardSettings+TunnelKit.swift index a43361a5..9b908387 100644 --- a/PassepartoutLibrary/Sources/PassepartoutVPNImpl/Extensions/WireGuardSettings+TunnelKit.swift +++ b/PassepartoutLibrary/Sources/PassepartoutVPNImpl/Extensions/WireGuardSettings+TunnelKit.swift @@ -56,6 +56,7 @@ extension Profile.WireGuardSettings: TunnelKitConfigurationProviding { var extra = NetworkExtensionExtra() extra.onDemandRules = parameters.onDemandRules extra.disconnectsOnSleep = !parameters.networkSettings.keepsAliveOnSleep + extra.userData = parameters.userData pp_log.verbose("Configuration:") pp_log.verbose(cfg) diff --git a/PassepartoutLibrary/Sources/PassepartoutVPNImpl/Strategies/CDProfileRepository.swift b/PassepartoutLibrary/Sources/PassepartoutVPNImpl/Strategies/CDProfileRepository.swift index a1d133e8..17414ab2 100644 --- a/PassepartoutLibrary/Sources/PassepartoutVPNImpl/Strategies/CDProfileRepository.swift +++ b/PassepartoutLibrary/Sources/PassepartoutVPNImpl/Strategies/CDProfileRepository.swift @@ -70,6 +70,7 @@ final class CDProfileRepository: ProfileRepository { } try context.save() } catch { + pp_log.error("Unable to save profiles: \(error)") context.rollback() throw error } diff --git a/PassepartoutLibrary/Tests/PassepartoutFrontendTests/ProductManagerTests.swift b/PassepartoutLibrary/Tests/PassepartoutFrontendTests/ProductManagerTests.swift index d0724851..4bd02410 100644 --- a/PassepartoutLibrary/Tests/PassepartoutFrontendTests/ProductManagerTests.swift +++ b/PassepartoutLibrary/Tests/PassepartoutFrontendTests/ProductManagerTests.swift @@ -98,24 +98,24 @@ final class ProductManagerTests: XCTestCase { let reader = MockReceiptReader() let sut = ProductManager(inApp: inApp, receiptReader: reader, buildProducts: noBuildProducts) - #if targetEnvironment(macCatalyst) +#if targetEnvironment(macCatalyst) reader.setReceipt(withBuild: 1500, products: [.fullVersion_macOS, .networkSettings]) sut.reloadReceipt() XCTAssertFalse(sut.isEligible(forFeature: .fullVersion_iOS)) XCTAssertTrue(sut.isEligible(forFeature: .fullVersion_macOS)) - #else +#else reader.setReceipt(withBuild: 1500, products: [.fullVersion_iOS, .networkSettings]) sut.reloadReceipt() XCTAssertTrue(sut.isEligible(forFeature: .fullVersion_iOS)) XCTAssertFalse(sut.isEligible(forFeature: .fullVersion_macOS)) - #endif +#endif XCTAssertTrue(sut.isCurrentPlatformVersion()) XCTAssertTrue(sut.isFullVersion()) XCTAssertTrue(sut.isEligible(forFeature: .fullVersion)) } - func test_givenFullVersion_thenIsEligibleForAnyFeature() { + func test_givenFullVersion_thenIsEligibleForAnyFeatureExceptAppleTV() { let reader = MockReceiptReader() reader.setReceipt(withBuild: 1500, products: [.fullVersion]) let sut = ProductManager(inApp: inApp, receiptReader: reader, buildProducts: noBuildProducts) @@ -123,7 +123,7 @@ final class ProductManagerTests: XCTestCase { XCTAssertTrue(sut.isFullVersion()) XCTAssertTrue(LocalProduct .allFeatures - .filter { !$0.isPlatformVersion } + .filter { $0 != .appleTV && !$0.isLegacyPlatformVersion } .allSatisfy(sut.isEligible(forFeature:)) ) } @@ -139,4 +139,28 @@ final class ProductManagerTests: XCTestCase { .allSatisfy(sut.isEligible(forFeature:)) ) } + + func test_givenFreeVersion_thenIsNotEligibleForAppleTV() { + let reader = MockReceiptReader() + reader.setReceipt(withBuild: 1500, products: []) + let sut = ProductManager(inApp: inApp, receiptReader: reader, buildProducts: noBuildProducts) + + XCTAssertFalse(sut.isEligible(forFeature: .appleTV)) + } + + func test_givenFullVersion_thenIsNotEligibleForAppleTV() { + let reader = MockReceiptReader() + reader.setReceipt(withBuild: 1500, products: [.fullVersion]) + let sut = ProductManager(inApp: inApp, receiptReader: reader, buildProducts: noBuildProducts) + + XCTAssertFalse(sut.isEligible(forFeature: .appleTV)) + } + + func test_givenAppleTV_thenIsEligibleForAppleTV() { + let reader = MockReceiptReader() + reader.setReceipt(withBuild: 1500, products: [.appleTV]) + let sut = ProductManager(inApp: inApp, receiptReader: reader, buildProducts: noBuildProducts) + + XCTAssertTrue(sut.isEligible(forFeature: .appleTV)) + } } diff --git a/README.md b/README.md index 05cdb2bc..c2a72791 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,8 @@ ![iOS 15+](https://img.shields.io/badge/iOS-15+-green.svg) ![macOS 12+](https://img.shields.io/badge/macOS-12+-green.svg) -[![TunnelKit 6.2](https://img.shields.io/badge/TunnelKit-6.2-d69c68.svg)][dep-tunnelkit] +![tvOS 17+](https://img.shields.io/badge/tvOS-17+-green.svg) +[![TunnelKit 6.3](https://img.shields.io/badge/TunnelKit-6.3-d69c68.svg)][dep-tunnelkit] [![License GPLv3](https://img.shields.io/badge/License-GPLv3-lightgray.svg)](LICENSE) [![Unit Tests](https://github.com/passepartoutvpn/passepartout-apple/actions/workflows/test.yml/badge.svg)](https://github.com/passepartoutvpn/passepartout-apple/actions/workflows/test.yml) @@ -87,7 +88,7 @@ Passepartout can import .ovpn (OpenVPN) and .conf/.wg (WireGuard) configuration ### Requirements -- iOS 15+ / macOS 12+ +- iOS 15+ / macOS 12+ / tvOS 17+ - Xcode 13+ (SwiftPM 5.3) - Git (preinstalled with Xcode Command Line Tools) - Ruby (preinstalled with macOS)