Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rework the Restore Purchases button to decrease user confusion #6624

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -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>"; };
Expand Down Expand Up @@ -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>";
Expand Down Expand Up @@ -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 */,
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 .restorePurchasesInfo:
showRestorePurchasesInfo()
}
}

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 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)
}
}
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
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 = {
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 restorePurchasesInfo
}

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?(.restorePurchasesInfo)
}

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
Loading