summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorEmīls <emils@mullvad.net>2023-07-12 11:11:53 +0200
committerEmīls <emils@mullvad.net>2023-07-12 11:11:53 +0200
commit7ffe7307ca2a969193d0eec4853248d5cdaa4fa7 (patch)
tree99d6faa07660617cb6a593104e6f53fafc1ddb02
parentbbc9840a8169923f87fb7305e20ca11165f24d59 (diff)
parent3cd00d09e8db96dcaa9a33185326782fafb77f10 (diff)
downloadmullvadvpn-7ffe7307ca2a969193d0eec4853248d5cdaa4fa7.tar.xz
mullvadvpn-7ffe7307ca2a969193d0eec4853248d5cdaa4fa7.zip
Merge branch 'redeeming-vouchers-on-creation-account-screen-ios-29'
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj88
-rw-r--r--ios/MullvadVPN/Containers/Root/HeaderBarView.swift12
-rw-r--r--ios/MullvadVPN/Containers/Root/RootConfiguration.swift13
-rw-r--r--ios/MullvadVPN/Containers/Root/RootContainerViewController.swift28
-rw-r--r--ios/MullvadVPN/Coordinators/App/AccountCoordinator.swift12
-rw-r--r--ios/MullvadVPN/Coordinators/App/AccountRedeemingVoucherCoordinator.swift68
-rw-r--r--ios/MullvadVPN/Coordinators/App/ApplicationCoordinator.swift116
-rw-r--r--ios/MullvadVPN/Coordinators/App/ApplicationRouter.swift16
-rw-r--r--ios/MullvadVPN/Coordinators/App/SettingsRedeemVoucherCoordinator.swift (renamed from ios/MullvadVPN/View controllers/RedeemVoucher/RedeemVoucherCoordinator.swift)21
-rw-r--r--ios/MullvadVPN/Coordinators/App/SetupAccountCompletedCoordinator.swift44
-rw-r--r--ios/MullvadVPN/Coordinators/App/WelcomeCoordinator.swift127
-rw-r--r--ios/MullvadVPN/SettingsManager/TunnelSettingsV2.swift3
-rw-r--r--ios/MullvadVPN/TunnelManager/RedeemVoucherOperation.swift3
-rw-r--r--ios/MullvadVPN/TunnelManager/SetAccountOperation.swift3
-rw-r--r--ios/MullvadVPN/UI appearance/UIMetrics.swift8
-rw-r--r--ios/MullvadVPN/View controllers/CreationAccount/Completed/SetupAccountCompletedContentView.swift153
-rw-r--r--ios/MullvadVPN/View controllers/CreationAccount/Completed/SetupAccountCompletedViewController.swift46
-rw-r--r--ios/MullvadVPN/View controllers/CreationAccount/Welcome/WelcomeContentView.swift246
-rw-r--r--ios/MullvadVPN/View controllers/CreationAccount/Welcome/WelcomeInteractor.swift25
-rw-r--r--ios/MullvadVPN/View controllers/CreationAccount/Welcome/WelcomeViewController.swift82
-rw-r--r--ios/MullvadVPN/View controllers/Login/LoginViewController.swift8
-rw-r--r--ios/MullvadVPN/View controllers/RedeemVoucher/RedeemVoucherContentView.swift6
-rw-r--r--ios/MullvadVPN/View controllers/RedeemVoucher/RedeemVoucherSucceededViewController.swift14
-rw-r--r--ios/Shared/ApplicationConfiguration.swift3
24 files changed, 1072 insertions, 73 deletions
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj
index 0c93ba9efe..8c92325d84 100644
--- a/ios/MullvadVPN.xcodeproj/project.pbxproj
+++ b/ios/MullvadVPN.xcodeproj/project.pbxproj
@@ -429,11 +429,19 @@
F028A56C2A34D8E600C0CAA3 /* RedeemVoucherSucceededViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F028A56B2A34D8E600C0CAA3 /* RedeemVoucherSucceededViewController.swift */; };
F028A56E2A34DCC600C0CAA3 /* RedeemVoucherInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = F028A56D2A34DCC600C0CAA3 /* RedeemVoucherInteractor.swift */; };
F03580252A13842C00E5DAFD /* IncreasedHitButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = F03580242A13842C00E5DAFD /* IncreasedHitButton.swift */; };
- F041CD562A38B0B7001B703B /* RedeemVoucherCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F041CD552A38B0B7001B703B /* RedeemVoucherCoordinator.swift */; };
+ F041CD562A38B0B7001B703B /* SettingsRedeemVoucherCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F041CD552A38B0B7001B703B /* SettingsRedeemVoucherCoordinator.swift */; };
F07BF2582A26112D00042943 /* InputTextFormatterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F07BF2572A26112D00042943 /* InputTextFormatterTests.swift */; };
F07BF2622A26279100042943 /* RedeemVoucherOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = F07BF2612A26279100042943 /* RedeemVoucherOperation.swift */; };
+ F07C0A052A52D4C3009825CA /* AccountRedeemingVoucherCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F07C0A042A52D4C3009825CA /* AccountRedeemingVoucherCoordinator.swift */; };
+ F07C0A072A52DA64009825CA /* SetupAccountCompletedCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F07C0A062A52DA64009825CA /* SetupAccountCompletedCoordinator.swift */; };
F07CFF2029F2720E008C0343 /* RegisteredDeviceInAppNotificationProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = F07CFF1F29F2720E008C0343 /* RegisteredDeviceInAppNotificationProvider.swift */; };
F0C2AEFD2A0BB5CC00986207 /* NotificationProviderIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0C2AEFC2A0BB5CC00986207 /* NotificationProviderIdentifier.swift */; };
+ F0E3618B2A4ADD2F00AEEF2B /* WelcomeContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0E3618A2A4ADD2F00AEEF2B /* WelcomeContentView.swift */; };
+ F0E8CC032A4C753B007ED3B4 /* WelcomeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0E8CC022A4C753B007ED3B4 /* WelcomeViewController.swift */; };
+ F0E8CC052A4CC88F007ED3B4 /* WelcomeCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0E8CC042A4CC88F007ED3B4 /* WelcomeCoordinator.swift */; };
+ F0E8CC0A2A4EE127007ED3B4 /* SetupAccountCompletedContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0E8CC092A4EE127007ED3B4 /* SetupAccountCompletedContentView.swift */; };
+ F0E8CC0C2A4EE672007ED3B4 /* SetupAccountCompletedViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0E8CC0B2A4EE672007ED3B4 /* SetupAccountCompletedViewController.swift */; };
+ F0E8E4BB2A56C9F100ED26A3 /* WelcomeInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0E8E4BA2A56C9F100ED26A3 /* WelcomeInteractor.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -1192,11 +1200,19 @@
F028A56B2A34D8E600C0CAA3 /* RedeemVoucherSucceededViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RedeemVoucherSucceededViewController.swift; sourceTree = "<group>"; };
F028A56D2A34DCC600C0CAA3 /* RedeemVoucherInteractor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RedeemVoucherInteractor.swift; sourceTree = "<group>"; };
F03580242A13842C00E5DAFD /* IncreasedHitButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncreasedHitButton.swift; sourceTree = "<group>"; };
- F041CD552A38B0B7001B703B /* RedeemVoucherCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RedeemVoucherCoordinator.swift; sourceTree = "<group>"; };
+ F041CD552A38B0B7001B703B /* SettingsRedeemVoucherCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsRedeemVoucherCoordinator.swift; sourceTree = "<group>"; };
F07BF2572A26112D00042943 /* InputTextFormatterTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InputTextFormatterTests.swift; sourceTree = "<group>"; };
F07BF2612A26279100042943 /* RedeemVoucherOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RedeemVoucherOperation.swift; sourceTree = "<group>"; };
+ F07C0A042A52D4C3009825CA /* AccountRedeemingVoucherCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountRedeemingVoucherCoordinator.swift; sourceTree = "<group>"; };
+ F07C0A062A52DA64009825CA /* SetupAccountCompletedCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupAccountCompletedCoordinator.swift; sourceTree = "<group>"; };
F07CFF1F29F2720E008C0343 /* RegisteredDeviceInAppNotificationProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegisteredDeviceInAppNotificationProvider.swift; sourceTree = "<group>"; };
F0C2AEFC2A0BB5CC00986207 /* NotificationProviderIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationProviderIdentifier.swift; sourceTree = "<group>"; };
+ F0E3618A2A4ADD2F00AEEF2B /* WelcomeContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeContentView.swift; sourceTree = "<group>"; };
+ F0E8CC022A4C753B007ED3B4 /* WelcomeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeViewController.swift; sourceTree = "<group>"; };
+ F0E8CC042A4CC88F007ED3B4 /* WelcomeCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeCoordinator.swift; sourceTree = "<group>"; };
+ F0E8CC092A4EE127007ED3B4 /* SetupAccountCompletedContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupAccountCompletedContentView.swift; sourceTree = "<group>"; };
+ F0E8CC0B2A4EE672007ED3B4 /* SetupAccountCompletedViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupAccountCompletedViewController.swift; sourceTree = "<group>"; };
+ F0E8E4BA2A56C9F100ED26A3 /* WelcomeInteractor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WelcomeInteractor.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -1526,6 +1542,7 @@
583FE01629C196E8006E85F9 /* View controllers */ = {
isa = PBXGroup;
children = (
+ F0E8E4B92A55593300ED26A3 /* CreationAccount */,
583FE02029C1A0B1006E85F9 /* Account */,
5878F4FA29CDA2D4003D4BE2 /* ChangeLog */,
583FE01D29C197C1006E85F9 /* DeviceList */,
@@ -1910,17 +1927,21 @@
isa = PBXGroup;
children = (
7AF0419D29E957EB00D492DD /* AccountCoordinator.swift */,
+ F07C0A042A52D4C3009825CA /* AccountRedeemingVoucherCoordinator.swift */,
58BBB39629717E0C00C8DB7C /* ApplicationCoordinator.swift */,
5893C6FB29C311E9009090D1 /* ApplicationRouter.swift */,
5878F50129CDB989003D4BE2 /* ChangeLogCoordinator.swift */,
+ F07C0A062A52DA64009825CA /* SetupAccountCompletedCoordinator.swift */,
58CAF9F92983E0C600BE19F7 /* LoginCoordinator.swift */,
583FE00D29C0D586006E85F9 /* OutOfTimeCoordinator.swift */,
5847D58C29B7740F008C3808 /* RevokedCoordinator.swift */,
586891CC29D452E4002A8278 /* SafariCoordinator.swift */,
587C92FD2986E28100FB9664 /* SelectLocationCoordinator.swift */,
58C3F4FA296C3AD500D72515 /* SettingsCoordinator.swift */,
+ F041CD552A38B0B7001B703B /* SettingsRedeemVoucherCoordinator.swift */,
587C92FF2986E2B600FB9664 /* TermsOfServiceCoordinator.swift */,
58F185A9298A3E3E00075977 /* TunnelCoordinator.swift */,
+ F0E8CC042A4CC88F007ED3B4 /* WelcomeCoordinator.swift */,
);
path = App;
sourceTree = "<group>";
@@ -2096,28 +2117,28 @@
58CE5E62224146200008646E /* MullvadVPN */ = {
isa = PBXGroup;
children = (
+ 58D7E3D629C78A130044B058 /* AddressCacheTracker */,
58CE5E63224146200008646E /* AppDelegate.swift */,
58C76A0A2A338E4300100D75 /* BackgroundTask.swift */,
- 58E25F802837BBBB002CFB2C /* SceneDelegate.swift */,
583FE02829C1B079006E85F9 /* Classes */,
- 5864AF0629C78816005B0CD9 /* Protocols */,
- 58B26E1F2943516500D5980C /* Notifications */,
- 583FE02729C1ADF7006E85F9 /* UI appearance */,
- 583FE02329C1AC9F006E85F9 /* Extensions */,
- 583FE01F29C197ED006E85F9 /* Views */,
58C774C929AB543C003A1A56 /* Containers */,
- 583FE01629C196E8006E85F9 /* View controllers */,
58CAF9F22983D32200BE19F7 /* Coordinators */,
- 5864859729A0D012006C5743 /* Presentation controllers */,
+ 583FE02329C1AC9F006E85F9 /* Extensions */,
+ 58B26E1F2943516500D5980C /* Notifications */,
586A950B2901250A007BAF2B /* Operations */,
+ 5864859729A0D012006C5743 /* Presentation controllers */,
+ 5864AF0629C78816005B0CD9 /* Protocols */,
+ 585DA87526B0249A00B8C587 /* RelayCacheTracker */,
+ 58E25F802837BBBB002CFB2C /* SceneDelegate.swift */,
+ 580F8B88281A79A7002E0998 /* SettingsManager */,
583FE02629C1ADB6006E85F9 /* SimulatorTunnelProvider */,
5846226F26E229CD0035F7C2 /* StorePaymentManager */,
- 58D7E3D629C78A130044B058 /* AddressCacheTracker */,
- 585DA87526B0249A00B8C587 /* RelayCacheTracker */,
+ 583FE02929C1B0E1006E85F9 /* Supporting Files */,
589E63DA28F7E9E7005FAB05 /* TransportMonitor */,
- 580F8B88281A79A7002E0998 /* SettingsManager */,
5823FA5726CE4A4100283BF8 /* TunnelManager */,
- 583FE02929C1B0E1006E85F9 /* Supporting Files */,
+ 583FE02729C1ADF7006E85F9 /* UI appearance */,
+ 583FE01629C196E8006E85F9 /* View controllers */,
+ 583FE01F29C197ED006E85F9 /* Views */,
);
path = MullvadVPN;
sourceTree = "<group>";
@@ -2270,7 +2291,6 @@
isa = PBXGroup;
children = (
F028A54A2A3370FA00C0CAA3 /* RedeemVoucherContentView.swift */,
- F041CD552A38B0B7001B703B /* RedeemVoucherCoordinator.swift */,
F028A56D2A34DCC600C0CAA3 /* RedeemVoucherInteractor.swift */,
F028A56B2A34D8E600C0CAA3 /* RedeemVoucherSucceededViewController.swift */,
F028A5692A34D4E700C0CAA3 /* RedeemVoucherViewController.swift */,
@@ -2279,6 +2299,34 @@
path = RedeemVoucher;
sourceTree = "<group>";
};
+ F0E361892A4ADCF500AEEF2B /* Welcome */ = {
+ isa = PBXGroup;
+ children = (
+ F0E8E4BA2A56C9F100ED26A3 /* WelcomeInteractor.swift */,
+ F0E3618A2A4ADD2F00AEEF2B /* WelcomeContentView.swift */,
+ F0E8CC022A4C753B007ED3B4 /* WelcomeViewController.swift */,
+ );
+ path = Welcome;
+ sourceTree = "<group>";
+ };
+ F0E8CC082A4EE0DC007ED3B4 /* Completed */ = {
+ isa = PBXGroup;
+ children = (
+ F0E8CC092A4EE127007ED3B4 /* SetupAccountCompletedContentView.swift */,
+ F0E8CC0B2A4EE672007ED3B4 /* SetupAccountCompletedViewController.swift */,
+ );
+ path = Completed;
+ sourceTree = "<group>";
+ };
+ F0E8E4B92A55593300ED26A3 /* CreationAccount */ = {
+ isa = PBXGroup;
+ children = (
+ F0E8CC082A4EE0DC007ED3B4 /* Completed */,
+ F0E361892A4ADCF500AEEF2B /* Welcome */,
+ );
+ path = CreationAccount;
+ sourceTree = "<group>";
+ };
/* End PBXGroup section */
/* Begin PBXHeadersBuildPhase section */
@@ -3131,6 +3179,7 @@
58C3F4FB296C3AD500D72515 /* SettingsCoordinator.swift in Sources */,
5846227126E229F20035F7C2 /* StoreSubscription.swift in Sources */,
58421030282D8A3C00F24E46 /* UpdateAccountDataOperation.swift in Sources */,
+ F0E8CC052A4CC88F007ED3B4 /* WelcomeCoordinator.swift in Sources */,
58FF2C03281BDE02009EF542 /* SettingsManager.swift in Sources */,
5803B4B02940A47300C23744 /* TunnelConfiguration.swift in Sources */,
587EB672271451E300123C75 /* PreferencesViewModel.swift in Sources */,
@@ -3141,7 +3190,7 @@
58BA693123EADA6A009DC256 /* SimulatorTunnelProvider.swift in Sources */,
587B753B2666467500DEF7E9 /* NotificationBannerView.swift in Sources */,
58B993B12608A34500BA7811 /* LoginContentView.swift in Sources */,
- F041CD562A38B0B7001B703B /* RedeemVoucherCoordinator.swift in Sources */,
+ F041CD562A38B0B7001B703B /* SettingsRedeemVoucherCoordinator.swift in Sources */,
5878A27529093A310096FC88 /* StorePaymentEvent.swift in Sources */,
7A7AD28D29DC677800480EF1 /* FirstTimeLaunch.swift in Sources */,
58B26E2A2943545A00D5980C /* NotificationManagerDelegate.swift in Sources */,
@@ -3183,15 +3232,18 @@
F028A54B2A3370FA00C0CAA3 /* RedeemVoucherContentView.swift in Sources */,
58FEEB58260B662E00A621A8 /* AutomaticKeyboardResponder.swift in Sources */,
5846227326E22A160035F7C2 /* StorePaymentObserver.swift in Sources */,
+ F0E3618B2A4ADD2F00AEEF2B /* WelcomeContentView.swift in Sources */,
58F2E146276A2C9900A79513 /* StopTunnelOperation.swift in Sources */,
E1187ABC289BBB850024E748 /* OutOfTimeViewController.swift in Sources */,
58CAF9FA2983E0C600BE19F7 /* LoginCoordinator.swift in Sources */,
+ F07C0A072A52DA64009825CA /* SetupAccountCompletedCoordinator.swift in Sources */,
58293FB125124117005D0BB5 /* CustomTextField.swift in Sources */,
58138E61294871C600684F0C /* DeviceDataThrottling.swift in Sources */,
5878A279290954790096FC88 /* TunnelViewControllerInteractor.swift in Sources */,
7A818F1F29F0305800C7F0F4 /* RootConfiguration.swift in Sources */,
582AE3102440A6CA00E6733A /* InputTextFormatter.swift in Sources */,
5820EDAB288FF0D2006BF4E4 /* DeviceRowView.swift in Sources */,
+ F0E8CC0C2A4EE672007ED3B4 /* SetupAccountCompletedViewController.swift in Sources */,
5846227726E22A7C0035F7C2 /* StorePaymentManagerDelegate.swift in Sources */,
58EF581125D69DB400AEBA94 /* StatusImageView.swift in Sources */,
58907D9524D17B4E00CFC3F5 /* DisconnectSplitButton.swift in Sources */,
@@ -3214,7 +3266,9 @@
7A1A26432A2612AE00B978AA /* PaymentAlertPresenter.swift in Sources */,
58CCA01822426713004F3011 /* AccountViewController.swift in Sources */,
5871FBA0254C26C00051A0A4 /* NSRegularExpression+IPAddress.swift in Sources */,
+ F0E8E4BB2A56C9F100ED26A3 /* WelcomeInteractor.swift in Sources */,
5878A27729093A4F0096FC88 /* StorePaymentBlockObserver.swift in Sources */,
+ F07C0A052A52D4C3009825CA /* AccountRedeemingVoucherCoordinator.swift in Sources */,
5868585524054096000B8131 /* AppButton.swift in Sources */,
5893C6FC29C311E9009090D1 /* ApplicationRouter.swift in Sources */,
58E25F812837BBBB002CFB2C /* SceneDelegate.swift in Sources */,
@@ -3279,6 +3333,7 @@
58677710290975E9006F721F /* SettingsInteractorFactory.swift in Sources */,
58B26E282943527300D5980C /* SystemNotificationProvider.swift in Sources */,
58CCA01222424D11004F3011 /* SettingsViewController.swift in Sources */,
+ F0E8CC0A2A4EE127007ED3B4 /* SetupAccountCompletedContentView.swift in Sources */,
581DA2752A1E283E0046ED47 /* WgKeyRotation.swift in Sources */,
06410DFE292CE18F00AFC18C /* KeychainSettingsStore.swift in Sources */,
58FB865526E8BF3100F188BC /* StorePaymentManagerError.swift in Sources */,
@@ -3302,6 +3357,7 @@
5871167F2910035700D41AAC /* PreferencesInteractor.swift in Sources */,
587AD7C623421D7000E93A53 /* TunnelSettingsV1.swift in Sources */,
58E20771274672CA00DE5D77 /* LaunchViewController.swift in Sources */,
+ F0E8CC032A4C753B007ED3B4 /* WelcomeViewController.swift in Sources */,
584D26C4270C855B004EA533 /* PreferencesDataSource.swift in Sources */,
58FD5BF024238EB300112C88 /* SKProduct+Formatting.swift in Sources */,
58B43C1925F77DB60002C8C3 /* TunnelControlView.swift in Sources */,
diff --git a/ios/MullvadVPN/Containers/Root/HeaderBarView.swift b/ios/MullvadVPN/Containers/Root/HeaderBarView.swift
index e938391be7..ef7e37688a 100644
--- a/ios/MullvadVPN/Containers/Root/HeaderBarView.swift
+++ b/ios/MullvadVPN/Containers/Root/HeaderBarView.swift
@@ -109,6 +109,12 @@ class HeaderBarView: UIView {
}
}
+ var isDeviceInfoHidden = false {
+ didSet {
+ deviceInfoHolder.arrangedSubviews.forEach { $0.isHidden = isDeviceInfoHidden }
+ }
+ }
+
private var isAccountButtonHidden = false {
didSet {
accountButton.isHidden = isAccountButtonHidden
@@ -118,7 +124,6 @@ class HeaderBarView: UIView {
private var timeLeft: Date? {
didSet {
if let timeLeft {
- timeLeftLabel.isHidden = false
let formattedTimeLeft = NSLocalizedString(
"TIME_LEFT_HEADER_VIEW",
tableName: "Account",
@@ -134,7 +139,7 @@ class HeaderBarView: UIView {
) ?? ""
)
} else {
- timeLeftLabel.isHidden = true
+ timeLeftLabel.text = ""
}
}
}
@@ -142,7 +147,6 @@ class HeaderBarView: UIView {
private var deviceName: String? {
didSet {
if let deviceName {
- deviceNameLabel.isHidden = false
let formattedDeviceName = NSLocalizedString(
"DEVICE_NAME_HEADER_VIEW",
tableName: "Account",
@@ -151,7 +155,7 @@ class HeaderBarView: UIView {
)
deviceNameLabel.text = String(format: formattedDeviceName, deviceName)
} else {
- deviceNameLabel.isHidden = true
+ deviceNameLabel.text = ""
}
}
}
diff --git a/ios/MullvadVPN/Containers/Root/RootConfiguration.swift b/ios/MullvadVPN/Containers/Root/RootConfiguration.swift
index b6426e16a9..c472f22a6a 100644
--- a/ios/MullvadVPN/Containers/Root/RootConfiguration.swift
+++ b/ios/MullvadVPN/Containers/Root/RootConfiguration.swift
@@ -8,6 +8,19 @@
import Foundation
+struct RootDeviceInfoViewModel {
+ let configuration: RootConfiguration
+ init(isPresentingAccountExpiryBanner: Bool, deviceState: DeviceState) {
+ configuration = RootConfiguration(
+ deviceName: deviceState.deviceData?.capitalizedName,
+ expiry: (isPresentingAccountExpiryBanner || (deviceState.accountData?.isExpired ?? true))
+ ? nil
+ : deviceState.accountData?.expiry,
+ showsAccountButton: deviceState.isLoggedIn
+ )
+ }
+}
+
struct RootConfiguration {
var deviceName: String?
var expiry: Date?
diff --git a/ios/MullvadVPN/Containers/Root/RootContainerViewController.swift b/ios/MullvadVPN/Containers/Root/RootContainerViewController.swift
index c1369118cb..b63206cdc1 100644
--- a/ios/MullvadVPN/Containers/Root/RootContainerViewController.swift
+++ b/ios/MullvadVPN/Containers/Root/RootContainerViewController.swift
@@ -44,12 +44,19 @@ protocol RootContainment {
/// Return true if the view controller prefers notification bar hidden
var prefersNotificationBarHidden: Bool { get }
+
+ /// Return true if the view controller prefers device info bar hidden
+ var prefersDeviceInfoBarHidden: Bool { get }
}
extension RootContainment {
var prefersNotificationBarHidden: Bool {
false
}
+
+ var prefersDeviceInfoBarHidden: Bool {
+ false
+ }
}
protocol RootContainerViewControllerDelegate: AnyObject {
@@ -517,6 +524,8 @@ class RootContainerViewController: UIViewController {
let alongSideAnimations = {
self.updateHeaderBarStyleFromChildPreferences(animated: shouldAnimate)
self.updateHeaderBarHiddenFromChildPreferences(animated: shouldAnimate)
+ self.updateNotificationBarHiddenFromChildPreferences()
+ self.updateDeviceInfoBarHiddenFromChildPreferences()
}
// Add new child controllers. The call to addChild() automatically calls child.willMove()
@@ -667,6 +676,21 @@ class RootContainerViewController: UIViewController {
}
}
+ private func updateDeviceInfoBarHiddenFromChildPreferences() {
+ if let conforming = topViewController as? RootContainment {
+ headerBarView.isDeviceInfoHidden = conforming.prefersDeviceInfoBarHidden
+ }
+ }
+
+ private func updateNotificationBarHiddenFromChildPreferences() {
+ if let notificationController,
+ let conforming = topViewController as? RootContainment {
+ conforming.prefersNotificationBarHidden
+ ? removeNotificationController(notificationController)
+ : addNotificationController(notificationController)
+ }
+ }
+
private func updateHeaderBarHiddenFromChildPreferences(animated: Bool) {
guard overrideHeaderBarHidden == nil else { return }
@@ -810,8 +834,4 @@ extension RootContainerViewController {
presentationContainerAccountButton?.isHidden = !configuration.showsAccountButton
headerBarView.update(configuration: configuration)
}
-
- func hideDeviceInfo() {
- update(configuration: RootConfiguration(showsAccountButton: false))
- }
}
diff --git a/ios/MullvadVPN/Coordinators/App/AccountCoordinator.swift b/ios/MullvadVPN/Coordinators/App/AccountCoordinator.swift
index 93da1a1404..e83b5c96bc 100644
--- a/ios/MullvadVPN/Coordinators/App/AccountCoordinator.swift
+++ b/ios/MullvadVPN/Coordinators/App/AccountCoordinator.swift
@@ -13,6 +13,11 @@ enum AccountDismissReason: Equatable {
case userLoggedOut
}
+enum AddedMoreCreditOption: Equatable {
+ case redeemingVoucher
+ case inAppPurchase
+}
+
final class AccountCoordinator: Coordinator, Presentable, Presenting {
private let interactor: AccountInteractor
private var accountController: AccountViewController?
@@ -28,6 +33,7 @@ final class AccountCoordinator: Coordinator, Presentable, Presenting {
}
var didFinish: ((AccountCoordinator, AccountDismissReason) -> Void)?
+ var didAddMoreCredit: ((AccountCoordinator, AddedMoreCreditOption) -> Void)?
init(
navigationController: UINavigationController,
@@ -68,12 +74,14 @@ final class AccountCoordinator: Coordinator, Presentable, Presenting {
}
private func navigateToRedeemVoucher() {
- let coordinator = RedeemVoucherCoordinator(
+ let coordinator = SettingsRedeemVoucherCoordinator(
navigationController: CustomNavigationController(),
interactor: RedeemVoucherInteractor(tunnelManager: interactor.tunnelManager)
)
- coordinator.didFinish = { redeemVoucherCoordinator in
+ coordinator.didFinish = { [weak self] redeemVoucherCoordinator in
redeemVoucherCoordinator.dismiss(animated: true)
+ guard let self else { return }
+ self.didAddMoreCredit?(self, .redeemingVoucher)
}
coordinator.didCancel = { redeemVoucherCoordinator in
redeemVoucherCoordinator.dismiss(animated: true)
diff --git a/ios/MullvadVPN/Coordinators/App/AccountRedeemingVoucherCoordinator.swift b/ios/MullvadVPN/Coordinators/App/AccountRedeemingVoucherCoordinator.swift
new file mode 100644
index 0000000000..160e4ff329
--- /dev/null
+++ b/ios/MullvadVPN/Coordinators/App/AccountRedeemingVoucherCoordinator.swift
@@ -0,0 +1,68 @@
+//
+// AccountRedeemingVoucherCoordinator.swift
+// MullvadVPN
+//
+// Created by Mojgan on 2023-07-03.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import MullvadREST
+import UIKit
+
+class AccountRedeemingVoucherCoordinator: Coordinator, Presentable {
+ private let navigationController: RootContainerViewController
+ private let viewController: RedeemVoucherViewController
+
+ var didFinish: ((AccountRedeemingVoucherCoordinator) -> Void)?
+ var didCancel: ((AccountRedeemingVoucherCoordinator) -> Void)?
+
+ var presentedViewController: UIViewController {
+ viewController
+ }
+
+ init(
+ navigationController: RootContainerViewController,
+ interactor: RedeemVoucherInteractor
+ ) {
+ self.navigationController = navigationController
+ viewController = RedeemVoucherViewController(interactor: interactor)
+ }
+
+ func start() {
+ viewController.delegate = self
+ navigationController.pushViewController(viewController, animated: true)
+ }
+}
+
+extension AccountRedeemingVoucherCoordinator: RedeemVoucherViewControllerDelegate {
+ func redeemVoucherDidSucceed(_ controller: RedeemVoucherViewController, with response: REST.SubmitVoucherResponse) {
+ let controller = RedeemVoucherSucceededViewController(timeAddedComponents: response.dateComponents)
+ controller.delegate = self
+ navigationController.pushViewController(controller, animated: true)
+ }
+
+ func redeemVoucherDidCancel(_ controller: RedeemVoucherViewController) {
+ didCancel?(self)
+ }
+}
+
+extension AccountRedeemingVoucherCoordinator: RedeemVoucherSucceededViewControllerDelegate {
+ func titleForAction(in controller: RedeemVoucherSucceededViewController) -> String {
+ NSLocalizedString(
+ "REDEEM_VOUCHER_DISMISS_BUTTON",
+ tableName: "Welcome",
+ value: "Next",
+ comment: ""
+ )
+ }
+
+ func redeemVoucherSucceededViewControllerDidFinish(_ controller: RedeemVoucherSucceededViewController) {
+ let coordinator = SetupAccountCompletedCoordinator(navigationController: navigationController)
+ coordinator.didFinish = { [self] coordinator in
+ coordinator.removeFromParent()
+ didFinish?(self)
+ }
+ addChild(coordinator)
+ coordinator.start(animated: true)
+ }
+}
diff --git a/ios/MullvadVPN/Coordinators/App/ApplicationCoordinator.swift b/ios/MullvadVPN/Coordinators/App/ApplicationCoordinator.swift
index 7707c18886..90109884a1 100644
--- a/ios/MullvadVPN/Coordinators/App/ApplicationCoordinator.swift
+++ b/ios/MullvadVPN/Coordinators/App/ApplicationCoordinator.swift
@@ -155,6 +155,12 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo
case .main:
presentMain(animated: animated, completion: completion)
+
+ case .welcome:
+ presentWelcome(animated: animated, completion: completion)
+
+ case .setupAccountCompleted:
+ presentSetupAccountCompleted(animated: animated, completion: completion)
}
}
@@ -180,8 +186,23 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo
let dismissedRoute = context.dismissedRoutes.first!
assert(context.dismissedRoutes.count == 1)
- if case .outOfTime = dismissedRoute.route {
- let coordinator = dismissedRoute.coordinator as! OutOfTimeCoordinator
+ if dismissedRoute.route == .outOfTime {
+ guard let coordinator = dismissedRoute.coordinator as? OutOfTimeCoordinator else {
+ completion()
+ return assertionFailure("Unhandled coordinator for \(dismissedRoute.route)")
+ }
+
+ coordinator.popFromNavigationStack(
+ animated: context.isAnimated,
+ completion: completion
+ )
+
+ coordinator.removeFromParent()
+ } else if dismissedRoute.route == .welcome {
+ guard let coordinator = dismissedRoute.coordinator as? WelcomeCoordinator else {
+ completion()
+ return assertionFailure("Unhandled coordinator for \(dismissedRoute.route)")
+ }
coordinator.popFromNavigationStack(
animated: context.isAnimated,
@@ -293,7 +314,7 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo
return .login
case let .loggedIn(accountData, _):
- return accountData.isExpired ? .outOfTime : .main
+ return accountData.isExpired ? (accountData.isNew ? .welcome : .outOfTime) : .main
}
}
@@ -306,7 +327,6 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo
private func didDismissAccount(_ reason: AccountDismissReason) {
if isPad {
router.dismiss(.account, animated: true)
-
if reason == .userLoggedOut {
router.dismissAll(.primary, animated: true)
continueFlow(animated: true)
@@ -316,7 +336,6 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo
router.dismissAll(.primary, animated: false)
continueFlow(animated: false)
}
-
router.dismiss(.account, animated: true)
}
}
@@ -553,6 +572,44 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo
}
}
+ private func presentWelcome(animated: Bool, completion: @escaping (Coordinator) -> Void) {
+ let coordinator = WelcomeCoordinator(
+ navigationController: horizontalFlowController,
+ storePaymentManager: storePaymentManager,
+ tunnelManager: tunnelManager
+ )
+
+ coordinator.didFinishPayment = { [weak self] coordinator in
+ guard let self else { return }
+ router.dismiss(.welcome, animated: false)
+ continueFlow(animated: false)
+ }
+
+ addChild(coordinator)
+ coordinator.start(animated: animated)
+
+ beginHorizontalFlow(animated: animated) {
+ completion(coordinator)
+ }
+ }
+
+ private func presentSetupAccountCompleted(animated: Bool, completion: @escaping (Coordinator) -> Void) {
+ let coordinator = SetupAccountCompletedCoordinator(navigationController: horizontalFlowController)
+
+ coordinator.didFinish = { [weak self] coordinator in
+ guard let self else { return }
+ coordinator.removeFromParent()
+ continueFlow(animated: false)
+ }
+
+ addChild(coordinator)
+ coordinator.start(animated: animated)
+
+ beginHorizontalFlow(animated: animated) {
+ completion(coordinator)
+ }
+ }
+
private func shouldDismissOutOfTime() -> Bool {
!(tunnelManager.deviceState.accountData?.isExpired ?? false)
}
@@ -633,6 +690,13 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo
self?.didDismissAccount(reason)
}
+ coordinator.didAddMoreCredit = { [weak self] coordinator, option in
+ guard let self,
+ self.isPresentingWelcome else { return }
+ self.router.dismiss(.welcome, animated: false)
+ self.router.present(.setupAccountCompleted, animated: false)
+ }
+
coordinator.start(animated: animated)
presentChild(
@@ -712,22 +776,24 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo
switch deviceState {
case let .loggedIn(accountData, _):
- updateOutOfTimeTimer(accountData: accountData)
+
+ // Account creation is being shown
+ guard !isPresentingWelcome && !accountData.isNew else { return }
// Handle transition to and from expired state.
- let accountWasExpired = previousDeviceState.accountData?.isExpired ?? false
- switch (accountWasExpired, accountData.isExpired) {
+ switch (previousDeviceState.accountData?.isExpired ?? false, accountData.isExpired) {
+ // add more credit
case (true, false):
+ updateOutOfTimeTimer(accountData: accountData)
continueFlow(animated: true)
router.dismiss(.outOfTime, animated: true)
-
+ // account was expired
case (false, true):
router.present(.outOfTime, animated: true)
default:
break
}
-
case .revoked:
cancelOutOfTimeTimer()
router.present(.revoked, animated: true)
@@ -737,21 +803,12 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo
}
private func updateDeviceInfo(deviceState: DeviceState) {
- switch deviceState {
- case let .loggedIn(storedAccountData, _):
- let configuration = RootConfiguration(
- deviceName: deviceState.deviceData?.capitalizedName,
- expiry: (isPresentingAccountExpiryBanner || storedAccountData.isExpired)
- ? nil
- : deviceState.accountData?.expiry,
- showsAccountButton: true
- )
- primaryNavigationContainer.update(configuration: configuration)
- secondaryNavigationContainer.update(configuration: configuration)
- case .loggedOut, .revoked:
- primaryNavigationContainer.hideDeviceInfo()
- secondaryNavigationContainer.hideDeviceInfo()
- }
+ let rootDeviceInfoViewModel = RootDeviceInfoViewModel(
+ isPresentingAccountExpiryBanner: isPresentingAccountExpiryBanner,
+ deviceState: deviceState
+ )
+ self.primaryNavigationContainer.update(configuration: rootDeviceInfoViewModel.configuration)
+ self.secondaryNavigationContainer.update(configuration: rootDeviceInfoViewModel.configuration)
}
// MARK: - Out of time
@@ -788,12 +845,17 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo
/// Returns `true` if settings are being presented.
var isPresentingSettings: Bool {
- router.isPresenting(.settings)
+ router.isPresenting(group: .settings)
}
/// Returns `true` if account controller is being presented.
var isPresentingAccount: Bool {
- router.isPresenting(.account)
+ router.isPresenting(group: .account)
+ }
+
+ /// Returns `true` if welcome controller is being presented.
+ private var isPresentingWelcome: Bool {
+ router.isPresenting(route: .welcome)
}
// MARK: - UISplitViewControllerDelegate
diff --git a/ios/MullvadVPN/Coordinators/App/ApplicationRouter.swift b/ios/MullvadVPN/Coordinators/App/ApplicationRouter.swift
index 3ba14b77f3..c83bdf3fe1 100644
--- a/ios/MullvadVPN/Coordinators/App/ApplicationRouter.swift
+++ b/ios/MullvadVPN/Coordinators/App/ApplicationRouter.swift
@@ -85,7 +85,7 @@ enum AppRoute: Equatable, Hashable {
/**
Routes that are part of primary horizontal navigation group.
*/
- case tos, changelog, login, main, revoked, outOfTime
+ case tos, changelog, login, main, revoked, outOfTime, welcome, setupAccountCompleted
/**
Returns `true` when only one route of a kind can be displayed.
@@ -115,7 +115,7 @@ enum AppRoute: Equatable, Hashable {
*/
var routeGroup: AppRouteGroup {
switch self {
- case .tos, .changelog, .login, .main, .revoked, .outOfTime:
+ case .tos, .changelog, .login, .main, .revoked, .outOfTime, .welcome, .setupAccountCompleted:
return .primary
case .selectLocation:
return .selectLocation
@@ -346,11 +346,21 @@ final class ApplicationRouter {
/**
Returns `true` is the given route group is currently being presented.
*/
- func isPresenting(_ group: AppRouteGroup) -> Bool {
+ func isPresenting(group: AppRouteGroup) -> Bool {
modalStack.contains(group)
}
/**
+ Returns `true` if is the given route is currently being presented.
+ */
+ func isPresenting(route: AppRoute) -> Bool {
+ guard let presentedRoute = presentedRoutes[route.routeGroup] else {
+ return false
+ }
+ return presentedRoute.contains(where: { $0.route == route })
+ }
+
+ /**
Enqueue route for presetnation.
*/
func present(_ route: AppRoute, animated: Bool = true) {
diff --git a/ios/MullvadVPN/View controllers/RedeemVoucher/RedeemVoucherCoordinator.swift b/ios/MullvadVPN/Coordinators/App/SettingsRedeemVoucherCoordinator.swift
index da0a6c93f0..9fa3de6f81 100644
--- a/ios/MullvadVPN/View controllers/RedeemVoucher/RedeemVoucherCoordinator.swift
+++ b/ios/MullvadVPN/Coordinators/App/SettingsRedeemVoucherCoordinator.swift
@@ -1,5 +1,5 @@
//
-// RedeemVoucherCoordinator.swift
+// SettingsRedeemVoucherCoordinator.swift
// MullvadVPN
//
// Created by Mojgan on 2023-06-13.
@@ -10,11 +10,11 @@ import Foundation
import MullvadREST
import UIKit
-final class RedeemVoucherCoordinator: Coordinator, Presentable {
+final class SettingsRedeemVoucherCoordinator: Coordinator, Presentable {
private let navigationController: UINavigationController
private let viewController: RedeemVoucherViewController
- var didFinish: ((RedeemVoucherCoordinator) -> Void)?
- var didCancel: ((RedeemVoucherCoordinator) -> Void)?
+ var didFinish: ((SettingsRedeemVoucherCoordinator) -> Void)?
+ var didCancel: ((SettingsRedeemVoucherCoordinator) -> Void)?
init(
navigationController: UINavigationController,
@@ -35,7 +35,7 @@ final class RedeemVoucherCoordinator: Coordinator, Presentable {
}
}
-extension RedeemVoucherCoordinator: RedeemVoucherViewControllerDelegate {
+extension SettingsRedeemVoucherCoordinator: RedeemVoucherViewControllerDelegate {
func redeemVoucherDidSucceed(
_ controller: RedeemVoucherViewController,
with response: REST.SubmitVoucherResponse
@@ -50,7 +50,16 @@ extension RedeemVoucherCoordinator: RedeemVoucherViewControllerDelegate {
}
}
-extension RedeemVoucherCoordinator: RedeemVoucherSucceededViewControllerDelegate {
+extension SettingsRedeemVoucherCoordinator: RedeemVoucherSucceededViewControllerDelegate {
+ func titleForAction(in controller: RedeemVoucherSucceededViewController) -> String {
+ NSLocalizedString(
+ "REDEEM_VOUCHER_DISMISS_BUTTON",
+ tableName: "RedeemVoucher",
+ value: "Got it!",
+ comment: ""
+ )
+ }
+
func redeemVoucherSucceededViewControllerDidFinish(_ controller: RedeemVoucherSucceededViewController) {
didFinish?(self)
}
diff --git a/ios/MullvadVPN/Coordinators/App/SetupAccountCompletedCoordinator.swift b/ios/MullvadVPN/Coordinators/App/SetupAccountCompletedCoordinator.swift
new file mode 100644
index 0000000000..262336e287
--- /dev/null
+++ b/ios/MullvadVPN/Coordinators/App/SetupAccountCompletedCoordinator.swift
@@ -0,0 +1,44 @@
+//
+// SetupAccountCompletedCoordinator.swift
+// MullvadVPN
+//
+// Created by Mojgan on 2023-07-03.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import UIKit
+
+class SetupAccountCompletedCoordinator: Coordinator, Presenting {
+ private let navigationController: RootContainerViewController
+ private var viewController: SetupAccountCompletedViewController?
+
+ var didFinish: ((SetupAccountCompletedCoordinator) -> Void)?
+
+ var presentationContext: UIViewController {
+ viewController ?? navigationController
+ }
+
+ init(navigationController: RootContainerViewController) {
+ self.navigationController = navigationController
+ }
+
+ func start(animated: Bool) {
+ let controller = SetupAccountCompletedViewController()
+ controller.delegate = self
+
+ viewController = controller
+
+ navigationController.pushViewController(controller, animated: animated)
+ }
+}
+
+extension SetupAccountCompletedCoordinator: SetupAccountCompletedViewControllerDelegate {
+ func didRequestToSeePrivacy(controller: SetupAccountCompletedViewController) {
+ presentChild(SafariCoordinator(url: ApplicationConfiguration.privacyGuidesURL), animated: true)
+ }
+
+ func didRequestToStartTheApp(controller: SetupAccountCompletedViewController) {
+ didFinish?(self)
+ }
+}
diff --git a/ios/MullvadVPN/Coordinators/App/WelcomeCoordinator.swift b/ios/MullvadVPN/Coordinators/App/WelcomeCoordinator.swift
new file mode 100644
index 0000000000..dec0fc086b
--- /dev/null
+++ b/ios/MullvadVPN/Coordinators/App/WelcomeCoordinator.swift
@@ -0,0 +1,127 @@
+//
+// WelcomeCoordinator.swift
+// MullvadVPN
+//
+// Created by Mojgan on 2023-06-28.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import MullvadREST
+import UIKit
+
+final class WelcomeCoordinator: Coordinator, Presentable {
+ private let navigationController: RootContainerViewController
+ private let storePaymentManager: StorePaymentManager
+ private let tunnelManager: TunnelManager
+ private var viewController: WelcomeViewController?
+
+ var didFinishPayment: ((WelcomeCoordinator) -> Void)?
+
+ var presentedViewController: UIViewController {
+ navigationController
+ }
+
+ init(
+ navigationController: RootContainerViewController,
+ storePaymentManager: StorePaymentManager,
+ tunnelManager: TunnelManager
+ ) {
+ self.navigationController = navigationController
+ self.storePaymentManager = storePaymentManager
+ self.tunnelManager = tunnelManager
+ }
+
+ func start(animated: Bool) {
+ guard case let .loggedIn(storedAccountData, storedDeviceData) = tunnelManager.deviceState else {
+ return
+ }
+ let interactor = WelcomeInteractor(
+ deviceData: storedDeviceData,
+ accountData: storedAccountData
+ )
+
+ let controller = WelcomeViewController(interactor: interactor)
+ controller.delegate = self
+
+ viewController = controller
+
+ navigationController.pushViewController(controller, animated: animated)
+ }
+
+ func popFromNavigationStack(animated: Bool, completion: @escaping () -> Void) {
+ guard let viewController,
+ let index = navigationController.viewControllers.firstIndex(of: viewController)
+ else {
+ completion()
+ return
+ }
+ navigationController.setViewControllers(
+ Array(navigationController.viewControllers[0 ..< index]),
+ animated: animated,
+ completion: completion
+ )
+ }
+}
+
+extension WelcomeCoordinator: WelcomeViewControllerDelegate {
+ func didRequestToShowInfo(controller: WelcomeViewController) {
+ let message = """
+ This is the name assigned to the device. Each device logged in on a \
+ Mullvad account gets a unique name that helps \
+ you identify it when you manage your devices in the app or on the website.
+
+ You can have up to 5 devices logged in on one Mullvad account.
+
+ If you log out, the device and the device name is removed. \
+ When you log back in again, the device will get a new name.
+ """
+ let alertController = CustomAlertViewController(
+ message: NSLocalizedString(
+ "WELCOME_DEVICE_CONCEPET_TEXT_DIALOG",
+ tableName: "Welcome",
+ value: message,
+ comment: ""
+ ),
+ icon: .info
+ )
+
+ alertController.addAction(
+ title: NSLocalizedString(
+ "WELCOME_DEVICE_NAME_DIALOG_OK_ACTION",
+ tableName: "Welcome",
+ value: "Got it!",
+ comment: ""
+ ),
+ style: .default
+ )
+ presentedViewController.present(alertController, animated: true)
+ }
+
+ func didRequestToPurchaseCredit(controller: WelcomeViewController) {
+ // TODO: In-app purchase
+ }
+
+ func didRequestToRedeemVoucher(controller: WelcomeViewController) {
+ let coordinator = AccountRedeemingVoucherCoordinator(
+ navigationController: navigationController,
+ interactor: RedeemVoucherInteractor(tunnelManager: tunnelManager)
+ )
+
+ coordinator.didCancel = { [weak self] coordinator in
+ guard let self else { return }
+ navigationController.popViewController(animated: true)
+ coordinator.removeFromParent()
+ }
+
+ coordinator.didFinish = { [weak self] coordinator in
+ guard let self else { return }
+ coordinator.removeFromParent()
+ didFinishPayment?(self)
+ }
+
+ addChild(coordinator)
+
+ coordinator.start()
+ }
+}
diff --git a/ios/MullvadVPN/SettingsManager/TunnelSettingsV2.swift b/ios/MullvadVPN/SettingsManager/TunnelSettingsV2.swift
index b7b94948b7..c7d3b7dbc7 100644
--- a/ios/MullvadVPN/SettingsManager/TunnelSettingsV2.swift
+++ b/ios/MullvadVPN/SettingsManager/TunnelSettingsV2.swift
@@ -43,6 +43,9 @@ struct StoredAccountData: Codable, Equatable {
/// Account expiry.
var expiry: Date
+ /// be set `true` when account is created and be flipped to `false` when user adds more credit
+ var isNew = false
+
/// Returns `true` if account has expired.
var isExpired: Bool {
expiry <= Date()
diff --git a/ios/MullvadVPN/TunnelManager/RedeemVoucherOperation.swift b/ios/MullvadVPN/TunnelManager/RedeemVoucherOperation.swift
index d87c61b37e..3af92bc6de 100644
--- a/ios/MullvadVPN/TunnelManager/RedeemVoucherOperation.swift
+++ b/ios/MullvadVPN/TunnelManager/RedeemVoucherOperation.swift
@@ -67,6 +67,9 @@ class RedeemVoucherOperation: ResultOperation<REST.SubmitVoucherResponse> {
case .loggedIn(var storedAccountData, let storedDeviceData):
storedAccountData.expiry = voucherResponse.newExpiry
+ // flip the value to `false` when adding credit is successful
+ storedAccountData.isNew = false
+
let newDeviceState = DeviceState.loggedIn(storedAccountData, storedDeviceData)
interactor.setDeviceState(newDeviceState, persist: true)
diff --git a/ios/MullvadVPN/TunnelManager/SetAccountOperation.swift b/ios/MullvadVPN/TunnelManager/SetAccountOperation.swift
index 154734c279..edb0fec6a4 100644
--- a/ios/MullvadVPN/TunnelManager/SetAccountOperation.swift
+++ b/ios/MullvadVPN/TunnelManager/SetAccountOperation.swift
@@ -226,7 +226,8 @@ class SetAccountOperation: ResultOperation<StoredAccountData?> {
return StoredAccountData(
identifier: newAccountData.id,
number: newAccountData.number,
- expiry: newAccountData.expiry
+ expiry: newAccountData.expiry,
+ isNew: true
)
}
diff --git a/ios/MullvadVPN/UI appearance/UIMetrics.swift b/ios/MullvadVPN/UI appearance/UIMetrics.swift
index 3a9a65b76b..5eedb5b20b 100644
--- a/ios/MullvadVPN/UI appearance/UIMetrics.swift
+++ b/ios/MullvadVPN/UI appearance/UIMetrics.swift
@@ -36,7 +36,8 @@ enum UIMetrics {
enum RedeemVoucher {
static let cornerRadius = 8.0
- static let preferredContentSize = CGSize(width: 292, height: 263)
+ static let preferredContentSize = CGSize(width: 300, height: 280)
+ static let contentLayoutMargins = NSDirectionalEdgeInsets(top: 16, leading: 0, bottom: 16, trailing: 0)
}
enum Button {
@@ -110,4 +111,9 @@ extension UIMetrics {
/// Height of brand name. Width is automatically produced based on aspect ratio.
static let headerBarBrandNameHeight: CGFloat = 18
+
+ /// Various paddings used throughout the app to visually separate elements in StackViews
+ static let padding8: CGFloat = 8
+ static let padding16: CGFloat = 16
+ static let padding24: CGFloat = 24
}
diff --git a/ios/MullvadVPN/View controllers/CreationAccount/Completed/SetupAccountCompletedContentView.swift b/ios/MullvadVPN/View controllers/CreationAccount/Completed/SetupAccountCompletedContentView.swift
new file mode 100644
index 0000000000..5b83cfffb4
--- /dev/null
+++ b/ios/MullvadVPN/View controllers/CreationAccount/Completed/SetupAccountCompletedContentView.swift
@@ -0,0 +1,153 @@
+//
+// SetupAccountCompletedContentView.swift
+// MullvadVPN
+//
+// Created by Mojgan on 2023-06-30.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import UIKit
+
+protocol SetupAccountCompletedContentViewDelegate: AnyObject {
+ func didTapPrivacyButton(view: SetupAccountCompletedContentView, button: AppButton)
+ func didTapStartingAppButton(view: SetupAccountCompletedContentView, button: AppButton)
+}
+
+class SetupAccountCompletedContentView: UIView {
+ private enum Action: String {
+ case learnAboutPrivacy, startUsingTheApp
+ }
+
+ private let titleLabel: UILabel = {
+ let label = UILabel()
+ label.font = .preferredFont(forTextStyle: .largeTitle, weight: .bold)
+ label.textColor = .white
+ label.adjustsFontForContentSizeCategory = true
+ label.lineBreakMode = .byWordWrapping
+ label.numberOfLines = .zero
+ label.text = NSLocalizedString(
+ "CREATED_ACCOUNT_CONFIRMATION_PAGE_TITLE",
+ tableName: "CreatedAccountConfirmation",
+ value: "You’re all set!!",
+ comment: ""
+ )
+ return label
+ }()
+
+ private let commentLabel: UILabel = {
+ let label = UILabel()
+ label.font = .preferredFont(forTextStyle: .body)
+ label.textColor = .white
+ label.adjustsFontForContentSizeCategory = true
+ label.lineBreakMode = .byWordWrapping
+ label.numberOfLines = .zero
+ label.text = NSLocalizedString(
+ "CREATED_ACCOUNT_CONFIRMATION_PAGE_BODY",
+ tableName: "CreatedAccountConfirmation",
+ value: """
+ Go ahead and start using the app to begin reclaiming your online privacy.
+
+ To continue your journey as a privacy ninja, \
+ visit our website to pick up other privacy-friendly habits and tools.
+ """,
+ comment: ""
+ )
+ return label
+ }()
+
+ private let privacyButton: AppButton = {
+ let button = AppButton(style: .success)
+ button.accessibilityIdentifier = Action.learnAboutPrivacy.rawValue
+ let localizedString = NSLocalizedString(
+ "LEARN_ABOUT_PRIVACY_BUTTON",
+ tableName: "CreatedAccountConfirmation",
+ value: "Learn about privacy",
+ comment: ""
+ )
+ button.setTitle(localizedString, for: .normal)
+ button.setImage(UIImage(named: "IconExtlink")?.imageFlippedForRightToLeftLayoutDirection(), for: .normal)
+ return button
+ }()
+
+ private let startButton: AppButton = {
+ let button = AppButton(style: .success)
+ button.accessibilityIdentifier = Action.startUsingTheApp.rawValue
+ button.setTitle(NSLocalizedString(
+ "START_USING_THE_APP_BUTTON",
+ tableName: "CreatedAccountConfirmation",
+ value: "Start using the app",
+ comment: ""
+ ), for: .normal)
+ return button
+ }()
+
+ private let textsStackView: UIStackView = {
+ let stackView = UIStackView()
+ stackView.axis = .vertical
+ return stackView
+ }()
+
+ private let buttonsStackView: UIStackView = {
+ let stackView = UIStackView()
+ stackView.axis = .vertical
+ stackView.spacing = UIMetrics.interButtonSpacing
+ return stackView
+ }()
+
+ weak var delegate: SetupAccountCompletedContentViewDelegate?
+
+ override init(frame: CGRect) {
+ super.init(frame: frame)
+
+ backgroundColor = .primaryColor
+ directionalLayoutMargins = UIMetrics.contentLayoutMargins
+ backgroundColor = .secondaryColor
+
+ configureUI()
+ addActions()
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ private func configureUI() {
+ textsStackView.addArrangedSubview(titleLabel)
+ textsStackView.setCustomSpacing(UIMetrics.padding8, after: titleLabel)
+ textsStackView.addArrangedSubview(commentLabel)
+ textsStackView.setCustomSpacing(UIMetrics.padding16, after: commentLabel)
+
+ buttonsStackView.addArrangedSubview(privacyButton)
+ buttonsStackView.addArrangedSubview(startButton)
+
+ addSubview(textsStackView)
+ addSubview(buttonsStackView)
+ addConstraints()
+ }
+
+ private func addConstraints() {
+ addConstrainedSubviews([textsStackView, buttonsStackView]) {
+ textsStackView
+ .pinEdgesToSuperviewMargins(.all().excluding(.bottom))
+
+ buttonsStackView
+ .pinEdgesToSuperviewMargins(.all().excluding(.top))
+ }
+ }
+
+ private func addActions() {
+ [privacyButton, startButton].forEach {
+ $0.addTarget(self, action: #selector(tapped(button:)), for: .touchUpInside)
+ }
+ }
+
+ @objc private func tapped(button: AppButton) {
+ switch button.accessibilityIdentifier {
+ case Action.learnAboutPrivacy.rawValue:
+ delegate?.didTapPrivacyButton(view: self, button: button)
+ case Action.startUsingTheApp.rawValue:
+ delegate?.didTapStartingAppButton(view: self, button: button)
+ default: return
+ }
+ }
+}
diff --git a/ios/MullvadVPN/View controllers/CreationAccount/Completed/SetupAccountCompletedViewController.swift b/ios/MullvadVPN/View controllers/CreationAccount/Completed/SetupAccountCompletedViewController.swift
new file mode 100644
index 0000000000..ad6dcadb54
--- /dev/null
+++ b/ios/MullvadVPN/View controllers/CreationAccount/Completed/SetupAccountCompletedViewController.swift
@@ -0,0 +1,46 @@
+//
+// SetupAccountCompletedViewController.swift
+// MullvadVPN
+//
+// Created by Mojgan on 2023-06-30.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import UIKit
+
+protocol SetupAccountCompletedViewControllerDelegate: AnyObject {
+ func didRequestToSeePrivacy(controller: SetupAccountCompletedViewController)
+ func didRequestToStartTheApp(controller: SetupAccountCompletedViewController)
+}
+
+class SetupAccountCompletedViewController: UIViewController {
+ private lazy var contentView: SetupAccountCompletedContentView = {
+ let view = SetupAccountCompletedContentView()
+ view.delegate = self
+ return view
+ }()
+
+ weak var delegate: SetupAccountCompletedViewControllerDelegate?
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+ configureUI()
+ }
+
+ private func configureUI() {
+ view.addSubview(contentView)
+ view.addConstrainedSubviews([contentView]) {
+ contentView.pinEdgesToSuperview()
+ }
+ }
+}
+
+extension SetupAccountCompletedViewController: SetupAccountCompletedContentViewDelegate {
+ func didTapPrivacyButton(view: SetupAccountCompletedContentView, button: AppButton) {
+ delegate?.didRequestToSeePrivacy(controller: self)
+ }
+
+ func didTapStartingAppButton(view: SetupAccountCompletedContentView, button: AppButton) {
+ delegate?.didRequestToStartTheApp(controller: self)
+ }
+}
diff --git a/ios/MullvadVPN/View controllers/CreationAccount/Welcome/WelcomeContentView.swift b/ios/MullvadVPN/View controllers/CreationAccount/Welcome/WelcomeContentView.swift
new file mode 100644
index 0000000000..73b06e6cf7
--- /dev/null
+++ b/ios/MullvadVPN/View controllers/CreationAccount/Welcome/WelcomeContentView.swift
@@ -0,0 +1,246 @@
+//
+// WelcomeContentView.swift
+// MullvadVPN
+//
+// Created by Mojgan on 2023-06-27.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import UIKit
+protocol WelcomeContentViewDelegate: AnyObject {
+ func didTapPurchaseButton(welcomeContentView: WelcomeContentView, button: AppButton)
+ func didTapRedeemVoucherButton(welcomeContentView: WelcomeContentView, button: AppButton)
+ func didTapInfoButton(welcomeContentView: WelcomeContentView, button: UIButton)
+}
+
+struct WelcomeViewModel {
+ let deviceName: String
+ let accountNumber: String
+}
+
+final class WelcomeContentView: UIView {
+ private enum Action: String {
+ case purchase, redeemVoucher, showInfo
+ }
+
+ private let titleLabel: UILabel = {
+ let label = UILabel()
+ label.font = .preferredFont(forTextStyle: .largeTitle, weight: .bold)
+ label.textColor = .white
+ label.adjustsFontForContentSizeCategory = true
+ label.lineBreakMode = .byWordWrapping
+ label.numberOfLines = .zero
+ label.text = NSLocalizedString(
+ "WELCOME_PAGE_TITLE",
+ tableName: "Welcome",
+ value: "Congrats!",
+ comment: ""
+ )
+ return label
+ }()
+
+ private let subtitleLabel: UILabel = {
+ let label = UILabel()
+ label.font = .preferredFont(forTextStyle: .body)
+ label.textColor = .white
+ label.adjustsFontForContentSizeCategory = true
+ label.lineBreakMode = .byWordWrapping
+ label.numberOfLines = .zero
+ label.text = NSLocalizedString(
+ "WELCOME_PAGE_SUBTITLE",
+ tableName: "Welcome",
+ value: "Here’s your account number. Save it!",
+ comment: ""
+ )
+ return label
+ }()
+
+ private let accountNumberLabel: UILabel = {
+ let label = UILabel()
+ label.adjustsFontForContentSizeCategory = true
+ label.lineBreakMode = .byWordWrapping
+ label.numberOfLines = .zero
+ label.font = .preferredFont(forTextStyle: .title2, weight: .bold)
+ label.textColor = .white
+ return label
+ }()
+
+ private let deviceNameLabel: UILabel = {
+ let label = UILabel()
+ label.adjustsFontForContentSizeCategory = true
+ label.translatesAutoresizingMaskIntoConstraints = false
+ label.font = .preferredFont(forTextStyle: .body)
+ label.textColor = .white
+ label.lineBreakMode = .byWordWrapping
+ label.numberOfLines = .zero
+ label.setContentHuggingPriority(.defaultHigh, for: .horizontal)
+ return label
+ }()
+
+ private let infoButton: UIButton = {
+ let button = IncreasedHitButton(type: .system)
+ button.accessibilityIdentifier = Action.showInfo.rawValue
+ button.tintColor = .white
+ button.translatesAutoresizingMaskIntoConstraints = false
+ button.setImage(UIImage(named: "IconInfo"), for: .normal)
+ button.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
+ return button
+ }()
+
+ private let descriptionLabel: UILabel = {
+ let label = UILabel()
+ label.font = .preferredFont(forTextStyle: .body)
+ label.adjustsFontForContentSizeCategory = true
+ label.textColor = .white
+ label.numberOfLines = .zero
+ label.lineBreakMode = .byWordWrapping
+ if #available(iOS 14.0, *) {
+ label.lineBreakStrategy = []
+ }
+ label.text = NSLocalizedString(
+ "WELCOME_PAGE_DESCRIPTION",
+ tableName: "Welcome",
+ value: """
+ To start using the app, you first need to \
+ add time to your account. Either buy credit \
+ on our website or redeem a voucher.
+ """,
+ comment: ""
+ )
+ return label
+ }()
+
+ private let purchaseButton: InAppPurchaseButton = {
+ let button = InAppPurchaseButton()
+ button.accessibilityIdentifier = Action.purchase.rawValue
+ let localizedString = NSLocalizedString(
+ "BUY_CREDIT_BUTTON",
+ tableName: "Welcome",
+ value: "Buy credit",
+ comment: ""
+ )
+ button.setTitle(localizedString, for: .normal)
+ button.setImage(UIImage(named: "IconExtlink")?.imageFlippedForRightToLeftLayoutDirection(), for: .normal)
+ return button
+ }()
+
+ private let redeemVoucherButton: AppButton = {
+ let button = AppButton(style: .success)
+ button.accessibilityIdentifier = Action.redeemVoucher.rawValue
+ button.setTitle(NSLocalizedString(
+ "REDEEM_VOUCHER_BUTTON_TITLE",
+ tableName: "Account",
+ value: "Redeem voucher",
+ comment: ""
+ ), for: .normal)
+ return button
+ }()
+
+ private let textsStackView: UIStackView = {
+ let stackView = UIStackView()
+ stackView.axis = .vertical
+ return stackView
+ }()
+
+ private let deviceRowStackView: UIStackView = {
+ let stackView = UIStackView()
+ stackView.axis = .horizontal
+ stackView.distribution = .fill
+ return stackView
+ }()
+
+ private let spacerView: UIView = {
+ let view = UIView()
+ view.setContentHuggingPriority(.fittingSizeLevel, for: .horizontal)
+ view.setContentCompressionResistancePriority(.fittingSizeLevel, for: .horizontal)
+ return view
+ }()
+
+ private let buttonsStackView: UIStackView = {
+ let stackView = UIStackView()
+ stackView.axis = .vertical
+ stackView.spacing = UIMetrics.interButtonSpacing
+ return stackView
+ }()
+
+ weak var delegate: WelcomeContentViewDelegate?
+ var viewModel: WelcomeViewModel? {
+ didSet {
+ accountNumberLabel.text = viewModel?.accountNumber
+ deviceNameLabel.text = String(format: NSLocalizedString(
+ "DEVICE_NAME_TEXT",
+ tableName: "Welcome",
+ value: "Device name: %@",
+ comment: ""
+ ), viewModel?.deviceName ?? "")
+ }
+ }
+
+ override init(frame: CGRect) {
+ super.init(frame: frame)
+
+ backgroundColor = .primaryColor
+ directionalLayoutMargins = UIMetrics.contentLayoutMargins
+ backgroundColor = .secondaryColor
+
+ configureUI()
+ addActions()
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ private func configureUI() {
+ textsStackView.addArrangedSubview(titleLabel)
+ textsStackView.setCustomSpacing(UIMetrics.padding8, after: titleLabel)
+ textsStackView.addArrangedSubview(subtitleLabel)
+ textsStackView.setCustomSpacing(UIMetrics.padding16, after: subtitleLabel)
+ textsStackView.addArrangedSubview(accountNumberLabel)
+ textsStackView.setCustomSpacing(UIMetrics.padding16, after: accountNumberLabel)
+
+ deviceRowStackView.addArrangedSubview(deviceNameLabel)
+ deviceRowStackView.setCustomSpacing(UIMetrics.padding8, after: deviceNameLabel)
+ deviceRowStackView.addArrangedSubview(infoButton)
+ deviceRowStackView.addArrangedSubview(spacerView)
+
+ textsStackView.addArrangedSubview(deviceRowStackView)
+ textsStackView.setCustomSpacing(UIMetrics.padding16, after: deviceRowStackView)
+ textsStackView.addArrangedSubview(descriptionLabel)
+
+ buttonsStackView.addArrangedSubview(purchaseButton)
+ buttonsStackView.addArrangedSubview(redeemVoucherButton)
+
+ addSubview(textsStackView)
+ addSubview(buttonsStackView)
+ addConstraints()
+ }
+
+ private func addConstraints() {
+ addConstrainedSubviews([textsStackView, buttonsStackView]) {
+ textsStackView
+ .pinEdgesToSuperviewMargins(.all().excluding(.bottom))
+
+ buttonsStackView
+ .pinEdgesToSuperviewMargins(.all().excluding(.top))
+ }
+ }
+
+ private func addActions() {
+ [redeemVoucherButton, purchaseButton, infoButton].forEach {
+ $0.addTarget(self, action: #selector(tapped(button:)), for: .touchUpInside)
+ }
+ }
+
+ @objc private func tapped(button: AppButton) {
+ switch button.accessibilityIdentifier {
+ case Action.purchase.rawValue:
+ delegate?.didTapPurchaseButton(welcomeContentView: self, button: button)
+ case Action.redeemVoucher.rawValue:
+ delegate?.didTapRedeemVoucherButton(welcomeContentView: self, button: button)
+ case Action.showInfo.rawValue:
+ delegate?.didTapInfoButton(welcomeContentView: self, button: button)
+ default: return
+ }
+ }
+}
diff --git a/ios/MullvadVPN/View controllers/CreationAccount/Welcome/WelcomeInteractor.swift b/ios/MullvadVPN/View controllers/CreationAccount/Welcome/WelcomeInteractor.swift
new file mode 100644
index 0000000000..ade337f469
--- /dev/null
+++ b/ios/MullvadVPN/View controllers/CreationAccount/Welcome/WelcomeInteractor.swift
@@ -0,0 +1,25 @@
+//
+// WelcomeInteractor.swift
+// MullvadVPN
+//
+// Created by Mojgan on 2023-06-29.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+class WelcomeInteractor {
+ private let deviceData: StoredDeviceData
+ private let accountData: StoredAccountData
+
+ var viewModel: WelcomeViewModel {
+ WelcomeViewModel(
+ deviceName: deviceData.capitalizedName,
+ accountNumber: accountData.number.formattedAccountNumber
+ )
+ }
+
+ init(deviceData: StoredDeviceData, accountData: StoredAccountData) {
+ self.deviceData = deviceData
+ self.accountData = accountData
+ }
+}
diff --git a/ios/MullvadVPN/View controllers/CreationAccount/Welcome/WelcomeViewController.swift b/ios/MullvadVPN/View controllers/CreationAccount/Welcome/WelcomeViewController.swift
new file mode 100644
index 0000000000..2acdc9d96e
--- /dev/null
+++ b/ios/MullvadVPN/View controllers/CreationAccount/Welcome/WelcomeViewController.swift
@@ -0,0 +1,82 @@
+//
+// WelcomeViewController.swift
+// MullvadVPN
+//
+// Created by Mojgan on 2023-06-28.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import UIKit
+protocol WelcomeViewControllerDelegate: AnyObject {
+ func didRequestToPurchaseCredit(controller: WelcomeViewController)
+ func didRequestToRedeemVoucher(controller: WelcomeViewController)
+ func didRequestToShowInfo(controller: WelcomeViewController)
+}
+
+class WelcomeViewController: UIViewController, RootContainment {
+ private lazy var contentView: WelcomeContentView = {
+ let view = WelcomeContentView()
+ view.delegate = self
+ return view
+ }()
+
+ private let interactor: WelcomeInteractor
+
+ weak var delegate: WelcomeViewControllerDelegate?
+
+ var preferredHeaderBarPresentation: HeaderBarPresentation {
+ HeaderBarPresentation(style: .default, showsDivider: true)
+ }
+
+ var prefersHeaderBarHidden: Bool {
+ false
+ }
+
+ var prefersNotificationBarHidden: Bool {
+ true
+ }
+
+ var prefersDeviceInfoBarHidden: Bool {
+ true
+ }
+
+ override var preferredStatusBarStyle: UIStatusBarStyle {
+ .lightContent
+ }
+
+ init(interactor: WelcomeInteractor) {
+ self.interactor = interactor
+ super.init(nibName: nil, bundle: nil)
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+ configureUI()
+ contentView.viewModel = interactor.viewModel
+ }
+
+ private func configureUI() {
+ view.addSubview(contentView)
+ view.addConstrainedSubviews([contentView]) {
+ contentView.pinEdgesToSuperview()
+ }
+ }
+}
+
+extension WelcomeViewController: WelcomeContentViewDelegate {
+ func didTapInfoButton(welcomeContentView: WelcomeContentView, button: UIButton) {
+ delegate?.didRequestToShowInfo(controller: self)
+ }
+
+ func didTapPurchaseButton(welcomeContentView: WelcomeContentView, button: AppButton) {
+ delegate?.didRequestToPurchaseCredit(controller: self)
+ }
+
+ func didTapRedeemVoucherButton(welcomeContentView: WelcomeContentView, button: AppButton) {
+ delegate?.didRequestToRedeemVoucher(controller: self)
+ }
+}
diff --git a/ios/MullvadVPN/View controllers/Login/LoginViewController.swift b/ios/MullvadVPN/View controllers/Login/LoginViewController.swift
index 1e30788921..e726107960 100644
--- a/ios/MullvadVPN/View controllers/Login/LoginViewController.swift
+++ b/ios/MullvadVPN/View controllers/Login/LoginViewController.swift
@@ -87,6 +87,14 @@ class LoginViewController: UIViewController, RootContainment {
contentView.accountInputGroup.satisfiesMinimumTokenLengthRequirement
}
+ var prefersNotificationBarHidden: Bool {
+ true
+ }
+
+ var prefersDeviceInfoBarHidden: Bool {
+ true
+ }
+
private let interactor: LoginInteractor
var didFinishLogin: ((LoginAction, Error?) -> EndLoginAction)?
diff --git a/ios/MullvadVPN/View controllers/RedeemVoucher/RedeemVoucherContentView.swift b/ios/MullvadVPN/View controllers/RedeemVoucher/RedeemVoucherContentView.swift
index 865a28b9ac..bd19d30eb8 100644
--- a/ios/MullvadVPN/View controllers/RedeemVoucher/RedeemVoucherContentView.swift
+++ b/ios/MullvadVPN/View controllers/RedeemVoucher/RedeemVoucherContentView.swift
@@ -105,7 +105,8 @@ final class RedeemVoucherContentView: UIView {
])
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .vertical
- stackView.spacing = UIMetrics.interButtonSpacing
+ stackView.setCustomSpacing(UIMetrics.padding16, after: titleLabel)
+ stackView.setCustomSpacing(UIMetrics.padding8, after: textField)
return stackView
}()
@@ -214,7 +215,8 @@ final class RedeemVoucherContentView: UIView {
private func addConstraints() {
addConstrainedSubviews([voucherCodeStackView, actionsStackView]) {
- voucherCodeStackView.pinEdgesToSuperviewMargins(.all().excluding(.bottom))
+ voucherCodeStackView
+ .pinEdgesToSuperviewMargins(.all(UIMetrics.RedeemVoucher.contentLayoutMargins).excluding(.bottom))
actionsStackView.pinEdgesToSuperviewMargins(.all().excluding(.top))
actionsStackView.topAnchor.constraint(
diff --git a/ios/MullvadVPN/View controllers/RedeemVoucher/RedeemVoucherSucceededViewController.swift b/ios/MullvadVPN/View controllers/RedeemVoucher/RedeemVoucherSucceededViewController.swift
index 6fa52cff0d..e8ee6865e9 100644
--- a/ios/MullvadVPN/View controllers/RedeemVoucher/RedeemVoucherSucceededViewController.swift
+++ b/ios/MullvadVPN/View controllers/RedeemVoucher/RedeemVoucherSucceededViewController.swift
@@ -12,6 +12,8 @@ protocol RedeemVoucherSucceededViewControllerDelegate: AnyObject {
func redeemVoucherSucceededViewControllerDidFinish(
_ controller: RedeemVoucherSucceededViewController
)
+
+ func titleForAction(in controller: RedeemVoucherSucceededViewController) -> String
}
class RedeemVoucherSucceededViewController: UIViewController {
@@ -47,12 +49,6 @@ class RedeemVoucherSucceededViewController: UIViewController {
private let dismissButton: AppButton = {
let button = AppButton(style: .default)
- button.setTitle(NSLocalizedString(
- "REDEEM_VOUCHER_DISMISS_BUTTON",
- tableName: "RedeemVoucher",
- value: "Got it!",
- comment: ""
- ), for: .normal)
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
@@ -61,7 +57,11 @@ class RedeemVoucherSucceededViewController: UIViewController {
.lightContent
}
- weak var delegate: RedeemVoucherSucceededViewControllerDelegate?
+ weak var delegate: RedeemVoucherSucceededViewControllerDelegate? {
+ didSet {
+ dismissButton.setTitle(delegate?.titleForAction(in: self), for: .normal)
+ }
+ }
init(timeAddedComponents: DateComponents) {
super.init(nibName: nil, bundle: nil)
diff --git a/ios/Shared/ApplicationConfiguration.swift b/ios/Shared/ApplicationConfiguration.swift
index f841a9c49b..d7190d9071 100644
--- a/ios/Shared/ApplicationConfiguration.swift
+++ b/ios/Shared/ApplicationConfiguration.swift
@@ -28,6 +28,9 @@ enum ApplicationConfiguration {
/// Privacy policy URL.
static let privacyPolicyURL = URL(string: "https://mullvad.net/help/privacy-policy/")!
+ /// Make a start regarding policy URL.
+ static let privacyGuidesURL = URL(string: "https://mullvad.net/help/first-steps-towards-online-privacy/")!
+
/// FAQ & Guides URL.
static let faqAndGuidesURL = URL(string: "https://mullvad.net/help/tag/mullvad-app/")!