summaryrefslogtreecommitdiffhomepage
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
parent060839d420a9cf222b49fe4932730a98fd5b1434 (diff)
parent5f9315b46dc7a364bc20d40420c2e0feb34a2d6c (diff)
downloadmullvadvpn-a1b47c23a9532abc0f51fc94de481b0528afb9fb.tar.xz
mullvadvpn-a1b47c23a9532abc0f51fc94de481b0528afb9fb.zip
Merge branch 'add-in-app-notification-banner-for-changelog-ios-989'
-rw-r--r--ios/CHANGELOG.md2
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj19
-rw-r--r--ios/MullvadVPN/AppDelegate.swift4
-rw-r--r--ios/MullvadVPN/Classes/AppRoutes.swift8
-rw-r--r--ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift27
-rw-r--r--ios/MullvadVPN/Coordinators/ChangeLogCoordinator.swift39
-rw-r--r--ios/MullvadVPN/Coordinators/Settings/SettingsViewControllerFactory.swift1
-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
-rw-r--r--ios/MullvadVPN/SceneDelegate.swift2
-rw-r--r--ios/MullvadVPN/View controllers/ChangeLog/ChangeLogView.swift13
-rw-r--r--ios/MullvadVPNUITests/Base/BaseUITestCase.swift15
-rw-r--r--ios/MullvadVPNUITests/Base/LoggedInWithTimeUITestCase.swift1
-rw-r--r--ios/MullvadVPNUITests/Base/LoggedOutUITestCase.swift1
-rw-r--r--ios/MullvadVPNUITests/SettingsMigrationTests.swift1
19 files changed, 191 insertions, 89 deletions
diff --git a/ios/CHANGELOG.md b/ios/CHANGELOG.md
index 01f57d4267..5dd7d2a643 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
+- Move changelog to settings and add an in-app notification banner for app update.
## [2025.1 - 2025-01-14]
### Added
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj
index 2d714acb6d..ac7b7bc054 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 = "<group>"; };
F07BF2572A26112D00042943 /* InputTextFormatterTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InputTextFormatterTests.swift; sourceTree = "<group>"; };
F07BF2612A26279100042943 /* RedeemVoucherOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RedeemVoucherOperation.swift; sourceTree = "<group>"; };
- F07CFF1F29F2720E008C0343 /* RegisteredDeviceInAppNotificationProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegisteredDeviceInAppNotificationProvider.swift; sourceTree = "<group>"; };
+ F07CFF1F29F2720E008C0343 /* NewDeviceNotificationProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewDeviceNotificationProvider.swift; sourceTree = "<group>"; };
F09084672C6E88ED001CD36E /* DaitaPromptAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DaitaPromptAlert.swift; sourceTree = "<group>"; };
F09A29782A9F8A9B00EA3B6F /* LogoutDialogueView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LogoutDialogueView.swift; sourceTree = "<group>"; };
F09A29792A9F8A9B00EA3B6F /* VoucherTextField.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VoucherTextField.swift; sourceTree = "<group>"; };
@@ -2286,6 +2288,7 @@
F0C4C9BF2C495E7500A79006 /* EphemeralPeerExchangeActorStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EphemeralPeerExchangeActorStub.swift; sourceTree = "<group>"; };
F0C6A8422AB08E54000777A8 /* RedeemVoucherViewConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RedeemVoucherViewConfiguration.swift; sourceTree = "<group>"; };
F0C6FA842A6A733700F521F0 /* InAppPurchaseInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InAppPurchaseInteractor.swift; sourceTree = "<group>"; };
+ F0D5591D2D3805050072B63F /* LatestChangesNotificationProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LatestChangesNotificationProvider.swift; sourceTree = "<group>"; };
F0D8825A2B04F53600D3EF9A /* OutgoingConnectionData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OutgoingConnectionData.swift; sourceTree = "<group>"; };
F0DA87462A9CB9A2006044F1 /* AccountExpiryRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountExpiryRow.swift; sourceTree = "<group>"; };
F0DA87482A9CBA9F006044F1 /* AccountDeviceRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountDeviceRow.swift; sourceTree = "<group>"; };
@@ -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 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)
}
diff --git a/ios/MullvadVPNUITests/Base/BaseUITestCase.swift b/ios/MullvadVPNUITests/Base/BaseUITestCase.swift
index 7af7de7030..bb3f7b860c 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 2b9b66eab4..9b13308fef 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 64d0414296..a89c6e732e 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 b1a7e22575..9ec514eb6e 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()