summaryrefslogtreecommitdiffhomepage
path: root/ios/MullvadVPN
diff options
context:
space:
mode:
Diffstat (limited to 'ios/MullvadVPN')
-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
13 files changed, 176 insertions, 65 deletions
diff --git a/ios/MullvadVPN/AppDelegate.swift b/ios/MullvadVPN/AppDelegate.swift
index d6620c992c..a598476ceb 100644
--- a/ios/MullvadVPN/AppDelegate.swift
+++ b/ios/MullvadVPN/AppDelegate.swift
@@ -46,6 +46,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
private var migrationManager: MigrationManager!
nonisolated(unsafe) private(set) var accessMethodRepository = AccessMethodRepository()
+ private(set) var appPreferences = AppPreferences()
private(set) var shadowsocksLoader: ShadowsocksLoaderProtocol!
private(set) var configuredTransportProvider: ProxyConfigurationTransportProvider!
private(set) var ipOverrideRepository = IPOverrideRepository()
@@ -450,10 +451,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
private func setupNotifications() {
NotificationManager.shared.notificationProviders = [
+ LatestChangesNotificationProvider(appPreferences: appPreferences),
TunnelStatusNotificationProvider(tunnelManager: tunnelManager),
AccountExpirySystemNotificationProvider(tunnelManager: tunnelManager),
AccountExpiryInAppNotificationProvider(tunnelManager: tunnelManager),
- RegisteredDeviceInAppNotificationProvider(tunnelManager: tunnelManager),
+ NewDeviceNotificationProvider(tunnelManager: tunnelManager),
]
UNUserNotificationCenter.current().delegate = self
}
diff --git a/ios/MullvadVPN/Classes/AppRoutes.swift b/ios/MullvadVPN/Classes/AppRoutes.swift
index 9f2d6a92b1..a7753589ba 100644
--- a/ios/MullvadVPN/Classes/AppRoutes.swift
+++ b/ios/MullvadVPN/Classes/AppRoutes.swift
@@ -93,7 +93,7 @@ enum AppRoute: AppRouteProtocol {
case selectLocation
/**
- Changelog route.
+ Changelog standalone route (not subsetting).
*/
case changelog
@@ -110,7 +110,7 @@ enum AppRoute: AppRouteProtocol {
var isExclusive: Bool {
switch self {
- case .account, .settings, .changelog, .alert:
+ case .account, .settings, .alert:
return true
default:
return false
@@ -129,13 +129,11 @@ enum AppRoute: AppRouteProtocol {
switch self {
case .tos, .login, .main, .revoked, .outOfTime, .welcome:
return .primary
- case .changelog:
- return .changelog
case .selectLocation:
return .selectLocation
case .account:
return .account
- case .settings, .daita:
+ case .settings, .daita, .changelog:
return .settings
case let .alert(id):
return .alert(id)
diff --git a/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift b/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift
index e2f2e6d32d..3f51f8cdb3 100644
--- a/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift
+++ b/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift
@@ -296,11 +296,6 @@ final class ApplicationCoordinator: Coordinator, Presenting, @preconcurrency Roo
}
}
- // Change log can be presented simultaneously with other routes.
- if !appPreferences.hasSeenLastChanges {
- routes.append(.changelog)
- }
-
return routes
}
@@ -336,14 +331,18 @@ final class ApplicationCoordinator: Coordinator, Presenting, @preconcurrency Roo
private func presentChangeLog(animated: Bool, completion: @escaping (Coordinator) -> Void) {
let coordinator = ChangeLogCoordinator(
+ route: .changelog,
navigationController: CustomNavigationController(),
viewModel: ChangeLogViewModel(changeLogReader: ChangeLogReader())
)
+ coordinator.didFinish = { [weak self] _ in
+ self?.router.dismiss(.changelog, animated: animated)
+ }
+
coordinator.start(animated: false)
- presentChild(coordinator, animated: animated) { [weak self] in
- self?.appPreferences.markChangeLogSeen()
+ presentChild(coordinator, animated: animated) {
completion(coordinator)
}
}
@@ -820,6 +819,8 @@ final class ApplicationCoordinator: Coordinator, Presenting, @preconcurrency Roo
case .accountExpiryInAppNotification:
isPresentingAccountExpiryBanner = false
updateDeviceInfo(deviceState: tunnelManager.deviceState)
+ case .latestChangesInAppNotificationProvider:
+ router.present(.changelog)
default: return
}
}
@@ -836,15 +837,3 @@ extension DeviceState {
isLoggedIn ? UISplitViewController.DisplayMode.oneBesideSecondary : .secondaryOnly
}
}
-
-fileprivate extension AppPreferencesDataSource {
- var hasSeenLastChanges: Bool {
- lastSeenChangeLogVersion == Bundle.main.shortVersion
- }
-
- mutating func markChangeLogSeen() {
- lastSeenChangeLogVersion = Bundle.main.shortVersion
- }
-
- // swiftlint:disable:next file_length
-}
diff --git a/ios/MullvadVPN/Coordinators/ChangeLogCoordinator.swift b/ios/MullvadVPN/Coordinators/ChangeLogCoordinator.swift
index 3fb5cb8048..cfa1d1bb71 100644
--- a/ios/MullvadVPN/Coordinators/ChangeLogCoordinator.swift
+++ b/ios/MullvadVPN/Coordinators/ChangeLogCoordinator.swift
@@ -12,14 +12,21 @@ import SwiftUI
import UIKit
final class ChangeLogCoordinator: Coordinator, Presentable, SettingsChildCoordinator {
- private var navigationController: UINavigationController?
+ private let route: AppRoute
private let viewModel: ChangeLogViewModel
+ private var navigationController: UINavigationController?
+ var didFinish: ((ChangeLogCoordinator) -> Void)?
var presentedViewController: UIViewController {
- return navigationController!
+ navigationController!
}
- init(navigationController: UINavigationController, viewModel: ChangeLogViewModel) {
+ init(
+ route: AppRoute,
+ navigationController: UINavigationController,
+ viewModel: ChangeLogViewModel
+ ) {
+ self.route = route
self.viewModel = viewModel
self.navigationController = navigationController
}
@@ -33,8 +40,30 @@ final class ChangeLogCoordinator: Coordinator, Presentable, SettingsChildCoordin
value: "What's new",
comment: ""
)
- changeLogViewController.navigationItem.largeTitleDisplayMode = .always
- navigationController?.navigationBar.prefersLargeTitles = true
+
+ switch route {
+ case .changelog:
+ let barButtonItem = UIBarButtonItem(
+ title: NSLocalizedString(
+ "CHANGELOG_NAVIGATION_DONE_BUTTON",
+ tableName: "Changelog",
+ value: "Done",
+ comment: ""
+ ),
+ primaryAction: UIAction { [weak self] _ in
+ guard let self else { return }
+ didFinish?(self)
+ }
+ )
+ barButtonItem.style = .done
+ changeLogViewController.navigationItem.rightBarButtonItem = barButtonItem
+ fallthrough
+ case .settings:
+ changeLogViewController.navigationItem.largeTitleDisplayMode = .always
+ navigationController?.navigationBar.prefersLargeTitles = true
+ default: break
+ }
+
navigationController?.pushViewController(changeLogViewController, animated: animated)
}
}
diff --git a/ios/MullvadVPN/Coordinators/Settings/SettingsViewControllerFactory.swift b/ios/MullvadVPN/Coordinators/Settings/SettingsViewControllerFactory.swift
index 778900dec3..41ac5343b6 100644
--- a/ios/MullvadVPN/Coordinators/Settings/SettingsViewControllerFactory.swift
+++ b/ios/MullvadVPN/Coordinators/Settings/SettingsViewControllerFactory.swift
@@ -98,6 +98,7 @@ struct SettingsViewControllerFactory {
private func makeChangelogCoordinator() -> MakeChildResult {
return .childCoordinator(
ChangeLogCoordinator(
+ route: .settings(.changelog),
navigationController: navigationController,
viewModel: ChangeLogViewModel(changeLogReader: ChangeLogReader())
)
diff --git a/ios/MullvadVPN/Notifications/InAppNotificationDescriptor.swift b/ios/MullvadVPN/Notifications/InAppNotificationDescriptor.swift
index 7ed81c1e77..5839b62c02 100644
--- a/ios/MullvadVPN/Notifications/InAppNotificationDescriptor.swift
+++ b/ios/MullvadVPN/Notifications/InAppNotificationDescriptor.swift
@@ -24,7 +24,10 @@ struct InAppNotificationDescriptor: Equatable {
var body: NSAttributedString
/// Notification action.
- var action: InAppNotificationAction?
+ var button: InAppNotificationAction?
+
+ /// Notification tap action (optional).
+ var tapAction: InAppNotificationAction?
}
/// Type describing a specific in-app notification action.
diff --git a/ios/MullvadVPN/Notifications/Notification Providers/LatestChangesNotificationProvider.swift b/ios/MullvadVPN/Notifications/Notification Providers/LatestChangesNotificationProvider.swift
new file mode 100644
index 0000000000..2e72f45545
--- /dev/null
+++ b/ios/MullvadVPN/Notifications/Notification Providers/LatestChangesNotificationProvider.swift
@@ -0,0 +1,85 @@
+//
+// LatestChangesNotificationProvider.swift
+// MullvadVPN
+//
+// Created by Mojgan on 2025-01-15.
+// Copyright © 2025 Mullvad VPN AB. All rights reserved.
+//
+import Foundation
+import UIKit
+
+class LatestChangesNotificationProvider: NotificationProvider, InAppNotificationProvider, @unchecked Sendable {
+ private var appPreferences: AppPreferencesDataSource
+ private let appVersion: String = Bundle.main.productVersion
+
+ init(appPreferences: AppPreferencesDataSource) {
+ self.appPreferences = appPreferences
+ }
+
+ var shouldShowNotification: Bool {
+ // If this is the first installation, no notification will be shown.
+ guard !appPreferences.lastSeenChangeLogVersion.isEmpty else { return false }
+ // Display the notification only if the app is updated from a previously installed version.
+ return appPreferences.lastSeenChangeLogVersion != appVersion
+ }
+
+ override var identifier: NotificationProviderIdentifier {
+ .latestChangesInAppNotificationProvider
+ }
+
+ var notificationDescriptor: InAppNotificationDescriptor? {
+ defer {
+ // Always update the last seen version
+ appPreferences.lastSeenChangeLogVersion = appVersion
+ }
+
+ guard shouldShowNotification else { return nil }
+
+ return InAppNotificationDescriptor(
+ identifier: identifier,
+ style: .success,
+ title: NSLocalizedString(
+ "LATEST_CHANGES_IN_APP_NOTIFICATION_TITLE",
+ value: "NEW VERSION INSTALLED",
+ comment: ""
+ ),
+ body: createNotificationBody(),
+ button: createCloseButtonAction(),
+ tapAction: createTapAction()
+ )
+ }
+
+ private func createNotificationBody() -> NSAttributedString {
+ NSAttributedString(
+ markdownString: NSLocalizedString(
+ "LATEST_CHANGES_IN_APP_NOTIFICATION_BODY",
+ value: "**Tap here** to see what’s new.",
+ comment: ""
+ ),
+ options: MarkdownStylingOptions(font: UIFont.preferredFont(forTextStyle: .body)),
+ applyEffect: { markdownType, _ in
+ guard case .bold = markdownType else { return [:] }
+ return [.foregroundColor: UIColor.InAppNotificationBanner.titleColor]
+ }
+ )
+ }
+
+ private func createCloseButtonAction() -> InAppNotificationAction {
+ InAppNotificationAction(
+ image: UIImage(named: "IconCloseSml"),
+ handler: { [weak self] in
+ self?.invalidate()
+ }
+ )
+ }
+
+ private func createTapAction() -> InAppNotificationAction {
+ InAppNotificationAction(
+ handler: { [weak self] in
+ guard let self else { return }
+ self.invalidate()
+ NotificationManager.shared.notificationProvider(self, didReceiveAction: "\(self.identifier)")
+ }
+ )
+ }
+}
diff --git a/ios/MullvadVPN/Notifications/Notification Providers/RegisteredDeviceInAppNotificationProvider.swift b/ios/MullvadVPN/Notifications/Notification Providers/NewDeviceNotificationProvider.swift
index ade1b0eb20..66b76f9116 100644
--- a/ios/MullvadVPN/Notifications/Notification Providers/RegisteredDeviceInAppNotificationProvider.swift
+++ b/ios/MullvadVPN/Notifications/Notification Providers/NewDeviceNotificationProvider.swift
@@ -1,5 +1,5 @@
//
-// RegisteredDeviceInAppNotification.swift
+// NewDeviceNotificationProvider.swift
// MullvadVPN
//
// Created by Mojgan on 2023-04-21.
@@ -11,7 +11,7 @@ import MullvadSettings
import UIKit.UIColor
import UIKit.UIFont
-final class RegisteredDeviceInAppNotificationProvider: NotificationProvider,
+final class NewDeviceNotificationProvider: NotificationProvider,
InAppNotificationProvider, @unchecked Sendable {
// MARK: - private properties
@@ -57,8 +57,8 @@ final class RegisteredDeviceInAppNotificationProvider: NotificationProvider,
comment: ""
),
body: attributedBody,
- action: .init(
- image: .init(named: "IconCloseSml"),
+ button: InAppNotificationAction(
+ image: UIImage(named: "IconCloseSml"),
handler: { [weak self] in
guard let self else { return }
isNewDeviceRegistered = false
diff --git a/ios/MullvadVPN/Notifications/NotificationProviderIdentifier.swift b/ios/MullvadVPN/Notifications/NotificationProviderIdentifier.swift
index e15ec4b01e..155d0f7bdb 100644
--- a/ios/MullvadVPN/Notifications/NotificationProviderIdentifier.swift
+++ b/ios/MullvadVPN/Notifications/NotificationProviderIdentifier.swift
@@ -13,6 +13,7 @@ enum NotificationProviderIdentifier: String {
case accountExpiryInAppNotification = "AccountExpiryInAppNotification"
case registeredDeviceInAppNotification = "RegisteredDeviceInAppNotification"
case tunnelStatusNotificationProvider = "TunnelStatusNotificationProvider"
+ case latestChangesInAppNotificationProvider = "LatestChangesInAppNotificationProvider"
case `default` = "default"
var domainIdentifier: String {
diff --git a/ios/MullvadVPN/Notifications/UI/NotificationBannerView.swift b/ios/MullvadVPN/Notifications/UI/NotificationBannerView.swift
index 32449fa59d..7dea419a73 100644
--- a/ios/MullvadVPN/Notifications/UI/NotificationBannerView.swift
+++ b/ios/MullvadVPN/Notifications/UI/NotificationBannerView.swift
@@ -46,9 +46,16 @@ final class NotificationBannerView: UIView {
}()
private lazy var bodyStackView: UIStackView = {
- let stackView = UIStackView(arrangedSubviews: [bodyLabel, actionButton])
+ let stackView = UIStackView(arrangedSubviews: [titleLabel, bodyLabel])
stackView.alignment = .top
stackView.distribution = .fill
+ stackView.axis = .vertical
+ stackView.spacing = UIStackView.spacingUseSystem
+ return stackView
+ }()
+
+ private lazy var contentStackView: UIStackView = {
+ let stackView = UIStackView(arrangedSubviews: [bodyStackView, actionButton])
stackView.spacing = UIStackView.spacingUseSystem
return stackView
}()
@@ -87,11 +94,13 @@ final class NotificationBannerView: UIView {
}
}
+ var tapAction: InAppNotificationAction?
+
override init(frame: CGRect) {
super.init(frame: frame)
-
- addActionHandlers()
addSubviews()
+ addTapHandler()
+ addActionHandlers()
addConstraints()
}
@@ -99,12 +108,22 @@ final class NotificationBannerView: UIView {
fatalError("init(coder:) has not been implemented")
}
+ private func addTapHandler() {
+ let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTap))
+ addGestureRecognizer(tapGesture)
+ }
+
private func addActionHandlers() {
actionButton.addTarget(self, action: #selector(handleActionTap), for: .touchUpInside)
}
+ @objc
+ private func handleTap() {
+ tapAction?.handler?()
+ }
+
private func addSubviews() {
- wrapperView.addConstrainedSubviews([titleLabel, indicatorView, bodyStackView])
+ wrapperView.addConstrainedSubviews([indicatorView, contentStackView])
backgroundView.contentView.addConstrainedSubviews([wrapperView]) {
wrapperView.pinEdgesToSuperview()
}
@@ -114,9 +133,6 @@ final class NotificationBannerView: UIView {
}
private func addConstraints() {
- actionButton.setContentCompressionResistancePriority(.required, for: .horizontal)
- actionButton.setContentHuggingPriority(.required, for: .horizontal)
-
NSLayoutConstraint.activate([
indicatorView.bottomAnchor.constraint(equalTo: titleLabel.firstBaselineAnchor),
indicatorView.leadingAnchor.constraint(equalTo: wrapperView.layoutMarginsGuide.leadingAnchor),
@@ -125,14 +141,13 @@ final class NotificationBannerView: UIView {
indicatorView.heightAnchor
.constraint(equalToConstant: UIMetrics.InAppBannerNotification.indicatorSize.height),
- titleLabel.topAnchor.constraint(equalTo: wrapperView.layoutMarginsGuide.topAnchor),
- titleLabel.leadingAnchor.constraint(equalToSystemSpacingAfter: indicatorView.trailingAnchor, multiplier: 1),
- titleLabel.trailingAnchor.constraint(equalTo: wrapperView.layoutMarginsGuide.trailingAnchor),
-
- bodyStackView.topAnchor.constraint(equalToSystemSpacingBelow: titleLabel.bottomAnchor, multiplier: 1),
- bodyStackView.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor),
- bodyStackView.trailingAnchor.constraint(equalTo: titleLabel.trailingAnchor),
- bodyStackView.bottomAnchor.constraint(equalTo: wrapperView.layoutMarginsGuide.bottomAnchor),
+ contentStackView.topAnchor.constraint(equalTo: wrapperView.layoutMarginsGuide.topAnchor),
+ contentStackView.leadingAnchor.constraint(
+ equalToSystemSpacingAfter: indicatorView.trailingAnchor,
+ multiplier: 1
+ ),
+ contentStackView.trailingAnchor.constraint(equalTo: wrapperView.layoutMarginsGuide.trailingAnchor),
+ contentStackView.bottomAnchor.constraint(equalTo: wrapperView.layoutMarginsGuide.bottomAnchor),
])
}
diff --git a/ios/MullvadVPN/Notifications/UI/NotificationController.swift b/ios/MullvadVPN/Notifications/UI/NotificationController.swift
index b29e3d2bae..f0e9a82e57 100644
--- a/ios/MullvadVPN/Notifications/UI/NotificationController.swift
+++ b/ios/MullvadVPN/Notifications/UI/NotificationController.swift
@@ -97,7 +97,8 @@ final class NotificationController: UIViewController {
bannerView.title = notification.title
bannerView.body = notification.body
bannerView.style = notification.style
- bannerView.action = notification.action
+ bannerView.action = notification.button
+ bannerView.tapAction = notification.tapAction
bannerView.accessibilityLabel = "\(notification.title)\n\(notification.body.string)"
// Do not emit the .layoutChanged unless the banner is focused to avoid capturing
diff --git a/ios/MullvadVPN/SceneDelegate.swift b/ios/MullvadVPN/SceneDelegate.swift
index ff5334d853..82327b24ba 100644
--- a/ios/MullvadVPN/SceneDelegate.swift
+++ b/ios/MullvadVPN/SceneDelegate.swift
@@ -78,7 +78,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate, @preconcurrency Setting
hostname: ApplicationConfiguration.hostName
)
),
- appPreferences: AppPreferences(),
+ appPreferences: appDelegate.appPreferences,
accessMethodRepository: accessMethodRepository,
transportProvider: appDelegate.configuredTransportProvider,
ipOverrideRepository: appDelegate.ipOverrideRepository
diff --git a/ios/MullvadVPN/View controllers/ChangeLog/ChangeLogView.swift b/ios/MullvadVPN/View controllers/ChangeLog/ChangeLogView.swift
index 4131d1333f..1b4e091fce 100644
--- a/ios/MullvadVPN/View controllers/ChangeLog/ChangeLogView.swift
+++ b/ios/MullvadVPN/View controllers/ChangeLog/ChangeLogView.swift
@@ -32,19 +32,6 @@ struct ChangeLogView<ViewModel>: View where ViewModel: ChangeLogViewModelProtoco
}
.listStyle(.plain)
.frame(maxHeight: .infinity)
-
- MainButton(
- text: LocalizedStringKey("See full changelog"),
- style: .default,
- image: Image(.iconExtlink),
- imagePosition: .trailing
- ) {
- if let url =
- URL(string: "https://github.com/mullvad/mullvadvpn-app/blob/main/ios/CHANGELOG.md") {
- UIApplication.shared.open(url)
- }
- }
- .padding(.vertical, 24)
}
.padding(.horizontal, 24.0)
}