diff options
| author | Andrej Mihajlov <and@mullvad.net> | 2019-05-21 18:49:48 +0200 |
|---|---|---|
| committer | Andrej Mihajlov <and@mullvad.net> | 2019-05-22 11:35:38 +0200 |
| commit | e4c07244f9064630133412050482308a2a34c502 (patch) | |
| tree | 867f0d2594413d31593ee6b34949d9f8d7b1f993 | |
| parent | 3c0099e0cfe1a29fb104513a9afc85b63ef987f7 (diff) | |
| download | mullvadvpn-e4c07244f9064630133412050482308a2a34c502.tar.xz mullvadvpn-e4c07244f9064630133412050482308a2a34c502.zip | |
Show login errors
| -rw-r--r-- | ios/MullvadVPN.xcodeproj/project.pbxproj | 8 | ||||
| -rw-r--r-- | ios/MullvadVPN/AccountInputGroupView.swift | 100 | ||||
| -rw-r--r-- | ios/MullvadVPN/Base.lproj/Main.storyboard | 17 | ||||
| -rw-r--r-- | ios/MullvadVPN/HeaderBarViewController.swift | 2 | ||||
| -rw-r--r-- | ios/MullvadVPN/LoginState.swift | 16 | ||||
| -rw-r--r-- | ios/MullvadVPN/LoginViewController.swift | 178 | ||||
| -rw-r--r-- | ios/MullvadVPN/UIColor+Palette.swift | 22 | ||||
| -rw-r--r-- | ios/MullvadVPN/WebLinks.swift | 17 |
8 files changed, 320 insertions, 40 deletions
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index 3773323af9..dfd261a7fb 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -12,6 +12,8 @@ 58461AD3228D622E00B72ECB /* Account.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58461AD2228D622E00B72ECB /* Account.swift */; }; 5862805422428EF100F5A6E1 /* TranslucentButtonBlurView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5862805322428EF100F5A6E1 /* TranslucentButtonBlurView.swift */; }; 5867A51C2248F26A005513C0 /* SegueIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5867A51B2248F26A005513C0 /* SegueIdentifier.swift */; }; + 587B08E0229433EB000E6F17 /* LoginState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587B08DF229433EB000E6F17 /* LoginState.swift */; }; + 587B08E2229460C1000E6F17 /* WebLinks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587B08E1229460C1000E6F17 /* WebLinks.swift */; }; 587CBFE322807F530028DED3 /* UIColor+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587CBFE222807F530028DED3 /* UIColor+Helpers.swift */; }; 5888AD7F2279B6BF0051EB06 /* RelayStatusIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5888AD7E2279B6BF0051EB06 /* RelayStatusIndicatorView.swift */; }; 5888AD83227B11080051EB06 /* SelectLocationCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5888AD82227B11080051EB06 /* SelectLocationCell.swift */; }; @@ -73,6 +75,8 @@ 5862805322428EF100F5A6E1 /* TranslucentButtonBlurView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranslucentButtonBlurView.swift; sourceTree = "<group>"; }; 5866F39B2243B82D00168AE5 /* MullvadVPN.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = MullvadVPN.entitlements; sourceTree = "<group>"; }; 5867A51B2248F26A005513C0 /* SegueIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SegueIdentifier.swift; sourceTree = "<group>"; }; + 587B08DF229433EB000E6F17 /* LoginState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginState.swift; sourceTree = "<group>"; }; + 587B08E1229460C1000E6F17 /* WebLinks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebLinks.swift; sourceTree = "<group>"; }; 587CBFE222807F530028DED3 /* UIColor+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+Helpers.swift"; sourceTree = "<group>"; }; 5888AD7E2279B6BF0051EB06 /* RelayStatusIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayStatusIndicatorView.swift; sourceTree = "<group>"; }; 5888AD82227B11080051EB06 /* SelectLocationCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectLocationCell.swift; sourceTree = "<group>"; }; @@ -178,6 +182,7 @@ 58F19E30228B2AEB00C7710B /* JSONRequestProcedure.swift */, 58ADDB3B227B1BD200FAFEA7 /* JsonRpc.swift */, 58CE5E6C224146210008646E /* LaunchScreen.storyboard */, + 587B08DF229433EB000E6F17 /* LoginState.swift */, 58CE5E65224146200008646E /* LoginViewController.swift */, 58CE5E67224146200008646E /* Main.storyboard */, 58ADDB3D227B1CD900FAFEA7 /* MullvadAPI.swift */, @@ -194,6 +199,7 @@ 587CBFE222807F530028DED3 /* UIColor+Helpers.swift */, 58CCA0152242560B004F3011 /* UIColor+Palette.swift */, 58FFE443228C82A00036F391 /* UserDefaultsInteractor.swift */, + 587B08E1229460C1000E6F17 /* WebLinks.swift */, ); path = MullvadVPN; sourceTree = "<group>"; @@ -407,8 +413,10 @@ 58CCA0162242560B004F3011 /* UIColor+Palette.swift in Sources */, 587CBFE322807F530028DED3 /* UIColor+Helpers.swift in Sources */, 589AB4F9227C50D80039131E /* CustomNavigationBar.swift in Sources */, + 587B08E2229460C1000E6F17 /* WebLinks.swift in Sources */, 58CCA01822426713004F3011 /* AccountViewController.swift in Sources */, 58ADDB3E227B1CD900FAFEA7 /* MullvadAPI.swift in Sources */, + 587B08E0229433EB000E6F17 /* LoginState.swift in Sources */, 5862805422428EF100F5A6E1 /* TranslucentButtonBlurView.swift in Sources */, 5888AD83227B11080051EB06 /* SelectLocationCell.swift in Sources */, 58CE5E66224146200008646E /* LoginViewController.swift in Sources */, diff --git a/ios/MullvadVPN/AccountInputGroupView.swift b/ios/MullvadVPN/AccountInputGroupView.swift index fb890a14fd..1575e3584f 100644 --- a/ios/MullvadVPN/AccountInputGroupView.swift +++ b/ios/MullvadVPN/AccountInputGroupView.swift @@ -12,20 +12,69 @@ import UIKit @IBOutlet var textField: UITextField! + enum Style { + case normal, error, authenticating + } + + var loginState = LoginState.default { + didSet { + updateAppearance() + updateTextFieldEnabled() + } + } + private let borderRadius = CGFloat(8) private let borderWidth = CGFloat(2) + private var borderColor: UIColor { + switch loginState { + case .default: + return textField.isEditing + ? UIColor.AccountTextField.NormalState.borderColor + : UIColor.clear + + case .failure: + return UIColor.AccountTextField.ErrorState.borderColor + + case .authenticating, .success: + return UIColor.AccountTextField.AuthenticatingState.borderColor + } + } + + private var textColor: UIColor { + switch loginState { + case .default: + return UIColor.AccountTextField.NormalState.textColor + + case .failure: + return UIColor.AccountTextField.ErrorState.textColor + + case .authenticating, .success: + return UIColor.AccountTextField.AuthenticatingState.textColor + } + } + + private var backgroundLayerColor: UIColor { + switch loginState { + case .default: + return UIColor.AccountTextField.NormalState.backgroundColor + + case .failure: + return UIColor.AccountTextField.ErrorState.backgroundColor + + case .authenticating, .success: + return UIColor.AccountTextField.AuthenticatingState.backgroundColor + } + } + private let borderLayer = CAShapeLayer() private let backgroundLayer = CAShapeLayer() private let maskLayer = CALayer() - override init(frame: CGRect) { - super.init(frame: frame) - setup() - } + // MARK: - View lifecycle - required init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) + override func awakeFromNib() { + super.awakeFromNib() setup() } @@ -55,11 +104,11 @@ import UIKit // MARK: - Notifications @objc func textDidBeginEditing() { - updateBorderStyle() + updateAppearance() } @objc func textDidEndEditing() { - updateBorderStyle() + updateAppearance() } // MARK: - Private @@ -68,30 +117,47 @@ import UIKit backgroundColor = UIColor.clear borderLayer.lineWidth = borderWidth - borderLayer.strokeColor = UIColor.clear.cgColor borderLayer.fillColor = UIColor.clear.cgColor - - backgroundLayer.backgroundColor = UIColor.white.cgColor backgroundLayer.mask = maskLayer layer.insertSublayer(borderLayer, at: 0) layer.insertSublayer(backgroundLayer, at: 0) + updateAppearance() + updateTextFieldEnabled() + addTextFieldNotificationObservers() } - private func addTextFieldNotificationObservers() { - NotificationCenter.default.addObserver(self, selector: #selector(textDidBeginEditing), name: UITextField.textDidBeginEditingNotification, object: textField) - NotificationCenter.default.addObserver(self, selector: #selector(textDidEndEditing), name: UITextField.textDidEndEditingNotification, object: textField) - } + let notificationCenter = NotificationCenter.default - private func updateBorderStyle() { - let borderColor = textField.isEditing ? UIColor.accountTextFieldBorderColor : UIColor.clear + notificationCenter.addObserver(self, + selector: #selector(textDidBeginEditing), + name: UITextField.textDidBeginEditingNotification, + object: textField) + notificationCenter.addObserver(self, + selector: #selector(textDidEndEditing), + name: UITextField.textDidEndEditingNotification, + object: textField) + } + private func updateAppearance() { borderLayer.strokeColor = borderColor.cgColor + backgroundLayer.backgroundColor = backgroundLayerColor.cgColor + textField.textColor = textColor } + private func updateTextFieldEnabled() { + switch loginState { + case .authenticating, .success: + textField.isEnabled = false + + default: + textField.isEnabled = true + } + } + private func borderBezierPath(size: CGSize) -> UIBezierPath { let borderPath = UIBezierPath(roundedRect: CGRect(origin: .zero, size: size), cornerRadius: borderRadius) borderPath.lineWidth = borderWidth diff --git a/ios/MullvadVPN/Base.lproj/Main.storyboard b/ios/MullvadVPN/Base.lproj/Main.storyboard index a79cd3d02c..6646fe40c1 100644 --- a/ios/MullvadVPN/Base.lproj/Main.storyboard +++ b/ios/MullvadVPN/Base.lproj/Main.storyboard @@ -19,6 +19,7 @@ <subviews> <containerView opaque="NO" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" translatesAutoresizingMaskIntoConstraints="NO" id="VIF-P4-vZU" userLabel="Header"> <rect key="frame" x="0.0" y="20" width="375" height="74"/> + <color key="backgroundColor" red="0.16078431369999999" green="0.30196078430000001" blue="0.45098039220000002" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> <constraints> <constraint firstAttribute="height" constant="74" id="ohH-N4-eN7"/> </constraints> @@ -37,6 +38,9 @@ <constraint firstAttribute="width" constant="48" id="ohE-fk-mg9"/> </constraints> </view> + <imageView clipsSubviews="YES" userInteractionEnabled="NO" alpha="0.0" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="IconSuccess" translatesAutoresizingMaskIntoConstraints="NO" id="7ux-Tb-Fzq"> + <rect key="frame" x="157.5" y="119.5" width="60" height="60"/> + </imageView> <view contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" translatesAutoresizingMaskIntoConstraints="NO" id="V3j-Lb-fSQ" userLabel="Form"> <rect key="frame" x="0.0" y="203.5" width="375" height="126"/> <subviews> @@ -92,8 +96,10 @@ </subviews> <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <constraints> + <constraint firstItem="7ux-Tb-Fzq" firstAttribute="centerX" secondItem="pID-oa-Rrg" secondAttribute="centerX" id="0pe-6n-wrA"/> <constraint firstItem="V3j-Lb-fSQ" firstAttribute="top" secondItem="pID-oa-Rrg" secondAttribute="bottom" constant="30" id="2Sy-bS-AZZ"/> <constraint firstItem="V3j-Lb-fSQ" firstAttribute="centerY" secondItem="0ZY-Kh-JiM" secondAttribute="centerY" constant="-20" id="3Uk-YZ-4C3"/> + <constraint firstItem="7ux-Tb-Fzq" firstAttribute="centerY" secondItem="pID-oa-Rrg" secondAttribute="centerY" id="BRH-Bd-Pe8"/> <constraint firstAttribute="trailing" secondItem="V3j-Lb-fSQ" secondAttribute="trailing" id="EHy-Cx-cGj"/> <constraint firstItem="pID-oa-Rrg" firstAttribute="centerX" secondItem="0ZY-Kh-JiM" secondAttribute="centerX" id="Ojm-D5-HnO"/> <constraint firstItem="V3j-Lb-fSQ" firstAttribute="leading" secondItem="0ZY-Kh-JiM" secondAttribute="leading" id="alr-G1-L4w"/> @@ -115,6 +121,9 @@ <state key="normal" title="Create account" backgroundImage="DefaultButton"> <color key="titleColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> </state> + <connections> + <action selector="openCreateAccount" destination="BYZ-38-t0r" eventType="touchUpInside" id="Ejr-wN-rdN"/> + </connections> </button> </subviews> <color key="backgroundColor" red="0.098039215686274508" green="0.1803921568627451" blue="0.27058823529411763" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> @@ -147,11 +156,15 @@ <viewLayoutGuide key="safeArea" id="RSb-dJ-fKl"/> </view> <connections> + <outlet property="accountInputGroup" destination="VmT-ya-ufe" id="ku3-qa-yfV"/> <outlet property="accountTextField" destination="XOB-ct-yLU" id="mXd-SV-E16"/> <outlet property="activityIndicator" destination="pID-oa-Rrg" id="GG2-Hv-FWl"/> <outlet property="keyboardToolbar" destination="waX-JF-VTG" id="kav-5t-mkA"/> + <outlet property="keyboardToolbarLoginButton" destination="0VH-wf-oEs" id="fsM-6o-9nL"/> <outlet property="loginForm" destination="V3j-Lb-fSQ" id="tYu-S8-ylm"/> <outlet property="loginFormWrapperBottomConstraint" destination="09L-EV-qfI" id="fYF-OK-trh"/> + <outlet property="messageLabel" destination="XSV-Lk-dj4" id="8bd-TU-yhD"/> + <outlet property="statusImageView" destination="7ux-Tb-Fzq" id="UhU-kS-PKR"/> <segue destination="Ki6-Mt-b6R" kind="presentation" identifier="ShowConnect" animates="NO" id="ccw-Nc-l0Q"/> <segue destination="Kqv-qu-mfF" kind="presentation" identifier="ShowSettings" id="RjC-Wk-Enk"/> </connections> @@ -404,6 +417,9 @@ <edgeInsets key="layoutMargins" top="0.0" left="0.0" bottom="0.0" right="0.0"/> <viewLayoutGuide key="safeArea" id="oeE-aF-UYv"/> </view> + <connections> + <outlet property="settingsButton" destination="uXv-Tf-PET" id="MuL-Bu-ZRF"/> + </connections> </viewController> <placeholder placeholderIdentifier="IBFirstResponder" id="Kbx-AI-gkv" userLabel="First Responder" sceneMemberID="firstResponder"/> </objects> @@ -532,6 +548,7 @@ <image name="IconChevronDown" width="24" height="24"/> <image name="IconClose" width="24" height="24"/> <image name="IconSettings" width="24" height="24"/> + <image name="IconSuccess" width="60" height="60"/> <image name="IconTick" width="24" height="24"/> <image name="LogoIcon" width="49" height="50"/> <image name="MapBackground" width="637" height="982"/> diff --git a/ios/MullvadVPN/HeaderBarViewController.swift b/ios/MullvadVPN/HeaderBarViewController.swift index f449217961..b2ab3630ba 100644 --- a/ios/MullvadVPN/HeaderBarViewController.swift +++ b/ios/MullvadVPN/HeaderBarViewController.swift @@ -15,6 +15,8 @@ protocol HeaderBarViewControllerDelegate: class { class HeaderBarViewController: UIViewController { weak var delegate: HeaderBarViewControllerDelegate? + @IBOutlet var settingsButton: UIButton! + @IBAction func handleSettingsButton() { delegate?.headerBarViewControllerShouldOpenSettings(self) } diff --git a/ios/MullvadVPN/LoginState.swift b/ios/MullvadVPN/LoginState.swift new file mode 100644 index 0000000000..b72e94da39 --- /dev/null +++ b/ios/MullvadVPN/LoginState.swift @@ -0,0 +1,16 @@ +// +// LoginState.swift +// MullvadVPN +// +// Created by pronebird on 21/05/2019. +// Copyright © 2019 Amagicom AB. All rights reserved. +// + +import Foundation + +enum LoginState { + case `default` + case authenticating + case failure(Error) + case success +} diff --git a/ios/MullvadVPN/LoginViewController.swift b/ios/MullvadVPN/LoginViewController.swift index 424a42e558..0fad658a44 100644 --- a/ios/MullvadVPN/LoginViewController.swift +++ b/ios/MullvadVPN/LoginViewController.swift @@ -10,15 +10,28 @@ import UIKit import ProcedureKit import os.log +private let kMinimumAccountTokenLength = 10 + class LoginViewController: UIViewController, HeaderBarViewControllerDelegate { @IBOutlet var keyboardToolbar: UIToolbar! + @IBOutlet var keyboardToolbarLoginButton: UIBarButtonItem! + @IBOutlet var accountInputGroup: AccountInputGroupView! @IBOutlet var accountTextField: UITextField! + @IBOutlet var messageLabel: UILabel! @IBOutlet var loginForm: UIView! @IBOutlet var loginFormWrapperBottomConstraint: NSLayoutConstraint! @IBOutlet var activityIndicator: SpinnerActivityIndicatorView! + @IBOutlet var statusImageView: UIImageView! + + private weak var headerBarController: HeaderBarViewController? private let procedureQueue = ProcedureQueue() + private var loginState = LoginState.default { + didSet { + loginStateDidChange() + } + } override var preferredStatusBarStyle: UIStatusBarStyle { return .lightContent @@ -26,7 +39,7 @@ class LoginViewController: UIViewController, HeaderBarViewControllerDelegate { override func prepare(for segue: UIStoryboardSegue, sender: Any?) { if case .embedHeader? = SegueIdentifier.Login.from(segue: segue) { - let headerBarController = segue.destination as? HeaderBarViewController + headerBarController = segue.destination as? HeaderBarViewController headerBarController?.delegate = self } } @@ -36,9 +49,39 @@ class LoginViewController: UIViewController, HeaderBarViewControllerDelegate { accountTextField.inputAccessoryView = keyboardToolbar - NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow(_:)), name: UIWindow.keyboardWillShowNotification, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillChangeFrame(_:)), name: UIWindow.keyboardWillChangeFrameNotification, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide(_:)), name: UIWindow.keyboardWillHideNotification, object: nil) + updateDisplayedMessage() + updateStatusIcon() + updateKeyboardToolbar() + + let notificationCenter = NotificationCenter.default + + notificationCenter.addObserver(self, + selector: #selector(keyboardWillShow(_:)), + name: UIWindow.keyboardWillShowNotification, + object: nil) + notificationCenter.addObserver(self, + selector: #selector(keyboardWillChangeFrame(_:)), + name: UIWindow.keyboardWillChangeFrameNotification, + object: nil) + notificationCenter.addObserver(self, + selector: #selector(keyboardWillHide(_:)), + name: UIWindow.keyboardWillHideNotification, + object: nil) + + notificationCenter.addObserver(self, + selector: #selector(textDidBeginEditing(_:)), + name: UITextField.textDidBeginEditingNotification, + object: accountTextField) + + notificationCenter.addObserver(self, + selector: #selector(textDidEndEditing(_:)), + name: UITextField.textDidEndEditingNotification, + object: accountTextField) + + notificationCenter.addObserver(self, + selector: #selector(textDidChange(_:)), + name: UITextField.textDidChangeNotification, + object: accountTextField) } // MARK: - HeaderBarViewControllerDelegate @@ -66,6 +109,26 @@ class LoginViewController: UIViewController, HeaderBarViewControllerDelegate { view.layoutIfNeeded() } + // MARK: - UITextField notifications + + @objc func textDidBeginEditing(_ notification: Notification) { + updateStatusIcon() + } + + @objc func textDidEndEditing(_ notification: Notification) { + updateStatusIcon() + } + + @objc func textDidChange(_ notification: Notification) { + // Reset the text style as user start typing + if case .failure = loginState { + loginState = .default + } + + // Enable the log in button in the keyboard toolbar + updateKeyboardToolbar() + } + // MARK: - IBActions @IBAction func cancelLogin() { @@ -75,28 +138,23 @@ class LoginViewController: UIViewController, HeaderBarViewControllerDelegate { @IBAction func doLogin() { let accountToken = accountTextField.text ?? "" - beginLoginAnimations() + beginLogin() verifyAccount(accountToken: accountToken) { [weak self] (result) in guard let self = self else { return } switch result { case .success: - self.performSegue(withIdentifier: SegueIdentifier.Login.showConnect.rawValue, - sender: self) - - case .failure(let error as Account.Error): - // TODO: Handle account errors - break + self.endLogin(.success) case .failure(let error): - // TODO: Handle any other errors - break + self.endLogin(.failure(error)) } - - self.endLoginAnimations() } + } + @IBAction func openCreateAccount() { + UIApplication.shared.open(WebLinks.createAccountURL, options: [:]) } // MARK: - Private @@ -113,16 +171,72 @@ class LoginViewController: UIViewController, HeaderBarViewControllerDelegate { procedureQueue.addOperations([delayProcedure, loginProcedure]) } - private func beginLoginAnimations() { - activityIndicator.isAnimating = true - accountTextField.isEnabled = false + private func loginStateDidChange() { + accountInputGroup.loginState = loginState + + if case .authenticating = loginState { + activityIndicator.isAnimating = true + headerBarController?.settingsButton.isEnabled = false + } else { + activityIndicator.isAnimating = false + headerBarController?.settingsButton.isEnabled = true + } + + updateDisplayedMessage() + updateStatusIcon() + } + + private func updateStatusIcon() { + switch loginState { + case .failure: + let opacity: CGFloat = self.accountTextField.isEditing ? 0 : 1 + statusImageView.image = UIImage(imageLiteralResourceName: "IconFail") + animateStatusImage(to: opacity) + + case .success: + statusImageView.image = UIImage(imageLiteralResourceName: "IconSuccess") + animateStatusImage(to: 1) + + default: + animateStatusImage(to: 0) + } + } + + private func animateStatusImage(to alpha: CGFloat) { + UIView.animate(withDuration: 0.25) { + self.statusImageView.alpha = alpha + } + } + + private func beginLogin() { + loginState = .authenticating view.endEditing(true) } - private func endLoginAnimations() { - activityIndicator.isAnimating = false - accountTextField.isEnabled = true + private func endLogin(_ nextLoginState: LoginState) { + loginState = nextLoginState + + if case .failure = loginState { + accountTextField.becomeFirstResponder() + } else if case .success = loginState { + // Navigate to the main view after 1s delay + DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1)) { + self.performSegue(withIdentifier: SegueIdentifier.Login.showConnect.rawValue, + sender: self) + } + } + } + + private func updateDisplayedMessage() { + messageLabel.text = loginState.localizedDescription + } + + private func updateKeyboardToolbar() { + let accountTokenLength = accountTextField.text?.count ?? 0 + let enableButton = accountTokenLength >= kMinimumAccountTokenLength + + keyboardToolbarLoginButton.isEnabled = enableButton } private func makeLoginFormVisible(keyboardFrame: CGRect) { @@ -134,3 +248,25 @@ class LoginViewController: UIViewController, HeaderBarViewControllerDelegate { } } +/// Private extension that brings localizable messages displayed in the Login view controller +private extension LoginState { + var localizedDescription: String { + switch self { + case .default: + return NSLocalizedString("Enter your account number", tableName: "Login", comment: "") + + case .authenticating: + return NSLocalizedString("Checking account number", tableName: "Login", comment: "") + + case .failure(let error): + if case .invalidAccount? = error as? Account.Error { + return NSLocalizedString("Invalid account number", tableName: "Login", comment: "") + } else { + return NSLocalizedString("Internal error", tableName: "Login", comment: "") + } + + case .success: + return NSLocalizedString("Correct account number", tableName: "Login", comment: "") + } + } +} diff --git a/ios/MullvadVPN/UIColor+Palette.swift b/ios/MullvadVPN/UIColor+Palette.swift index bfe80e5bd2..42d6b36635 100644 --- a/ios/MullvadVPN/UIColor+Palette.swift +++ b/ios/MullvadVPN/UIColor+Palette.swift @@ -9,8 +9,26 @@ import UIKit extension UIColor { - // Account text field - static let accountTextFieldBorderColor = UIColor(red: 0.10, green: 0.18, blue: 0.27, alpha: 1.0) + + struct AccountTextField { + struct NormalState { + static let borderColor = UIColor(red: 0.10, green: 0.18, blue: 0.27, alpha: 1.0) + static let textColor = UIColor(red: 0.16, green: 0.30, blue: 0.45, alpha: 1.0) + static let backgroundColor = UIColor.white + } + + struct ErrorState { + static let borderColor = UIColor(red: 0.82, green: 0.01, blue: 0.11, alpha: 0.4) + static let textColor = UIColor(red: 0.82, green: 0.01, blue: 0.11, alpha: 1.0) + static let backgroundColor = UIColor.white + } + + struct AuthenticatingState { + static let borderColor = UIColor.clear + static let textColor = UIColor.white + static let backgroundColor = UIColor.white.withAlphaComponent(0.2) + } + } // Relay availability indicator view static let relayStatusIndicatorActiveColor = UIColor(red: 0.27, green: 0.68, blue: 0.30, alpha: 0.9) diff --git a/ios/MullvadVPN/WebLinks.swift b/ios/MullvadVPN/WebLinks.swift new file mode 100644 index 0000000000..3a48f96c16 --- /dev/null +++ b/ios/MullvadVPN/WebLinks.swift @@ -0,0 +1,17 @@ +// +// WebLinks.swift +// MullvadVPN +// +// Created by pronebird on 21/05/2019. +// Copyright © 2019 Amagicom AB. All rights reserved. +// + +import Foundation + +struct WebLinks { + + static let createAccountURL = URL(string: "https://mullvad.net/account/create/")! + static let purchaseURL = URL(string: "https://mullvad.net/account/login/")! + static let faqURL = URL(string: "https://mullvad.net/faq/")! + +} |
