diff options
| author | Andrej Mihajlov <and@mullvad.net> | 2021-07-21 12:13:44 +0200 |
|---|---|---|
| committer | Andrej Mihajlov <and@mullvad.net> | 2021-07-21 12:13:44 +0200 |
| commit | e5e1cc386454e75c9c8c145f58f3bde8236d1fa0 (patch) | |
| tree | 42dfe1493ce9f2941e17eb698db574ba6e9c0243 | |
| parent | 57104ccc094845bb5b14b38adce829d3c818cf15 (diff) | |
| parent | 12c10b9a804b28da436429512210a81f4ce66049 (diff) | |
| download | mullvadvpn-e5e1cc386454e75c9c8c145f58f3bde8236d1fa0.tar.xz mullvadvpn-e5e1cc386454e75c9c8c145f58f3bde8236d1fa0.zip | |
Merge branch 'account-ax'
| -rw-r--r-- | ios/MullvadVPN.xcodeproj/project.pbxproj | 23 | ||||
| -rw-r--r-- | ios/MullvadVPN/AccountContentView.swift | 254 | ||||
| -rw-r--r-- | ios/MullvadVPN/AccountViewController.swift | 250 | ||||
| -rw-r--r-- | ios/MullvadVPN/AccountViewController.xib | 222 | ||||
| -rw-r--r-- | ios/MullvadVPN/InAppPurchaseButton.swift | 16 | ||||
| -rw-r--r-- | ios/MullvadVPN/en.lproj/Account.strings | 86 |
6 files changed, 545 insertions, 306 deletions
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index 8ff8894616..d475d0dc46 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -51,6 +51,7 @@ 581503A624D6F4AE00C9C50E /* Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 581503A524D6F4AE00C9C50E /* Logging.swift */; }; 581503A724D6F4AE00C9C50E /* Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 581503A524D6F4AE00C9C50E /* Logging.swift */; }; 581CBCEE229826FD00727D7F /* StaticTableViewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 581CBCED229826FD00727D7F /* StaticTableViewDataSource.swift */; }; + 581FC4FA2695ACE100AA97BA /* Account.strings in Resources */ = {isa = PBXBuildFile; fileRef = 581FC4F82695ACE100AA97BA /* Account.strings */; }; 58293FAE2510CA58005D0BB5 /* ProblemReportViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58293FAC2510CA58005D0BB5 /* ProblemReportViewController.swift */; }; 58293FB125124117005D0BB5 /* CustomTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58293FB025124117005D0BB5 /* CustomTextField.swift */; }; 58293FB3251241B4005D0BB5 /* CustomTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58293FB2251241B3005D0BB5 /* CustomTextView.swift */; }; @@ -155,6 +156,7 @@ 5896AE84246D5889005B36CB /* CustomDateComponentsFormatting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5896AE83246D5889005B36CB /* CustomDateComponentsFormatting.swift */; }; 5896AE86246D6AD8005B36CB /* CustomDateComponentsFormattingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5896AE85246D6AD8005B36CB /* CustomDateComponentsFormattingTests.swift */; }; 5896AE88246D7FAF005B36CB /* CustomDateComponentsFormatting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5896AE83246D5889005B36CB /* CustomDateComponentsFormatting.swift */; }; + 5896CEF226972DEB00B0FAE8 /* AccountContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5896CEF126972DEB00B0FAE8 /* AccountContentView.swift */; }; 589AB4F7227B64450039131E /* BasicTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 589AB4F6227B64450039131E /* BasicTableViewCell.swift */; }; 58A1AA8C23F5584C009F7EA6 /* ConnectionPanelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58A1AA8B23F5584B009F7EA6 /* ConnectionPanelView.swift */; }; 58A8BE81239FBE62006B74AC /* IPEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58561C98239A5D1500BD6B5E /* IPEndpoint.swift */; }; @@ -211,7 +213,6 @@ 58D0C79E23F1CEBA00FE9BA7 /* SnapshotHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58D0C79D23F1CEBA00FE9BA7 /* SnapshotHelper.swift */; }; 58D0C7A223F1CECF00FE9BA7 /* MullvadVPNScreenshots.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58D0C7A023F1CECF00FE9BA7 /* MullvadVPNScreenshots.swift */; }; 58DF28A52417CB4B00E836B0 /* AppStorePaymentManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58DF28A42417CB4B00E836B0 /* AppStorePaymentManager.swift */; }; - 58E5BC2624FEB6DB00A53A76 /* AccountViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 58E5BC2524FEB6DB00A53A76 /* AccountViewController.xib */; }; 58E6771F24ADFE7800AA26E7 /* SettingsNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E6771E24ADFE7800AA26E7 /* SettingsNavigationController.swift */; }; 58EF580B25D69D7A00AEBA94 /* ProblemReportSubmissionOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58EF580A25D69D7A00AEBA94 /* ProblemReportSubmissionOverlayView.swift */; }; 58EF581125D69DB400AEBA94 /* StatusImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58EF581025D69DB400AEBA94 /* StatusImageView.swift */; }; @@ -318,6 +319,7 @@ 581503A224D6F1EC00C9C50E /* ChainedError+Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ChainedError+Logger.swift"; sourceTree = "<group>"; }; 581503A524D6F4AE00C9C50E /* Logging.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logging.swift; sourceTree = "<group>"; }; 581CBCED229826FD00727D7F /* StaticTableViewDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StaticTableViewDataSource.swift; sourceTree = "<group>"; }; + 581FC4F92695ACE100AA97BA /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Account.strings; sourceTree = "<group>"; }; 58293FAC2510CA58005D0BB5 /* ProblemReportViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProblemReportViewController.swift; sourceTree = "<group>"; }; 58293FB025124117005D0BB5 /* CustomTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTextField.swift; sourceTree = "<group>"; }; 58293FB2251241B3005D0BB5 /* CustomTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTextView.swift; sourceTree = "<group>"; }; @@ -380,6 +382,7 @@ 5892A45D265FABFF00890742 /* EmptyTableViewHeaderFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyTableViewHeaderFooterView.swift; sourceTree = "<group>"; }; 5896AE83246D5889005B36CB /* CustomDateComponentsFormatting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomDateComponentsFormatting.swift; sourceTree = "<group>"; }; 5896AE85246D6AD8005B36CB /* CustomDateComponentsFormattingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomDateComponentsFormattingTests.swift; sourceTree = "<group>"; }; + 5896CEF126972DEB00B0FAE8 /* AccountContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountContentView.swift; sourceTree = "<group>"; }; 589AB4F6227B64450039131E /* BasicTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BasicTableViewCell.swift; sourceTree = "<group>"; }; 58A1AA8623F43901009F7EA6 /* Location.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Location.swift; sourceTree = "<group>"; }; 58A1AA8B23F5584B009F7EA6 /* ConnectionPanelView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConnectionPanelView.swift; sourceTree = "<group>"; }; @@ -427,7 +430,6 @@ 58D0C79F23F1CECF00FE9BA7 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; 58D0C7A023F1CECF00FE9BA7 /* MullvadVPNScreenshots.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MullvadVPNScreenshots.swift; sourceTree = "<group>"; }; 58DF28A42417CB4B00E836B0 /* AppStorePaymentManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStorePaymentManager.swift; sourceTree = "<group>"; }; - 58E5BC2524FEB6DB00A53A76 /* AccountViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AccountViewController.xib; sourceTree = "<group>"; }; 58E6771E24ADFE7800AA26E7 /* SettingsNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsNavigationController.swift; sourceTree = "<group>"; }; 58E973DD24850EB600096F90 /* AsyncOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncOperation.swift; sourceTree = "<group>"; }; 58ECD29123F178FD004298B6 /* Screenshots.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Screenshots.xcconfig; sourceTree = "<group>"; }; @@ -542,6 +544,7 @@ 582CFEE1269448160072883A /* Localizations */ = { isa = PBXGroup; children = ( + 581FC4F82695ACE100AA97BA /* Account.strings */, 582CFEE526945FC30072883A /* AppStoreSubscriptions.strings */, 58F559072697002100F630D0 /* ConnectionPanel.strings */, 587B7543266922BF00DEF7E9 /* Localizable.strings */, @@ -549,6 +552,7 @@ 58F61F4D2692F21C00DCFC2B /* WireguardKeys.strings */, ); name = Localizations; + path = MullvadVPN; sourceTree = "<group>"; }; 586ADD4323FC13AD00CE9E87 /* GeoJSON */ = { @@ -586,6 +590,7 @@ 58ECD29023F178FD004298B6 /* Configurations */, 0E15C74FDCF763609B367486 /* Frameworks */, 586ADD4323FC13AD00CE9E87 /* GeoJSON */, + 582CFEE1269448160072883A /* Localizations */, 58CE5E62224146200008646E /* MullvadVPN */, 58D0C79423F1CE7000FE9BA7 /* MullvadVPNScreenshots */, 58B0A2A1238EE67E00BC001D /* MullvadVPNTests */, @@ -609,12 +614,12 @@ isa = PBXGroup; children = ( 587AD7C92342283900E93A53 /* Account.swift */, + 5896CEF126972DEB00B0FAE8 /* AccountContentView.swift */, 582BB1B42295780F0055B6EF /* AccountExpiry.swift */, 58C3A4B122456F1A00340BDB /* AccountInputGroupView.swift */, 58CCA01D2242787B004F3011 /* AccountTextField.swift */, 582AE30F2440A6CA00E6733A /* AccountTokenInput.swift */, 58CCA01722426713004F3011 /* AccountViewController.swift */, - 58E5BC2524FEB6DB00A53A76 /* AccountViewController.xib */, 58B9EB122488ED2100095626 /* AlertPresenter.swift */, 5868585424054096000B8131 /* AppButton.swift */, 58CE5E63224146200008646E /* AppDelegate.swift */, @@ -660,7 +665,6 @@ 58FAEDFC24533A5500CB0F5B /* KeychainMatchLimit.swift */, 58FAEDFE24533A7000CB0F5B /* KeychainReturn.swift */, 58727282265D173C00F315B2 /* LaunchScreen.storyboard */, - 582CFEE1269448160072883A /* Localizations */, 58A1AA8623F43901009F7EA6 /* Location.swift */, 583DA21325FA4B5C00318683 /* LocationDataSource.swift */, 58BA692D23E99EFF009DC256 /* Locking.swift */, @@ -960,8 +964,8 @@ 584789B8264D4A2A000E45FB /* old_le_root_cert.cer in Resources */, 584789BE264D4A2A000E45FB /* new_le_root_cert.cer in Resources */, 58F61F4F2692F21C00DCFC2B /* WireguardKeys.strings in Resources */, - 58E5BC2624FEB6DB00A53A76 /* AccountViewController.xib in Resources */, 58F5590E2697002100F630D0 /* Main.strings in Resources */, + 581FC4FA2695ACE100AA97BA /* Account.strings in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1037,6 +1041,7 @@ 587B75412668FD7800DEF7E9 /* AccountExpiryNotificationProvider.swift in Sources */, 58BA692E23E99EFF009DC256 /* Locking.swift in Sources */, 580EE21E24B3237F00F9D8A1 /* OutputOperation.swift in Sources */, + 5896CEF226972DEB00B0FAE8 /* AccountContentView.swift in Sources */, 5840250122B1124600E4CFEC /* IPAddress+Codable.swift in Sources */, 5857F24724C882D700CF6F47 /* SelectLocationNavigationController.swift in Sources */, 580EE21224B322FC00F9D8A1 /* ResultOperation.swift in Sources */, @@ -1256,6 +1261,14 @@ /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ + 581FC4F82695ACE100AA97BA /* Account.strings */ = { + isa = PBXVariantGroup; + children = ( + 581FC4F92695ACE100AA97BA /* en */, + ); + name = Account.strings; + sourceTree = "<group>"; + }; 582CFEE526945FC30072883A /* AppStoreSubscriptions.strings */ = { isa = PBXVariantGroup; children = ( diff --git a/ios/MullvadVPN/AccountContentView.swift b/ios/MullvadVPN/AccountContentView.swift new file mode 100644 index 0000000000..127ee39b3c --- /dev/null +++ b/ios/MullvadVPN/AccountContentView.swift @@ -0,0 +1,254 @@ +// +// AccountContentView.swift +// MullvadVPN +// +// Created by pronebird on 08/07/2021. +// Copyright © 2021 Mullvad VPN AB. All rights reserved. +// + +import UIKit + +class AccountContentView: UIView { + + let purchaseButton: InAppPurchaseButton = { + let button = InAppPurchaseButton() + button.translatesAutoresizingMaskIntoConstraints = false + return button + }() + + let restorePurchasesButton: AppButton = { + let button = AppButton(style: .default) + button.translatesAutoresizingMaskIntoConstraints = false + button.setTitle(NSLocalizedString( + "RESTORE_PURCHASES_BUTTON_TITLE", + tableName: "Account", + value: "Restore purchases", + comment: "" + ), for: .normal) + return button + }() + + let logoutButton: AppButton = { + let button = AppButton(style: .danger) + button.translatesAutoresizingMaskIntoConstraints = false + button.setTitle(NSLocalizedString( + "LOGOUT_BUTTON_TITLE", + tableName: "Account", + value: "Log out", + comment: "" + ), for: .normal) + return button + }() + + let accountTokenRowView: AccountTokenRow = { + let view = AccountTokenRow() + view.translatesAutoresizingMaskIntoConstraints = false + return view + }() + + let accountExpiryRowView: AccountExpiryRow = { + let view = AccountExpiryRow() + view.translatesAutoresizingMaskIntoConstraints = false + return view + }() + + lazy var contentStackView: UIStackView = { + let stackView = UIStackView(arrangedSubviews: [accountTokenRowView, accountExpiryRowView]) + stackView.translatesAutoresizingMaskIntoConstraints = false + stackView.axis = .vertical + stackView.spacing = UIMetrics.sectionSpacing + return stackView + }() + + lazy var buttonStackView: UIStackView = { + let stackView = UIStackView(arrangedSubviews: [purchaseButton, restorePurchasesButton, logoutButton]) + stackView.translatesAutoresizingMaskIntoConstraints = false + stackView.axis = .vertical + stackView.spacing = UIMetrics.interButtonSpacing + stackView.setCustomSpacing(UIMetrics.interButtonSpacing, after: restorePurchasesButton) + return stackView + }() + + override init(frame: CGRect) { + super.init(frame: frame) + + layoutMargins = UIMetrics.contentLayoutMargins + + addSubview(contentStackView) + addSubview(buttonStackView) + + NSLayoutConstraint.activate([ + contentStackView.topAnchor.constraint(equalTo: layoutMarginsGuide.topAnchor), + contentStackView.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor), + contentStackView.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor), + + buttonStackView.topAnchor.constraint(greaterThanOrEqualTo: contentStackView.bottomAnchor, constant: UIMetrics.sectionSpacing), + buttonStackView.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor), + buttonStackView.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor), + buttonStackView.bottomAnchor.constraint(equalTo: layoutMarginsGuide.bottomAnchor) + ]) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +class AccountTokenRow: UIView { + + var value: String? { + didSet { + valueButton.setTitle(value, for: .normal) + accessibilityValue = value + } + } + var actionHandler: (() -> Void)? + + private let textLabel: UILabel = { + let textLabel = UILabel() + textLabel.translatesAutoresizingMaskIntoConstraints = false + textLabel.text = NSLocalizedString("ACCOUNT_TOKEN_LABEL", tableName: "Account", comment: "") + textLabel.font = UIFont.systemFont(ofSize: 14) + textLabel.textColor = UIColor(white: 1.0, alpha: 0.6) + return textLabel + }() + + private let valueButton: UIButton = { + let button = UIButton(type: .system) + button.translatesAutoresizingMaskIntoConstraints = false + button.titleLabel?.font = UIFont.systemFont(ofSize: 17) + button.setTitleColor(.white, for: .normal) + button.contentHorizontalAlignment = .leading + button.contentEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 1) + button.accessibilityHint = NSLocalizedString("ACCOUNT_TOKEN_ACCESSIBILITY_HINT", tableName: "Account", comment: "") + return button + }() + + override init(frame: CGRect) { + super.init(frame: frame) + + addSubview(textLabel) + addSubview(valueButton) + + NSLayoutConstraint.activate([ + textLabel.topAnchor.constraint(equalTo: topAnchor), + textLabel.leadingAnchor.constraint(equalTo: leadingAnchor), + textLabel.trailingAnchor.constraint(greaterThanOrEqualTo: trailingAnchor), + + valueButton.topAnchor.constraint(equalTo: textLabel.bottomAnchor, constant: 8), + valueButton.leadingAnchor.constraint(equalTo: leadingAnchor), + valueButton.trailingAnchor.constraint(equalTo: trailingAnchor), + valueButton.bottomAnchor.constraint(equalTo: bottomAnchor) + ]) + + isAccessibilityElement = true + accessibilityLabel = textLabel.text + + let actionName = NSLocalizedString( + "ACCOUNT_TOKEN_ACCESSIBILITY_ACTION_TITLE", + tableName: "Account", + comment: "" + ) + accessibilityCustomActions = [UIAccessibilityCustomAction(name: actionName, target: self, selector: #selector(performAccessibilityAction))] + + valueButton.addTarget(self, action: #selector(handleTap), for: .touchUpInside) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func handleTap() { + self.actionHandler?() + } + + @objc private func performAccessibilityAction() { + self.actionHandler?() + } +} + +class AccountExpiryRow: UIView { + + var value: Date? { + didSet { + let expiry = value.flatMap { AccountExpiry(date: $0) } + + if let expiry = expiry, expiry.isExpired { + let localizedString = NSLocalizedString( + "ACCOUNT_OUT_OF_TIME_LABEL", + tableName: "Account", + value: "OUT OF TIME", + comment: "Label displayed in place of account expiration when account is out of time." + ) + + valueLabel.text = localizedString + accessibilityValue = localizedString + + valueLabel.textColor = .dangerColor + } else { + let formattedDate = expiry?.formattedDate + + valueLabel.text = formattedDate + accessibilityValue = formattedDate + + valueLabel.textColor = .white + } + } + } + + private let textLabel: UILabel = { + let textLabel = UILabel() + textLabel.translatesAutoresizingMaskIntoConstraints = false + textLabel.text = NSLocalizedString("ACCOUNT_EXPIRY_LABEL", tableName: "Account", comment: "") + textLabel.font = UIFont.systemFont(ofSize: 14) + textLabel.textColor = UIColor(white: 1.0, alpha: 0.6) + return textLabel + }() + + private let valueLabel: UILabel = { + let valueLabel = UILabel() + valueLabel.translatesAutoresizingMaskIntoConstraints = false + valueLabel.font = UIFont.systemFont(ofSize: 17) + valueLabel.textColor = .white + return valueLabel + }() + + let activityIndicator: SpinnerActivityIndicatorView = { + let activityIndicator = SpinnerActivityIndicatorView(style: .small) + activityIndicator.translatesAutoresizingMaskIntoConstraints = false + activityIndicator.tintColor = .white + activityIndicator.setContentHuggingPriority(.defaultHigh, for: .horizontal) + activityIndicator.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) + return activityIndicator + }() + + override init(frame: CGRect) { + super.init(frame: frame) + + addSubview(textLabel) + addSubview(activityIndicator) + addSubview(valueLabel) + + NSLayoutConstraint.activate([ + textLabel.topAnchor.constraint(equalTo: topAnchor), + textLabel.leadingAnchor.constraint(equalTo: leadingAnchor), + textLabel.trailingAnchor.constraint(greaterThanOrEqualTo: activityIndicator.leadingAnchor, constant: -8), + + activityIndicator.topAnchor.constraint(equalTo: textLabel.topAnchor), + activityIndicator.bottomAnchor.constraint(equalTo: textLabel.bottomAnchor), + activityIndicator.trailingAnchor.constraint(equalTo: trailingAnchor), + + valueLabel.topAnchor.constraint(equalTo: textLabel.bottomAnchor, constant: 8), + valueLabel.leadingAnchor.constraint(equalTo: leadingAnchor), + valueLabel.trailingAnchor.constraint(equalTo: trailingAnchor), + valueLabel.bottomAnchor.constraint(equalTo: bottomAnchor) + ]) + + isAccessibilityElement = true + accessibilityLabel = textLabel.text + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/ios/MullvadVPN/AccountViewController.swift b/ios/MullvadVPN/AccountViewController.swift index 398d304f0a..976decf21a 100644 --- a/ios/MullvadVPN/AccountViewController.swift +++ b/ios/MullvadVPN/AccountViewController.swift @@ -16,12 +16,11 @@ protocol AccountViewControllerDelegate: AnyObject { class AccountViewController: UIViewController, AppStorePaymentObserver, AccountObserver { - @IBOutlet var accountTokenButton: UIButton! - @IBOutlet var purchaseButton: InAppPurchaseButton! - @IBOutlet var restoreButton: AppButton! - @IBOutlet var logoutButton: AppButton! - @IBOutlet var expiryLabel: UILabel! - @IBOutlet var activityIndicator: SpinnerActivityIndicatorView! + private let contentView: AccountContentView = { + let contentView = AccountContentView() + contentView.translatesAutoresizingMaskIntoConstraints = false + return contentView + }() private var copyToPasteboardWork: DispatchWorkItem? @@ -34,7 +33,7 @@ class AccountViewController: UIViewController, AppStorePaymentObserver, AccountO private lazy var purchaseButtonInteractionRestriction = UserInterfaceInteractionRestriction { [weak self] (enableUserInteraction, _) in // Make sure to disable the button if the product is not loaded - self?.purchaseButton.isEnabled = enableUserInteraction && + self?.contentView.purchaseButton.isEnabled = enableUserInteraction && self?.product != nil && AppStorePaymentManager.canMakePayments } @@ -55,17 +54,46 @@ class AccountViewController: UIViewController, AppStorePaymentObserver, AccountO override func viewDidLoad() { super.viewDidLoad() - navigationItem.title = NSLocalizedString("Account", comment: "Navigation title") + view.backgroundColor = .secondaryColor - AppStorePaymentManager.shared.addPaymentObserver(self) - Account.shared.addObserver(self) + let scrollView = UIScrollView() + scrollView.translatesAutoresizingMaskIntoConstraints = false + scrollView.addSubview(contentView) + view.addSubview(scrollView) - accountTokenButton.setTitle(Account.shared.formattedToken, for: .normal) + NSLayoutConstraint.activate([ + scrollView.topAnchor.constraint(equalTo: view.topAnchor), + scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor), - if let expiryDate = Account.shared.expiry { - updateAccountExpiry(expiryDate: expiryDate) + contentView.topAnchor.constraint(equalTo: scrollView.topAnchor), + contentView.bottomAnchor.constraint(greaterThanOrEqualTo: scrollView.safeAreaLayoutGuide.bottomAnchor), + contentView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor), + contentView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor), + contentView.widthAnchor.constraint(equalTo: scrollView.widthAnchor), + ]) + + navigationItem.title = NSLocalizedString( + "NAVIGATION_TITLE", + tableName: "Account", + comment: "Navigation title" + ) + + contentView.accountTokenRowView.value = Account.shared.formattedToken + contentView.accountTokenRowView.actionHandler = { [weak self] in + self?.copyAccountToken() } + contentView.restorePurchasesButton.addTarget(self, action: #selector(restorePurchases), for: .touchUpInside) + contentView.purchaseButton.addTarget(self, action: #selector(doPurchase), for: .touchUpInside) + contentView.logoutButton.addTarget(self, action: #selector(doLogout), for: .touchUpInside) + + AppStorePaymentManager.shared.addPaymentObserver(self) + Account.shared.addObserver(self) + + updateAccountExpiry(expiryDate: Account.shared.expiry) + // Make sure to disable IAPs when payments are restricted if AppStorePaymentManager.canMakePayments { requestStoreProducts() @@ -76,23 +104,15 @@ class AccountViewController: UIViewController, AppStorePaymentObserver, AccountO // MARK: - Private methods - private func updateAccountExpiry(expiryDate: Date) { - let accountExpiry = AccountExpiry(date: expiryDate) - - if accountExpiry.isExpired { - expiryLabel.text = NSLocalizedString("OUT OF TIME", comment: "") - expiryLabel.textColor = .dangerColor - } else { - expiryLabel.text = accountExpiry.formattedDate - expiryLabel.textColor = .white - } + private func updateAccountExpiry(expiryDate: Date?) { + contentView.accountExpiryRowView.value = expiryDate } private func requestStoreProducts() { let inAppPurchase = AppStoreSubscription.thirtyDays - purchaseButton.setTitle(inAppPurchase.localizedTitle, for: .normal) - purchaseButton.isLoading = true + contentView.purchaseButton.setTitle(inAppPurchase.localizedTitle, for: .normal) + contentView.purchaseButton.isLoading = true purchaseButtonInteractionRestriction.increase(animated: true) @@ -110,7 +130,7 @@ class AccountViewController: UIViewController, AppStorePaymentObserver, AccountO self.didFailLoadingProducts(with: error) } - self.purchaseButton.isLoading = false + self.contentView.purchaseButton.isLoading = false self.purchaseButtonInteractionRestriction.decrease(animated: true) } } @@ -123,33 +143,42 @@ class AccountViewController: UIViewController, AppStorePaymentObserver, AccountO let localizedPrice = product.localizedPrice ?? "" let format = NSLocalizedString( - "%1$@ (%2$@)", - comment: "The buy button title: <TITLE> (<PRICE>). The order can be changed by swapping %1 and %2." + "PURCHASE_BUTTON_TITLE_FORMAT", + tableName: "Account", + value: "%1$@ (%2$@)", + comment: "Purchase button title: <TITLE> (<PRICE>). The order can be changed by swapping %1 and %2." ) let title = String(format: format, localizedTitle, localizedPrice) - purchaseButton.setTitle(title, for: .normal) + contentView.purchaseButton.setTitle(title, for: .normal) } private func didFailLoadingProducts(with error: Error) { let title = NSLocalizedString( - "Cannot connect to AppStore", - comment: "The buy button title displayed when unable to load the price of subscription" + "PURCHASE_BUTTON_CANNOT_CONNECT_TO_APPSTORE_LABEL", + tableName: "Account", + value: "Cannot connect to AppStore", + comment: "Purchase button title displayed when unable to load the price of in-app purchase." ) - purchaseButton.setTitle(title, for: .normal) + contentView.purchaseButton.setTitle(title, for: .normal) } private func setPaymentsRestricted() { - let title = NSLocalizedString("Payments restricted", comment: "") + let title = NSLocalizedString( + "PURCHASE_BUTTON_PAYMENTS_RESTRICTED_LABEL", + tableName: "Account", + value: "Payments restricted", + comment: "Purchase button title displayed when payments are restriced on device." + ) - purchaseButton.setTitle(title, for: .normal) - purchaseButton.isEnabled = false + contentView.purchaseButton.setTitle(title, for: .normal) + contentView.purchaseButton.isEnabled = false } private func setEnableUserInteraction(_ enableUserInteraction: Bool, animated: Bool) { // Disable all buttons - [restoreButton, logoutButton].forEach { (button) in + [contentView.restorePurchasesButton, contentView.logoutButton].forEach { (button) in button?.isEnabled = enableUserInteraction } @@ -168,9 +197,9 @@ class AccountViewController: UIViewController, AppStorePaymentObserver, AccountO // Show/hide the spinner next to "Paid until" if enableUserInteraction { - activityIndicator.stopAnimating() + contentView.accountExpiryRowView.activityIndicator.stopAnimating() } else { - activityIndicator.startAnimating() + contentView.accountExpiryRowView.activityIndicator.startAnimating() } } @@ -183,25 +212,44 @@ class AccountViewController: UIViewController, AppStorePaymentObserver, AccountO message: response.alertMessage(context: context), preferredStyle: .alert ) - alertController.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .cancel)) + alertController.addAction( + UIAlertAction( + title: NSLocalizedString( + "TIME_ADDED_ALERT_OK_ACTION", + tableName: "Account", + value: "OK", + comment: "" + ), + style: .cancel + ) + ) alertPresenter.enqueue(alertController, presentingController: self) } private func showLogoutConfirmation(completion: @escaping (Bool) -> Void, animated: Bool) { - let message = NSLocalizedString( - "Are you sure you want to log out?\n\nThis will erase the account number from this device. It is not possible for us to recover it for you. Make sure you have your account number saved somewhere, to be able to log back in.", - comment: "Alert message in log out confirmation") - let alertController = UIAlertController( - title: NSLocalizedString("Log out", comment: "Alert title in log out confirmation"), - message: message, + title: NSLocalizedString( + "LOGOUT_CONFIRMATION_ALERT_TITLE", + tableName: "Account", + comment: "Title for logout dialog" + ), + message: NSLocalizedString( + "LOGOUT_CONFIRMATION_ALERT_MESSAGE", + tableName: "Account", + comment: "Message for logout dialog" + ), preferredStyle: .alert ) alertController.addAction( UIAlertAction( - title: NSLocalizedString("Cancel", comment: "Log out confirmation action"), + title: NSLocalizedString( + "LOGOUT_CONFIRMATION_ALERT_CANCEL_ACTION", + tableName: "Account", + value: "Cancel", + comment: "Title for cancel button in logout dialog" + ), style: .cancel, handler: { (alertAction) in completion(false) @@ -210,7 +258,12 @@ class AccountViewController: UIViewController, AppStorePaymentObserver, AccountO alertController.addAction( UIAlertAction( - title: NSLocalizedString("Log out", comment: "Log out confirmation action"), + title: NSLocalizedString( + "LOGOUT_CONFIRMATION_ALERT_YES_ACTION", + tableName: "Account", + value: "Log out", + comment: "Title for confirmation button in logout dialog" + ), style: .destructive, handler: { (alertAction) in completion(true) @@ -221,8 +274,12 @@ class AccountViewController: UIViewController, AppStorePaymentObserver, AccountO } private func confirmLogout() { - let message = NSLocalizedString("Logging out. Please wait...", - comment: "A modal message displayed during log out") + let message = NSLocalizedString( + "LOGGING_OUT_ALERT_TITLE", + tableName: "Account", + value: "Logging out. Please wait...", + comment: "Modal message displayed during logout" + ) let alertController = UIAlertController( title: nil, @@ -238,12 +295,22 @@ class AccountViewController: UIViewController, AppStorePaymentObserver, AccountO self.logger.error(chainedError: error, message: "Failed to log out") let errorAlertController = UIAlertController( - title: NSLocalizedString("Failed to log out", comment: ""), + title: NSLocalizedString( + "LOGOUT_FAILURE_ALERT_TITLE", + tableName: "Account", + value: "Failed to log out", + comment: "Title for logout failure alert" + ), message: error.errorChainDescription, preferredStyle: .alert ) errorAlertController.addAction( - UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .cancel) + UIAlertAction(title: NSLocalizedString( + "LOGOUT_FAILURE_ALERT_OK_ACTION", + tableName: "Account", + value: "OK", + comment: "Message for logout failure alert" + ), style: .cancel) ) self.alertPresenter.enqueue(errorAlertController, presentingController: self) @@ -275,13 +342,24 @@ class AccountViewController: UIViewController, AppStorePaymentObserver, AccountO func appStorePaymentManager(_ manager: AppStorePaymentManager, transaction: SKPaymentTransaction, accountToken: String?, didFailWithError error: AppStorePaymentManager.Error) { DispatchQueue.main.async { let alertController = UIAlertController( - title: NSLocalizedString("Cannot complete the purchase", comment: ""), + title: NSLocalizedString( + "CANNOT_COMPLETE_PURCHASE_ALERT_TITLE", + tableName: "Account", + value: "Cannot complete the purchase", + comment: "Title for purchase failure dialog" + ), message: error.errorChainDescription, preferredStyle: .alert ) alertController.addAction( - UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .cancel) + UIAlertAction( + title: NSLocalizedString( + "CANNOT_COMPLETE_PURCHASE_ALERT_OK_ACTION", + tableName: "Account", + value: "OK", + comment: "Title for OK button in purchase failure dialog" + ), style: .cancel) ) self.alertPresenter.enqueue(alertController, presentingController: self) @@ -305,7 +383,7 @@ class AccountViewController: UIViewController, AppStorePaymentObserver, AccountO // MARK: - Actions - @IBAction func doLogout() { + @objc private func doLogout() { showLogoutConfirmation(completion: { (confirmed) in if confirmed { self.confirmLogout() @@ -313,15 +391,17 @@ class AccountViewController: UIViewController, AppStorePaymentObserver, AccountO }, animated: true) } - @IBAction func copyAccountToken() { + private func copyAccountToken() { UIPasteboard.general.string = Account.shared.token - accountTokenButton.setTitle( - NSLocalizedString("COPIED TO PASTEBOARD!", comment: ""), - for: .normal) + contentView.accountTokenRowView.value = NSLocalizedString( + "COPIED_TO_PASTEBOARD_LABEL", + tableName: "Account", + comment: "Message, temporarily displayed in place account token, after copying the account token to pasteboard on tap." + ) let dispatchWork = DispatchWorkItem { [weak self] in - self?.accountTokenButton.setTitle(Account.shared.formattedToken, for: .normal) + self?.contentView.accountTokenRowView.value = Account.shared.formattedToken } DispatchQueue.main.asyncAfter(wallDeadline: .now() + .seconds(3), execute: dispatchWork) @@ -330,7 +410,7 @@ class AccountViewController: UIViewController, AppStorePaymentObserver, AccountO self.copyToPasteboardWork = dispatchWork } - @IBAction func doPurchase() { + @objc private func doPurchase() { guard let product = product, let accountToken = Account.shared.token else { return } let payment = SKPayment(product: product) @@ -341,7 +421,7 @@ class AccountViewController: UIViewController, AppStorePaymentObserver, AccountO AppStorePaymentManager.shared.addPayment(payment, for: accountToken) } - @IBAction func restorePurchases() { + @objc private func restorePurchases() { guard let accountToken = Account.shared.token else { return } compoundInteractionRestriction.increase(animated: true) @@ -354,12 +434,22 @@ class AccountViewController: UIViewController, AppStorePaymentObserver, AccountO case .failure(let error): let alertController = UIAlertController( - title: NSLocalizedString("Cannot restore purchases", comment: ""), + title: NSLocalizedString( + "RESTORE_PURCHASES_FAILURE_ALERT_TITLE", + tableName: "Account", + value: "Cannot restore purchases", + comment: "Title for failure dialog when restoring purchases" + ), message: error.errorChainDescription, preferredStyle: .alert ) alertController.addAction( - UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .cancel) + UIAlertAction(title: NSLocalizedString( + "RESTORE_PURCHASES_FAILURE_ALERT_OK_ACTION", + tableName: "Account", + value: "OK", + comment: "Title for 'OK' button in failure dialog when restoring purchases" + ), style: .cancel) ) self.alertPresenter.enqueue(alertController, presentingController: self) } @@ -381,9 +471,18 @@ private extension CreateApplePaymentResponse { func alertTitle(context: Context) -> String { switch context { case .purchase: - return NSLocalizedString("Thanks for your purchase", comment: "") + return NSLocalizedString( + "TIME_ADDED_ALERT_SUCCESS_TITLE", + tableName: "Account", + value: "Thanks for your purchase", + comment: "Title for purchase completion dialog" + ) case .restoration: - return NSLocalizedString("Restore purchases", comment: "") + return NSLocalizedString( + "RESTORE_PURCHASES_ALERT_TITLE", + tableName: "Account", value: "Restore purchases", + comment: "Title for purchase restoration dialog" + ) } } @@ -391,18 +490,31 @@ private extension CreateApplePaymentResponse { switch context { case .purchase: return String( - format: NSLocalizedString("%@ have been added to your account", comment: ""), + format: NSLocalizedString( + "TIME_ADDED_ALERT_SUCCESS_MESSAGE", + tableName: "Account", + value: "%@ have been added to your account", + comment: "Message displayed upon successful purchase and containing the time duration credited to user account. Use %@ placeholder to position the localized text with duration added (i.e '30 days')" + ), formattedTimeAdded ?? "" ) case .restoration: switch self { case .noTimeAdded: return NSLocalizedString( - "Your previous purchases have already been added to this account.", - comment: "") + "RESTORE_PURCHASES_ALERT_NO_TIME_ADDED_MESSAGE", + tableName: "Account", + value: "Your previous purchases have already been added to this account.", + comment: "Message displayed when no time credited to user account during purchase restoration; communicates that user account has already been credited with all outstanding purchased time duration." + ) case .timeAdded: return String( - format: NSLocalizedString("%@ have been added to your account", comment: ""), + format: NSLocalizedString( + "RESTORE_PURCHASES_ALERT_TIME_ADDED_MESSAGE", + tableName: "Account", + value: "%@ have been added to your account", + comment: "Message displayed upon successful restoration of existing purchases, containing the time duration credited to user account. Use %@ placeholder to position the localized text with duration added (i.e '30 days')" + ), self.formattedTimeAdded ?? "" ) } diff --git a/ios/MullvadVPN/AccountViewController.xib b/ios/MullvadVPN/AccountViewController.xib deleted file mode 100644 index 42014035e3..0000000000 --- a/ios/MullvadVPN/AccountViewController.xib +++ /dev/null @@ -1,222 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="16097.3" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES"> - <device id="retina6_1" orientation="portrait" appearance="light"/> - <dependencies> - <deployment identifier="iOS"/> - <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="16087"/> - <capability name="Named colors" minToolsVersion="9.0"/> - <capability name="Safe area layout guides" minToolsVersion="9.0"/> - <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> - </dependencies> - <objects> - <placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="AccountViewController" customModule="MullvadVPN" customModuleProvider="target"> - <connections> - <outlet property="accountTokenButton" destination="lCa-aa-Pm3" id="R2W-4z-06o"/> - <outlet property="activityIndicator" destination="eGi-ok-x76" id="Lq5-Ry-ec8"/> - <outlet property="expiryLabel" destination="2i5-GK-hJb" id="0yg-To-nL7"/> - <outlet property="logoutButton" destination="hLF-CV-4mn" id="Tae-qy-70n"/> - <outlet property="purchaseButton" destination="Jll-2f-Pkg" id="Qbx-89-bCu"/> - <outlet property="restoreButton" destination="Of2-bz-zp8" id="P8L-j9-7m7"/> - <outlet property="view" destination="N94-2G-eN0" id="dAC-wQ-aYn"/> - </connections> - </placeholder> - <placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/> - <view contentMode="scaleToFill" id="N94-2G-eN0"> - <rect key="frame" x="0.0" y="0.0" width="414" height="896"/> - <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> - <subviews> - <scrollView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" translatesAutoresizingMaskIntoConstraints="NO" id="0Lz-bX-FzY"> - <rect key="frame" x="0.0" y="0.0" width="414" height="896"/> - <subviews> - <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="A7Y-7l-t1J" userLabel="Container"> - <rect key="frame" x="0.0" y="0.0" width="414" height="349.5"/> - <subviews> - <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="cUt-HL-Is8" userLabel="Content"> - <rect key="frame" x="24" y="24" width="366" height="301.5"/> - <subviews> - <view contentMode="scaleToFill" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="NgY-qI-yOq" userLabel="Account number"> - <rect key="frame" x="0.0" y="0.0" width="366" height="46"/> - <subviews> - <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="vAg-UO-s90"> - <rect key="frame" x="0.0" y="0.0" width="366" height="46"/> - <subviews> - <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Account number" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="wYg-Sx-sht"> - <rect key="frame" x="0.0" y="0.0" width="366" height="17"/> - <fontDescription key="fontDescription" type="system" pointSize="14"/> - <color key="textColor" white="1" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> - <nil key="highlightedColor"/> - </label> - <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="leading" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="lCa-aa-Pm3"> - <rect key="frame" x="0.0" y="25" width="366" height="21"/> - <fontDescription key="fontDescription" type="system" weight="medium" pointSize="17"/> - <inset key="contentEdgeInsets" minX="0.01" minY="0.0" maxX="1" maxY="0.0"/> - <state key="normal" title="123456789"> - <color key="titleColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> - </state> - <connections> - <action selector="copyAccountToken" destination="-1" eventType="touchUpInside" id="hU3-zX-gvR"/> - </connections> - </button> - </subviews> - </stackView> - </subviews> - <constraints> - <constraint firstItem="vAg-UO-s90" firstAttribute="leading" secondItem="NgY-qI-yOq" secondAttribute="leading" id="03w-uO-dTN"/> - <constraint firstAttribute="trailing" secondItem="vAg-UO-s90" secondAttribute="trailing" id="Lfo-go-G45"/> - <constraint firstItem="vAg-UO-s90" firstAttribute="top" secondItem="NgY-qI-yOq" secondAttribute="top" id="VxJ-9R-Z8g"/> - <constraint firstAttribute="bottom" secondItem="vAg-UO-s90" secondAttribute="bottom" id="b7b-Im-8Ei"/> - </constraints> - </view> - <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="C4H-CM-EXc" userLabel="Expiry"> - <rect key="frame" x="0.0" y="70" width="366" height="45.5"/> - <subviews> - <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="t98-46-zC2"> - <rect key="frame" x="0.0" y="0.0" width="366" height="45.5"/> - <subviews> - <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="oeO-Xm-rOB"> - <rect key="frame" x="0.0" y="0.0" width="366" height="17"/> - <subviews> - <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="751" text="Paid until" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="k3W-5i-Dbf"> - <rect key="frame" x="0.0" y="0.0" width="59.5" height="17"/> - <fontDescription key="fontDescription" type="system" pointSize="14"/> - <color key="textColor" white="1" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> - <nil key="highlightedColor"/> - </label> - <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="eGi-ok-x76" customClass="SpinnerActivityIndicatorView" customModule="MullvadVPN" customModuleProvider="target"> - <rect key="frame" x="350" y="0.5" width="16" height="16"/> - <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> - <color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> - <constraints> - <constraint firstAttribute="width" constant="16" id="W8Y-gT-Qgm"/> - <constraint firstAttribute="height" constant="16" id="atJ-sI-Bp0"/> - </constraints> - <userDefinedRuntimeAttributes> - <userDefinedRuntimeAttribute type="number" keyPath="thickness"> - <real key="value" value="2"/> - </userDefinedRuntimeAttribute> - </userDefinedRuntimeAttributes> - </view> - </subviews> - <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> - <constraints> - <constraint firstAttribute="bottom" secondItem="k3W-5i-Dbf" secondAttribute="bottom" id="4J3-AE-5Hy"/> - <constraint firstAttribute="trailing" secondItem="eGi-ok-x76" secondAttribute="trailing" id="5gP-Vc-aP2"/> - <constraint firstItem="k3W-5i-Dbf" firstAttribute="top" secondItem="oeO-Xm-rOB" secondAttribute="top" id="VZh-En-ucb"/> - <constraint firstItem="eGi-ok-x76" firstAttribute="centerY" secondItem="oeO-Xm-rOB" secondAttribute="centerY" id="Vlb-z0-sSB"/> - <constraint firstItem="eGi-ok-x76" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="k3W-5i-Dbf" secondAttribute="trailing" constant="8" symbolic="YES" id="h3f-p3-TI0"/> - <constraint firstItem="k3W-5i-Dbf" firstAttribute="leading" secondItem="oeO-Xm-rOB" secondAttribute="leading" id="z5j-jP-WPE"/> - </constraints> - </view> - <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" text="May 16, 2019" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsLetterSpacingToFitWidth="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="2i5-GK-hJb"> - <rect key="frame" x="0.0" y="25" width="366" height="20.5"/> - <fontDescription key="fontDescription" type="system" weight="medium" pointSize="17"/> - <color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> - <nil key="highlightedColor"/> - </label> - </subviews> - </stackView> - </subviews> - <constraints> - <constraint firstItem="t98-46-zC2" firstAttribute="leading" secondItem="C4H-CM-EXc" secondAttribute="leading" id="TZN-lX-Cfd"/> - <constraint firstAttribute="bottom" secondItem="t98-46-zC2" secondAttribute="bottom" id="dzU-41-4Ce"/> - <constraint firstAttribute="trailing" secondItem="t98-46-zC2" secondAttribute="trailing" id="gUN-YB-Dub"/> - <constraint firstItem="t98-46-zC2" firstAttribute="top" secondItem="C4H-CM-EXc" secondAttribute="top" id="ycA-QZ-paj"/> - </constraints> - </view> - <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="12" translatesAutoresizingMaskIntoConstraints="NO" id="9TF-RQ-EIQ"> - <rect key="frame" x="0.0" y="139.5" width="366" height="96"/> - <subviews> - <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Jll-2f-Pkg" customClass="InAppPurchaseButton" customModule="MullvadVPN" customModuleProvider="target"> - <rect key="frame" x="0.0" y="0.0" width="366" height="42"/> - <constraints> - <constraint firstAttribute="height" constant="42" placeholder="YES" id="T0e-dF-aO3"/> - </constraints> - <state key="normal" title="Display name for in-app purchase" backgroundImage="SuccessButton"> - <color key="titleColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> - </state> - <connections> - <action selector="doPurchase" destination="-1" eventType="touchUpInside" id="cOe-fB-cnj"/> - </connections> - </button> - <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Of2-bz-zp8" customClass="AppButton" customModule="MullvadVPN" customModuleProvider="target"> - <rect key="frame" x="0.0" y="54" width="366" height="42"/> - <constraints> - <constraint firstAttribute="height" constant="42" placeholder="YES" id="akv-uD-R7b"/> - </constraints> - <state key="normal" title="Restore purchases" backgroundImage="DefaultButton"> - <color key="titleColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> - </state> - <connections> - <action selector="restorePurchases" destination="-1" eventType="touchUpInside" id="G4r-zv-oE7"/> - </connections> - </button> - </subviews> - </stackView> - <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="hLF-CV-4mn" customClass="AppButton" customModule="MullvadVPN" customModuleProvider="target"> - <rect key="frame" x="0.0" y="259.5" width="366" height="42"/> - <accessibility key="accessibilityConfiguration" identifier="LogoutButton"/> - <constraints> - <constraint firstAttribute="height" constant="42" placeholder="YES" id="96p-fe-pCW"/> - </constraints> - <state key="normal" title="Log out" backgroundImage="DangerButton"> - <color key="titleColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> - </state> - <connections> - <action selector="doLogout" destination="-1" eventType="touchUpInside" id="cQP-IQ-YXH"/> - </connections> - </button> - </subviews> - <constraints> - <constraint firstAttribute="bottom" secondItem="hLF-CV-4mn" secondAttribute="bottom" id="8Q9-HL-ots"/> - <constraint firstItem="C4H-CM-EXc" firstAttribute="top" secondItem="NgY-qI-yOq" secondAttribute="bottom" constant="24" id="BXu-aw-iGR"/> - <constraint firstItem="9TF-RQ-EIQ" firstAttribute="top" secondItem="C4H-CM-EXc" secondAttribute="bottom" constant="24" id="DAE-90-Mp8"/> - <constraint firstAttribute="trailing" secondItem="hLF-CV-4mn" secondAttribute="trailing" id="Dsn-hU-tIY"/> - <constraint firstAttribute="trailing" secondItem="NgY-qI-yOq" secondAttribute="trailing" id="HqF-A7-TVI"/> - <constraint firstAttribute="trailing" secondItem="9TF-RQ-EIQ" secondAttribute="trailing" id="Ige-HU-iDn"/> - <constraint firstItem="hLF-CV-4mn" firstAttribute="leading" secondItem="cUt-HL-Is8" secondAttribute="leading" id="L7D-2u-m46"/> - <constraint firstItem="NgY-qI-yOq" firstAttribute="top" secondItem="cUt-HL-Is8" secondAttribute="top" id="UBb-Ur-xTn"/> - <constraint firstItem="C4H-CM-EXc" firstAttribute="leading" secondItem="cUt-HL-Is8" secondAttribute="leading" id="aLA-Ny-ns1"/> - <constraint firstItem="hLF-CV-4mn" firstAttribute="top" secondItem="9TF-RQ-EIQ" secondAttribute="bottom" constant="24" id="ebA-OO-a9k"/> - <constraint firstItem="NgY-qI-yOq" firstAttribute="leading" secondItem="cUt-HL-Is8" secondAttribute="leading" id="poq-lk-quh"/> - <constraint firstAttribute="trailing" secondItem="C4H-CM-EXc" secondAttribute="trailing" id="rCO-oS-vUH"/> - <constraint firstItem="9TF-RQ-EIQ" firstAttribute="leading" secondItem="cUt-HL-Is8" secondAttribute="leading" id="z6r-P0-UIz"/> - </constraints> - </view> - </subviews> - <constraints> - <constraint firstAttribute="trailing" secondItem="cUt-HL-Is8" secondAttribute="trailing" constant="24" id="6ID-oY-lMo"/> - <constraint firstItem="cUt-HL-Is8" firstAttribute="top" secondItem="A7Y-7l-t1J" secondAttribute="top" constant="24" id="gIs-Jy-3Wt"/> - <constraint firstItem="cUt-HL-Is8" firstAttribute="leading" secondItem="A7Y-7l-t1J" secondAttribute="leading" constant="24" id="mYu-Hf-PyO"/> - <constraint firstAttribute="bottom" secondItem="cUt-HL-Is8" secondAttribute="bottom" constant="24" id="tvu-dy-eUg"/> - </constraints> - </view> - </subviews> - <constraints> - <constraint firstItem="A7Y-7l-t1J" firstAttribute="top" secondItem="0Lz-bX-FzY" secondAttribute="top" id="1cN-aK-hFF"/> - <constraint firstItem="A7Y-7l-t1J" firstAttribute="width" secondItem="0Lz-bX-FzY" secondAttribute="width" id="FZh-X9-Ucr"/> - <constraint firstAttribute="trailing" secondItem="A7Y-7l-t1J" secondAttribute="trailing" id="hSK-e2-Hvk"/> - <constraint firstAttribute="bottom" secondItem="A7Y-7l-t1J" secondAttribute="bottom" id="mVG-9l-3sw"/> - <constraint firstItem="A7Y-7l-t1J" firstAttribute="leading" secondItem="0Lz-bX-FzY" secondAttribute="leading" id="nYy-Ub-bKV"/> - </constraints> - </scrollView> - </subviews> - <color key="backgroundColor" name="Secondary"/> - <constraints> - <constraint firstAttribute="bottom" secondItem="0Lz-bX-FzY" secondAttribute="bottom" id="Vmc-qV-8ql"/> - <constraint firstItem="0Lz-bX-FzY" firstAttribute="top" secondItem="N94-2G-eN0" secondAttribute="top" id="lLv-TR-i3s"/> - <constraint firstAttribute="trailing" secondItem="0Lz-bX-FzY" secondAttribute="trailing" id="vLz-OO-5Fk"/> - <constraint firstItem="0Lz-bX-FzY" firstAttribute="leading" secondItem="N94-2G-eN0" secondAttribute="leading" id="vQL-ZY-loY"/> - </constraints> - <viewLayoutGuide key="safeArea" id="qcy-9H-fTo"/> - <point key="canvasLocation" x="139" y="153"/> - </view> - </objects> - <resources> - <image name="DangerButton" width="9" height="9"/> - <image name="DefaultButton" width="9" height="9"/> - <image name="SuccessButton" width="9" height="9"/> - <namedColor name="Secondary"> - <color red="0.098039215686274508" green="0.1803921568627451" blue="0.27058823529411763" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> - </namedColor> - </resources> -</document> diff --git a/ios/MullvadVPN/InAppPurchaseButton.swift b/ios/MullvadVPN/InAppPurchaseButton.swift index 63d91c3b9e..483ccfdf4a 100644 --- a/ios/MullvadVPN/InAppPurchaseButton.swift +++ b/ios/MullvadVPN/InAppPurchaseButton.swift @@ -25,17 +25,9 @@ class InAppPurchaseButton: AppButton { } } - override init(frame: CGRect) { - super.init(frame: frame) - commonInit() - } - - required init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - commonInit() - } + init() { + super.init(style: .success) - private func commonInit() { addSubview(activityIndicator) // Make sure the buy button scales down the font size to fit the long labels. @@ -45,6 +37,10 @@ class InAppPurchaseButton: AppButton { titleLabel?.baselineAdjustment = .alignCenters } + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + override func layoutSubviews() { super.layoutSubviews() diff --git a/ios/MullvadVPN/en.lproj/Account.strings b/ios/MullvadVPN/en.lproj/Account.strings new file mode 100644 index 0000000000..5df4e93dbd --- /dev/null +++ b/ios/MullvadVPN/en.lproj/Account.strings @@ -0,0 +1,86 @@ +/* No comment provided by engineer. */ +"ACCOUNT_EXPIRY_LABEL" = "Paid until"; + +/* Label displayed in place of account expiration when account is out of time. */ +"ACCOUNT_OUT_OF_TIME_LABEL" = "OUT OF TIME"; + +/* No comment provided by engineer. */ +"ACCOUNT_TOKEN_ACCESSIBILITY_ACTION_TITLE" = "Copy account token to pasteboard"; + +/* No comment provided by engineer. */ +"ACCOUNT_TOKEN_ACCESSIBILITY_HINT" = "Tap to copy to pasteboard."; + +/* No comment provided by engineer. */ +"ACCOUNT_TOKEN_LABEL" = "Account number"; + +/* Title for OK button in purchase failure dialog */ +"CANNOT_COMPLETE_PURCHASE_ALERT_OK_ACTION" = "OK"; + +/* Title for purchase failure dialog */ +"CANNOT_COMPLETE_PURCHASE_ALERT_TITLE" = "Cannot complete the purchase"; + +/* Message, temporarily displayed in place account token, after copying the account token to pasteboard on tap. */ +"COPIED_TO_PASTEBOARD_LABEL" = "COPIED TO PASTEBOARD!"; + +/* Modal message displayed during logout */ +"LOGGING_OUT_ALERT_TITLE" = "Logging out. Please wait..."; + +/* No comment provided by engineer. */ +"LOGOUT_BUTTON_TITLE" = "Log out"; + +/* Title for cancel button in logout dialog */ +"LOGOUT_CONFIRMATION_ALERT_CANCEL_ACTION" = "Cancel"; + +/* Message for logout dialog */ +"LOGOUT_CONFIRMATION_ALERT_MESSAGE" = "Are you sure you want to log out?\n\nThis will erase the account number from this device. It is not possible for us to recover it for you. Make sure you have your account number saved somewhere, to be able to log back in."; + +/* Title for logout dialog */ +"LOGOUT_CONFIRMATION_ALERT_TITLE" = "Log out"; + +/* Title for confirmation button in logout dialog */ +"LOGOUT_CONFIRMATION_ALERT_YES_ACTION" = "Log out"; + +/* Message for logout failure alert */ +"LOGOUT_FAILURE_ALERT_OK_ACTION" = "OK"; + +/* Title for logout failure alert */ +"LOGOUT_FAILURE_ALERT_TITLE" = "Failed to log out"; + +/* Navigation title */ +"NAVIGATION_TITLE" = "Account"; + +/* Purchase button title displayed when unable to load the price of in-app purchase. */ +"PURCHASE_BUTTON_CANNOT_CONNECT_TO_APPSTORE_LABEL" = "Cannot connect to AppStore"; + +/* Purchase button title displayed when payments are restriced on device. */ +"PURCHASE_BUTTON_PAYMENTS_RESTRICTED_LABEL" = "Payments restricted"; + +/* Purchase button title: <TITLE> (<PRICE>). The order can be changed by swapping %1 and %2. */ +"PURCHASE_BUTTON_TITLE_FORMAT" = "%1$@ (%2$@)"; + +/* Message displayed when no time credited to user account during purchase restoration; communicates that user account has already been credited with all outstanding purchased time duration. */ +"RESTORE_PURCHASES_ALERT_NO_TIME_ADDED_MESSAGE" = "Your previous purchases have already been added to this account."; + +/* Message displayed upon successful restoration of existing purchases, containing the time duration credited to user account. Use %@ placeholder to position the localized text with duration added (i.e '30 days') */ +"RESTORE_PURCHASES_ALERT_TIME_ADDED_MESSAGE" = "%@ have been added to your account"; + +/* Title for purchase restoration dialog */ +"RESTORE_PURCHASES_ALERT_TITLE" = "Restore purchases"; + +/* No comment provided by engineer. */ +"RESTORE_PURCHASES_BUTTON_TITLE" = "Restore purchases"; + +/* Title for 'OK' button in failure dialog when restoring purchases */ +"RESTORE_PURCHASES_FAILURE_ALERT_OK_ACTION" = "OK"; + +/* Title for failure dialog when restoring purchases */ +"RESTORE_PURCHASES_FAILURE_ALERT_TITLE" = "Cannot restore purchases"; + +/* No comment provided by engineer. */ +"TIME_ADDED_ALERT_OK_ACTION" = "OK"; + +/* Message displayed upon successful purchase and containing the time duration credited to user account. Use %@ placeholder to position the localized text with duration added (i.e '30 days') */ +"TIME_ADDED_ALERT_SUCCESS_MESSAGE" = "%@ have been added to your account"; + +/* Title for purchase completion dialog */ +"TIME_ADDED_ALERT_SUCCESS_TITLE" = "Thanks for your purchase"; |
