Skip to content

Commit a1b47c2

Browse files
committed
Merge branch 'add-in-app-notification-banner-for-changelog-ios-989'
2 parents 060839d + 5f9315b commit a1b47c2

19 files changed

+191
-89
lines changed

ios/CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ Line wrap the file at 100 chars. Th
2424
## Unreleased
2525
### Fixed
2626
- Broken DAITA settings view on iOS 15.
27+
### Changed
28+
- Move changelog to settings and add an in-app notification banner for app update.
2729

2830
## [2025.1 - 2025-01-14]
2931
### Added

ios/MullvadVPN.xcodeproj/project.pbxproj

+13-6
Original file line numberDiff line numberDiff line change
@@ -784,7 +784,7 @@
784784
A9A5F9F12ACB05160083449F /* String+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5807E2BF2432038B00F5FF30 /* String+Helpers.swift */; };
785785
A9A5F9F22ACB05160083449F /* NotificationConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C8191729FAA2C400DEB1B4 /* NotificationConfiguration.swift */; };
786786
A9A5F9F32ACB05160083449F /* AccountExpirySystemNotificationProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587B75402668FD7700DEF7E9 /* AccountExpirySystemNotificationProvider.swift */; };
787-
A9A5F9F52ACB05160083449F /* RegisteredDeviceInAppNotificationProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = F07CFF1F29F2720E008C0343 /* RegisteredDeviceInAppNotificationProvider.swift */; };
787+
A9A5F9F52ACB05160083449F /* NewDeviceNotificationProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = F07CFF1F29F2720E008C0343 /* NewDeviceNotificationProvider.swift */; };
788788
A9A5F9F62ACB05160083449F /* TunnelStatusNotificationProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58A94AE326CFD945001CB97C /* TunnelStatusNotificationProvider.swift */; };
789789
A9A5F9F72ACB05160083449F /* NotificationProviderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58B26E232943520C00D5980C /* NotificationProviderProtocol.swift */; };
790790
A9A5F9F82ACB05160083449F /* NotificationProviderIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0C2AEFC2A0BB5CC00986207 /* NotificationProviderIdentifier.swift */; };
@@ -954,7 +954,7 @@
954954
F07B53572C53B5270024F547 /* LocalNetworkIPs.swift in Sources */ = {isa = PBXBuildFile; fileRef = F07B53562C53B5270024F547 /* LocalNetworkIPs.swift */; };
955955
F07BF2622A26279100042943 /* RedeemVoucherOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = F07BF2612A26279100042943 /* RedeemVoucherOperation.swift */; };
956956
F07C9D952B220C77006F1C5E /* libmullvad_ios.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 01F1FF1D29F0627D007083C3 /* libmullvad_ios.a */; };
957-
F07CFF2029F2720E008C0343 /* RegisteredDeviceInAppNotificationProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = F07CFF1F29F2720E008C0343 /* RegisteredDeviceInAppNotificationProvider.swift */; };
957+
F07CFF2029F2720E008C0343 /* NewDeviceNotificationProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = F07CFF1F29F2720E008C0343 /* NewDeviceNotificationProvider.swift */; };
958958
F07F63CE2C63E5790027A351 /* AccessMethodRepository+Stub.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0164EB92B4456D30020268D /* AccessMethodRepository+Stub.swift */; };
959959
F08827872B318C840020A383 /* ShadowsocksCipherOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58DFF7D92B02862E00F864E0 /* ShadowsocksCipherOptions.swift */; };
960960
F08827882B318F960020A383 /* PersistentAccessMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 586C0D962B04E0AC00E7CDD7 /* PersistentAccessMethod.swift */; };
@@ -1023,6 +1023,8 @@
10231023
F0C3333C2B31A29C00D1A478 /* MullvadSettings.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 58B2FDD32AA71D2A003EB5C6 /* MullvadSettings.framework */; };
10241024
F0C6A8432AB08E54000777A8 /* RedeemVoucherViewConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0C6A8422AB08E54000777A8 /* RedeemVoucherViewConfiguration.swift */; };
10251025
F0C6FA852A6A733700F521F0 /* InAppPurchaseInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0C6FA842A6A733700F521F0 /* InAppPurchaseInteractor.swift */; };
1026+
F0D5591E2D38051C0072B63F /* LatestChangesNotificationProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0D5591D2D3805050072B63F /* LatestChangesNotificationProvider.swift */; };
1027+
F0D5591F2D38051C0072B63F /* LatestChangesNotificationProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0D5591D2D3805050072B63F /* LatestChangesNotificationProvider.swift */; };
10261028
F0D7FF8F2B31DF5900E0FDE5 /* AccessMethodRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5827B0A02B0E064E00CCBBA1 /* AccessMethodRepository.swift */; };
10271029
F0D7FF902B31E00B00E0FDE5 /* AccessMethodKind.swift in Sources */ = {isa = PBXBuildFile; fileRef = 588D7ED72AF3A533005DF40A /* AccessMethodKind.swift */; };
10281030
F0D8825B2B04F53600D3EF9A /* OutgoingConnectionData.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0D8825A2B04F53600D3EF9A /* OutgoingConnectionData.swift */; };
@@ -2241,7 +2243,7 @@
22412243
F07B53562C53B5270024F547 /* LocalNetworkIPs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalNetworkIPs.swift; sourceTree = "<group>"; };
22422244
F07BF2572A26112D00042943 /* InputTextFormatterTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InputTextFormatterTests.swift; sourceTree = "<group>"; };
22432245
F07BF2612A26279100042943 /* RedeemVoucherOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RedeemVoucherOperation.swift; sourceTree = "<group>"; };
2244-
F07CFF1F29F2720E008C0343 /* RegisteredDeviceInAppNotificationProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegisteredDeviceInAppNotificationProvider.swift; sourceTree = "<group>"; };
2246+
F07CFF1F29F2720E008C0343 /* NewDeviceNotificationProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewDeviceNotificationProvider.swift; sourceTree = "<group>"; };
22452247
F09084672C6E88ED001CD36E /* DaitaPromptAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DaitaPromptAlert.swift; sourceTree = "<group>"; };
22462248
F09A29782A9F8A9B00EA3B6F /* LogoutDialogueView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LogoutDialogueView.swift; sourceTree = "<group>"; };
22472249
F09A29792A9F8A9B00EA3B6F /* VoucherTextField.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VoucherTextField.swift; sourceTree = "<group>"; };
@@ -2286,6 +2288,7 @@
22862288
F0C4C9BF2C495E7500A79006 /* EphemeralPeerExchangeActorStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EphemeralPeerExchangeActorStub.swift; sourceTree = "<group>"; };
22872289
F0C6A8422AB08E54000777A8 /* RedeemVoucherViewConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RedeemVoucherViewConfiguration.swift; sourceTree = "<group>"; };
22882290
F0C6FA842A6A733700F521F0 /* InAppPurchaseInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InAppPurchaseInteractor.swift; sourceTree = "<group>"; };
2291+
F0D5591D2D3805050072B63F /* LatestChangesNotificationProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LatestChangesNotificationProvider.swift; sourceTree = "<group>"; };
22892292
F0D8825A2B04F53600D3EF9A /* OutgoingConnectionData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OutgoingConnectionData.swift; sourceTree = "<group>"; };
22902293
F0DA87462A9CB9A2006044F1 /* AccountExpiryRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountExpiryRow.swift; sourceTree = "<group>"; };
22912294
F0DA87482A9CBA9F006044F1 /* AccountDeviceRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountDeviceRow.swift; sourceTree = "<group>"; };
@@ -3382,8 +3385,9 @@
33823385
7A6F2FA62AFBB9AE006D0856 /* AccountExpiry.swift */,
33833386
58607A4C2947287800BC467D /* AccountExpiryInAppNotificationProvider.swift */,
33843387
587B75402668FD7700DEF7E9 /* AccountExpirySystemNotificationProvider.swift */,
3388+
F0D5591D2D3805050072B63F /* LatestChangesNotificationProvider.swift */,
3389+
F07CFF1F29F2720E008C0343 /* NewDeviceNotificationProvider.swift */,
33853390
58C8191729FAA2C400DEB1B4 /* NotificationConfiguration.swift */,
3386-
F07CFF1F29F2720E008C0343 /* RegisteredDeviceInAppNotificationProvider.swift */,
33873391
58A94AE326CFD945001CB97C /* TunnelStatusNotificationProvider.swift */,
33883392
);
33893393
path = "Notification Providers";
@@ -5620,7 +5624,7 @@
56205624
A9A5F9F12ACB05160083449F /* String+Helpers.swift in Sources */,
56215625
A9A5F9F22ACB05160083449F /* NotificationConfiguration.swift in Sources */,
56225626
A9A5F9F32ACB05160083449F /* AccountExpirySystemNotificationProvider.swift in Sources */,
5623-
A9A5F9F52ACB05160083449F /* RegisteredDeviceInAppNotificationProvider.swift in Sources */,
5627+
A9A5F9F52ACB05160083449F /* NewDeviceNotificationProvider.swift in Sources */,
56245628
F09D04B72AE941DA003D4F89 /* OutgoingConnectionProxyTests.swift in Sources */,
56255629
F09D04B92AE95111003D4F89 /* OutgoingConnectionProxy.swift in Sources */,
56265630
7A6000F92B6273A4001CF0D9 /* AccessMethodViewModel.swift in Sources */,
@@ -5715,6 +5719,7 @@
57155719
A9A5FA2C2ACB05160083449F /* DeviceCheckOperationTests.swift in Sources */,
57165720
A9A5FA2D2ACB05160083449F /* DurationTests.swift in Sources */,
57175721
A9A5FA2E2ACB05160083449F /* FileCacheTests.swift in Sources */,
5722+
F0D5591F2D38051C0072B63F /* LatestChangesNotificationProvider.swift in Sources */,
57185723
7A9F28FC2CA69D0C005F2089 /* DAITASettingsTests.swift in Sources */,
57195724
A9A5FA2F2ACB05160083449F /* FixedWidthIntegerArithmeticsTests.swift in Sources */,
57205725
7AA513862BC91C6B00D081A4 /* LogRotationTests.swift in Sources */,
@@ -6092,6 +6097,8 @@
60926097
5868585524054096000B8131 /* CustomButton.swift in Sources */,
60936098
58E25F812837BBBB002CFB2C /* SceneDelegate.swift in Sources */,
60946099
7A1A26492A29D48A00B978AA /* RelayFilterCellFactory.swift in Sources */,
6100+
5867771629097C5B006F721F /* ProductState.swift in Sources */,
6101+
F0D5591E2D38051C0072B63F /* LatestChangesNotificationProvider.swift in Sources */,
60956102
7A28826A2BA8336600FD9F20 /* VPNSettingsCoordinator.swift in Sources */,
60966103
7A6389DE2B7E3BD6008E77E1 /* CustomListItemIdentifier.swift in Sources */,
60976104
58C76A082A33850E00100D75 /* ApplicationTarget.swift in Sources */,
@@ -6114,7 +6121,7 @@
61146121
58F3C0A4249CB069003E76BE /* HeaderBarView.swift in Sources */,
61156122
5864AF0829C78849005B0CD9 /* CellFactoryProtocol.swift in Sources */,
61166123
7A6389E22B7E3BD6008E77E1 /* CustomListInteractor.swift in Sources */,
6117-
F07CFF2029F2720E008C0343 /* RegisteredDeviceInAppNotificationProvider.swift in Sources */,
6124+
F07CFF2029F2720E008C0343 /* NewDeviceNotificationProvider.swift in Sources */,
61186125
7A6389E12B7E3BD6008E77E1 /* CustomListSectionIdentifier.swift in Sources */,
61196126
58CEB2F32AFD0BA100E6E088 /* TextCellContentView.swift in Sources */,
61206127
7A6389E72B7E42BE008E77E1 /* CustomListViewController.swift in Sources */,

ios/MullvadVPN/AppDelegate.swift

+3-1
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
4646
private var migrationManager: MigrationManager!
4747

4848
nonisolated(unsafe) private(set) var accessMethodRepository = AccessMethodRepository()
49+
private(set) var appPreferences = AppPreferences()
4950
private(set) var shadowsocksLoader: ShadowsocksLoaderProtocol!
5051
private(set) var configuredTransportProvider: ProxyConfigurationTransportProvider!
5152
private(set) var ipOverrideRepository = IPOverrideRepository()
@@ -450,10 +451,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
450451

451452
private func setupNotifications() {
452453
NotificationManager.shared.notificationProviders = [
454+
LatestChangesNotificationProvider(appPreferences: appPreferences),
453455
TunnelStatusNotificationProvider(tunnelManager: tunnelManager),
454456
AccountExpirySystemNotificationProvider(tunnelManager: tunnelManager),
455457
AccountExpiryInAppNotificationProvider(tunnelManager: tunnelManager),
456-
RegisteredDeviceInAppNotificationProvider(tunnelManager: tunnelManager),
458+
NewDeviceNotificationProvider(tunnelManager: tunnelManager),
457459
]
458460
UNUserNotificationCenter.current().delegate = self
459461
}

ios/MullvadVPN/Classes/AppRoutes.swift

+3-5
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ enum AppRoute: AppRouteProtocol {
9393
case selectLocation
9494

9595
/**
96-
Changelog route.
96+
Changelog standalone route (not subsetting).
9797
*/
9898
case changelog
9999

@@ -110,7 +110,7 @@ enum AppRoute: AppRouteProtocol {
110110

111111
var isExclusive: Bool {
112112
switch self {
113-
case .account, .settings, .changelog, .alert:
113+
case .account, .settings, .alert:
114114
return true
115115
default:
116116
return false
@@ -129,13 +129,11 @@ enum AppRoute: AppRouteProtocol {
129129
switch self {
130130
case .tos, .login, .main, .revoked, .outOfTime, .welcome:
131131
return .primary
132-
case .changelog:
133-
return .changelog
134132
case .selectLocation:
135133
return .selectLocation
136134
case .account:
137135
return .account
138-
case .settings, .daita:
136+
case .settings, .daita, .changelog:
139137
return .settings
140138
case let .alert(id):
141139
return .alert(id)

ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift

+8-19
Original file line numberDiff line numberDiff line change
@@ -296,11 +296,6 @@ final class ApplicationCoordinator: Coordinator, Presenting, @preconcurrency Roo
296296
}
297297
}
298298

299-
// Change log can be presented simultaneously with other routes.
300-
if !appPreferences.hasSeenLastChanges {
301-
routes.append(.changelog)
302-
}
303-
304299
return routes
305300
}
306301

@@ -336,14 +331,18 @@ final class ApplicationCoordinator: Coordinator, Presenting, @preconcurrency Roo
336331

337332
private func presentChangeLog(animated: Bool, completion: @escaping (Coordinator) -> Void) {
338333
let coordinator = ChangeLogCoordinator(
334+
route: .changelog,
339335
navigationController: CustomNavigationController(),
340336
viewModel: ChangeLogViewModel(changeLogReader: ChangeLogReader())
341337
)
342338

339+
coordinator.didFinish = { [weak self] _ in
340+
self?.router.dismiss(.changelog, animated: animated)
341+
}
342+
343343
coordinator.start(animated: false)
344344

345-
presentChild(coordinator, animated: animated) { [weak self] in
346-
self?.appPreferences.markChangeLogSeen()
345+
presentChild(coordinator, animated: animated) {
347346
completion(coordinator)
348347
}
349348
}
@@ -820,6 +819,8 @@ final class ApplicationCoordinator: Coordinator, Presenting, @preconcurrency Roo
820819
case .accountExpiryInAppNotification:
821820
isPresentingAccountExpiryBanner = false
822821
updateDeviceInfo(deviceState: tunnelManager.deviceState)
822+
case .latestChangesInAppNotificationProvider:
823+
router.present(.changelog)
823824
default: return
824825
}
825826
}
@@ -836,15 +837,3 @@ extension DeviceState {
836837
isLoggedIn ? UISplitViewController.DisplayMode.oneBesideSecondary : .secondaryOnly
837838
}
838839
}
839-
840-
fileprivate extension AppPreferencesDataSource {
841-
var hasSeenLastChanges: Bool {
842-
lastSeenChangeLogVersion == Bundle.main.shortVersion
843-
}
844-
845-
mutating func markChangeLogSeen() {
846-
lastSeenChangeLogVersion = Bundle.main.shortVersion
847-
}
848-
849-
// swiftlint:disable:next file_length
850-
}

ios/MullvadVPN/Coordinators/ChangeLogCoordinator.swift

+34-5
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,21 @@ import SwiftUI
1212
import UIKit
1313

1414
final class ChangeLogCoordinator: Coordinator, Presentable, SettingsChildCoordinator {
15-
private var navigationController: UINavigationController?
15+
private let route: AppRoute
1616
private let viewModel: ChangeLogViewModel
17+
private var navigationController: UINavigationController?
18+
var didFinish: ((ChangeLogCoordinator) -> Void)?
1719

1820
var presentedViewController: UIViewController {
19-
return navigationController!
21+
navigationController!
2022
}
2123

22-
init(navigationController: UINavigationController, viewModel: ChangeLogViewModel) {
24+
init(
25+
route: AppRoute,
26+
navigationController: UINavigationController,
27+
viewModel: ChangeLogViewModel
28+
) {
29+
self.route = route
2330
self.viewModel = viewModel
2431
self.navigationController = navigationController
2532
}
@@ -33,8 +40,30 @@ final class ChangeLogCoordinator: Coordinator, Presentable, SettingsChildCoordin
3340
value: "What's new",
3441
comment: ""
3542
)
36-
changeLogViewController.navigationItem.largeTitleDisplayMode = .always
37-
navigationController?.navigationBar.prefersLargeTitles = true
43+
44+
switch route {
45+
case .changelog:
46+
let barButtonItem = UIBarButtonItem(
47+
title: NSLocalizedString(
48+
"CHANGELOG_NAVIGATION_DONE_BUTTON",
49+
tableName: "Changelog",
50+
value: "Done",
51+
comment: ""
52+
),
53+
primaryAction: UIAction { [weak self] _ in
54+
guard let self else { return }
55+
didFinish?(self)
56+
}
57+
)
58+
barButtonItem.style = .done
59+
changeLogViewController.navigationItem.rightBarButtonItem = barButtonItem
60+
fallthrough
61+
case .settings:
62+
changeLogViewController.navigationItem.largeTitleDisplayMode = .always
63+
navigationController?.navigationBar.prefersLargeTitles = true
64+
default: break
65+
}
66+
3867
navigationController?.pushViewController(changeLogViewController, animated: animated)
3968
}
4069
}

ios/MullvadVPN/Coordinators/Settings/SettingsViewControllerFactory.swift

+1
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ struct SettingsViewControllerFactory {
9898
private func makeChangelogCoordinator() -> MakeChildResult {
9999
return .childCoordinator(
100100
ChangeLogCoordinator(
101+
route: .settings(.changelog),
101102
navigationController: navigationController,
102103
viewModel: ChangeLogViewModel(changeLogReader: ChangeLogReader())
103104
)

ios/MullvadVPN/Notifications/InAppNotificationDescriptor.swift

+4-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,10 @@ struct InAppNotificationDescriptor: Equatable {
2424
var body: NSAttributedString
2525

2626
/// Notification action.
27-
var action: InAppNotificationAction?
27+
var button: InAppNotificationAction?
28+
29+
/// Notification tap action (optional).
30+
var tapAction: InAppNotificationAction?
2831
}
2932

3033
/// Type describing a specific in-app notification action.

0 commit comments

Comments
 (0)