Skip to content

Commit

Permalink
Add in-app notification for latest changes
Browse files Browse the repository at this point in the history
  • Loading branch information
mojganii committed Jan 22, 2025
1 parent 060839d commit eb21418
Show file tree
Hide file tree
Showing 19 changed files with 191 additions and 89 deletions.
2 changes: 2 additions & 0 deletions ios/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
19 changes: 13 additions & 6 deletions ios/MullvadVPN.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -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 */; };
Expand Down Expand Up @@ -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 */; };
Expand Down Expand Up @@ -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>"; };
Expand Down Expand Up @@ -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>"; };
Expand Down Expand Up @@ -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";
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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 */,
Expand All @@ -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 */,
Expand Down
4 changes: 3 additions & 1 deletion ios/MullvadVPN/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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
}
Expand Down
8 changes: 3 additions & 5 deletions ios/MullvadVPN/Classes/AppRoutes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ enum AppRoute: AppRouteProtocol {
case selectLocation

/**
Changelog route.
Changelog standalone route (not subsetting).
*/
case changelog

Expand All @@ -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
Expand All @@ -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)
Expand Down
27 changes: 8 additions & 19 deletions ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down Expand Up @@ -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)
}
}
Expand Down Expand Up @@ -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
}
}
Expand All @@ -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
}
39 changes: 34 additions & 5 deletions ios/MullvadVPN/Coordinators/ChangeLogCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ struct SettingsViewControllerFactory {
private func makeChangelogCoordinator() -> MakeChildResult {
return .childCoordinator(
ChangeLogCoordinator(
route: .settings(.changelog),
navigationController: navigationController,
viewModel: ChangeLogViewModel(changeLogReader: ChangeLogReader())
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading

0 comments on commit eb21418

Please sign in to comment.