Skip to content

Commit ed5b060

Browse files
committed
Merge branch 'rework-the-restore-purchases-to-decrease-user-confusion-ios-721'
2 parents b575d16 + 48b0a1b commit ed5b060

File tree

6 files changed

+173
-60
lines changed

6 files changed

+173
-60
lines changed

ios/MullvadVPN.xcodeproj/project.pbxproj

+7-3
Original file line numberDiff line numberDiff line change
@@ -533,6 +533,7 @@
533533
7A7907332BC0280A00B61F81 /* InterceptibleNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A7907322BC0280A00B61F81 /* InterceptibleNavigationController.swift */; };
534534
7A7AD14F2BF21EF200B30B3C /* NameInputFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A7AD14D2BF21DCE00B30B3C /* NameInputFormatter.swift */; };
535535
7A7AD28D29DC677800480EF1 /* FirstTimeLaunch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A7AD28C29DC677800480EF1 /* FirstTimeLaunch.swift */; };
536+
7A7B3AB62C6DE4DA00D4BCCE /* RestorePurchasesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A7B3AB52C6DE4DA00D4BCCE /* RestorePurchasesView.swift */; };
536537
7A818F1F29F0305800C7F0F4 /* RootConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A818F1E29F0305800C7F0F4 /* RootConfiguration.swift */; };
537538
7A83A0C62B29A750008B5CE7 /* APIAccessMethodsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A83A0C52B29A750008B5CE7 /* APIAccessMethodsTests.swift */; };
538539
7A83C3FF2A55B72E00DFB83A /* MullvadVPNApp.xctestplan in Resources */ = {isa = PBXBuildFile; fileRef = 7A83C3FE2A55B72E00DFB83A /* MullvadVPNApp.xctestplan */; };
@@ -1848,6 +1849,7 @@
18481849
7A7907322BC0280A00B61F81 /* InterceptibleNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InterceptibleNavigationController.swift; sourceTree = "<group>"; };
18491850
7A7AD14D2BF21DCE00B30B3C /* NameInputFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NameInputFormatter.swift; sourceTree = "<group>"; };
18501851
7A7AD28C29DC677800480EF1 /* FirstTimeLaunch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirstTimeLaunch.swift; sourceTree = "<group>"; };
1852+
7A7B3AB52C6DE4DA00D4BCCE /* RestorePurchasesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestorePurchasesView.swift; sourceTree = "<group>"; };
18511853
7A818F1E29F0305800C7F0F4 /* RootConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootConfiguration.swift; sourceTree = "<group>"; };
18521854
7A83A0C52B29A750008B5CE7 /* APIAccessMethodsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIAccessMethodsTests.swift; sourceTree = "<group>"; };
18531855
7A83C3FE2A55B72E00DFB83A /* MullvadVPNApp.xctestplan */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = MullvadVPNApp.xctestplan; sourceTree = "<group>"; };
@@ -2887,14 +2889,15 @@
28872889
isa = PBXGroup;
28882890
children = (
28892891
5896CEF126972DEB00B0FAE8 /* AccountContentView.swift */,
2892+
F0DA87482A9CBA9F006044F1 /* AccountDeviceRow.swift */,
2893+
F0DA87462A9CB9A2006044F1 /* AccountExpiryRow.swift */,
28902894
5878A27029091CF20096FC88 /* AccountInteractor.swift */,
2895+
F0DA874A2A9CBACB006044F1 /* AccountNumberRow.swift */,
28912896
58CCA01722426713004F3011 /* AccountViewController.swift */,
28922897
7A1A26422A2612AE00B978AA /* PaymentAlertPresenter.swift */,
28932898
5867771329097BCD006F721F /* PaymentState.swift */,
28942899
5867771529097C5B006F721F /* ProductState.swift */,
2895-
F0DA87462A9CB9A2006044F1 /* AccountExpiryRow.swift */,
2896-
F0DA87482A9CBA9F006044F1 /* AccountDeviceRow.swift */,
2897-
F0DA874A2A9CBACB006044F1 /* AccountNumberRow.swift */,
2900+
7A7B3AB52C6DE4DA00D4BCCE /* RestorePurchasesView.swift */,
28982901
);
28992902
path = Account;
29002903
sourceTree = "<group>";
@@ -5650,6 +5653,7 @@
56505653
E1187ABC289BBB850024E748 /* OutOfTimeViewController.swift in Sources */,
56515654
586C0D872B03D39600E7CDD7 /* AccessMethodCellReuseIdentifier.swift in Sources */,
56525655
7A9CCCBD2A96302800DD6A34 /* LoginCoordinator.swift in Sources */,
5656+
7A7B3AB62C6DE4DA00D4BCCE /* RestorePurchasesView.swift in Sources */,
56535657
58293FB125124117005D0BB5 /* CustomTextField.swift in Sources */,
56545658
F09A29822A9F8AD200EA3B6F /* RedeemVoucherInteractor.swift in Sources */,
56555659
58138E61294871C600684F0C /* DeviceDataThrottling.swift in Sources */,

ios/MullvadVPN/Coordinators/AccountCoordinator.swift

+39
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ final class AccountCoordinator: Coordinator, Presentable, Presenting {
6565
navigateToRedeemVoucher()
6666
case .navigateToDeleteAccount:
6767
navigateToDeleteAccount()
68+
case .restorePurchasesInfo:
69+
showRestorePurchasesInfo()
6870
}
6971
}
7072

@@ -188,4 +190,41 @@ final class AccountCoordinator: Coordinator, Presentable, Presenting {
188190
let presenter = AlertPresenter(context: self)
189191
presenter.showAlert(presentation: presentation, animated: true)
190192
}
193+
194+
private func showRestorePurchasesInfo() {
195+
let message = NSLocalizedString(
196+
"RESTORE_PURCHASES_DIALOG_MESSAGE",
197+
tableName: "Account",
198+
value: """
199+
You can use the “restore purchases” function to check for any in-app payments \
200+
made via Apple services. If there is a payment that has not been credited, it will \
201+
add the time to the currently logged in Mullvad account.
202+
""",
203+
comment: ""
204+
)
205+
206+
let presentation = AlertPresentation(
207+
id: "account-device-info-alert",
208+
icon: .info,
209+
title: NSLocalizedString(
210+
"RESTORE_PURCHASES_DIALOG_TITLE",
211+
tableName: "Account",
212+
value: "If you haven’t received additional VPN time after purchasing",
213+
comment: ""
214+
),
215+
message: message,
216+
buttons: [AlertAction(
217+
title: NSLocalizedString(
218+
"RESTORE_PURCHASES_DIALOG_OK_ACTION",
219+
tableName: "Account",
220+
value: "Got it!",
221+
comment: ""
222+
),
223+
style: .default
224+
)]
225+
)
226+
227+
let presenter = AlertPresenter(context: self)
228+
presenter.showAlert(presentation: presentation, animated: true)
229+
}
191230
}

ios/MullvadVPN/View controllers/Account/AccountContentView.swift

+10-31
Original file line numberDiff line numberDiff line change
@@ -11,27 +11,12 @@ import UIKit
1111
class AccountContentView: UIView {
1212
let purchaseButton: InAppPurchaseButton = {
1313
let button = InAppPurchaseButton()
14-
button.translatesAutoresizingMaskIntoConstraints = false
1514
button.accessibilityIdentifier = .purchaseButton
1615
return button
1716
}()
1817

19-
let restorePurchasesButton: AppButton = {
20-
let button = AppButton(style: .default)
21-
button.translatesAutoresizingMaskIntoConstraints = false
22-
button.accessibilityIdentifier = .restorePurchasesButton
23-
button.setTitle(NSLocalizedString(
24-
"RESTORE_PURCHASES_BUTTON_TITLE",
25-
tableName: "Account",
26-
value: "Restore purchases",
27-
comment: ""
28-
), for: .normal)
29-
return button
30-
}()
31-
3218
let redeemVoucherButton: AppButton = {
3319
let button = AppButton(style: .success)
34-
button.translatesAutoresizingMaskIntoConstraints = false
3520
button.accessibilityIdentifier = .redeemVoucherButton
3621
button.setTitle(NSLocalizedString(
3722
"REDEEM_VOUCHER_BUTTON_TITLE",
@@ -44,7 +29,6 @@ class AccountContentView: UIView {
4429

4530
let logoutButton: AppButton = {
4631
let button = AppButton(style: .danger)
47-
button.translatesAutoresizingMaskIntoConstraints = false
4832
button.accessibilityIdentifier = .logoutButton
4933
button.setTitle(NSLocalizedString(
5034
"LOGOUT_BUTTON_TITLE",
@@ -57,7 +41,6 @@ class AccountContentView: UIView {
5741

5842
let deleteButton: AppButton = {
5943
let button = AppButton(style: .danger)
60-
button.translatesAutoresizingMaskIntoConstraints = false
6144
button.accessibilityIdentifier = .deleteButton
6245
button.setTitle(NSLocalizedString(
6346
"DELETE_BUTTON_TITLE",
@@ -69,21 +52,19 @@ class AccountContentView: UIView {
6952
}()
7053

7154
let accountDeviceRow: AccountDeviceRow = {
72-
let view = AccountDeviceRow()
73-
view.translatesAutoresizingMaskIntoConstraints = false
74-
return view
55+
AccountDeviceRow()
7556
}()
7657

7758
let accountTokenRowView: AccountNumberRow = {
78-
let view = AccountNumberRow()
79-
view.translatesAutoresizingMaskIntoConstraints = false
80-
return view
59+
AccountNumberRow()
8160
}()
8261

8362
let accountExpiryRowView: AccountExpiryRow = {
84-
let view = AccountExpiryRow()
85-
view.translatesAutoresizingMaskIntoConstraints = false
86-
return view
63+
AccountExpiryRow()
64+
}()
65+
66+
let restorePurchasesView: RestorePurchasesView = {
67+
RestorePurchasesView()
8768
}()
8869

8970
lazy var contentStackView: UIStackView = {
@@ -92,10 +73,11 @@ class AccountContentView: UIView {
9273
accountDeviceRow,
9374
accountTokenRowView,
9475
accountExpiryRowView,
76+
restorePurchasesView,
9577
])
96-
stackView.translatesAutoresizingMaskIntoConstraints = false
9778
stackView.axis = .vertical
98-
stackView.spacing = UIMetrics.padding8
79+
stackView.spacing = UIMetrics.padding24
80+
stackView.setCustomSpacing(UIMetrics.padding8, after: accountExpiryRowView)
9981
return stackView
10082
}()
10183

@@ -106,15 +88,12 @@ class AccountContentView: UIView {
10688
#endif
10789
arrangedSubviews.append(contentsOf: [
10890
purchaseButton,
109-
restorePurchasesButton,
11091
logoutButton,
11192
deleteButton,
11293
])
11394
let stackView = UIStackView(arrangedSubviews: arrangedSubviews)
114-
stackView.translatesAutoresizingMaskIntoConstraints = false
11595
stackView.axis = .vertical
11696
stackView.spacing = UIMetrics.padding16
117-
stackView.setCustomSpacing(UIMetrics.interButtonSpacing, after: restorePurchasesButton)
11897
return stackView
11998
}()
12099

ios/MullvadVPN/View controllers/Account/AccountViewController.swift

+11-6
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ enum AccountViewControllerAction {
2020
case logOut
2121
case navigateToVoucher
2222
case navigateToDeleteAccount
23+
case restorePurchasesInfo
2324
}
2425

2526
class AccountViewController: UIViewController {
@@ -81,6 +82,14 @@ class AccountViewController: UIViewController {
8182
self?.actionHandler?(.deviceInfo)
8283
}
8384

85+
contentView.restorePurchasesView.restoreButtonAction = { [weak self] in
86+
self?.restorePurchases()
87+
}
88+
89+
contentView.restorePurchasesView.infoButtonAction = { [weak self] in
90+
self?.actionHandler?(.restorePurchasesInfo)
91+
}
92+
8493
interactor.didReceiveDeviceState = { [weak self] deviceState in
8594
self?.updateView(from: deviceState)
8695
}
@@ -126,16 +135,12 @@ class AccountViewController: UIViewController {
126135
for: .touchUpInside
127136
)
128137

129-
contentView.restorePurchasesButton.addTarget(
130-
self,
131-
action: #selector(restorePurchases),
132-
for: .touchUpInside
133-
)
134138
contentView.purchaseButton.addTarget(
135139
self,
136140
action: #selector(doPurchase),
137141
for: .touchUpInside
138142
)
143+
139144
contentView.logoutButton.addTarget(self, action: #selector(logOut), for: .touchUpInside)
140145

141146
contentView.deleteButton.addTarget(self, action: #selector(deleteAccount), for: .touchUpInside)
@@ -193,7 +198,7 @@ class AccountViewController: UIViewController {
193198
purchaseButton.isEnabled = productState.isReceived && isInteractionEnabled
194199
contentView.accountDeviceRow.setButtons(enabled: isInteractionEnabled)
195200
contentView.accountTokenRowView.setButtons(enabled: isInteractionEnabled)
196-
contentView.restorePurchasesButton.isEnabled = isInteractionEnabled
201+
contentView.restorePurchasesView.setButtons(enabled: isInteractionEnabled)
197202
contentView.logoutButton.isEnabled = isInteractionEnabled
198203
contentView.redeemVoucherButton.isEnabled = isInteractionEnabled
199204
contentView.deleteButton.isEnabled = isInteractionEnabled
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
//
2+
// RestorePurchasesView.swift
3+
// MullvadVPN
4+
//
5+
// Created by Jon Petersson on 2024-08-15.
6+
// Copyright © 2024 Mullvad VPN AB. All rights reserved.
7+
//
8+
9+
import UIKit
10+
11+
class RestorePurchasesView: UIView {
12+
var restoreButtonAction: (() -> Void)?
13+
var infoButtonAction: (() -> Void)?
14+
15+
private lazy var contentView: UIStackView = {
16+
let stackView = UIStackView(arrangedSubviews: [
17+
restoreButton,
18+
infoButton,
19+
UIView(), // Pushes the other views to the left.
20+
])
21+
stackView.spacing = UIMetrics.padding8
22+
return stackView
23+
}()
24+
25+
private lazy var restoreButton: UILabel = {
26+
let label = UILabel()
27+
label.accessibilityIdentifier = .restorePurchasesButton
28+
label.attributedText = makeAttributedString()
29+
label.isUserInteractionEnabled = true
30+
label.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(didTapRestoreButton)))
31+
return label
32+
}()
33+
34+
private lazy var infoButton: UIButton = {
35+
let button = IncreasedHitButton(type: .custom)
36+
button.setImage(UIImage(resource: .iconInfo), for: .normal)
37+
button.tintColor = .white
38+
button.addTarget(self, action: #selector(didTapInfoButton), for: .touchUpInside)
39+
return button
40+
}()
41+
42+
override init(frame: CGRect) {
43+
super.init(frame: frame)
44+
45+
addConstrainedSubviews([contentView]) {
46+
contentView.pinEdgesToSuperview()
47+
}
48+
}
49+
50+
required init?(coder: NSCoder) {
51+
fatalError("init(coder:) has not been implemented")
52+
}
53+
54+
func setButtons(enabled: Bool) {
55+
restoreButton.isUserInteractionEnabled = enabled
56+
restoreButton.alpha = enabled ? 1 : 0.5
57+
infoButton.isEnabled = enabled
58+
}
59+
60+
private func makeAttributedString() -> NSAttributedString {
61+
let text = NSLocalizedString(
62+
"RESTORE_PURCHASES_BUTTON_TITLE",
63+
tableName: "Account",
64+
value: "Restore purchases",
65+
comment: ""
66+
)
67+
68+
return NSAttributedString(string: text, attributes: [
69+
.font: UIFont.systemFont(ofSize: 13, weight: .semibold),
70+
.foregroundColor: UIColor.white,
71+
.underlineStyle: NSUnderlineStyle.single.rawValue,
72+
])
73+
}
74+
75+
@objc private func didTapRestoreButton() {
76+
restoreButtonAction?()
77+
}
78+
79+
@objc private func didTapInfoButton() {
80+
infoButtonAction?()
81+
}
82+
}

ios/MullvadVPN/View controllers/Alert/AlertViewController.swift

+24-20
Original file line numberDiff line numberDiff line change
@@ -161,9 +161,6 @@ class AlertViewController: UIViewController {
161161
viewContainer.centerXAnchor.constraint(equalTo: view.centerXAnchor)
162162
viewContainer.centerYAnchor.constraint(equalTo: view.centerYAnchor)
163163

164-
viewContainer.widthAnchor
165-
.constraint(lessThanOrEqualToConstant: UIMetrics.preferredFormSheetContentSize.width)
166-
167164
viewContainer.topAnchor
168165
.constraint(greaterThanOrEqualTo: view.layoutMarginsGuide.topAnchor)
169166
.withPriority(.defaultHigh)
@@ -172,13 +169,20 @@ class AlertViewController: UIViewController {
172169
.constraint(greaterThanOrEqualTo: viewContainer.bottomAnchor)
173170
.withPriority(.defaultHigh)
174171

175-
viewContainer.leadingAnchor
172+
let leadingConstraint = viewContainer.leadingAnchor
176173
.constraint(equalTo: view.layoutMarginsGuide.leadingAnchor)
177-
.withPriority(.defaultHigh)
178-
179-
view.layoutMarginsGuide.trailingAnchor
174+
let trailingConstraint = view.layoutMarginsGuide.trailingAnchor
180175
.constraint(equalTo: viewContainer.trailingAnchor)
181-
.withPriority(.defaultHigh)
176+
177+
if traitCollection.userInterfaceIdiom == .pad {
178+
viewContainer.widthAnchor
179+
.constraint(lessThanOrEqualToConstant: UIMetrics.preferredFormSheetContentSize.width)
180+
leadingConstraint.withPriority(.defaultHigh)
181+
trailingConstraint.withPriority(.defaultHigh)
182+
} else {
183+
leadingConstraint
184+
trailingConstraint
185+
}
182186
}
183187
}
184188

@@ -195,18 +199,18 @@ class AlertViewController: UIViewController {
195199
}
196200

197201
private func addHeader(_ title: String) {
198-
let header = UILabel()
199-
200-
header.text = title
201-
header.font = .preferredFont(forTextStyle: .largeTitle, weight: .bold)
202-
header.textColor = .white
203-
header.adjustsFontForContentSizeCategory = true
204-
header.textAlignment = .center
205-
header.numberOfLines = 0
206-
header.accessibilityIdentifier = .alertTitle
207-
208-
contentView.addArrangedSubview(header)
209-
contentView.setCustomSpacing(16, after: header)
202+
let label = UILabel()
203+
204+
label.text = title
205+
label.font = .preferredFont(forTextStyle: .largeTitle, weight: .bold)
206+
label.textColor = .white
207+
label.adjustsFontForContentSizeCategory = true
208+
label.textAlignment = .center
209+
label.numberOfLines = 0
210+
label.accessibilityIdentifier = .alertTitle
211+
212+
contentView.addArrangedSubview(label)
213+
contentView.setCustomSpacing(16, after: label)
210214
}
211215

212216
private func addTitle(_ title: String) {

0 commit comments

Comments
 (0)