summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorJon Petersson <jon.petersson@kvadrat.se>2023-05-22 13:25:38 +0200
committerJon Petersson <jon.petersson@kvadrat.se>2023-06-08 16:36:40 +0200
commite4677b9fe4b22bfbdb9140507ad6222feddf6b9d (patch)
tree533abd51cf76d60786ef4f9130d47d271707d6fd
parent04e49a9f00467a35138ba12d4c812397797ff1ce (diff)
downloadmullvadvpn-e4677b9fe4b22bfbdb9140507ad6222feddf6b9d.tar.xz
mullvadvpn-e4677b9fe4b22bfbdb9140507ad6222feddf6b9d.zip
Create custom alert dialogs
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj28
-rw-r--r--ios/MullvadVPN/Classes/CustomAlertViewController.swift190
-rw-r--r--ios/MullvadVPN/Classes/Swizzle.swift30
-rw-r--r--ios/MullvadVPN/Coordinators/App/TunnelCoordinator.swift41
-rw-r--r--ios/MullvadVPN/Extensions/NSAttributedString+Markdown.swift59
-rw-r--r--ios/MullvadVPN/Extensions/UIFont+Weight.swift20
-rw-r--r--ios/MullvadVPN/Notifications/Notification Providers/RegisteredDeviceInAppNotificationProvider.swift10
-rw-r--r--ios/MullvadVPN/Operations/AlertPresenter.swift41
-rw-r--r--ios/MullvadVPN/Operations/PresentAlertOperation.swift31
-rw-r--r--ios/MullvadVPN/Operations/ProductsRequestOperation.swift12
-rw-r--r--ios/MullvadVPN/SceneDelegate.swift17
-rw-r--r--ios/MullvadVPN/Supporting Files/Assets.xcassets/IconAlert.imageset/Contents.json15
-rw-r--r--ios/MullvadVPN/Supporting Files/Assets.xcassets/IconAlert.imageset/IconAlert.pdfbin0 -> 1070 bytes
-rw-r--r--ios/MullvadVPN/UI appearance/UIMetrics.swift15
-rw-r--r--ios/MullvadVPN/View controllers/Account/AccountViewController.swift75
-rw-r--r--ios/MullvadVPN/View controllers/DeviceList/DeviceManagementViewController.swift85
-rw-r--r--ios/MullvadVPN/View controllers/OutOfTime/OutOfTimeContentView.swift2
-rw-r--r--ios/MullvadVPN/View controllers/OutOfTime/OutOfTimeViewController.swift75
-rw-r--r--ios/MullvadVPN/View controllers/Preferences/PreferencesViewController.swift31
-rw-r--r--ios/MullvadVPN/View controllers/Preferences/PreferencesViewModel.swift2
-rw-r--r--ios/MullvadVPN/View controllers/ProblemReport/ProblemReportViewController.swift39
-rwxr-xr-xios/convert-assets.rb1
22 files changed, 504 insertions, 315 deletions
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj
index 2be0a3ed02..85f36e1a15 100644
--- a/ios/MullvadVPN.xcodeproj/project.pbxproj
+++ b/ios/MullvadVPN.xcodeproj/project.pbxproj
@@ -218,7 +218,6 @@
5898D2AB2901845400EB5EBA /* libRelaySelector.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5898D29829017DAC00EB5EBA /* libRelaySelector.a */; };
5898D2AC2901845400EB5EBA /* libTunnelProviderMessaging.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5898D28929017BD400EB5EBA /* libTunnelProviderMessaging.a */; };
5898D2AE290185D200EB5EBA /* ProxyURLResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5898D2AD290185D200EB5EBA /* ProxyURLResponse.swift */; };
- 589A454C28DDF5E100565204 /* Swizzle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 589A454B28DDF5E100565204 /* Swizzle.swift */; };
589A455C28E094BF00565204 /* OperationSmokeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58DF5B7E2852778600E92647 /* OperationSmokeTests.swift */; };
589A455D28E094BF00565204 /* OperationObserverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 583E1E292848DF67004838B3 /* OperationObserverTests.swift */; };
589A455F28E094BF00565204 /* OperationConditionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580CBFB72848D503007878F0 /* OperationConditionTests.swift */; };
@@ -373,12 +372,14 @@
58FEEB58260B662E00A621A8 /* AutomaticKeyboardResponder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FEEB57260B662E00A621A8 /* AutomaticKeyboardResponder.swift */; };
58FF2C03281BDE02009EF542 /* SettingsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FF2C02281BDE02009EF542 /* SettingsManager.swift */; };
7A09C98129D99215000C2CAC /* String+FuzzyMatch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A09C98029D99215000C2CAC /* String+FuzzyMatch.swift */; };
+ 7A21DACF2A30AA3700A787A9 /* UITextField+Appearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A21DACE2A30AA3700A787A9 /* UITextField+Appearance.swift */; };
7A42DEC92A05164100B209BE /* SettingsInputCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A42DEC82A05164100B209BE /* SettingsInputCell.swift */; };
7A42DECD2A09064C00B209BE /* SelectableSettingsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A42DECC2A09064C00B209BE /* SelectableSettingsCell.swift */; };
7A7AD28D29DC677800480EF1 /* FirstTimeLaunch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A7AD28C29DC677800480EF1 /* FirstTimeLaunch.swift */; };
7A7AD28F29DEDB1C00480EF1 /* SettingsHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A7AD28E29DEDB1C00480EF1 /* SettingsHeaderView.swift */; };
7A818F1F29F0305800C7F0F4 /* RootConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A818F1E29F0305800C7F0F4 /* RootConfiguration.swift */; };
- 7AD2DA1529DC4EB900250737 /* UITextField+Appearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AD2DA1429DC4EB900250737 /* UITextField+Appearance.swift */; };
+ 7ABE318D2A1CDD4500DF4963 /* UIFont+Weight.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ABE318C2A1CDD4500DF4963 /* UIFont+Weight.swift */; };
+ 7AE47E522A17972A000418DA /* CustomAlertViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AE47E512A17972A000418DA /* CustomAlertViewController.swift */; };
7AF0419E29E957EB00D492DD /* AccountCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AF0419D29E957EB00D492DD /* AccountCoordinator.swift */; };
A917351F29FAA9C400D5DCFD /* RESTTransportStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = A917351E29FAA9C400D5DCFD /* RESTTransportStrategy.swift */; };
A917352129FAAA5200D5DCFD /* TransportStrategyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A917352029FAAA5200D5DCFD /* TransportStrategyTests.swift */; };
@@ -976,7 +977,6 @@
5898D2AF2902A67C00EB5EBA /* RelayLocation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayLocation.swift; sourceTree = "<group>"; };
5898D2B12902A6DE00EB5EBA /* RelayConstraint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayConstraint.swift; sourceTree = "<group>"; };
5898D2B62902A9EA00EB5EBA /* PacketTunnelRelay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelRelay.swift; sourceTree = "<group>"; };
- 589A454B28DDF5E100565204 /* Swizzle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Swizzle.swift; sourceTree = "<group>"; };
589A455228E094B300565204 /* OperationsTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = OperationsTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
589D28772846250500F9A7B3 /* OperationCondition.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OperationCondition.swift; sourceTree = "<group>"; };
589D28782846250500F9A7B3 /* AsyncOperationQueue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AsyncOperationQueue.swift; sourceTree = "<group>"; };
@@ -1093,12 +1093,14 @@
58FEEB57260B662E00A621A8 /* AutomaticKeyboardResponder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutomaticKeyboardResponder.swift; sourceTree = "<group>"; };
58FF2C02281BDE02009EF542 /* SettingsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsManager.swift; sourceTree = "<group>"; };
7A09C98029D99215000C2CAC /* String+FuzzyMatch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+FuzzyMatch.swift"; sourceTree = "<group>"; };
+ 7A21DACE2A30AA3700A787A9 /* UITextField+Appearance.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UITextField+Appearance.swift"; sourceTree = "<group>"; };
7A42DEC82A05164100B209BE /* SettingsInputCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsInputCell.swift; sourceTree = "<group>"; };
7A42DECC2A09064C00B209BE /* SelectableSettingsCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectableSettingsCell.swift; sourceTree = "<group>"; };
7A7AD28C29DC677800480EF1 /* FirstTimeLaunch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirstTimeLaunch.swift; sourceTree = "<group>"; };
7A7AD28E29DEDB1C00480EF1 /* SettingsHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsHeaderView.swift; sourceTree = "<group>"; };
7A818F1E29F0305800C7F0F4 /* RootConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootConfiguration.swift; sourceTree = "<group>"; };
- 7AD2DA1429DC4EB900250737 /* UITextField+Appearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITextField+Appearance.swift"; sourceTree = "<group>"; };
+ 7ABE318C2A1CDD4500DF4963 /* UIFont+Weight.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIFont+Weight.swift"; sourceTree = "<group>"; };
+ 7AE47E512A17972A000418DA /* CustomAlertViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomAlertViewController.swift; sourceTree = "<group>"; };
7AF0419D29E957EB00D492DD /* AccountCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountCoordinator.swift; sourceTree = "<group>"; };
A917351E29FAA9C400D5DCFD /* RESTTransportStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RESTTransportStrategy.swift; sourceTree = "<group>"; };
A917352029FAAA5200D5DCFD /* TransportStrategyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransportStrategyTests.swift; sourceTree = "<group>"; };
@@ -1615,7 +1617,8 @@
5864211E29F04CED00822139 /* UIBarButtonItem+Blocks.swift */,
5891BF5025E66B1E006D6FB0 /* UIBarButtonItem+KeyboardNavigation.swift */,
587CBFE222807F530028DED3 /* UIColor+Helpers.swift */,
- 7AD2DA1429DC4EB900250737 /* UITextField+Appearance.swift */,
+ 7ABE318C2A1CDD4500DF4963 /* UIFont+Weight.swift */,
+ 7A21DACE2A30AA3700A787A9 /* UITextField+Appearance.swift */,
5878F4FF29CDA742003D4BE2 /* UIView+AutoLayoutBuilder.swift */,
);
path = Extensions;
@@ -1651,16 +1654,16 @@
583FE02829C1B079006E85F9 /* Classes */ = {
isa = PBXGroup;
children = (
- 58CC40EE24A601900019D96E /* ObserverList.swift */,
+ 587988C628A2A01F00E3DF54 /* AccountDataThrottling.swift */,
58FEEB57260B662E00A621A8 /* AutomaticKeyboardResponder.swift */,
+ 5859A55229CD9B1300F66591 /* ChangeLog.swift */,
5871FB95254ADE4E0051A0A4 /* ConsolidatedApplicationLog.swift */,
+ 7AE47E512A17972A000418DA /* CustomAlertViewController.swift */,
5896AE83246D5889005B36CB /* CustomDateComponentsFormatting.swift */,
- 5872D6E7286304DE00DB5F4E /* TermsOfService.swift */,
- 5859A55229CD9B1300F66591 /* ChangeLog.swift */,
- 587988C628A2A01F00E3DF54 /* AccountDataThrottling.swift */,
58138E60294871C600684F0C /* DeviceDataThrottling.swift */,
- 589A454B28DDF5E100565204 /* Swizzle.swift */,
7A7AD28C29DC677800480EF1 /* FirstTimeLaunch.swift */,
+ 58CC40EE24A601900019D96E /* ObserverList.swift */,
+ 5872D6E7286304DE00DB5F4E /* TermsOfService.swift */,
);
path = Classes;
sourceTree = "<group>";
@@ -2869,13 +2872,11 @@
5867771429097BCD006F721F /* PaymentState.swift in Sources */,
F0C2AEFD2A0BB5CC00986207 /* NotificationProviderIdentifier.swift in Sources */,
587D96742886D87C00CD8F1C /* DeviceManagementContentView.swift in Sources */,
- 589A454C28DDF5E100565204 /* Swizzle.swift in Sources */,
5864211F29F04CED00822139 /* UIBarButtonItem+Blocks.swift in Sources */,
58C3F4FB296C3AD500D72515 /* SettingsCoordinator.swift in Sources */,
5846227126E229F20035F7C2 /* StoreSubscription.swift in Sources */,
58421030282D8A3C00F24E46 /* UpdateAccountDataOperation.swift in Sources */,
58FF2C03281BDE02009EF542 /* SettingsManager.swift in Sources */,
- 7AD2DA1529DC4EB900250737 /* UITextField+Appearance.swift in Sources */,
5803B4B02940A47300C23744 /* TunnelConfiguration.swift in Sources */,
587EB672271451E300123C75 /* PreferencesViewModel.swift in Sources */,
586A950C290125EE007BAF2B /* AlertPresenter.swift in Sources */,
@@ -2907,6 +2908,7 @@
E158B360285381C60002F069 /* String+AccountFormatting.swift in Sources */,
582BB1B1229569620055B6EF /* UINavigationBar+Appearance.swift in Sources */,
58ACF6492655365700ACE4B7 /* PreferencesViewController.swift in Sources */,
+ 7ABE318D2A1CDD4500DF4963 /* UIFont+Weight.swift in Sources */,
58C774BE29A7A249003A1A56 /* CustomNavigationController.swift in Sources */,
E1FD0DF528AA7CE400299DB4 /* StatusActivityView.swift in Sources */,
0697D6E728F01513007A9E99 /* TransportMonitor.swift in Sources */,
@@ -2978,6 +2980,7 @@
5888AD83227B11080051EB06 /* SelectLocationCell.swift in Sources */,
5891BF1C25E3E3EB006D6FB0 /* Bundle+ProductVersion.swift in Sources */,
5878A26F2907E7E00096FC88 /* ProblemReportInteractor.swift in Sources */,
+ 7AE47E522A17972A000418DA /* CustomAlertViewController.swift in Sources */,
58E11188292FA11F009FCA84 /* SettingsMigrationUIHandler.swift in Sources */,
58CAFA002983FF0200BE19F7 /* LoginInteractor.swift in Sources */,
5807E2C02432038B00F5FF30 /* String+Split.swift in Sources */,
@@ -3040,6 +3043,7 @@
58FD5BF024238EB300112C88 /* SKProduct+Formatting.swift in Sources */,
58B43C1925F77DB60002C8C3 /* TunnelControlView.swift in Sources */,
5811DE50239014550011EB53 /* NEVPNStatus+Debug.swift in Sources */,
+ 7A21DACF2A30AA3700A787A9 /* UITextField+Appearance.swift in Sources */,
58C3A4B222456F1B00340BDB /* AccountInputGroupView.swift in Sources */,
58ACF64B26553C3F00ACE4B7 /* SettingsSwitchCell.swift in Sources */,
583FE00E29C0D586006E85F9 /* OutOfTimeCoordinator.swift in Sources */,
diff --git a/ios/MullvadVPN/Classes/CustomAlertViewController.swift b/ios/MullvadVPN/Classes/CustomAlertViewController.swift
new file mode 100644
index 0000000000..42ceaf3c4b
--- /dev/null
+++ b/ios/MullvadVPN/Classes/CustomAlertViewController.swift
@@ -0,0 +1,190 @@
+//
+// CustomAlertController.swift
+// MullvadVPN
+//
+// Created by Jon Petersson on 2023-05-19.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import UIKit
+
+class CustomAlertViewController: UIViewController {
+ typealias Handler = () -> Void
+
+ enum Icon {
+ case alert
+ case info
+ case spinner
+
+ var image: UIImage? {
+ switch self {
+ case .alert:
+ return UIImage(named: "IconAlert")?.withTintColor(.dangerColor)
+ case .info:
+ return UIImage(named: "IconInfo")?.withTintColor(.white)
+ default:
+ return nil
+ }
+ }
+ }
+
+ enum ActionStyle {
+ case `default`
+ case destructive
+
+ fileprivate var buttonStyle: AppButton.Style {
+ switch self {
+ case .default:
+ return .default
+ case .destructive:
+ return .danger
+ }
+ }
+ }
+
+ var didDismiss: (() -> Void)?
+
+ private let containerView: UIStackView = {
+ let view = UIStackView()
+
+ view.axis = .vertical
+ view.backgroundColor = .secondaryColor
+ view.layer.cornerRadius = 11
+ view.spacing = UIMetrics.CustomAlert.containerSpacing
+ view.isLayoutMarginsRelativeArrangement = true
+ view.directionalLayoutMargins = UIMetrics.CustomAlert.containerMargins
+
+ return view
+ }()
+
+ private var handlers = [UIButton: Handler]()
+
+ init(title: String? = nil, message: String? = nil, icon: Icon? = nil) {
+ super.init(nibName: nil, bundle: nil)
+
+ modalPresentationStyle = .overFullScreen
+ modalTransitionStyle = .crossDissolve
+
+ icon.flatMap { addIcon($0) }
+ title.flatMap { addTitle($0) }
+ message.flatMap { addMessage($0) }
+
+ containerView.arrangedSubviews.last.flatMap {
+ containerView.setCustomSpacing(UIMetrics.CustomAlert.containerMargins.top, after: $0)
+ }
+
+ // Icon only alerts should have equal top and bottom margin.
+ if title == nil, message == nil {
+ containerView.directionalLayoutMargins.bottom = UIMetrics.CustomAlert.containerMargins.top
+ }
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+
+ view.backgroundColor = .black.withAlphaComponent(0.5)
+
+ view.addConstrainedSubviews([containerView]) {
+ containerView.pinEdges(.init([.leading(0), .trailing(0)]), to: view.layoutMarginsGuide)
+ containerView.centerYAnchor.constraint(equalTo: view.centerYAnchor)
+ }
+ }
+
+ override func viewDidDisappear(_ animated: Bool) {
+ super.viewDidDisappear(animated)
+ handlers.removeAll()
+ }
+
+ func addAction(title: String, style: ActionStyle, handler: (() -> Void)? = nil) {
+ // The presence of a button should reset any custom button margin to default.
+ containerView.directionalLayoutMargins.bottom = UIMetrics.CustomAlert.containerMargins.bottom
+
+ let button = AppButton(style: style.buttonStyle)
+
+ button.addTarget(self, action: #selector(didTapButton), for: .touchUpInside)
+ button.setTitle(title, for: .normal)
+
+ containerView.addArrangedSubview(button)
+ handler.flatMap { handlers[button] = $0 }
+ }
+
+ private func addTitle(_ title: String) {
+ let label = UILabel()
+
+ label.text = title
+ label.font = .preferredFont(forTextStyle: .title3, weight: .semibold)
+ label.textColor = .white
+ label.adjustsFontForContentSizeCategory = true
+ label.numberOfLines = 0
+
+ containerView.addArrangedSubview(label)
+ }
+
+ private func addMessage(_ message: String) {
+ let label = UILabel()
+
+ let style = NSMutableParagraphStyle()
+ style.paragraphSpacing = 16
+
+ label.attributedText = NSAttributedString(
+ markdownString: message,
+ options: .init(font: .preferredFont(forTextStyle: .body), paragraphStyle: style)
+ )
+ label.font = .preferredFont(forTextStyle: .body)
+ label.textColor = .white.withAlphaComponent(0.8)
+ label.adjustsFontForContentSizeCategory = true
+ label.numberOfLines = 0
+
+ containerView.addArrangedSubview(label)
+ }
+
+ private func addIcon(_ icon: Icon) {
+ let iconView = icon == .spinner ? getSpinnerView() : getImageView(for: icon)
+ containerView.addArrangedSubview(iconView)
+ }
+
+ private func getImageView(for icon: Icon) -> UIView {
+ let imageView = UIImageView()
+ let imageContainerView = UIView()
+
+ imageContainerView.addConstrainedSubviews([imageView]) {
+ imageView.pinEdges(.init([.top(0), .bottom(0)]), to: imageContainerView)
+ imageView.centerXAnchor.constraint(equalTo: imageContainerView.centerXAnchor, constant: 0)
+ imageView.heightAnchor.constraint(equalToConstant: 44)
+ imageView.widthAnchor.constraint(equalTo: imageView.heightAnchor, multiplier: 1)
+ }
+
+ imageView.image = icon.image?.withRenderingMode(.alwaysOriginal)
+ imageView.contentMode = .scaleAspectFit
+
+ return imageContainerView
+ }
+
+ private func getSpinnerView() -> UIView {
+ let spinnerView = SpinnerActivityIndicatorView(style: .large)
+ let spinnerContainerView = UIView()
+
+ spinnerContainerView.addConstrainedSubviews([spinnerView]) {
+ spinnerView.pinEdges(.init([.top(0), .bottom(0)]), to: spinnerContainerView)
+ spinnerView.centerXAnchor.constraint(equalTo: spinnerContainerView.centerXAnchor, constant: 0)
+ }
+
+ spinnerView.startAnimating()
+
+ return spinnerContainerView
+ }
+
+ @objc private func didTapButton(_ button: AppButton) {
+ dismiss(animated: true) { [self] in
+ if let handler = handlers[button] {
+ handler()
+ }
+
+ didDismiss?()
+ }
+ }
+}
diff --git a/ios/MullvadVPN/Classes/Swizzle.swift b/ios/MullvadVPN/Classes/Swizzle.swift
deleted file mode 100644
index 6073384cf4..0000000000
--- a/ios/MullvadVPN/Classes/Swizzle.swift
+++ /dev/null
@@ -1,30 +0,0 @@
-//
-// Swizzle.swift
-// MullvadVPN
-//
-// Created by pronebird on 28/10/2020.
-// Copyright © 2020 Mullvad VPN AB. All rights reserved.
-//
-
-import Foundation
-
-@inlinable func swizzleMethod(aClass: AnyClass, originalSelector: Selector, newSelector: Selector) {
- guard let originalMethod = class_getInstanceMethod(aClass, originalSelector),
- let newMethod = class_getInstanceMethod(aClass, newSelector) else { return }
-
- if class_addMethod(
- aClass,
- originalSelector,
- method_getImplementation(newMethod),
- method_getTypeEncoding(newMethod)
- ) {
- class_replaceMethod(
- aClass,
- newSelector,
- method_getImplementation(originalMethod),
- method_getTypeEncoding(originalMethod)
- )
- } else {
- method_exchangeImplementations(originalMethod, newMethod)
- }
-}
diff --git a/ios/MullvadVPN/Coordinators/App/TunnelCoordinator.swift b/ios/MullvadVPN/Coordinators/App/TunnelCoordinator.swift
index 98faee4f85..771b6ea397 100644
--- a/ios/MullvadVPN/Coordinators/App/TunnelCoordinator.swift
+++ b/ios/MullvadVPN/Coordinators/App/TunnelCoordinator.swift
@@ -58,7 +58,7 @@ class TunnelCoordinator: Coordinator {
}
private func showCancelTunnelAlert() {
- let alertController = UIAlertController(
+ let alertController = CustomAlertViewController(
title: nil,
message: NSLocalizedString(
"CANCEL_TUNNEL_ALERT_MESSAGE",
@@ -66,33 +66,30 @@ class TunnelCoordinator: Coordinator {
value: "If you disconnect now, you won’t be able to secure your connection until the device is online.",
comment: ""
),
- preferredStyle: .alert
+ icon: .alert
)
alertController.addAction(
- UIAlertAction(
- title: NSLocalizedString(
- "CANCEL_TUNNEL_ALERT_DISCONNECT_ACTION",
- tableName: "Main",
- value: "Disconnect",
- comment: ""
- ),
- style: .destructive,
- handler: { [weak self] _ in
- self?.tunnelManager.stopTunnel()
- }
- )
+ title: NSLocalizedString(
+ "CANCEL_TUNNEL_ALERT_DISCONNECT_ACTION",
+ tableName: "Main",
+ value: "Disconnect",
+ comment: ""
+ ),
+ style: .destructive,
+ handler: { [weak self] in
+ self?.tunnelManager.stopTunnel()
+ }
)
alertController.addAction(
- UIAlertAction(
- title: NSLocalizedString(
- "CANCEL_TUNNEL_ALERT_CANCEL_ACTION",
- tableName: "Main",
- value: "Cancel",
- comment: ""
- ), style: .cancel
- )
+ title: NSLocalizedString(
+ "CANCEL_TUNNEL_ALERT_CANCEL_ACTION",
+ tableName: "Main",
+ value: "Cancel",
+ comment: ""
+ ),
+ style: .default
)
alertPresenter.enqueue(alertController, presentingController: rootViewController)
diff --git a/ios/MullvadVPN/Extensions/NSAttributedString+Markdown.swift b/ios/MullvadVPN/Extensions/NSAttributedString+Markdown.swift
index d27a472e14..d273e2ad00 100644
--- a/ios/MullvadVPN/Extensions/NSAttributedString+Markdown.swift
+++ b/ios/MullvadVPN/Extensions/NSAttributedString+Markdown.swift
@@ -9,31 +9,62 @@
import UIKit
extension NSAttributedString {
+ enum MarkdownElement {
+ case bold
+ }
+
+ struct MarkdownStylingOptions {
+ var font: UIFont
+ var paragraphStyle: NSParagraphStyle = .default
+
+ var boldFont: UIFont {
+ let fontDescriptor = font.fontDescriptor.withSymbolicTraits(.traitBold) ?? font.fontDescriptor
+ return UIFont(descriptor: fontDescriptor, size: font.pointSize)
+ }
+ }
+
convenience init(
markdownString: String,
- font: UIFont,
- applyEffect: ((String) -> [NSAttributedString.Key: Any])? = nil
+ options: MarkdownStylingOptions,
+ applyEffect: ((MarkdownElement, String) -> [NSAttributedString.Key: Any])? = nil
) {
let attributedString = NSMutableAttributedString()
- let components = markdownString.components(separatedBy: "**")
+ let paragraphs = markdownString.replacingOccurrences(of: "\r\n", with: "\n").components(separatedBy: "\n\n")
- let fontDescriptor = font.fontDescriptor.withSymbolicTraits(.traitBold) ?? font
- .fontDescriptor
- let boldFont = UIFont(descriptor: fontDescriptor, size: font.pointSize)
+ for (paragraphIndex, paragraph) in paragraphs.enumerated() {
+ let attributedParagraph = NSMutableAttributedString()
- for (index, string) in components.enumerated() {
- var attributes = [NSAttributedString.Key: Any]()
+ // Replace \n with \u2028 to prevent attributed string from picking up single line breaks as paragraphs.
+ let components = paragraph.replacingOccurrences(of: "\n", with: "\u{2028}")
+ .components(separatedBy: "**")
- if index % 2 == 0 {
- attributes[.font] = font
- } else {
- attributes[.font] = boldFont
- attributes.merge(applyEffect?(string) ?? [:], uniquingKeysWith: { $1 })
+ if paragraphIndex > 0 {
+ // Add single line break to add spacing between paragraphs.
+ attributedParagraph.append(NSAttributedString(string: "\n"))
}
- attributedString.append(NSAttributedString(string: string, attributes: attributes))
+ for (stringIndex, string) in components.enumerated() {
+ var attributes: [NSAttributedString.Key: Any] = [:]
+
+ if stringIndex % 2 == 0 {
+ attributes[.font] = options.font
+ } else {
+ attributes[.font] = options.boldFont
+ attributes.merge(applyEffect?(.bold, string) ?? [:], uniquingKeysWith: { $1 })
+ }
+
+ attributedParagraph.append(NSAttributedString(string: string, attributes: attributes))
+ }
+
+ attributedString.append(attributedParagraph)
}
+ attributedString.addAttribute(
+ .paragraphStyle,
+ value: options.paragraphStyle,
+ range: NSRange(location: 0, length: attributedString.length)
+ )
+
self.init(attributedString: attributedString)
}
}
diff --git a/ios/MullvadVPN/Extensions/UIFont+Weight.swift b/ios/MullvadVPN/Extensions/UIFont+Weight.swift
new file mode 100644
index 0000000000..547327f844
--- /dev/null
+++ b/ios/MullvadVPN/Extensions/UIFont+Weight.swift
@@ -0,0 +1,20 @@
+//
+// UIFont+Weight.swift
+// MullvadVPN
+//
+// Created by Jon Petersson on 2023-05-23.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import UIKit
+
+extension UIFont {
+ static func preferredFont(forTextStyle style: TextStyle, weight: Weight) -> UIFont {
+ let descriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: style)
+ .addingAttributes([
+ .traits: [UIFontDescriptor.TraitKey.weight: weight],
+ ])
+
+ return UIFont(descriptor: descriptor, size: 0)
+ }
+}
diff --git a/ios/MullvadVPN/Notifications/Notification Providers/RegisteredDeviceInAppNotificationProvider.swift b/ios/MullvadVPN/Notifications/Notification Providers/RegisteredDeviceInAppNotificationProvider.swift
index a11e9433ca..345fe44a2c 100644
--- a/ios/MullvadVPN/Notifications/Notification Providers/RegisteredDeviceInAppNotificationProvider.swift
+++ b/ios/MullvadVPN/Notifications/Notification Providers/RegisteredDeviceInAppNotificationProvider.swift
@@ -32,8 +32,14 @@ final class RegisteredDeviceInAppNotificationProvider: NotificationProvider,
)
let deviceName = storedDeviceData?.capitalizedName ?? ""
let string = String(format: formattedString, deviceName)
- return NSAttributedString(markdownString: string, font: .systemFont(ofSize: 14.0)) { deviceName in
- return [.foregroundColor: UIColor.InAppNotificationBanner.titleColor]
+
+ let stylingOptions = NSAttributedString.MarkdownStylingOptions(font: .systemFont(ofSize: 14.0))
+
+ return NSMutableAttributedString(markdownString: string, options: stylingOptions) { markdownType, string in
+ switch markdownType {
+ case .bold:
+ return [.foregroundColor: UIColor.InAppNotificationBanner.titleColor]
+ }
}
}
diff --git a/ios/MullvadVPN/Operations/AlertPresenter.swift b/ios/MullvadVPN/Operations/AlertPresenter.swift
index bb86989d78..7c31017a4e 100644
--- a/ios/MullvadVPN/Operations/AlertPresenter.swift
+++ b/ios/MullvadVPN/Operations/AlertPresenter.swift
@@ -9,30 +9,11 @@
import Operations
import UIKit
-public final class AlertPresenter {
- static let alertControllerDidDismissNotification = Notification
- .Name("UIAlertControllerDidDismiss")
-
+final class AlertPresenter {
private let operationQueue = AsyncOperationQueue.makeSerial()
- private static let initClass: Void = {
- /// Swizzle `viewDidDisappear` on `UIAlertController` in order to be able to
- /// detect when the controller disappears.
- /// The event is broadcasted via
- /// `AlertPresenter.alertControllerDidDismissNotification` notification.
- swizzleMethod(
- aClass: UIAlertController.self,
- originalSelector: #selector(UIAlertController.viewDidDisappear(_:)),
- newSelector: #selector(UIAlertController.alertPresenter_viewDidDisappear(_:))
- )
- }()
-
- public init() {
- _ = Self.initClass
- }
-
- public func enqueue(
- _ alertController: UIAlertController,
+ func enqueue(
+ _ alertController: CustomAlertViewController,
presentingController: UIViewController,
presentCompletion: (() -> Void)? = nil
) {
@@ -45,21 +26,7 @@ public final class AlertPresenter {
operationQueue.addOperation(operation)
}
- public func cancelAll() {
+ func cancelAll() {
operationQueue.cancelAllOperations()
}
}
-
-private extension UIAlertController {
- @objc dynamic func alertPresenter_viewDidDisappear(_ animated: Bool) {
- // Call super implementation
- alertPresenter_viewDidDisappear(animated)
-
- if presentingViewController == nil {
- NotificationCenter.default.post(
- name: AlertPresenter.alertControllerDidDismissNotification,
- object: self
- )
- }
- }
-}
diff --git a/ios/MullvadVPN/Operations/PresentAlertOperation.swift b/ios/MullvadVPN/Operations/PresentAlertOperation.swift
index 09ddb1dfca..dda17b15af 100644
--- a/ios/MullvadVPN/Operations/PresentAlertOperation.swift
+++ b/ios/MullvadVPN/Operations/PresentAlertOperation.swift
@@ -9,13 +9,13 @@
import Operations
import UIKit
-public final class PresentAlertOperation: AsyncOperation {
- private let alertController: UIAlertController
+final class PresentAlertOperation: AsyncOperation {
+ private let alertController: CustomAlertViewController
private let presentingController: UIViewController
private let presentCompletion: (() -> Void)?
- public init(
- alertController: UIAlertController,
+ init(
+ alertController: CustomAlertViewController,
presentingController: UIViewController,
presentCompletion: (() -> Void)? = nil
) {
@@ -26,7 +26,7 @@ public final class PresentAlertOperation: AsyncOperation {
super.init(dispatchQueue: .main)
}
- override public func operationDidCancel() {
+ override func operationDidCancel() {
// Guard against trying to dismiss the alert when operation hasn't started yet.
guard isExecuting else { return }
@@ -36,13 +36,10 @@ public final class PresentAlertOperation: AsyncOperation {
}
}
- override public func main() {
- NotificationCenter.default.addObserver(
- self,
- selector: #selector(alertControllerDidDismiss(_:)),
- name: AlertPresenter.alertControllerDidDismissNotification,
- object: alertController
- )
+ override func main() {
+ alertController.didDismiss = {
+ self.finish()
+ }
presentingController.present(alertController, animated: true) {
self.presentCompletion?()
@@ -55,18 +52,8 @@ public final class PresentAlertOperation: AsyncOperation {
}
private func dismissAndFinish() {
- NotificationCenter.default.removeObserver(
- self,
- name: AlertPresenter.alertControllerDidDismissNotification,
- object: alertController
- )
-
alertController.dismiss(animated: false) {
self.finish()
}
}
-
- @objc private func alertControllerDidDismiss(_ note: Notification) {
- finish()
- }
}
diff --git a/ios/MullvadVPN/Operations/ProductsRequestOperation.swift b/ios/MullvadVPN/Operations/ProductsRequestOperation.swift
index 380dc5267e..5138776fdb 100644
--- a/ios/MullvadVPN/Operations/ProductsRequestOperation.swift
+++ b/ios/MullvadVPN/Operations/ProductsRequestOperation.swift
@@ -9,7 +9,7 @@
import Operations
import StoreKit
-public final class ProductsRequestOperation: ResultOperation<SKProductsResponse>,
+final class ProductsRequestOperation: ResultOperation<SKProductsResponse>,
SKProductsRequestDelegate
{
private let productIdentifiers: Set<String>
@@ -31,22 +31,22 @@ public final class ProductsRequestOperation: ResultOperation<SKProductsResponse>
)
}
- override public func main() {
+ override func main() {
startRequest()
}
- override public func operationDidCancel() {
+ override func operationDidCancel() {
request?.cancel()
retryTimer?.cancel()
}
// - MARK: SKProductsRequestDelegate
- public func requestDidFinish(_ request: SKRequest) {
+ func requestDidFinish(_ request: SKRequest) {
// no-op
}
- public func request(_ request: SKRequest, didFailWithError error: Error) {
+ func request(_ request: SKRequest, didFailWithError error: Error) {
dispatchQueue.async {
if self.retryCount < self.maxRetryCount, !self.isCancelled {
self.retryCount += 1
@@ -57,7 +57,7 @@ public final class ProductsRequestOperation: ResultOperation<SKProductsResponse>
}
}
- public func productsRequest(
+ func productsRequest(
_ request: SKProductsRequest,
didReceive response: SKProductsResponse
) {
diff --git a/ios/MullvadVPN/SceneDelegate.swift b/ios/MullvadVPN/SceneDelegate.swift
index f167d38a26..290ead03a6 100644
--- a/ios/MullvadVPN/SceneDelegate.swift
+++ b/ios/MullvadVPN/SceneDelegate.swift
@@ -184,24 +184,21 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate, SettingsMigrationUIHand
// MARK: - SettingsMigrationUIHandler
func showMigrationError(_ error: Error, completionHandler: @escaping () -> Void) {
- let alertController = UIAlertController(
+ let alertController = CustomAlertViewController(
title: NSLocalizedString(
"ALERT_TITLE",
tableName: "SettingsMigrationUI",
value: "Settings migration error",
comment: ""
),
- message: Self.migrationErrorReason(error),
- preferredStyle: .alert
+ message: Self.migrationErrorReason(error)
)
alertController.addAction(
- UIAlertAction(
- title: NSLocalizedString("OK", tableName: "SettingsMigrationUI", comment: ""),
- style: .default,
- handler: { _ in
- completionHandler()
- }
- )
+ title: NSLocalizedString("Got it!", tableName: "SettingsMigrationUI", comment: ""),
+ style: .default,
+ handler: {
+ completionHandler()
+ }
)
if let rootViewController = window?.rootViewController {
diff --git a/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconAlert.imageset/Contents.json b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconAlert.imageset/Contents.json
new file mode 100644
index 0000000000..4800727ae2
--- /dev/null
+++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconAlert.imageset/Contents.json
@@ -0,0 +1,15 @@
+{
+ "images" : [
+ {
+ "filename" : "IconAlert.pdf",
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ },
+ "properties" : {
+ "preserves-vector-representation" : true
+ }
+}
diff --git a/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconAlert.imageset/IconAlert.pdf b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconAlert.imageset/IconAlert.pdf
new file mode 100644
index 0000000000..2b350b1b89
--- /dev/null
+++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconAlert.imageset/IconAlert.pdf
Binary files differ
diff --git a/ios/MullvadVPN/UI appearance/UIMetrics.swift b/ios/MullvadVPN/UI appearance/UIMetrics.swift
index 02b30f6974..7e8ac30da7 100644
--- a/ios/MullvadVPN/UI appearance/UIMetrics.swift
+++ b/ios/MullvadVPN/UI appearance/UIMetrics.swift
@@ -8,7 +8,20 @@
import UIKit
-enum UIMetrics {}
+enum UIMetrics {
+ enum CustomAlert {
+ /// Layout margins for container (main view) in `CustomAlertViewController`
+ static let containerMargins = NSDirectionalEdgeInsets(
+ top: 28,
+ leading: 16,
+ bottom: 16,
+ trailing: 16
+ )
+
+ /// Spacing between views in container (main view) in `CustomAlertViewController`
+ static let containerSpacing: CGFloat = 16
+ }
+}
extension UIMetrics {
/// Common layout margins for content presentation
diff --git a/ios/MullvadVPN/View controllers/Account/AccountViewController.swift b/ios/MullvadVPN/View controllers/Account/AccountViewController.swift
index 82a87bf8df..2e1e73ea82 100644
--- a/ios/MullvadVPN/View controllers/Account/AccountViewController.swift
+++ b/ios/MullvadVPN/View controllers/Account/AccountViewController.swift
@@ -100,7 +100,7 @@ class AccountViewController: UIViewController {
action: #selector(doPurchase),
for: .touchUpInside
)
- contentView.logoutButton.addTarget(self, action: #selector(doLogout), for: .touchUpInside)
+ contentView.logoutButton.addTarget(self, action: #selector(logOut), for: .touchUpInside)
interactor.didReceiveDeviceState = { [weak self] deviceState in
self?.updateView(from: deviceState)
@@ -122,6 +122,10 @@ class AccountViewController: UIViewController {
// MARK: - Private
+ @objc private func logOut() {
+ delegate?.accountViewControllerDidLogout(self)
+ }
+
@objc private func handleDismiss() {
delegate?.accountViewControllerDidFinish(self)
}
@@ -207,50 +211,50 @@ class AccountViewController: UIViewController {
}
private func showPaymentErrorAlert(error: StorePaymentManagerError) {
- let alertController = UIAlertController(
+ let alertController = CustomAlertViewController(
title: NSLocalizedString(
"CANNOT_COMPLETE_PURCHASE_ALERT_TITLE",
tableName: "Account",
value: "Cannot complete the purchase",
comment: ""
),
- message: error.displayErrorDescription,
- preferredStyle: .alert
+ message: error.displayErrorDescription
)
alertController.addAction(
- UIAlertAction(
- title: NSLocalizedString(
- "CANNOT_COMPLETE_PURCHASE_ALERT_OK_ACTION",
- tableName: "Account",
- value: "OK",
- comment: ""
- ), style: .cancel
- )
+ title: NSLocalizedString(
+ "CANNOT_COMPLETE_PURCHASE_ALERT_OK_ACTION",
+ tableName: "Account",
+ value: "Got it!",
+ comment: ""
+ ),
+ style: .default
)
alertPresenter.enqueue(alertController, presentingController: self)
}
private func showRestorePurchasesErrorAlert(error: StorePaymentManagerError) {
- let alertController = UIAlertController(
+ let alertController = CustomAlertViewController(
title: NSLocalizedString(
"RESTORE_PURCHASES_FAILURE_ALERT_TITLE",
tableName: "Account",
value: "Cannot restore purchases",
comment: ""
),
- message: error.displayErrorDescription,
- preferredStyle: .alert
+ message: error.displayErrorDescription
)
+
alertController.addAction(
- UIAlertAction(title: NSLocalizedString(
+ title: NSLocalizedString(
"RESTORE_PURCHASES_FAILURE_ALERT_OK_ACTION",
tableName: "Account",
- value: "OK",
+ value: "Got it!",
comment: ""
- ), style: .cancel)
+ ),
+ style: .default
)
+
alertPresenter.enqueue(alertController, presentingController: self)
}
@@ -258,21 +262,19 @@ class AccountViewController: UIViewController {
with response: REST.CreateApplePaymentResponse,
context: REST.CreateApplePaymentResponse.Context
) {
- let alertController = UIAlertController(
+ let alertController = CustomAlertViewController(
title: response.alertTitle(context: context),
- message: response.alertMessage(context: context),
- preferredStyle: .alert
+ message: response.alertMessage(context: context)
)
+
alertController.addAction(
- UIAlertAction(
- title: NSLocalizedString(
- "TIME_ADDED_ALERT_OK_ACTION",
- tableName: "Account",
- value: "OK",
- comment: ""
- ),
- style: .cancel
- )
+ title: NSLocalizedString(
+ "TIME_ADDED_ALERT_OK_ACTION",
+ tableName: "Account",
+ value: "Got it!",
+ comment: ""
+ ),
+ style: .default
)
alertPresenter.enqueue(alertController, presentingController: self)
@@ -281,17 +283,8 @@ class AccountViewController: UIViewController {
// MARK: - Actions
@objc private func doLogout() {
- let message = NSLocalizedString(
- "LOGGING_OUT_ALERT_TITLE",
- tableName: "Account",
- value: "Logging out. Please wait...",
- comment: ""
- )
-
- let alertController = UIAlertController(
- title: nil,
- message: message,
- preferredStyle: .alert
+ let alertController = CustomAlertViewController(
+ icon: .spinner
)
alertPresenter.enqueue(alertController, presentingController: self) {
diff --git a/ios/MullvadVPN/View controllers/DeviceList/DeviceManagementViewController.swift b/ios/MullvadVPN/View controllers/DeviceList/DeviceManagementViewController.swift
index 3a29d0fe34..f76e202874 100644
--- a/ios/MullvadVPN/View controllers/DeviceList/DeviceManagementViewController.swift
+++ b/ios/MullvadVPN/View controllers/DeviceList/DeviceManagementViewController.swift
@@ -122,7 +122,7 @@ class DeviceManagementViewController: UIViewController, RootContainment {
_ device: DeviceViewModel,
completionHandler: @escaping () -> Void
) {
- showDeleteConfirmation(deviceName: device.name) { [weak self] shouldDelete in
+ showLogoutConfirmation(deviceName: device.name) { [weak self] shouldDelete in
guard let self else { return }
guard shouldDelete else {
@@ -157,32 +157,29 @@ class DeviceManagementViewController: UIViewController, RootContainment {
}
private func showErrorAlert(title: String, error: Error) {
- let alertController = UIAlertController(
+ let alertController = CustomAlertViewController(
title: title,
- message: getErrorDescription(error),
- preferredStyle: .alert
+ message: getErrorDescription(error)
)
alertController.addAction(
- UIAlertAction(
- title: NSLocalizedString(
- "ERROR_ALERT_OK_ACTION",
- tableName: "DeviceManagement",
- value: "OK",
- comment: ""
- ),
- style: .cancel
- )
+ title: NSLocalizedString(
+ "ERROR_ALERT_OK_ACTION",
+ tableName: "DeviceManagement",
+ value: "Got it!",
+ comment: ""
+ ),
+ style: .default
)
alertPresenter.enqueue(alertController, presentingController: self)
}
- private func showDeleteConfirmation(
+ private func showLogoutConfirmation(
deviceName: String,
completion: @escaping (_ shouldDelete: Bool) -> Void
) {
- let localizedTitle = String(
+ let message = String(
format: NSLocalizedString(
"DELETE_ALERT_TITLE",
tableName: "DeviceManagement",
@@ -191,42 +188,36 @@ class DeviceManagementViewController: UIViewController, RootContainment {
), deviceName
)
- let alertController = UIAlertController(
- title: localizedTitle,
- message: nil,
- preferredStyle: .alert
+ let alertController = CustomAlertViewController(
+ message: message,
+ icon: .alert
)
- let actions = [
- UIAlertAction(
- title: NSLocalizedString(
- "DELETE_ALERT_CANCEL_ACTION",
- tableName: "DeviceManagement",
- value: "Back",
- comment: ""
- ),
- style: .cancel,
- handler: { _ in
- completion(false)
- }
- ),
- UIAlertAction(
- title: NSLocalizedString(
- "DELETE_ALERT_CONFIRM_ACTION",
- tableName: "DeviceManagement",
- value: "Yes, log out device",
- comment: ""
- ),
- style: .destructive,
- handler: { _ in
- completion(true)
- }
+ alertController.addAction(
+ title: NSLocalizedString(
+ "DELETE_ALERT_CANCEL_ACTION",
+ tableName: "DeviceManagement",
+ value: "Back",
+ comment: ""
),
- ]
+ style: .default,
+ handler: {
+ completion(false)
+ }
+ )
- for action in actions {
- alertController.addAction(action)
- }
+ alertController.addAction(
+ title: NSLocalizedString(
+ "DELETE_ALERT_CONFIRM_ACTION",
+ tableName: "DeviceManagement",
+ value: "Yes, log out device",
+ comment: ""
+ ),
+ style: .destructive,
+ handler: {
+ completion(true)
+ }
+ )
alertPresenter.enqueue(alertController, presentingController: self)
}
diff --git a/ios/MullvadVPN/View controllers/OutOfTime/OutOfTimeContentView.swift b/ios/MullvadVPN/View controllers/OutOfTime/OutOfTimeContentView.swift
index 8bafb81ddf..b55cfaf386 100644
--- a/ios/MullvadVPN/View controllers/OutOfTime/OutOfTimeContentView.swift
+++ b/ios/MullvadVPN/View controllers/OutOfTime/OutOfTimeContentView.swift
@@ -146,7 +146,7 @@ class OutOfTimeContentView: UIView {
func setBodyLabelText(_ text: String) {
bodyLabel.attributedText = NSAttributedString(
markdownString: text,
- font: UIFont.systemFont(ofSize: 17)
+ options: NSAttributedString.MarkdownStylingOptions(font: .systemFont(ofSize: 17))
)
}
}
diff --git a/ios/MullvadVPN/View controllers/OutOfTime/OutOfTimeViewController.swift b/ios/MullvadVPN/View controllers/OutOfTime/OutOfTimeViewController.swift
index 32627ab738..ce0d4ae9d2 100644
--- a/ios/MullvadVPN/View controllers/OutOfTime/OutOfTimeViewController.swift
+++ b/ios/MullvadVPN/View controllers/OutOfTime/OutOfTimeViewController.swift
@@ -211,28 +211,27 @@ class OutOfTimeViewController: UIViewController, RootContainment {
error: StorePaymentManagerError,
completion: @escaping () -> Void
) {
- let alertController = UIAlertController(
+ let alertController = CustomAlertViewController(
title: NSLocalizedString(
"CANNOT_COMPLETE_PURCHASE_ALERT_TITLE",
tableName: "OutOfTime",
value: "Cannot complete the purchase",
comment: ""
),
- message: error.displayErrorDescription,
- preferredStyle: .alert
+ message: error.displayErrorDescription
)
alertController.addAction(
- UIAlertAction(
- title: NSLocalizedString(
- "CANNOT_COMPLETE_PURCHASE_ALERT_OK_ACTION",
- tableName: "OutOfTime",
- value: "OK",
- comment: ""
- ),
- style: .cancel,
- handler: { _ in completion() }
- )
+ title: NSLocalizedString(
+ "CANNOT_COMPLETE_PURCHASE_ALERT_OK_ACTION",
+ tableName: "OutOfTime",
+ value: "Got it!",
+ comment: ""
+ ),
+ style: .default,
+ handler: {
+ completion()
+ }
)
alertPresenter.enqueue(alertController, presentingController: self)
@@ -242,28 +241,27 @@ class OutOfTimeViewController: UIViewController, RootContainment {
error: StorePaymentManagerError,
completion: @escaping () -> Void
) {
- let alertController = UIAlertController(
+ let alertController = CustomAlertViewController(
title: NSLocalizedString(
"RESTORE_PURCHASES_FAILURE_ALERT_TITLE",
tableName: "OutOfTime",
value: "Cannot restore purchases",
comment: ""
),
- message: error.displayErrorDescription,
- preferredStyle: .alert
+ message: error.displayErrorDescription
)
alertController.addAction(
- UIAlertAction(
- title: NSLocalizedString(
- "RESTORE_PURCHASES_FAILURE_ALERT_OK_ACTION",
- tableName: "OutOfTime",
- value: "OK",
- comment: ""
- ),
- style: .cancel,
- handler: { _ in completion() }
- )
+ title: NSLocalizedString(
+ "RESTORE_PURCHASES_FAILURE_ALERT_OK_ACTION",
+ tableName: "OutOfTime",
+ value: "Got it!",
+ comment: ""
+ ),
+ style: .default,
+ handler: {
+ completion()
+ }
)
alertPresenter.enqueue(alertController, presentingController: self)
@@ -279,23 +277,22 @@ class OutOfTimeViewController: UIViewController, RootContainment {
return
}
- let alertController = UIAlertController(
+ let alertController = CustomAlertViewController(
title: response.alertTitle(context: context),
- message: response.alertMessage(context: context),
- preferredStyle: .alert
+ message: response.alertMessage(context: context)
)
alertController.addAction(
- UIAlertAction(
- title: NSLocalizedString(
- "TIME_ADDED_ALERT_OK_ACTION",
- tableName: "OutOfTime",
- value: "OK",
- comment: ""
- ),
- style: .cancel,
- handler: { _ in completion() }
- )
+ title: NSLocalizedString(
+ "TIME_ADDED_ALERT_OK_ACTION",
+ tableName: "OutOfTime",
+ value: "Got it!",
+ comment: ""
+ ),
+ style: .default,
+ handler: {
+ completion()
+ }
)
alertPresenter.enqueue(alertController, presentingController: self)
diff --git a/ios/MullvadVPN/View controllers/Preferences/PreferencesViewController.swift b/ios/MullvadVPN/View controllers/Preferences/PreferencesViewController.swift
index d3279a3564..d4f4e7691b 100644
--- a/ios/MullvadVPN/View controllers/Preferences/PreferencesViewController.swift
+++ b/ios/MullvadVPN/View controllers/Preferences/PreferencesViewController.swift
@@ -75,19 +75,21 @@ class PreferencesViewController: UITableViewController, PreferencesDataSourceDel
}
private func showContentBlockerInfo(with message: String) {
- let alertController = UIAlertController(
- title: nil,
+ let alertController = CustomAlertViewController(
message: message,
- preferredStyle: .alert
+ icon: .info
)
+
alertController.addAction(
- UIAlertAction(title: NSLocalizedString(
+ title: NSLocalizedString(
"PREFERENCES_CONTENT_BLOCKERS_OK_ACTION",
tableName: "ContentBlockers",
value: "Got it!",
comment: ""
- ), style: .cancel)
+ ),
+ style: .default
)
+
alertPresenter.enqueue(alertController, presentingController: self)
}
@@ -143,11 +145,20 @@ class PreferencesViewController: UITableViewController, PreferencesDataSourceDel
.portRanges ?? []
)
- message = NSLocalizedString(
- "PREFERENCES_WIRE_GUARD_PORTS_GENERAL",
- tableName: "WireGuardPorts",
- value: "The automatic setting will randomly choose from the valid port ranges shown below.\n\nThe custom port can be any value inside the valid ranges:\n\n\(portsString)",
- comment: ""
+ message = String(
+ format: NSLocalizedString(
+ "PREFERENCES_WIRE_GUARD_PORTS_GENERAL",
+ tableName: "WireGuardPorts",
+ value: """
+ The automatic setting will randomly choose from the valid port ranges shown below.
+
+ The custom port can be any value inside the valid ranges:
+
+ %@
+ """,
+ comment: ""
+ ),
+ portsString
)
default:
diff --git a/ios/MullvadVPN/View controllers/Preferences/PreferencesViewModel.swift b/ios/MullvadVPN/View controllers/Preferences/PreferencesViewModel.swift
index 0ec1f08ef7..26fc169161 100644
--- a/ios/MullvadVPN/View controllers/Preferences/PreferencesViewModel.swift
+++ b/ios/MullvadVPN/View controllers/Preferences/PreferencesViewModel.swift
@@ -55,7 +55,7 @@ enum CustomDNSPrecondition {
value: "Tap **Edit** to add at least one DNS server.",
comment: "Foot note displayed if there are no DNS entries, but table view is not in editing mode."
),
- font: preferredFont
+ options: NSAttributedString.MarkdownStylingOptions(font: preferredFont)
)
}
diff --git a/ios/MullvadVPN/View controllers/ProblemReport/ProblemReportViewController.swift b/ios/MullvadVPN/View controllers/ProblemReport/ProblemReportViewController.swift
index b1deac8f5f..530dfa35d6 100644
--- a/ios/MullvadVPN/View controllers/ProblemReport/ProblemReportViewController.swift
+++ b/ios/MullvadVPN/View controllers/ProblemReport/ProblemReportViewController.swift
@@ -508,37 +508,36 @@ final class ProblemReportViewController: UIViewController, UITextFieldDelegate {
comment: ""
)
- let alertController = UIAlertController(
- title: nil,
+ let alertController = CustomAlertViewController(
message: message,
- preferredStyle: .alert
+ icon: .alert
)
- let cancelAction = UIAlertAction(
+ alertController.addAction(
title: NSLocalizedString(
- "EMPTY_EMAIL_ALERT_CANCEL_ACTION",
+ "EMPTY_EMAIL_ALERT_SEND_ANYWAY_ACTION",
tableName: "ProblemReport",
- value: "Cancel",
+ value: "Send anyway",
comment: ""
),
- style: .cancel
- ) { _ in
- completion(false)
- }
- let sendAction = UIAlertAction(
+ style: .destructive,
+ handler: {
+ completion(true)
+ }
+ )
+
+ alertController.addAction(
title: NSLocalizedString(
- "EMPTY_EMAIL_ALERT_SEND_ANYWAY_ACTION",
+ "EMPTY_EMAIL_ALERT_CANCEL_ACTION",
tableName: "ProblemReport",
- value: "Send anyway",
+ value: "Cancel",
comment: ""
),
- style: .destructive
- ) { _ in
- completion(true)
- }
-
- alertController.addAction(cancelAction)
- alertController.addAction(sendAction)
+ style: .default,
+ handler: {
+ completion(false)
+ }
+ )
present(alertController, animated: true)
}
diff --git a/ios/convert-assets.rb b/ios/convert-assets.rb
index 9b2954faea..06401408d3 100755
--- a/ios/convert-assets.rb
+++ b/ios/convert-assets.rb
@@ -20,6 +20,7 @@ XCASSETS_APPICON_SIZE = 1024
# graphical assets to import
GRAPHICAL_ASSETS = [
"icon-account.svg",
+ "icon-alert.svg",
"icon-arrow.svg",
"icon-back.svg",
"icon-chevron-down.svg",