diff options
Diffstat (limited to 'ios/MullvadVPN')
17 files changed, 340 insertions, 236 deletions
diff --git a/ios/MullvadVPN/Containers/Root/HeaderBarView.swift b/ios/MullvadVPN/Containers/Root/HeaderBarView.swift index efa5e392f9..dcc9cbac92 100644 --- a/ios/MullvadVPN/Containers/Root/HeaderBarView.swift +++ b/ios/MullvadVPN/Containers/Root/HeaderBarView.swift @@ -51,29 +51,43 @@ class HeaderBarView: UIView { return label }() - let settingsButton = makeSettingsButton() - - class func makeSettingsButton() -> HeaderBarButton { - let settingsImage = UIImage(named: "IconSettings")? - .withTintColor(UIColor.HeaderBar.buttonColor, renderingMode: .alwaysOriginal) - let disabledSettingsImage = UIImage(named: "IconSettings")? - .withTintColor( - UIColor.HeaderBar.disabledButtonColor, - renderingMode: .alwaysOriginal - ) + let accountButton: HeaderBarButton = { + let button = makeHeaderBarButton(with: UIImage(named: "IconAccount")) + button.accessibilityIdentifier = "AccountButton" + button.accessibilityLabel = NSLocalizedString( + "HEADER_BAR_ACCOUNT_BUTTON_ACCESSIBILITY_LABEL", + tableName: "HeaderBar", + value: "Account", + comment: "" + ) + return button + }() - let settingsButton = HeaderBarButton(type: .system) - settingsButton.setImage(settingsImage, for: .normal) - settingsButton.setImage(disabledSettingsImage, for: .disabled) - settingsButton.translatesAutoresizingMaskIntoConstraints = false - settingsButton.accessibilityIdentifier = "SettingsButton" - settingsButton.accessibilityLabel = NSLocalizedString( + let settingsButton: HeaderBarButton = { + let button = makeHeaderBarButton(with: UIImage(named: "IconSettings")) + button.accessibilityIdentifier = "SettingsButton" + button.accessibilityLabel = NSLocalizedString( "HEADER_BAR_SETTINGS_BUTTON_ACCESSIBILITY_LABEL", tableName: "HeaderBar", value: "Settings", comment: "" ) - return settingsButton + return button + }() + + class func makeHeaderBarButton(with image: UIImage?) -> HeaderBarButton { + let buttonImage = image?.withTintColor(UIColor.HeaderBar.buttonColor, renderingMode: .alwaysOriginal) + let disabledButtonImage = image?.withTintColor( + UIColor.HeaderBar.disabledButtonColor, + renderingMode: .alwaysOriginal + ) + + let barButton = HeaderBarButton(type: .system) + barButton.setImage(buttonImage, for: .normal) + barButton.setImage(disabledButtonImage, for: .disabled) + barButton.translatesAutoresizingMaskIntoConstraints = false + + return barButton } private let borderLayer: CALayer = { @@ -107,49 +121,52 @@ class HeaderBarView: UIView { let imageSize = brandNameImage?.size ?? .zero let brandNameAspectRatio = imageSize.width / max(imageSize.height, 1) - let constraints = [ - logoImageView.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor), - logoImageView.centerYAnchor.constraint(equalTo: brandNameImageView.centerYAnchor), - logoImageView.widthAnchor.constraint(equalToConstant: 44), + [deviceName, timeLeft].forEach { deviceInfoHolder.addArrangedSubview($0) } + + addConstrainedSubviews([logoImageView, brandNameImageView, accountButton, settingsButton, deviceInfoHolder]) { + logoImageView.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor) + logoImageView.centerYAnchor.constraint(equalTo: brandNameImageView.centerYAnchor) + logoImageView.widthAnchor.constraint(equalToConstant: 44) logoImageView.heightAnchor.constraint( equalTo: logoImageView.widthAnchor, multiplier: 1 - ), + ) brandNameImageView.leadingAnchor.constraint( equalTo: logoImageView.trailingAnchor, constant: 9 - ), + ) brandNameImageView.topAnchor.constraint( equalTo: layoutMarginsGuide.topAnchor, constant: 22 - ), + ) brandNameImageView.widthAnchor.constraint( equalTo: brandNameImageView.heightAnchor, multiplier: brandNameAspectRatio - ), - brandNameImageView.heightAnchor.constraint(equalToConstant: 18), + ) + brandNameImageView.heightAnchor.constraint(equalToConstant: 18) layoutMarginsGuide.bottomAnchor.constraint( equalTo: deviceInfoHolder.bottomAnchor, constant: 8 - ), + ) - settingsButton.leadingAnchor.constraint( + accountButton.leadingAnchor.constraint( greaterThanOrEqualTo: brandNameImageView.trailingAnchor, constant: 8 - ), - settingsButton.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor), - settingsButton.centerYAnchor.constraint(equalTo: brandNameImageView.centerYAnchor), - - deviceInfoHolder.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor), - deviceInfoHolder.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor), - deviceInfoHolder.topAnchor.constraint(equalTo: logoImageView.bottomAnchor, constant: 7), - ] + ) + accountButton.centerYAnchor.constraint(equalTo: brandNameImageView.centerYAnchor) - [logoImageView, brandNameImageView, settingsButton, deviceInfoHolder].forEach { addSubview($0) } - [deviceName, timeLeft].forEach { deviceInfoHolder.addArrangedSubview($0) } + settingsButton.leadingAnchor.constraint( + equalTo: accountButton.trailingAnchor, + constant: 20 + ).withPriority(.defaultHigh) + settingsButton.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor) + settingsButton.centerYAnchor.constraint(equalTo: accountButton.centerYAnchor) - NSLayoutConstraint.activate(constraints) + deviceInfoHolder.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor) + deviceInfoHolder.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor) + deviceInfoHolder.topAnchor.constraint(equalTo: logoImageView.bottomAnchor, constant: 7) + } } required init?(coder: NSCoder) { @@ -164,33 +181,35 @@ class HeaderBarView: UIView { } extension HeaderBarView { - func update(deviceState: DeviceState) { - switch deviceState { - case let .loggedIn(storedAccountData, storedDeviceData): + func update(configuration: RootConfigration) { + if let name = configuration.deviceName { let formattedDeviceName = NSLocalizedString( "DEVICE_NAME_HEADER_VIEW", tableName: "Account", - value: "Device name : %@", + value: "Device name: %@", comment: "" ) + deviceName.text = .init(format: formattedDeviceName, name) + } + + if let expiry = configuration.expiry { let formattedTimeLeft = NSLocalizedString( "TIME_LEFT_HEADER_VIEW", tableName: "Account", - value: "Time left : %@", + value: "Time left: %@", comment: "" ) - deviceName.text = .init(format: formattedDeviceName, storedDeviceData.name) timeLeft.text = .init( format: formattedTimeLeft, CustomDateComponentsFormatting.localizedString( from: Date(), - to: storedAccountData.expiry, + to: expiry, unitsStyle: .full ) ?? "" ) - deviceInfoHolder.arrangedSubviews.forEach { $0.isHidden = false } - case .loggedOut, .revoked: - deviceInfoHolder.arrangedSubviews.forEach { $0.isHidden = true } } + + deviceInfoHolder.arrangedSubviews.forEach { $0.isHidden = configuration.deviceName == nil } + accountButton.isHidden = !configuration.showsAccountButton } } diff --git a/ios/MullvadVPN/Containers/Root/RootConfiguration.swift b/ios/MullvadVPN/Containers/Root/RootConfiguration.swift new file mode 100644 index 0000000000..2229bdf4a0 --- /dev/null +++ b/ios/MullvadVPN/Containers/Root/RootConfiguration.swift @@ -0,0 +1,15 @@ +// +// RootConfiguration.swift +// MullvadVPN +// +// Created by Jon Petersson on 2023-04-19. +// Copyright © 2023 Mullvad VPN AB. All rights reserved. +// + +import Foundation + +struct RootConfigration { + var deviceName: String? + var expiry: Date? + var showsAccountButton: Bool +} diff --git a/ios/MullvadVPN/Containers/Root/RootContainerViewController.swift b/ios/MullvadVPN/Containers/Root/RootContainerViewController.swift index 3b96728d12..679964d46c 100644 --- a/ios/MullvadVPN/Containers/Root/RootContainerViewController.swift +++ b/ios/MullvadVPN/Containers/Root/RootContainerViewController.swift @@ -44,6 +44,11 @@ protocol RootContainment { } protocol RootContainerViewControllerDelegate: AnyObject { + func rootContainerViewControllerShouldShowAccount( + _ controller: RootContainerViewController, + animated: Bool + ) + func rootContainerViewControllerShouldShowSettings( _ controller: RootContainerViewController, navigateTo route: SettingsNavigationRoute?, @@ -62,8 +67,10 @@ class RootContainerViewController: UIViewController { typealias CompletionHandler = () -> Void private let headerBarView = HeaderBarView(frame: CGRect(x: 0, y: 0, width: 100, height: 100)) - private let transitionContainer = UIView(frame: UIScreen.main.bounds) + let transitionContainer = UIView(frame: UIScreen.main.bounds) + private var presentationContainerAccountButton: UIButton? private var presentationContainerSettingsButton: UIButton? + private var configuration = RootConfigration(showsAccountButton: false) private(set) var headerBarPresentation = HeaderBarPresentation.default private(set) var headerBarHidden = false @@ -256,6 +263,14 @@ class RootContainerViewController: UIViewController { } /// Request to display settings controller + func showAccount(animated: Bool) { + delegate?.rootContainerViewControllerShouldShowAccount( + self, + animated: animated + ) + } + + /// Request to display settings controller func showSettings(navigateTo route: SettingsNavigationRoute? = nil, animated: Bool) { delegate?.rootContainerViewControllerShouldShowSettings( self, @@ -264,47 +279,37 @@ class RootContainerViewController: UIViewController { ) } - /// Enable or disable the settings bar button displayed in the header bar - func setEnableSettingsButton(_ isEnabled: Bool) { - headerBarView.settingsButton.isEnabled = isEnabled - presentationContainerSettingsButton?.isEnabled = isEnabled - } - - /// Add settings bar button into the presentation container to make settings accessible even - /// when the root container is covered with modal. - func addSettingsButtonToPresentationContainer(_ presentationContainer: UIView) { - let settingsButton: UIButton + /// Add account and settings bar buttons into the presentation container to make them accessible even + /// when the root container is covered with a modal. + func addTrailingButtonsToPresentationContainer(_ presentationContainer: UIView) { + let accountButton = getPresentationContainerAccountButton() + let settingsButton = getPresentationContainerSettingsButton() - if let transitionViewSettingsButton = presentationContainerSettingsButton { - transitionViewSettingsButton.removeFromSuperview() - settingsButton = transitionViewSettingsButton - } else { - settingsButton = HeaderBarView.makeSettingsButton() - settingsButton.isEnabled = headerBarView.settingsButton.isEnabled - settingsButton.addTarget( - self, - action: #selector(handleSettingsButtonTap), - for: .touchUpInside - ) - - presentationContainerSettingsButton = settingsButton - } + presentationContainerAccountButton = accountButton + presentationContainerSettingsButton = settingsButton - // Hide the settings button inside the header bar to avoid color blending issues + // Hide the account button inside the header bar to avoid color blending issues + headerBarView.accountButton.alpha = 0 headerBarView.settingsButton.alpha = 0 - presentationContainer.addSubview(settingsButton) + presentationContainer.addConstrainedSubviews([accountButton, settingsButton]) { + accountButton.centerXAnchor + .constraint(equalTo: headerBarView.accountButton.centerXAnchor) + accountButton.centerYAnchor + .constraint(equalTo: headerBarView.accountButton.centerYAnchor) - NSLayoutConstraint.activate([ settingsButton.centerXAnchor - .constraint(equalTo: headerBarView.settingsButton.centerXAnchor), + .constraint(equalTo: headerBarView.settingsButton.centerXAnchor) settingsButton.centerYAnchor - .constraint(equalTo: headerBarView.settingsButton.centerYAnchor), - ]) + .constraint(equalTo: headerBarView.settingsButton.centerYAnchor) + } } - func removeSettingsButtonFromPresentationContainer() { + func removeTrailingButtonsFromPresentationContainer() { + presentationContainerAccountButton?.removeFromSuperview() presentationContainerSettingsButton?.removeFromSuperview() + + headerBarView.accountButton.alpha = 1 headerBarView.settingsButton.alpha = 1 } @@ -353,6 +358,12 @@ class RootContainerViewController: UIViewController { // Prevent automatic layout margins adjustment as we manually control them. headerBarView.insetsLayoutMarginsFromSafeArea = false + headerBarView.accountButton.addTarget( + self, + action: #selector(handleAccountButtonTap), + for: .touchUpInside + ) + headerBarView.settingsButton.addTarget( self, action: #selector(handleSettingsButtonTap), @@ -364,6 +375,50 @@ class RootContainerViewController: UIViewController { NSLayoutConstraint.activate(constraints) } + private func getPresentationContainerAccountButton() -> UIButton { + let button: UIButton + + if let transitionViewButton = presentationContainerAccountButton { + transitionViewButton.removeFromSuperview() + button = transitionViewButton + } else { + button = HeaderBarView.makeHeaderBarButton(with: UIImage(named: "IconAccount")) + button.addTarget( + self, + action: #selector(handleAccountButtonTap), + for: .touchUpInside + ) + } + + button.isEnabled = headerBarView.accountButton.isEnabled + button.isHidden = !configuration.showsAccountButton + + return button + } + + private func getPresentationContainerSettingsButton() -> UIButton { + let button: UIButton + + if let transitionViewButton = presentationContainerSettingsButton { + transitionViewButton.removeFromSuperview() + button = transitionViewButton + } else { + button = HeaderBarView.makeHeaderBarButton(with: UIImage(named: "IconSettings")) + button.isEnabled = headerBarView.settingsButton.isEnabled + button.addTarget( + self, + action: #selector(handleSettingsButtonTap), + for: .touchUpInside + ) + } + + return button + } + + @objc private func handleAccountButtonTap() { + showAccount(animated: true) + } + @objc private func handleSettingsButtonTap() { showSettings(animated: true) } @@ -676,7 +731,10 @@ extension UIViewController { } extension RootContainerViewController { - func update(deviceState: DeviceState) { - headerBarView.update(deviceState: deviceState) + func update(configuration: RootConfigration) { + self.configuration = configuration + + presentationContainerAccountButton?.isHidden = !configuration.showsAccountButton + headerBarView.update(configuration: configuration) } } diff --git a/ios/MullvadVPN/Coordinators/App/AccountCoordinator.swift b/ios/MullvadVPN/Coordinators/App/AccountCoordinator.swift new file mode 100644 index 0000000000..13ca7bcb5d --- /dev/null +++ b/ios/MullvadVPN/Coordinators/App/AccountCoordinator.swift @@ -0,0 +1,58 @@ +// +// AccountCoordinator.swift +// MullvadVPN +// +// Created by Jon Petersson on 2023-04-14. +// Copyright © 2023 Mullvad VPN AB. All rights reserved. +// + +import UIKit + +enum AccountDismissReason: Equatable { + case none + case userLoggedOut +} + +final class AccountCoordinator: Coordinator, Presentable, Presenting { + private let interactor: AccountInteractor + private var accountController: AccountViewController? + + let navigationController: UINavigationController + var presentedViewController: UIViewController { + return navigationController + } + + var presentationContext: UIViewController { + return navigationController + } + + var didFinish: ((AccountCoordinator, AccountDismissReason) -> Void)? + + init( + navigationController: UINavigationController, + interactor: AccountInteractor + ) { + self.navigationController = navigationController + self.interactor = interactor + } + + func start(animated: Bool) { + navigationController.navigationBar.prefersLargeTitles = true + + let accountController = AccountViewController(interactor: interactor) + accountController.delegate = self + + navigationController.pushViewController(accountController, animated: animated) + self.accountController = accountController + } +} + +extension AccountCoordinator: AccountViewControllerDelegate { + func accountViewControllerDidFinish(_ controller: AccountViewController) { + didFinish?(self, .none) + } + + func accountViewControllerDidLogout(_ controller: AccountViewController) { + didFinish?(self, .userLoggedOut) + } +} diff --git a/ios/MullvadVPN/Coordinators/App/ApplicationCoordinator.swift b/ios/MullvadVPN/Coordinators/App/ApplicationCoordinator.swift index 710b0e2cdc..fa6e879240 100644 --- a/ios/MullvadVPN/Coordinators/App/ApplicationCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/App/ApplicationCoordinator.swift @@ -123,6 +123,9 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo completion: @escaping (Coordinator) -> Void ) { switch route { + case .account: + presentAccount(animated: animated, completion: completion) + case let .settings(subRoute): presentSettings(route: subRoute, animated: animated, completion: completion) @@ -162,7 +165,7 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo endHorizontalFlow(animated: context.isAnimated, completion: completion) context.dismissedRoutes.forEach { $0.coordinator.removeFromParent() } - case .selectLocation, .settings: + case .selectLocation, .account, .settings: let coordinator = dismissedRoute.coordinator as! Presentable coordinator.dismiss(animated: context.isAnimated, completion: completion) @@ -292,9 +295,9 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo } } - private func didDismissSettings(_ reason: SettingsDismissReason) { + private func didDismissAccount(_ reason: AccountDismissReason) { if isPad { - router.dismissAll(.settings, animated: true) + router.dismiss(.account, animated: true) if reason == .userLoggedOut { router.dismissAll(.primary, animated: true) @@ -306,7 +309,7 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo continueFlow(animated: false) } - router.dismissAll(.settings, animated: true) + router.dismiss(.account, animated: true) } } @@ -541,6 +544,35 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo return selectLocationCoordinator } + private func presentAccount(animated: Bool, completion: @escaping (Coordinator) -> Void) { + let accountInteractor = AccountInteractor( + storePaymentManager: storePaymentManager, + tunnelManager: tunnelManager + ) + + let coordinator = AccountCoordinator( + navigationController: CustomNavigationController(), + interactor: accountInteractor + ) + + coordinator.didFinish = { [weak self] coordinator, reason in + self?.didDismissAccount(reason) + } + + coordinator.start(animated: animated) + + presentChild( + coordinator, + animated: animated, + configuration: ModalPresentationConfiguration( + preferredContentSize: preferredFormSheetContentSize, + modalPresentationStyle: .formSheet + ) + ) { + completion(coordinator) + } + } + private func presentSettings( route: SettingsNavigationRoute?, animated: Bool, @@ -558,8 +590,8 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo interactorFactory: interactorFactory ) - coordinator.didFinish = { [weak self] coordinator, reason in - self?.didDismissSettings(reason) + coordinator.didFinish = { [weak self] coordinator in + self?.router.dismissAll(.settings, animated: true) } coordinator.willNavigate = { [weak self] coordinator, from, to in @@ -608,7 +640,6 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo if !accountData.isExpired { router.dismiss(.outOfTime, animated: true) } - case .revoked: cancelOutOfTimeTimer() router.present(.revoked, animated: true) @@ -619,8 +650,14 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo } private func updateView(deviceState: DeviceState) { - primaryNavigationContainer.update(deviceState: deviceState) - secondaryNavigationContainer.update(deviceState: deviceState) + let configuration = RootConfigration( + deviceName: deviceState.deviceData?.name, + expiry: deviceState.accountData?.expiry, + showsAccountButton: deviceState.isLoggedIn + ) + + primaryNavigationContainer.update(configuration: configuration) + secondaryNavigationContainer.update(configuration: configuration) } // MARK: - Out of time @@ -658,8 +695,8 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo // MARK: - Deep link - func showAccountSettings() { - router.present(.settings(.account)) + func showAccount() { + router.present(.account) } // MARK: - UISplitViewControllerDelegate @@ -697,6 +734,13 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo // MARK: - RootContainerViewControllerDelegate + func rootContainerViewControllerShouldShowAccount( + _ controller: RootContainerViewController, + animated: Bool + ) { + router.present(.account, animated: animated) + } + func rootContainerViewControllerShouldShowSettings( _ controller: RootContainerViewController, navigateTo route: SettingsNavigationRoute?, diff --git a/ios/MullvadVPN/Coordinators/App/ApplicationRouter.swift b/ios/MullvadVPN/Coordinators/App/ApplicationRouter.swift index a6277dba58..663de96b07 100644 --- a/ios/MullvadVPN/Coordinators/App/ApplicationRouter.swift +++ b/ios/MullvadVPN/Coordinators/App/ApplicationRouter.swift @@ -26,6 +26,11 @@ enum AppRouteGroup: Comparable, Equatable, Hashable { case selectLocation /** + Account group. + */ + case account + + /** Settings group. */ case settings @@ -39,7 +44,7 @@ enum AppRouteGroup: Comparable, Equatable, Hashable { case .primary: return UIDevice.current.userInterfaceIdiom == .pad - case .selectLocation, .settings: + case .selectLocation, .account, .settings: return true } } @@ -48,7 +53,7 @@ enum AppRouteGroup: Comparable, Equatable, Hashable { switch self { case .primary: return 0 - case .settings, .selectLocation: + case .settings, .account, .selectLocation: return 1 } } @@ -63,6 +68,11 @@ enum AppRouteGroup: Comparable, Equatable, Hashable { */ enum AppRoute: Equatable, Hashable { /** + Account route. + */ + case account + + /** Settings route. Contains sub-route to display. */ case settings(SettingsNavigationRoute?) @@ -82,7 +92,7 @@ enum AppRoute: Equatable, Hashable { */ var isExclusive: Bool { switch self { - case .selectLocation, .settings: + case .selectLocation, .account, .settings: return true default: return false @@ -109,6 +119,8 @@ enum AppRoute: Equatable, Hashable { return .primary case .selectLocation: return .selectLocation + case .account: + return .account case .settings: return .settings } diff --git a/ios/MullvadVPN/Coordinators/App/SettingsCoordinator.swift b/ios/MullvadVPN/Coordinators/App/SettingsCoordinator.swift index 89fffaf2c7..e6b7f232a4 100644 --- a/ios/MullvadVPN/Coordinators/App/SettingsCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/App/SettingsCoordinator.swift @@ -12,19 +12,13 @@ import UIKit enum SettingsNavigationRoute: Equatable { case root - case account case preferences case problemReport case faq } -enum SettingsDismissReason: Equatable { - case none - case userLoggedOut -} - final class SettingsCoordinator: Coordinator, Presentable, Presenting, SettingsViewControllerDelegate, - AccountViewControllerDelegate, UINavigationControllerDelegate + UINavigationControllerDelegate { private let logger = Logger(label: "SettingsNavigationCoordinator") @@ -48,7 +42,7 @@ final class SettingsCoordinator: Coordinator, Presentable, Presenting, SettingsV _ to: SettingsNavigationRoute ) -> Void)? - var didFinish: ((SettingsCoordinator, SettingsDismissReason) -> Void)? + var didFinish: ((SettingsCoordinator) -> Void)? init( navigationController: UINavigationController, @@ -137,7 +131,7 @@ final class SettingsCoordinator: Coordinator, Presentable, Presenting, SettingsV // MARK: - SettingsViewControllerDelegate func settingsViewControllerDidFinish(_ controller: SettingsViewController) { - didFinish?(self, .none) + didFinish?(self) } func settingsViewController( @@ -147,12 +141,6 @@ final class SettingsCoordinator: Coordinator, Presentable, Presenting, SettingsV navigate(to: route, animated: true) } - // MARK: - AccountViewControllerDelegate - - func accountViewControllerDidLogout(_ controller: AccountViewController) { - didFinish?(self, .userLoggedOut) - } - // MARK: - Route mapping private func makeViewController(for route: SettingsNavigationRoute) -> UIViewController? { @@ -164,13 +152,6 @@ final class SettingsCoordinator: Coordinator, Presentable, Presenting, SettingsV controller.delegate = self return controller - case .account: - let controller = AccountViewController( - interactor: interactorFactory.makeAccountInteractor() - ) - controller.delegate = self - return controller - case .preferences: return PreferencesViewController( interactor: interactorFactory.makePreferencesInteractor() @@ -190,8 +171,6 @@ final class SettingsCoordinator: Coordinator, Presentable, Presenting, SettingsV switch viewController { case is SettingsViewController: return .root - case is AccountViewController: - return .account case is PreferencesViewController: return .preferences case is ProblemReportViewController: diff --git a/ios/MullvadVPN/Presentation controllers/SecondaryContextPresentationController.swift b/ios/MullvadVPN/Presentation controllers/SecondaryContextPresentationController.swift index 6a0397efa2..6324bf3e31 100644 --- a/ios/MullvadVPN/Presentation controllers/SecondaryContextPresentationController.swift +++ b/ios/MullvadVPN/Presentation controllers/SecondaryContextPresentationController.swift @@ -21,7 +21,7 @@ class SecondaryContextPresentationController: FormsheetPresentationController { if let containerView = containerView, let rootContainer = presentingViewController as? RootContainerViewController { - rootContainer.addSettingsButtonToPresentationContainer(containerView) + rootContainer.addTrailingButtonsToPresentationContainer(containerView) } } @@ -29,7 +29,7 @@ class SecondaryContextPresentationController: FormsheetPresentationController { super.dismissalTransitionDidEnd(completed) if let rootContainer = presentingViewController as? RootContainerViewController, completed { - rootContainer.removeSettingsButtonFromPresentationContainer() + rootContainer.removeTrailingButtonsFromPresentationContainer() } } diff --git a/ios/MullvadVPN/SceneDelegate.swift b/ios/MullvadVPN/SceneDelegate.swift index 689d904be8..5a6651e594 100644 --- a/ios/MullvadVPN/SceneDelegate.swift +++ b/ios/MullvadVPN/SceneDelegate.swift @@ -37,7 +37,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate, SettingsMigrationUIHand // MARK: - Deep link func showUserAccount() { - appCoordinator?.showAccountSettings() + appCoordinator?.showAccount() } // MARK: - Private diff --git a/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconAccount.imageset/Contents.json b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconAccount.imageset/Contents.json new file mode 100644 index 0000000000..573b102f42 --- /dev/null +++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconAccount.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "IconAccount.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconAccount.imageset/IconAccount.pdf b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconAccount.imageset/IconAccount.pdf Binary files differnew file mode 100644 index 0000000000..1d57a67c83 --- /dev/null +++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconAccount.imageset/IconAccount.pdf diff --git a/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconSettings.imageset/IconSettings.pdf b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconSettings.imageset/IconSettings.pdf Binary files differindex 3f2ef14157..1efefb905b 100644 --- a/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconSettings.imageset/IconSettings.pdf +++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconSettings.imageset/IconSettings.pdf diff --git a/ios/MullvadVPN/View controllers/Account/AccountViewController.swift b/ios/MullvadVPN/View controllers/Account/AccountViewController.swift index 34f16b3233..a7c4beb867 100644 --- a/ios/MullvadVPN/View controllers/Account/AccountViewController.swift +++ b/ios/MullvadVPN/View controllers/Account/AccountViewController.swift @@ -14,6 +14,7 @@ import StoreKit import UIKit protocol AccountViewControllerDelegate: AnyObject { + func accountViewControllerDidFinish(_ controller: AccountViewController) func accountViewControllerDidLogout(_ controller: AccountViewController) } @@ -79,6 +80,12 @@ class AccountViewController: UIViewController { comment: "" ) + navigationItem.rightBarButtonItem = UIBarButtonItem( + barButtonSystemItem: .done, + target: self, + action: #selector(handleDismiss) + ) + contentView.accountTokenRowView.copyAccountNumber = { [weak self] in self?.copyAccountToken() } @@ -115,6 +122,10 @@ class AccountViewController: UIViewController { // MARK: - Private + @objc private func handleDismiss() { + delegate?.accountViewControllerDidFinish(self) + } + private func requestStoreProducts() { let productKind = StoreSubscription.thirtyDays diff --git a/ios/MullvadVPN/View controllers/Settings/SettingsAccountCell.swift b/ios/MullvadVPN/View controllers/Settings/SettingsAccountCell.swift deleted file mode 100644 index f0621f31dc..0000000000 --- a/ios/MullvadVPN/View controllers/Settings/SettingsAccountCell.swift +++ /dev/null @@ -1,54 +0,0 @@ -// -// SettingsAccountCell.swift -// MullvadVPN -// -// Created by pronebird on 22/05/2019. -// Copyright © 2019 Mullvad VPN AB. All rights reserved. -// - -import UIKit - -class SettingsAccountCell: SettingsCell { - var accountExpiryDate: Date? { - didSet { - didUpdateAccountExpiry() - } - } - - private func didUpdateAccountExpiry() { - guard let accountExpiryDate = accountExpiryDate else { - detailTitleLabel.text = "" - detailTitleLabel.textColor = UIColor.Cell.detailTextColor - return - } - - guard accountExpiryDate > Date() else { - detailTitleLabel.text = NSLocalizedString( - "ACCOUNT_CELL_OUT_OF_TIME_LABEL", - tableName: "Settings", - value: "OUT OF TIME", - comment: "" - ) - detailTitleLabel.textColor = .dangerColor - return - } - - let formattedTime = CustomDateComponentsFormatting.localizedString( - from: Date(), - to: accountExpiryDate, - unitsStyle: .full - ) - - detailTitleLabel.text = formattedTime.map { remainingTimeString in - let localizedString = NSLocalizedString( - "ACCOUNT_CELL_TIME_LEFT_LABEL_FORMAT", - tableName: "Settings", - value: "%@ left", - comment: "" - ) - - return String(format: localizedString, remainingTimeString).uppercased() - } ?? "" - detailTitleLabel.textColor = UIColor.Cell.detailTextColor - } -} diff --git a/ios/MullvadVPN/View controllers/Settings/SettingsCellFactory.swift b/ios/MullvadVPN/View controllers/Settings/SettingsCellFactory.swift index 4aef9175bc..3bb4440fa1 100644 --- a/ios/MullvadVPN/View controllers/Settings/SettingsCellFactory.swift +++ b/ios/MullvadVPN/View controllers/Settings/SettingsCellFactory.swift @@ -27,19 +27,6 @@ struct SettingsCellFactory: CellFactoryProtocol { func configureCell(_ cell: UITableViewCell, item: SettingsDataSource.Item, indexPath: IndexPath) { switch item { - case .account: - guard let cell = cell as? SettingsAccountCell else { return } - - cell.titleLabel.text = NSLocalizedString( - "ACCOUNT_CELL_LABEL", - tableName: "Settings", - value: "Account", - comment: "" - ) - cell.accountExpiryDate = interactor.deviceState.accountData?.expiry - cell.accessibilityIdentifier = "AccountCell" - cell.disclosureType = .chevron - case .preferences: guard let cell = cell as? SettingsCell else { return } diff --git a/ios/MullvadVPN/View controllers/Settings/SettingsDataSource.swift b/ios/MullvadVPN/View controllers/Settings/SettingsDataSource.swift index cc460e596b..954361c463 100644 --- a/ios/MullvadVPN/View controllers/Settings/SettingsDataSource.swift +++ b/ios/MullvadVPN/View controllers/Settings/SettingsDataSource.swift @@ -13,16 +13,10 @@ final class SettingsDataSource: UITableViewDiffableDataSource< SettingsDataSource.Item >, UITableViewDelegate { enum CellReuseIdentifiers: String, CaseIterable { - case accountCell case basicCell var reusableViewClass: AnyClass { - switch self { - case .accountCell: - return SettingsAccountCell.self - case .basicCell: - return SettingsCell.self - } + return SettingsCell.self } } @@ -30,10 +24,7 @@ final class SettingsDataSource: UITableViewDiffableDataSource< case spacer var reusableViewClass: AnyClass { - switch self { - case .spacer: - return EmptyTableViewHeaderFooterView.self - } + return EmptyTableViewHeaderFooterView.self } } @@ -44,19 +35,13 @@ final class SettingsDataSource: UITableViewDiffableDataSource< } enum Item: String { - case account case preferences case version case problemReport case faq var reuseIdentifier: CellReuseIdentifiers { - switch self { - case .account: - return .accountCell - default: - return .basicCell - } + return .basicCell } } @@ -83,7 +68,7 @@ final class SettingsDataSource: UITableViewDiffableDataSource< updateDataSnapshot() interactor.didUpdateDeviceState = { [weak self] deviceState in - self?.didUpdateDeviceState(deviceState) + self?.updateDataSnapshot() } storedAccountData = interactor.deviceState.accountData } @@ -145,7 +130,7 @@ final class SettingsDataSource: UITableViewDiffableDataSource< if interactor.deviceState.isLoggedIn { snapshot.appendSections([.main]) - snapshot.appendItems([.account, .preferences], toSection: .main) + snapshot.appendItems([.preferences], toSection: .main) } snapshot.appendSections([.version, .problemReport]) @@ -154,27 +139,4 @@ final class SettingsDataSource: UITableViewDiffableDataSource< apply(snapshot) } - - private func didUpdateDeviceState(_ deviceState: DeviceState) { - let newAccountData = deviceState.accountData - let oldAccountData = storedAccountData - - storedAccountData = newAccountData - - // Refresh individual row if expiry changed. - if let newAccountData = newAccountData, let oldAccountData = oldAccountData, - oldAccountData.number == newAccountData.number, - oldAccountData.expiry != newAccountData.expiry - { - if let indexPath = indexPath(for: .account), - let cell = tableView?.cellForRow(at: indexPath) - { - settingsCellFactory.configureCell(cell, item: .account, indexPath: indexPath) - } - - return - } - - updateDataSnapshot() - } } diff --git a/ios/MullvadVPN/View controllers/Settings/SettingsViewController.swift b/ios/MullvadVPN/View controllers/Settings/SettingsViewController.swift index 158c91b7b0..d9fd4771c1 100644 --- a/ios/MullvadVPN/View controllers/Settings/SettingsViewController.swift +++ b/ios/MullvadVPN/View controllers/Settings/SettingsViewController.swift @@ -80,8 +80,6 @@ class SettingsViewController: UITableViewController, SettingsDataSourceDelegate extension SettingsDataSource.Item { var navigationRoute: SettingsNavigationRoute? { switch self { - case .account: - return .account case .preferences: return .preferences case .version: |
