summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAndrej Mihajlov <and@mullvad.net>2021-06-21 12:46:23 +0200
committerAndrej Mihajlov <and@mullvad.net>2021-06-21 12:46:23 +0200
commit99a5c4a924e869f31eff1598fe24c1e58bbae192 (patch)
tree7c9d752e876db2bdbd4c863470bb69127947d172
parentde421aa17d4af8c23d7864b849eccfa35ca5b5bc (diff)
parent08e7aabcbeae114693b36b0788af9deaa850589b (diff)
downloadmullvadvpn-99a5c4a924e869f31eff1598fe24c1e58bbae192.tar.xz
mullvadvpn-99a5c4a924e869f31eff1598fe24c1e58bbae192.zip
Merge branch 'add-login-button'
-rw-r--r--ios/CHANGELOG.md4
-rw-r--r--ios/MullvadVPN/AccountInputGroupView.swift199
-rw-r--r--ios/MullvadVPN/LoginContentView.swift6
-rw-r--r--ios/MullvadVPN/LoginViewController.swift37
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
}
}