summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorBug Magnet <marco.nikic@mullvad.net>2024-08-19 10:18:11 +0200
committerBug Magnet <marco.nikic@mullvad.net>2024-08-19 10:18:11 +0200
commited5b060f39f8739673ca9470f1fea24abc729211 (patch)
tree7434d7adc7fc74920fe4b90550ec4f1acff0d823
parentb575d1602769deaf4408bec75c0fdee4bf915ce8 (diff)
parent48b0a1b69ae24b11d3011308698f60e860025303 (diff)
downloadmullvadvpn-ed5b060f39f8739673ca9470f1fea24abc729211.tar.xz
mullvadvpn-ed5b060f39f8739673ca9470f1fea24abc729211.zip
Merge branch 'rework-the-restore-purchases-to-decrease-user-confusion-ios-721'
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj10
-rw-r--r--ios/MullvadVPN/Coordinators/AccountCoordinator.swift39
-rw-r--r--ios/MullvadVPN/View controllers/Account/AccountContentView.swift41
-rw-r--r--ios/MullvadVPN/View controllers/Account/AccountViewController.swift17
-rw-r--r--ios/MullvadVPN/View controllers/Account/RestorePurchasesView.swift82
-rw-r--r--ios/MullvadVPN/View controllers/Alert/AlertViewController.swift40
6 files changed, 171 insertions, 58 deletions
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj
index 26816ec231..2e42549f62 100644
--- a/ios/MullvadVPN.xcodeproj/project.pbxproj
+++ b/ios/MullvadVPN.xcodeproj/project.pbxproj
@@ -533,6 +533,7 @@
7A7907332BC0280A00B61F81 /* InterceptibleNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A7907322BC0280A00B61F81 /* InterceptibleNavigationController.swift */; };
7A7AD14F2BF21EF200B30B3C /* NameInputFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A7AD14D2BF21DCE00B30B3C /* NameInputFormatter.swift */; };
7A7AD28D29DC677800480EF1 /* FirstTimeLaunch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A7AD28C29DC677800480EF1 /* FirstTimeLaunch.swift */; };
+ 7A7B3AB62C6DE4DA00D4BCCE /* RestorePurchasesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A7B3AB52C6DE4DA00D4BCCE /* RestorePurchasesView.swift */; };
7A818F1F29F0305800C7F0F4 /* RootConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A818F1E29F0305800C7F0F4 /* RootConfiguration.swift */; };
7A83A0C62B29A750008B5CE7 /* APIAccessMethodsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A83A0C52B29A750008B5CE7 /* APIAccessMethodsTests.swift */; };
7A83C3FF2A55B72E00DFB83A /* MullvadVPNApp.xctestplan in Resources */ = {isa = PBXBuildFile; fileRef = 7A83C3FE2A55B72E00DFB83A /* MullvadVPNApp.xctestplan */; };
@@ -1848,6 +1849,7 @@
7A7907322BC0280A00B61F81 /* InterceptibleNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InterceptibleNavigationController.swift; sourceTree = "<group>"; };
7A7AD14D2BF21DCE00B30B3C /* NameInputFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NameInputFormatter.swift; sourceTree = "<group>"; };
7A7AD28C29DC677800480EF1 /* FirstTimeLaunch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirstTimeLaunch.swift; sourceTree = "<group>"; };
+ 7A7B3AB52C6DE4DA00D4BCCE /* RestorePurchasesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestorePurchasesView.swift; sourceTree = "<group>"; };
7A818F1E29F0305800C7F0F4 /* RootConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootConfiguration.swift; sourceTree = "<group>"; };
7A83A0C52B29A750008B5CE7 /* APIAccessMethodsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIAccessMethodsTests.swift; sourceTree = "<group>"; };
7A83C3FE2A55B72E00DFB83A /* MullvadVPNApp.xctestplan */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = MullvadVPNApp.xctestplan; sourceTree = "<group>"; };
@@ -2887,14 +2889,15 @@
isa = PBXGroup;
children = (
5896CEF126972DEB00B0FAE8 /* AccountContentView.swift */,
+ F0DA87482A9CBA9F006044F1 /* AccountDeviceRow.swift */,
+ F0DA87462A9CB9A2006044F1 /* AccountExpiryRow.swift */,
5878A27029091CF20096FC88 /* AccountInteractor.swift */,
+ F0DA874A2A9CBACB006044F1 /* AccountNumberRow.swift */,
58CCA01722426713004F3011 /* AccountViewController.swift */,
7A1A26422A2612AE00B978AA /* PaymentAlertPresenter.swift */,
5867771329097BCD006F721F /* PaymentState.swift */,
5867771529097C5B006F721F /* ProductState.swift */,
- F0DA87462A9CB9A2006044F1 /* AccountExpiryRow.swift */,
- F0DA87482A9CBA9F006044F1 /* AccountDeviceRow.swift */,
- F0DA874A2A9CBACB006044F1 /* AccountNumberRow.swift */,
+ 7A7B3AB52C6DE4DA00D4BCCE /* RestorePurchasesView.swift */,
);
path = Account;
sourceTree = "<group>";
@@ -5650,6 +5653,7 @@
E1187ABC289BBB850024E748 /* OutOfTimeViewController.swift in Sources */,
586C0D872B03D39600E7CDD7 /* AccessMethodCellReuseIdentifier.swift in Sources */,
7A9CCCBD2A96302800DD6A34 /* LoginCoordinator.swift in Sources */,
+ 7A7B3AB62C6DE4DA00D4BCCE /* RestorePurchasesView.swift in Sources */,
58293FB125124117005D0BB5 /* CustomTextField.swift in Sources */,
F09A29822A9F8AD200EA3B6F /* RedeemVoucherInteractor.swift in Sources */,
58138E61294871C600684F0C /* DeviceDataThrottling.swift in Sources */,
diff --git a/ios/MullvadVPN/Coordinators/AccountCoordinator.swift b/ios/MullvadVPN/Coordinators/AccountCoordinator.swift
index 839126f4fc..ee2277ee38 100644
--- a/ios/MullvadVPN/Coordinators/AccountCoordinator.swift
+++ b/ios/MullvadVPN/Coordinators/AccountCoordinator.swift
@@ -65,6 +65,8 @@ final class AccountCoordinator: Coordinator, Presentable, Presenting {
navigateToRedeemVoucher()
case .navigateToDeleteAccount:
navigateToDeleteAccount()
+ case .restorePurchasesInfo:
+ showRestorePurchasesInfo()
}
}
@@ -188,4 +190,41 @@ final class AccountCoordinator: Coordinator, Presentable, Presenting {
let presenter = AlertPresenter(context: self)
presenter.showAlert(presentation: presentation, animated: true)
}
+
+ private func showRestorePurchasesInfo() {
+ let message = NSLocalizedString(
+ "RESTORE_PURCHASES_DIALOG_MESSAGE",
+ tableName: "Account",
+ value: """
+ You can use the “restore purchases” function to check for any in-app payments \
+ made via Apple services. If there is a payment that has not been credited, it will \
+ add the time to the currently logged in Mullvad account.
+ """,
+ comment: ""
+ )
+
+ let presentation = AlertPresentation(
+ id: "account-device-info-alert",
+ icon: .info,
+ title: NSLocalizedString(
+ "RESTORE_PURCHASES_DIALOG_TITLE",
+ tableName: "Account",
+ value: "If you haven’t received additional VPN time after purchasing",
+ comment: ""
+ ),
+ message: message,
+ buttons: [AlertAction(
+ title: NSLocalizedString(
+ "RESTORE_PURCHASES_DIALOG_OK_ACTION",
+ tableName: "Account",
+ value: "Got it!",
+ comment: ""
+ ),
+ style: .default
+ )]
+ )
+
+ let presenter = AlertPresenter(context: self)
+ presenter.showAlert(presentation: presentation, animated: true)
+ }
}
diff --git a/ios/MullvadVPN/View controllers/Account/AccountContentView.swift b/ios/MullvadVPN/View controllers/Account/AccountContentView.swift
index 3d8ae81512..3e1cac3e51 100644
--- a/ios/MullvadVPN/View controllers/Account/AccountContentView.swift
+++ b/ios/MullvadVPN/View controllers/Account/AccountContentView.swift
@@ -11,27 +11,12 @@ import UIKit
class AccountContentView: UIView {
let purchaseButton: InAppPurchaseButton = {
let button = InAppPurchaseButton()
- button.translatesAutoresizingMaskIntoConstraints = false
button.accessibilityIdentifier = .purchaseButton
return button
}()
- let restorePurchasesButton: AppButton = {
- let button = AppButton(style: .default)
- button.translatesAutoresizingMaskIntoConstraints = false
- button.accessibilityIdentifier = .restorePurchasesButton
- button.setTitle(NSLocalizedString(
- "RESTORE_PURCHASES_BUTTON_TITLE",
- tableName: "Account",
- value: "Restore purchases",
- comment: ""
- ), for: .normal)
- return button
- }()
-
let redeemVoucherButton: AppButton = {
let button = AppButton(style: .success)
- button.translatesAutoresizingMaskIntoConstraints = false
button.accessibilityIdentifier = .redeemVoucherButton
button.setTitle(NSLocalizedString(
"REDEEM_VOUCHER_BUTTON_TITLE",
@@ -44,7 +29,6 @@ class AccountContentView: UIView {
let logoutButton: AppButton = {
let button = AppButton(style: .danger)
- button.translatesAutoresizingMaskIntoConstraints = false
button.accessibilityIdentifier = .logoutButton
button.setTitle(NSLocalizedString(
"LOGOUT_BUTTON_TITLE",
@@ -57,7 +41,6 @@ class AccountContentView: UIView {
let deleteButton: AppButton = {
let button = AppButton(style: .danger)
- button.translatesAutoresizingMaskIntoConstraints = false
button.accessibilityIdentifier = .deleteButton
button.setTitle(NSLocalizedString(
"DELETE_BUTTON_TITLE",
@@ -69,21 +52,19 @@ class AccountContentView: UIView {
}()
let accountDeviceRow: AccountDeviceRow = {
- let view = AccountDeviceRow()
- view.translatesAutoresizingMaskIntoConstraints = false
- return view
+ AccountDeviceRow()
}()
let accountTokenRowView: AccountNumberRow = {
- let view = AccountNumberRow()
- view.translatesAutoresizingMaskIntoConstraints = false
- return view
+ AccountNumberRow()
}()
let accountExpiryRowView: AccountExpiryRow = {
- let view = AccountExpiryRow()
- view.translatesAutoresizingMaskIntoConstraints = false
- return view
+ AccountExpiryRow()
+ }()
+
+ let restorePurchasesView: RestorePurchasesView = {
+ RestorePurchasesView()
}()
lazy var contentStackView: UIStackView = {
@@ -92,10 +73,11 @@ class AccountContentView: UIView {
accountDeviceRow,
accountTokenRowView,
accountExpiryRowView,
+ restorePurchasesView,
])
- stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .vertical
- stackView.spacing = UIMetrics.padding8
+ stackView.spacing = UIMetrics.padding24
+ stackView.setCustomSpacing(UIMetrics.padding8, after: accountExpiryRowView)
return stackView
}()
@@ -106,15 +88,12 @@ class AccountContentView: UIView {
#endif
arrangedSubviews.append(contentsOf: [
purchaseButton,
- restorePurchasesButton,
logoutButton,
deleteButton,
])
let stackView = UIStackView(arrangedSubviews: arrangedSubviews)
- stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .vertical
stackView.spacing = UIMetrics.padding16
- stackView.setCustomSpacing(UIMetrics.interButtonSpacing, after: restorePurchasesButton)
return stackView
}()
diff --git a/ios/MullvadVPN/View controllers/Account/AccountViewController.swift b/ios/MullvadVPN/View controllers/Account/AccountViewController.swift
index 888fcd8d05..f39a228e66 100644
--- a/ios/MullvadVPN/View controllers/Account/AccountViewController.swift
+++ b/ios/MullvadVPN/View controllers/Account/AccountViewController.swift
@@ -20,6 +20,7 @@ enum AccountViewControllerAction {
case logOut
case navigateToVoucher
case navigateToDeleteAccount
+ case restorePurchasesInfo
}
class AccountViewController: UIViewController {
@@ -81,6 +82,14 @@ class AccountViewController: UIViewController {
self?.actionHandler?(.deviceInfo)
}
+ contentView.restorePurchasesView.restoreButtonAction = { [weak self] in
+ self?.restorePurchases()
+ }
+
+ contentView.restorePurchasesView.infoButtonAction = { [weak self] in
+ self?.actionHandler?(.restorePurchasesInfo)
+ }
+
interactor.didReceiveDeviceState = { [weak self] deviceState in
self?.updateView(from: deviceState)
}
@@ -126,16 +135,12 @@ class AccountViewController: UIViewController {
for: .touchUpInside
)
- contentView.restorePurchasesButton.addTarget(
- self,
- action: #selector(restorePurchases),
- for: .touchUpInside
- )
contentView.purchaseButton.addTarget(
self,
action: #selector(doPurchase),
for: .touchUpInside
)
+
contentView.logoutButton.addTarget(self, action: #selector(logOut), for: .touchUpInside)
contentView.deleteButton.addTarget(self, action: #selector(deleteAccount), for: .touchUpInside)
@@ -193,7 +198,7 @@ class AccountViewController: UIViewController {
purchaseButton.isEnabled = productState.isReceived && isInteractionEnabled
contentView.accountDeviceRow.setButtons(enabled: isInteractionEnabled)
contentView.accountTokenRowView.setButtons(enabled: isInteractionEnabled)
- contentView.restorePurchasesButton.isEnabled = isInteractionEnabled
+ contentView.restorePurchasesView.setButtons(enabled: isInteractionEnabled)
contentView.logoutButton.isEnabled = isInteractionEnabled
contentView.redeemVoucherButton.isEnabled = isInteractionEnabled
contentView.deleteButton.isEnabled = isInteractionEnabled
diff --git a/ios/MullvadVPN/View controllers/Account/RestorePurchasesView.swift b/ios/MullvadVPN/View controllers/Account/RestorePurchasesView.swift
new file mode 100644
index 0000000000..c683772126
--- /dev/null
+++ b/ios/MullvadVPN/View controllers/Account/RestorePurchasesView.swift
@@ -0,0 +1,82 @@
+//
+// RestorePurchasesView.swift
+// MullvadVPN
+//
+// Created by Jon Petersson on 2024-08-15.
+// Copyright © 2024 Mullvad VPN AB. All rights reserved.
+//
+
+import UIKit
+
+class RestorePurchasesView: UIView {
+ var restoreButtonAction: (() -> Void)?
+ var infoButtonAction: (() -> Void)?
+
+ private lazy var contentView: UIStackView = {
+ let stackView = UIStackView(arrangedSubviews: [
+ restoreButton,
+ infoButton,
+ UIView(), // Pushes the other views to the left.
+ ])
+ stackView.spacing = UIMetrics.padding8
+ return stackView
+ }()
+
+ private lazy var restoreButton: UILabel = {
+ let label = UILabel()
+ label.accessibilityIdentifier = .restorePurchasesButton
+ label.attributedText = makeAttributedString()
+ label.isUserInteractionEnabled = true
+ label.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(didTapRestoreButton)))
+ return label
+ }()
+
+ private lazy var infoButton: UIButton = {
+ let button = IncreasedHitButton(type: .custom)
+ button.setImage(UIImage(resource: .iconInfo), for: .normal)
+ button.tintColor = .white
+ button.addTarget(self, action: #selector(didTapInfoButton), for: .touchUpInside)
+ return button
+ }()
+
+ override init(frame: CGRect) {
+ super.init(frame: frame)
+
+ addConstrainedSubviews([contentView]) {
+ contentView.pinEdgesToSuperview()
+ }
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ func setButtons(enabled: Bool) {
+ restoreButton.isUserInteractionEnabled = enabled
+ restoreButton.alpha = enabled ? 1 : 0.5
+ infoButton.isEnabled = enabled
+ }
+
+ private func makeAttributedString() -> NSAttributedString {
+ let text = NSLocalizedString(
+ "RESTORE_PURCHASES_BUTTON_TITLE",
+ tableName: "Account",
+ value: "Restore purchases",
+ comment: ""
+ )
+
+ return NSAttributedString(string: text, attributes: [
+ .font: UIFont.systemFont(ofSize: 13, weight: .semibold),
+ .foregroundColor: UIColor.white,
+ .underlineStyle: NSUnderlineStyle.single.rawValue,
+ ])
+ }
+
+ @objc private func didTapRestoreButton() {
+ restoreButtonAction?()
+ }
+
+ @objc private func didTapInfoButton() {
+ infoButtonAction?()
+ }
+}
diff --git a/ios/MullvadVPN/View controllers/Alert/AlertViewController.swift b/ios/MullvadVPN/View controllers/Alert/AlertViewController.swift
index cc717a6180..9a4d26ae14 100644
--- a/ios/MullvadVPN/View controllers/Alert/AlertViewController.swift
+++ b/ios/MullvadVPN/View controllers/Alert/AlertViewController.swift
@@ -161,9 +161,6 @@ class AlertViewController: UIViewController {
viewContainer.centerXAnchor.constraint(equalTo: view.centerXAnchor)
viewContainer.centerYAnchor.constraint(equalTo: view.centerYAnchor)
- viewContainer.widthAnchor
- .constraint(lessThanOrEqualToConstant: UIMetrics.preferredFormSheetContentSize.width)
-
viewContainer.topAnchor
.constraint(greaterThanOrEqualTo: view.layoutMarginsGuide.topAnchor)
.withPriority(.defaultHigh)
@@ -172,13 +169,20 @@ class AlertViewController: UIViewController {
.constraint(greaterThanOrEqualTo: viewContainer.bottomAnchor)
.withPriority(.defaultHigh)
- viewContainer.leadingAnchor
+ let leadingConstraint = viewContainer.leadingAnchor
.constraint(equalTo: view.layoutMarginsGuide.leadingAnchor)
- .withPriority(.defaultHigh)
-
- view.layoutMarginsGuide.trailingAnchor
+ let trailingConstraint = view.layoutMarginsGuide.trailingAnchor
.constraint(equalTo: viewContainer.trailingAnchor)
- .withPriority(.defaultHigh)
+
+ if traitCollection.userInterfaceIdiom == .pad {
+ viewContainer.widthAnchor
+ .constraint(lessThanOrEqualToConstant: UIMetrics.preferredFormSheetContentSize.width)
+ leadingConstraint.withPriority(.defaultHigh)
+ trailingConstraint.withPriority(.defaultHigh)
+ } else {
+ leadingConstraint
+ trailingConstraint
+ }
}
}
@@ -195,18 +199,18 @@ class AlertViewController: UIViewController {
}
private func addHeader(_ title: String) {
- let header = UILabel()
+ let label = UILabel()
- header.text = title
- header.font = .preferredFont(forTextStyle: .largeTitle, weight: .bold)
- header.textColor = .white
- header.adjustsFontForContentSizeCategory = true
- header.textAlignment = .center
- header.numberOfLines = 0
- header.accessibilityIdentifier = .alertTitle
+ label.text = title
+ label.font = .preferredFont(forTextStyle: .largeTitle, weight: .bold)
+ label.textColor = .white
+ label.adjustsFontForContentSizeCategory = true
+ label.textAlignment = .center
+ label.numberOfLines = 0
+ label.accessibilityIdentifier = .alertTitle
- contentView.addArrangedSubview(header)
- contentView.setCustomSpacing(16, after: header)
+ contentView.addArrangedSubview(label)
+ contentView.setCustomSpacing(16, after: label)
}
private func addTitle(_ title: String) {