mirror of
https://github.com/confirmedcode/Lockdown-iOS.git
synced 2025-12-21 12:14:02 +01:00
520 lines
21 KiB
Swift
520 lines
21 KiB
Swift
//
|
|
// SignUpViewController.swift
|
|
// Lockdown
|
|
//
|
|
// Created by Alexander Parshakov on 11/3/22
|
|
// Copyright © 2022 Confirmed Inc. All rights reserved.
|
|
//
|
|
|
|
import CocoaLumberjackSwift
|
|
import Foundation
|
|
import PopupDialog
|
|
import PromiseKit
|
|
import UIKit
|
|
|
|
final class SignUpViewController: BaseViewController, Loadable {
|
|
|
|
@IBOutlet private var closeButtonSpacer: UIView!
|
|
@IBOutlet private var closeButton: UIButton!
|
|
|
|
@IBOutlet private var upperLabelsStackView: UIStackView!
|
|
@IBOutlet private var welcomeLabel: UILabel!
|
|
@IBOutlet private var descriptionLabel: UILabel!
|
|
|
|
@IBOutlet private var emailTextField: FloatingTextInputTextField!
|
|
@IBOutlet private var passwordTextField: FloatingTextInputTextField!
|
|
@IBOutlet private var passwordValidationLabel: UILabel!
|
|
|
|
@IBOutlet private var advantageLabelOne: UILabel!
|
|
@IBOutlet private var advantageLabelTwo: UILabel!
|
|
@IBOutlet private var advantageLabelThree: UILabel!
|
|
|
|
@IBOutlet private var alternativeActionButton: UIButton!
|
|
@IBOutlet private var mainButton: UIButton!
|
|
|
|
@IBOutlet private var byContinuingLabel: UILabel!
|
|
@IBOutlet private var termsOfServiceLabel: UIButton!
|
|
@IBOutlet private var andLabel: UILabel!
|
|
@IBOutlet private var privacyPolicyLabel: UIButton!
|
|
|
|
private var signUpButtonGradientLayer: CAGradientLayer?
|
|
|
|
private var mode: AuthenticationViewControllerMode {
|
|
didSet {
|
|
DispatchQueue.main.async {
|
|
self.updateScreen()
|
|
}
|
|
}
|
|
}
|
|
|
|
private var textFieldBorderWidth: CGFloat { isDarkMode ? 2.5 : 2 }
|
|
private var isPasswordValid = false
|
|
private var didAutofillTextfield = false
|
|
|
|
/// For cases when user is focused on a textfield and swipes back to the previous screen.
|
|
private var isDisappearing = false
|
|
|
|
init(mode: AuthenticationViewControllerMode) {
|
|
self.mode = mode
|
|
|
|
super.init(nibName: nil, bundle: nil)
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
override func viewDidLoad() {
|
|
super.viewDidLoad()
|
|
|
|
setupTextFields()
|
|
setupSkipButton()
|
|
setupSignUpButton()
|
|
|
|
setupTexts()
|
|
|
|
setupGestureRecognizers()
|
|
|
|
updateScreen()
|
|
|
|
navigationController?.navigationBar.topItem?.backButtonTitle = ""
|
|
navigationController?.navigationBar.tintColor = .label
|
|
}
|
|
|
|
override func viewWillAppear(_ animated: Bool) {
|
|
super.viewWillAppear(animated)
|
|
isDisappearing = false
|
|
}
|
|
|
|
override func viewWillDisappear(_ animated: Bool) {
|
|
super.viewWillDisappear(animated)
|
|
|
|
isDisappearing = true
|
|
}
|
|
|
|
override func viewDidLayoutSubviews() {
|
|
super.viewDidLayoutSubviews()
|
|
|
|
mainButton.corners = .continuous(mainButton.bounds.midY)
|
|
signUpButtonGradientLayer?.corners = mainButton.corners
|
|
signUpButtonGradientLayer?.frame = mainButton.bounds
|
|
}
|
|
|
|
@IBAction private func didTapCloseButton(_ sender: Any) {
|
|
dismiss(animated: true)
|
|
}
|
|
|
|
@IBAction private func didTapAlternativeActionButton(_ sender: Any) {
|
|
switch mode {
|
|
case .login:
|
|
// Forgot password
|
|
guard let forgotPasswordViewController = UIStoryboard.main.instantiateViewController(withIdentifier: "ForgotPasswordViewController")
|
|
as? ForgotPasswordViewController else { return }
|
|
forgotPasswordViewController.modalPresentationStyle = .fullScreen
|
|
present(forgotPasswordViewController, animated: true)
|
|
case .signUp:
|
|
// I already have an account, change to login
|
|
mode = .login
|
|
}
|
|
}
|
|
|
|
@IBAction private func didTapMainButton() {
|
|
mainButton.showAnimatedPress { [weak self] in
|
|
guard let self else { return }
|
|
switch self.mode {
|
|
case .signUp:
|
|
self.createAccount()
|
|
case .login:
|
|
self.login()
|
|
}
|
|
}
|
|
}
|
|
|
|
@IBAction private func didTapTermsOfService(_ sender: Any) {
|
|
showModalWebView(title: .localized("terms_of_service"), urlString: .lockdownUrlTerms)
|
|
}
|
|
|
|
@IBAction private func didTapPrivacyPolicy(_ sender: Any) {
|
|
showModalWebView(title: .localized("Privacy Policy"), urlString: .lockdownUrlPrivacy)
|
|
}
|
|
|
|
private func setupTextFields() {
|
|
[emailTextField, passwordTextField].forEach {
|
|
$0?.titleFont = .regularLockdownFont(size: 13)
|
|
$0?.placeholderFont = .regularLockdownFont(size: 17)
|
|
$0?.titleColor = .gray
|
|
$0?.textColor = .black
|
|
$0?.corners = .continuous(8)
|
|
$0?.delegate = self
|
|
|
|
if traitCollection.layoutDirection == .rightToLeft {
|
|
$0?.textAlignment = .right
|
|
$0?.semanticContentAttribute = .forceRightToLeft
|
|
}
|
|
}
|
|
emailTextField.addTarget(self, action: #selector(updateMainButtonState), for: .editingChanged)
|
|
passwordTextField.addTarget(self, action: #selector(validatePassword), for: .editingChanged)
|
|
}
|
|
|
|
private func setupSkipButton() {
|
|
let skipButton = UIBarButtonItem(title: .localized("skip"), style: .plain, target: self, action: #selector(proceedToEnableNotifications))
|
|
|
|
[.normal, .highlighted].forEach {
|
|
skipButton.setTitleTextAttributes([
|
|
.font: UIFont.regularLockdownFont(size: 17),
|
|
.underlineStyle: NSUnderlineStyle.single.rawValue
|
|
], for: $0)
|
|
}
|
|
navigationItem.rightBarButtonItem = skipButton
|
|
|
|
closeButtonSpacer.isHidden = navigationController != nil
|
|
closeButton.isHidden = navigationController != nil
|
|
}
|
|
|
|
@objc private func proceedToEnableNotifications() {
|
|
let enableNotificationsViewController = EnableNotificationsViewController()
|
|
navigationController?.pushViewController(enableNotificationsViewController, animated: true)
|
|
}
|
|
|
|
private func setupSignUpButton() {
|
|
signUpButtonGradientLayer = mainButton.applyGradient(.lightBlue)
|
|
updateMainButtonState()
|
|
}
|
|
|
|
private func setupTexts() {
|
|
welcomeLabel.text = .localized("welcome")
|
|
descriptionLabel.text = .localized("already_have_account_login")
|
|
emailTextField.title = .localized("email")
|
|
passwordTextField.title = .localized("password")
|
|
|
|
advantageLabelOne.text = .localized("access_our_curated_block_lists_and_build_your_own")
|
|
advantageLabelTwo.text = .localized("access_lockdown_across_all_your_devices")
|
|
advantageLabelThree.text = .localized("firewall_and_vpn_support")
|
|
|
|
mainButton.setTitle(.localized("onboarding_sign_up"), for: .normal)
|
|
|
|
byContinuingLabel.text = .localized("by_continuing_you_agree_with_our")
|
|
privacyPolicyLabel.setTitle(.localized("Privacy Policy"), for: .normal)
|
|
andLabel.text = .localized("and")
|
|
termsOfServiceLabel.setTitle(.localized("terms_of_service"), for: .normal)
|
|
}
|
|
|
|
private func setupGestureRecognizers() {
|
|
let tapOutsideTextfields = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard))
|
|
view.addGestureRecognizer(tapOutsideTextfields)
|
|
}
|
|
|
|
@objc private func dismissKeyboard() {
|
|
view.endEditing(true)
|
|
}
|
|
}
|
|
|
|
extension SignUpViewController {
|
|
|
|
private func updateScreen() {
|
|
switch mode {
|
|
case .signUp:
|
|
updateForSignUp()
|
|
case .login:
|
|
updateForLogin()
|
|
}
|
|
}
|
|
|
|
private func updateForSignUp() {
|
|
// So that the textfields won't trigger autofill suggestions
|
|
// https://developer.apple.com/forums/thread/108085
|
|
emailTextField.textContentType = .oneTimeCode
|
|
passwordTextField.textContentType = .oneTimeCode
|
|
|
|
transition(with: upperLabelsStackView) { [weak self] in
|
|
self?.descriptionLabel.text = .localized("sign_up_to_access_new_block_lists_from_trackers")
|
|
}
|
|
alternativeActionButton.setTitle(.localized("already_have_account_login"), for: .normal)
|
|
}
|
|
|
|
private func updateForLogin() {
|
|
emailTextField.textContentType = .username
|
|
passwordTextField.textContentType = .password
|
|
|
|
validatePassword()
|
|
|
|
transition(with: upperLabelsStackView) { [weak self] in
|
|
self?.descriptionLabel.text = .localized("already_have_account_login_below")
|
|
}
|
|
transition(with: alternativeActionButton) { [weak self] in
|
|
self?.alternativeActionButton.setTitle(.localized("forgot_password"), for: .normal)
|
|
}
|
|
transition(with: mainButton) { [weak self] in
|
|
self?.mainButton.setTitle(.localized("onboarding_login"), for: .normal)
|
|
}
|
|
}
|
|
}
|
|
|
|
extension SignUpViewController: UITextFieldDelegate, EmailValidatable {
|
|
|
|
func textField(_ textField: UITextField,
|
|
shouldChangeCharactersIn range: NSRange,
|
|
replacementString string: String) -> Bool {
|
|
// If the range is {0,0} and the string count > 1, then user copy paste text or used password autofill.
|
|
didAutofillTextfield = range == NSRange(location: 0, length: 0) && string.count > 1 && mode == .login
|
|
|
|
return true
|
|
}
|
|
|
|
func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool {
|
|
// If autofilled, don't show keyboard
|
|
if mode == .login && didAutofillTextfield {
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
|
self.didAutofillTextfield = false
|
|
}
|
|
|
|
DispatchQueue.main.async {
|
|
self.view.endEditing(true)
|
|
|
|
// Color somehow falls back to default, fixing it
|
|
textField.textColor = .black
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
textField.animateBorderWidth(toValue: textFieldBorderWidth)
|
|
return true
|
|
}
|
|
|
|
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
|
|
if textField == emailTextField {
|
|
emailTextField.animateBorderWidth(toValue: 0)
|
|
passwordTextField.becomeFirstResponder()
|
|
} else {
|
|
passwordTextField.animateBorderWidth(toValue: textFieldBorderWidth)
|
|
dismissKeyboard()
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func textFieldDidEndEditing(_ textField: UITextField, reason: UITextField.DidEndEditingReason) {
|
|
guard !isDisappearing else { return }
|
|
|
|
if textField == passwordTextField, textField.text?.count ?? 0 > 0, !isPasswordValid {
|
|
textField.animateBorderWidth(toValue: textFieldBorderWidth, color: .tunnelsWarning)
|
|
} else if textField == emailTextField, let emailError = errorValidatingEmail(emailTextField.text) {
|
|
showPopupDialog(title: .localized("invalid_email_address"),
|
|
message: emailError.localizedDescription,
|
|
transitionStyle: .fadeIn,
|
|
acceptButton: .localizedOkay)
|
|
textField.animateBorderWidth(toValue: textFieldBorderWidth, color: .tunnelsWarning)
|
|
} else {
|
|
textField.animateBorderWidth(toValue: 0)
|
|
}
|
|
}
|
|
|
|
@objc private func validatePassword() {
|
|
updateMainButtonState()
|
|
|
|
guard mode == .signUp else {
|
|
passwordTextField.animateBorderWidth(toValue: 0)
|
|
passwordValidationLabel.isHidden = true
|
|
isPasswordValid = true
|
|
return
|
|
}
|
|
|
|
let passwordRequirements = """
|
|
Password must be at least 8 characters, contain at least one uppercase letter, \
|
|
one lowercase letter, one number, and one symbol.
|
|
"""
|
|
let attrStr = NSMutableAttributedString(
|
|
string: .localized(passwordRequirements),
|
|
attributes: [
|
|
.font: UIFont.mediumLockdownFont(size: 13),
|
|
.foregroundColor: UIColor.lightGray
|
|
])
|
|
|
|
if let txt = passwordTextField.text {
|
|
isPasswordValid = true
|
|
attrStr.addAttributes(setupAttributeColor(if: (txt.count >= 8)),
|
|
range: findRange(in: attrStr.string, for: .localized("at least 8 characters")))
|
|
attrStr.addAttributes(setupAttributeColor(if: (txt.rangeOfCharacter(from: CharacterSet.uppercaseLetters) != nil)),
|
|
range: findRange(in: attrStr.string, for: .localized("one uppercase letter")))
|
|
attrStr.addAttributes(setupAttributeColor(if: (txt.rangeOfCharacter(from: CharacterSet.lowercaseLetters) != nil)),
|
|
range: findRange(in: attrStr.string, for: .localized("one lowercase letter")))
|
|
attrStr.addAttributes(setupAttributeColor(if: (txt.rangeOfCharacter(from: CharacterSet.decimalDigits) != nil)),
|
|
range: findRange(in: attrStr.string, for: .localized("one number")))
|
|
attrStr.addAttributes(setupAttributeColor(if: ((txt.rangeOfCharacter(from: CharacterSet.symbols) != nil)
|
|
|| (txt.rangeOfCharacter(from: CharacterSet.punctuationCharacters) != nil))),
|
|
range: findRange(in: attrStr.string, for: .localized("one symbol")))
|
|
} else {
|
|
isPasswordValid = false
|
|
}
|
|
|
|
passwordValidationLabel.attributedText = attrStr
|
|
passwordValidationLabel.isHidden = isPasswordValid
|
|
|
|
updateMainButtonState()
|
|
}
|
|
|
|
private func setupAttributeColor(if isValid: Bool) -> [NSAttributedString.Key: Any] {
|
|
if isValid {
|
|
return [NSAttributedString.Key.foregroundColor: UIColor.lightGray]
|
|
} else {
|
|
isPasswordValid = false
|
|
return [NSAttributedString.Key.foregroundColor: UIColor.red]
|
|
}
|
|
}
|
|
|
|
private func findRange(in baseString: String, for substring: String) -> NSRange {
|
|
if let range = baseString.localizedStandardRange(of: substring) {
|
|
let startIndex = baseString.distance(from: baseString.startIndex, to: range.lowerBound)
|
|
let length = substring.count
|
|
return NSRange(location: startIndex, length: length)
|
|
} else {
|
|
print("Range does not exist in the base string.")
|
|
return NSRange(location: 0, length: 0)
|
|
}
|
|
}
|
|
|
|
@objc private func updateMainButtonState() {
|
|
if mode == .signUp {
|
|
mainButton.isUserInteractionEnabled = emailTextField.text?.count ?? 0 > 0 && isPasswordValid
|
|
} else {
|
|
mainButton.isUserInteractionEnabled = emailTextField.text?.count ?? 0 > 0 && passwordTextField.text?.count ?? 0 > 0
|
|
}
|
|
mainButton.isEnabled = mainButton.isUserInteractionEnabled
|
|
signUpButtonGradientLayer?.isHidden = !mainButton.isUserInteractionEnabled
|
|
}
|
|
}
|
|
|
|
// MARK: - Backend
|
|
extension SignUpViewController {
|
|
|
|
private func createAccount() {
|
|
// Do /signup (do subscription-event later, user needs to confirm email first though)
|
|
showLoadingView()
|
|
|
|
// TODO: client side preliminary password fields, email validation - server does additional checking later
|
|
|
|
firstly {
|
|
try Client.signup(email: self.emailTextField.text ?? "", password: self.passwordTextField.text ?? "")
|
|
}
|
|
.catch { error in
|
|
self.hideLoadingView()
|
|
if self.popupErrorAsNSURLError(error) {
|
|
return
|
|
} else if let apiError = error as? ApiError {
|
|
switch apiError.code {
|
|
case kApiCodeEmailNotConfirmed:
|
|
// This is the "correct" case for /signup, we are expecting "1" = email confirmation sent
|
|
do {
|
|
try setAPICredentials(email: self.emailTextField.text!, password: self.passwordTextField.text!)
|
|
setAPICredentialsConfirmed(confirmed: false)
|
|
let message = """
|
|
To finish signup, click the confirmation link in the email we just sent. \
|
|
If you don't see it, check if it's stuck in your spam folder.
|
|
"""
|
|
let popup = PopupDialog(title: .localized("confirm_your_email"),
|
|
message: .localized(message),
|
|
image: nil,
|
|
buttonAlignment: .horizontal,
|
|
transitionStyle: .bounceDown,
|
|
preferredWidth: 270,
|
|
tapGestureDismissal: true,
|
|
panGestureDismissal: false,
|
|
hideStatusBar: false,
|
|
completion: nil)
|
|
popup.addButtons([
|
|
DefaultButton(title: .localizedOkay, dismissOnTap: true) {
|
|
self.hideLoadingView()
|
|
self.proceedToEnableNotifications()
|
|
}
|
|
])
|
|
self.present(popup, animated: true, completion: nil)
|
|
NotificationCenter.default.post(name: AccountUI.accountStateDidChange, object: self)
|
|
} catch {
|
|
self.showPopupDialog(
|
|
title: "Error Saving Credentials",
|
|
message: "Couldn't save credentials to local keychain. Please report this error to team@lockdownprivacy.com.",
|
|
acceptButton: .localizedOkay)
|
|
}
|
|
default:
|
|
_ = self.popupErrorAsApiError(error)
|
|
}
|
|
} else {
|
|
self.showPopupDialog(title: .localized("Error Creating Email Account"),
|
|
message: "\(error)",
|
|
acceptButton: .localizedOkay)
|
|
}
|
|
}
|
|
}
|
|
|
|
private func login() {
|
|
guard let email = emailTextField.text, let password = passwordTextField.text else {
|
|
showPopupDialog(title: .localized("check_fields"), message: .localized("email_and_password_must_not_be_empty"), acceptButton: .localizedOkay)
|
|
return
|
|
}
|
|
|
|
showLoadingView()
|
|
firstly {
|
|
try Client.signInWithEmail(email: email, password: password)
|
|
}
|
|
.done { (_: SignIn) in
|
|
try setAPICredentials(email: email, password: password)
|
|
setAPICredentialsConfirmed(confirmed: true)
|
|
self.hideLoadingView()
|
|
NotificationCenter.default.post(name: AccountUI.accountStateDidChange, object: self)
|
|
self.showPopupDialog(title: .localized("Success! 🎉"),
|
|
message: .localized("you_have_successfully_sign_in"),
|
|
acceptButton: .localizedOkay,
|
|
tapGestureDismissal: false,
|
|
panGestureDismissal: false) {
|
|
if let navigationController = self.navigationController {
|
|
let enableNotificationsViewController = EnableNotificationsViewController()
|
|
navigationController.isNavigationBarHidden = true
|
|
navigationController.setViewControllers([enableNotificationsViewController], animated: true)
|
|
self.processSuccessfulLogin()
|
|
} else {
|
|
self.presentingViewController?.dismiss(animated: true) {
|
|
self.processSuccessfulLogin()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.catch { error in
|
|
self.hideLoadingView()
|
|
var errorMessage = error.localizedDescription
|
|
if let apiError = error as? ApiError {
|
|
errorMessage = apiError.message
|
|
}
|
|
self.showPopupDialog(
|
|
title: .localized("error_signing_in"),
|
|
message: errorMessage,
|
|
transitionStyle: .zoomIn,
|
|
acceptButton: .localizedOkay) {}
|
|
}
|
|
}
|
|
|
|
private func processSuccessfulLogin() {
|
|
// logged in and confirmed - update this email with the receipt and refresh VPN credentials
|
|
firstly { () -> Promise<SubscriptionEvent> in
|
|
try Client.subscriptionEvent()
|
|
}
|
|
.then { (_: SubscriptionEvent) -> Promise<GetKey> in
|
|
try Client.getKey()
|
|
}
|
|
.done { (getKey: GetKey) in
|
|
try setVPNCredentials(id: getKey.id, keyBase64: getKey.b64)
|
|
if getUserWantsVPNEnabled() {
|
|
VPNController.shared.restart()
|
|
}
|
|
}
|
|
.catch { error in
|
|
// it's okay for this to error out with "no subscription in receipt"
|
|
DDLogError("HomeViewController ConfirmEmail subscriptionevent error (ok for it to be \"no subscription in receipt\"): \(error)")
|
|
}
|
|
}
|
|
}
|
|
|
|
enum AuthenticationViewControllerMode {
|
|
case login, signUp
|
|
}
|