summaryrefslogtreecommitdiffhomepage
path: root/ios/MullvadVPN/Notifications
diff options
context:
space:
mode:
authorJon Petersson <jon.petersson@mullvad.net>2025-01-22 16:39:21 +0100
committerJon Petersson <jon.petersson@mullvad.net>2025-01-22 16:39:21 +0100
commita1b47c23a9532abc0f51fc94de481b0528afb9fb (patch)
tree8c93aed3d2fc5540e963bc6b02dbdd8549268429 /ios/MullvadVPN/Notifications
parent060839d420a9cf222b49fe4932730a98fd5b1434 (diff)
parent5f9315b46dc7a364bc20d40420c2e0feb34a2d6c (diff)
downloadmullvadvpn-a1b47c23a9532abc0f51fc94de481b0528afb9fb.tar.xz
mullvadvpn-a1b47c23a9532abc0f51fc94de481b0528afb9fb.zip
Merge branch 'add-in-app-notification-banner-for-changelog-ios-989'
Diffstat (limited to 'ios/MullvadVPN/Notifications')
-rw-r--r--ios/MullvadVPN/Notifications/InAppNotificationDescriptor.swift5
-rw-r--r--ios/MullvadVPN/Notifications/Notification Providers/LatestChangesNotificationProvider.swift85
-rw-r--r--ios/MullvadVPN/Notifications/Notification Providers/NewDeviceNotificationProvider.swift (renamed from ios/MullvadVPN/Notifications/Notification Providers/RegisteredDeviceInAppNotificationProvider.swift)8
-rw-r--r--ios/MullvadVPN/Notifications/NotificationProviderIdentifier.swift1
-rw-r--r--ios/MullvadVPN/Notifications/UI/NotificationBannerView.swift45
-rw-r--r--ios/MullvadVPN/Notifications/UI/NotificationController.swift3
6 files changed, 126 insertions, 21 deletions
diff --git a/ios/MullvadVPN/Notifications/InAppNotificationDescriptor.swift b/ios/MullvadVPN/Notifications/InAppNotificationDescriptor.swift
index 7ed81c1e77..5839b62c02 100644
--- a/ios/MullvadVPN/Notifications/InAppNotificationDescriptor.swift
+++ b/ios/MullvadVPN/Notifications/InAppNotificationDescriptor.swift
@@ -24,7 +24,10 @@ struct InAppNotificationDescriptor: Equatable {
var body: NSAttributedString
/// Notification action.
- var action: InAppNotificationAction?
+ var button: InAppNotificationAction?
+
+ /// Notification tap action (optional).
+ var tapAction: InAppNotificationAction?
}
/// Type describing a specific in-app notification action.
diff --git a/ios/MullvadVPN/Notifications/Notification Providers/LatestChangesNotificationProvider.swift b/ios/MullvadVPN/Notifications/Notification Providers/LatestChangesNotificationProvider.swift
new file mode 100644
index 0000000000..2e72f45545
--- /dev/null
+++ b/ios/MullvadVPN/Notifications/Notification Providers/LatestChangesNotificationProvider.swift
@@ -0,0 +1,85 @@
+//
+// LatestChangesNotificationProvider.swift
+// MullvadVPN
+//
+// Created by Mojgan on 2025-01-15.
+// Copyright © 2025 Mullvad VPN AB. All rights reserved.
+//
+import Foundation
+import UIKit
+
+class LatestChangesNotificationProvider: NotificationProvider, InAppNotificationProvider, @unchecked Sendable {
+ private var appPreferences: AppPreferencesDataSource
+ private let appVersion: String = Bundle.main.productVersion
+
+ init(appPreferences: AppPreferencesDataSource) {
+ self.appPreferences = appPreferences
+ }
+
+ var shouldShowNotification: Bool {
+ // If this is the first installation, no notification will be shown.
+ guard !appPreferences.lastSeenChangeLogVersion.isEmpty else { return false }
+ // Display the notification only if the app is updated from a previously installed version.
+ return appPreferences.lastSeenChangeLogVersion != appVersion
+ }
+
+ override var identifier: NotificationProviderIdentifier {
+ .latestChangesInAppNotificationProvider
+ }
+
+ var notificationDescriptor: InAppNotificationDescriptor? {
+ defer {
+ // Always update the last seen version
+ appPreferences.lastSeenChangeLogVersion = appVersion
+ }
+
+ guard shouldShowNotification else { return nil }
+
+ return InAppNotificationDescriptor(
+ identifier: identifier,
+ style: .success,
+ title: NSLocalizedString(
+ "LATEST_CHANGES_IN_APP_NOTIFICATION_TITLE",
+ value: "NEW VERSION INSTALLED",
+ comment: ""
+ ),
+ body: createNotificationBody(),
+ button: createCloseButtonAction(),
+ tapAction: createTapAction()
+ )
+ }
+
+ private func createNotificationBody() -> NSAttributedString {
+ NSAttributedString(
+ markdownString: NSLocalizedString(
+ "LATEST_CHANGES_IN_APP_NOTIFICATION_BODY",
+ value: "**Tap here** to see what’s new.",
+ comment: ""
+ ),
+ options: MarkdownStylingOptions(font: UIFont.preferredFont(forTextStyle: .body)),
+ applyEffect: { markdownType, _ in
+ guard case .bold = markdownType else { return [:] }
+ return [.foregroundColor: UIColor.InAppNotificationBanner.titleColor]
+ }
+ )
+ }
+
+ private func createCloseButtonAction() -> InAppNotificationAction {
+ InAppNotificationAction(
+ image: UIImage(named: "IconCloseSml"),
+ handler: { [weak self] in
+ self?.invalidate()
+ }
+ )
+ }
+
+ private func createTapAction() -> InAppNotificationAction {
+ InAppNotificationAction(
+ handler: { [weak self] in
+ guard let self else { return }
+ self.invalidate()
+ NotificationManager.shared.notificationProvider(self, didReceiveAction: "\(self.identifier)")
+ }
+ )
+ }
+}
diff --git a/ios/MullvadVPN/Notifications/Notification Providers/RegisteredDeviceInAppNotificationProvider.swift b/ios/MullvadVPN/Notifications/Notification Providers/NewDeviceNotificationProvider.swift
index ade1b0eb20..66b76f9116 100644
--- a/ios/MullvadVPN/Notifications/Notification Providers/RegisteredDeviceInAppNotificationProvider.swift
+++ b/ios/MullvadVPN/Notifications/Notification Providers/NewDeviceNotificationProvider.swift
@@ -1,5 +1,5 @@
//
-// RegisteredDeviceInAppNotification.swift
+// NewDeviceNotificationProvider.swift
// MullvadVPN
//
// Created by Mojgan on 2023-04-21.
@@ -11,7 +11,7 @@ import MullvadSettings
import UIKit.UIColor
import UIKit.UIFont
-final class RegisteredDeviceInAppNotificationProvider: NotificationProvider,
+final class NewDeviceNotificationProvider: NotificationProvider,
InAppNotificationProvider, @unchecked Sendable {
// MARK: - private properties
@@ -57,8 +57,8 @@ final class RegisteredDeviceInAppNotificationProvider: NotificationProvider,
comment: ""
),
body: attributedBody,
- action: .init(
- image: .init(named: "IconCloseSml"),
+ button: InAppNotificationAction(
+ image: UIImage(named: "IconCloseSml"),
handler: { [weak self] in
guard let self else { return }
isNewDeviceRegistered = false
diff --git a/ios/MullvadVPN/Notifications/NotificationProviderIdentifier.swift b/ios/MullvadVPN/Notifications/NotificationProviderIdentifier.swift
index e15ec4b01e..155d0f7bdb 100644
--- a/ios/MullvadVPN/Notifications/NotificationProviderIdentifier.swift
+++ b/ios/MullvadVPN/Notifications/NotificationProviderIdentifier.swift
@@ -13,6 +13,7 @@ enum NotificationProviderIdentifier: String {
case accountExpiryInAppNotification = "AccountExpiryInAppNotification"
case registeredDeviceInAppNotification = "RegisteredDeviceInAppNotification"
case tunnelStatusNotificationProvider = "TunnelStatusNotificationProvider"
+ case latestChangesInAppNotificationProvider = "LatestChangesInAppNotificationProvider"
case `default` = "default"
var domainIdentifier: String {
diff --git a/ios/MullvadVPN/Notifications/UI/NotificationBannerView.swift b/ios/MullvadVPN/Notifications/UI/NotificationBannerView.swift
index 32449fa59d..7dea419a73 100644
--- a/ios/MullvadVPN/Notifications/UI/NotificationBannerView.swift
+++ b/ios/MullvadVPN/Notifications/UI/NotificationBannerView.swift
@@ -46,9 +46,16 @@ final class NotificationBannerView: UIView {
}()
private lazy var bodyStackView: UIStackView = {
- let stackView = UIStackView(arrangedSubviews: [bodyLabel, actionButton])
+ let stackView = UIStackView(arrangedSubviews: [titleLabel, bodyLabel])
stackView.alignment = .top
stackView.distribution = .fill
+ stackView.axis = .vertical
+ stackView.spacing = UIStackView.spacingUseSystem
+ return stackView
+ }()
+
+ private lazy var contentStackView: UIStackView = {
+ let stackView = UIStackView(arrangedSubviews: [bodyStackView, actionButton])
stackView.spacing = UIStackView.spacingUseSystem
return stackView
}()
@@ -87,11 +94,13 @@ final class NotificationBannerView: UIView {
}
}
+ var tapAction: InAppNotificationAction?
+
override init(frame: CGRect) {
super.init(frame: frame)
-
- addActionHandlers()
addSubviews()
+ addTapHandler()
+ addActionHandlers()
addConstraints()
}
@@ -99,12 +108,22 @@ final class NotificationBannerView: UIView {
fatalError("init(coder:) has not been implemented")
}
+ private func addTapHandler() {
+ let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTap))
+ addGestureRecognizer(tapGesture)
+ }
+
private func addActionHandlers() {
actionButton.addTarget(self, action: #selector(handleActionTap), for: .touchUpInside)
}
+ @objc
+ private func handleTap() {
+ tapAction?.handler?()
+ }
+
private func addSubviews() {
- wrapperView.addConstrainedSubviews([titleLabel, indicatorView, bodyStackView])
+ wrapperView.addConstrainedSubviews([indicatorView, contentStackView])
backgroundView.contentView.addConstrainedSubviews([wrapperView]) {
wrapperView.pinEdgesToSuperview()
}
@@ -114,9 +133,6 @@ final class NotificationBannerView: UIView {
}
private func addConstraints() {
- actionButton.setContentCompressionResistancePriority(.required, for: .horizontal)
- actionButton.setContentHuggingPriority(.required, for: .horizontal)
-
NSLayoutConstraint.activate([
indicatorView.bottomAnchor.constraint(equalTo: titleLabel.firstBaselineAnchor),
indicatorView.leadingAnchor.constraint(equalTo: wrapperView.layoutMarginsGuide.leadingAnchor),
@@ -125,14 +141,13 @@ final class NotificationBannerView: UIView {
indicatorView.heightAnchor
.constraint(equalToConstant: UIMetrics.InAppBannerNotification.indicatorSize.height),
- titleLabel.topAnchor.constraint(equalTo: wrapperView.layoutMarginsGuide.topAnchor),
- titleLabel.leadingAnchor.constraint(equalToSystemSpacingAfter: indicatorView.trailingAnchor, multiplier: 1),
- titleLabel.trailingAnchor.constraint(equalTo: wrapperView.layoutMarginsGuide.trailingAnchor),
-
- bodyStackView.topAnchor.constraint(equalToSystemSpacingBelow: titleLabel.bottomAnchor, multiplier: 1),
- bodyStackView.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor),
- bodyStackView.trailingAnchor.constraint(equalTo: titleLabel.trailingAnchor),
- bodyStackView.bottomAnchor.constraint(equalTo: wrapperView.layoutMarginsGuide.bottomAnchor),
+ contentStackView.topAnchor.constraint(equalTo: wrapperView.layoutMarginsGuide.topAnchor),
+ contentStackView.leadingAnchor.constraint(
+ equalToSystemSpacingAfter: indicatorView.trailingAnchor,
+ multiplier: 1
+ ),
+ contentStackView.trailingAnchor.constraint(equalTo: wrapperView.layoutMarginsGuide.trailingAnchor),
+ contentStackView.bottomAnchor.constraint(equalTo: wrapperView.layoutMarginsGuide.bottomAnchor),
])
}
diff --git a/ios/MullvadVPN/Notifications/UI/NotificationController.swift b/ios/MullvadVPN/Notifications/UI/NotificationController.swift
index b29e3d2bae..f0e9a82e57 100644
--- a/ios/MullvadVPN/Notifications/UI/NotificationController.swift
+++ b/ios/MullvadVPN/Notifications/UI/NotificationController.swift
@@ -97,7 +97,8 @@ final class NotificationController: UIViewController {
bannerView.title = notification.title
bannerView.body = notification.body
bannerView.style = notification.style
- bannerView.action = notification.action
+ bannerView.action = notification.button
+ bannerView.tapAction = notification.tapAction
bannerView.accessibilityLabel = "\(notification.title)\n\(notification.body.string)"
// Do not emit the .layoutChanged unless the banner is focused to avoid capturing