summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authormojganii <mojgan.jelodar@codic.se>2025-01-17 14:41:08 +0100
committermojganii <mojgan.jelodar@codic.se>2025-01-17 14:46:33 +0100
commitd944125f2f526baf9824ebe61126355e5095101b (patch)
tree553aaf05441f805b1d9901eca6a1afdf61d9028d
parentc89b219bb382be365b60868c5b40f669c1292941 (diff)
downloadmullvadvpn-d944125f2f526baf9824ebe61126355e5095101b.tar.xz
mullvadvpn-d944125f2f526baf9824ebe61126355e5095101b.zip
Add new changelog design
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj20
-rw-r--r--ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift18
-rw-r--r--ios/MullvadVPN/Coordinators/ChangeLogCoordinator.swift51
-rw-r--r--ios/MullvadVPN/Coordinators/Settings/SettingsCoordinator.swift3
-rw-r--r--ios/MullvadVPN/Coordinators/Settings/SettingsViewControllerFactory.swift17
-rw-r--r--ios/MullvadVPN/View controllers/ChangeLog/BulletPointText.swift27
-rw-r--r--ios/MullvadVPN/View controllers/ChangeLog/ChangeLogInteractor.swift59
-rw-r--r--ios/MullvadVPN/View controllers/ChangeLog/ChangeLogModel.swift12
-rw-r--r--ios/MullvadVPN/View controllers/ChangeLog/ChangeLogReader.swift38
-rw-r--r--ios/MullvadVPN/View controllers/ChangeLog/ChangeLogView.swift59
-rw-r--r--ios/MullvadVPN/View controllers/ChangeLog/ChangeLogViewModel.swift61
-rw-r--r--ios/MullvadVPN/View controllers/Settings/SettingsCellFactory.swift18
-rw-r--r--ios/MullvadVPN/View controllers/Settings/SettingsDataSource.swift13
-rw-r--r--ios/MullvadVPN/View controllers/Settings/SettingsViewController.swift4
-rw-r--r--ios/MullvadVPN/Views/MainButton.swift45
-rw-r--r--ios/MullvadVPN/Views/MainButtonStyle.swift2
-rw-r--r--ios/MullvadVPNUITests/Base/BaseUITestCase.swift3
17 files changed, 287 insertions, 163 deletions
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj
index ec046d9f94..1a1df0918b 100644
--- a/ios/MullvadVPN.xcodeproj/project.pbxproj
+++ b/ios/MullvadVPN.xcodeproj/project.pbxproj
@@ -917,6 +917,7 @@
F041BE532C9878B60083EC28 /* ConnectionConfigurationBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = F041BE522C9878B60083EC28 /* ConnectionConfigurationBuilder.swift */; };
F04413612BA45CD70018A6EE /* CustomListLocationNodeBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = F04413602BA45CD70018A6EE /* CustomListLocationNodeBuilder.swift */; };
F04413622BA45CE30018A6EE /* CustomListLocationNodeBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = F04413602BA45CD70018A6EE /* CustomListLocationNodeBuilder.swift */; };
+ F048BFA22D31843000251CB9 /* ChangeLogModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F048BFA12D31842B00251CB9 /* ChangeLogModel.swift */; };
F04AF92D2C466013004A8314 /* EphemeralPeerNegotiationState.swift in Sources */ = {isa = PBXBuildFile; fileRef = F04AF92C2C466013004A8314 /* EphemeralPeerNegotiationState.swift */; };
F04FBE612A8379EE009278D7 /* AppPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = F04FBE602A8379EE009278D7 /* AppPreferences.swift */; };
F050AE4E2B70D7F8003F4EDB /* LocationCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F050AE4D2B70D7F8003F4EDB /* LocationCellViewModel.swift */; };
@@ -970,6 +971,9 @@
F09A297C2A9F8A9B00EA3B6F /* VoucherTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = F09A29792A9F8A9B00EA3B6F /* VoucherTextField.swift */; };
F09A297D2A9F8A9B00EA3B6F /* RedeemVoucherContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F09A297A2A9F8A9B00EA3B6F /* RedeemVoucherContentView.swift */; };
F09A29822A9F8AD200EA3B6F /* RedeemVoucherInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = F09A297F2A9F8AD200EA3B6F /* RedeemVoucherInteractor.swift */; };
+ F09C97212D311D8800ADE747 /* ChangeLogView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F09C97202D311D7A00ADE747 /* ChangeLogView.swift */; };
+ F09C97232D3122F300ADE747 /* ChangeLogReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = F09C97222D3122E400ADE747 /* ChangeLogReader.swift */; };
+ F09C97252D312ED000ADE747 /* BulletPointText.swift in Sources */ = {isa = PBXBuildFile; fileRef = F09C97242D312ECD00ADE747 /* BulletPointText.swift */; };
F09D04B32AE919AC003D4F89 /* OutgoingConnectionProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = F09D04AF2AE7F83D003D4F89 /* OutgoingConnectionProxy.swift */; };
F09D04B52AE93CB6003D4F89 /* OutgoingConnectionProxy+Stub.swift in Sources */ = {isa = PBXBuildFile; fileRef = F09D04B42AE93CB6003D4F89 /* OutgoingConnectionProxy+Stub.swift */; };
F09D04B72AE941DA003D4F89 /* OutgoingConnectionProxyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F09D04B62AE941DA003D4F89 /* OutgoingConnectionProxyTests.swift */; };
@@ -1046,7 +1050,6 @@
F0E8E4C32A602E0D00ED26A3 /* AccountDeletionViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0E8E4C22A602E0D00ED26A3 /* AccountDeletionViewModel.swift */; };
F0E8E4C52A60499100ED26A3 /* AccountDeletionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0E8E4C42A60499100ED26A3 /* AccountDeletionViewController.swift */; };
F0E8E4C92A604E7400ED26A3 /* AccountDeletionInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0E8E4C82A604E7400ED26A3 /* AccountDeletionInteractor.swift */; };
- F0EF50D32A8FA47E0031E8DF /* ChangeLogInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0EF50D22A8FA47E0031E8DF /* ChangeLogInteractor.swift */; };
F0EF50D52A949F8E0031E8DF /* ChangeLogViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0EF50D42A949F8E0031E8DF /* ChangeLogViewModel.swift */; };
F0F316192BF3572B0078DBCF /* RelaySelectorResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0F316182BF3572B0078DBCF /* RelaySelectorResult.swift */; };
F0F3161B2BF358590078DBCF /* NoRelaysSatisfyingConstraintsError.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0F3161A2BF358590078DBCF /* NoRelaysSatisfyingConstraintsError.swift */; };
@@ -2204,6 +2207,7 @@
F041BE4E2C983C2B0083EC28 /* DAITASettingsPromptItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DAITASettingsPromptItem.swift; sourceTree = "<group>"; };
F041BE522C9878B60083EC28 /* ConnectionConfigurationBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectionConfigurationBuilder.swift; sourceTree = "<group>"; };
F04413602BA45CD70018A6EE /* CustomListLocationNodeBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomListLocationNodeBuilder.swift; sourceTree = "<group>"; };
+ F048BFA12D31842B00251CB9 /* ChangeLogModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChangeLogModel.swift; sourceTree = "<group>"; };
F04AF92C2C466013004A8314 /* EphemeralPeerNegotiationState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EphemeralPeerNegotiationState.swift; sourceTree = "<group>"; };
F04DD3D72C130DF600E03E28 /* TunnelSettingsManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TunnelSettingsManager.swift; sourceTree = "<group>"; };
F04FBE602A8379EE009278D7 /* AppPreferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppPreferences.swift; sourceTree = "<group>"; };
@@ -2242,6 +2246,9 @@
F09A29792A9F8A9B00EA3B6F /* VoucherTextField.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VoucherTextField.swift; sourceTree = "<group>"; };
F09A297A2A9F8A9B00EA3B6F /* RedeemVoucherContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RedeemVoucherContentView.swift; sourceTree = "<group>"; };
F09A297F2A9F8AD200EA3B6F /* RedeemVoucherInteractor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RedeemVoucherInteractor.swift; sourceTree = "<group>"; };
+ F09C97202D311D7A00ADE747 /* ChangeLogView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChangeLogView.swift; sourceTree = "<group>"; };
+ F09C97222D3122E400ADE747 /* ChangeLogReader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChangeLogReader.swift; sourceTree = "<group>"; };
+ F09C97242D312ECD00ADE747 /* BulletPointText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BulletPointText.swift; sourceTree = "<group>"; };
F09D04AF2AE7F83D003D4F89 /* OutgoingConnectionProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OutgoingConnectionProxy.swift; sourceTree = "<group>"; };
F09D04B42AE93CB6003D4F89 /* OutgoingConnectionProxy+Stub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OutgoingConnectionProxy+Stub.swift"; sourceTree = "<group>"; };
F09D04B62AE941DA003D4F89 /* OutgoingConnectionProxyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OutgoingConnectionProxyTests.swift; sourceTree = "<group>"; };
@@ -2301,7 +2308,6 @@
F0E8E4C22A602E0D00ED26A3 /* AccountDeletionViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountDeletionViewModel.swift; sourceTree = "<group>"; };
F0E8E4C42A60499100ED26A3 /* AccountDeletionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountDeletionViewController.swift; sourceTree = "<group>"; };
F0E8E4C82A604E7400ED26A3 /* AccountDeletionInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountDeletionInteractor.swift; sourceTree = "<group>"; };
- F0EF50D22A8FA47E0031E8DF /* ChangeLogInteractor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChangeLogInteractor.swift; sourceTree = "<group>"; };
F0EF50D42A949F8E0031E8DF /* ChangeLogViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChangeLogViewModel.swift; sourceTree = "<group>"; };
F0F1EF8C2BE8FF0A00CED01D /* LaunchArguments.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchArguments.swift; sourceTree = "<group>"; };
F0F316182BF3572B0078DBCF /* RelaySelectorResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelaySelectorResult.swift; sourceTree = "<group>"; };
@@ -4564,7 +4570,10 @@
F0EF50D12A8FA47E0031E8DF /* ChangeLog */ = {
isa = PBXGroup;
children = (
- F0EF50D22A8FA47E0031E8DF /* ChangeLogInteractor.swift */,
+ F09C97242D312ECD00ADE747 /* BulletPointText.swift */,
+ F048BFA12D31842B00251CB9 /* ChangeLogModel.swift */,
+ F09C97222D3122E400ADE747 /* ChangeLogReader.swift */,
+ F09C97202D311D7A00ADE747 /* ChangeLogView.swift */,
F0EF50D42A949F8E0031E8DF /* ChangeLogViewModel.swift */,
);
path = ChangeLog;
@@ -5895,7 +5904,6 @@
7A3353932AAA089000F0A71C /* SimulatorTunnelInfo.swift in Sources */,
7A5869AB2B55527C00640D27 /* IPOverrideCoordinator.swift in Sources */,
5867771429097BCD006F721F /* PaymentState.swift in Sources */,
- F0EF50D32A8FA47E0031E8DF /* ChangeLogInteractor.swift in Sources */,
7AC8A3AF2ABC71D600DC4939 /* TermsOfServiceCoordinator.swift in Sources */,
7A8A191A2CEF41AF000BCB5B /* GroupedRowView.swift in Sources */,
58FF9FE22B075BA600E4C97D /* EditAccessMethodSectionIdentifier.swift in Sources */,
@@ -5928,6 +5936,7 @@
586C0D892B03D5E000E7CDD7 /* TextCellContentConfiguration+Extensions.swift in Sources */,
58C3F4F92964B08300D72515 /* MapViewController.swift in Sources */,
584D26C6270C8741004EA533 /* SettingsDNSTextCell.swift in Sources */,
+ F09C97212D311D8800ADE747 /* ChangeLogView.swift in Sources */,
58F2E148276A307400A79513 /* MapConnectionStatusOperation.swift in Sources */,
58BA693123EADA6A009DC256 /* SimulatorTunnelProvider.swift in Sources */,
7A9CCCB32A96302800DD6A34 /* WelcomeCoordinator.swift in Sources */,
@@ -5968,6 +5977,7 @@
58B26E22294351EA00D5980C /* InAppNotificationProvider.swift in Sources */,
5893716A28817A45004EE76C /* DeviceManagementViewController.swift in Sources */,
7A9CCCB82A96302800DD6A34 /* SetupAccountCompletedCoordinator.swift in Sources */,
+ F09C97232D3122F300ADE747 /* ChangeLogReader.swift in Sources */,
447F3D8B2CDE1853006E3462 /* ShadowsocksObfuscationSettingsView.swift in Sources */,
58BFA5C622A7C97F00A6173D /* RelayCacheTracker.swift in Sources */,
7A0B311E2B303A0D004B12E0 /* AccessbilityIdentifier.swift in Sources */,
@@ -6129,6 +6139,7 @@
58677712290976FB006F721F /* SettingsInteractor.swift in Sources */,
58EF875D2B1638BF00C098B2 /* ProxyConfigurationTesterProtocol.swift in Sources */,
58CE5E66224146200008646E /* LoginViewController.swift in Sources */,
+ F048BFA22D31843000251CB9 /* ChangeLogModel.swift in Sources */,
F0C6FA852A6A733700F521F0 /* InAppPurchaseInteractor.swift in Sources */,
58CEB2F92AFD136E00E6E088 /* UIBackgroundConfiguration+Extensions.swift in Sources */,
5878F50029CDA742003D4BE2 /* UIView+AutoLayoutBuilder.swift in Sources */,
@@ -6274,6 +6285,7 @@
5803B4B22940A48700C23744 /* TunnelStore.swift in Sources */,
586A950F29012BEE007BAF2B /* AddressCacheTracker.swift in Sources */,
7A7907332BC0280A00B61F81 /* InterceptibleNavigationController.swift in Sources */,
+ F09C97252D312ED000ADE747 /* BulletPointText.swift in Sources */,
F02F41A02B9723AF00625A4F /* AddLocationsViewController.swift in Sources */,
587B753D2666468F00DEF7E9 /* NotificationController.swift in Sources */,
);
diff --git a/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift b/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift
index 706637086f..2175835045 100644
--- a/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift
+++ b/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift
@@ -332,16 +332,15 @@ final class ApplicationCoordinator: Coordinator, Presenting, @preconcurrency Roo
}
private func presentChangeLog(animated: Bool, completion: @escaping (Coordinator) -> Void) {
- let coordinator = ChangeLogCoordinator(interactor: ChangeLogInteractor())
-
- coordinator.didFinish = { [weak self] _ in
- self?.appPreferences.markChangeLogSeen()
- self?.router.dismiss(.changelog)
- }
+ let coordinator = ChangeLogCoordinator(
+ navigationController: CustomNavigationController(),
+ viewModel: ChangeLogViewModel(changeLogReader: ChangeLogReader())
+ )
- coordinator.start()
+ coordinator.start(animated: false)
- presentChild(coordinator, animated: animated) {
+ presentChild(coordinator, animated: animated) { [weak self] in
+ self?.appPreferences.markChangeLogSeen()
completion(coordinator)
}
}
@@ -824,8 +823,7 @@ extension DeviceState {
fileprivate extension AppPreferencesDataSource {
var hasSeenLastChanges: Bool {
- !ChangeLogInteractor().hasNewChanges ||
- (lastSeenChangeLogVersion == Bundle.main.shortVersion)
+ lastSeenChangeLogVersion == Bundle.main.shortVersion
}
mutating func markChangeLogSeen() {
diff --git a/ios/MullvadVPN/Coordinators/ChangeLogCoordinator.swift b/ios/MullvadVPN/Coordinators/ChangeLogCoordinator.swift
index 031ba6a972..3fb5cb8048 100644
--- a/ios/MullvadVPN/Coordinators/ChangeLogCoordinator.swift
+++ b/ios/MullvadVPN/Coordinators/ChangeLogCoordinator.swift
@@ -8,46 +8,33 @@
import MullvadLogging
import Routing
+import SwiftUI
import UIKit
-final class ChangeLogCoordinator: Coordinator, Presentable {
- private var alertController: AlertViewController?
- private let interactor: ChangeLogInteractor
- var didFinish: ((ChangeLogCoordinator) -> Void)?
+final class ChangeLogCoordinator: Coordinator, Presentable, SettingsChildCoordinator {
+ private var navigationController: UINavigationController?
+ private let viewModel: ChangeLogViewModel
var presentedViewController: UIViewController {
- return alertController!
+ return navigationController!
}
- init(interactor: ChangeLogInteractor) {
- self.interactor = interactor
+ init(navigationController: UINavigationController, viewModel: ChangeLogViewModel) {
+ self.viewModel = viewModel
+ self.navigationController = navigationController
}
- func start() {
- let presentation = AlertPresentation(
- id: "change-log-ok-alert",
- accessibilityIdentifier: .changeLogAlert,
- header: interactor.viewModel.header,
- title: interactor.viewModel.title,
- attributedMessage: interactor.viewModel.body,
- buttons: [
- AlertAction(
- title: NSLocalizedString(
- "CHANGE_LOG_OK_ACTION",
- tableName: "Account",
- value: "Got it!",
- comment: ""
- ),
- style: .default,
- accessibilityId: .alertOkButton,
- handler: { [weak self] in
- guard let self else { return }
- didFinish?(self)
- }
- ),
- ]
+ func start(animated: Bool) {
+ let changeLogViewController = UIHostingController(rootView: ChangeLogView(viewModel: viewModel))
+ changeLogViewController.view.setAccessibilityIdentifier(.changeLogAlert)
+ changeLogViewController.navigationItem.title = NSLocalizedString(
+ "whats_new_title",
+ tableName: "Changelog",
+ value: "What's new",
+ comment: ""
)
-
- alertController = AlertViewController(presentation: presentation)
+ changeLogViewController.navigationItem.largeTitleDisplayMode = .always
+ navigationController?.navigationBar.prefersLargeTitles = true
+ navigationController?.pushViewController(changeLogViewController, animated: animated)
}
}
diff --git a/ios/MullvadVPN/Coordinators/Settings/SettingsCoordinator.swift b/ios/MullvadVPN/Coordinators/Settings/SettingsCoordinator.swift
index 2bf076115d..eeaa3cccdc 100644
--- a/ios/MullvadVPN/Coordinators/Settings/SettingsCoordinator.swift
+++ b/ios/MullvadVPN/Coordinators/Settings/SettingsCoordinator.swift
@@ -30,6 +30,9 @@ enum SettingsNavigationRoute: Equatable {
/// API access route.
case apiAccess
+ /// changelog route.
+ case changelog
+
/// Multihop route.
case multihop
diff --git a/ios/MullvadVPN/Coordinators/Settings/SettingsViewControllerFactory.swift b/ios/MullvadVPN/Coordinators/Settings/SettingsViewControllerFactory.swift
index 829bb05b58..f25a652d34 100644
--- a/ios/MullvadVPN/Coordinators/Settings/SettingsViewControllerFactory.swift
+++ b/ios/MullvadVPN/Coordinators/Settings/SettingsViewControllerFactory.swift
@@ -54,15 +54,17 @@ struct SettingsViewControllerFactory {
case .root:
// Handled in SettingsCoordinator.
.failed
+ case .faq:
+ // Handled separately and presented as a modal.
+ .failed
case .vpnSettings:
makeVPNSettingsViewController()
case .problemReport:
makeProblemReportViewController()
case .apiAccess:
makeAPIAccessViewController()
- case .faq:
- // Handled separately and presented as a modal.
- .failed
+ case .changelog:
+ makeChangelogViewController()
case .multihop:
makeMultihopViewController()
case .daita:
@@ -93,6 +95,15 @@ struct SettingsViewControllerFactory {
))
}
+ private func makeChangelogViewController() -> MakeChildResult {
+ return .childCoordinator(
+ ChangeLogCoordinator(
+ navigationController: navigationController,
+ viewModel: ChangeLogViewModel(changeLogReader: ChangeLogReader())
+ )
+ )
+ }
+
private func makeMultihopViewController() -> MakeChildResult {
let viewModel = MultihopTunnelSettingsViewModel(tunnelManager: interactorFactory.tunnelManager)
let view = SettingsMultihopView(tunnelViewModel: viewModel)
diff --git a/ios/MullvadVPN/View controllers/ChangeLog/BulletPointText.swift b/ios/MullvadVPN/View controllers/ChangeLog/BulletPointText.swift
new file mode 100644
index 0000000000..93d0b234a7
--- /dev/null
+++ b/ios/MullvadVPN/View controllers/ChangeLog/BulletPointText.swift
@@ -0,0 +1,27 @@
+//
+// BulletPointText.swift
+// MullvadVPN
+//
+// Created by Mojgan on 2025-01-10.
+// Copyright © 2025 Mullvad VPN AB. All rights reserved.
+//
+import SwiftUI
+
+struct BulletPointText: View {
+ let text: String
+ let bullet = "•"
+
+ var body: some View {
+ HStack(alignment: .firstTextBaseline) {
+ Text(bullet)
+ .font(.body)
+ .foregroundColor(UIColor.secondaryTextColor.color)
+ Text(text)
+ .font(.body)
+ .foregroundColor(UIColor.secondaryTextColor.color)
+ .lineLimit(nil)
+ .multilineTextAlignment(.leading)
+ }
+ .frame(maxWidth: .infinity, alignment: .leading)
+ }
+}
diff --git a/ios/MullvadVPN/View controllers/ChangeLog/ChangeLogInteractor.swift b/ios/MullvadVPN/View controllers/ChangeLog/ChangeLogInteractor.swift
deleted file mode 100644
index 571eeaf756..0000000000
--- a/ios/MullvadVPN/View controllers/ChangeLog/ChangeLogInteractor.swift
+++ /dev/null
@@ -1,59 +0,0 @@
-//
-// ChangeLogInteractor.swift
-// MullvadVPN
-//
-// Created by Mojgan on 2023-08-11.
-// Copyright © 2023 Mullvad VPN AB. All rights reserved.
-//
-
-import Foundation
-import MullvadLogging
-
-final class ChangeLogInteractor {
- private let logger = Logger(label: "ChangeLogInteractor")
- private var items: [String] = []
-
- var hasNewChanges: Bool {
- !items.isEmpty
- }
-
- var viewModel: ChangeLogViewModel {
- return ChangeLogViewModel(
- body: items
- )
- }
-
- init() {
- do {
- let string = try readFromFile()
- items = string.split(whereSeparator: { $0.isNewline }).map { String($0) }
- } catch {
- logger.error(error: error, message: "Cannot read change log from bundle.")
- }
- }
-
- /**
- Reads change log file from bundle and returns its contents as a string.
- */
- private func readFromFile() throws -> String {
- try String(contentsOfFile: try getPathToChangesFile())
- .split(whereSeparator: { $0.isNewline })
- .compactMap { line in
- let trimmedString = line.trimmingCharacters(in: .whitespaces)
-
- return trimmedString.isEmpty ? nil : trimmedString
- }
- .joined(separator: "\n")
- }
-
- /**
- Returns path to change log file in bundle.
- */
- private func getPathToChangesFile() throws -> String {
- if let filePath = Bundle.main.path(forResource: "changes", ofType: "txt") {
- return filePath
- } else {
- throw CocoaError(.fileNoSuchFile)
- }
- }
-}
diff --git a/ios/MullvadVPN/View controllers/ChangeLog/ChangeLogModel.swift b/ios/MullvadVPN/View controllers/ChangeLog/ChangeLogModel.swift
new file mode 100644
index 0000000000..768935c1b2
--- /dev/null
+++ b/ios/MullvadVPN/View controllers/ChangeLog/ChangeLogModel.swift
@@ -0,0 +1,12 @@
+//
+// ChangeLogModel.swift
+// MullvadVPN
+//
+// Created by Mojgan on 2025-01-10.
+// Copyright © 2025 Mullvad VPN AB. All rights reserved.
+//
+
+struct ChangeLogModel {
+ let title: String
+ let changes: [String]
+}
diff --git a/ios/MullvadVPN/View controllers/ChangeLog/ChangeLogReader.swift b/ios/MullvadVPN/View controllers/ChangeLog/ChangeLogReader.swift
new file mode 100644
index 0000000000..6e99ea4ba8
--- /dev/null
+++ b/ios/MullvadVPN/View controllers/ChangeLog/ChangeLogReader.swift
@@ -0,0 +1,38 @@
+//
+// ChangeLogReader.swift
+// MullvadVPN
+//
+// Created by Mojgan on 2025-01-10.
+// Copyright © 2025 Mullvad VPN AB. All rights reserved.
+//
+import Foundation
+
+protocol ChangeLogReaderProtocol {
+ func read() throws -> [String]
+}
+
+struct ChangeLogReader: ChangeLogReaderProtocol {
+ /**
+ Reads change log file from bundle and returns its contents as a string.
+ */
+ func read() throws -> [String] {
+ try String(contentsOfFile: try getPathToChangesFile())
+ .split(whereSeparator: { $0.isNewline })
+ .compactMap { line in
+ let trimmedString = line.trimmingCharacters(in: .whitespaces)
+
+ return trimmedString.isEmpty ? nil : trimmedString
+ }
+ }
+
+ /**
+ Returns path to change log file in bundle.
+ */
+ private func getPathToChangesFile() throws -> String {
+ if let filePath = Bundle.main.path(forResource: "changes", ofType: "txt") {
+ return filePath
+ } else {
+ throw CocoaError(.fileNoSuchFile)
+ }
+ }
+}
diff --git a/ios/MullvadVPN/View controllers/ChangeLog/ChangeLogView.swift b/ios/MullvadVPN/View controllers/ChangeLog/ChangeLogView.swift
new file mode 100644
index 0000000000..4131d1333f
--- /dev/null
+++ b/ios/MullvadVPN/View controllers/ChangeLog/ChangeLogView.swift
@@ -0,0 +1,59 @@
+//
+// ChangeLogView.swift
+// MullvadVPN
+//
+// Created by Mojgan on 2025-01-10.
+// Copyright © 2025 Mullvad VPN AB. All rights reserved.
+//
+import SwiftUI
+
+struct ChangeLogView<ViewModel>: View where ViewModel: ChangeLogViewModelProtocol {
+ @ObservedObject var viewModel: ViewModel
+
+ init(viewModel: ViewModel) {
+ self.viewModel = viewModel
+ }
+
+ var body: some View {
+ ZStack {
+ UIColor.secondaryColor.color.ignoresSafeArea()
+ VStack {
+ Text(viewModel.changeLog?.title ?? "")
+ .font(.title)
+ .fontWeight(.semibold)
+ .foregroundColor(UIColor.primaryTextColor.color)
+ .frame(maxWidth: .infinity, alignment: .leading)
+ List {
+ ForEach(viewModel.changeLog?.changes ?? [], id: \.self) { item in
+ BulletPointText(text: item)
+ .listRowSeparator(.hidden)
+ .listRowBackground(Color.clear)
+ }
+ }
+ .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)
+ }
+ .onAppear {
+ viewModel.getLatestChanges()
+ }
+ }
+}
+
+#Preview {
+ ChangeLogView(viewModel: MockChangeLogViewModel())
+}
diff --git a/ios/MullvadVPN/View controllers/ChangeLog/ChangeLogViewModel.swift b/ios/MullvadVPN/View controllers/ChangeLog/ChangeLogViewModel.swift
index ca45c5d3c1..fd8d1355a8 100644
--- a/ios/MullvadVPN/View controllers/ChangeLog/ChangeLogViewModel.swift
+++ b/ios/MullvadVPN/View controllers/ChangeLog/ChangeLogViewModel.swift
@@ -7,42 +7,39 @@
//
import Foundation
-import UIKit.NSAttributedString
-import UIKit.UIFont
+import MullvadLogging
+import SwiftUI
-struct ChangeLogViewModel {
- let header: String = Bundle.main.shortVersion
- let title: String = NSLocalizedString(
- "CHANGE_LOG_TITLE",
- tableName: "Account",
- value: "Changes in this version:",
- comment: ""
- )
- let body: NSAttributedString
+protocol ChangeLogViewModelProtocol: ObservableObject {
+ var changeLog: ChangeLogModel? { get }
+ func getLatestChanges()
+}
+
+class ChangeLogViewModel: ChangeLogViewModelProtocol {
+ private let logger = Logger(label: "ChangeLogViewModel")
+ private let changeLogReader: ChangeLogReaderProtocol
+
+ @Published var changeLog: ChangeLogModel?
- init(body: [String]) {
- self.body = body.changeLogAttributedString
+ init(changeLogReader: ChangeLogReaderProtocol) {
+ self.changeLogReader = changeLogReader
}
-}
-fileprivate extension Array where Element == String {
- var changeLogAttributedString: NSAttributedString {
- let bullet = "• "
- let font = UIFont.preferredFont(forTextStyle: .body)
- let paragraphStyle = NSMutableParagraphStyle()
- paragraphStyle.lineBreakMode = .byWordWrapping
- paragraphStyle.headIndent = bullet.size(withAttributes: [.font: font]).width
+ func getLatestChanges() {
+ do {
+ changeLog = ChangeLogModel(title: Bundle.main.productVersion, changes: try changeLogReader.read())
+ } catch {
+ logger.error(error: error, message: "Cannot read change log from bundle.")
+ }
+ }
+}
- return NSAttributedString(
- string: self.map {
- "\(bullet)\($0)"
- }
- .joined(separator: "\n"),
- attributes: [
- .paragraphStyle: paragraphStyle,
- .font: font,
- .foregroundColor: UIColor.white.withAlphaComponent(0.8),
- ]
- )
+class MockChangeLogViewModel: ChangeLogViewModelProtocol {
+ @Published var changeLog: ChangeLogModel?
+ func getLatestChanges() {
+ changeLog = ChangeLogModel(title: "2025.1", changes: [
+ "Introduced a dark mode for better accessibility and user experience.",
+ "Added two-factor authentication (2FA) for all user accounts.",
+ ])
}
}
diff --git a/ios/MullvadVPN/View controllers/Settings/SettingsCellFactory.swift b/ios/MullvadVPN/View controllers/Settings/SettingsCellFactory.swift
index dc365734e8..ee916d2e83 100644
--- a/ios/MullvadVPN/View controllers/Settings/SettingsCellFactory.swift
+++ b/ios/MullvadVPN/View controllers/Settings/SettingsCellFactory.swift
@@ -28,8 +28,16 @@ final class SettingsCellFactory: @preconcurrency CellFactoryProtocol, Sendable {
}
func makeCell(for item: SettingsDataSource.Item, indexPath: IndexPath) -> UITableViewCell {
- let cell = tableView.dequeueReusableCell(withIdentifier: item.reuseIdentifier.rawValue, for: indexPath)
+ let cell: UITableViewCell
+ // Instantiate cell based on the specific item type
+ if item == .changelog {
+ cell = SettingsCell(style: .subtitle, reuseIdentifier: item.reuseIdentifier.rawValue)
+ } else {
+ cell = tableView.dequeueReusableCell(withIdentifier: item.reuseIdentifier.rawValue, for: indexPath)
+ }
+
+ // Configure the cell with the common logic
configureCell(cell, item: item, indexPath: indexPath)
return cell
@@ -51,18 +59,17 @@ final class SettingsCellFactory: @preconcurrency CellFactoryProtocol, Sendable {
cell.setAccessibilityIdentifier(item.accessibilityIdentifier)
cell.disclosureType = .chevron
- case .version:
+ case .changelog:
guard let cell = cell as? SettingsCell else { return }
-
cell.titleLabel.text = NSLocalizedString(
"APP_VERSION_CELL_LABEL",
tableName: "Settings",
- value: "App version",
+ value: "What's new",
comment: ""
)
cell.detailTitleLabel.text = Bundle.main.productVersion
cell.setAccessibilityIdentifier(item.accessibilityIdentifier)
- cell.disclosureType = .none
+ cell.disclosureType = .chevron
case .problemReport:
guard let cell = cell as? SettingsCell else { return }
@@ -92,7 +99,6 @@ final class SettingsCellFactory: @preconcurrency CellFactoryProtocol, Sendable {
case .apiAccess:
guard let cell = cell as? SettingsCell else { return }
-
cell.titleLabel.text = NSLocalizedString(
"API_ACCESS_CELL_LABEL",
tableName: "Settings",
diff --git a/ios/MullvadVPN/View controllers/Settings/SettingsDataSource.swift b/ios/MullvadVPN/View controllers/Settings/SettingsDataSource.swift
index b769690d26..6f79882c0d 100644
--- a/ios/MullvadVPN/View controllers/Settings/SettingsDataSource.swift
+++ b/ios/MullvadVPN/View controllers/Settings/SettingsDataSource.swift
@@ -42,7 +42,7 @@ final class SettingsDataSource: UITableViewDiffableDataSource<SettingsDataSource
enum Item: String {
case vpnSettings
- case version
+ case changelog
case problemReport
case faq
case apiAccess
@@ -53,7 +53,7 @@ final class SettingsDataSource: UITableViewDiffableDataSource<SettingsDataSource
switch self {
case .vpnSettings:
return .vpnSettingsCell
- case .version:
+ case .changelog:
return .versionCell
case .problemReport:
return .problemReportCell
@@ -115,12 +115,7 @@ final class SettingsDataSource: UITableViewDiffableDataSource<SettingsDataSource
// MARK: - UITableViewDelegate
func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool {
- switch itemIdentifier(for: indexPath) {
- case .vpnSettings, .problemReport, .faq, .apiAccess, .daita, .multihop:
- true
- case .version, .none:
- false
- }
+ true
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
@@ -168,7 +163,7 @@ final class SettingsDataSource: UITableViewDiffableDataSource<SettingsDataSource
snapshot.appendItems([.apiAccess], toSection: .apiAccess)
snapshot.appendSections([.version, .problemReport])
- snapshot.appendItems([.version], toSection: .version)
+ snapshot.appendItems([.changelog], toSection: .version)
snapshot.appendItems([.problemReport, .faq], toSection: .problemReport)
apply(snapshot)
diff --git a/ios/MullvadVPN/View controllers/Settings/SettingsViewController.swift b/ios/MullvadVPN/View controllers/Settings/SettingsViewController.swift
index 607d4d6488..0a58e524f0 100644
--- a/ios/MullvadVPN/View controllers/Settings/SettingsViewController.swift
+++ b/ios/MullvadVPN/View controllers/Settings/SettingsViewController.swift
@@ -109,8 +109,8 @@ extension SettingsDataSource.Item {
switch self {
case .vpnSettings:
return .vpnSettings
- case .version:
- return nil
+ case .changelog:
+ return .changelog
case .problemReport:
return .problemReport
case .faq:
diff --git a/ios/MullvadVPN/Views/MainButton.swift b/ios/MullvadVPN/Views/MainButton.swift
index a4240c433d..93e23f4a3e 100644
--- a/ios/MullvadVPN/Views/MainButton.swift
+++ b/ios/MullvadVPN/Views/MainButton.swift
@@ -7,21 +7,60 @@
//
import SwiftUI
+enum MainButtonImagePosition {
+ case leading
+ case trailing
+}
struct MainButton: View {
var text: LocalizedStringKey
var style: MainButtonStyle.Style
+ var image: Image?
+ var imagePosition: MainButtonImagePosition = .leading
var action: () -> Void
+ @State private var imageHeight: CGFloat = 24.0
+
var body: some View {
Button(action: action, label: {
- HStack {
- Spacer()
+ ZStack {
+ // Centered Text
Text(text)
- Spacer()
+ .lineLimit(nil)
+ .multilineTextAlignment(.center)
+ .if(image != nil) { view in
+ // Reserve space for image if present
+ view.padding(.horizontal, imageHeight)
+ }
+
+ // Image on Leading or Trailing
+ HStack {
+ if imagePosition == .leading, let image = image {
+ image
+ .resizable()
+ .scaledToFit()
+ .frame(height: imageHeight)
+ .padding(.leading, 8.0)
+ Spacer()
+ }
+ Spacer()
+ if imagePosition == .trailing, let image = image {
+ Spacer() // Push the text to center
+ image
+ .resizable()
+ .scaledToFit()
+ .frame(height: imageHeight)
+ .padding(.trailing, 8.0)
+ }
+ }
}
})
+ .sizeOfView { size in
+ let actualHeight = size.height - 16.0
+ let baseHeight = max(actualHeight, 24.0)
+ imageHeight = baseHeight * 0.8
+ }
.buttonStyle(MainButtonStyle(style))
.cornerRadius(UIMetrics.MainButton.cornerRadius)
}
diff --git a/ios/MullvadVPN/Views/MainButtonStyle.swift b/ios/MullvadVPN/Views/MainButtonStyle.swift
index e13758a155..40f8346584 100644
--- a/ios/MullvadVPN/Views/MainButtonStyle.swift
+++ b/ios/MullvadVPN/Views/MainButtonStyle.swift
@@ -18,7 +18,7 @@ struct MainButtonStyle: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
return configuration.label
- .frame(height: 44)
+ .frame(minHeight: 44)
.foregroundColor(
isEnabled
? UIColor.primaryTextColor.color
diff --git a/ios/MullvadVPNUITests/Base/BaseUITestCase.swift b/ios/MullvadVPNUITests/Base/BaseUITestCase.swift
index 9eea1dd7ba..7af7de7030 100644
--- a/ios/MullvadVPNUITests/Base/BaseUITestCase.swift
+++ b/ios/MullvadVPNUITests/Base/BaseUITestCase.swift
@@ -242,8 +242,7 @@ class BaseUITestCase: XCTestCase {
.waitForExistence(timeout: Self.shortTimeout)
if changeLogIsShown {
- ChangeLogAlert(app)
- .tapOkay()
+ ChangeLogAlert(app).swipeDownToDismissModal()
}
// Ensure changelog is no longer shown