diff options
| author | Emīls <emils@mullvad.net> | 2025-01-17 15:02:52 +0100 |
|---|---|---|
| committer | Emīls <emils@mullvad.net> | 2025-01-17 15:02:52 +0100 |
| commit | 15703ba86fbe60f2383afa0c0d2bb9776fb71a75 (patch) | |
| tree | 553aaf05441f805b1d9901eca6a1afdf61d9028d | |
| parent | c89b219bb382be365b60868c5b40f669c1292941 (diff) | |
| parent | d944125f2f526baf9824ebe61126355e5095101b (diff) | |
| download | mullvadvpn-15703ba86fbe60f2383afa0c0d2bb9776fb71a75.tar.xz mullvadvpn-15703ba86fbe60f2383afa0c0d2bb9776fb71a75.zip | |
Merge branch 'add-a-changelog-view-ios-988'
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 |
