diff options
| author | Andrej Mihajlov <and@mullvad.net> | 2021-06-21 12:46:23 +0200 |
|---|---|---|
| committer | Andrej Mihajlov <and@mullvad.net> | 2021-06-21 12:46:23 +0200 |
| commit | 99a5c4a924e869f31eff1598fe24c1e58bbae192 (patch) | |
| tree | 7c9d752e876db2bdbd4c863470bb69127947d172 | |
| parent | de421aa17d4af8c23d7864b849eccfa35ca5b5bc (diff) | |
| parent | 08e7aabcbeae114693b36b0788af9deaa850589b (diff) | |
| download | mullvadvpn-99a5c4a924e869f31eff1598fe24c1e58bbae192.tar.xz mullvadvpn-99a5c4a924e869f31eff1598fe24c1e58bbae192.zip | |
Merge branch 'add-login-button'
| -rw-r--r-- | ios/CHANGELOG.md | 4 | ||||
| -rw-r--r-- | ios/MullvadVPN/AccountInputGroupView.swift | 199 | ||||
| -rw-r--r-- | ios/MullvadVPN/LoginContentView.swift | 6 | ||||
| -rw-r--r-- | ios/MullvadVPN/LoginViewController.swift | 37 |
4 files changed, 175 insertions, 71 deletions
diff --git a/ios/CHANGELOG.md b/ios/CHANGELOG.md index b9e8e5213a..eb25a9ed76 100644 --- a/ios/CHANGELOG.md +++ b/ios/CHANGELOG.md @@ -25,6 +25,10 @@ Line wrap the file at 100 chars. Th ## [Unreleased] ### Added - Show a reminder to add more credits 3 days before account expiry via system notification and in-app message. +- Add submit button next to account input field on login screen. + +### Fixed +- Update WireGuardKit to the latest. Fixes iOS 15 support. ## [2021.2] - 2021-06-03 ### Added diff --git a/ios/MullvadVPN/AccountInputGroupView.swift b/ios/MullvadVPN/AccountInputGroupView.swift index 5f9c99190f..a5b5d17277 100644 --- a/ios/MullvadVPN/AccountInputGroupView.swift +++ b/ios/MullvadVPN/AccountInputGroupView.swift @@ -10,7 +10,35 @@ import UIKit class AccountInputGroupView: UIView { - let textField: AccountTextField = { + enum Style { + case normal, error, authenticating + } + + var onSendButton: ((AccountInputGroupView) -> Void)? + + let sendButton: UIButton = { + let button = UIButton(type: .custom) + button.setImage(UIImage(named: "IconArrow"), for: .normal) + button.translatesAutoresizingMaskIntoConstraints = false + button.backgroundColor = .successColor + return button + }() + + var textField: UITextField { + return privateTextField + } + + var parsedToken: String { + return privateTextField.parsedToken + } + + let minimumAccountTokenLength = 10 + + var satisfiesMinimumTokenLengthRequirement: Bool { + return privateTextField.parsedToken.count > minimumAccountTokenLength + } + + private let privateTextField: AccountTextField = { let textField = AccountTextField() textField.font = UIFont.systemFont(ofSize: 20) textField.translatesAutoresizingMaskIntoConstraints = false @@ -32,16 +60,13 @@ class AccountInputGroupView: UIView { return textField }() - enum Style { - case normal, error, authenticating - } + private let contentView: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + return view + }() - var loginState = LoginState.default { - didSet { - updateAppearance() - updateTextFieldEnabled() - } - } + private(set) var loginState = LoginState.default private let borderRadius = CGFloat(8) private let borderWidth = CGFloat(2) @@ -49,7 +74,7 @@ class AccountInputGroupView: UIView { private var borderColor: UIColor { switch loginState { case .default: - return textField.isEditing + return privateTextField.isEditing ? UIColor.AccountTextField.NormalState.borderColor : UIColor.clear @@ -88,30 +113,84 @@ class AccountInputGroupView: UIView { } private let borderLayer = CAShapeLayer() - private let backgroundLayer = CAShapeLayer() - private let maskLayer = CALayer() + private let contentLayerMask = CALayer() // MARK: - View lifecycle override init(frame: CGRect) { super.init(frame: frame) - addSubview(textField) + addSubview(contentView) + contentView.addSubview(privateTextField) + contentView.addSubview(sendButton) NSLayoutConstraint.activate([ - textField.topAnchor.constraint(equalTo: topAnchor), - textField.leadingAnchor.constraint(equalTo: leadingAnchor), - textField.trailingAnchor.constraint(equalTo: trailingAnchor), - textField.bottomAnchor.constraint(equalTo: bottomAnchor), + contentView.topAnchor.constraint(equalTo: topAnchor), + contentView.bottomAnchor.constraint(equalTo: bottomAnchor), + contentView.leadingAnchor.constraint(equalTo: leadingAnchor), + contentView.trailingAnchor.constraint(equalTo: trailingAnchor), + + sendButton.topAnchor.constraint(equalTo: contentView.topAnchor), + sendButton.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), + sendButton.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), + sendButton.widthAnchor.constraint(equalTo: sendButton.heightAnchor), + + privateTextField.topAnchor.constraint(equalTo: contentView.topAnchor), + privateTextField.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), + privateTextField.trailingAnchor.constraint(equalTo: sendButton.leadingAnchor), + privateTextField.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), ]) - setupView() + backgroundColor = UIColor.clear + borderLayer.lineWidth = borderWidth + borderLayer.fillColor = UIColor.clear.cgColor + contentView.layer.mask = contentLayerMask + + layer.insertSublayer(borderLayer, at: 0) + + updateAppearance() + updateTextFieldEnabled() + updateSendButtonVisible(animated: false) + updateKeyboardReturnKeyEnabled() + + addTextFieldNotificationObservers() + sendButton.addTarget(self, action: #selector(handleSendButton(_:)), for: .touchUpInside) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + func setLoginState(_ state: LoginState, animated: Bool) { + loginState = state + + updateAppearance() + updateTextFieldEnabled() + updateSendButtonVisible(animated: animated) + } + + func setOnReturnKey(_ onReturnKey: ((AccountInputGroupView) -> Bool)?) { + if let onReturnKey = onReturnKey { + privateTextField.onReturnKey = { [weak self] _ -> Bool in + guard let self = self else { return true } + + return onReturnKey(self) + } + } else { + privateTextField.onReturnKey = nil + } + } + + func setToken(_ token: String) { + privateTextField.autoformattingText = token + updateSendButtonVisible(animated: false) + } + + func clearToken() { + privateTextField.autoformattingText = "" + updateSendButtonVisible(animated: false) + } + // MARK: - CALayerDelegate override func layoutSublayers(of layer: CALayer) { @@ -126,71 +205,93 @@ class AccountInputGroupView: UIView { let borderPath = borderBezierPath(size: borderFrame.size) // update the background layer mask - maskLayer.frame.size = borderFrame.size - maskLayer.contents = backgroundMaskImage(borderPath: borderPath).cgImage - - backgroundLayer.frame = borderFrame + contentLayerMask.frame = borderFrame + contentLayerMask.contents = backgroundMaskImage(borderPath: borderPath).cgImage borderLayer.path = borderPath.cgPath borderLayer.frame = borderFrame } - // MARK: - Notifications + // MARK: - Actions - @objc func textDidBeginEditing() { + @objc private func textDidBeginEditing() { updateAppearance() } - @objc func textDidEndEditing() { - updateAppearance() + @objc private func textDidChange() { + updateSendButtonVisible(animated: true) + updateKeyboardReturnKeyEnabled() } - // MARK: - Private - - private func setupView() { - backgroundColor = UIColor.clear - - borderLayer.lineWidth = borderWidth - borderLayer.fillColor = UIColor.clear.cgColor - backgroundLayer.mask = maskLayer - - layer.insertSublayer(borderLayer, at: 0) - layer.insertSublayer(backgroundLayer, at: 0) - + @objc private func textDidEndEditing() { updateAppearance() - updateTextFieldEnabled() + } - addTextFieldNotificationObservers() + @objc private func handleSendButton(_ sender: Any) { + onSendButton?(self) } + // MARK: - Private + private func addTextFieldNotificationObservers() { let notificationCenter = NotificationCenter.default notificationCenter.addObserver(self, selector: #selector(textDidBeginEditing), name: UITextField.textDidBeginEditingNotification, - object: textField) + object: privateTextField) + notificationCenter.addObserver(self, + selector: #selector(textDidChange), + name: UITextField.textDidChangeNotification, + object: privateTextField) notificationCenter.addObserver(self, selector: #selector(textDidEndEditing), name: UITextField.textDidEndEditingNotification, - object: textField) + object: privateTextField) } private func updateAppearance() { borderLayer.strokeColor = borderColor.cgColor - backgroundLayer.backgroundColor = backgroundLayerColor.cgColor - textField.textColor = textColor + contentView.backgroundColor = backgroundLayerColor + privateTextField.textColor = textColor } private func updateTextFieldEnabled() { switch loginState { case .authenticating, .success: - textField.isEnabled = false + privateTextField.isEnabled = false - default: - textField.isEnabled = true + case .default, .failure: + privateTextField.isEnabled = true } - } + } + + private func updateSendButtonVisible(animated: Bool) { + let actions = { + switch self.loginState { + case .authenticating, .success: + self.sendButton.alpha = 0 + + case .default, .failure: + let isEnabled = self.satisfiesMinimumTokenLengthRequirement + + self.sendButton.alpha = isEnabled ? 1 : 0 + self.sendButton.isUserInteractionEnabled = isEnabled + } + } + + if animated { + UIView.animate(withDuration: 0.25) { + actions() + } + } else { + actions() + } + } + + private func updateKeyboardReturnKeyEnabled() { + privateTextField.enableReturnKey = satisfiesMinimumTokenLengthRequirement + } private func borderBezierPath(size: CGSize) -> UIBezierPath { let borderPath = UIBezierPath(roundedRect: CGRect(origin: .zero, size: size), cornerRadius: borderRadius) diff --git a/ios/MullvadVPN/LoginContentView.swift b/ios/MullvadVPN/LoginContentView.swift index 8f804e556a..ad91eab558 100644 --- a/ios/MullvadVPN/LoginContentView.swift +++ b/ios/MullvadVPN/LoginContentView.swift @@ -34,10 +34,6 @@ class LoginContentView: UIView { return inputGroup }() - var accountTextField: AccountTextField { - return accountInputGroup.textField - } - let statusImageView: StatusImageView = { let imageView = StatusImageView(style: .failure) imageView.translatesAutoresizingMaskIntoConstraints = false @@ -99,7 +95,7 @@ class LoginContentView: UIView { backgroundColor = .primaryColor layoutMargins = UIMetrics.contentLayoutMargins - accountTextField.accessibilityIdentifier = "LoginTextField" + accountInputGroup.textField.accessibilityIdentifier = "LoginTextField" keyboardResponder = AutomaticKeyboardResponder(targetView: self, handler: { [weak self] (view, adjustment) in self?.contentContainerBottomConstraint?.constant = adjustment diff --git a/ios/MullvadVPN/LoginViewController.swift b/ios/MullvadVPN/LoginViewController.swift index a061fffecf..6c4bd2b920 100644 --- a/ios/MullvadVPN/LoginViewController.swift +++ b/ios/MullvadVPN/LoginViewController.swift @@ -9,8 +9,6 @@ import UIKit import Logging -private let kMinimumAccountTokenLength = 10 - enum AuthenticationMethod { case existingAccount, newAccount } @@ -91,7 +89,15 @@ class LoginViewController: UIViewController, RootContainment { contentView.bottomAnchor.constraint(equalTo: view.bottomAnchor), ]) - contentView.accountTextField.onReturnKey = { [weak self] _ in + contentView.accountInputGroup.onSendButton = { [weak self] _ in + guard let self = self else { return } + + if self.canBeginLogin() { + self.doLogin() + } + } + + contentView.accountInputGroup.setOnReturnKey { [weak self] _ in guard let self = self else { return true } if self.canBeginLogin() { @@ -105,9 +111,9 @@ class LoginViewController: UIViewController, RootContainment { // There is no need to set the input accessory toolbar on iPad since it has a dedicated // button to dismiss the keyboard. if case .phone = UIDevice.current.userInterfaceIdiom { - contentView.accountTextField.inputAccessoryView = self.accountInputAccessoryToolbar + contentView.accountInputGroup.textField.inputAccessoryView = self.accountInputAccessoryToolbar } else { - contentView.accountTextField.inputAccessoryView = nil + contentView.accountInputGroup.textField.inputAccessoryView = nil } updateDisplayedMessage() @@ -121,7 +127,7 @@ class LoginViewController: UIViewController, RootContainment { notificationCenter.addObserver(self, selector: #selector(textDidChange(_:)), name: UITextField.textDidChangeNotification, - object: contentView.accountTextField) + object: contentView.accountInputGroup.textField) } override var disablesAutomaticKeyboardDismissal: Bool { @@ -133,7 +139,7 @@ class LoginViewController: UIViewController, RootContainment { func reset() { loginState = .default - contentView.accountTextField.autoformattingText = "" + contentView.accountInputGroup.clearToken() updateKeyboardToolbar() } @@ -156,7 +162,7 @@ class LoginViewController: UIViewController, RootContainment { } @objc func doLogin() { - let accountToken = contentView.accountTextField.parsedToken + let accountToken = contentView.accountInputGroup.parsedToken beginLogin(method: .existingAccount) self.delegate?.loginViewController(self, loginWithAccountToken: accountToken, completion: { [weak self] (result) in @@ -172,13 +178,13 @@ class LoginViewController: UIViewController, RootContainment { @objc func createNewAccount() { beginLogin(method: .newAccount) - contentView.accountTextField.autoformattingText = "" + contentView.accountInputGroup.clearToken() updateKeyboardToolbar() self.delegate?.loginViewControllerLoginWithNewAccount(self, completion: { [weak self] (result) in switch result { case .success(let response): - self?.contentView.accountTextField.autoformattingText = response.token + self?.contentView.accountInputGroup.setToken(response.token) self?.endLogin(.success(.newAccount)) case .failure(let error): self?.endLogin(.failure(error)) @@ -189,7 +195,7 @@ class LoginViewController: UIViewController, RootContainment { // MARK: - Private private func loginStateDidChange() { - contentView.accountInputGroup.loginState = loginState + contentView.accountInputGroup.setLoginState(loginState, animated: true) // Keep the settings button disabled to prevent user from going to settings while // authentication or during the delay after the successful login and transition to the main @@ -235,9 +241,8 @@ class LoginViewController: UIViewController, RootContainment { loginState = nextLoginState - if case .authenticating(.existingAccount) = oldLoginState, - case .failure = loginState { - contentView.accountTextField.becomeFirstResponder() + if case .authenticating(.existingAccount) = oldLoginState, case .failure = loginState { + contentView.accountInputGroup.textField.becomeFirstResponder() } else if case .success = loginState { // Navigate to the main view after 1s delay DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1)) { @@ -253,12 +258,10 @@ class LoginViewController: UIViewController, RootContainment { private func updateKeyboardToolbar() { accountInputAccessoryLoginButton.isEnabled = canBeginLogin() - contentView.accountTextField.enableReturnKey = canBeginLogin() } private func canBeginLogin() -> Bool { - let accountTokenLength = contentView.accountTextField.parsedToken.count - return accountTokenLength >= kMinimumAccountTokenLength + return contentView.accountInputGroup.satisfiesMinimumTokenLengthRequirement } } |
