diff options
| author | Emīls <emils@mullvad.net> | 2023-07-12 11:11:53 +0200 |
|---|---|---|
| committer | Emīls <emils@mullvad.net> | 2023-07-12 11:11:53 +0200 |
| commit | 7ffe7307ca2a969193d0eec4853248d5cdaa4fa7 (patch) | |
| tree | 99d6faa07660617cb6a593104e6f53fafc1ddb02 | |
| parent | bbc9840a8169923f87fb7305e20ca11165f24d59 (diff) | |
| parent | 3cd00d09e8db96dcaa9a33185326782fafb77f10 (diff) | |
| download | mullvadvpn-7ffe7307ca2a969193d0eec4853248d5cdaa4fa7.tar.xz mullvadvpn-7ffe7307ca2a969193d0eec4853248d5cdaa4fa7.zip | |
Merge branch 'redeeming-vouchers-on-creation-account-screen-ios-29'
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/")! |
