diff options
| author | Andrej Mihajlov <and@mullvad.net> | 2020-02-04 16:31:57 +0100 |
|---|---|---|
| committer | Andrej Mihajlov <and@mullvad.net> | 2020-02-11 14:04:37 +0100 |
| commit | fb2241d34336f85e9829ccaa9f1ca257308b8892 (patch) | |
| tree | bbda343ed4bab49db45c28b573dd5a7dfa2467aa | |
| parent | 2b83e5ab4a0b4c3df813cdc879fc844646cf20d2 (diff) | |
| download | mullvadvpn-fb2241d34336f85e9829ccaa9f1ca257308b8892.tar.xz mullvadvpn-fb2241d34336f85e9829ccaa9f1ca257308b8892.zip | |
Add screenshot script and simulated tunnels
24 files changed, 1330 insertions, 54 deletions
diff --git a/ios/Configurations/.gitignore b/ios/Configurations/.gitignore new file mode 100644 index 0000000000..7431e305b0 --- /dev/null +++ b/ios/Configurations/.gitignore @@ -0,0 +1 @@ +Screenshots.xcconfig diff --git a/ios/Configurations/Screenshots.xcconfig.template b/ios/Configurations/Screenshots.xcconfig.template new file mode 100644 index 0000000000..9e472844da --- /dev/null +++ b/ios/Configurations/Screenshots.xcconfig.template @@ -0,0 +1 @@ +MULLVAD_ACCOUNT_TOKEN=<ACCOUNT TOKEN> diff --git a/ios/Gemfile b/ios/Gemfile new file mode 100644 index 0000000000..abde641ddc --- /dev/null +++ b/ios/Gemfile @@ -0,0 +1,3 @@ +source "https://rubygems.org" + +gem "fastlane", "~> 2.141.0" diff --git a/ios/Gemfile.lock b/ios/Gemfile.lock new file mode 100644 index 0000000000..5aa4f1acac --- /dev/null +++ b/ios/Gemfile.lock @@ -0,0 +1,157 @@ +GEM + remote: https://rubygems.org/ + specs: + CFPropertyList (3.0.0) + addressable (2.6.0) + public_suffix (>= 2.0.2, < 4.0) + atomos (0.1.3) + babosa (1.0.2) + claide (1.0.2) + colored (1.2) + colored2 (3.1.2) + commander-fastlane (4.4.6) + highline (~> 1.7.2) + declarative (0.0.10) + declarative-option (0.1.0) + digest-crc (0.4.1) + domain_name (0.5.20180417) + unf (>= 0.0.5, < 1.0.0) + dotenv (2.7.1) + emoji_regex (1.0.1) + excon (0.72.0) + faraday (0.17.0) + multipart-post (>= 1.2, < 3) + faraday-cookie_jar (0.0.6) + faraday (>= 0.7.4) + http-cookie (~> 1.0.0) + faraday_middleware (0.13.1) + faraday (>= 0.7.4, < 1.0) + fastimage (2.1.5) + fastlane (2.141.0) + CFPropertyList (>= 2.3, < 4.0.0) + addressable (>= 2.3, < 3.0.0) + babosa (>= 1.0.2, < 2.0.0) + bundler (>= 1.12.0, < 3.0.0) + colored + commander-fastlane (>= 4.4.6, < 5.0.0) + dotenv (>= 2.1.1, < 3.0.0) + emoji_regex (>= 0.1, < 2.0) + excon (>= 0.71.0, < 1.0.0) + faraday (~> 0.17) + faraday-cookie_jar (~> 0.0.6) + faraday_middleware (~> 0.13.1) + fastimage (>= 2.1.0, < 3.0.0) + gh_inspector (>= 1.1.2, < 2.0.0) + google-api-client (>= 0.29.2, < 0.37.0) + google-cloud-storage (>= 1.15.0, < 2.0.0) + highline (>= 1.7.2, < 2.0.0) + json (< 3.0.0) + jwt (~> 2.1.0) + mini_magick (>= 4.9.4, < 5.0.0) + multi_xml (~> 0.5) + multipart-post (~> 2.0.0) + plist (>= 3.1.0, < 4.0.0) + public_suffix (~> 2.0.0) + rubyzip (>= 1.3.0, < 2.0.0) + security (= 0.1.3) + simctl (~> 1.6.3) + slack-notifier (>= 2.0.0, < 3.0.0) + terminal-notifier (>= 2.0.0, < 3.0.0) + terminal-table (>= 1.4.5, < 2.0.0) + tty-screen (>= 0.6.3, < 1.0.0) + tty-spinner (>= 0.8.0, < 1.0.0) + word_wrap (~> 1.0.0) + xcodeproj (>= 1.13.0, < 2.0.0) + xcpretty (~> 0.3.0) + xcpretty-travis-formatter (>= 0.0.3) + gh_inspector (1.1.3) + google-api-client (0.36.4) + addressable (~> 2.5, >= 2.5.1) + googleauth (~> 0.9) + httpclient (>= 2.8.1, < 3.0) + mini_mime (~> 1.0) + representable (~> 3.0) + retriable (>= 2.0, < 4.0) + signet (~> 0.12) + google-cloud-core (1.3.0) + google-cloud-env (~> 1.0) + google-cloud-env (1.0.5) + faraday (~> 0.11) + google-cloud-storage (1.16.0) + digest-crc (~> 0.4) + google-api-client (~> 0.23) + google-cloud-core (~> 1.2) + googleauth (>= 0.6.2, < 0.10.0) + googleauth (0.9.0) + faraday (~> 0.12) + jwt (>= 1.4, < 3.0) + memoist (~> 0.16) + multi_json (~> 1.11) + os (>= 0.9, < 2.0) + signet (~> 0.7) + highline (1.7.10) + http-cookie (1.0.3) + domain_name (~> 0.5) + httpclient (2.8.3) + json (2.1.0) + jwt (2.1.0) + memoist (0.16.0) + mini_magick (4.9.5) + mini_mime (1.0.2) + multi_json (1.13.1) + multi_xml (0.6.0) + multipart-post (2.0.0) + nanaimo (0.2.6) + naturally (2.2.0) + os (1.0.0) + plist (3.5.0) + public_suffix (2.0.5) + representable (3.0.4) + declarative (< 0.1.0) + declarative-option (< 0.2.0) + uber (< 0.2.0) + retriable (3.1.2) + rouge (2.0.7) + rubyzip (1.3.0) + security (0.1.3) + signet (0.12.0) + addressable (~> 2.3) + faraday (~> 0.9) + jwt (>= 1.5, < 3.0) + multi_json (~> 1.10) + simctl (1.6.5) + CFPropertyList + naturally + slack-notifier (2.3.2) + terminal-notifier (2.0.0) + terminal-table (1.8.0) + unicode-display_width (~> 1.1, >= 1.1.1) + tty-cursor (0.6.1) + tty-screen (0.6.5) + tty-spinner (0.9.0) + tty-cursor (~> 0.6.0) + uber (0.1.0) + unf (0.1.4) + unf_ext + unf_ext (0.0.7.5) + unicode-display_width (1.5.0) + word_wrap (1.0.0) + xcodeproj (1.13.0) + CFPropertyList (>= 2.3.3, < 4.0) + atomos (~> 0.1.3) + claide (>= 1.0.2, < 2.0) + colored2 (~> 3.1) + nanaimo (~> 0.2.6) + xcpretty (0.3.0) + rouge (~> 2.0.7) + xcpretty-travis-formatter (1.0.0) + xcpretty (~> 0.2, >= 0.0.7) + +PLATFORMS + ruby + +DEPENDENCIES + fastlane (~> 2.141.0) + +BUNDLED WITH + 1.16.6 diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index fd5e8106a0..5a892a9d45 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -46,6 +46,7 @@ 58781CC922AE7CA8009B9D8E /* RelayConstraints.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58781CC822AE7CA8009B9D8E /* RelayConstraints.swift */; }; 58781CCE22AE8918009B9D8E /* RelayConstraints.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58781CC822AE7CA8009B9D8E /* RelayConstraints.swift */; }; 58781CD522AFBA39009B9D8E /* RelaySelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58781CD422AFBA39009B9D8E /* RelaySelector.swift */; }; + 587A01FC23F1F0BE00B68763 /* SimulatorTunnelProviderHost.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587A01FB23F1F0BE00B68763 /* SimulatorTunnelProviderHost.swift */; }; 587AD7C623421D7000E93A53 /* TunnelConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587AD7C523421D7000E93A53 /* TunnelConfiguration.swift */; }; 587AD7C723421D8600E93A53 /* TunnelConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587AD7C523421D7000E93A53 /* TunnelConfiguration.swift */; }; 587AD7C82342237300E93A53 /* TunnelManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5835B7CB233B76CB0096D79F /* TunnelManager.swift */; }; @@ -79,6 +80,10 @@ 58B0A2AD238EE6EC00BC001D /* MullvadEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5840250322B11AB700E4CFEC /* MullvadEndpoint.swift */; }; 58B8743222B25A7600015324 /* WireguardAssociatedAddresses.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58B8743122B25A7600015324 /* WireguardAssociatedAddresses.swift */; }; 58B8743B22B788D200015324 /* PacketTunnelSettingsGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58B8743722B25EAB00015324 /* PacketTunnelSettingsGenerator.swift */; }; + 58BA692E23E99EFF009DC256 /* Locking.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BA692D23E99EFF009DC256 /* Locking.swift */; }; + 58BA692F23E99F5B009DC256 /* Locking.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BA692D23E99EFF009DC256 /* Locking.swift */; }; + 58BA693123EADA6A009DC256 /* SimulatorTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BA693023EADA6A009DC256 /* SimulatorTunnelProvider.swift */; }; + 58BA693223EAE1AE009DC256 /* SimulatorTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BA693023EADA6A009DC256 /* SimulatorTunnelProvider.swift */; }; 58BFA5C022A7C8A900A6173D /* MullvadAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58ADDB3D227B1CD900FAFEA7 /* MullvadAPI.swift */; }; 58BFA5C222A7C92900A6173D /* JsonRpc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58ADDB3B227B1BD200FAFEA7 /* JsonRpc.swift */; }; 58BFA5C322A7C93400A6173D /* RelayList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5888AD88227B18C40051EB06 /* RelayList.swift */; }; @@ -108,6 +113,8 @@ 58CE5E6E224146210008646E /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 58CE5E6C224146210008646E /* LaunchScreen.storyboard */; }; 58CE5E7C224146470008646E /* PacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58CE5E7B224146470008646E /* PacketTunnelProvider.swift */; }; 58CE5E81224146470008646E /* PacketTunnel.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 58CE5E79224146470008646E /* PacketTunnel.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 58D0C79E23F1CEBA00FE9BA7 /* SnapshotHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58D0C79D23F1CEBA00FE9BA7 /* SnapshotHelper.swift */; }; + 58D0C7A223F1CECF00FE9BA7 /* MullvadVPNScreenshots.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58D0C7A023F1CECF00FE9BA7 /* MullvadVPNScreenshots.swift */; }; 58EC4E6C23915325003F5C5B /* Bundle+MullvadVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58EC4E6B23915325003F5C5B /* Bundle+MullvadVersion.swift */; }; 58F19E35228C15BA00C7710B /* SpinnerActivityIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F19E34228C15BA00C7710B /* SpinnerActivityIndicatorView.swift */; }; /* End PBXBuildFile section */ @@ -120,6 +127,13 @@ remoteGlobalIDString = 58CE5E78224146470008646E; remoteInfo = PacketTunnel; }; + 58D0C79823F1CE7000FE9BA7 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 58CE5E58224146200008646E /* Project object */; + proxyType = 1; + remoteGlobalIDString = 58CE5E5F224146200008646E; + remoteInfo = MullvadVPN; + }; 58FBDAA122A52A6800EB69A3 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 58CE5E58224146200008646E /* Project object */; @@ -176,6 +190,7 @@ 5877152F23981F7B001F8237 /* WireguardKeysViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WireguardKeysViewController.swift; sourceTree = "<group>"; }; 58781CC822AE7CA8009B9D8E /* RelayConstraints.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayConstraints.swift; sourceTree = "<group>"; }; 58781CD422AFBA39009B9D8E /* RelaySelector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelaySelector.swift; sourceTree = "<group>"; }; + 587A01FB23F1F0BE00B68763 /* SimulatorTunnelProviderHost.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimulatorTunnelProviderHost.swift; sourceTree = "<group>"; }; 587AD7C523421D7000E93A53 /* TunnelConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TunnelConfiguration.swift; sourceTree = "<group>"; }; 587AD7C92342283900E93A53 /* Account.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Account.swift; sourceTree = "<group>"; }; 587B08DF229433EB000E6F17 /* LoginState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginState.swift; sourceTree = "<group>"; }; @@ -199,6 +214,8 @@ 58B0A2A4238EE67E00BC001D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; 58B8743122B25A7600015324 /* WireguardAssociatedAddresses.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WireguardAssociatedAddresses.swift; sourceTree = "<group>"; }; 58B8743722B25EAB00015324 /* PacketTunnelSettingsGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelSettingsGenerator.swift; sourceTree = "<group>"; }; + 58BA692D23E99EFF009DC256 /* Locking.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Locking.swift; sourceTree = "<group>"; }; + 58BA693023EADA6A009DC256 /* SimulatorTunnelProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimulatorTunnelProvider.swift; sourceTree = "<group>"; }; 58BFA5C522A7C97F00A6173D /* RelayCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayCache.swift; sourceTree = "<group>"; }; 58BFA5CB22A7CE1F00A6173D /* ApplicationConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationConfiguration.swift; sourceTree = "<group>"; }; 58C3A4B122456F1A00340BDB /* AccountInputGroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountInputGroupView.swift; sourceTree = "<group>"; }; @@ -224,7 +241,12 @@ 58CE5E7B224146470008646E /* PacketTunnelProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelProvider.swift; sourceTree = "<group>"; }; 58CE5E7D224146470008646E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; 58CE5E7E224146470008646E /* PacketTunnel.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = PacketTunnel.entitlements; sourceTree = "<group>"; }; + 58D0C79323F1CE7000FE9BA7 /* MullvadVPNScreenshots.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MullvadVPNScreenshots.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 58D0C79D23F1CEBA00FE9BA7 /* SnapshotHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SnapshotHelper.swift; sourceTree = "<group>"; }; + 58D0C79F23F1CECF00FE9BA7 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; + 58D0C7A023F1CECF00FE9BA7 /* MullvadVPNScreenshots.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MullvadVPNScreenshots.swift; sourceTree = "<group>"; }; 58EC4E6B23915325003F5C5B /* Bundle+MullvadVersion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+MullvadVersion.swift"; sourceTree = "<group>"; }; + 58ECD29123F178FD004298B6 /* Screenshots.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Screenshots.xcconfig; sourceTree = "<group>"; }; 58F19E34228C15BA00C7710B /* SpinnerActivityIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpinnerActivityIndicatorView.swift; sourceTree = "<group>"; }; 58FBDAA422A52BDA00EB69A3 /* PacketTunnel-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "PacketTunnel-Bridging-Header.h"; sourceTree = "<group>"; }; 58FBDAAA22A52DC500EB69A3 /* MullvadVPN-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "MullvadVPN-Bridging-Header.h"; sourceTree = "<group>"; }; @@ -255,6 +277,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 58D0C79023F1CE7000FE9BA7 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -279,9 +308,11 @@ 58CE5E57224146200008646E = { isa = PBXGroup; children = ( + 58ECD29023F178FD004298B6 /* Configurations */, 58CE5E62224146200008646E /* MullvadVPN */, 58CE5E7A224146470008646E /* PacketTunnel */, 58B0A2A1238EE67E00BC001D /* MullvadVPNTests */, + 58D0C79423F1CE7000FE9BA7 /* MullvadVPNScreenshots */, 0E15C74FDCF763609B367486 /* Frameworks */, 58CE5E61224146200008646E /* Products */, ); @@ -294,6 +325,7 @@ 58CE5E60224146200008646E /* MullvadVPN.app */, 58CE5E79224146470008646E /* PacketTunnel.appex */, 58B0A2A0238EE67E00BC001D /* MullvadVPNTests.xctest */, + 58D0C79323F1CE7000FE9BA7 /* MullvadVPNScreenshots.xctest */, ); name = Products; sourceTree = "<group>"; @@ -366,6 +398,9 @@ 58B8743122B25A7600015324 /* WireguardAssociatedAddresses.swift */, 5877152F23981F7B001F8237 /* WireguardKeysViewController.swift */, 58C6B35322BB87C4003C19AD /* WireguardPrivateKey.swift */, + 58BA692D23E99EFF009DC256 /* Locking.swift */, + 58BA693023EADA6A009DC256 /* SimulatorTunnelProvider.swift */, + 587A01FB23F1F0BE00B68763 /* SimulatorTunnelProviderHost.swift */, ); path = MullvadVPN; sourceTree = "<group>"; @@ -388,6 +423,24 @@ path = PacketTunnel; sourceTree = "<group>"; }; + 58D0C79423F1CE7000FE9BA7 /* MullvadVPNScreenshots */ = { + isa = PBXGroup; + children = ( + 58D0C79F23F1CECF00FE9BA7 /* Info.plist */, + 58D0C7A023F1CECF00FE9BA7 /* MullvadVPNScreenshots.swift */, + 58D0C79D23F1CEBA00FE9BA7 /* SnapshotHelper.swift */, + ); + path = MullvadVPNScreenshots; + sourceTree = "<group>"; + }; + 58ECD29023F178FD004298B6 /* Configurations */ = { + isa = PBXGroup; + children = ( + 58ECD29123F178FD004298B6 /* Screenshots.xcconfig */, + ); + path = Configurations; + sourceTree = "<group>"; + }; /* End PBXGroup section */ /* Begin PBXLegacyTarget section */ @@ -464,14 +517,32 @@ productReference = 58CE5E79224146470008646E /* PacketTunnel.appex */; productType = "com.apple.product-type.app-extension"; }; + 58D0C79223F1CE7000FE9BA7 /* MullvadVPNScreenshots */ = { + isa = PBXNativeTarget; + buildConfigurationList = 58D0C79A23F1CE7000FE9BA7 /* Build configuration list for PBXNativeTarget "MullvadVPNScreenshots" */; + buildPhases = ( + 58D0C78F23F1CE7000FE9BA7 /* Sources */, + 58D0C79023F1CE7000FE9BA7 /* Frameworks */, + 58D0C79123F1CE7000FE9BA7 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 58D0C79923F1CE7000FE9BA7 /* PBXTargetDependency */, + ); + name = MullvadVPNScreenshots; + productName = MullvadVPNScreenshots; + productReference = 58D0C79323F1CE7000FE9BA7 /* MullvadVPNScreenshots.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 58CE5E58224146200008646E /* Project object */ = { isa = PBXProject; attributes = { - LastSwiftUpdateCheck = 1120; - LastUpgradeCheck = 1000; + LastSwiftUpdateCheck = 1130; + LastUpgradeCheck = 1130; ORGANIZATIONNAME = "Mullvad VPN AB"; TargetAttributes = { 58B0A29F238EE67E00BC001D = { @@ -498,6 +569,10 @@ }; }; }; + 58D0C79223F1CE7000FE9BA7 = { + CreatedOnToolsVersion = 11.3; + TestTargetID = 58CE5E5F224146200008646E; + }; 58FBDA9722A519BC00EB69A3 = { CreatedOnToolsVersion = 10.2.1; }; @@ -517,9 +592,10 @@ projectRoot = ""; targets = ( 58CE5E5F224146200008646E /* MullvadVPN */, - 58CE5E78224146470008646E /* PacketTunnel */, 58FBDA9722A519BC00EB69A3 /* WireGuardGoBridge */, + 58CE5E78224146470008646E /* PacketTunnel */, 58B0A29F238EE67E00BC001D /* MullvadVPNTests */, + 58D0C79223F1CE7000FE9BA7 /* MullvadVPNScreenshots */, ); }; /* End PBXProject section */ @@ -549,6 +625,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 58D0C79123F1CE7000FE9BA7 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ @@ -611,8 +694,10 @@ buildActionMask = 2147483647; files = ( 58BFA5CC22A7CE1F00A6173D /* ApplicationConfiguration.swift in Sources */, + 58BA692E23E99EFF009DC256 /* Locking.swift in Sources */, 5840250122B1124600E4CFEC /* IpAddress+Codable.swift in Sources */, 58EC4E6C23915325003F5C5B /* Bundle+MullvadVersion.swift in Sources */, + 58BA693123EADA6A009DC256 /* SimulatorTunnelProvider.swift in Sources */, 582BB1B52295780F0055B6EF /* AccountExpiry.swift in Sources */, 582BB1B3229574F40055B6EF /* SettingsAccountCell.swift in Sources */, 58CCA010224249A1004F3011 /* ConnectViewController.swift in Sources */, @@ -641,6 +726,7 @@ 582BB1AF229566420055B6EF /* SettingsCell.swift in Sources */, 5873884D239E6D7E00E96C4E /* EmbeddedViewContainerView.swift in Sources */, 582650862384116F00FA7A86 /* ReplaceNilWithError.swift in Sources */, + 587A01FC23F1F0BE00B68763 /* SimulatorTunnelProviderHost.swift in Sources */, 5862805422428EF100F5A6E1 /* TranslucentButtonBlurView.swift in Sources */, 5888AD83227B11080051EB06 /* SelectLocationCell.swift in Sources */, 58CE5E66224146200008646E /* LoginViewController.swift in Sources */, @@ -685,9 +771,11 @@ 58AEEF662344A37400C9BBD5 /* KeychainError.swift in Sources */, 587AD7C82342237300E93A53 /* TunnelManager.swift in Sources */, 5840250222B1124600E4CFEC /* IpAddress+Codable.swift in Sources */, + 58BA693223EAE1AE009DC256 /* SimulatorTunnelProvider.swift in Sources */, 58C6B36522C10596003C19AD /* AnyIPEndpoint+Wireguard.swift in Sources */, 58CE5E7C224146470008646E /* PacketTunnelProvider.swift in Sources */, 586AA296234B696B00502875 /* WireguardAssociatedAddresses.swift in Sources */, + 58BA692F23E99F5B009DC256 /* Locking.swift in Sources */, 58B8743B22B788D200015324 /* PacketTunnelSettingsGenerator.swift in Sources */, 5860F1EB23AA4CF300CEA666 /* Logging.swift in Sources */, 5860F1C223A785C600CEA666 /* WireguardDevice.swift in Sources */, @@ -709,6 +797,15 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 58D0C78F23F1CE7000FE9BA7 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 58D0C7A223F1CECF00FE9BA7 /* MullvadVPNScreenshots.swift in Sources */, + 58D0C79E23F1CEBA00FE9BA7 /* SnapshotHelper.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ @@ -717,6 +814,11 @@ target = 58CE5E78224146470008646E /* PacketTunnel */; targetProxy = 58CE5E7F224146470008646E /* PBXContainerItemProxy */; }; + 58D0C79923F1CE7000FE9BA7 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 58CE5E5F224146200008646E /* MullvadVPN */; + targetProxy = 58D0C79823F1CE7000FE9BA7 /* PBXContainerItemProxy */; + }; 58FBDAA222A52A6800EB69A3 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 58FBDA9722A519BC00EB69A3 /* WireGuardGoBridge */; @@ -1008,6 +1110,48 @@ }; name = Release; }; + 58D0C79B23F1CE7000FE9BA7 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 58ECD29123F178FD004298B6 /* Screenshots.xcconfig */; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = G7CDBEG477; + INFOPLIST_FILE = MullvadVPNScreenshots/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 13.2; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = net.mullvad.MullvadVPNScreenshots; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = MullvadVPN; + }; + name = Debug; + }; + 58D0C79C23F1CE7000FE9BA7 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 58ECD29123F178FD004298B6 /* Screenshots.xcconfig */; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = G7CDBEG477; + INFOPLIST_FILE = MullvadVPNScreenshots/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 13.2; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = net.mullvad.MullvadVPNScreenshots; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = MullvadVPN; + }; + name = Release; + }; 58FBDA9822A519BC00EB69A3 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -1072,6 +1216,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + 58D0C79A23F1CE7000FE9BA7 /* Build configuration list for PBXNativeTarget "MullvadVPNScreenshots" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 58D0C79B23F1CE7000FE9BA7 /* Debug */, + 58D0C79C23F1CE7000FE9BA7 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 58FBDA9A22A519BC00EB69A3 /* Build configuration list for PBXLegacyTarget "WireGuardGoBridge" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/ios/MullvadVPN.xcodeproj/xcshareddata/xcschemes/MullvadVPN.xcscheme b/ios/MullvadVPN.xcodeproj/xcshareddata/xcschemes/MullvadVPN.xcscheme index 405be68500..310f6c6621 100644 --- a/ios/MullvadVPN.xcodeproj/xcshareddata/xcschemes/MullvadVPN.xcscheme +++ b/ios/MullvadVPN.xcodeproj/xcshareddata/xcschemes/MullvadVPN.xcscheme @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="UTF-8"?> <Scheme - LastUpgradeVersion = "1120" + LastUpgradeVersion = "1130" version = "1.3"> <BuildAction parallelizeBuildables = "YES" @@ -48,6 +48,16 @@ ReferencedContainer = "container:MullvadVPN.xcodeproj"> </BuildableReference> </TestableReference> + <TestableReference + skipped = "NO"> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "58D0C79223F1CE7000FE9BA7" + BuildableName = "MullvadVPNScreenshots.xctest" + BlueprintName = "MullvadVPNScreenshots" + ReferencedContainer = "container:MullvadVPN.xcodeproj"> + </BuildableReference> + </TestableReference> </Testables> </TestAction> <LaunchAction diff --git a/ios/MullvadVPN.xcodeproj/xcshareddata/xcschemes/MullvadVPNScreenshots.xcscheme b/ios/MullvadVPN.xcodeproj/xcshareddata/xcschemes/MullvadVPNScreenshots.xcscheme new file mode 100644 index 0000000000..dafedb1356 --- /dev/null +++ b/ios/MullvadVPN.xcodeproj/xcshareddata/xcschemes/MullvadVPNScreenshots.xcscheme @@ -0,0 +1,78 @@ +<?xml version="1.0" encoding="UTF-8"?> +<Scheme + LastUpgradeVersion = "1130" + version = "1.3"> + <BuildAction + parallelizeBuildables = "YES" + buildImplicitDependencies = "YES"> + <BuildActionEntries> + <BuildActionEntry + buildForTesting = "YES" + buildForRunning = "YES" + buildForProfiling = "NO" + buildForArchiving = "NO" + buildForAnalyzing = "NO"> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "58D0C79223F1CE7000FE9BA7" + BuildableName = "MullvadVPNScreenshots.xctest" + BlueprintName = "MullvadVPNScreenshots" + ReferencedContainer = "container:MullvadVPN.xcodeproj"> + </BuildableReference> + </BuildActionEntry> + </BuildActionEntries> + </BuildAction> + <TestAction + buildConfiguration = "Debug" + selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" + selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + shouldUseLaunchSchemeArgsEnv = "YES" + systemAttachmentLifetime = "keepNever"> + <Testables> + <TestableReference + skipped = "NO"> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "58D0C79223F1CE7000FE9BA7" + BuildableName = "MullvadVPNScreenshots.xctest" + BlueprintName = "MullvadVPNScreenshots" + ReferencedContainer = "container:MullvadVPN.xcodeproj"> + </BuildableReference> + </TestableReference> + </Testables> + </TestAction> + <LaunchAction + buildConfiguration = "Debug" + selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" + selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + launchStyle = "0" + useCustomWorkingDirectory = "NO" + ignoresPersistentStateOnLaunch = "NO" + debugDocumentVersioning = "YES" + debugServiceExtension = "internal" + allowLocationSimulation = "YES"> + </LaunchAction> + <ProfileAction + buildConfiguration = "Release" + shouldUseLaunchSchemeArgsEnv = "YES" + savedToolIdentifier = "" + useCustomWorkingDirectory = "NO" + debugDocumentVersioning = "YES"> + <MacroExpansion> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "58D0C79223F1CE7000FE9BA7" + BuildableName = "MullvadVPNScreenshots.xctest" + BlueprintName = "MullvadVPNScreenshots" + ReferencedContainer = "container:MullvadVPN.xcodeproj"> + </BuildableReference> + </MacroExpansion> + </ProfileAction> + <AnalyzeAction + buildConfiguration = "Debug"> + </AnalyzeAction> + <ArchiveAction + buildConfiguration = "Release" + revealArchiveInOrganizer = "YES"> + </ArchiveAction> +</Scheme> diff --git a/ios/MullvadVPN.xcodeproj/xcshareddata/xcschemes/MullvadVPNTests.xcscheme b/ios/MullvadVPN.xcodeproj/xcshareddata/xcschemes/MullvadVPNTests.xcscheme index 0c2fdada19..3d05981154 100644 --- a/ios/MullvadVPN.xcodeproj/xcshareddata/xcschemes/MullvadVPNTests.xcscheme +++ b/ios/MullvadVPN.xcodeproj/xcshareddata/xcschemes/MullvadVPNTests.xcscheme @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="UTF-8"?> <Scheme - LastUpgradeVersion = "1120" + LastUpgradeVersion = "1130" version = "1.3"> <BuildAction parallelizeBuildables = "YES" diff --git a/ios/MullvadVPN.xcodeproj/xcshareddata/xcschemes/PacketTunnel.xcscheme b/ios/MullvadVPN.xcodeproj/xcshareddata/xcschemes/PacketTunnel.xcscheme index 1c7c49e85b..5de677fa63 100644 --- a/ios/MullvadVPN.xcodeproj/xcshareddata/xcschemes/PacketTunnel.xcscheme +++ b/ios/MullvadVPN.xcodeproj/xcshareddata/xcschemes/PacketTunnel.xcscheme @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="UTF-8"?> <Scheme - LastUpgradeVersion = "1120" + LastUpgradeVersion = "1130" wasCreatedForAppExtension = "YES" version = "2.0"> <BuildAction @@ -43,6 +43,16 @@ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" shouldUseLaunchSchemeArgsEnv = "YES"> <Testables> + <TestableReference + skipped = "NO"> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "58D0C79223F1CE7000FE9BA7" + BuildableName = "MullvadVPNScreenshots.xctest" + BlueprintName = "MullvadVPNScreenshots" + ReferencedContainer = "container:MullvadVPN.xcodeproj"> + </BuildableReference> + </TestableReference> </Testables> </TestAction> <LaunchAction diff --git a/ios/MullvadVPN.xcodeproj/xcshareddata/xcschemes/WireGuardGoBridge.xcscheme b/ios/MullvadVPN.xcodeproj/xcshareddata/xcschemes/WireGuardGoBridge.xcscheme index 19216367a9..b68c2f954d 100644 --- a/ios/MullvadVPN.xcodeproj/xcshareddata/xcschemes/WireGuardGoBridge.xcscheme +++ b/ios/MullvadVPN.xcodeproj/xcshareddata/xcschemes/WireGuardGoBridge.xcscheme @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="UTF-8"?> <Scheme - LastUpgradeVersion = "1020" + LastUpgradeVersion = "1130" version = "1.3"> <BuildAction parallelizeBuildables = "YES" @@ -29,8 +29,6 @@ shouldUseLaunchSchemeArgsEnv = "YES"> <Testables> </Testables> - <AdditionalOptions> - </AdditionalOptions> </TestAction> <LaunchAction buildConfiguration = "Debug" @@ -51,8 +49,6 @@ ReferencedContainer = "container:MullvadVPN.xcodeproj"> </BuildableReference> </MacroExpansion> - <AdditionalOptions> - </AdditionalOptions> </LaunchAction> <ProfileAction buildConfiguration = "Release" diff --git a/ios/MullvadVPN/AppDelegate.swift b/ios/MullvadVPN/AppDelegate.swift index a190ea9622..84e0fd7f81 100644 --- a/ios/MullvadVPN/AppDelegate.swift +++ b/ios/MullvadVPN/AppDelegate.swift @@ -16,9 +16,18 @@ class AppDelegate: UIResponder, UIApplicationDelegate { let mainStoryboard = UIStoryboard(name: "Main", bundle: nil) + #if targetEnvironment(simulator) + let simulatorTunnelProvider = SimulatorTunnelProviderHost() + #endif + + private var loadTunnelSubscriber: AnyCancellable? func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + #if targetEnvironment(simulator) + SimulatorTunnelProvider.shared.delegate = simulatorTunnelProvider + #endif + let accountToken = Account.shared.token loadTunnelSubscriber = TunnelManager.shared.loadTunnel(accountToken: accountToken) @@ -71,4 +80,3 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } } - diff --git a/ios/MullvadVPN/Base.lproj/Main.storyboard b/ios/MullvadVPN/Base.lproj/Main.storyboard index 846746b2a1..aec8dd5f4f 100644 --- a/ios/MullvadVPN/Base.lproj/Main.storyboard +++ b/ios/MullvadVPN/Base.lproj/Main.storyboard @@ -40,17 +40,18 @@ <rect key="frame" x="0.0" y="0.0" width="375" height="93"/> <subviews> <imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="LogoIcon" translatesAutoresizingMaskIntoConstraints="NO" id="cKg-hE-JsS"> - <rect key="frame" x="12" y="6.5" width="98" height="100"/> + <rect key="frame" x="12" y="31.5" width="49.000000000000014" height="50"/> </imageView> <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="uXv-Tf-PET"> - <rect key="frame" x="311" y="32.5" width="48" height="48"/> + <rect key="frame" x="335" y="44.5" width="24" height="24"/> + <accessibility key="accessibilityConfiguration" identifier="SettingsButton"/> <state key="normal" image="IconSettings"/> <connections> <segue destination="Kqv-qu-mfF" kind="presentation" identifier="ShowSettings" id="cxu-NC-VeP"/> </connections> </button> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="MULLVAD VPN" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="dqy-A0-TdV"> - <rect key="frame" x="118" y="42" width="168" height="29"/> + <rect key="frame" x="69" y="42" width="168" height="29"/> <fontDescription key="fontDescription" type="boldSystem" pointSize="24"/> <color key="textColor" white="1" alpha="0.80000000000000004" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <nil key="highlightedColor"/> @@ -112,7 +113,7 @@ </constraints> </view> <imageView clipsSubviews="YES" userInteractionEnabled="NO" alpha="0.0" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="IconSuccess" translatesAutoresizingMaskIntoConstraints="NO" id="7ux-Tb-Fzq"> - <rect key="frame" x="127.5" y="137" width="120" height="120"/> + <rect key="frame" x="157.5" y="167" width="60" height="60"/> </imageView> <view contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" translatesAutoresizingMaskIntoConstraints="NO" id="V3j-Lb-fSQ" userLabel="Form"> <rect key="frame" x="0.0" y="251" width="375" height="125.5"/> @@ -134,6 +135,7 @@ <subviews> <textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder="0000 0000 0000 0000" textAlignment="natural" adjustsFontSizeToFit="NO" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="XOB-ct-yLU" userLabel="Account Text Field" customClass="AccountTextField" customModule="MullvadVPN" customModuleProvider="target"> <rect key="frame" x="0.0" y="0.0" width="327" height="48"/> + <accessibility key="accessibilityConfiguration" identifier="LoginTextField"/> <fontDescription key="fontDescription" type="system" pointSize="20"/> <textInputTraits key="textInputTraits" autocorrectionType="no" spellCheckingType="no" keyboardType="numberPad" enablesReturnKeyAutomatically="YES" smartDashesType="no" smartInsertDeleteType="no" smartQuotesType="no" textContentType="username"/> <connections> @@ -253,6 +255,9 @@ </barButtonItem> <barButtonItem style="plain" systemItem="flexibleSpace" id="Llz-4U-rOT"/> <barButtonItem title="Log in" style="done" id="0VH-wf-oEs"> + <userDefinedRuntimeAttributes> + <userDefinedRuntimeAttribute type="string" keyPath="accessibilityIdentifier" value="LoginBarButtonItem"/> + </userDefinedRuntimeAttributes> <connections> <action selector="doLogin" destination="BYZ-38-t0r" id="8Mv-Di-I6Y"/> </connections> @@ -275,6 +280,7 @@ <subviews> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="SECURE CONNECTION" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="HNy-mU-nui"> <rect key="frame" x="0.0" y="0.0" width="327" height="26.5"/> + <accessibility key="accessibilityConfiguration" identifier="SecureConnectionLabel"/> <fontDescription key="fontDescription" type="system" pointSize="22"/> <color key="textColor" name="Success"/> <nil key="highlightedColor"/> @@ -361,6 +367,7 @@ </constraints> </tableViewCellContentView> <color key="backgroundColor" name="Primary"/> + <accessibility key="accessibilityConfiguration" identifier="AccountCell"/> <connections> <outlet property="expiryLabel" destination="QeD-EQ-Ruo" id="sr0-cQ-JV1"/> <outlet property="titleLabel" destination="Lve-Kd-qTr" id="psd-kM-u1u"/> @@ -708,16 +715,16 @@ <rect key="frame" x="0.0" y="0.0" width="375" height="647"/> <subviews> <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="rkG-Xa-pEO" userLabel="Container"> - <rect key="frame" x="0.0" y="0.0" width="375" height="307.5"/> + <rect key="frame" x="0.0" y="0.0" width="375" height="295.5"/> <subviews> <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="nkx-Eb-7le" userLabel="Content"> - <rect key="frame" x="24" y="24" width="327" height="259.5"/> + <rect key="frame" x="24" y="24" width="327" height="247.5"/> <subviews> <view contentMode="scaleToFill" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="HzF-8Z-UBs" userLabel="Account number"> - <rect key="frame" x="0.0" y="0.0" width="327" height="58"/> + <rect key="frame" x="0.0" y="0.0" width="327" height="46"/> <subviews> <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="5ux-jY-AC5"> - <rect key="frame" x="0.0" y="0.0" width="327" height="58"/> + <rect key="frame" x="0.0" y="0.0" width="327" height="46"/> <subviews> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Account number" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="0L8-AT-A51"> <rect key="frame" x="0.0" y="0.0" width="327" height="17"/> @@ -747,7 +754,7 @@ </constraints> </view> <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="459-0n-9V2" userLabel="Expiry"> - <rect key="frame" x="0.0" y="82" width="327" height="45.5"/> + <rect key="frame" x="0.0" y="70" width="327" height="45.5"/> <subviews> <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="NMg-f0-BTW"> <rect key="frame" x="0.0" y="0.0" width="327" height="45.5"/> @@ -775,7 +782,7 @@ </constraints> </view> <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Acd-vw-Pu7" userLabel="Buttons"> - <rect key="frame" x="0.0" y="151.5" width="327" height="108"/> + <rect key="frame" x="0.0" y="139.5" width="327" height="108"/> <subviews> <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="24" translatesAutoresizingMaskIntoConstraints="NO" id="wNk-FP-mVD"> <rect key="frame" x="0.0" y="0.0" width="327" height="108"/> @@ -793,6 +800,7 @@ </button> <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="QHr-Lz-v6t" customClass="CustomButton" customModule="MullvadVPN" customModuleProvider="target"> <rect key="frame" x="0.0" y="66" width="327" height="42"/> + <accessibility key="accessibilityConfiguration" identifier="LogoutButton"/> <constraints> <constraint firstAttribute="height" constant="42" placeholder="YES" id="VYx-GQ-CIz"/> </constraints> @@ -935,31 +943,31 @@ </view> <prototypes> <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" reuseIdentifier="Cell" id="aFz-H5-sPu" customClass="SelectLocationCell" customModule="MullvadVPN" customModuleProvider="target"> - <rect key="frame" x="0.0" y="173" width="375" height="48.5"/> + <rect key="frame" x="0.0" y="173" width="375" height="43"/> <autoresizingMask key="autoresizingMask"/> <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="aFz-H5-sPu" id="6nQ-gT-vzf"> - <rect key="frame" x="0.0" y="0.0" width="375" height="48.5"/> + <rect key="frame" x="0.0" y="0.0" width="375" height="43"/> <autoresizingMask key="autoresizingMask"/> <subviews> <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="5ag-N4-pUg" customClass="RelayStatusIndicatorView" customModule="MullvadVPN" customModuleProvider="target"> - <rect key="frame" x="16" y="16.5" width="16" height="16"/> + <rect key="frame" x="16" y="13.5" width="16" height="16"/> <constraints> <constraint firstAttribute="height" constant="16" id="QWj-hh-I3P"/> <constraint firstAttribute="width" constant="16" id="TFV-yi-LXG"/> </constraints> </view> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="y7o-0b-MUV"> - <rect key="frame" x="44" y="11" width="42" height="26.5"/> + <rect key="frame" x="44" y="11" width="42" height="21"/> <fontDescription key="fontDescription" type="system" pointSize="17"/> <color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <nil key="highlightedColor"/> </label> <imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="IconTick" translatesAutoresizingMaskIntoConstraints="NO" id="e1o-Bl-zd5"> - <rect key="frame" x="0.0" y="0.5" width="48" height="48"/> + <rect key="frame" x="12" y="9.5" width="24" height="24"/> <color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> </imageView> <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="KaW-bN-I51"> - <rect key="frame" x="311" y="0.0" width="64" height="48.5"/> + <rect key="frame" x="311" y="0.0" width="64" height="43"/> <constraints> <constraint firstAttribute="width" constant="64" id="UU3-Di-65E"/> </constraints> @@ -1070,6 +1078,7 @@ </visualEffectView> <button opaque="NO" contentMode="scaleToFill" verticalHuggingPriority="251" verticalCompressionResistancePriority="751" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="vxU-Mt-fMo" customClass="CustomButton" customModule="MullvadVPN" customModuleProvider="target"> <rect key="frame" x="0.0" y="68.5" width="261" height="52.5"/> + <accessibility key="accessibilityConfiguration" identifier="ConnectButton"/> <state key="normal" title="Secure my connection" backgroundImage="SuccessButton"> <color key="titleColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> </state> @@ -1206,6 +1215,7 @@ <subviews> <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="d5t-ia-qxF" customClass="CustomButton" customModule="MullvadVPN" customModuleProvider="target"> <rect key="frame" x="0.0" y="0.0" width="261" height="52"/> + <accessibility key="accessibilityConfiguration" identifier="DisconnectButton"/> <state key="normal" title="Disconnect" backgroundImage="TranslucentDangerButton"> <color key="titleColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> </state> diff --git a/ios/MullvadVPN/Locking.swift b/ios/MullvadVPN/Locking.swift new file mode 100644 index 0000000000..8610f95309 --- /dev/null +++ b/ios/MullvadVPN/Locking.swift @@ -0,0 +1,27 @@ +// +// Locking.swift +// MullvadVPN +// +// Created by pronebird on 04/02/2020. +// Copyright © 2020 Mullvad VPN AB. All rights reserved. +// + +import Foundation + +extension NSLock { + func withCriticalBlock<T>(_ body: () -> T) -> T { + lock() + defer { unlock() } + + return body() + } +} + +extension NSRecursiveLock { + func withCriticalBlock<T>(_ body: () -> T) -> T { + lock() + defer { unlock() } + + return body() + } +} diff --git a/ios/MullvadVPN/MutuallyExclusive.swift b/ios/MullvadVPN/MutuallyExclusive.swift index faedaba35d..fc0bd254c5 100644 --- a/ios/MullvadVPN/MutuallyExclusive.swift +++ b/ios/MullvadVPN/MutuallyExclusive.swift @@ -122,13 +122,4 @@ private extension Publishers.MutuallyExclusive { } -private extension NSLock { - func withCriticalBlock<T>(_ body: () -> T) -> T { - lock() - defer { unlock() } - - return body() - } -} - typealias MutuallyExclusive = Publishers.MutuallyExclusive diff --git a/ios/MullvadVPN/PacketTunnelIpc.swift b/ios/MullvadVPN/PacketTunnelIpc.swift index b38f1815be..4dd581c6aa 100644 --- a/ios/MullvadVPN/PacketTunnelIpc.swift +++ b/ios/MullvadVPN/PacketTunnelIpc.swift @@ -95,9 +95,10 @@ extension PacketTunnelIpcHandler { } class PacketTunnelIpc { - let session: NETunnelProviderSession - init(session: NETunnelProviderSession) { + let session: VPNTunnelProviderSessionProtocol + + init(session: VPNTunnelProviderSessionProtocol) { self.session = session } diff --git a/ios/MullvadVPN/SettingsViewController.swift b/ios/MullvadVPN/SettingsViewController.swift index 9e001d21f1..ec280f92bf 100644 --- a/ios/MullvadVPN/SettingsViewController.swift +++ b/ios/MullvadVPN/SettingsViewController.swift @@ -48,6 +48,7 @@ class SettingsViewController: UITableViewController { let cell = cell as! SettingsBasicCell cell.titleLabel.text = NSLocalizedString("WireGuard key", comment: "") + cell.accessibilityIdentifier = "WireGuardKeyCell" } wireguardKeyRow.actionBlock = { [weak self] (indexPath) in diff --git a/ios/MullvadVPN/SimulatorTunnelProvider.swift b/ios/MullvadVPN/SimulatorTunnelProvider.swift new file mode 100644 index 0000000000..4fb22eb951 --- /dev/null +++ b/ios/MullvadVPN/SimulatorTunnelProvider.swift @@ -0,0 +1,272 @@ +// +// SimulatorTunnelProvider.swift +// MullvadVPN +// +// Created by pronebird on 05/02/2020. +// Copyright © 2020 Mullvad VPN AB. All rights reserved. +// + + +import Foundation +import NetworkExtension + +// MARK: - Formal conformances + +protocol VPNConnectionProtocol: NSObject { + var status: NEVPNStatus { get } + + func startVPNTunnel() throws + func startVPNTunnel(options: [String: NSObject]?) throws + func stopVPNTunnel() +} + +protocol VPNTunnelProviderSessionProtocol { + func sendProviderMessage(_ messageData: Data, responseHandler: ((Data?) -> Void)?) throws +} + +protocol VPNTunnelProviderManagerProtocol: Equatable { + associatedtype SelfType: VPNTunnelProviderManagerProtocol + associatedtype ConnectionType: VPNConnectionProtocol + + var isEnabled: Bool { get set } + var protocolConfiguration: NEVPNProtocol? { get set } + var localizedDescription: String? { get set } + var connection: ConnectionType { get } + + init() + + func loadFromPreferences(completionHandler: @escaping (Error?) -> Void) + func saveToPreferences(completionHandler: ((Error?) -> Void)?) + func removeFromPreferences(completionHandler: ((Error?) -> Void)?) + + static func loadAllFromPreferences(completionHandler: @escaping ([SelfType]?, Error?) -> Void) +} + +extension NEVPNConnection: VPNConnectionProtocol {} +extension NETunnelProviderSession: VPNTunnelProviderSessionProtocol {} +extension NETunnelProviderManager: VPNTunnelProviderManagerProtocol {} + +#if targetEnvironment(simulator) + +// MARK: - NEPacketTunnelProvider stubs + +protocol SimulatorTunnelProviderDelegate { + func startTunnel(options: [String: Any]?, completionHandler: @escaping (Error?) -> Void) + func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) + func handleAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)?) +} + +class SimulatorTunnelProvider { + static let shared = SimulatorTunnelProvider() + + private let lock = NSLock() + private var _delegate: SimulatorTunnelProviderDelegate? + + var delegate: SimulatorTunnelProviderDelegate! { + get { + lock.withCriticalBlock { _delegate } + } + set { + lock.withCriticalBlock { + _delegate = newValue + } + } + } + + private init() {} + + fileprivate func handleAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)? = nil) { + self.delegate.handleAppMessage(messageData, completionHandler: completionHandler) + } +} + +// MARK: - NEVPNConnection stubs + +class SimulatorVPNConnection: NSObject, VPNConnectionProtocol { + + private let lock = NSRecursiveLock() + + private var _status: NEVPNStatus = .disconnected + private(set) var status: NEVPNStatus { + get { + lock.withCriticalBlock { _status } + } + set { + lock.withCriticalBlock { + if newValue != _status { + _status = newValue + + // Send notification while holding the lock. This should enable the receiver + // to fetch the `SimulatorVPNConnection.status` before it changes. + postStatusDidChangeNotification() + } + } + } + } + + func startVPNTunnel() throws { + try startVPNTunnel(options: nil) + } + + func startVPNTunnel(options: [String: NSObject]?) throws { + status = .connecting + + SimulatorTunnelProvider.shared.delegate.startTunnel(options: options) { (error) in + if error == nil { + self.status = .connected + } else { + self.status = .disconnected + } + } + } + + func stopVPNTunnel() { + status = .disconnecting + + SimulatorTunnelProvider.shared.delegate.stopTunnel(with: .none) { + self.status = .disconnected + } + } + + private func postStatusDidChangeNotification() { + NotificationCenter.default.post(name: .NEVPNStatusDidChange, object: self) + } +} + +// MARK: - NETunnelProviderSession stubs + +class SimulatorTunnelProviderSession: SimulatorVPNConnection, VPNTunnelProviderSessionProtocol { + + func sendProviderMessage(_ messageData: Data, responseHandler: ((Data?) -> Void)?) throws { + SimulatorTunnelProvider.shared.handleAppMessage(messageData, completionHandler: responseHandler) + } + +} + +// MARK: - NETunnelProviderManager stubs + +/// A mock struct for tunnel configuration and connection +private struct SimulatorTunnelInfo { + /// A unique identifier for the configuration + var identifier = UUID().uuidString + + /// An associated VPN connection. + /// Intentionally initialized with a `SimulatorTunnelProviderSession` subclass which + /// implements the necessary protocol + var connection: SimulatorVPNConnection = SimulatorTunnelProviderSession() + + /// Whether configuration is enabled + var isEnabled = false + + /// Protocol configuration + var protocolConfiguration: NEVPNProtocol? + + /// Tunnel description + var localizedDescription: String? +} + +class SimulatorTunnelProviderManager: VPNTunnelProviderManagerProtocol, Equatable { + + static let tunnelsLock = NSRecursiveLock() + fileprivate static var tunnels = [SimulatorTunnelInfo]() + + private let lock = NSLock() + private var tunnelInfo: SimulatorTunnelInfo + private var identifier: String { + lock.withCriticalBlock { tunnelInfo.identifier } + } + + var isEnabled: Bool { + get { + lock.withCriticalBlock { tunnelInfo.isEnabled } + } + set { + lock.withCriticalBlock { + tunnelInfo.isEnabled = newValue + } + } + } + + var protocolConfiguration: NEVPNProtocol? { + get { + lock.withCriticalBlock { tunnelInfo.protocolConfiguration } + } + set { + lock.withCriticalBlock { + tunnelInfo.protocolConfiguration = newValue + } + } + } + + var localizedDescription: String? { + get { + lock.withCriticalBlock { tunnelInfo.localizedDescription } + } + set { + lock.withCriticalBlock { + tunnelInfo.localizedDescription = newValue + } + } + } + + var connection: SimulatorVPNConnection { + lock.withCriticalBlock { tunnelInfo.connection } + } + + static func loadAllFromPreferences(completionHandler: ([SimulatorTunnelProviderManager]?, Error?) -> Void) { + tunnelsLock.withCriticalBlock { + completionHandler(tunnels.map { SimulatorTunnelProviderManager(tunnelInfo: $0) }, nil) + } + } + + required init() { + self.tunnelInfo = SimulatorTunnelInfo() + } + + private init(tunnelInfo: SimulatorTunnelInfo) { + self.tunnelInfo = tunnelInfo + } + + func loadFromPreferences(completionHandler: (Error?) -> Void) { + Self.tunnelsLock.withCriticalBlock { + if let savedTunnel = Self.tunnels.first(where: { $0.identifier == self.identifier }) { + self.tunnelInfo = savedTunnel + + completionHandler(nil) + } else { + completionHandler(NEVPNError(.configurationInvalid)) + } + + } + } + + func saveToPreferences(completionHandler: ((Error?) -> Void)?) { + Self.tunnelsLock.withCriticalBlock { + if let index = Self.tunnels.firstIndex(where: { $0.identifier == self.identifier }) { + Self.tunnels[index] = self.tunnelInfo + } else { + Self.tunnels.append(self.tunnelInfo) + } + + completionHandler?(nil) + } + } + + func removeFromPreferences(completionHandler: ((Error?) -> Void)?) { + Self.tunnelsLock.withCriticalBlock { + if let index = Self.tunnels.firstIndex(where: { $0.identifier == self.identifier }) { + Self.tunnels.remove(at: index) + completionHandler?(nil) + } else { + completionHandler?(NEVPNError(.configurationReadWriteFailed)) + } + } + } + + static func == (lhs: SimulatorTunnelProviderManager, rhs: SimulatorTunnelProviderManager) -> Bool { + lhs.identifier == rhs.identifier + } + +} + +#endif diff --git a/ios/MullvadVPN/SimulatorTunnelProviderHost.swift b/ios/MullvadVPN/SimulatorTunnelProviderHost.swift new file mode 100644 index 0000000000..bbbfa5a7a2 --- /dev/null +++ b/ios/MullvadVPN/SimulatorTunnelProviderHost.swift @@ -0,0 +1,66 @@ +// +// SimulatorTunnelProviderHost.swift +// MullvadVPN +// +// Created by pronebird on 10/02/2020. +// Copyright © 2020 Mullvad VPN AB. All rights reserved. +// + +#if targetEnvironment(simulator) + +import Combine +import Foundation +import Network +import NetworkExtension + +class SimulatorTunnelProviderHost: SimulatorTunnelProviderDelegate { + + private let cancellableSet = CancellableSet() + private var connectionInfo: TunnelConnectionInfo? + + func startTunnel(options: [String: Any]?, completionHandler: @escaping (Error?) -> Void) { + DispatchQueue.main.async { + self.connectionInfo = TunnelConnectionInfo( + ipv4Relay: IPv4Endpoint(ip: IPv4Address("1.2.3.4")!, port: 53), + ipv6Relay: nil, + hostname: "se7-wireguard") + + completionHandler(nil) + } + } + + func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) { + DispatchQueue.main.async { + self.connectionInfo = nil + + completionHandler() + } + } + + func handleAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)?) { + PacketTunnelIpcHandler.decodeRequest(messageData: messageData) + .receive(on: DispatchQueue.main) + .flatMap { (request) -> AnyPublisher<AnyEncodable, PacketTunnelIpcHandlerError> in + switch request { + case .reloadConfiguration: + return Result.Publisher(AnyEncodable(true)) + .eraseToAnyPublisher() + + case .tunnelInformation: + return Result.Publisher(AnyEncodable(self.connectionInfo)) + .eraseToAnyPublisher() + } + }.flatMap({ (response) in + return PacketTunnelIpcHandler.encodeResponse(response: response) + }).autoDisposableSink(cancellableSet: cancellableSet, receiveCompletion: { (completion) in + if case .failure = completion { + completionHandler?(nil) + } + }, receiveValue: { (responseData) in + completionHandler?(responseData) + }) + } + +} + +#endif diff --git a/ios/MullvadVPN/TunnelManager.swift b/ios/MullvadVPN/TunnelManager.swift index 4f5046468b..bc80d8b0c3 100644 --- a/ios/MullvadVPN/TunnelManager.swift +++ b/ios/MullvadVPN/TunnelManager.swift @@ -283,6 +283,13 @@ extension TunnelState: CustomDebugStringConvertible { /// monitoring. class TunnelManager { + // Switch to stabs on simulator + #if targetEnvironment(simulator) + typealias TunnelProviderManagerType = SimulatorTunnelProviderManager + #else + typealias TunnelProviderManagerType = NETunnelProviderManager + #endif + static let shared = TunnelManager() // MARK: - Internal variables @@ -294,7 +301,7 @@ class TunnelManager { private let executionQueue = DispatchQueue(label: "net.mullvad.vpn.tunnel-manager.execution-queue") private let apiClient = MullvadAPI() - private var tunnelProvider: NETunnelProviderManager? + private var tunnelProvider: TunnelProviderManagerType? private var tunnelIpc: PacketTunnelIpc? /// A subscriber used for tunnel connection status changes @@ -318,7 +325,7 @@ class TunnelManager { /// account. The system tunnel is removed in case of inconsistency. func loadTunnel(accountToken: String?) -> AnyPublisher<(), TunnelManagerError> { MutuallyExclusive(exclusivityQueue: exclusivityQueue, executionQueue: executionQueue) { - NETunnelProviderManager.loadAllFromPreferences() + TunnelProviderManagerType.loadAllFromPreferences() .mapError { LoadTunnelError.loadTunnels($0) } .receive(on: self.executionQueue) .flatMap { (tunnels) -> AnyPublisher<(), LoadTunnelError> in @@ -668,7 +675,7 @@ class TunnelManager { } /// Set the instance of the active tunnel and add the tunnel status observer - private func setTunnelProvider(tunnelProvider: NETunnelProviderManager) { + private func setTunnelProvider(tunnelProvider: TunnelProviderManagerType) { guard self.tunnelProvider != tunnelProvider else { return } let connection = tunnelProvider.connection @@ -677,17 +684,20 @@ class TunnelManager { self.tunnelProvider = tunnelProvider // Set up tunnel IPC - if let session = connection as? NETunnelProviderSession { + if let session = connection as? VPNTunnelProviderSessionProtocol { self.tunnelIpc = PacketTunnelIpc(session: session) } // Register for tunnel connection status changes tunnelStatusSubscriber = NotificationCenter.default.publisher(for: .NEVPNStatusDidChange, object: connection) - .receive(on: executionQueue) - .sink { [weak self] (notification) in - guard let connection = notification.object as? NEVPNConnection else { return } + .map { notification in + (notification.object as? VPNConnectionProtocol)?.status + } + .receive(on: executionQueue) + .sink { [weak self] (connectionStatus) in + guard let connectionStatus = connectionStatus else { return } - self?.updateTunnelState(connectionStatus: connection.status) + self?.updateTunnelState(connectionStatus: connectionStatus) } // Update the existing connection status @@ -768,13 +778,13 @@ class TunnelManager { } } - private func setupTunnel(accountToken: String) -> AnyPublisher<NETunnelProviderManager, SetupTunnelError> { - NETunnelProviderManager.loadAllFromPreferences() + private func setupTunnel(accountToken: String) -> AnyPublisher<TunnelProviderManagerType, SetupTunnelError> { + TunnelProviderManagerType.loadAllFromPreferences() .receive(on: executionQueue) .mapError { SetupTunnelError.loadTunnels($0) } .map { (tunnels) in // Return the first available tunnel or make a new one - return tunnels?.first ?? NETunnelProviderManager() + return tunnels?.first ?? TunnelProviderManagerType() } .flatMap { (tunnelProvider) in TunnelConfigurationManager.getPersistentKeychainRef(account: accountToken) @@ -782,8 +792,9 @@ class TunnelManager { .map { (tunnelProvider, $0) } .publisher } - .flatMap { (tunnelProvider, passwordReference) -> AnyPublisher<NETunnelProviderManager, SetupTunnelError> in + .flatMap { (tunnelProvider, passwordReference) -> AnyPublisher<TunnelProviderManagerType, SetupTunnelError> in tunnelProvider.isEnabled = true + tunnelProvider.localizedDescription = "WireGuard" tunnelProvider.protocolConfiguration = self.makeProtocolConfiguration( accountToken: accountToken, passwordReference: passwordReference @@ -845,10 +856,10 @@ class TunnelManager { } /// Convenience methods to provide `Future` based alternatives for working with -/// `NETunnelProviderManager` -private extension NETunnelProviderManager { +/// `TunnelProviderManager` +private extension VPNTunnelProviderManagerProtocol { - class func loadAllFromPreferences() -> Future<[NETunnelProviderManager]?, Error> { + static func loadAllFromPreferences() -> Future<[SelfType]?, Error> { Future { (fulfill) in self.loadAllFromPreferences { (tunnels, error) in fulfill(error.flatMap { .failure($0) } ?? .success(tunnels)) diff --git a/ios/MullvadVPNScreenshots/Info.plist b/ios/MullvadVPNScreenshots/Info.plist new file mode 100644 index 0000000000..c6c1b7bc70 --- /dev/null +++ b/ios/MullvadVPNScreenshots/Info.plist @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>CFBundleDevelopmentRegion</key> + <string>$(DEVELOPMENT_LANGUAGE)</string> + <key>CFBundleExecutable</key> + <string>$(EXECUTABLE_NAME)</string> + <key>CFBundleIdentifier</key> + <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string> + <key>CFBundleInfoDictionaryVersion</key> + <string>6.0</string> + <key>CFBundleName</key> + <string>$(PRODUCT_NAME)</string> + <key>CFBundlePackageType</key> + <string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string> + <key>CFBundleShortVersionString</key> + <string>1.0</string> + <key>MullvadAccountToken</key> + <string>$(MULLVAD_ACCOUNT_TOKEN)</string> + <key>CFBundleVersion</key> + <string>1</string> +</dict> +</plist> diff --git a/ios/MullvadVPNScreenshots/MullvadVPNScreenshots.swift b/ios/MullvadVPNScreenshots/MullvadVPNScreenshots.swift new file mode 100644 index 0000000000..bc492c5d82 --- /dev/null +++ b/ios/MullvadVPNScreenshots/MullvadVPNScreenshots.swift @@ -0,0 +1,77 @@ +// +// MullvadVPNScreenshots.swift +// MullvadVPNScreenshots +// +// Created by pronebird on 04/02/2020. +// Copyright © 2020 Mullvad VPN AB. All rights reserved. +// + +import XCTest + +class MullvadVPNScreenshots: XCTestCase { + + override func setUp() { + // Put setup code here. This method is called before the invocation of each test method in the class. + + // In UI tests it is usually best to stop immediately when a failure occurs. + continueAfterFailure = false + + // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. + } + + override func tearDown() { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testTakeScreenshots() { + let accountToken = Bundle(for: Self.self).infoDictionary?["MullvadAccountToken"] as! String + + // UI tests must launch the application that they test. + let app = XCUIApplication() + setupSnapshot(app) + + app.launch() + + snapshot("Login") + + // Enter account token + let textField = app.textFields["LoginTextField"] + textField.tap() + textField.typeText(accountToken) + + // Tap "Log in" button to log in + app.toolbars["Toolbar"].buttons["LoginBarButtonItem"].tap() + + // Connect VPN + _ = app.buttons["ConnectButton"].waitForExistence(timeout: 10) + app.buttons["ConnectButton"].tap() + + // Wait for Disconnect button to appear + _ = app.buttons["DisconnectButton"].waitForExistence(timeout: 2) + + snapshot("MainSecured") + + // Open Settings + app.buttons["SettingsButton"].tap() + + // Tap on WireGuard key cell + _ = app.tables.cells["WireGuardKeyCell"].waitForExistence(timeout: 2) + app.tables.cells["WireGuardKeyCell"].tap() + + snapshot("WireGuardKeys") + + // Tap back button + app.navigationBars.buttons.firstMatch.tap() + + // Tap "Account" cell + _ = app.tables.cells["AccountCell"].waitForExistence(timeout: 2) + app.tables.cells["AccountCell"].tap() + + // Hit "Log out" button + _ = app.buttons["LogoutButton"].waitForExistence(timeout: 2) + app.buttons["LogoutButton"].tap() + + // Wait for Login view to appear after log out + _ = app.textFields["LoginTextField"].waitForExistence(timeout: 10) + } +} diff --git a/ios/MullvadVPNScreenshots/SnapshotHelper.swift b/ios/MullvadVPNScreenshots/SnapshotHelper.swift new file mode 100644 index 0000000000..aaa2a9a923 --- /dev/null +++ b/ios/MullvadVPNScreenshots/SnapshotHelper.swift @@ -0,0 +1,303 @@ +// +// SnapshotHelper.swift +// Example +// +// Created by Felix Krause on 10/8/15. +// + +// ----------------------------------------------------- +// IMPORTANT: When modifying this file, make sure to +// increment the version number at the very +// bottom of the file to notify users about +// the new SnapshotHelper.swift +// ----------------------------------------------------- + +import Foundation +import XCTest + +var deviceLanguage = "" +var locale = "" + +func setupSnapshot(_ app: XCUIApplication, waitForAnimations: Bool = true) { + Snapshot.setupSnapshot(app, waitForAnimations: waitForAnimations) +} + +func snapshot(_ name: String, waitForLoadingIndicator: Bool) { + if waitForLoadingIndicator { + Snapshot.snapshot(name) + } else { + Snapshot.snapshot(name, timeWaitingForIdle: 0) + } +} + +/// - Parameters: +/// - name: The name of the snapshot +/// - timeout: Amount of seconds to wait until the network loading indicator disappears. Pass `0` if you don't want to wait. +func snapshot(_ name: String, timeWaitingForIdle timeout: TimeInterval = 20) { + Snapshot.snapshot(name, timeWaitingForIdle: timeout) +} + +enum SnapshotError: Error, CustomDebugStringConvertible { + case cannotDetectUser + case cannotFindHomeDirectory + case cannotFindSimulatorHomeDirectory + case cannotAccessSimulatorHomeDirectory(String) + case cannotRunOnPhysicalDevice + + var debugDescription: String { + switch self { + case .cannotDetectUser: + return "Couldn't find Snapshot configuration files - can't detect current user " + case .cannotFindHomeDirectory: + return "Couldn't find Snapshot configuration files - can't detect `Users` dir" + case .cannotFindSimulatorHomeDirectory: + return "Couldn't find simulator home location. Please, check SIMULATOR_HOST_HOME env variable." + case .cannotAccessSimulatorHomeDirectory(let simulatorHostHome): + return "Can't prepare environment. Simulator home location is inaccessible. Does \(simulatorHostHome) exist?" + case .cannotRunOnPhysicalDevice: + return "Can't use Snapshot on a physical device." + } + } +} + +@objcMembers +open class Snapshot: NSObject { + static var app: XCUIApplication? + static var waitForAnimations = true + static var cacheDirectory: URL? + static var screenshotsDirectory: URL? { + return cacheDirectory?.appendingPathComponent("screenshots", isDirectory: true) + } + + open class func setupSnapshot(_ app: XCUIApplication, waitForAnimations: Bool = true) { + + Snapshot.app = app + Snapshot.waitForAnimations = waitForAnimations + + do { + let cacheDir = try pathPrefix() + Snapshot.cacheDirectory = cacheDir + setLanguage(app) + setLocale(app) + setLaunchArguments(app) + } catch let error { + NSLog(error.localizedDescription) + } + } + + class func setLanguage(_ app: XCUIApplication) { + guard let cacheDirectory = self.cacheDirectory else { + NSLog("CacheDirectory is not set - probably running on a physical device?") + return + } + + let path = cacheDirectory.appendingPathComponent("language.txt") + + do { + let trimCharacterSet = CharacterSet.whitespacesAndNewlines + deviceLanguage = try String(contentsOf: path, encoding: .utf8).trimmingCharacters(in: trimCharacterSet) + app.launchArguments += ["-AppleLanguages", "(\(deviceLanguage))"] + } catch { + NSLog("Couldn't detect/set language...") + } + } + + class func setLocale(_ app: XCUIApplication) { + guard let cacheDirectory = self.cacheDirectory else { + NSLog("CacheDirectory is not set - probably running on a physical device?") + return + } + + let path = cacheDirectory.appendingPathComponent("locale.txt") + + do { + let trimCharacterSet = CharacterSet.whitespacesAndNewlines + locale = try String(contentsOf: path, encoding: .utf8).trimmingCharacters(in: trimCharacterSet) + } catch { + NSLog("Couldn't detect/set locale...") + } + + if locale.isEmpty && !deviceLanguage.isEmpty { + locale = Locale(identifier: deviceLanguage).identifier + } + + if !locale.isEmpty { + app.launchArguments += ["-AppleLocale", "\"\(locale)\""] + } + } + + class func setLaunchArguments(_ app: XCUIApplication) { + guard let cacheDirectory = self.cacheDirectory else { + NSLog("CacheDirectory is not set - probably running on a physical device?") + return + } + + let path = cacheDirectory.appendingPathComponent("snapshot-launch_arguments.txt") + app.launchArguments += ["-FASTLANE_SNAPSHOT", "YES", "-ui_testing"] + + do { + let launchArguments = try String(contentsOf: path, encoding: String.Encoding.utf8) + let regex = try NSRegularExpression(pattern: "(\\\".+?\\\"|\\S+)", options: []) + let matches = regex.matches(in: launchArguments, options: [], range: NSRange(location: 0, length: launchArguments.count)) + let results = matches.map { result -> String in + (launchArguments as NSString).substring(with: result.range) + } + app.launchArguments += results + } catch { + NSLog("Couldn't detect/set launch_arguments...") + } + } + + open class func snapshot(_ name: String, timeWaitingForIdle timeout: TimeInterval = 20) { + if timeout > 0 { + waitForLoadingIndicatorToDisappear(within: timeout) + } + + NSLog("snapshot: \(name)") // more information about this, check out https://docs.fastlane.tools/actions/snapshot/#how-does-it-work + + if Snapshot.waitForAnimations { + sleep(1) // Waiting for the animation to be finished (kind of) + } + + #if os(OSX) + guard let app = self.app else { + NSLog("XCUIApplication is not set. Please call setupSnapshot(app) before snapshot().") + return + } + + app.typeKey(XCUIKeyboardKeySecondaryFn, modifierFlags: []) + #else + + guard self.app != nil else { + NSLog("XCUIApplication is not set. Please call setupSnapshot(app) before snapshot().") + return + } + + let screenshot = XCUIScreen.main.screenshot() + guard var simulator = ProcessInfo().environment["SIMULATOR_DEVICE_NAME"], let screenshotsDir = screenshotsDirectory else { return } + + do { + // The simulator name contains "Clone X of " inside the screenshot file when running parallelized UI Tests on concurrent devices + let regex = try NSRegularExpression(pattern: "Clone [0-9]+ of ") + let range = NSRange(location: 0, length: simulator.count) + simulator = regex.stringByReplacingMatches(in: simulator, range: range, withTemplate: "") + + let path = screenshotsDir.appendingPathComponent("\(simulator)-\(name).png") + try screenshot.pngRepresentation.write(to: path) + } catch let error { + NSLog("Problem writing screenshot: \(name) to \(screenshotsDir)/\(simulator)-\(name).png") + NSLog(error.localizedDescription) + } + #endif + } + + class func waitForLoadingIndicatorToDisappear(within timeout: TimeInterval) { + #if os(tvOS) + return + #endif + + guard let app = self.app else { + NSLog("XCUIApplication is not set. Please call setupSnapshot(app) before snapshot().") + return + } + + let networkLoadingIndicator = app.otherElements.deviceStatusBars.networkLoadingIndicators.element + let networkLoadingIndicatorDisappeared = XCTNSPredicateExpectation(predicate: NSPredicate(format: "exists == false"), object: networkLoadingIndicator) + _ = XCTWaiter.wait(for: [networkLoadingIndicatorDisappeared], timeout: timeout) + } + + class func pathPrefix() throws -> URL? { + let homeDir: URL + // on OSX config is stored in /Users/<username>/Library + // and on iOS/tvOS/WatchOS it's in simulator's home dir + #if os(OSX) + guard let user = ProcessInfo().environment["USER"] else { + throw SnapshotError.cannotDetectUser + } + + guard let usersDir = FileManager.default.urls(for: .userDirectory, in: .localDomainMask).first else { + throw SnapshotError.cannotFindHomeDirectory + } + + homeDir = usersDir.appendingPathComponent(user) + #else + #if arch(i386) || arch(x86_64) + guard let simulatorHostHome = ProcessInfo().environment["SIMULATOR_HOST_HOME"] else { + throw SnapshotError.cannotFindSimulatorHomeDirectory + } + guard let homeDirUrl = URL(string: simulatorHostHome) else { + throw SnapshotError.cannotAccessSimulatorHomeDirectory(simulatorHostHome) + } + homeDir = URL(fileURLWithPath: homeDirUrl.path) + #else + throw SnapshotError.cannotRunOnPhysicalDevice + #endif + #endif + return homeDir.appendingPathComponent("Library/Caches/tools.fastlane") + } +} + +private extension XCUIElementAttributes { + var isNetworkLoadingIndicator: Bool { + if hasWhiteListedIdentifier { return false } + + let hasOldLoadingIndicatorSize = frame.size == CGSize(width: 10, height: 20) + let hasNewLoadingIndicatorSize = frame.size.width.isBetween(46, and: 47) && frame.size.height.isBetween(2, and: 3) + + return hasOldLoadingIndicatorSize || hasNewLoadingIndicatorSize + } + + var hasWhiteListedIdentifier: Bool { + let whiteListedIdentifiers = ["GeofenceLocationTrackingOn", "StandardLocationTrackingOn"] + + return whiteListedIdentifiers.contains(identifier) + } + + func isStatusBar(_ deviceWidth: CGFloat) -> Bool { + if elementType == .statusBar { return true } + guard frame.origin == .zero else { return false } + + let oldStatusBarSize = CGSize(width: deviceWidth, height: 20) + let newStatusBarSize = CGSize(width: deviceWidth, height: 44) + + return [oldStatusBarSize, newStatusBarSize].contains(frame.size) + } +} + +private extension XCUIElementQuery { + var networkLoadingIndicators: XCUIElementQuery { + let isNetworkLoadingIndicator = NSPredicate { (evaluatedObject, _) in + guard let element = evaluatedObject as? XCUIElementAttributes else { return false } + + return element.isNetworkLoadingIndicator + } + + return self.containing(isNetworkLoadingIndicator) + } + + var deviceStatusBars: XCUIElementQuery { + guard let app = Snapshot.app else { + fatalError("XCUIApplication is not set. Please call setupSnapshot(app) before snapshot().") + } + + let deviceWidth = app.windows.firstMatch.frame.width + + let isStatusBar = NSPredicate { (evaluatedObject, _) in + guard let element = evaluatedObject as? XCUIElementAttributes else { return false } + + return element.isStatusBar(deviceWidth) + } + + return self.containing(isStatusBar) + } +} + +private extension CGFloat { + func isBetween(_ numberA: CGFloat, and numberB: CGFloat) -> Bool { + return numberA...numberB ~= self + } +} + +// Please don't remove the lines below +// They are used to detect outdated configuration files +// SnapshotHelperVersion [1.21] diff --git a/ios/README.md b/ios/README.md new file mode 100644 index 0000000000..48992bf2d7 --- /dev/null +++ b/ios/README.md @@ -0,0 +1,43 @@ +# Screenshots for AppStore + +The process of taking AppStore screenshots is automated using a UI Testing bundle and Snapshot tool, +a part of Fastlane tools. + +## Configuration + +The screenshot script uses the real account token to log in, which is provided via Xcode build +configuration. + +1. Create the build configuration using a template file: + + ``` + cp ios/Configurations/Screenshots.xcconfig.template ios/Configurations/Screenshots.xcconfig + ``` + +1. Edit the configuration file and put your account token without quotes: + + ``` + vim ios/Configurations/Screenshots.xcconfig + ``` + +## Prerequisitives + +1. Make sure you have [rvm](https://rvm.io) installed. +1. Install Ruby 2.5.1 or later using `rvm install <VERSION>`. +1. Install necessary third-party ruby gems: + + ``` + cd ios + bundle install + ``` + +## Take screenshots + +Run the following command to take screenshots: + +``` +cd ios +bundle exec fastlane snapshot +``` + +Once done all screenshots should be saved under `ios/Screenshots` folder. diff --git a/ios/Snapfile b/ios/Snapfile new file mode 100644 index 0000000000..6466ec7076 --- /dev/null +++ b/ios/Snapfile @@ -0,0 +1,33 @@ +# A list of devices you want to take the screenshots from +devices([ + "iPhone 8", + "iPhone 8 Plus", + "iPhone 11", + "iPhone 11 Pro", + "iPhone 11 Pro Max" +]) + +languages([ + "en-US" + # "de-DE", + # "it-IT", + # ["pt", "pt_BR"] # Portuguese with Brazilian locale +]) + +# The name of the scheme which contains the UI Tests +scheme("MullvadVPNScreenshots") + +# Where should the resulting screenshots be stored? +output_directory("./Screenshots") + +# Clear old screenshots +clear_previous_screenshots(true) + +# Erase simulator before running +erase_simulator(true) + +# Arguments to pass to the app on launch. See https://docs.fastlane.tools/actions/snapshot/#launch-arguments +# launch_arguments(["-favColor red"]) + +# For more information about all available options run +# fastlane action snapshot |
