summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--ios/Configurations/.gitignore1
-rw-r--r--ios/Configurations/Screenshots.xcconfig.template1
-rw-r--r--ios/Gemfile3
-rw-r--r--ios/Gemfile.lock157
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj159
-rw-r--r--ios/MullvadVPN.xcodeproj/xcshareddata/xcschemes/MullvadVPN.xcscheme12
-rw-r--r--ios/MullvadVPN.xcodeproj/xcshareddata/xcschemes/MullvadVPNScreenshots.xcscheme78
-rw-r--r--ios/MullvadVPN.xcodeproj/xcshareddata/xcschemes/MullvadVPNTests.xcscheme2
-rw-r--r--ios/MullvadVPN.xcodeproj/xcshareddata/xcschemes/PacketTunnel.xcscheme12
-rw-r--r--ios/MullvadVPN.xcodeproj/xcshareddata/xcschemes/WireGuardGoBridge.xcscheme6
-rw-r--r--ios/MullvadVPN/AppDelegate.swift10
-rw-r--r--ios/MullvadVPN/Base.lproj/Main.storyboard42
-rw-r--r--ios/MullvadVPN/Locking.swift27
-rw-r--r--ios/MullvadVPN/MutuallyExclusive.swift9
-rw-r--r--ios/MullvadVPN/PacketTunnelIpc.swift5
-rw-r--r--ios/MullvadVPN/SettingsViewController.swift1
-rw-r--r--ios/MullvadVPN/SimulatorTunnelProvider.swift272
-rw-r--r--ios/MullvadVPN/SimulatorTunnelProviderHost.swift66
-rw-r--r--ios/MullvadVPN/TunnelManager.swift41
-rw-r--r--ios/MullvadVPNScreenshots/Info.plist24
-rw-r--r--ios/MullvadVPNScreenshots/MullvadVPNScreenshots.swift77
-rw-r--r--ios/MullvadVPNScreenshots/SnapshotHelper.swift303
-rw-r--r--ios/README.md43
-rw-r--r--ios/Snapfile33
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