From fe27d80236374922f050d3b3b71e616aa3195924 Mon Sep 17 00:00:00 2001 From: Mojgan Date: Tue, 29 Aug 2023 15:22:24 +0200 Subject: [PATCH] fix some linting violations --- ios/MullvadVPN.xcodeproj/project.pbxproj | 12 + ios/MullvadVPN/Classes/AppPreferences.swift | 2 +- .../Coordinators/AccountCoordinator.swift | 6 +- .../Coordinators/ApplicationCoordinator.swift | 30 +- .../InAppPurchaseCoordinator.swift | 2 +- .../NSRegularExpression+IPAddress.swift | 4 +- ...countExpiryInAppNotificationProvider.swift | 2 +- ...ountExpirySystemNotificationProvider.swift | 2 +- .../FormsheetPresentationController.swift | 6 +- .../Account/AccountContentView.swift | 430 +----------------- .../Account/AccountDeviceRow.swift | 81 ++++ .../Account/AccountExpiryRow.swift | 101 ++++ .../Account/AccountInteractor.swift | 4 +- .../Account/AccountNumberRow.swift | 246 ++++++++++ .../Account/AccountViewController.swift | 84 ++-- .../AccountDeletionContentView.swift | 6 +- .../Login/AccountInputGroupView.swift | 147 +++--- .../RedeemVoucherContentView.swift | 2 +- .../SelectLocation/LocationCellFactory.swift | 2 +- ios/Operations/AsyncOperationQueue.swift | 2 +- ios/Shared/ApplicationConfiguration.swift | 1 + ios/Shared/ApplicationTarget.swift | 1 + 22 files changed, 582 insertions(+), 591 deletions(-) create mode 100644 ios/MullvadVPN/View controllers/Account/AccountDeviceRow.swift create mode 100644 ios/MullvadVPN/View controllers/Account/AccountExpiryRow.swift create mode 100644 ios/MullvadVPN/View controllers/Account/AccountNumberRow.swift diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index c6733a622cc7..7011d1dc795b 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -491,6 +491,9 @@ F0C2AEFD2A0BB5CC00986207 /* NotificationProviderIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0C2AEFC2A0BB5CC00986207 /* NotificationProviderIdentifier.swift */; }; F0C6FA812A66E23300F521F0 /* DeleteAccountOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0C6FA802A66E23300F521F0 /* DeleteAccountOperation.swift */; }; F0C6FA852A6A733700F521F0 /* InAppPurchaseInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0C6FA842A6A733700F521F0 /* InAppPurchaseInteractor.swift */; }; + F0DA87472A9CB9A2006044F1 /* AccountExpiryRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0DA87462A9CB9A2006044F1 /* AccountExpiryRow.swift */; }; + F0DA87492A9CBA9F006044F1 /* AccountDeviceRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0DA87482A9CBA9F006044F1 /* AccountDeviceRow.swift */; }; + F0DA874B2A9CBACB006044F1 /* AccountNumberRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0DA874A2A9CBACB006044F1 /* AccountNumberRow.swift */; }; F0E3618B2A4ADD2F00AEEF2B /* WelcomeContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0E3618A2A4ADD2F00AEEF2B /* WelcomeContentView.swift */; }; F0E8CC032A4C753B007ED3B4 /* WelcomeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0E8CC022A4C753B007ED3B4 /* WelcomeViewController.swift */; }; F0E8CC0A2A4EE127007ED3B4 /* SetupAccountCompletedContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0E8CC092A4EE127007ED3B4 /* SetupAccountCompletedContentView.swift */; }; @@ -1380,6 +1383,9 @@ F0C6FA802A66E23300F521F0 /* DeleteAccountOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteAccountOperation.swift; sourceTree = ""; }; F0C6FA822A6A729500F521F0 /* InAppPurchaseCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InAppPurchaseCoordinator.swift; sourceTree = ""; }; F0C6FA842A6A733700F521F0 /* InAppPurchaseInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InAppPurchaseInteractor.swift; sourceTree = ""; }; + F0DA87462A9CB9A2006044F1 /* AccountExpiryRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountExpiryRow.swift; sourceTree = ""; }; + F0DA87482A9CBA9F006044F1 /* AccountDeviceRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountDeviceRow.swift; sourceTree = ""; }; + F0DA874A2A9CBACB006044F1 /* AccountNumberRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountNumberRow.swift; sourceTree = ""; }; F0E3618A2A4ADD2F00AEEF2B /* WelcomeContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeContentView.swift; sourceTree = ""; }; F0E8CC022A4C753B007ED3B4 /* WelcomeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeViewController.swift; sourceTree = ""; }; F0E8CC092A4EE127007ED3B4 /* SetupAccountCompletedContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupAccountCompletedContentView.swift; sourceTree = ""; }; @@ -1911,6 +1917,9 @@ 7A1A26422A2612AE00B978AA /* PaymentAlertPresenter.swift */, 5867771329097BCD006F721F /* PaymentState.swift */, 5867771529097C5B006F721F /* ProductState.swift */, + F0DA87462A9CB9A2006044F1 /* AccountExpiryRow.swift */, + F0DA87482A9CBA9F006044F1 /* AccountDeviceRow.swift */, + F0DA874A2A9CBACB006044F1 /* AccountNumberRow.swift */, ); path = Account; sourceTree = ""; @@ -3780,6 +3789,7 @@ 7A9CCCB92A96302800DD6A34 /* SelectLocationCoordinator.swift in Sources */, 58FB865A26EA214400F188BC /* RelayCacheTrackerObserver.swift in Sources */, 58ACF64D26567A5000ACE4B7 /* CustomSwitch.swift in Sources */, + F0DA874B2A9CBACB006044F1 /* AccountNumberRow.swift in Sources */, 58F2E14C276A61C000A79513 /* RotateKeyOperation.swift in Sources */, 5871FB96254ADE4E0051A0A4 /* ConsolidatedApplicationLog.swift in Sources */, F0E8E4C52A60499100ED26A3 /* AccountDeletionViewController.swift in Sources */, @@ -3818,6 +3828,7 @@ 587CBFE322807F530028DED3 /* UIColor+Helpers.swift in Sources */, 7A9CCCBE2A96302800DD6A34 /* AccountDeletionCoordinator.swift in Sources */, 588527B4276B4F2F00BAA373 /* SetAccountOperation.swift in Sources */, + F0DA87472A9CB9A2006044F1 /* AccountExpiryRow.swift in Sources */, 585CA70F25F8C44600B47C62 /* UIMetrics.swift in Sources */, E1187ABD289BBB850024E748 /* OutOfTimeContentView.swift in Sources */, 58CC40EF24A601900019D96E /* ObserverList.swift in Sources */, @@ -3879,6 +3890,7 @@ 58B93A1326C3F13600A55733 /* TunnelState.swift in Sources */, 58B26E262943522400D5980C /* NotificationProvider.swift in Sources */, 58CE5E64224146200008646E /* AppDelegate.swift in Sources */, + F0DA87492A9CBA9F006044F1 /* AccountDeviceRow.swift in Sources */, 5878A27329091D6D0096FC88 /* TunnelBlockObserver.swift in Sources */, 58E0A98827C8F46300FE6BDD /* Tunnel.swift in Sources */, 58ACF64F26567A7100ACE4B7 /* CustomSwitchContainer.swift in Sources */, diff --git a/ios/MullvadVPN/Classes/AppPreferences.swift b/ios/MullvadVPN/Classes/AppPreferences.swift index 4a790ba8fc91..95e49ad2a023 100644 --- a/ios/MullvadVPN/Classes/AppPreferences.swift +++ b/ios/MullvadVPN/Classes/AppPreferences.swift @@ -9,7 +9,7 @@ import Foundation protocol AppPreferencesDataSource { - var isShownOnboarding: Bool { set get } + var isShownOnboarding: Bool { get set } var isAgreedToTermsOfService: Bool { get set } var lastSeenChangeLogVersion: String { get set } } diff --git a/ios/MullvadVPN/Coordinators/AccountCoordinator.swift b/ios/MullvadVPN/Coordinators/AccountCoordinator.swift index a7d6f261e765..516ade62a10c 100644 --- a/ios/MullvadVPN/Coordinators/AccountCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/AccountCoordinator.swift @@ -157,11 +157,13 @@ final class AccountCoordinator: Coordinator, Presentable, Presenting { "DEVICE_INFO_DIALOG_MESSAGE_PART_1", tableName: "Account", value: """ - 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. + 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. + 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. """, comment: "" ) diff --git a/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift b/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift index 1ccbb32632fc..2c4a0cacab15 100644 --- a/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift @@ -238,8 +238,9 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo making payment. It will dismiss itself once done. */ if dismissedRoute.route == .outOfTime { - let coordinator = dismissedRoute.coordinator as! OutOfTimeCoordinator - + guard let coordinator = dismissedRoute.coordinator as? OutOfTimeCoordinator else { + return false + } return !coordinator.isMakingPayment } @@ -254,8 +255,7 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo ) { switch context.route { case let .settings(subRoute): - let coordinator = context.presentedRoute.coordinator as! SettingsCoordinator - + guard let coordinator = context.presentedRoute.coordinator as? SettingsCoordinator else { return } if let subRoute { coordinator.navigate( to: subRoute, @@ -501,7 +501,7 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo private func presentTOS(animated: Bool, completion: @escaping (Coordinator) -> Void) { let coordinator = TermsOfServiceCoordinator(navigationController: horizontalFlowController) - coordinator.didFinish = { [weak self] coordinator in + coordinator.didFinish = { [weak self] _ in self?.appPreferences.isAgreedToTermsOfService = true self?.continueFlow(animated: true) } @@ -517,7 +517,7 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo private func presentChangeLog(animated: Bool, completion: @escaping (Coordinator) -> Void) { let coordinator = ChangeLogCoordinator(interactor: ChangeLogInteractor()) - coordinator.didFinish = { [weak self] coordinator in + coordinator.didFinish = { [weak self] _ in self?.appPreferences.markChangeLogSeen() self?.router.dismiss(.changelog) } @@ -552,7 +552,7 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo tunnelManager: tunnelManager ) - coordinator.didFinish = { [weak self] coordinator in + coordinator.didFinish = { [weak self] _ in self?.logoutRevokedDevice() } @@ -571,7 +571,7 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo tunnelManager: tunnelManager ) - coordinator.didFinishPayment = { [weak self] coordinator in + coordinator.didFinishPayment = { [weak self] _ in guard let self else { return } if shouldDismissOutOfTime() { @@ -596,7 +596,7 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo tunnelManager: tunnelManager ) - coordinator.didFinish = { [weak self] coordinator in + coordinator.didFinish = { [weak self] _ in guard let self else { return } appPreferences.isShownOnboarding = true router.dismiss(.welcome, animated: false) @@ -634,7 +634,7 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo devicesProxy: devicesProxy ) - coordinator.didFinish = { [weak self] coordinator in + coordinator.didFinish = { [weak self] _ in self?.continueFlow(animated: true) } coordinator.didCreateAccount = { [weak self] in @@ -670,7 +670,7 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo relayCacheTracker: relayCacheTracker ) - selectLocationCoordinator.didFinish = { [weak self] coordinator, relay in + selectLocationCoordinator.didFinish = { [weak self] _, _ in if isModalPresentation { self?.router.dismiss(.selectLocation, animated: true) } @@ -690,7 +690,7 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo interactor: accountInteractor ) - coordinator.didFinish = { [weak self] coordinator, reason in + coordinator.didFinish = { [weak self] _, reason in self?.didDismissAccount(reason) } @@ -732,11 +732,11 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo interactorFactory: interactorFactory ) - coordinator.didFinish = { [weak self] coordinator in + coordinator.didFinish = { [weak self] _ in self?.router.dismissAll(.settings, animated: true) } - coordinator.willNavigate = { [weak self] coordinator, from, to in + coordinator.willNavigate = { [weak self] _, _, to in if to == .root { self?.onShowSettings?() } @@ -758,7 +758,7 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo private func addTunnelObserver() { let tunnelObserver = - TunnelBlockObserver(didUpdateDeviceState: { [weak self] manager, deviceState, previousDeviceState in + TunnelBlockObserver(didUpdateDeviceState: { [weak self] _, deviceState, previousDeviceState in self?.deviceStateDidChange(deviceState, previousDeviceState: previousDeviceState) }) diff --git a/ios/MullvadVPN/Coordinators/InAppPurchaseCoordinator.swift b/ios/MullvadVPN/Coordinators/InAppPurchaseCoordinator.swift index dc97e680dc2a..f4cea6a0138e 100644 --- a/ios/MullvadVPN/Coordinators/InAppPurchaseCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/InAppPurchaseCoordinator.swift @@ -29,7 +29,7 @@ class InAppPurchaseCoordinator: Coordinator, Presentable { func start(accountNumber: String, product: SKProduct) { interactor.purchase(accountNumber: accountNumber, product: product) - interactor.didFinishPayment = { [weak self] interactor, paymentEvent in + interactor.didFinishPayment = { [weak self] _, paymentEvent in guard let self else { return } switch paymentEvent { case let .finished(value): diff --git a/ios/MullvadVPN/Extensions/NSRegularExpression+IPAddress.swift b/ios/MullvadVPN/Extensions/NSRegularExpression+IPAddress.swift index 30e698f9c4c7..7cb5ae90da59 100644 --- a/ios/MullvadVPN/Extensions/NSRegularExpression+IPAddress.swift +++ b/ios/MullvadVPN/Extensions/NSRegularExpression+IPAddress.swift @@ -18,7 +18,7 @@ extension NSRegularExpression { (25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\. (25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\b """# - + // swiftlint:disable:next force_try return try! NSRegularExpression(pattern: pattern, options: [.allowCommentsAndWhitespace]) } @@ -46,7 +46,7 @@ extension NSRegularExpression { (25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]) # 2001:db8:3:4::192.0.2.33 64:ff9b::192.0.2.33 (IPv4-Embedded IPv6 Address) ) """# - + // swiftlint:disable:next force_try return try! NSRegularExpression(pattern: pattern, options: [.allowCommentsAndWhitespace]) } } diff --git a/ios/MullvadVPN/Notifications/Notification Providers/AccountExpiryInAppNotificationProvider.swift b/ios/MullvadVPN/Notifications/Notification Providers/AccountExpiryInAppNotificationProvider.swift index 0d0550401a49..da60a1396dfa 100644 --- a/ios/MullvadVPN/Notifications/Notification Providers/AccountExpiryInAppNotificationProvider.swift +++ b/ios/MullvadVPN/Notifications/Notification Providers/AccountExpiryInAppNotificationProvider.swift @@ -21,7 +21,7 @@ final class AccountExpiryInAppNotificationProvider: NotificationProvider, InAppN didLoadConfiguration: { [weak self] tunnelManager in self?.invalidate(deviceState: tunnelManager.deviceState) }, - didUpdateDeviceState: { [weak self] tunnelManager, deviceState, previousDeviceState in + didUpdateDeviceState: { [weak self] _, deviceState, _ in self?.invalidate(deviceState: deviceState) } ) diff --git a/ios/MullvadVPN/Notifications/Notification Providers/AccountExpirySystemNotificationProvider.swift b/ios/MullvadVPN/Notifications/Notification Providers/AccountExpirySystemNotificationProvider.swift index 520983371a20..b8ad7054f05d 100644 --- a/ios/MullvadVPN/Notifications/Notification Providers/AccountExpirySystemNotificationProvider.swift +++ b/ios/MullvadVPN/Notifications/Notification Providers/AccountExpirySystemNotificationProvider.swift @@ -20,7 +20,7 @@ final class AccountExpirySystemNotificationProvider: NotificationProvider, Syste didLoadConfiguration: { [weak self] tunnelManager in self?.invalidate(deviceState: tunnelManager.deviceState) }, - didUpdateDeviceState: { [weak self] tunnelManager, deviceState, previousDeviceState in + didUpdateDeviceState: { [weak self] _, deviceState, _ in self?.invalidate(deviceState: deviceState) } ) diff --git a/ios/MullvadVPN/Presentation controllers/FormsheetPresentationController.swift b/ios/MullvadVPN/Presentation controllers/FormsheetPresentationController.swift index 016153f9067b..5bee1b2156b3 100644 --- a/ios/MullvadVPN/Presentation controllers/FormsheetPresentationController.swift +++ b/ios/MullvadVPN/Presentation controllers/FormsheetPresentationController.swift @@ -115,7 +115,7 @@ class FormSheetPresentationController: UIPresentationController { } if let transitionCoordinator = presentingViewController.transitionCoordinator { - transitionCoordinator.animate { context in + transitionCoordinator.animate { _ in revealDimmingView() } } else { @@ -137,7 +137,7 @@ class FormSheetPresentationController: UIPresentationController { } if let transitionCoordinator = presentingViewController.transitionCoordinator { - transitionCoordinator.animate { context in + transitionCoordinator.animate { _ in fadeDimmingView() } } else { @@ -167,7 +167,7 @@ class FormSheetPresentationController: UIPresentationController { NotificationCenter.default.post( name: Self.willChangeFullScreenPresentation, object: presentedViewController, - userInfo: [Self.isFullScreenUserInfoKey: NSNumber(booleanLiteral: currentIsInFullScreen)] + userInfo: [Self.isFullScreenUserInfoKey: NSNumber(value: currentIsInFullScreen)] ) } diff --git a/ios/MullvadVPN/View controllers/Account/AccountContentView.swift b/ios/MullvadVPN/View controllers/Account/AccountContentView.swift index 623d2e52f234..707784e21e28 100644 --- a/ios/MullvadVPN/View controllers/Account/AccountContentView.swift +++ b/ios/MullvadVPN/View controllers/Account/AccountContentView.swift @@ -119,440 +119,16 @@ class AccountContentView: UIView { directionalLayoutMargins = UIMetrics.contentLayoutMargins - addSubview(contentStackView) - addSubview(buttonStackView) - - NSLayoutConstraint.activate([ - contentStackView.topAnchor.constraint(equalTo: layoutMarginsGuide.topAnchor), - contentStackView.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor), - contentStackView.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor), - + addConstrainedSubviews([contentStackView, buttonStackView]) { + contentStackView.pinEdgesToSuperviewMargins(.all().excluding(.bottom)) buttonStackView.topAnchor.constraint( greaterThanOrEqualTo: contentStackView.bottomAnchor, constant: UIMetrics.sectionSpacing - ), - buttonStackView.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor), - buttonStackView.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor), - buttonStackView.bottomAnchor.constraint(equalTo: layoutMarginsGuide.bottomAnchor), - ]) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } -} - -class AccountDeviceRow: UIView { - var deviceName: String? { - didSet { - deviceLabel.text = deviceName?.capitalized ?? "" - accessibilityValue = deviceName - } - } - - var infoButtonAction: (() -> Void)? - - private let titleLabel: UILabel = { - let label = UILabel() - label.text = NSLocalizedString( - "DEVICE_NAME", - tableName: "Account", - value: "Device name", - comment: "" - ) - label.font = UIFont.systemFont(ofSize: 14) - label.textColor = UIColor(white: 1.0, alpha: 0.6) - return label - }() - - private let deviceLabel: UILabel = { - let label = UILabel() - label.font = UIFont.systemFont(ofSize: 17) - label.textColor = .white - return label - }() - - private let infoButton: UIButton = { - let button = IncreasedHitButton(type: .system) - button.accessibilityIdentifier = "InfoButton" - button.tintColor = .white - button.setImage(UIImage(named: "IconInfo"), for: .normal) - return button - }() - - override init(frame: CGRect) { - super.init(frame: frame) - - let contentContainerView = UIStackView(arrangedSubviews: [titleLabel, deviceLabel]) - contentContainerView.axis = .vertical - contentContainerView.alignment = .leading - contentContainerView.spacing = 8 - - addConstrainedSubviews([contentContainerView, infoButton]) { - contentContainerView.pinEdgesToSuperview() - infoButton.leadingAnchor.constraint(equalToSystemSpacingAfter: deviceLabel.trailingAnchor, multiplier: 1) - infoButton.centerYAnchor.constraint(equalTo: deviceLabel.centerYAnchor) - } - - isAccessibilityElement = true - accessibilityLabel = titleLabel.text - - infoButton.addTarget( - self, - action: #selector(didTapInfoButton), - for: .touchUpInside - ) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - @objc private func didTapInfoButton() { - infoButtonAction?() - } -} - -class AccountNumberRow: UIView { - var accountNumber: String? { - didSet { - updateView() - } - } - - var isObscured = true { - didSet { - updateView() - } - } - - var copyAccountNumber: (() -> Void)? - - private let titleLabel: UILabel = { - let textLabel = UILabel() - textLabel.translatesAutoresizingMaskIntoConstraints = false - textLabel.text = NSLocalizedString( - "ACCOUNT_TOKEN_LABEL", - tableName: "Account", - value: "Account number", - comment: "" - ) - textLabel.font = UIFont.systemFont(ofSize: 14) - textLabel.textColor = UIColor(white: 1.0, alpha: 0.6) - return textLabel - }() - - private let accountNumberLabel: UILabel = { - let textLabel = UILabel() - textLabel.translatesAutoresizingMaskIntoConstraints = false - textLabel.font = UIFont.monospacedSystemFont(ofSize: 17, weight: .regular) - textLabel.textColor = .white - return textLabel - }() - - private let showHideButton: UIButton = { - let button = UIButton(type: .system) - button.translatesAutoresizingMaskIntoConstraints = false - button.tintColor = .white - button.setContentHuggingPriority(.defaultHigh, for: .horizontal) - return button - }() - - private let copyButton: UIButton = { - let button = UIButton(type: .system) - button.translatesAutoresizingMaskIntoConstraints = false - button.tintColor = .white - button.setContentHuggingPriority(.defaultHigh, for: .horizontal) - return button - }() - - private var revertCopyImageWorkItem: DispatchWorkItem? - - override init(frame: CGRect) { - super.init(frame: frame) - - addSubview(titleLabel) - addSubview(accountNumberLabel) - addSubview(showHideButton) - addSubview(copyButton) - - NSLayoutConstraint.activate([ - titleLabel.topAnchor.constraint(equalTo: topAnchor), - titleLabel.leadingAnchor.constraint(equalTo: leadingAnchor), - titleLabel.trailingAnchor.constraint(greaterThanOrEqualTo: trailingAnchor), - - accountNumberLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 8), - accountNumberLabel.leadingAnchor.constraint(equalTo: leadingAnchor), - accountNumberLabel.trailingAnchor.constraint(equalTo: showHideButton.leadingAnchor), - accountNumberLabel.bottomAnchor.constraint(equalTo: bottomAnchor), - - showHideButton.heightAnchor.constraint(equalTo: accountNumberLabel.heightAnchor), - showHideButton.centerYAnchor.constraint(equalTo: accountNumberLabel.centerYAnchor), - showHideButton.leadingAnchor.constraint(equalTo: accountNumberLabel.trailingAnchor), - - copyButton.heightAnchor.constraint(equalTo: accountNumberLabel.heightAnchor), - copyButton.centerYAnchor.constraint(equalTo: accountNumberLabel.centerYAnchor), - copyButton.leadingAnchor.constraint( - equalTo: showHideButton.trailingAnchor, - constant: 24 - ), - copyButton.trailingAnchor.constraint(equalTo: trailingAnchor), - ]) - - showHideButton.addTarget( - self, - action: #selector(didTapShowHideAccount), - for: .touchUpInside - ) - - copyButton.addTarget( - self, - action: #selector(didTapCopyAccountNumber), - for: .touchUpInside - ) - - isAccessibilityElement = true - accessibilityLabel = titleLabel.text - - showCheckmark(false) - updateView() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - // MARK: - Private - - private func updateView() { - accountNumberLabel.text = displayAccountNumber ?? "" - showHideButton.setImage(showHideImage, for: .normal) - - accessibilityAttributedValue = _accessibilityAttributedValue - accessibilityCustomActions = _accessibilityCustomActions - } - - private var displayAccountNumber: String? { - guard let accountNumber else { - return nil - } - - let formattedString = accountNumber.formattedAccountNumber - - if isObscured { - return String(formattedString.map { ch in - ch == " " ? ch : "•" - }) - } else { - return formattedString - } - } - - private var showHideImage: UIImage? { - if isObscured { - return UIImage(named: "IconUnobscure") - } else { - return UIImage(named: "IconObscure") - } - } - - private var _accessibilityAttributedValue: NSAttributedString? { - guard let accountNumber else { - return nil - } - - if isObscured { - return NSAttributedString( - string: NSLocalizedString( - "ACCOUNT_ACCESSIBILITY_OBSCURED", - tableName: "Account", - value: "Obscured", - comment: "" - ) - ) - } else { - return NSAttributedString( - string: accountNumber, - attributes: [.accessibilitySpeechSpellOut: true] - ) - } - } - - private var _accessibilityCustomActions: [UIAccessibilityCustomAction]? { - guard accountNumber != nil else { return nil } - - return [ - UIAccessibilityCustomAction( - name: showHideAccessibilityActionName, - target: self, - selector: #selector(didTapShowHideAccount) - ), - UIAccessibilityCustomAction( - name: NSLocalizedString( - "ACCOUNT_ACCESSIBILITY_COPY_TO_PASTEBOARD", - tableName: "Account", - value: "Copy to pasteboard", - comment: "" - ), - target: self, - selector: #selector(didTapCopyAccountNumber) - ), - ] - } - - private var showHideAccessibilityActionName: String { - if isObscured { - return NSLocalizedString( - "ACCOUNT_ACCESSIBILITY_SHOW_ACCOUNT_NUMBER", - tableName: "Account", - value: "Show account number", - comment: "" - ) - } else { - return NSLocalizedString( - "ACCOUNT_ACCESSIBILITY_HIDE_ACCOUNT_NUMBER", - tableName: "Account", - value: "Hide account number", - comment: "" ) + buttonStackView.pinEdgesToSuperviewMargins(.all().excluding(.top)) } } - private func showCheckmark(_ showCheckmark: Bool) { - if showCheckmark { - let tickIcon = UIImage(named: "IconTick") - - copyButton.setImage(tickIcon, for: .normal) - copyButton.tintColor = .successColor - } else { - let copyIcon = UIImage(named: "IconCopy") - - copyButton.setImage(copyIcon, for: .normal) - copyButton.tintColor = .white - } - } - - // MARK: - Actions - - @objc private func didTapShowHideAccount() { - isObscured.toggle() - updateView() - - UIAccessibility.post(notification: .layoutChanged, argument: nil) - } - - @objc private func didTapCopyAccountNumber() { - let delayedWorkItem = DispatchWorkItem { [weak self] in - self?.showCheckmark(false) - } - - revertCopyImageWorkItem?.cancel() - revertCopyImageWorkItem = delayedWorkItem - - showCheckmark(true) - copyAccountNumber?() - - DispatchQueue.main.asyncAfter( - deadline: .now() + .seconds(2), - execute: delayedWorkItem - ) - } -} - -class AccountExpiryRow: UIView { - var value: Date? { - didSet { - let expiry = value - - if let expiry, expiry <= Date() { - let localizedString = NSLocalizedString( - "ACCOUNT_OUT_OF_TIME_LABEL", - tableName: "Account", - value: "OUT OF TIME", - comment: "" - ) - - valueLabel.text = localizedString - accessibilityValue = localizedString - - valueLabel.textColor = .dangerColor - } else { - let formattedDate = expiry.map { date in - DateFormatter.localizedString( - from: date, - dateStyle: .medium, - timeStyle: .short - ) - } - - valueLabel.text = formattedDate ?? "" - accessibilityValue = formattedDate - - valueLabel.textColor = .white - } - } - } - - private let textLabel: UILabel = { - let textLabel = UILabel() - textLabel.translatesAutoresizingMaskIntoConstraints = false - textLabel.text = NSLocalizedString( - "ACCOUNT_EXPIRY_LABEL", - tableName: "Account", - value: "Paid until", - comment: "" - ) - textLabel.font = UIFont.systemFont(ofSize: 14) - textLabel.textColor = UIColor(white: 1.0, alpha: 0.6) - return textLabel - }() - - private let valueLabel: UILabel = { - let valueLabel = UILabel() - valueLabel.translatesAutoresizingMaskIntoConstraints = false - valueLabel.font = UIFont.systemFont(ofSize: 17) - valueLabel.textColor = .white - return valueLabel - }() - - let activityIndicator: SpinnerActivityIndicatorView = { - let activityIndicator = SpinnerActivityIndicatorView(style: .small) - activityIndicator.translatesAutoresizingMaskIntoConstraints = false - activityIndicator.tintColor = .white - activityIndicator.setContentHuggingPriority(.defaultHigh, for: .horizontal) - activityIndicator.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) - return activityIndicator - }() - - override init(frame: CGRect) { - super.init(frame: frame) - - addSubview(textLabel) - addSubview(activityIndicator) - addSubview(valueLabel) - - NSLayoutConstraint.activate([ - textLabel.topAnchor.constraint(equalTo: topAnchor), - textLabel.leadingAnchor.constraint(equalTo: leadingAnchor), - textLabel.trailingAnchor.constraint( - greaterThanOrEqualTo: activityIndicator.leadingAnchor, - constant: -8 - ), - - activityIndicator.topAnchor.constraint(equalTo: textLabel.topAnchor), - activityIndicator.bottomAnchor.constraint(equalTo: textLabel.bottomAnchor), - activityIndicator.trailingAnchor.constraint(equalTo: trailingAnchor), - - valueLabel.topAnchor.constraint(equalTo: textLabel.bottomAnchor, constant: 8), - valueLabel.leadingAnchor.constraint(equalTo: leadingAnchor), - valueLabel.trailingAnchor.constraint(equalTo: trailingAnchor), - valueLabel.bottomAnchor.constraint(equalTo: bottomAnchor), - ]) - - isAccessibilityElement = true - accessibilityLabel = textLabel.text - } - required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } diff --git a/ios/MullvadVPN/View controllers/Account/AccountDeviceRow.swift b/ios/MullvadVPN/View controllers/Account/AccountDeviceRow.swift new file mode 100644 index 000000000000..be9bbce2b148 --- /dev/null +++ b/ios/MullvadVPN/View controllers/Account/AccountDeviceRow.swift @@ -0,0 +1,81 @@ +// +// AccountDeviceRow.swift +// MullvadVPN +// +// Created by Mojgan on 2023-08-28. +// Copyright © 2023 Mullvad VPN AB. All rights reserved. +// + +import Foundation +import UIKit + +class AccountDeviceRow: UIView { + var deviceName: String? { + didSet { + deviceLabel.text = deviceName?.capitalized ?? "" + accessibilityValue = deviceName + } + } + + var infoButtonAction: (() -> Void)? + + private let titleLabel: UILabel = { + let label = UILabel() + label.text = NSLocalizedString( + "DEVICE_NAME", + tableName: "Account", + value: "Device name", + comment: "" + ) + label.font = UIFont.systemFont(ofSize: 14) + label.textColor = UIColor(white: 1.0, alpha: 0.6) + return label + }() + + private let deviceLabel: UILabel = { + let label = UILabel() + label.font = UIFont.systemFont(ofSize: 17) + label.textColor = .white + return label + }() + + private let infoButton: UIButton = { + let button = IncreasedHitButton(type: .system) + button.accessibilityIdentifier = "InfoButton" + button.tintColor = .white + button.setImage(UIImage(named: "IconInfo"), for: .normal) + return button + }() + + override init(frame: CGRect) { + super.init(frame: frame) + + let contentContainerView = UIStackView(arrangedSubviews: [titleLabel, deviceLabel]) + contentContainerView.axis = .vertical + contentContainerView.alignment = .leading + contentContainerView.spacing = 8 + + addConstrainedSubviews([contentContainerView, infoButton]) { + contentContainerView.pinEdgesToSuperview() + infoButton.leadingAnchor.constraint(equalToSystemSpacingAfter: deviceLabel.trailingAnchor, multiplier: 1) + infoButton.centerYAnchor.constraint(equalTo: deviceLabel.centerYAnchor) + } + + isAccessibilityElement = true + accessibilityLabel = titleLabel.text + + infoButton.addTarget( + self, + action: #selector(didTapInfoButton), + for: .touchUpInside + ) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func didTapInfoButton() { + infoButtonAction?() + } +} diff --git a/ios/MullvadVPN/View controllers/Account/AccountExpiryRow.swift b/ios/MullvadVPN/View controllers/Account/AccountExpiryRow.swift new file mode 100644 index 000000000000..b87e7c33cf2a --- /dev/null +++ b/ios/MullvadVPN/View controllers/Account/AccountExpiryRow.swift @@ -0,0 +1,101 @@ +// +// AccountExpiryRow.swift +// MullvadVPN +// +// Created by Mojgan on 2023-08-28. +// Copyright © 2023 Mullvad VPN AB. All rights reserved. +// + +import Foundation +import UIKit + +class AccountExpiryRow: UIView { + var value: Date? { + didSet { + let expiry = value + + if let expiry, expiry <= Date() { + let localizedString = NSLocalizedString( + "ACCOUNT_OUT_OF_TIME_LABEL", + tableName: "Account", + value: "OUT OF TIME", + comment: "" + ) + + valueLabel.text = localizedString + accessibilityValue = localizedString + + valueLabel.textColor = .dangerColor + } else { + let formattedDate = expiry.map { date in + DateFormatter.localizedString( + from: date, + dateStyle: .medium, + timeStyle: .short + ) + } + + valueLabel.text = formattedDate ?? "" + accessibilityValue = formattedDate + + valueLabel.textColor = .white + } + } + } + + private let textLabel: UILabel = { + let textLabel = UILabel() + textLabel.translatesAutoresizingMaskIntoConstraints = false + textLabel.text = NSLocalizedString( + "ACCOUNT_EXPIRY_LABEL", + tableName: "Account", + value: "Paid until", + comment: "" + ) + textLabel.font = UIFont.systemFont(ofSize: 14) + textLabel.textColor = UIColor(white: 1.0, alpha: 0.6) + return textLabel + }() + + private let valueLabel: UILabel = { + let valueLabel = UILabel() + valueLabel.translatesAutoresizingMaskIntoConstraints = false + valueLabel.font = UIFont.systemFont(ofSize: 17) + valueLabel.textColor = .white + return valueLabel + }() + + let activityIndicator: SpinnerActivityIndicatorView = { + let activityIndicator = SpinnerActivityIndicatorView(style: .small) + activityIndicator.translatesAutoresizingMaskIntoConstraints = false + activityIndicator.tintColor = .white + activityIndicator.setContentHuggingPriority(.defaultHigh, for: .horizontal) + activityIndicator.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) + return activityIndicator + }() + + override init(frame: CGRect) { + super.init(frame: frame) + + addConstrainedSubviews([textLabel, activityIndicator, valueLabel]) { + textLabel.pinEdgesToSuperview(.all().excluding([.trailing, .bottom])) + textLabel.trailingAnchor.constraint( + greaterThanOrEqualTo: activityIndicator.leadingAnchor, + constant: -UIMetrics.padding8 + ) + + activityIndicator.topAnchor.constraint(equalTo: textLabel.topAnchor) + activityIndicator.bottomAnchor.constraint(equalTo: textLabel.bottomAnchor) + activityIndicator.trailingAnchor.constraint(equalTo: trailingAnchor) + + valueLabel.pinEdgesToSuperview(.all().excluding(.top)) + valueLabel.topAnchor.constraint(equalTo: textLabel.bottomAnchor, constant: UIMetrics.padding8) + } + isAccessibilityElement = true + accessibilityLabel = textLabel.text + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/ios/MullvadVPN/View controllers/Account/AccountInteractor.swift b/ios/MullvadVPN/View controllers/Account/AccountInteractor.swift index 3b14c62c0272..956703bff049 100644 --- a/ios/MullvadVPN/View controllers/Account/AccountInteractor.swift +++ b/ios/MullvadVPN/View controllers/Account/AccountInteractor.swift @@ -27,11 +27,11 @@ final class AccountInteractor { self.tunnelManager = tunnelManager let tunnelObserver = - TunnelBlockObserver(didUpdateDeviceState: { [weak self] manager, deviceState, previousDeviceState in + TunnelBlockObserver(didUpdateDeviceState: { [weak self] _, deviceState, _ in self?.didReceiveDeviceState?(deviceState) }) - let paymentObserver = StorePaymentBlockObserver { [weak self] manager, event in + let paymentObserver = StorePaymentBlockObserver { [weak self] _, event in self?.didReceivePaymentEvent?(event) } diff --git a/ios/MullvadVPN/View controllers/Account/AccountNumberRow.swift b/ios/MullvadVPN/View controllers/Account/AccountNumberRow.swift new file mode 100644 index 000000000000..2f8ce5f037d2 --- /dev/null +++ b/ios/MullvadVPN/View controllers/Account/AccountNumberRow.swift @@ -0,0 +1,246 @@ +// +// AccountNumberRow.swift +// MullvadVPN +// +// Created by Mojgan on 2023-08-28. +// Copyright © 2023 Mullvad VPN AB. All rights reserved. +// + +import Foundation +import UIKit + +class AccountNumberRow: UIView { + var accountNumber: String? { + didSet { + updateView() + } + } + + var isObscured = true { + didSet { + updateView() + } + } + + var copyAccountNumber: (() -> Void)? + + private let titleLabel: UILabel = { + let textLabel = UILabel() + textLabel.text = NSLocalizedString( + "ACCOUNT_TOKEN_LABEL", + tableName: "Account", + value: "Account number", + comment: "" + ) + textLabel.font = UIFont.systemFont(ofSize: 14) + textLabel.textColor = UIColor(white: 1.0, alpha: 0.6) + return textLabel + }() + + private let accountNumberLabel: UILabel = { + let textLabel = UILabel() + textLabel.font = UIFont.monospacedSystemFont(ofSize: 17, weight: .regular) + textLabel.textColor = .white + return textLabel + }() + + private let showHideButton: UIButton = { + let button = UIButton(type: .system) + button.tintColor = .white + button.setContentHuggingPriority(.defaultHigh, for: .horizontal) + return button + }() + + private let copyButton: UIButton = { + let button = UIButton(type: .system) + button.tintColor = .white + button.setContentHuggingPriority(.defaultHigh, for: .horizontal) + return button + }() + + private var revertCopyImageWorkItem: DispatchWorkItem? + + override init(frame: CGRect) { + super.init(frame: frame) + + addConstrainedSubviews([titleLabel, accountNumberLabel, showHideButton, copyButton]) { + titleLabel.pinEdgesToSuperview(.all().excluding([.trailing, .bottom])) + titleLabel.trailingAnchor.constraint(greaterThanOrEqualTo: trailingAnchor) + + accountNumberLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: UIMetrics.padding8) + accountNumberLabel.leadingAnchor.constraint(equalTo: leadingAnchor) + accountNumberLabel.trailingAnchor.constraint(equalTo: showHideButton.leadingAnchor) + accountNumberLabel.bottomAnchor.constraint(equalTo: bottomAnchor) + + showHideButton.heightAnchor.constraint(equalTo: accountNumberLabel.heightAnchor) + showHideButton.centerYAnchor.constraint(equalTo: accountNumberLabel.centerYAnchor) + showHideButton.leadingAnchor.constraint(equalTo: accountNumberLabel.trailingAnchor) + + copyButton.heightAnchor.constraint(equalTo: accountNumberLabel.heightAnchor) + copyButton.centerYAnchor.constraint(equalTo: accountNumberLabel.centerYAnchor) + copyButton.leadingAnchor.constraint( + equalTo: showHideButton.trailingAnchor, + constant: UIMetrics.padding24 + ) + copyButton.trailingAnchor.constraint(equalTo: trailingAnchor) + } + + showHideButton.addTarget( + self, + action: #selector(didTapShowHideAccount), + for: .touchUpInside + ) + + copyButton.addTarget( + self, + action: #selector(didTapCopyAccountNumber), + for: .touchUpInside + ) + + isAccessibilityElement = true + accessibilityLabel = titleLabel.text + + showCheckmark(false) + updateView() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Private + + private func updateView() { + accountNumberLabel.text = displayAccountNumber ?? "" + showHideButton.setImage(showHideImage, for: .normal) + + accessibilityAttributedValue = _accessibilityAttributedValue + accessibilityCustomActions = _accessibilityCustomActions + } + + private var displayAccountNumber: String? { + guard let accountNumber else { + return nil + } + + let formattedString = accountNumber.formattedAccountNumber + + if isObscured { + return String(formattedString.map { ch in + ch == " " ? ch : "•" + }) + } else { + return formattedString + } + } + + private var showHideImage: UIImage? { + if isObscured { + return UIImage(named: "IconUnobscure") + } else { + return UIImage(named: "IconObscure") + } + } + + private var _accessibilityAttributedValue: NSAttributedString? { + guard let accountNumber else { + return nil + } + + if isObscured { + return NSAttributedString( + string: NSLocalizedString( + "ACCOUNT_ACCESSIBILITY_OBSCURED", + tableName: "Account", + value: "Obscured", + comment: "" + ) + ) + } else { + return NSAttributedString( + string: accountNumber, + attributes: [.accessibilitySpeechSpellOut: true] + ) + } + } + + private var _accessibilityCustomActions: [UIAccessibilityCustomAction]? { + guard accountNumber != nil else { return nil } + + return [ + UIAccessibilityCustomAction( + name: showHideAccessibilityActionName, + target: self, + selector: #selector(didTapShowHideAccount) + ), + UIAccessibilityCustomAction( + name: NSLocalizedString( + "ACCOUNT_ACCESSIBILITY_COPY_TO_PASTEBOARD", + tableName: "Account", + value: "Copy to pasteboard", + comment: "" + ), + target: self, + selector: #selector(didTapCopyAccountNumber) + ), + ] + } + + private var showHideAccessibilityActionName: String { + if isObscured { + return NSLocalizedString( + "ACCOUNT_ACCESSIBILITY_SHOW_ACCOUNT_NUMBER", + tableName: "Account", + value: "Show account number", + comment: "" + ) + } else { + return NSLocalizedString( + "ACCOUNT_ACCESSIBILITY_HIDE_ACCOUNT_NUMBER", + tableName: "Account", + value: "Hide account number", + comment: "" + ) + } + } + + private func showCheckmark(_ showCheckmark: Bool) { + if showCheckmark { + let tickIcon = UIImage(named: "IconTick") + + copyButton.setImage(tickIcon, for: .normal) + copyButton.tintColor = .successColor + } else { + let copyIcon = UIImage(named: "IconCopy") + + copyButton.setImage(copyIcon, for: .normal) + copyButton.tintColor = .white + } + } + + // MARK: - Actions + + @objc private func didTapShowHideAccount() { + isObscured.toggle() + updateView() + + UIAccessibility.post(notification: .layoutChanged, argument: nil) + } + + @objc private func didTapCopyAccountNumber() { + let delayedWorkItem = DispatchWorkItem { [weak self] in + self?.showCheckmark(false) + } + + revertCopyImageWorkItem?.cancel() + revertCopyImageWorkItem = delayedWorkItem + + showCheckmark(true) + copyAccountNumber?() + + DispatchQueue.main.asyncAfter( + deadline: .now() + .seconds(2), + execute: delayedWorkItem + ) + } +} diff --git a/ios/MullvadVPN/View controllers/Account/AccountViewController.swift b/ios/MullvadVPN/View controllers/Account/AccountViewController.swift index 8384993285f5..4c50c91110c9 100644 --- a/ios/MullvadVPN/View controllers/Account/AccountViewController.swift +++ b/ios/MullvadVPN/View controllers/Account/AccountViewController.swift @@ -57,28 +57,8 @@ class AccountViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() - view.backgroundColor = .secondaryColor - let scrollView = UIScrollView() - scrollView.translatesAutoresizingMaskIntoConstraints = false - scrollView.addSubview(contentView) - view.addSubview(scrollView) - - NSLayoutConstraint.activate([ - scrollView.topAnchor.constraint(equalTo: view.topAnchor), - scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor), - scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor), - scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor), - - contentView.topAnchor.constraint(equalTo: scrollView.topAnchor), - contentView.bottomAnchor - .constraint(greaterThanOrEqualTo: scrollView.safeAreaLayoutGuide.bottomAnchor), - contentView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor), - contentView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor), - contentView.widthAnchor.constraint(equalTo: scrollView.widthAnchor), - ]) - navigationItem.title = NSLocalizedString( "NAVIGATION_TITLE", tableName: "Account", @@ -96,30 +76,10 @@ class AccountViewController: UIViewController { self?.copyAccountToken() } - contentView.redeemVoucherButton.addTarget( - self, - action: #selector(redeemVoucher), - for: .touchUpInside - ) - contentView.accountDeviceRow.infoButtonAction = { [weak self] in self?.actionHandler?(.deviceInfo) } - contentView.restorePurchasesButton.addTarget( - self, - action: #selector(restorePurchases), - for: .touchUpInside - ) - contentView.purchaseButton.addTarget( - self, - action: #selector(doPurchase), - for: .touchUpInside - ) - contentView.logoutButton.addTarget(self, action: #selector(logOut), for: .touchUpInside) - - contentView.deleteButton.addTarget(self, action: #selector(deleteAccount), for: .touchUpInside) - interactor.didReceiveDeviceState = { [weak self] deviceState in self?.updateView(from: deviceState) } @@ -127,10 +87,16 @@ class AccountViewController: UIViewController { interactor.didReceivePaymentEvent = { [weak self] event in self?.didReceivePaymentEvent(event) } - + configUI() + addActions() updateView(from: interactor.deviceState) applyViewState(animated: false) + requestStoreProductsIfCan() + } + // MARK: - Private + + private func requestStoreProductsIfCan() { if StorePaymentManager.canMakePayments { requestStoreProducts() } else { @@ -138,7 +104,41 @@ class AccountViewController: UIViewController { } } - // MARK: - Private + private func configUI() { + let scrollView = UIScrollView() + + view.addConstrainedSubviews([scrollView]) { + scrollView.pinEdgesToSuperview() + } + + scrollView.addConstrainedSubviews([contentView]) { + contentView.pinEdgesToSuperview(.all().excluding(.bottom)) + contentView.bottomAnchor.constraint(greaterThanOrEqualTo: scrollView.safeAreaLayoutGuide.bottomAnchor) + contentView.widthAnchor.constraint(equalTo: scrollView.widthAnchor) + } + } + + private func addActions() { + contentView.redeemVoucherButton.addTarget( + self, + action: #selector(redeemVoucher), + for: .touchUpInside + ) + + contentView.restorePurchasesButton.addTarget( + self, + action: #selector(restorePurchases), + for: .touchUpInside + ) + contentView.purchaseButton.addTarget( + self, + action: #selector(doPurchase), + for: .touchUpInside + ) + contentView.logoutButton.addTarget(self, action: #selector(logOut), for: .touchUpInside) + + contentView.deleteButton.addTarget(self, action: #selector(deleteAccount), for: .touchUpInside) + } private func requestStoreProducts() { let productKind = StoreSubscription.thirtyDays diff --git a/ios/MullvadVPN/View controllers/AccountDeletion/AccountDeletionContentView.swift b/ios/MullvadVPN/View controllers/AccountDeletion/AccountDeletionContentView.swift index 64be3a200bcc..7884904c06b1 100644 --- a/ios/MullvadVPN/View controllers/AccountDeletion/AccountDeletionContentView.swift +++ b/ios/MullvadVPN/View controllers/AccountDeletion/AccountDeletionContentView.swift @@ -72,7 +72,9 @@ class AccountDeletionContentView: UIView { "TIP_TEXT", tableName: "Account", value: """ - This logs out all devices using this account and all VPN access will be denied even if there is time left on the account. Enter the last 4 digits of the account number and hit OK if you really want to delete the account : + This logs out all devices using this account and all \ + VPN access will be denied even if there is time left on the account. \ + Enter the last 4 digits of the account number and hit OK if you really want to delete the account : """, comment: "" ) @@ -338,7 +340,7 @@ class AccountDeletionContentView: UIView { private func addKeyboardResponder() { keyboardResponder = AutomaticKeyboardResponder( targetView: self, - handler: { [weak self] targetView, offset in + handler: { [weak self] _, offset in guard let self else { return } self.bottomsOfButtonsConstraint?.constant = self.accountTextField.isFirstResponder ? -offset : 0 self.layoutIfNeeded() diff --git a/ios/MullvadVPN/View controllers/Login/AccountInputGroupView.swift b/ios/MullvadVPN/View controllers/Login/AccountInputGroupView.swift index ac0c21831566..2a34644b6ddc 100644 --- a/ios/MullvadVPN/View controllers/Login/AccountInputGroupView.swift +++ b/ios/MullvadVPN/View controllers/Login/AccountInputGroupView.swift @@ -13,6 +13,7 @@ private let animationDuration: Duration = .milliseconds(250) final class AccountInputGroupView: UIView { private let minimumAccountTokenLength = 10 + private var showsLastUsedAccountRow = false enum Style { case normal, error, authenticating @@ -22,6 +23,7 @@ final class AccountInputGroupView: UIView { let button = UIButton(type: .custom) button.translatesAutoresizingMaskIntoConstraints = false button.setImage(UIImage(named: "IconArrow"), for: .normal) + button.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) button.accessibilityLabel = NSLocalizedString( "ACCOUNT_INPUT_LOGIN_BUTTON_ACCESSIBILITY_LABEL", tableName: "AccountInput", @@ -63,7 +65,7 @@ final class AccountInputGroupView: UIView { textField.keyboardType = .numberPad textField.returnKeyType = .done textField.enablesReturnKeyAutomatically = false - + textField.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) return textField }() @@ -77,7 +79,6 @@ final class AccountInputGroupView: UIView { let view = UIView() view.translatesAutoresizingMaskIntoConstraints = false view.backgroundColor = .white - return view }() @@ -86,12 +87,9 @@ final class AccountInputGroupView: UIView { view.translatesAutoresizingMaskIntoConstraints = false view.backgroundColor = .white.withAlphaComponent(0.8) view.accessibilityElementsHidden = true - return view }() - private var showsLastUsedAccountRow = false - private let lastUsedAccountButton: UIButton = { let button = UIButton(type: .system) button.translatesAutoresizingMaskIntoConstraints = false @@ -100,6 +98,7 @@ final class AccountInputGroupView: UIView { button.contentHorizontalAlignment = .leading button.contentEdgeInsets = UIMetrics.textFieldMargins button.setTitleColor(UIColor.AccountTextField.NormalState.textColor, for: .normal) + button.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) button.accessibilityLabel = NSLocalizedString( "LAST_USED_ACCOUNT_ACCESSIBILITY_LABEL", tableName: "AccountInput", @@ -114,6 +113,7 @@ final class AccountInputGroupView: UIView { button.translatesAutoresizingMaskIntoConstraints = false button.setImage(UIImage(named: "IconCloseSml"), for: .normal) button.imageView?.tintColor = .primaryColor.withAlphaComponent(0.4) + button.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) button.accessibilityLabel = NSLocalizedString( "REMOVE_LAST_USED_ACCOUNT_ACCESSIBILITY_LABEL", tableName: "AccountInput", @@ -126,16 +126,12 @@ final class AccountInputGroupView: UIView { let contentView: UIView = { let view = UIView() view.backgroundColor = .clear - view.translatesAutoresizingMaskIntoConstraints = false - return view }() private(set) var loginState = LoginState.default - private let borderRadius = CGFloat(8) private let borderWidth = CGFloat(2) - private var lastUsedAccount: String? private var borderColor: UIColor { @@ -181,7 +177,6 @@ final class AccountInputGroupView: UIView { private let borderLayer = AccountInputBorderLayer() private let contentLayerMask = CALayer() - private var lastUsedAccountVisibleConstraint: NSLayoutConstraint! private var lastUsedAccountHiddenConstraint: NSLayoutConstraint! @@ -189,87 +184,68 @@ final class AccountInputGroupView: UIView { override init(frame: CGRect) { super.init(frame: frame) + configUI() + setAppearance() + addActions() + updateAppearance() + updateTextFieldEnabled() + updateSendButtonAppearance(animated: false) + updateKeyboardReturnKeyEnabled() + addTextFieldNotificationObservers() + addAccessibilityNotificationObservers() + } - addSubview(contentView) - contentView.addSubview(topRowView) - contentView.addSubview(separator) - contentView.addSubview(bottomRowView) - topRowView.addSubview(privateTextField) - topRowView.addSubview(sendButton) - bottomRowView.addSubview(lastUsedAccountButton) - bottomRowView.addSubview(removeLastUsedAccountButton) - - privateTextField.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) - sendButton.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) - - lastUsedAccountButton.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) - removeLastUsedAccountButton.setContentCompressionResistancePriority( - .defaultHigh, - for: .horizontal - ) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } - lastUsedAccountVisibleConstraint = heightAnchor - .constraint(equalTo: contentView.heightAnchor) - lastUsedAccountHiddenConstraint = heightAnchor.constraint(equalTo: topRowView.heightAnchor) + private func configUI() { + addConstrainedSubviews([contentView]) { + contentView.pinEdgesToSuperview(.all().excluding(.bottom)) + } + + contentView.addConstrainedSubviews([topRowView, separator, bottomRowView]) { + topRowView.pinEdgesToSuperview(.all().excluding(.bottom)) + topRowView.bottomAnchor.constraint(equalTo: separator.topAnchor) - NSLayoutConstraint.activate([ - lastUsedAccountHiddenConstraint, - - contentView.topAnchor.constraint(equalTo: topAnchor), - contentView.leadingAnchor.constraint(equalTo: leadingAnchor), - contentView.trailingAnchor.constraint(equalTo: trailingAnchor), - - topRowView.topAnchor.constraint(equalTo: contentView.topAnchor), - topRowView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), - topRowView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), - topRowView.bottomAnchor.constraint(equalTo: separator.topAnchor), - - separator.topAnchor.constraint(equalTo: topRowView.bottomAnchor), - separator.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), - separator.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), - separator.heightAnchor.constraint(equalToConstant: borderWidth), - - bottomRowView.topAnchor.constraint(equalTo: separator.bottomAnchor), - bottomRowView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), - bottomRowView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), - bottomRowView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), - - privateTextField.topAnchor.constraint(equalTo: topRowView.topAnchor), - privateTextField.leadingAnchor.constraint(equalTo: topRowView.leadingAnchor), - privateTextField.trailingAnchor.constraint(equalTo: sendButton.leadingAnchor), - privateTextField.bottomAnchor.constraint(equalTo: topRowView.bottomAnchor), - - sendButton.topAnchor.constraint(equalTo: topRowView.topAnchor), - sendButton.trailingAnchor.constraint(equalTo: topRowView.trailingAnchor), - sendButton.bottomAnchor.constraint(equalTo: topRowView.bottomAnchor), - sendButton.widthAnchor.constraint(equalTo: sendButton.heightAnchor), - - lastUsedAccountButton.topAnchor.constraint(equalTo: bottomRowView.topAnchor), - lastUsedAccountButton.bottomAnchor.constraint(equalTo: bottomRowView.bottomAnchor), - lastUsedAccountButton.leadingAnchor.constraint(equalTo: bottomRowView.leadingAnchor), - lastUsedAccountButton.trailingAnchor - .constraint(equalTo: removeLastUsedAccountButton.leadingAnchor), - - removeLastUsedAccountButton.topAnchor.constraint(equalTo: bottomRowView.topAnchor), - removeLastUsedAccountButton.bottomAnchor - .constraint(equalTo: bottomRowView.bottomAnchor), - removeLastUsedAccountButton.trailingAnchor - .constraint(equalTo: bottomRowView.trailingAnchor), - removeLastUsedAccountButton.widthAnchor.constraint(equalTo: sendButton.widthAnchor), - ]) + separator.pinEdgesToSuperview(.all().excluding([.bottom, .top])) + separator.topAnchor.constraint(equalTo: topRowView.bottomAnchor) + separator.heightAnchor.constraint(equalToConstant: borderWidth) + bottomRowView.topAnchor.constraint(equalTo: separator.bottomAnchor) + bottomRowView.pinEdgesToSuperview(.all().excluding(.top)) + } + + topRowView.addConstrainedSubviews([privateTextField, sendButton]) { + privateTextField.trailingAnchor.constraint(equalTo: sendButton.leadingAnchor) + privateTextField.pinEdgesToSuperview(.all().excluding(.trailing)) + + sendButton.pinEdgesToSuperview(.all().excluding(.leading)) + sendButton.widthAnchor.constraint(equalTo: sendButton.heightAnchor) + } + + bottomRowView.addConstrainedSubviews([lastUsedAccountButton, removeLastUsedAccountButton]) { + lastUsedAccountButton.pinEdgesToSuperview(.all().excluding(.trailing)) + lastUsedAccountButton.trailingAnchor.constraint(equalTo: removeLastUsedAccountButton.leadingAnchor) + + removeLastUsedAccountButton.pinEdgesToSuperview(.all().excluding(.leading)) + removeLastUsedAccountButton.widthAnchor.constraint(equalTo: sendButton.widthAnchor) + } + + lastUsedAccountVisibleConstraint = heightAnchor.constraint(equalTo: contentView.heightAnchor) + lastUsedAccountHiddenConstraint = heightAnchor.constraint(equalTo: topRowView.heightAnchor) + lastUsedAccountHiddenConstraint.isActive = true + } + + private func setAppearance() { backgroundColor = UIColor.clear borderLayer.lineWidth = borderWidth borderLayer.fillColor = UIColor.clear.cgColor contentView.layer.mask = contentLayerMask - layer.insertSublayer(borderLayer, at: 0) + } - updateAppearance() - updateTextFieldEnabled() - updateSendButtonAppearance(animated: false) - updateKeyboardReturnKeyEnabled() - + private func addActions() { lastUsedAccountButton.addTarget( self, action: #selector(didTapLastUsedAccount), @@ -282,15 +258,9 @@ final class AccountInputGroupView: UIView { for: .touchUpInside ) - addTextFieldNotificationObservers() - addAccessibilityNotificationObservers() sendButton.addTarget(self, action: #selector(handleSendButton(_:)), for: .touchUpInside) } - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - func setLoginState(_ state: LoginState, animated: Bool) { loginState = state @@ -548,9 +518,8 @@ final class AccountInputGroupView: UIView { private func backgroundMaskImage(borderPath: UIBezierPath) -> UIImage { let renderer = UIGraphicsImageRenderer(bounds: borderPath.bounds) - return renderer.image { ctx in + return renderer.image { _ in borderPath.fill() - // strip out any overlapping pixels between the border and the background borderPath.stroke(with: .clear, alpha: 0) } diff --git a/ios/MullvadVPN/View controllers/RedeemVoucher/RedeemVoucherContentView.swift b/ios/MullvadVPN/View controllers/RedeemVoucher/RedeemVoucherContentView.swift index 97ada9e1a93c..b31b5c499984 100644 --- a/ios/MullvadVPN/View controllers/RedeemVoucher/RedeemVoucherContentView.swift +++ b/ios/MullvadVPN/View controllers/RedeemVoucher/RedeemVoucherContentView.swift @@ -301,7 +301,7 @@ final class RedeemVoucherContentView: UIView { private func addKeyboardResponder() { keyboardResponder = AutomaticKeyboardResponder( targetView: self, - handler: { [weak self] targetView, offset in + handler: { [weak self] _, offset in guard let self else { return } guard self.textField.isFirstResponder else { return } self.bottomsOfButtonsConstraint?.constant = -offset diff --git a/ios/MullvadVPN/View controllers/SelectLocation/LocationCellFactory.swift b/ios/MullvadVPN/View controllers/SelectLocation/LocationCellFactory.swift index a6ff5e497948..46e7d97aacc7 100644 --- a/ios/MullvadVPN/View controllers/SelectLocation/LocationCellFactory.swift +++ b/ios/MullvadVPN/View controllers/SelectLocation/LocationCellFactory.swift @@ -43,7 +43,7 @@ final class LocationCellFactory: CellFactoryProtocol { cell.locationLabel.text = node.displayName cell.showsCollapseControl = node.isCollapsible cell.isExpanded = node.showsChildren - cell.didCollapseHandler = { [weak self] cell in + cell.didCollapseHandler = { [weak self] _ in self?.delegate?.collapseCell(for: item) } } diff --git a/ios/Operations/AsyncOperationQueue.swift b/ios/Operations/AsyncOperationQueue.swift index 1944dccf2540..57c4451138a7 100644 --- a/ios/Operations/AsyncOperationQueue.swift +++ b/ios/Operations/AsyncOperationQueue.swift @@ -73,7 +73,7 @@ private final class ExclusivityManager { operationsByCategory[category] = operations - operation.onFinish { [weak self] op, error in + operation.onFinish { [weak self] op, _ in self?.removeOperation(op, categories: categories) } } diff --git a/ios/Shared/ApplicationConfiguration.swift b/ios/Shared/ApplicationConfiguration.swift index d7190d9071e7..b6198b2749ce 100644 --- a/ios/Shared/ApplicationConfiguration.swift +++ b/ios/Shared/ApplicationConfiguration.swift @@ -12,6 +12,7 @@ import struct Network.IPv4Address enum ApplicationConfiguration { /// Shared container security group identifier. static var securityGroupIdentifier: String { + // swiftlint:disable:next force_cast Bundle.main.object(forInfoDictionaryKey: "ApplicationSecurityGroupIdentifier") as! String } diff --git a/ios/Shared/ApplicationTarget.swift b/ios/Shared/ApplicationTarget.swift index 88a2674728f2..f46fa2c64ee5 100644 --- a/ios/Shared/ApplicationTarget.swift +++ b/ios/Shared/ApplicationTarget.swift @@ -13,6 +13,7 @@ enum ApplicationTarget: CaseIterable { /// Returns target bundle identifier. var bundleIdentifier: String { + // swiftlint:disable:next force_cast let mainBundleIdentifier = Bundle.main.object(forInfoDictionaryKey: "MainApplicationIdentifier") as! String switch self { case .mainApp: