diff options
| author | Jon Petersson <jon.petersson@kvadrat.se> | 2023-05-22 13:25:38 +0200 |
|---|---|---|
| committer | Jon Petersson <jon.petersson@kvadrat.se> | 2023-06-08 16:36:40 +0200 |
| commit | e4677b9fe4b22bfbdb9140507ad6222feddf6b9d (patch) | |
| tree | 533abd51cf76d60786ef4f9130d47d271707d6fd | |
| parent | 04e49a9f00467a35138ba12d4c812397797ff1ce (diff) | |
| download | mullvadvpn-e4677b9fe4b22bfbdb9140507ad6222feddf6b9d.tar.xz mullvadvpn-e4677b9fe4b22bfbdb9140507ad6222feddf6b9d.zip | |
Create custom alert dialogs
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 Binary files differnew file mode 100644 index 0000000000..2b350b1b89 --- /dev/null +++ b/ios/MullvadVPN/Supporting Files/Assets.xcassets/IconAlert.imageset/IconAlert.pdf 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", |
