diff options
Diffstat (limited to 'ios/MullvadVPN/Notifications')
| -rw-r--r-- | ios/MullvadVPN/Notifications/InAppNotificationDescriptor.swift | 5 | ||||
| -rw-r--r-- | ios/MullvadVPN/Notifications/Notification Providers/LatestChangesNotificationProvider.swift | 85 | ||||
| -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.swift | 1 | ||||
| -rw-r--r-- | ios/MullvadVPN/Notifications/UI/NotificationBannerView.swift | 45 | ||||
| -rw-r--r-- | ios/MullvadVPN/Notifications/UI/NotificationController.swift | 3 |
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 |
