summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAndrej Mihajlov <and@mullvad.net>2019-05-21 18:49:48 +0200
committerAndrej Mihajlov <and@mullvad.net>2019-05-22 11:35:38 +0200
commite4c07244f9064630133412050482308a2a34c502 (patch)
tree867f0d2594413d31593ee6b34949d9f8d7b1f993
parent3c0099e0cfe1a29fb104513a9afc85b63ef987f7 (diff)
downloadmullvadvpn-e4c07244f9064630133412050482308a2a34c502.tar.xz
mullvadvpn-e4c07244f9064630133412050482308a2a34c502.zip
Show login errors
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj8
-rw-r--r--ios/MullvadVPN/AccountInputGroupView.swift100
-rw-r--r--ios/MullvadVPN/Base.lproj/Main.storyboard17
-rw-r--r--ios/MullvadVPN/HeaderBarViewController.swift2
-rw-r--r--ios/MullvadVPN/LoginState.swift16
-rw-r--r--ios/MullvadVPN/LoginViewController.swift178
-rw-r--r--ios/MullvadVPN/UIColor+Palette.swift22
-rw-r--r--ios/MullvadVPN/WebLinks.swift17
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/")!
+
+}