summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorMojgan <Mojgan.jelodar@codic.se>2023-08-29 15:22:24 +0200
committerMojgan <Mojgan.jelodar@codic.se>2023-08-29 15:23:07 +0200
commitfe27d80236374922f050d3b3b71e616aa3195924 (patch)
tree7b85394067ee5bf4a20e260655d1da1be4c8b422
parent90d15e4e3e4858a9d5173157c1b79aa57f6736d1 (diff)
downloadmullvadvpn-fe27d80236374922f050d3b3b71e616aa3195924.tar.xz
mullvadvpn-fe27d80236374922f050d3b3b71e616aa3195924.zip
fix some linting violations
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj12
-rw-r--r--ios/MullvadVPN/Classes/AppPreferences.swift2
-rw-r--r--ios/MullvadVPN/Coordinators/AccountCoordinator.swift6
-rw-r--r--ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift30
-rw-r--r--ios/MullvadVPN/Coordinators/InAppPurchaseCoordinator.swift2
-rw-r--r--ios/MullvadVPN/Extensions/NSRegularExpression+IPAddress.swift4
-rw-r--r--ios/MullvadVPN/Notifications/Notification Providers/AccountExpiryInAppNotificationProvider.swift2
-rw-r--r--ios/MullvadVPN/Notifications/Notification Providers/AccountExpirySystemNotificationProvider.swift2
-rw-r--r--ios/MullvadVPN/Presentation controllers/FormsheetPresentationController.swift6
-rw-r--r--ios/MullvadVPN/View controllers/Account/AccountContentView.swift430
-rw-r--r--ios/MullvadVPN/View controllers/Account/AccountDeviceRow.swift81
-rw-r--r--ios/MullvadVPN/View controllers/Account/AccountExpiryRow.swift101
-rw-r--r--ios/MullvadVPN/View controllers/Account/AccountInteractor.swift4
-rw-r--r--ios/MullvadVPN/View controllers/Account/AccountNumberRow.swift246
-rw-r--r--ios/MullvadVPN/View controllers/Account/AccountViewController.swift84
-rw-r--r--ios/MullvadVPN/View controllers/AccountDeletion/AccountDeletionContentView.swift6
-rw-r--r--ios/MullvadVPN/View controllers/Login/AccountInputGroupView.swift133
-rw-r--r--ios/MullvadVPN/View controllers/RedeemVoucher/RedeemVoucherContentView.swift2
-rw-r--r--ios/MullvadVPN/View controllers/SelectLocation/LocationCellFactory.swift2
-rw-r--r--ios/Operations/AsyncOperationQueue.swift2
-rw-r--r--ios/Shared/ApplicationConfiguration.swift1
-rw-r--r--ios/Shared/ApplicationTarget.swift1
22 files changed, 575 insertions, 584 deletions
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj
index c6733a622c..7011d1dc79 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 = "<group>"; };
F0C6FA822A6A729500F521F0 /* InAppPurchaseCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InAppPurchaseCoordinator.swift; sourceTree = "<group>"; };
F0C6FA842A6A733700F521F0 /* InAppPurchaseInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InAppPurchaseInteractor.swift; sourceTree = "<group>"; };
+ F0DA87462A9CB9A2006044F1 /* AccountExpiryRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountExpiryRow.swift; sourceTree = "<group>"; };
+ F0DA87482A9CBA9F006044F1 /* AccountDeviceRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountDeviceRow.swift; sourceTree = "<group>"; };
+ F0DA874A2A9CBACB006044F1 /* AccountNumberRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountNumberRow.swift; sourceTree = "<group>"; };
F0E3618A2A4ADD2F00AEEF2B /* WelcomeContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeContentView.swift; sourceTree = "<group>"; };
F0E8CC022A4C753B007ED3B4 /* WelcomeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeViewController.swift; sourceTree = "<group>"; };
F0E8CC092A4EE127007ED3B4 /* SetupAccountCompletedContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupAccountCompletedContentView.swift; sourceTree = "<group>"; };
@@ -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 = "<group>";
@@ -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 4a790ba8fc..95e49ad2a0 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 a7d6f261e7..516ade62a1 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 1ccbb32632..2c4a0cacab 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 dc97e680dc..f4cea6a013 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 30e698f9c4..7cb5ae90da 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 0d0550401a..da60a1396d 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 520983371a..b8ad7054f0 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 016153f906..5bee1b2156 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 623d2e52f2..707784e21e 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 0000000000..be9bbce2b1
--- /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 0000000000..b87e7c33cf
--- /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 3b14c62c02..956703bff0 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 0000000000..2f8ce5f037
--- /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 8384993285..4c50c91110 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 64be3a200b..7884904c06 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 ac0c218315..2a34644b6d 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
- )
-
- lastUsedAccountVisibleConstraint = heightAnchor
- .constraint(equalTo: contentView.heightAnchor)
- lastUsedAccountHiddenConstraint = heightAnchor.constraint(equalTo: topRowView.heightAnchor)
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
- NSLayoutConstraint.activate([
- lastUsedAccountHiddenConstraint,
+ private func configUI() {
+ addConstrainedSubviews([contentView]) {
+ contentView.pinEdgesToSuperview(.all().excluding(.bottom))
+ }
- contentView.topAnchor.constraint(equalTo: topAnchor),
- contentView.leadingAnchor.constraint(equalTo: leadingAnchor),
- contentView.trailingAnchor.constraint(equalTo: trailingAnchor),
+ contentView.addConstrainedSubviews([topRowView, separator, bottomRowView]) {
+ topRowView.pinEdgesToSuperview(.all().excluding(.bottom))
+ topRowView.bottomAnchor.constraint(equalTo: separator.topAnchor)
- 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.pinEdgesToSuperview(.all().excluding([.bottom, .top]))
+ separator.topAnchor.constraint(equalTo: topRowView.bottomAnchor)
+ separator.heightAnchor.constraint(equalToConstant: borderWidth)
- 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.pinEdgesToSuperview(.all().excluding(.top))
+ }
- bottomRowView.topAnchor.constraint(equalTo: separator.bottomAnchor),
- bottomRowView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
- bottomRowView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
- bottomRowView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
+ topRowView.addConstrainedSubviews([privateTextField, sendButton]) {
+ privateTextField.trailingAnchor.constraint(equalTo: sendButton.leadingAnchor)
+ privateTextField.pinEdgesToSuperview(.all().excluding(.trailing))
- 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.pinEdgesToSuperview(.all().excluding(.leading))
+ sendButton.widthAnchor.constraint(equalTo: sendButton.heightAnchor)
+ }
- sendButton.topAnchor.constraint(equalTo: topRowView.topAnchor),
- sendButton.trailingAnchor.constraint(equalTo: topRowView.trailingAnchor),
- sendButton.bottomAnchor.constraint(equalTo: topRowView.bottomAnchor),
- sendButton.widthAnchor.constraint(equalTo: sendButton.heightAnchor),
+ bottomRowView.addConstrainedSubviews([lastUsedAccountButton, removeLastUsedAccountButton]) {
+ lastUsedAccountButton.pinEdgesToSuperview(.all().excluding(.trailing))
+ lastUsedAccountButton.trailingAnchor.constraint(equalTo: removeLastUsedAccountButton.leadingAnchor)
- 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.pinEdgesToSuperview(.all().excluding(.leading))
+ removeLastUsedAccountButton.widthAnchor.constraint(equalTo: sendButton.widthAnchor)
+ }
- removeLastUsedAccountButton.topAnchor.constraint(equalTo: bottomRowView.topAnchor),
- removeLastUsedAccountButton.bottomAnchor
- .constraint(equalTo: bottomRowView.bottomAnchor),
- removeLastUsedAccountButton.trailingAnchor
- .constraint(equalTo: bottomRowView.trailingAnchor),
- 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 97ada9e1a9..b31b5c4999 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 a6ff5e4979..46e7d97aac 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 1944dccf25..57c4451138 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 d7190d9071..b6198b2749 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 88a2674728..f46fa2c64e 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: