diff options
| author | Jon Petersson <jon.petersson@mullvad.net> | 2025-01-22 16:39:21 +0100 |
|---|---|---|
| committer | Jon Petersson <jon.petersson@mullvad.net> | 2025-01-22 16:39:21 +0100 |
| commit | a1b47c23a9532abc0f51fc94de481b0528afb9fb (patch) | |
| tree | 8c93aed3d2fc5540e963bc6b02dbdd8549268429 /ios/MullvadVPN | |
| parent | 060839d420a9cf222b49fe4932730a98fd5b1434 (diff) | |
| parent | 5f9315b46dc7a364bc20d40420c2e0feb34a2d6c (diff) | |
| download | mullvadvpn-a1b47c23a9532abc0f51fc94de481b0528afb9fb.tar.xz mullvadvpn-a1b47c23a9532abc0f51fc94de481b0528afb9fb.zip | |
Merge branch 'add-in-app-notification-banner-for-changelog-ios-989'
Diffstat (limited to 'ios/MullvadVPN')
13 files changed, 176 insertions, 65 deletions
diff --git a/ios/MullvadVPN/AppDelegate.swift b/ios/MullvadVPN/AppDelegate.swift index d6620c992c..a598476ceb 100644 --- a/ios/MullvadVPN/AppDelegate.swift +++ b/ios/MullvadVPN/AppDelegate.swift @@ -46,6 +46,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD private var migrationManager: MigrationManager! nonisolated(unsafe) private(set) var accessMethodRepository = AccessMethodRepository() + private(set) var appPreferences = AppPreferences() private(set) var shadowsocksLoader: ShadowsocksLoaderProtocol! private(set) var configuredTransportProvider: ProxyConfigurationTransportProvider! private(set) var ipOverrideRepository = IPOverrideRepository() @@ -450,10 +451,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD private func setupNotifications() { NotificationManager.shared.notificationProviders = [ + LatestChangesNotificationProvider(appPreferences: appPreferences), TunnelStatusNotificationProvider(tunnelManager: tunnelManager), AccountExpirySystemNotificationProvider(tunnelManager: tunnelManager), AccountExpiryInAppNotificationProvider(tunnelManager: tunnelManager), - RegisteredDeviceInAppNotificationProvider(tunnelManager: tunnelManager), + NewDeviceNotificationProvider(tunnelManager: tunnelManager), ] UNUserNotificationCenter.current().delegate = self } diff --git a/ios/MullvadVPN/Classes/AppRoutes.swift b/ios/MullvadVPN/Classes/AppRoutes.swift index 9f2d6a92b1..a7753589ba 100644 --- a/ios/MullvadVPN/Classes/AppRoutes.swift +++ b/ios/MullvadVPN/Classes/AppRoutes.swift @@ -93,7 +93,7 @@ enum AppRoute: AppRouteProtocol { case selectLocation /** - Changelog route. + Changelog standalone route (not subsetting). */ case changelog @@ -110,7 +110,7 @@ enum AppRoute: AppRouteProtocol { var isExclusive: Bool { switch self { - case .account, .settings, .changelog, .alert: + case .account, .settings, .alert: return true default: return false @@ -129,13 +129,11 @@ enum AppRoute: AppRouteProtocol { switch self { case .tos, .login, .main, .revoked, .outOfTime, .welcome: return .primary - case .changelog: - return .changelog case .selectLocation: return .selectLocation case .account: return .account - case .settings, .daita: + case .settings, .daita, .changelog: return .settings case let .alert(id): return .alert(id) diff --git a/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift b/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift index e2f2e6d32d..3f51f8cdb3 100644 --- a/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift @@ -296,11 +296,6 @@ final class ApplicationCoordinator: Coordinator, Presenting, @preconcurrency Roo } } - // Change log can be presented simultaneously with other routes. - if !appPreferences.hasSeenLastChanges { - routes.append(.changelog) - } - return routes } @@ -336,14 +331,18 @@ final class ApplicationCoordinator: Coordinator, Presenting, @preconcurrency Roo private func presentChangeLog(animated: Bool, completion: @escaping (Coordinator) -> Void) { let coordinator = ChangeLogCoordinator( + route: .changelog, navigationController: CustomNavigationController(), viewModel: ChangeLogViewModel(changeLogReader: ChangeLogReader()) ) + coordinator.didFinish = { [weak self] _ in + self?.router.dismiss(.changelog, animated: animated) + } + coordinator.start(animated: false) - presentChild(coordinator, animated: animated) { [weak self] in - self?.appPreferences.markChangeLogSeen() + presentChild(coordinator, animated: animated) { completion(coordinator) } } @@ -820,6 +819,8 @@ final class ApplicationCoordinator: Coordinator, Presenting, @preconcurrency Roo case .accountExpiryInAppNotification: isPresentingAccountExpiryBanner = false updateDeviceInfo(deviceState: tunnelManager.deviceState) + case .latestChangesInAppNotificationProvider: + router.present(.changelog) default: return } } @@ -836,15 +837,3 @@ extension DeviceState { isLoggedIn ? UISplitViewController.DisplayMode.oneBesideSecondary : .secondaryOnly } } - -fileprivate extension AppPreferencesDataSource { - var hasSeenLastChanges: Bool { - lastSeenChangeLogVersion == Bundle.main.shortVersion - } - - mutating func markChangeLogSeen() { - lastSeenChangeLogVersion = Bundle.main.shortVersion - } - - // swiftlint:disable:next file_length -} diff --git a/ios/MullvadVPN/Coordinators/ChangeLogCoordinator.swift b/ios/MullvadVPN/Coordinators/ChangeLogCoordinator.swift index 3fb5cb8048..cfa1d1bb71 100644 --- a/ios/MullvadVPN/Coordinators/ChangeLogCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/ChangeLogCoordinator.swift @@ -12,14 +12,21 @@ import SwiftUI import UIKit final class ChangeLogCoordinator: Coordinator, Presentable, SettingsChildCoordinator { - private var navigationController: UINavigationController? + private let route: AppRoute private let viewModel: ChangeLogViewModel + private var navigationController: UINavigationController? + var didFinish: ((ChangeLogCoordinator) -> Void)? var presentedViewController: UIViewController { - return navigationController! + navigationController! } - init(navigationController: UINavigationController, viewModel: ChangeLogViewModel) { + init( + route: AppRoute, + navigationController: UINavigationController, + viewModel: ChangeLogViewModel + ) { + self.route = route self.viewModel = viewModel self.navigationController = navigationController } @@ -33,8 +40,30 @@ final class ChangeLogCoordinator: Coordinator, Presentable, SettingsChildCoordin value: "What's new", comment: "" ) - changeLogViewController.navigationItem.largeTitleDisplayMode = .always - navigationController?.navigationBar.prefersLargeTitles = true + + switch route { + case .changelog: + let barButtonItem = UIBarButtonItem( + title: NSLocalizedString( + "CHANGELOG_NAVIGATION_DONE_BUTTON", + tableName: "Changelog", + value: "Done", + comment: "" + ), + primaryAction: UIAction { [weak self] _ in + guard let self else { return } + didFinish?(self) + } + ) + barButtonItem.style = .done + changeLogViewController.navigationItem.rightBarButtonItem = barButtonItem + fallthrough + case .settings: + changeLogViewController.navigationItem.largeTitleDisplayMode = .always + navigationController?.navigationBar.prefersLargeTitles = true + default: break + } + navigationController?.pushViewController(changeLogViewController, animated: animated) } } diff --git a/ios/MullvadVPN/Coordinators/Settings/SettingsViewControllerFactory.swift b/ios/MullvadVPN/Coordinators/Settings/SettingsViewControllerFactory.swift index 778900dec3..41ac5343b6 100644 --- a/ios/MullvadVPN/Coordinators/Settings/SettingsViewControllerFactory.swift +++ b/ios/MullvadVPN/Coordinators/Settings/SettingsViewControllerFactory.swift @@ -98,6 +98,7 @@ struct SettingsViewControllerFactory { private func makeChangelogCoordinator() -> MakeChildResult { return .childCoordinator( ChangeLogCoordinator( + route: .settings(.changelog), navigationController: navigationController, viewModel: ChangeLogViewModel(changeLogReader: ChangeLogReader()) ) 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 diff --git a/ios/MullvadVPN/SceneDelegate.swift b/ios/MullvadVPN/SceneDelegate.swift index ff5334d853..82327b24ba 100644 --- a/ios/MullvadVPN/SceneDelegate.swift +++ b/ios/MullvadVPN/SceneDelegate.swift @@ -78,7 +78,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate, @preconcurrency Setting hostname: ApplicationConfiguration.hostName ) ), - appPreferences: AppPreferences(), + appPreferences: appDelegate.appPreferences, accessMethodRepository: accessMethodRepository, transportProvider: appDelegate.configuredTransportProvider, ipOverrideRepository: appDelegate.ipOverrideRepository diff --git a/ios/MullvadVPN/View controllers/ChangeLog/ChangeLogView.swift b/ios/MullvadVPN/View controllers/ChangeLog/ChangeLogView.swift index 4131d1333f..1b4e091fce 100644 --- a/ios/MullvadVPN/View controllers/ChangeLog/ChangeLogView.swift +++ b/ios/MullvadVPN/View controllers/ChangeLog/ChangeLogView.swift @@ -32,19 +32,6 @@ struct ChangeLogView<ViewModel>: View where ViewModel: ChangeLogViewModelProtoco } .listStyle(.plain) .frame(maxHeight: .infinity) - - MainButton( - text: LocalizedStringKey("See full changelog"), - style: .default, - image: Image(.iconExtlink), - imagePosition: .trailing - ) { - if let url = - URL(string: "https://github.com/mullvad/mullvadvpn-app/blob/main/ios/CHANGELOG.md") { - UIApplication.shared.open(url) - } - } - .padding(.vertical, 24) } .padding(.horizontal, 24.0) } |
