diff --git a/ios/CHANGELOG.md b/ios/CHANGELOG.md index 01f57d42672b..775a8675bac0 100644 --- a/ios/CHANGELOG.md +++ b/ios/CHANGELOG.md @@ -24,6 +24,8 @@ Line wrap the file at 100 chars. Th ## Unreleased ### Fixed - Broken DAITA settings view on iOS 15. +### Changed +- Moved Changelog to settings and introduced an in-app notification banner to inform users of changes ## [2025.1 - 2025-01-14] ### Added diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index 2d714acb6d21..ac7b7bc0549e 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -784,7 +784,7 @@ A9A5F9F12ACB05160083449F /* String+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5807E2BF2432038B00F5FF30 /* String+Helpers.swift */; }; A9A5F9F22ACB05160083449F /* NotificationConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C8191729FAA2C400DEB1B4 /* NotificationConfiguration.swift */; }; A9A5F9F32ACB05160083449F /* AccountExpirySystemNotificationProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587B75402668FD7700DEF7E9 /* AccountExpirySystemNotificationProvider.swift */; }; - A9A5F9F52ACB05160083449F /* RegisteredDeviceInAppNotificationProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = F07CFF1F29F2720E008C0343 /* RegisteredDeviceInAppNotificationProvider.swift */; }; + A9A5F9F52ACB05160083449F /* NewDeviceNotificationProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = F07CFF1F29F2720E008C0343 /* NewDeviceNotificationProvider.swift */; }; A9A5F9F62ACB05160083449F /* TunnelStatusNotificationProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58A94AE326CFD945001CB97C /* TunnelStatusNotificationProvider.swift */; }; A9A5F9F72ACB05160083449F /* NotificationProviderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58B26E232943520C00D5980C /* NotificationProviderProtocol.swift */; }; A9A5F9F82ACB05160083449F /* NotificationProviderIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0C2AEFC2A0BB5CC00986207 /* NotificationProviderIdentifier.swift */; }; @@ -954,7 +954,7 @@ F07B53572C53B5270024F547 /* LocalNetworkIPs.swift in Sources */ = {isa = PBXBuildFile; fileRef = F07B53562C53B5270024F547 /* LocalNetworkIPs.swift */; }; F07BF2622A26279100042943 /* RedeemVoucherOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = F07BF2612A26279100042943 /* RedeemVoucherOperation.swift */; }; F07C9D952B220C77006F1C5E /* libmullvad_ios.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 01F1FF1D29F0627D007083C3 /* libmullvad_ios.a */; }; - F07CFF2029F2720E008C0343 /* RegisteredDeviceInAppNotificationProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = F07CFF1F29F2720E008C0343 /* RegisteredDeviceInAppNotificationProvider.swift */; }; + F07CFF2029F2720E008C0343 /* NewDeviceNotificationProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = F07CFF1F29F2720E008C0343 /* NewDeviceNotificationProvider.swift */; }; F07F63CE2C63E5790027A351 /* AccessMethodRepository+Stub.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0164EB92B4456D30020268D /* AccessMethodRepository+Stub.swift */; }; F08827872B318C840020A383 /* ShadowsocksCipherOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58DFF7D92B02862E00F864E0 /* ShadowsocksCipherOptions.swift */; }; F08827882B318F960020A383 /* PersistentAccessMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 586C0D962B04E0AC00E7CDD7 /* PersistentAccessMethod.swift */; }; @@ -1023,6 +1023,8 @@ F0C3333C2B31A29C00D1A478 /* MullvadSettings.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 58B2FDD32AA71D2A003EB5C6 /* MullvadSettings.framework */; }; F0C6A8432AB08E54000777A8 /* RedeemVoucherViewConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0C6A8422AB08E54000777A8 /* RedeemVoucherViewConfiguration.swift */; }; F0C6FA852A6A733700F521F0 /* InAppPurchaseInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0C6FA842A6A733700F521F0 /* InAppPurchaseInteractor.swift */; }; + F0D5591E2D38051C0072B63F /* LatestChangesNotificationProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0D5591D2D3805050072B63F /* LatestChangesNotificationProvider.swift */; }; + F0D5591F2D38051C0072B63F /* LatestChangesNotificationProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0D5591D2D3805050072B63F /* LatestChangesNotificationProvider.swift */; }; F0D7FF8F2B31DF5900E0FDE5 /* AccessMethodRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5827B0A02B0E064E00CCBBA1 /* AccessMethodRepository.swift */; }; F0D7FF902B31E00B00E0FDE5 /* AccessMethodKind.swift in Sources */ = {isa = PBXBuildFile; fileRef = 588D7ED72AF3A533005DF40A /* AccessMethodKind.swift */; }; F0D8825B2B04F53600D3EF9A /* OutgoingConnectionData.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0D8825A2B04F53600D3EF9A /* OutgoingConnectionData.swift */; }; @@ -2241,7 +2243,7 @@ F07B53562C53B5270024F547 /* LocalNetworkIPs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalNetworkIPs.swift; sourceTree = ""; }; F07BF2572A26112D00042943 /* InputTextFormatterTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InputTextFormatterTests.swift; sourceTree = ""; }; F07BF2612A26279100042943 /* RedeemVoucherOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RedeemVoucherOperation.swift; sourceTree = ""; }; - F07CFF1F29F2720E008C0343 /* RegisteredDeviceInAppNotificationProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegisteredDeviceInAppNotificationProvider.swift; sourceTree = ""; }; + F07CFF1F29F2720E008C0343 /* NewDeviceNotificationProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewDeviceNotificationProvider.swift; sourceTree = ""; }; F09084672C6E88ED001CD36E /* DaitaPromptAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DaitaPromptAlert.swift; sourceTree = ""; }; F09A29782A9F8A9B00EA3B6F /* LogoutDialogueView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LogoutDialogueView.swift; sourceTree = ""; }; F09A29792A9F8A9B00EA3B6F /* VoucherTextField.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VoucherTextField.swift; sourceTree = ""; }; @@ -2286,6 +2288,7 @@ F0C4C9BF2C495E7500A79006 /* EphemeralPeerExchangeActorStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EphemeralPeerExchangeActorStub.swift; sourceTree = ""; }; F0C6A8422AB08E54000777A8 /* RedeemVoucherViewConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RedeemVoucherViewConfiguration.swift; sourceTree = ""; }; F0C6FA842A6A733700F521F0 /* InAppPurchaseInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InAppPurchaseInteractor.swift; sourceTree = ""; }; + F0D5591D2D3805050072B63F /* LatestChangesNotificationProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LatestChangesNotificationProvider.swift; sourceTree = ""; }; F0D8825A2B04F53600D3EF9A /* OutgoingConnectionData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OutgoingConnectionData.swift; sourceTree = ""; }; F0DA87462A9CB9A2006044F1 /* AccountExpiryRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountExpiryRow.swift; sourceTree = ""; }; F0DA87482A9CBA9F006044F1 /* AccountDeviceRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountDeviceRow.swift; sourceTree = ""; }; @@ -3382,8 +3385,9 @@ 7A6F2FA62AFBB9AE006D0856 /* AccountExpiry.swift */, 58607A4C2947287800BC467D /* AccountExpiryInAppNotificationProvider.swift */, 587B75402668FD7700DEF7E9 /* AccountExpirySystemNotificationProvider.swift */, + F0D5591D2D3805050072B63F /* LatestChangesNotificationProvider.swift */, + F07CFF1F29F2720E008C0343 /* NewDeviceNotificationProvider.swift */, 58C8191729FAA2C400DEB1B4 /* NotificationConfiguration.swift */, - F07CFF1F29F2720E008C0343 /* RegisteredDeviceInAppNotificationProvider.swift */, 58A94AE326CFD945001CB97C /* TunnelStatusNotificationProvider.swift */, ); path = "Notification Providers"; @@ -5620,7 +5624,7 @@ A9A5F9F12ACB05160083449F /* String+Helpers.swift in Sources */, A9A5F9F22ACB05160083449F /* NotificationConfiguration.swift in Sources */, A9A5F9F32ACB05160083449F /* AccountExpirySystemNotificationProvider.swift in Sources */, - A9A5F9F52ACB05160083449F /* RegisteredDeviceInAppNotificationProvider.swift in Sources */, + A9A5F9F52ACB05160083449F /* NewDeviceNotificationProvider.swift in Sources */, F09D04B72AE941DA003D4F89 /* OutgoingConnectionProxyTests.swift in Sources */, F09D04B92AE95111003D4F89 /* OutgoingConnectionProxy.swift in Sources */, 7A6000F92B6273A4001CF0D9 /* AccessMethodViewModel.swift in Sources */, @@ -5715,6 +5719,7 @@ A9A5FA2C2ACB05160083449F /* DeviceCheckOperationTests.swift in Sources */, A9A5FA2D2ACB05160083449F /* DurationTests.swift in Sources */, A9A5FA2E2ACB05160083449F /* FileCacheTests.swift in Sources */, + F0D5591F2D38051C0072B63F /* LatestChangesNotificationProvider.swift in Sources */, 7A9F28FC2CA69D0C005F2089 /* DAITASettingsTests.swift in Sources */, A9A5FA2F2ACB05160083449F /* FixedWidthIntegerArithmeticsTests.swift in Sources */, 7AA513862BC91C6B00D081A4 /* LogRotationTests.swift in Sources */, @@ -6092,6 +6097,8 @@ 5868585524054096000B8131 /* CustomButton.swift in Sources */, 58E25F812837BBBB002CFB2C /* SceneDelegate.swift in Sources */, 7A1A26492A29D48A00B978AA /* RelayFilterCellFactory.swift in Sources */, + 5867771629097C5B006F721F /* ProductState.swift in Sources */, + F0D5591E2D38051C0072B63F /* LatestChangesNotificationProvider.swift in Sources */, 7A28826A2BA8336600FD9F20 /* VPNSettingsCoordinator.swift in Sources */, 7A6389DE2B7E3BD6008E77E1 /* CustomListItemIdentifier.swift in Sources */, 58C76A082A33850E00100D75 /* ApplicationTarget.swift in Sources */, @@ -6114,7 +6121,7 @@ 58F3C0A4249CB069003E76BE /* HeaderBarView.swift in Sources */, 5864AF0829C78849005B0CD9 /* CellFactoryProtocol.swift in Sources */, 7A6389E22B7E3BD6008E77E1 /* CustomListInteractor.swift in Sources */, - F07CFF2029F2720E008C0343 /* RegisteredDeviceInAppNotificationProvider.swift in Sources */, + F07CFF2029F2720E008C0343 /* NewDeviceNotificationProvider.swift in Sources */, 7A6389E12B7E3BD6008E77E1 /* CustomListSectionIdentifier.swift in Sources */, 58CEB2F32AFD0BA100E6E088 /* TextCellContentView.swift in Sources */, 7A6389E72B7E42BE008E77E1 /* CustomListViewController.swift in Sources */, diff --git a/ios/MullvadVPN/AppDelegate.swift b/ios/MullvadVPN/AppDelegate.swift index d6620c992ca1..a598476cebc3 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 9f2d6a92b1aa..a7753589ba0b 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 e2f2e6d32db9..3f51f8cdb3e8 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 3fb5cb804840..cfa1d1bb719d 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 778900dec38a..41ac5343b619 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 7ed81c1e772a..5839b62c0235 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 000000000000..2e72f45545d1 --- /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 similarity index 94% rename from ios/MullvadVPN/Notifications/Notification Providers/RegisteredDeviceInAppNotificationProvider.swift rename to ios/MullvadVPN/Notifications/Notification Providers/NewDeviceNotificationProvider.swift index ade1b0eb201a..66b76f9116fc 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 e15ec4b01e1e..155d0f7bdb4d 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 32449fa59d4e..7dea419a73e0 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 b29e3d2baeec..f0e9a82e57cf 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 ff5334d8535d..82327b24ba60 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 4131d1333fad..1b4e091fced9 100644 --- a/ios/MullvadVPN/View controllers/ChangeLog/ChangeLogView.swift +++ b/ios/MullvadVPN/View controllers/ChangeLog/ChangeLogView.swift @@ -32,19 +32,6 @@ struct ChangeLogView: 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) } diff --git a/ios/MullvadVPNUITests/Base/BaseUITestCase.swift b/ios/MullvadVPNUITests/Base/BaseUITestCase.swift index 7af7de7030f3..bb3f7b860c25 100644 --- a/ios/MullvadVPNUITests/Base/BaseUITestCase.swift +++ b/ios/MullvadVPNUITests/Base/BaseUITestCase.swift @@ -236,21 +236,6 @@ class BaseUITestCase: XCTestCase { } } - func dismissChangeLogIfShown() { - let changeLogIsShown = app - .otherElements[.changeLogAlert] - .waitForExistence(timeout: Self.shortTimeout) - - if changeLogIsShown { - ChangeLogAlert(app).swipeDownToDismissModal() - } - - // Ensure changelog is no longer shown - _ = app - .otherElements[.changeLogAlert] - .waitForNonExistence(timeout: Self.shortTimeout) - } - /// Login with specified account number. It is a prerequisite that the login page is currently shown. func login(accountNumber: String) { var successIconShown = false diff --git a/ios/MullvadVPNUITests/Base/LoggedInWithTimeUITestCase.swift b/ios/MullvadVPNUITests/Base/LoggedInWithTimeUITestCase.swift index 2b9b66eab43b..9b13308fefb8 100644 --- a/ios/MullvadVPNUITests/Base/LoggedInWithTimeUITestCase.swift +++ b/ios/MullvadVPNUITests/Base/LoggedInWithTimeUITestCase.swift @@ -19,7 +19,6 @@ class LoggedInWithTimeUITestCase: BaseUITestCase { hasTimeAccountNumber = getAccountWithTime() agreeToTermsOfServiceIfShown() - dismissChangeLogIfShown() logoutIfLoggedIn() guard let hasTimeAccountNumber = self.hasTimeAccountNumber else { diff --git a/ios/MullvadVPNUITests/Base/LoggedOutUITestCase.swift b/ios/MullvadVPNUITests/Base/LoggedOutUITestCase.swift index 64d04142962f..a89c6e732ef2 100644 --- a/ios/MullvadVPNUITests/Base/LoggedOutUITestCase.swift +++ b/ios/MullvadVPNUITests/Base/LoggedOutUITestCase.swift @@ -14,7 +14,6 @@ class LoggedOutUITestCase: BaseUITestCase { super.setUp() agreeToTermsOfServiceIfShown() - dismissChangeLogIfShown() logoutIfLoggedIn() // Relaunch app so that tests start from a deterministic state diff --git a/ios/MullvadVPNUITests/SettingsMigrationTests.swift b/ios/MullvadVPNUITests/SettingsMigrationTests.swift index b1a7e22575c0..9ec514eb6ee3 100644 --- a/ios/MullvadVPNUITests/SettingsMigrationTests.swift +++ b/ios/MullvadVPNUITests/SettingsMigrationTests.swift @@ -47,7 +47,6 @@ class SettingsMigrationTests: BaseUITestCase { super.setUp() agreeToTermsOfServiceIfShown() - dismissChangeLogIfShown() // Relaunch app so that tests start from a deterministic state app.terminate()