Skip to content

Commit

Permalink
Rework the Restore Purchases button to decrease user confusion
Browse files Browse the repository at this point in the history
  • Loading branch information
Jon Petersson committed Aug 15, 2024
1 parent 92eab6f commit 751c334
Show file tree
Hide file tree
Showing 6 changed files with 173 additions and 60 deletions.
10 changes: 7 additions & 3 deletions ios/MullvadVPN.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -1846,6 +1847,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>"; };
Expand Down Expand Up @@ -2883,14 +2885,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>";
Expand Down Expand Up @@ -5640,6 +5643,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 */,
Expand Down
39 changes: 39 additions & 0 deletions ios/MullvadVPN/Coordinators/AccountCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ final class AccountCoordinator: Coordinator, Presentable, Presenting {
navigateToRedeemVoucher()
case .navigateToDeleteAccount:
navigateToDeleteAccount()
case .restorePurchaseInfo:
showRestorePurchaseInfo()
}
}

Expand Down Expand Up @@ -188,4 +190,41 @@ final class AccountCoordinator: Coordinator, Presentable, Presenting {
let presenter = AlertPresenter(context: self)
presenter.showAlert(presentation: presentation, animated: true)
}

private func showRestorePurchaseInfo() {
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)
}
}
41 changes: 10 additions & 31 deletions ios/MullvadVPN/View controllers/Account/AccountContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand All @@ -69,21 +52,19 @@ class AccountContentView: UIView {
}()

let accountDeviceRow: AccountDeviceRow = {
let view = AccountDeviceRow()
view.translatesAutoresizingMaskIntoConstraints = false
return view
return AccountDeviceRow()
}()

let accountTokenRowView: AccountNumberRow = {
let view = AccountNumberRow()
view.translatesAutoresizingMaskIntoConstraints = false
return view
return AccountNumberRow()
}()

let accountExpiryRowView: AccountExpiryRow = {
let view = AccountExpiryRow()
view.translatesAutoresizingMaskIntoConstraints = false
return view
return AccountExpiryRow()
}()

let restorePurchasesView: RestorePurchasesView = {
return RestorePurchasesView()
}()

lazy var contentStackView: UIStackView = {
Expand All @@ -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
}()

Expand All @@ -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
}()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ enum AccountViewControllerAction {
case logOut
case navigateToVoucher
case navigateToDeleteAccount
case restorePurchaseInfo
}

class AccountViewController: UIViewController {
Expand Down Expand Up @@ -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?(.restorePurchaseInfo)
}

interactor.didReceiveDeviceState = { [weak self] deviceState in
self?.updateView(from: deviceState)
}
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
82 changes: 82 additions & 0 deletions ios/MullvadVPN/View controllers/Account/RestorePurchasesView.swift
Original file line number Diff line number Diff line change
@@ -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?()
}
}
44 changes: 24 additions & 20 deletions ios/MullvadVPN/View controllers/Alert/AlertViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
}
}
}

Expand All @@ -195,18 +199,18 @@ class AlertViewController: UIViewController {
}

private func addHeader(_ title: String) {
let header = 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

contentView.addArrangedSubview(header)
contentView.setCustomSpacing(16, after: header)
let label = UILabel()

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(label)
contentView.setCustomSpacing(16, after: label)
}

private func addTitle(_ title: String) {
Expand Down

0 comments on commit 751c334

Please sign in to comment.