diff options
| author | Andreas Lif <andreas.lif@shortcut.io> | 2022-06-21 10:42:32 +0200 |
|---|---|---|
| committer | Andrej Mihajlov <and@mullvad.net> | 2022-07-01 09:13:01 +0200 |
| commit | 90146670b106d08fcc1b14c256e2bf4181284c37 (patch) | |
| tree | cc1f5110caeacab16b9ce835267b8999bec84387 | |
| parent | 786b27643a0bd963bb180cc0af92c0e0ea466966 (diff) | |
| download | mullvadvpn-90146670b106d08fcc1b14c256e2bf4181284c37.tar.xz mullvadvpn-90146670b106d08fcc1b14c256e2bf4181284c37.zip | |
Add device name to account view and button to show/hide account number
| -rw-r--r-- | ios/MullvadVPN/AccountContentView.swift | 311 | ||||
| -rw-r--r-- | ios/MullvadVPN/AccountViewController.swift | 36 | ||||
| -rw-r--r-- | ios/MullvadVPN/Assets.xcassets/IconCopy.imageset/Contents.json | 16 | ||||
| -rw-r--r-- | ios/MullvadVPN/Assets.xcassets/IconCopy.imageset/IconCopy.pdf | bin | 0 -> 1174 bytes | |||
| -rw-r--r-- | ios/MullvadVPN/Assets.xcassets/IconObscure.imageset/Contents.json | 16 | ||||
| -rw-r--r-- | ios/MullvadVPN/Assets.xcassets/IconObscure.imageset/IconObscure.pdf | bin | 0 -> 1481 bytes | |||
| -rw-r--r-- | ios/MullvadVPN/Assets.xcassets/IconUnobscure.imageset/Contents.json | 16 | ||||
| -rw-r--r-- | ios/MullvadVPN/Assets.xcassets/IconUnobscure.imageset/IconUnobscure.pdf | bin | 0 -> 1154 bytes | |||
| -rwxr-xr-x | ios/convert-assets.rb | 5 |
9 files changed, 333 insertions, 67 deletions
diff --git a/ios/MullvadVPN/AccountContentView.swift b/ios/MullvadVPN/AccountContentView.swift index e61dc0688b..1bc05ac064 100644 --- a/ios/MullvadVPN/AccountContentView.swift +++ b/ios/MullvadVPN/AccountContentView.swift @@ -41,8 +41,14 @@ class AccountContentView: UIView { return button }() - let accountTokenRowView: AccountTokenRow = { - let view = AccountTokenRow() + let accountDeviceRow: AccountDeviceRow = { + let view = AccountDeviceRow() + view.translatesAutoresizingMaskIntoConstraints = false + return view + }() + + let accountTokenRowView: AccountNumberRow = { + let view = AccountNumberRow() view.translatesAutoresizingMaskIntoConstraints = false return view }() @@ -54,7 +60,7 @@ class AccountContentView: UIView { }() lazy var contentStackView: UIStackView = { - let stackView = UIStackView(arrangedSubviews: [accountTokenRowView, accountExpiryRowView]) + let stackView = UIStackView(arrangedSubviews: [accountDeviceRow, accountTokenRowView, accountExpiryRowView]) stackView.translatesAutoresizingMaskIntoConstraints = false stackView.axis = .vertical stackView.spacing = UIMetrics.sectionSpacing @@ -95,17 +101,79 @@ class AccountContentView: UIView { } } -class AccountTokenRow: UIView { +class AccountDeviceRow: UIView { - var value: String? { + var deviceName: String? { didSet { - valueButton.setTitle(value, for: .normal) - accessibilityValue = value + deviceLabel.text = deviceName?.capitalized ?? "" + accessibilityValue = deviceName } } - var actionHandler: (() -> Void)? - private let textLabel: UILabel = { + private let titleLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.text = NSLocalizedString( + "DEVICE_NAME", + tableName: "Account", + value: "Device name", + comment: "" + ) + label.font = UIFont.systemFont(ofSize: 14) + label.textColor = UIColor(white: 1.0, alpha: 0.6) + return label + }() + + private let deviceLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.font = UIFont.systemFont(ofSize: 17) + label.textColor = .white + return label + }() + + override init(frame: CGRect) { + super.init(frame: frame) + + addSubview(titleLabel) + addSubview(deviceLabel) + + NSLayoutConstraint.activate([ + titleLabel.topAnchor.constraint(equalTo: topAnchor), + titleLabel.leadingAnchor.constraint(equalTo: leadingAnchor), + titleLabel.trailingAnchor.constraint(equalTo: trailingAnchor), + + deviceLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 8), + deviceLabel.leadingAnchor.constraint(equalTo: leadingAnchor), + deviceLabel.trailingAnchor.constraint(equalTo: trailingAnchor), + deviceLabel.bottomAnchor.constraint(equalTo: bottomAnchor) + ]) + + isAccessibilityElement = true + accessibilityLabel = titleLabel.text + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +class AccountNumberRow: UIView { + var accountNumber: String? { + didSet { + updateView() + } + } + + var isObscured = true { + didSet { + updateView() + } + } + + var copyAccountNumber: (() -> Void)? + + private let titleLabel: UILabel = { let textLabel = UILabel() textLabel.translatesAutoresizingMaskIntoConstraints = false textLabel.text = NSLocalizedString( @@ -119,63 +187,220 @@ class AccountTokenRow: UIView { return textLabel }() - private let valueButton: UIButton = { + private let accountNumberLabel: UILabel = { + let textLabel = UILabel() + textLabel.translatesAutoresizingMaskIntoConstraints = false + textLabel.font = UIFont.systemFont(ofSize: 17) + textLabel.textColor = .white + return textLabel + }() + + private let showHideButton: 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", - value: "Tap to copy to pasteboard.", - comment: "" - ) + button.tintColor = .white + button.setContentHuggingPriority(.defaultHigh, for: .horizontal) return button }() + private let copyButton: UIButton = { + let button = UIButton(type: .system) + button.translatesAutoresizingMaskIntoConstraints = false + button.tintColor = .white + button.setContentHuggingPriority(.defaultHigh, for: .horizontal) + return button + }() + + private var revertCopyImageWorkItem: DispatchWorkItem? + override init(frame: CGRect) { super.init(frame: frame) - addSubview(textLabel) - addSubview(valueButton) + addSubview(titleLabel) + addSubview(accountNumberLabel) + addSubview(showHideButton) + addSubview(copyButton) NSLayoutConstraint.activate([ - textLabel.topAnchor.constraint(equalTo: topAnchor), - textLabel.leadingAnchor.constraint(equalTo: leadingAnchor), - textLabel.trailingAnchor.constraint(greaterThanOrEqualTo: trailingAnchor), + titleLabel.topAnchor.constraint(equalTo: topAnchor), + titleLabel.leadingAnchor.constraint(equalTo: leadingAnchor), + titleLabel.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) + accountNumberLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 8), + accountNumberLabel.leadingAnchor.constraint(equalTo: leadingAnchor), + accountNumberLabel.trailingAnchor.constraint(equalTo: showHideButton.leadingAnchor), + accountNumberLabel.bottomAnchor.constraint(equalTo: bottomAnchor), + + showHideButton.heightAnchor.constraint(equalTo: accountNumberLabel.heightAnchor), + showHideButton.centerYAnchor.constraint(equalTo: accountNumberLabel.centerYAnchor), + showHideButton.leadingAnchor.constraint(equalTo: accountNumberLabel.trailingAnchor), + + copyButton.heightAnchor.constraint(equalTo: accountNumberLabel.heightAnchor), + copyButton.centerYAnchor.constraint(equalTo: accountNumberLabel.centerYAnchor), + copyButton.leadingAnchor.constraint(equalTo: showHideButton.trailingAnchor, constant: 24), + copyButton.trailingAnchor.constraint(equalTo: trailingAnchor), ]) - isAccessibilityElement = true - accessibilityLabel = textLabel.text + showHideButton.addTarget( + self, + action: #selector(didTapShowHideAccount), + for: .touchUpInside + ) - let actionName = NSLocalizedString( - "ACCOUNT_TOKEN_ACCESSIBILITY_ACTION_TITLE", - tableName: "Account", - value: "Copy account token to pasteboard", - comment: "" + copyButton.addTarget( + self, + action: #selector(didTapCopyAccountNumber), + for: .touchUpInside ) - accessibilityCustomActions = [UIAccessibilityCustomAction(name: actionName, target: self, selector: #selector(performAccessibilityAction))] - valueButton.addTarget(self, action: #selector(handleTap), for: .touchUpInside) + isAccessibilityElement = true + accessibilityLabel = titleLabel.text + + showCheckmark(false) + updateView() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - @objc private func handleTap() { - self.actionHandler?() + // MARK: - Private + + private func updateView() { + accountNumberLabel.text = displayAccountNumber ?? "" + showHideButton.setImage(showHideImage, for: .normal) + + accessibilityAttributedValue = _accessibilityAttributedValue + accessibilityCustomActions = _accessibilityCustomActions } - @objc private func performAccessibilityAction() { - self.actionHandler?() + private var displayAccountNumber: String? { + guard let accountNumber = accountNumber else { + return nil + } + + let formattedString = StringFormatter.formattedAccountNumber(from: accountNumber) + + if isObscured { + return String(formattedString.map { ch in + return ch == " " ? ch : "•" + }) + } else { + return formattedString + } + } + + private var showHideImage: UIImage? { + if isObscured { + return UIImage(named: "IconUnobscure") + } else { + return UIImage(named: "IconObscure") + } + } + + + private var _accessibilityAttributedValue: NSAttributedString? { + guard let accountNumber = accountNumber else { + return nil + } + + if isObscured { + return NSAttributedString( + string: NSLocalizedString( + "ACCOUNT_ACCESSIBILITY_OBSCURED", + tableName: "Account", + value: "Obscured", + comment: "" + ) + ) + } else { + var attributes: [NSAttributedString.Key: Any]? + if #available(iOS 13.0, *) { + attributes = [.accessibilitySpeechSpellOut: true] + } + + return NSAttributedString(string: accountNumber, attributes: attributes) + } + } + + private var _accessibilityCustomActions: [UIAccessibilityCustomAction]? { + guard accountNumber != nil else { return nil } + + return [ + UIAccessibilityCustomAction( + name: showHideAccessibilityActionName, + target: self, + selector: #selector(didTapShowHideAccount) + ), + UIAccessibilityCustomAction( + name: NSLocalizedString( + "ACCOUNT_ACCESSIBILITY_COPY_TO_PASTEBOARD", + tableName: "Account", + value: "Copy to pasteboard", + comment: "" + ), + target: self, + selector: #selector(didTapCopyAccountNumber) + ) + ] + } + + private var showHideAccessibilityActionName: String { + if isObscured { + return NSLocalizedString( + "ACCOUNT_ACCESSIBILITY_SHOW_ACCOUNT_NUMBER", + tableName: "Account", + value: "Show account number", + comment: "" + ) + } else { + return NSLocalizedString( + "ACCOUNT_ACCESSIBILITY_HIDE_ACCOUNT_NUMBER", + tableName: "Account", + value: "Hide account number", + comment: "" + ) + } + } + + private func showCheckmark(_ showCheckmark: Bool) { + if showCheckmark { + let tickIcon = UIImage(named: "IconTick") + + copyButton.setImage(tickIcon, for: .normal) + copyButton.tintColor = .successColor + } else { + let copyIcon = UIImage(named: "IconCopy") + + copyButton.setImage(copyIcon, for: .normal) + copyButton.tintColor = .white + } + } + + // MARK: - Actions + + @objc private func didTapShowHideAccount() { + isObscured.toggle() + updateView() + + UIAccessibility.post(notification: .layoutChanged, argument: nil) + } + + @objc private func didTapCopyAccountNumber() { + let delayedWorkItem = DispatchWorkItem { [weak self] in + self?.showCheckmark(false) + } + + revertCopyImageWorkItem?.cancel() + revertCopyImageWorkItem = delayedWorkItem + + showCheckmark(true) + copyAccountNumber?() + + DispatchQueue.main.asyncAfter( + deadline: .now() + .seconds(2), + execute: delayedWorkItem + ) } } @@ -206,7 +431,7 @@ class AccountExpiryRow: UIView { ) } - valueLabel.text = formattedDate + valueLabel.text = formattedDate ?? "" accessibilityValue = formattedDate valueLabel.textColor = .white diff --git a/ios/MullvadVPN/AccountViewController.swift b/ios/MullvadVPN/AccountViewController.swift index 9753d8aac9..f594e0a6c1 100644 --- a/ios/MullvadVPN/AccountViewController.swift +++ b/ios/MullvadVPN/AccountViewController.swift @@ -84,10 +84,8 @@ class AccountViewController: UIViewController, AppStorePaymentObserver, TunnelOb comment: "" ) - contentView.accountTokenRowView.value = TunnelManager.shared.accountNumber.map { string in - return StringFormatter.formattedAccountNumber(from: string) - } - contentView.accountTokenRowView.actionHandler = { [weak self] in + contentView.accountTokenRowView.accountNumber = TunnelManager.shared.accountNumber + contentView.accountTokenRowView.copyAccountNumber = { [weak self] in self?.copyAccountToken() } @@ -99,6 +97,7 @@ class AccountViewController: UIViewController, AppStorePaymentObserver, TunnelOb TunnelManager.shared.addObserver(self) updateAccountExpiry(expiryDate: TunnelManager.shared.accountExpiry) + updateDeviceName(TunnelManager.shared.device?.name) // Make sure to disable IAPs when payments are restricted if AppStorePaymentManager.canMakePayments { @@ -110,6 +109,10 @@ class AccountViewController: UIViewController, AppStorePaymentObserver, TunnelOb // MARK: - Private methods + private func updateDeviceName(_ deviceName: String?) { + contentView.accountDeviceRow.deviceName = deviceName + } + private func updateAccountExpiry(expiryDate: Date?) { contentView.accountExpiryRowView.value = expiryDate } @@ -322,7 +325,12 @@ class AccountViewController: UIViewController, AppStorePaymentObserver, TunnelOb } func tunnelManager(_ manager: TunnelManager, didUpdateTunnelSettings tunnelSettings: TunnelSettingsV2?) { - updateAccountExpiry(expiryDate: tunnelSettings?.account.expiry) + guard let tunnelSettings = tunnelSettings else { + return + } + + updateDeviceName(tunnelSettings.device.name) + updateAccountExpiry(expiryDate: tunnelSettings.account.expiry) } // MARK: - AppStorePaymentObserver @@ -377,24 +385,6 @@ class AccountViewController: UIViewController, AppStorePaymentObserver, TunnelOb private func copyAccountToken() { UIPasteboard.general.string = TunnelManager.shared.accountNumber - - contentView.accountTokenRowView.value = NSLocalizedString( - "COPIED_TO_PASTEBOARD_LABEL", - tableName: "Account", - value: "COPIED TO PASTEBOARD!", - comment: "" - ) - - let workItem = DispatchWorkItem { [weak self] in - guard let accountNumber = TunnelManager.shared.accountNumber else { return } - - self?.contentView.accountTokenRowView.value = StringFormatter.formattedAccountNumber(from: accountNumber) - } - - copyToPasteboardWork?.cancel() - copyToPasteboardWork = workItem - - DispatchQueue.main.asyncAfter(wallDeadline: .now() + .seconds(3), execute: workItem) } @objc private func doPurchase() { diff --git a/ios/MullvadVPN/Assets.xcassets/IconCopy.imageset/Contents.json b/ios/MullvadVPN/Assets.xcassets/IconCopy.imageset/Contents.json new file mode 100644 index 0000000000..761f256346 --- /dev/null +++ b/ios/MullvadVPN/Assets.xcassets/IconCopy.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "filename" : "IconCopy.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true, + "template-rendering-intent" : "template" + } +} diff --git a/ios/MullvadVPN/Assets.xcassets/IconCopy.imageset/IconCopy.pdf b/ios/MullvadVPN/Assets.xcassets/IconCopy.imageset/IconCopy.pdf Binary files differnew file mode 100644 index 0000000000..47ec193d80 --- /dev/null +++ b/ios/MullvadVPN/Assets.xcassets/IconCopy.imageset/IconCopy.pdf diff --git a/ios/MullvadVPN/Assets.xcassets/IconObscure.imageset/Contents.json b/ios/MullvadVPN/Assets.xcassets/IconObscure.imageset/Contents.json new file mode 100644 index 0000000000..e3f7020b58 --- /dev/null +++ b/ios/MullvadVPN/Assets.xcassets/IconObscure.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "filename" : "IconObscure.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true, + "template-rendering-intent" : "template" + } +} diff --git a/ios/MullvadVPN/Assets.xcassets/IconObscure.imageset/IconObscure.pdf b/ios/MullvadVPN/Assets.xcassets/IconObscure.imageset/IconObscure.pdf Binary files differnew file mode 100644 index 0000000000..813fa38764 --- /dev/null +++ b/ios/MullvadVPN/Assets.xcassets/IconObscure.imageset/IconObscure.pdf diff --git a/ios/MullvadVPN/Assets.xcassets/IconUnobscure.imageset/Contents.json b/ios/MullvadVPN/Assets.xcassets/IconUnobscure.imageset/Contents.json new file mode 100644 index 0000000000..c69f047004 --- /dev/null +++ b/ios/MullvadVPN/Assets.xcassets/IconUnobscure.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "filename" : "IconUnobscure.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true, + "template-rendering-intent" : "template" + } +} diff --git a/ios/MullvadVPN/Assets.xcassets/IconUnobscure.imageset/IconUnobscure.pdf b/ios/MullvadVPN/Assets.xcassets/IconUnobscure.imageset/IconUnobscure.pdf Binary files differnew file mode 100644 index 0000000000..77452cf7b2 --- /dev/null +++ b/ios/MullvadVPN/Assets.xcassets/IconUnobscure.imageset/IconUnobscure.pdf diff --git a/ios/convert-assets.rb b/ios/convert-assets.rb index 5d8be56277..5a26616e9e 100755 --- a/ios/convert-assets.rb +++ b/ios/convert-assets.rb @@ -32,7 +32,10 @@ GRAPHICAL_ASSETS = [ "location-marker-unsecure.svg", "logo-icon.svg", "logo-text.svg", - "icon-close-sml.svg" + "icon-close-sml.svg", + "icon-copy.svg", + "icon-obscure.svg", + "icon-unobscure.svg" ] # App icon sizes |
