summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorBug Magnet <marco.nikic@mullvad.net>2025-06-12 15:54:47 +0200
committerBug Magnet <marco.nikic@mullvad.net>2025-06-12 15:54:47 +0200
commit9bdbcbaec9a99004cc7ac0bfa4eef32f21b962a7 (patch)
tree7fde0052d524b36baa52f3dbae3b4a2375ca6c9c
parent7ef26ac21de0bc64aa1043e693725fe255c14487 (diff)
parent95727b1887d935cf72ef1b8125f2f0a4e867d36c (diff)
downloadmullvadvpn-9bdbcbaec9a99004cc7ac0bfa4eef32f21b962a7.tar.xz
mullvadvpn-9bdbcbaec9a99004cc7ac0bfa4eef32f21b962a7.zip
Merge branch 'rewrite-tos-page-swift-ui'
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj20
-rw-r--r--ios/MullvadVPN/Containers/Root/HeaderBarView.swift8
-rw-r--r--ios/MullvadVPN/Containers/Root/UIHostingRootController.swift34
-rw-r--r--ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift2
-rw-r--r--ios/MullvadVPN/Coordinators/TermsOfServiceCoordinator.swift19
-rw-r--r--ios/MullvadVPN/Extensions/UIImage+Assets.swift4
-rw-r--r--ios/MullvadVPN/Notifications/UI/NotificationBannerView.swift4
-rw-r--r--ios/MullvadVPN/UI appearance/Color+Mullvad.swift1
-rw-r--r--ios/MullvadVPN/View controllers/TermsOfService/TermsOfServiceContentView.swift178
-rw-r--r--ios/MullvadVPN/View controllers/TermsOfService/TermsOfServiceView.swift80
-rw-r--r--ios/MullvadVPN/View controllers/TermsOfService/TermsOfServiceViewController.swift65
-rw-r--r--ios/Shared/ApplicationConfiguration.swift2
12 files changed, 143 insertions, 274 deletions
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj
index eb12f8006a..7f27111634 100644
--- a/ios/MullvadVPN.xcodeproj/project.pbxproj
+++ b/ios/MullvadVPN.xcodeproj/project.pbxproj
@@ -156,7 +156,6 @@
583FE02429C1ACB3006E85F9 /* RESTCreateApplePaymentResponse+Localization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06FAE67828F83CA50033DD93 /* RESTCreateApplePaymentResponse+Localization.swift */; };
58421030282D8A3C00F24E46 /* UpdateAccountDataOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5842102F282D8A3C00F24E46 /* UpdateAccountDataOperation.swift */; };
58421032282E42B000F24E46 /* UpdateDeviceDataOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58421031282E42B000F24E46 /* UpdateDeviceDataOperation.swift */; };
- 584592612639B4A200EF967F /* TermsOfServiceContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 584592602639B4A200EF967F /* TermsOfServiceContentView.swift */; };
5846227126E229F20035F7C2 /* StoreSubscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5846227026E229F20035F7C2 /* StoreSubscription.swift */; };
5846227326E22A160035F7C2 /* StorePaymentObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5846227226E22A160035F7C2 /* StorePaymentObserver.swift */; };
5846227726E22A7C0035F7C2 /* StorePaymentManagerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5846227626E22A7C0035F7C2 /* StorePaymentManagerDelegate.swift */; };
@@ -255,7 +254,6 @@
589E76C02A9378F100E502F3 /* RESTRequestExecutor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 589E76BF2A9378F100E502F3 /* RESTRequestExecutor.swift */; };
58A8EE5A2976BFBB009C0F8D /* SKError+Localized.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58A8EE592976BFBB009C0F8D /* SKError+Localized.swift */; };
58A8EE5E2976DB00009C0F8D /* StorePaymentManagerError+Display.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58A8EE5D2976DB00009C0F8D /* StorePaymentManagerError+Display.swift */; };
- 58A99ED3240014A0006599E9 /* TermsOfServiceViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58A99ED2240014A0006599E9 /* TermsOfServiceViewController.swift */; };
58ACF6492655365700ACE4B7 /* VPNSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58ACF6482655365700ACE4B7 /* VPNSettingsViewController.swift */; };
58ACF64B26553C3F00ACE4B7 /* SettingsSwitchCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58ACF64A26553C3F00ACE4B7 /* SettingsSwitchCell.swift */; };
58ACF64D26567A5000ACE4B7 /* CustomSwitch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58ACF64C26567A4F00ACE4B7 /* CustomSwitch.swift */; };
@@ -908,6 +906,7 @@
A9A5FA412ACB05D90083449F /* DeviceStateAccessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 583D86472A2678DC0060D63B /* DeviceStateAccessor.swift */; };
A9A5FA422ACB05D90083449F /* DeviceStateAccessorProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580810E42A30E13A00B74552 /* DeviceStateAccessorProtocol.swift */; };
A9A5FA432ACB05F20083449F /* UIColor+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587CBFE222807F530028DED3 /* UIColor+Helpers.swift */; };
+ A9A60ED42DF6E5AC00CD9C3D /* UIHostingRootController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9A60ED32DF6E5AC00CD9C3D /* UIHostingRootController.swift */; };
A9A8A8EB2A262AB30086D569 /* FileCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9A8A8EA2A262AB30086D569 /* FileCache.swift */; };
A9B6AC182ADE8F4300F7802A /* MigrationManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9B6AC172ADE8F4300F7802A /* MigrationManagerTests.swift */; };
A9B6AC1A2ADE8FBB00F7802A /* InMemorySettingsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9B6AC192ADE8FBB00F7802A /* InMemorySettingsStore.swift */; };
@@ -937,6 +936,8 @@
A9E0317A2ACB0AE70095D843 /* UIApplication+Stubs.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9E031792ACB0AE70095D843 /* UIApplication+Stubs.swift */; };
A9E0317F2ACC331C0095D843 /* TunnelStatusBlockObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9E0317D2ACC32920095D843 /* TunnelStatusBlockObserver.swift */; };
A9E034642ABB302000E59A5A /* UIEdgeInsets+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9E034632ABB302000E59A5A /* UIEdgeInsets+Extensions.swift */; };
+ A9EE855F2DF0893E00F2D769 /* Color+Mullvad.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9394EEB2DBF56AA009595EA /* Color+Mullvad.swift */; };
+ A9EE85612DF1BE2900F2D769 /* TermsOfServiceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9EE85602DF1BE2900F2D769 /* TermsOfServiceView.swift */; };
E1187ABC289BBB850024E748 /* OutOfTimeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1187ABA289BBB850024E748 /* OutOfTimeViewController.swift */; };
E1187ABD289BBB850024E748 /* OutOfTimeContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1187ABB289BBB850024E748 /* OutOfTimeContentView.swift */; };
E158B360285381C60002F069 /* String+AccountFormatting.swift in Sources */ = {isa = PBXBuildFile; fileRef = E158B35F285381C60002F069 /* String+AccountFormatting.swift */; };
@@ -1119,7 +1120,6 @@
F9394EEC2DBF56B6009595EA /* Color+Mullvad.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9394EEB2DBF56AA009595EA /* Color+Mullvad.swift */; };
F9394EF02DC0B58D009595EA /* MullvadListNavigationItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9394EEF2DC0B58D009595EA /* MullvadListNavigationItemView.swift */; };
F9394EF32DC21D8C009595EA /* MullvadList.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9394EF22DC21D8C009595EA /* MullvadList.swift */; };
- F97C38C82DE48AAE006DCB08 /* Color+Mullvad.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9394EEB2DBF56AA009595EA /* Color+Mullvad.swift */; };
F97C38CA2DE49869006DCB08 /* MultihopSettingsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97C38C92DE49869006DCB08 /* MultihopSettingsCoordinator.swift */; };
F97C38D92DE5930F006DCB08 /* CustomDNSCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97C38D82DE59307006DCB08 /* CustomDNSCoordinator.swift */; };
F998EFF82D359C4600D88D01 /* SKProduct+Formatting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FD5BEF24238EB300112C88 /* SKProduct+Formatting.swift */; };
@@ -1767,7 +1767,6 @@
5842102D282D3FC200F24E46 /* ResultBlockOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResultBlockOperation.swift; sourceTree = "<group>"; };
5842102F282D8A3C00F24E46 /* UpdateAccountDataOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateAccountDataOperation.swift; sourceTree = "<group>"; };
58421031282E42B000F24E46 /* UpdateDeviceDataOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateDeviceDataOperation.swift; sourceTree = "<group>"; };
- 584592602639B4A200EF967F /* TermsOfServiceContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TermsOfServiceContentView.swift; sourceTree = "<group>"; };
5846226426E0D9630035F7C2 /* ProductsRequestOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductsRequestOperation.swift; sourceTree = "<group>"; };
5846227026E229F20035F7C2 /* StoreSubscription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreSubscription.swift; sourceTree = "<group>"; };
5846227226E22A160035F7C2 /* StorePaymentObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorePaymentObserver.swift; sourceTree = "<group>"; };
@@ -1895,7 +1894,6 @@
58A8EE592976BFBB009C0F8D /* SKError+Localized.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SKError+Localized.swift"; sourceTree = "<group>"; };
58A8EE5D2976DB00009C0F8D /* StorePaymentManagerError+Display.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StorePaymentManagerError+Display.swift"; sourceTree = "<group>"; };
58A94AE326CFD945001CB97C /* TunnelStatusNotificationProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelStatusNotificationProvider.swift; sourceTree = "<group>"; };
- 58A99ED2240014A0006599E9 /* TermsOfServiceViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TermsOfServiceViewController.swift; sourceTree = "<group>"; };
58ACF6482655365700ACE4B7 /* VPNSettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNSettingsViewController.swift; sourceTree = "<group>"; };
58ACF64A26553C3F00ACE4B7 /* SettingsSwitchCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsSwitchCell.swift; sourceTree = "<group>"; };
58ACF64C26567A4F00ACE4B7 /* CustomSwitch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomSwitch.swift; sourceTree = "<group>"; };
@@ -2374,6 +2372,7 @@
A9A1DE782AD5708E0073F689 /* TransportStrategy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransportStrategy.swift; sourceTree = "<group>"; };
A9A557F42B7E3E5C0017ADA8 /* EphemeralPeerReceiver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EphemeralPeerReceiver.swift; sourceTree = "<group>"; };
A9A5F9A12ACB003D0083449F /* TunnelManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelManagerTests.swift; sourceTree = "<group>"; };
+ A9A60ED32DF6E5AC00CD9C3D /* UIHostingRootController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIHostingRootController.swift; sourceTree = "<group>"; };
A9A8A8EA2A262AB30086D569 /* FileCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileCache.swift; sourceTree = "<group>"; };
A9B6AC172ADE8F4300F7802A /* MigrationManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrationManagerTests.swift; sourceTree = "<group>"; };
A9B6AC192ADE8FBB00F7802A /* InMemorySettingsStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InMemorySettingsStore.swift; sourceTree = "<group>"; };
@@ -2392,6 +2391,7 @@
A9E034632ABB302000E59A5A /* UIEdgeInsets+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIEdgeInsets+Extensions.swift"; sourceTree = "<group>"; };
A9EB4F9C2B7FAB21002A2D7A /* EphemeralPeerNegotiator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EphemeralPeerNegotiator.swift; sourceTree = "<group>"; };
A9EC20E72A5D3A8C0040D56E /* CoordinatesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoordinatesTests.swift; sourceTree = "<group>"; };
+ A9EE85602DF1BE2900F2D769 /* TermsOfServiceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TermsOfServiceView.swift; sourceTree = "<group>"; };
A9F360332AAB626300F53531 /* VPNConnectionProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNConnectionProtocol.swift; sourceTree = "<group>"; };
E1187ABA289BBB850024E748 /* OutOfTimeViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OutOfTimeViewController.swift; sourceTree = "<group>"; };
E1187ABB289BBB850024E748 /* OutOfTimeContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OutOfTimeContentView.swift; sourceTree = "<group>"; };
@@ -3382,8 +3382,7 @@
583FE02229C1AC68006E85F9 /* TermsOfService */ = {
isa = PBXGroup;
children = (
- 584592602639B4A200EF967F /* TermsOfServiceContentView.swift */,
- 58A99ED2240014A0006599E9 /* TermsOfServiceViewController.swift */,
+ A9EE85602DF1BE2900F2D769 /* TermsOfServiceView.swift */,
);
path = TermsOfService;
sourceTree = "<group>";
@@ -4043,6 +4042,7 @@
58F3C0A3249CB069003E76BE /* HeaderBarView.swift */,
7A818F1E29F0305800C7F0F4 /* RootConfiguration.swift */,
587425C02299833500CA2045 /* RootContainerViewController.swift */,
+ A9A60ED32DF6E5AC00CD9C3D /* UIHostingRootController.swift */,
);
path = Root;
sourceTree = "<group>";
@@ -5948,7 +5948,7 @@
7A5869C32B5820CE00640D27 /* IPOverrideRepositoryTests.swift in Sources */,
A9A5FA392ACB05910083449F /* UIColor+Palette.swift in Sources */,
7A5468AD2C6B5E4B00590086 /* LocationRelays.swift in Sources */,
- F97C38C82DE48AAE006DCB08 /* Color+Mullvad.swift in Sources */,
+ A9EE855F2DF0893E00F2D769 /* Color+Mullvad.swift in Sources */,
A9A5FA3A2ACB05910083449F /* UIEdgeInsets+Extensions.swift in Sources */,
A9A5FA3B2ACB05910083449F /* UIMetrics.swift in Sources */,
58B07C182AEFDD6C00A09625 /* StoreTransactionLog.swift in Sources */,
@@ -6410,6 +6410,7 @@
58138E61294871C600684F0C /* DeviceDataThrottling.swift in Sources */,
7A6389ED2B7FADA1008E77E1 /* SettingsFieldValidationErrorConfiguration.swift in Sources */,
5878A279290954790096FC88 /* TunnelViewControllerInteractor.swift in Sources */,
+ A9A60ED42DF6E5AC00CD9C3D /* UIHostingRootController.swift in Sources */,
7A818F1F29F0305800C7F0F4 /* RootConfiguration.swift in Sources */,
7A9CCCBF2A96302800DD6A34 /* SettingsCoordinator.swift in Sources */,
58F70FE52AEA707800E6890E /* StoreTransactionLog.swift in Sources */,
@@ -6435,7 +6436,6 @@
7AA1309F2D007B2500640DF9 /* VisualEffectView.swift in Sources */,
7A0C0F632A979C4A0058EFCE /* Coordinator+Router.swift in Sources */,
7A6F2FAB2AFD3097006D0856 /* CustomDNSCellFactory.swift in Sources */,
- 58A99ED3240014A0006599E9 /* TermsOfServiceViewController.swift in Sources */,
7A6000FE2B628E9F001CF0D9 /* ListCellContentView.swift in Sources */,
4419AA8B2D2826E5001B13C9 /* DetailsView.swift in Sources */,
58CCA0162242560B004F3011 /* UIColor+Palette.swift in Sources */,
@@ -6556,6 +6556,7 @@
58CE5E64224146200008646E /* AppDelegate.swift in Sources */,
F9394EEC2DBF56B6009595EA /* Color+Mullvad.swift in Sources */,
F0DA87492A9CBA9F006044F1 /* AccountDeviceRow.swift in Sources */,
+ A9EE85612DF1BE2900F2D769 /* TermsOfServiceView.swift in Sources */,
58FF9FE42B075BDD00E4C97D /* EditAccessMethodItemIdentifier.swift in Sources */,
449E9A6F2D283C7400F8574A /* ButtonPanel.swift in Sources */,
4419AA8E2D2828A4001B13C9 /* HeaderView.swift in Sources */,
@@ -6610,7 +6611,6 @@
58F2E144276A13F300A79513 /* StartTunnelOperation.swift in Sources */,
58CCA01E2242787B004F3011 /* AccountTextField.swift in Sources */,
586E54FB27A2DF6D0029B88B /* SendTunnelProviderMessageOperation.swift in Sources */,
- 584592612639B4A200EF967F /* TermsOfServiceContentView.swift in Sources */,
5875960A26F371FC00BF6711 /* Tunnel+Messaging.swift in Sources */,
586C0D912B03D8A400E7CDD7 /* AccessMethodHeaderFooterReuseIdentifier.swift in Sources */,
F0B583D42D6DCE12007F5AE4 /* FilterDescriptor.swift in Sources */,
diff --git a/ios/MullvadVPN/Containers/Root/HeaderBarView.swift b/ios/MullvadVPN/Containers/Root/HeaderBarView.swift
index 3ea6fbd041..977296c532 100644
--- a/ios/MullvadVPN/Containers/Root/HeaderBarView.swift
+++ b/ios/MullvadVPN/Containers/Root/HeaderBarView.swift
@@ -30,7 +30,7 @@ class HeaderBarView: UIView {
private lazy var deviceNameLabel: UILabel = {
let label = UILabel()
- label.font = UIFont.systemFont(ofSize: 14)
+ label.font = .mullvadMiniSemiBold
label.textColor = UIColor(white: 1.0, alpha: 0.8)
label.setContentHuggingPriority(.defaultHigh, for: .horizontal)
label.setAccessibilityIdentifier(.headerDeviceNameLabel)
@@ -39,7 +39,7 @@ class HeaderBarView: UIView {
private lazy var timeLeftLabel: UILabel = {
let label = UILabel()
- label.font = UIFont.systemFont(ofSize: 14)
+ label.font = .mullvadMiniSemiBold
label.textColor = UIColor(white: 1.0, alpha: 0.8)
label.setContentHuggingPriority(.defaultLow, for: .horizontal)
return label
@@ -159,9 +159,9 @@ class HeaderBarView: UIView {
super.init(frame: frame)
directionalLayoutMargins = NSDirectionalEdgeInsets(
top: 0,
- leading: UIMetrics.contentLayoutMargins.leading,
+ leading: 16,
bottom: 0,
- trailing: UIMetrics.contentLayoutMargins.trailing
+ trailing: 16
)
accessibilityContainerType = .semanticGroup
diff --git a/ios/MullvadVPN/Containers/Root/UIHostingRootController.swift b/ios/MullvadVPN/Containers/Root/UIHostingRootController.swift
new file mode 100644
index 0000000000..15e98c66bc
--- /dev/null
+++ b/ios/MullvadVPN/Containers/Root/UIHostingRootController.swift
@@ -0,0 +1,34 @@
+//
+// UIHostingRootController.swift
+// MullvadVPN
+//
+// Created by Marco Nikic on 2025-06-09.
+// Copyright © 2025 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import SwiftUI
+
+@MainActor
+public final class UIHostingRootController<Content: View>: UIHostingController<Content>, RootContainment {
+ let preferredHeaderBarPresentation: HeaderBarPresentation
+ let prefersHeaderBarHidden: Bool
+ let prefersDeviceInfoBarHidden: Bool
+
+ init(
+ preferredHeaderBarPresentation: HeaderBarPresentation =
+ HeaderBarPresentation(style: .default, showsDivider: false),
+ prefersHeaderBarHidden: Bool = false,
+ prefersDeviceInfoBarHidden: Bool = true,
+ rootView: Content
+ ) {
+ self.preferredHeaderBarPresentation = preferredHeaderBarPresentation
+ self.prefersHeaderBarHidden = prefersHeaderBarHidden
+ self.prefersDeviceInfoBarHidden = prefersDeviceInfoBarHidden
+ super.init(rootView: rootView)
+ }
+
+ required init?(coder aDecoder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+}
diff --git a/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift b/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift
index b429c9053e..bd67bbb072 100644
--- a/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift
+++ b/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift
@@ -333,7 +333,7 @@ final class ApplicationCoordinator: Coordinator, Presenting, @preconcurrency Roo
private func presentTOS(animated: Bool, completion: @escaping (Coordinator) -> Void) {
let coordinator = TermsOfServiceCoordinator(navigationController: navigationContainer)
- coordinator.didFinish = { [weak self] _ in
+ coordinator.didAgreeToTermsOfService = { [weak self] in
self?.appPreferences.isAgreedToTermsOfService = true
self?.continueFlow(animated: true)
}
diff --git a/ios/MullvadVPN/Coordinators/TermsOfServiceCoordinator.swift b/ios/MullvadVPN/Coordinators/TermsOfServiceCoordinator.swift
index b359187ca6..042e21b7c6 100644
--- a/ios/MullvadVPN/Coordinators/TermsOfServiceCoordinator.swift
+++ b/ios/MullvadVPN/Coordinators/TermsOfServiceCoordinator.swift
@@ -7,6 +7,7 @@
//
import Routing
+import SwiftUI
import UIKit
class TermsOfServiceCoordinator: Coordinator, Presenting {
@@ -16,24 +17,16 @@ class TermsOfServiceCoordinator: Coordinator, Presenting {
navigationController
}
- var didFinish: ((TermsOfServiceCoordinator) -> Void)?
+ var didAgreeToTermsOfService: (() -> Void)?
init(navigationController: RootContainerViewController) {
self.navigationController = navigationController
}
func start() {
- let controller = TermsOfServiceViewController()
-
- controller.showPrivacyPolicy = { [weak self] in
- self?.presentChild(SafariCoordinator(url: ApplicationConfiguration.privacyPolicyURL), animated: true)
- }
-
- controller.completionHandler = { [weak self] in
- guard let self else { return }
- didFinish?(self)
- }
-
- navigationController.pushViewController(controller, animated: false)
+ let termsOfService = TermsOfServiceView(agreeToTermsAndServices: didAgreeToTermsOfService)
+ let hostingController = UIHostingRootController(rootView: termsOfService)
+ hostingController.view.setAccessibilityIdentifier(.termsOfServiceView)
+ navigationController.pushViewController(hostingController, animated: false)
}
}
diff --git a/ios/MullvadVPN/Extensions/UIImage+Assets.swift b/ios/MullvadVPN/Extensions/UIImage+Assets.swift
index ac0571b6da..60026ca90b 100644
--- a/ios/MullvadVPN/Extensions/UIImage+Assets.swift
+++ b/ios/MullvadVPN/Extensions/UIImage+Assets.swift
@@ -103,6 +103,10 @@ extension UIImage {
UIImage(named: "IconTickSml")!
}
+ static var iconExtLink: UIImage {
+ UIImage(named: "IconExtlink")!
+ }
+
static var checkboxSelected: UIImage {
UIImage(named: "CheckboxSelected")!
}
diff --git a/ios/MullvadVPN/Notifications/UI/NotificationBannerView.swift b/ios/MullvadVPN/Notifications/UI/NotificationBannerView.swift
index 1eff1a5d7e..54b2695b3e 100644
--- a/ios/MullvadVPN/Notifications/UI/NotificationBannerView.swift
+++ b/ios/MullvadVPN/Notifications/UI/NotificationBannerView.swift
@@ -13,7 +13,7 @@ final class NotificationBannerView: UIView {
private let titleLabel: UILabel = {
let textLabel = UILabel()
- textLabel.font = UIFont.systemFont(ofSize: 17, weight: .bold)
+ textLabel.font = .mullvadTinySemiBold
textLabel.textColor = UIColor.InAppNotificationBanner.titleColor
textLabel.numberOfLines = 0
textLabel.lineBreakMode = .byWordWrapping
@@ -23,7 +23,7 @@ final class NotificationBannerView: UIView {
private let bodyLabel: UILabel = {
let textLabel = UILabel()
- textLabel.font = UIFont.systemFont(ofSize: 17)
+ textLabel.font = .mullvadTiny
textLabel.textColor = UIColor.InAppNotificationBanner.bodyColor
textLabel.numberOfLines = 0
textLabel.lineBreakMode = .byWordWrapping
diff --git a/ios/MullvadVPN/UI appearance/Color+Mullvad.swift b/ios/MullvadVPN/UI appearance/Color+Mullvad.swift
index 6fad2f47b2..ab3987c722 100644
--- a/ios/MullvadVPN/UI appearance/Color+Mullvad.swift
+++ b/ios/MullvadVPN/UI appearance/Color+Mullvad.swift
@@ -12,6 +12,7 @@ extension Color {
static let mullvadTextPrimaryDisabled: Color = .mullvadTextPrimary.opacity(
0.2
)
+ static let secondaryTextColor: Color = UIColor.secondaryTextColor.color
enum MullvadButton {
static let primary: Color = .mullvadPrimaryColor
diff --git a/ios/MullvadVPN/View controllers/TermsOfService/TermsOfServiceContentView.swift b/ios/MullvadVPN/View controllers/TermsOfService/TermsOfServiceContentView.swift
deleted file mode 100644
index a72f1ff3f0..0000000000
--- a/ios/MullvadVPN/View controllers/TermsOfService/TermsOfServiceContentView.swift
+++ /dev/null
@@ -1,178 +0,0 @@
-//
-// TermsOfServiceContentView.swift
-// MullvadVPN
-//
-// Created by pronebird on 28/04/2021.
-// Copyright © 2025 Mullvad VPN AB. All rights reserved.
-//
-
-import UIKit
-
-class TermsOfServiceContentView: UIView {
- let titleLabel: UILabel = {
- let titleLabel = UILabel()
- titleLabel.translatesAutoresizingMaskIntoConstraints = false
- titleLabel.font = UIFont.systemFont(ofSize: 24, weight: .bold)
- titleLabel.numberOfLines = 0
- titleLabel.textColor = .white
- titleLabel.allowsDefaultTighteningForTruncation = true
- titleLabel.text = NSLocalizedString(
- "PRIVACY_NOTICE_HEADING",
- tableName: "TermsOfService",
- value: "Do you agree to remaining anonymous?",
- comment: ""
- )
- titleLabel.lineBreakMode = .byWordWrapping
- titleLabel.lineBreakStrategy = []
- return titleLabel
- }()
-
- let bodyLabel: UILabel = {
- let bodyLabel = UILabel()
-
- let message = NSMutableAttributedString(string: NSLocalizedString(
- "PRIVACY_NOTICE_BODY",
- tableName: "TermsOfService",
- value: """
- You have a right to privacy. That’s why we never store activity logs, don’t ask for personal \
- information, and encourage anonymous payments.
- In some situations, as outlined in our privacy policy, we might process personal data that you \
- choose to send, for example if you email us.
- We strongly believe in retaining as little data as possible because we want you to remain anonymous.
- """,
- comment: ""
- ))
- message.apply(paragraphStyle: .alert)
-
- bodyLabel.attributedText = message
- bodyLabel.translatesAutoresizingMaskIntoConstraints = false
- bodyLabel.font = UIFont.systemFont(ofSize: 18)
- bodyLabel.textColor = .white
- bodyLabel.numberOfLines = 0
-
- return bodyLabel
- }()
-
- let privacyPolicyLink: LinkButton = {
- let button = LinkButton()
- button.translatesAutoresizingMaskIntoConstraints = false
- button.titleString = NSLocalizedString(
- "PRIVACY_POLICY_LINK_TITLE",
- tableName: "TermsOfService",
- value: "Privacy policy",
- comment: ""
- )
- button.setImage(UIImage(named: "IconExtlink"), for: .normal)
- return button
- }()
-
- let agreeButton: AppButton = {
- let button = AppButton(style: .default)
- button.translatesAutoresizingMaskIntoConstraints = false
- button.setAccessibilityIdentifier(.agreeButton)
- button.setTitle(NSLocalizedString(
- "CONTINUE_BUTTON_TITLE",
- tableName: "TermsOfService",
- value: "Agree and continue",
- comment: ""
- ), for: .normal)
- return button
- }()
-
- let scrollView: UIScrollView = {
- let scrollView = UIScrollView()
- scrollView.translatesAutoresizingMaskIntoConstraints = false
- return scrollView
- }()
-
- let scrollContentContainer: UIView = {
- let contentView = UIView()
- contentView.translatesAutoresizingMaskIntoConstraints = false
- contentView.directionalLayoutMargins = UIMetrics.contentLayoutMargins
- return contentView
- }()
-
- let footerContainer: UIView = {
- let container = UIView()
- container.translatesAutoresizingMaskIntoConstraints = false
- container.directionalLayoutMargins = UIMetrics.contentLayoutMargins
- container.backgroundColor = .secondaryColor
- return container
- }()
-
- override init(frame: CGRect) {
- super.init(frame: frame)
-
- self.setAccessibilityIdentifier(.termsOfServiceView)
-
- addSubviews()
- }
-
- required init?(coder: NSCoder) {
- fatalError("init(coder:) has not been implemented")
- }
-
- // MARK: - Private
-
- private func addSubviews() {
- addSubview(scrollView)
- addSubview(footerContainer)
-
- scrollView.addSubview(scrollContentContainer)
- [titleLabel, bodyLabel, privacyPolicyLink].forEach { scrollContentContainer.addSubview($0) }
- footerContainer.addSubview(agreeButton)
-
- scrollView.setContentCompressionResistancePriority(.defaultLow, for: .vertical)
- footerContainer.setContentCompressionResistancePriority(.defaultHigh, for: .vertical)
-
- NSLayoutConstraint.activate([
- scrollView.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor),
- scrollView.leadingAnchor.constraint(equalTo: leadingAnchor),
- scrollView.trailingAnchor.constraint(equalTo: trailingAnchor),
-
- scrollContentContainer.widthAnchor.constraint(equalTo: scrollView.widthAnchor),
- scrollContentContainer.topAnchor.constraint(equalTo: scrollView.topAnchor),
- scrollContentContainer.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
- scrollContentContainer.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
- scrollContentContainer.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
-
- footerContainer.topAnchor.constraint(equalTo: scrollView.bottomAnchor),
- footerContainer.leadingAnchor.constraint(equalTo: leadingAnchor),
- footerContainer.trailingAnchor.constraint(equalTo: trailingAnchor),
- footerContainer.bottomAnchor.constraint(equalTo: bottomAnchor),
-
- agreeButton.topAnchor.constraint(equalTo: footerContainer.layoutMarginsGuide.topAnchor),
- agreeButton.leadingAnchor
- .constraint(equalTo: footerContainer.layoutMarginsGuide.leadingAnchor),
- agreeButton.trailingAnchor
- .constraint(equalTo: footerContainer.layoutMarginsGuide.trailingAnchor),
- agreeButton.bottomAnchor
- .constraint(equalTo: footerContainer.layoutMarginsGuide.bottomAnchor),
-
- titleLabel.topAnchor
- .constraint(equalTo: scrollContentContainer.layoutMarginsGuide.topAnchor),
- titleLabel.leadingAnchor
- .constraint(equalTo: scrollContentContainer.layoutMarginsGuide.leadingAnchor),
- titleLabel.trailingAnchor
- .constraint(equalTo: scrollContentContainer.layoutMarginsGuide.trailingAnchor),
-
- bodyLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 24),
- bodyLabel.leadingAnchor
- .constraint(equalTo: scrollContentContainer.layoutMarginsGuide.leadingAnchor),
- bodyLabel.trailingAnchor
- .constraint(equalTo: scrollContentContainer.layoutMarginsGuide.trailingAnchor),
-
- privacyPolicyLink.topAnchor.constraint(equalTo: bodyLabel.bottomAnchor, constant: 24),
- privacyPolicyLink.leadingAnchor
- .constraint(equalTo: scrollContentContainer.layoutMarginsGuide.leadingAnchor),
- privacyPolicyLink.trailingAnchor
- .constraint(
- lessThanOrEqualTo: scrollContentContainer.layoutMarginsGuide
- .trailingAnchor
- ),
- privacyPolicyLink.bottomAnchor
- .constraint(equalTo: scrollContentContainer.layoutMarginsGuide.bottomAnchor),
-
- ])
- }
-}
diff --git a/ios/MullvadVPN/View controllers/TermsOfService/TermsOfServiceView.swift b/ios/MullvadVPN/View controllers/TermsOfService/TermsOfServiceView.swift
new file mode 100644
index 0000000000..b95b9cb717
--- /dev/null
+++ b/ios/MullvadVPN/View controllers/TermsOfService/TermsOfServiceView.swift
@@ -0,0 +1,80 @@
+//
+// TermsOfServiceView.swift
+// MullvadVPN
+//
+// Created by Marco Nikic on 2025-06-05.
+// Copyright © 2025 Mullvad VPN AB. All rights reserved.
+//
+
+import SwiftUI
+
+struct TermsOfServiceView: View {
+ public var agreeToTermsAndServices: (() -> Void)?
+ let padding = EdgeInsets(top: 24, leading: 16, bottom: 24, trailing: 16)
+ @ScaledMetric(relativeTo: .footnote)
+ var imageHeight = 20
+
+ let termsOfService = LocalizedStringKey("""
+ You have a right to privacy. That’s why we never store activity logs, don’t ask for personal \
+ information, and encourage anonymous payments.
+
+ In some situations, as outlined in our privacy policy, we might process personal data that you \
+ choose to send, for example if you email us.
+
+ We strongly believe in retaining as little data as possible because we want you to remain anonymous.
+ """)
+
+ let privacyPolicyLink =
+ LocalizedStringKey(stringLiteral: "[Privacy Policy](\(ApplicationConfiguration.privacyPolicyLink))")
+ var scrollableContent: some View {
+ ScrollView {
+ Text(LocalizedStringKey("Do you agree to remaining anonymous?"))
+ .font(.mullvadLarge)
+ .foregroundStyle(.white)
+ .frame(maxWidth: .infinity, alignment: .leading)
+ .padding(.bottom, 16)
+ Text(termsOfService)
+ .font(.mullvadSmall)
+ .foregroundStyle(Color.secondaryTextColor)
+ }
+ .padding(padding)
+ }
+
+ var body: some View {
+ VStack(alignment: .leading) {
+ // Disable scrolling if the contents do not overflow
+ if #available(iOS 16.4, *) {
+ scrollableContent.scrollBounceBehavior(.basedOnSize)
+ } else {
+ scrollableContent
+ }
+ HStack {
+ Text(privacyPolicyLink)
+ .font(.mullvadSmall)
+ .underline(true, color: .white)
+ .foregroundStyle(.white)
+ .tint(.white)
+ Image(uiImage: UIImage.iconExtLink)
+ .resizable()
+ .scaledToFit()
+ .frame(height: imageHeight)
+ .foregroundStyle(.white)
+ }
+ .padding(padding)
+ MainButton(
+ text: LocalizedStringKey("Agree and continue"),
+ style: .default,
+ action: agreeToTermsAndServices ?? {}
+ )
+ .accessibilityIdentifier(AccessibilityIdentifier.agreeButton.asString)
+ .padding(padding)
+ .background(Color(UIColor.secondaryColor))
+ }
+ .background(Color(UIColor.primaryColor))
+ }
+}
+
+#Preview {
+ TermsOfServiceView()
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
+}
diff --git a/ios/MullvadVPN/View controllers/TermsOfService/TermsOfServiceViewController.swift b/ios/MullvadVPN/View controllers/TermsOfService/TermsOfServiceViewController.swift
deleted file mode 100644
index 29cb646826..0000000000
--- a/ios/MullvadVPN/View controllers/TermsOfService/TermsOfServiceViewController.swift
+++ /dev/null
@@ -1,65 +0,0 @@
-//
-// TermsOfServiceViewController.swift
-// MullvadVPN
-//
-// Created by pronebird on 21/02/2020.
-// Copyright © 2025 Mullvad VPN AB. All rights reserved.
-//
-
-import UIKit
-
-class TermsOfServiceViewController: UIViewController, RootContainment {
- var showPrivacyPolicy: (() -> Void)?
- var completionHandler: (() -> Void)?
-
- override var preferredStatusBarStyle: UIStatusBarStyle {
- .lightContent
- }
-
- var preferredHeaderBarPresentation: HeaderBarPresentation {
- HeaderBarPresentation(style: .default, showsDivider: false)
- }
-
- var prefersHeaderBarHidden: Bool {
- false
- }
-
- // MARK: - View lifecycle
-
- override func viewDidLoad() {
- super.viewDidLoad()
-
- let contentView = TermsOfServiceContentView()
- contentView.translatesAutoresizingMaskIntoConstraints = false
- contentView.agreeButton.addTarget(
- self,
- action: #selector(handleAgreeButton(_:)),
- for: .touchUpInside
- )
- contentView.privacyPolicyLink.addTarget(
- self,
- action: #selector(handlePrivacyPolicyButton(_:)),
- for: .touchUpInside
- )
-
- view.backgroundColor = .primaryColor
- view.addSubview(contentView)
-
- NSLayoutConstraint.activate([
- contentView.topAnchor.constraint(equalTo: view.topAnchor),
- contentView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
- contentView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
- contentView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
- ])
- }
-
- // MARK: - Actions
-
- @objc private func handlePrivacyPolicyButton(_ sender: Any) {
- showPrivacyPolicy?()
- }
-
- @objc private func handleAgreeButton(_ sender: Any) {
- completionHandler?()
- }
-}
diff --git a/ios/Shared/ApplicationConfiguration.swift b/ios/Shared/ApplicationConfiguration.swift
index 16240b9bea..7235425b23 100644
--- a/ios/Shared/ApplicationConfiguration.swift
+++ b/ios/Shared/ApplicationConfiguration.swift
@@ -64,7 +64,7 @@ enum ApplicationConfiguration {
static let logMaximumFileSize: UInt64 = 131_072 // 128 kB.
/// Privacy policy URL.
- static let privacyPolicyURL = URL(string: "https://\(Self.hostName)/help/privacy-policy/")!
+ static let privacyPolicyLink = "https://\(Self.hostName)/help/privacy-policy/"
/// Make a start regarding policy URL.
static let privacyGuidesURL = URL(string: "https://\(Self.hostName)/help/first-steps-towards-online-privacy/")!