summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorMojgan <Mojgan.jelodar@codic.se>2023-04-25 16:31:08 +0200
committerAndrej Mihajlov <and@mullvad.net>2023-04-26 09:51:00 +0200
commitf83576b292b5e293e99ac71b7564d3f5950b210e (patch)
treebd8be4e36e9fa9815b4a3d57779108d948ae0d11
parent5ddf2a83859a4a1f313fa9e1455af1339bddf28b (diff)
downloadmullvadvpn-f83576b292b5e293e99ac71b7564d3f5950b210e.tar.xz
mullvadvpn-f83576b292b5e293e99ac71b7564d3f5950b210e.zip
Add in-app banner message for a new device
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj4
-rw-r--r--ios/MullvadVPN/AppDelegate.swift5
-rw-r--r--ios/MullvadVPN/Containers/Root/HeaderBarView.swift2
-rw-r--r--ios/MullvadVPN/Containers/Root/RootConfiguration.swift1
-rw-r--r--ios/MullvadVPN/Containers/Root/RootContainerViewController.swift2
-rw-r--r--ios/MullvadVPN/Coordinators/App/ApplicationCoordinator.swift13
-rw-r--r--ios/MullvadVPN/Extensions/NSAttributedString+Markdown.swift7
-rw-r--r--ios/MullvadVPN/Notifications/InAppNotificationDescriptor.swift19
-rw-r--r--ios/MullvadVPN/Notifications/Notification Providers/AccountExpiryInAppNotificationProvider.swift4
-rw-r--r--ios/MullvadVPN/Notifications/Notification Providers/RegisteredDeviceInAppNotification.swift90
-rw-r--r--ios/MullvadVPN/Notifications/Notification Providers/TunnelStatusNotificationProvider.swift22
-rw-r--r--ios/MullvadVPN/Notifications/UI/NotificationBannerView.swift33
-rw-r--r--ios/MullvadVPN/Notifications/UI/NotificationController.swift3
-rw-r--r--ios/MullvadVPN/SceneDelegate.swift4
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() {