diff options
| author | Mojgan <Mojgan.jelodar@codic.se> | 2023-04-25 16:31:08 +0200 |
|---|---|---|
| committer | Andrej Mihajlov <and@mullvad.net> | 2023-04-26 09:51:00 +0200 |
| commit | f83576b292b5e293e99ac71b7564d3f5950b210e (patch) | |
| tree | bd8be4e36e9fa9815b4a3d57779108d948ae0d11 | |
| parent | 5ddf2a83859a4a1f313fa9e1455af1339bddf28b (diff) | |
| download | mullvadvpn-f83576b292b5e293e99ac71b7564d3f5950b210e.tar.xz mullvadvpn-f83576b292b5e293e99ac71b7564d3f5950b210e.zip | |
Add in-app banner message for a new device
14 files changed, 181 insertions, 28 deletions
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index ad4cec56cd..726f752d8b 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -368,6 +368,7 @@ E1187ABD289BBB850024E748 /* OutOfTimeContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1187ABB289BBB850024E748 /* OutOfTimeContentView.swift */; }; E158B360285381C60002F069 /* String+AccountFormatting.swift in Sources */ = {isa = PBXBuildFile; fileRef = E158B35F285381C60002F069 /* String+AccountFormatting.swift */; }; E1FD0DF528AA7CE400299DB4 /* StatusActivityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1FD0DF428AA7CE400299DB4 /* StatusActivityView.swift */; }; + F07CFF2029F2720E008C0343 /* RegisteredDeviceInAppNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = F07CFF1F29F2720E008C0343 /* RegisteredDeviceInAppNotification.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -955,6 +956,7 @@ E1187ABB289BBB850024E748 /* OutOfTimeContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OutOfTimeContentView.swift; sourceTree = "<group>"; }; E158B35F285381C60002F069 /* String+AccountFormatting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+AccountFormatting.swift"; sourceTree = "<group>"; }; E1FD0DF428AA7CE400299DB4 /* StatusActivityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusActivityView.swift; sourceTree = "<group>"; }; + F07CFF1F29F2720E008C0343 /* RegisteredDeviceInAppNotification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegisteredDeviceInAppNotification.swift; sourceTree = "<group>"; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -1579,6 +1581,7 @@ children = ( 587B75402668FD7700DEF7E9 /* AccountExpirySystemNotificationProvider.swift */, 58607A4C2947287800BC467D /* AccountExpiryInAppNotificationProvider.swift */, + F07CFF1F29F2720E008C0343 /* RegisteredDeviceInAppNotification.swift */, 58A94AE326CFD945001CB97C /* TunnelStatusNotificationProvider.swift */, ); path = "Notification Providers"; @@ -2695,6 +2698,7 @@ 582BB1AF229566420055B6EF /* SettingsCell.swift in Sources */, 58F3C0A4249CB069003E76BE /* HeaderBarView.swift in Sources */, 5864AF0829C78849005B0CD9 /* CellFactoryProtocol.swift in Sources */, + F07CFF2029F2720E008C0343 /* RegisteredDeviceInAppNotification.swift in Sources */, 587A01FC23F1F0BE00B68763 /* SimulatorTunnelProviderHost.swift in Sources */, 58CAF9F82983D36800BE19F7 /* Coordinator.swift in Sources */, 5819C2172729595500D6EC38 /* SettingsAddDNSEntryCell.swift in Sources */, diff --git a/ios/MullvadVPN/AppDelegate.swift b/ios/MullvadVPN/AppDelegate.swift index d5a5d987d9..29db4311c8 100644 --- a/ios/MullvadVPN/AppDelegate.swift +++ b/ios/MullvadVPN/AppDelegate.swift @@ -347,6 +347,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD private func setupNotificationHandler() { NotificationManager.shared.notificationProviders = [ + RegisteredDeviceInAppNotification(tunnelManager: tunnelManager, completionHandler: { deviceState in + let sceneDelegate = UIApplication.shared.connectedScenes + .first?.delegate as? SceneDelegate + sceneDelegate?.didDismissRegisteredDeviceInAppBanner(deviceState: deviceState) + }), TunnelStatusNotificationProvider(tunnelManager: tunnelManager), AccountExpirySystemNotificationProvider( tunnelManager: tunnelManager, diff --git a/ios/MullvadVPN/Containers/Root/HeaderBarView.swift b/ios/MullvadVPN/Containers/Root/HeaderBarView.swift index dcc9cbac92..b6d07c4fbd 100644 --- a/ios/MullvadVPN/Containers/Root/HeaderBarView.swift +++ b/ios/MullvadVPN/Containers/Root/HeaderBarView.swift @@ -209,7 +209,7 @@ extension HeaderBarView { ) } - deviceInfoHolder.arrangedSubviews.forEach { $0.isHidden = configuration.deviceName == nil } + deviceInfoHolder.arrangedSubviews.forEach { $0.isHidden = !configuration.showsDeviceInfo } accountButton.isHidden = !configuration.showsAccountButton } } diff --git a/ios/MullvadVPN/Containers/Root/RootConfiguration.swift b/ios/MullvadVPN/Containers/Root/RootConfiguration.swift index 2229bdf4a0..237c4acccf 100644 --- a/ios/MullvadVPN/Containers/Root/RootConfiguration.swift +++ b/ios/MullvadVPN/Containers/Root/RootConfiguration.swift @@ -12,4 +12,5 @@ struct RootConfigration { var deviceName: String? var expiry: Date? var showsAccountButton: Bool + let showsDeviceInfo: Bool } diff --git a/ios/MullvadVPN/Containers/Root/RootContainerViewController.swift b/ios/MullvadVPN/Containers/Root/RootContainerViewController.swift index 679964d46c..05e9383a06 100644 --- a/ios/MullvadVPN/Containers/Root/RootContainerViewController.swift +++ b/ios/MullvadVPN/Containers/Root/RootContainerViewController.swift @@ -70,7 +70,7 @@ class RootContainerViewController: UIViewController { let transitionContainer = UIView(frame: UIScreen.main.bounds) private var presentationContainerAccountButton: UIButton? private var presentationContainerSettingsButton: UIButton? - private var configuration = RootConfigration(showsAccountButton: false) + private var configuration = RootConfigration(showsAccountButton: false, showsDeviceInfo: true) private(set) var headerBarPresentation = HeaderBarPresentation.default private(set) var headerBarHidden = false diff --git a/ios/MullvadVPN/Coordinators/App/ApplicationCoordinator.swift b/ios/MullvadVPN/Coordinators/App/ApplicationCoordinator.swift index b0bb6c97bc..17b02b23cf 100644 --- a/ios/MullvadVPN/Coordinators/App/ApplicationCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/App/ApplicationCoordinator.swift @@ -618,7 +618,6 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo let tunnelObserver = TunnelBlockObserver(didUpdateDeviceState: { [weak self] manager, deviceState in self?.deviceStateDidChange(deviceState) - self?.updateView(deviceState: deviceState) }) tunnelManager.addObserver(tunnelObserver) @@ -636,7 +635,7 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo switch deviceState { case let .loggedIn(accountData, _): updateOutOfTimeTimer() - + updateView(deviceState: deviceState, showDeviceInfo: false) if !accountData.isExpired { router.dismiss(.outOfTime, animated: true) } @@ -646,14 +645,16 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo case .loggedOut: cancelOutOfTimeTimer() + updateView(deviceState: deviceState, showDeviceInfo: false) } } - private func updateView(deviceState: DeviceState) { + private func updateView(deviceState: DeviceState, showDeviceInfo: Bool = true) { let configuration = RootConfigration( deviceName: deviceState.deviceData?.capitalizedName, expiry: deviceState.accountData?.expiry, - showsAccountButton: deviceState.isLoggedIn + showsAccountButton: deviceState.isLoggedIn, + showsDeviceInfo: showDeviceInfo ) primaryNavigationContainer.update(configuration: configuration) @@ -699,6 +700,10 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo router.present(.account) } + func didDismissRegisteredDeviceInAppBanner(deviceState: DeviceState) { + updateView(deviceState: deviceState) + } + // MARK: - UISplitViewControllerDelegate func primaryViewController(forExpanding splitViewController: UISplitViewController) diff --git a/ios/MullvadVPN/Extensions/NSAttributedString+Markdown.swift b/ios/MullvadVPN/Extensions/NSAttributedString+Markdown.swift index 5e7c064b9d..d27a472e14 100644 --- a/ios/MullvadVPN/Extensions/NSAttributedString+Markdown.swift +++ b/ios/MullvadVPN/Extensions/NSAttributedString+Markdown.swift @@ -9,7 +9,11 @@ import UIKit extension NSAttributedString { - convenience init(markdownString: String, font: UIFont) { + convenience init( + markdownString: String, + font: UIFont, + applyEffect: ((String) -> [NSAttributedString.Key: Any])? = nil + ) { let attributedString = NSMutableAttributedString() let components = markdownString.components(separatedBy: "**") @@ -24,6 +28,7 @@ extension NSAttributedString { attributes[.font] = font } else { attributes[.font] = boldFont + attributes.merge(applyEffect?(string) ?? [:], uniquingKeysWith: { $1 }) } attributedString.append(NSAttributedString(string: string, attributes: attributes)) diff --git a/ios/MullvadVPN/Notifications/InAppNotificationDescriptor.swift b/ios/MullvadVPN/Notifications/InAppNotificationDescriptor.swift index 7d16791fc7..63bc9810e3 100644 --- a/ios/MullvadVPN/Notifications/InAppNotificationDescriptor.swift +++ b/ios/MullvadVPN/Notifications/InAppNotificationDescriptor.swift @@ -7,9 +7,10 @@ // import Foundation +import UIKit.UIImage /// Struct describing in-app notification. -struct InAppNotificationDescriptor: Equatable { +struct InAppNotificationDescriptor { /// Notification identifier. var identifier: String @@ -20,7 +21,21 @@ struct InAppNotificationDescriptor: Equatable { var title: String /// Notification body. - var body: String + var body: NSAttributedString + + /// Notification action + var action: InAppNotificationAction? +} + +extension InAppNotificationDescriptor: Equatable { + static func == (lhs: InAppNotificationDescriptor, rhs: InAppNotificationDescriptor) -> Bool { + lhs.identifier == rhs.identifier + } +} + +struct InAppNotificationAction { + var image: UIImage? + var handler: (() -> Void)? } enum NotificationBannerStyle { diff --git a/ios/MullvadVPN/Notifications/Notification Providers/AccountExpiryInAppNotificationProvider.swift b/ios/MullvadVPN/Notifications/Notification Providers/AccountExpiryInAppNotificationProvider.swift index ec5ef9e0ae..350aa0dcb0 100644 --- a/ios/MullvadVPN/Notifications/Notification Providers/AccountExpiryInAppNotificationProvider.swift +++ b/ios/MullvadVPN/Notifications/Notification Providers/AccountExpiryInAppNotificationProvider.swift @@ -72,13 +72,13 @@ final class AccountExpiryInAppNotificationProvider: NotificationProvider, InAppN value: "ACCOUNT CREDIT EXPIRES SOON", comment: "Title for in-app notification, displayed within the last 3 days until account expiry." ), - body: String( + body: .init(string: String( format: NSLocalizedString( "ACCOUNT_EXPIRY_INAPP_NOTIFICATION_BODY", value: "%@ left. Buy more credit.", comment: "Message for in-app notification, displayed within the last 3 days until account expiry." ), duration - ) + )) ) } diff --git a/ios/MullvadVPN/Notifications/Notification Providers/RegisteredDeviceInAppNotification.swift b/ios/MullvadVPN/Notifications/Notification Providers/RegisteredDeviceInAppNotification.swift new file mode 100644 index 0000000000..d468e009c7 --- /dev/null +++ b/ios/MullvadVPN/Notifications/Notification Providers/RegisteredDeviceInAppNotification.swift @@ -0,0 +1,90 @@ +// +// AccountCreationInAppNotification.swift +// MullvadVPN +// +// Created by Mojgan on 2023-04-21. +// Copyright © 2023 Mullvad VPN AB. All rights reserved. +// + +import Foundation +import UIKit.UIColor +import UIKit.UIFont + +final class RegisteredDeviceInAppNotification: NotificationProvider, + InAppNotificationProvider +{ + typealias CompletionHandler = (DeviceState) -> Void + + // MARK: - private properties + + private let tunnelManager: TunnelManager + private let completionHandler: CompletionHandler? + + private var shouldShowBanner = false + private var deviceState: DeviceState + private var tunnelObserver: TunnelBlockObserver? + + private var attributedBody: NSAttributedString { + guard case let .loggedIn(_, storedDeviceData) = deviceState else { return .init(string: "") } + let formattedString = NSLocalizedString( + "ACCOUNT_CREATION_INAPP_NOTIFICATION_BODY", + value: "Welcome, this device is now called **%@**. For more details see the info button in Account.", + comment: "" + ) + let deviceName = storedDeviceData.capitalizedName + let string = String(format: formattedString, deviceName) + return NSMutableAttributedString(markdownString: string, font: .systemFont(ofSize: 14.0)) { deviceName in + return [.foregroundColor: UIColor.InAppNotificationBanner.titleColor] + } + } + + // MARK: - public properties + + var notificationDescriptor: InAppNotificationDescriptor? { + guard shouldShowBanner else { return nil } + return InAppNotificationDescriptor( + identifier: identifier, + style: .success, + title: NSLocalizedString( + "ACCOUNT_CREATION_INAPP_NOTIFICATION_TITLE", + value: "NEW DEVICE CREATED", + comment: "" + ), + body: attributedBody, + action: .init( + image: .init(named: "IconCloseSml"), + handler: { [weak self] in + guard let self = self else { return } + self.shouldShowBanner = false + self.invalidate() + self.completionHandler?(self.deviceState) + } + ) + ) + } + + // MARK: - initialize + + init(tunnelManager: TunnelManager, completionHandler: CompletionHandler? = nil) { + self.tunnelManager = tunnelManager + self.completionHandler = completionHandler + deviceState = tunnelManager.deviceState + super.init() + addObservers() + } + + override var identifier: String { + "net.mullvad.MullvadVPN.AccountCreationInAppNotification" + } + + private func addObservers() { + tunnelObserver = TunnelBlockObserver(didUpdateDeviceState: { [weak self] tunnelManager, deviceState in + guard let self = self, + case .loggedIn = deviceState else { return } + self.shouldShowBanner = true + self.deviceState = deviceState + self.invalidate() + }) + tunnelObserver.flatMap { tunnelManager.addObserver($0) } + } +} diff --git a/ios/MullvadVPN/Notifications/Notification Providers/TunnelStatusNotificationProvider.swift b/ios/MullvadVPN/Notifications/Notification Providers/TunnelStatusNotificationProvider.swift index 52f3b4d75c..daa5b6bf9b 100644 --- a/ios/MullvadVPN/Notifications/Notification Providers/TunnelStatusNotificationProvider.swift +++ b/ios/MullvadVPN/Notifications/Notification Providers/TunnelStatusNotificationProvider.swift @@ -112,14 +112,14 @@ final class TunnelStatusNotificationProvider: NotificationProvider, InAppNotific value: "NETWORK TRAFFIC MIGHT BE LEAKING", comment: "" ), - body: String( + body: .init(string: String( format: NSLocalizedString( "PACKET_TUNNEL_ERROR_INAPP_NOTIFICATION_BODY", value: "Could not configure VPN: %@", comment: "" ), packetTunnelError - ) + )) ) } @@ -156,7 +156,7 @@ final class TunnelStatusNotificationProvider: NotificationProvider, InAppNotific value: "TUNNEL ERROR", comment: "" ), - body: body + body: .init(string: body) ) } @@ -169,13 +169,15 @@ final class TunnelStatusNotificationProvider: NotificationProvider, InAppNotific value: "NETWORK ISSUES", comment: "" ), - body: NSLocalizedString( - "TUNNEL_NO_CONNECTIVITY_INAPP_NOTIFICATION_BODY", - value: """ - Your device is offline. The tunnel will automatically connect once \ - your device is back online. - """, - comment: "" + body: .init( + string: NSLocalizedString( + "TUNNEL_NO_CONNECTIVITY_INAPP_NOTIFICATION_BODY", + value: """ + Your device is offline. The tunnel will automatically connect once \ + your device is back online. + """, + comment: "" + ) ) ) } diff --git a/ios/MullvadVPN/Notifications/UI/NotificationBannerView.swift b/ios/MullvadVPN/Notifications/UI/NotificationBannerView.swift index 858d8a7f6b..0d12090959 100644 --- a/ios/MullvadVPN/Notifications/UI/NotificationBannerView.swift +++ b/ios/MullvadVPN/Notifications/UI/NotificationBannerView.swift @@ -10,6 +10,7 @@ import UIKit final class NotificationBannerView: UIView { private static let indicatorViewSize = CGSize(width: 12, height: 12) + private static let buttonSize = CGSize(width: 18, height: 18) private let backgroundView: UIVisualEffectView = { let effect = UIBlurEffect(style: .dark) @@ -62,15 +63,21 @@ final class NotificationBannerView: UIView { return view }() + private let actionButton: UIButton = { + let button = UIButton() + button.translatesAutoresizingMaskIntoConstraints = false + return button + }() + var title: String? { didSet { titleLabel.text = title } } - var body: String? { + var body: NSAttributedString? { didSet { - bodyLabel.text = body + bodyLabel.attributedText = body } } @@ -80,10 +87,17 @@ final class NotificationBannerView: UIView { } } + var actionHandler: InAppNotificationAction? { + didSet { + actionButton.setImage(actionHandler?.image, for: .normal) + actionButton.addTarget(self, action: #selector(didPress), for: .touchUpInside) + } + } + override init(frame: CGRect) { super.init(frame: frame) - for subview in [titleLabel, bodyLabel, indicatorView] { + for subview in [titleLabel, bodyLabel, indicatorView, actionButton] { wrapperView.addSubview(subview) } @@ -113,22 +127,29 @@ final class NotificationBannerView: UIView { equalToSystemSpacingAfter: indicatorView.trailingAnchor, multiplier: 1 ), - titleLabel.trailingAnchor - .constraint(equalTo: wrapperView.layoutMarginsGuide.trailingAnchor), bodyLabel.topAnchor.constraint( equalToSystemSpacingBelow: titleLabel.bottomAnchor, multiplier: 1 ), bodyLabel.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor), - bodyLabel.trailingAnchor.constraint(equalTo: titleLabel.trailingAnchor), bodyLabel.bottomAnchor.constraint(equalTo: wrapperView.layoutMarginsGuide.bottomAnchor), + + actionButton.leadingAnchor.constraint(equalTo: bodyLabel.trailingAnchor), + actionButton.topAnchor.constraint(equalTo: bodyLabel.topAnchor), + actionButton.trailingAnchor.constraint(equalTo: wrapperView.layoutMarginsGuide.trailingAnchor), + actionButton.widthAnchor.constraint(equalToConstant: NotificationBannerView.buttonSize.width), + actionButton.heightAnchor.constraint(equalToConstant: NotificationBannerView.buttonSize.height), ]) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + + @objc private func didPress() { + actionHandler?.handler?() + } } private extension NotificationBannerStyle { diff --git a/ios/MullvadVPN/Notifications/UI/NotificationController.swift b/ios/MullvadVPN/Notifications/UI/NotificationController.swift index aa325332ed..888c4ef497 100644 --- a/ios/MullvadVPN/Notifications/UI/NotificationController.swift +++ b/ios/MullvadVPN/Notifications/UI/NotificationController.swift @@ -105,7 +105,8 @@ final class NotificationController: UIViewController { bannerView.title = notification.title bannerView.body = notification.body bannerView.style = notification.style - bannerView.accessibilityLabel = "\(notification.title)\n\(notification.body)" + bannerView.actionHandler = notification.action + bannerView.accessibilityLabel = "\(notification.title)\n\(notification.body.string)" if animated { let animator = UIViewPropertyAnimator( diff --git a/ios/MullvadVPN/SceneDelegate.swift b/ios/MullvadVPN/SceneDelegate.swift index 5a6651e594..a256f1add1 100644 --- a/ios/MullvadVPN/SceneDelegate.swift +++ b/ios/MullvadVPN/SceneDelegate.swift @@ -40,6 +40,10 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate, SettingsMigrationUIHand appCoordinator?.showAccount() } + func didDismissRegisteredDeviceInAppBanner(deviceState: DeviceState) { + appCoordinator?.didDismissRegisteredDeviceInAppBanner(deviceState: deviceState) + } + // MARK: - Private private func addTunnelObserver() { |
