diff --git a/CleanCodeApp/Modules/Features/Luz/HomeFlow/Home/LuzHomeCoordinator.swift b/CleanCodeApp/Modules/Features/Luz/HomeFlow/Home/LuzHomeCoordinator.swift new file mode 100644 index 0000000..10c1623 --- /dev/null +++ b/CleanCodeApp/Modules/Features/Luz/HomeFlow/Home/LuzHomeCoordinator.swift @@ -0,0 +1,19 @@ +import UIKit + +protocol LuzHomeCoordinatorProtocol { + func start() +} + +final class LuzHomeCoordinator: LuzHomeCoordinatorProtocol { + private var window: UIWindow? + + public init(window: UIWindow?) { + self.window = window + } + + func start() { + let navigationController = LuzHomeFactory.make() + window?.rootViewController = navigationController + window?.makeKeyAndVisible() + } +} diff --git a/CleanCodeApp/Modules/Features/Luz/HomeFlow/Home/LuzHomeFactory.swift b/CleanCodeApp/Modules/Features/Luz/HomeFlow/Home/LuzHomeFactory.swift new file mode 100644 index 0000000..827132b --- /dev/null +++ b/CleanCodeApp/Modules/Features/Luz/HomeFlow/Home/LuzHomeFactory.swift @@ -0,0 +1,7 @@ +import UIKit + +enum LuzHomeFactory { + static func make() -> UINavigationController { + UINavigationController(rootViewController: LuzHomeViewController()) + } +} diff --git a/CleanCodeApp/Modules/Features/Luz/Login/LuzLoginEmailValidator.swift b/CleanCodeApp/Modules/Features/Luz/Login/LuzLoginEmailValidator.swift new file mode 100644 index 0000000..f5e0aba --- /dev/null +++ b/CleanCodeApp/Modules/Features/Luz/Login/LuzLoginEmailValidator.swift @@ -0,0 +1,17 @@ +enum LuzLoginEmailValidatorError: Error { + case invalidEmail + case emptyEmail +} + +enum LuzLoginEmailValidator { + static func validate(_ email: String?) throws { + guard let email, !email.isEmpty else { + throw LuzLoginEmailValidatorError.emptyEmail + } + + let emailRegex = #"^[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$"# + guard email.range(of: emailRegex, options: .regularExpression) != nil else { + throw LuzLoginEmailValidatorError.invalidEmail + } + } +} diff --git a/CleanCodeApp/Modules/Features/Luz/Login/LuzLoginFactory.swift b/CleanCodeApp/Modules/Features/Luz/Login/LuzLoginFactory.swift new file mode 100644 index 0000000..e41f77f --- /dev/null +++ b/CleanCodeApp/Modules/Features/Luz/Login/LuzLoginFactory.swift @@ -0,0 +1,8 @@ +import UIKit + +enum LuzLoginFactory { + static func make() -> UIViewController { + let viewModel = LuzLoginViewModel() + return LuzLoginViewController(viewModel: viewModel) + } +} diff --git a/CleanCodeApp/Modules/Features/Luz/Login/LuzLoginViewController.swift b/CleanCodeApp/Modules/Features/Luz/Login/LuzLoginViewController.swift index 93362fe..c316c9f 100644 --- a/CleanCodeApp/Modules/Features/Luz/Login/LuzLoginViewController.swift +++ b/CleanCodeApp/Modules/Features/Luz/Login/LuzLoginViewController.swift @@ -1,179 +1,184 @@ import UIKit -class LuzLoginViewController: UIViewController { - +final class LuzLoginViewController: UIViewController { + private let viewModel: LuzLoginViewModel + @IBOutlet weak var heightLabelError: NSLayoutConstraint! @IBOutlet weak var errorLabel: UILabel! - + @IBOutlet weak var emailTextField: UITextField! @IBOutlet weak var passwordTextField: UITextField! - + @IBOutlet weak var loginButton: UIButton! @IBOutlet weak var createAccountButton: UIButton! - - var showPassword = true @IBOutlet weak var showPasswordButton: UIButton! + + var showPassword = true var errorInLogin = false - - override func viewDidLoad() { - super.viewDidLoad() - verifyLogin() - #if DEBUG - emailTextField.text = "clean.code@devpass.com" - passwordTextField.text = "111111" - #endif + init(viewModel: LuzLoginViewModel) { + self.viewModel = viewModel + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } - self.setupView() - self.validateButton() + override func viewDidLoad() { + super.viewDidLoad() + setupDebugConfiguration() + setupView() + validateSession() + validateButton() } - - open override var preferredStatusBarStyle: UIStatusBarStyle { + + public override var preferredStatusBarStyle: UIStatusBarStyle { return .lightContent } - func verifyLogin() { - if let _ = UserDefaultsManager.UserInfos.shared.readSesion() { - let vc = UINavigationController(rootViewController: LuzHomeViewController()) - let scenes = UIApplication.shared.connectedScenes - let windowScene = scenes.first as? UIWindowScene - let window = windowScene?.windows.first - window?.rootViewController = vc - window?.makeKeyAndVisible() + func setupDebugConfiguration() { + viewModel.setupDebugConfiguration( + for: emailTextField, + passwordTextField: passwordTextField + ) + } + + func validateSession() { + viewModel.validateSession { [weak self] in + self?.setupHomeViewController() } } - - @IBAction func loginButton(_ sender: Any) { - if !ConnectivityManager.shared.isConnected { - let alertController = UIAlertController(title: "Sem conexão", message: "Conecte-se à internet para tentar novamente", preferredStyle: .alert) - let actin = UIAlertAction(title: "Ok", style: .default) - alertController.addAction(actin) - present(alertController, animated: true) - return + + func checkConnectionInternet() { + viewModel.checkConnectionInternet { [weak self] alert in + self?.present(alert, animated: true) } + } + + // MARK: - IBActions + @IBAction func didTapLogin(_ sender: Any) { + checkConnectionInternet() emailTextField.setDefaultColor() passwordTextField.setDefaultColor() showLoading() - let parameters: [String: String] = ["email": emailTextField.text!, - "password": passwordTextField.text!] - let endpoint = Endpoints.Auth.login - AF.shared.request(endpoint, method: .get, parameters: parameters, headers: nil) { result in - DispatchQueue.main.async { - self.stopLoading() - switch result { - case .success(let data): - let decoder = JSONDecoder() - if let session = try? decoder.decode(Session.self, from: data) { - let vc = UINavigationController(rootViewController: LuzHomeViewController()) - let scenes = UIApplication.shared.connectedScenes - let windowScene = scenes.first as? UIWindowScene - let window = windowScene?.windows.first - window?.rootViewController = vc - window?.makeKeyAndVisible() - UserDefaultsManager.UserInfos.shared.save(session: session, user: nil) - } else { - Globals.alertMessage(title: "Ops..", message: "Houve um problema, tente novamente mais tarde.", targetVC: self) - } - case .failure: - self.setErrorLogin("E-mail ou senha incorretos") - Globals.alertMessage(title: "Ops..", message: "Houve um problema, tente novamente mais tarde.", targetVC: self) - } + + Task { + do { + let data = try await viewModel.login( + email: emailTextField.text, + password: passwordTextField.text + ) + stopLoading() + try viewModel.handleLoginSuccess(data: data) + Task { @MainActor in self.setupHomeViewController() } + } catch { + debugPrint("error: \(error.localizedDescription)") + handleError() } } } - - @IBAction func showPassword(_ sender: Any) { - if(showPassword == true) { - passwordTextField.isSecureTextEntry = false - showPasswordButton.setImage(UIImage.init(systemName: "eye.slash")?.withRenderingMode(.alwaysTemplate), for: .normal) - } else { - passwordTextField.isSecureTextEntry = true - showPasswordButton.setImage(UIImage.init(systemName: "eye")?.withRenderingMode(.alwaysTemplate), for: .normal) - } - showPassword = !showPassword - } - - @IBAction func resetPasswordButton(_ sender: Any) { - let storyboard = UIStoryboard(name: "LuzUser", bundle: nil) - let vc = storyboard.instantiateViewController(withIdentifier: "LuzResetPasswordViewController") as! LuzResetPasswordViewController - vc.modalPresentationStyle = .fullScreen - present(vc, animated: true) - } - - - @IBAction func createAccountButton(_ sender: Any) { + + @IBAction func didTapShowPassword(_ sender: Any) { + showPassword.toggle() + passwordTextField.isSecureTextEntry = !showPassword + let imageName = showPassword ? "eye.slash" : "eye" + showPasswordButton.setImage( + UIImage(systemName: imageName)?.withRenderingMode(.alwaysTemplate), + for: .normal + ) + } + + @IBAction func didTapResetPassword(_ sender: Any) { + let viewController = LuzResetPasswordFactory.make() + viewController.modalPresentationStyle = .fullScreen + present(viewController, animated: true) + } + + + @IBAction func didTapCreateAccount(_ sender: Any) { let controller = LuzCreateAccountViewController() controller.modalPresentationStyle = .fullScreen present(controller, animated: true) } } -// MARK: - Comportamentos de layout -extension LuzLoginViewController { - +// MARK: - Setup View +private extension LuzLoginViewController { func setupView() { - heightLabelError.constant = 0 - loginButton.layer.cornerRadius = loginButton.frame.height / 2 - loginButton.backgroundColor = .blue - loginButton.setTitleColor(.white, for: .normal) - loginButton.isEnabled = true + configureUI() + configureGestureRecognizer() + validateButton() + } + func configureUI() { + heightLabelError.constant = 0 + configureButton(for: loginButton, backgroundColor: .blue, titleColor: .white) + configureBorderButton(for: createAccountButton, borderColor: .blue) showPasswordButton.tintColor = .lightGray - - createAccountButton.layer.cornerRadius = createAccountButton.frame.height / 2 - createAccountButton.layer.borderWidth = 1 - createAccountButton.layer.borderColor = UIColor.blue.cgColor - createAccountButton.setTitleColor(.blue, for: .normal) - createAccountButton.backgroundColor = .white - emailTextField.setDefaultColor() passwordTextField.setDefaultColor() - let gesture = UITapGestureRecognizer(target: self, action: #selector(didClickView)) - view.addGestureRecognizer(gesture) - view.isUserInteractionEnabled = true -// view.backgroundColor = .lightGray - validateButton() + } + + func configureButton( + for button: UIButton, + backgroundColor: UIColor, + titleColor: UIColor + ) { + button.layer.cornerRadius = button.frame.height / 2 + button.backgroundColor = backgroundColor + button.setTitleColor(titleColor, for: .normal) + button.isEnabled = true + } + + func configureBorderButton( + for button: UIButton, + borderColor: UIColor + ) { + button.layer.cornerRadius = button.frame.height / 2 + button.layer.borderWidth = 1 + button.layer.borderColor = borderColor.cgColor + button.setTitleColor(borderColor, for: .normal) + button.backgroundColor = .white + } + + func configureGestureRecognizer() { + let tapGestureRecognizer = UITapGestureRecognizer( + target: self, + action: #selector(viewDidTap) + ) + view.addGestureRecognizer(tapGestureRecognizer) } @objc - func didClickView() { + func viewDidTap() { view.endEditing(true) } - - //email + @IBAction func emailBeginEditing(_ sender: Any) { - if errorInLogin { - resetErrorLogin(emailTextField) - } else { - emailTextField.setEditingColor() - } + errorInLogin ? resetErrorLogin(emailTextField) : emailTextField.setEditingColor() } - + @IBAction func emailEditing(_ sender: Any) { validateButton() } - + @IBAction func emailEndEditing(_ sender: Any) { emailTextField.setDefaultColor() } - - //senha + @IBAction func passwordBeginEditing(_ sender: Any) { - if errorInLogin { - resetErrorLogin(passwordTextField) - } else { - passwordTextField.setEditingColor() - } + errorInLogin ? resetErrorLogin(passwordTextField) : passwordTextField.setEditingColor() } - + @IBAction func passwordEditing(_ sender: Any) { validateButton() } - + @IBAction func passwordEndEditing(_ sender: Any) { passwordTextField.setDefaultColor() } - + func setErrorLogin(_ message: String) { errorInLogin = true heightLabelError.constant = 20 @@ -181,7 +186,7 @@ extension LuzLoginViewController { emailTextField.setErrorColor() passwordTextField.setErrorColor() } - + func resetErrorLogin(_ textField: UITextField) { heightLabelError.constant = 0 if textField == emailTextField { @@ -194,34 +199,50 @@ extension LuzLoginViewController { } } +// MARK: - Validate extension LuzLoginViewController { - func validateButton() { - if !emailTextField.text!.contains(".") || - !emailTextField.text!.contains("@") || - emailTextField.text!.count <= 5 { - disableButton() - } else { - if let atIndex = emailTextField.text!.firstIndex(of: "@") { - let substring = emailTextField.text![atIndex...] - if substring.contains(".") { - enableButton() - } else { - disableButton() - } - } else { - disableButton() - } - } + viewModel.validateButton( + email: emailTextField.text, + enable: enableButton, + disable: disableButton + ) } - + func disableButton() { loginButton.backgroundColor = .gray loginButton.isEnabled = false } - + func enableButton() { loginButton.backgroundColor = .blue loginButton.isEnabled = true } } + +// MARK: - Handlers +extension LuzLoginViewController { + func handleError() { + setErrorLogin("E-mail ou senha incorretos") + showErrorAlert() + } + + func showErrorAlert() { + Globals.alertMessage( + title: "Ops..", + message: "Houve um problema, tente novamente mais tarde.", + targetVC: self + ) + } +} + +// MARK: - Setup HomeViewController +extension LuzLoginViewController { + func setupHomeViewController() { + let scenes = UIApplication.shared.connectedScenes + let windowScene = scenes.first as? UIWindowScene + let window = windowScene?.windows.first + let homeCoordinator = LuzHomeCoordinator(window: window) + homeCoordinator.start() + } +} diff --git a/CleanCodeApp/Modules/Features/Luz/Login/LuzLoginViewModel.swift b/CleanCodeApp/Modules/Features/Luz/Login/LuzLoginViewModel.swift new file mode 100644 index 0000000..38bc08a --- /dev/null +++ b/CleanCodeApp/Modules/Features/Luz/Login/LuzLoginViewModel.swift @@ -0,0 +1,90 @@ +import UIKit + +final class LuzLoginViewModel { + func setupDebugConfiguration( + for emailTextField: UITextField, + passwordTextField: UITextField + ) { + #if DEBUG + emailTextField.text = "clean.code@devpass.com" + passwordTextField.text = "111111" + #endif + } + + func validateSession(completion: @escaping () -> Void) { + guard UserDefaultsManager.UserInfos.shared.readSesion() != nil else { return } + completion() + } + + func checkConnectionInternet( + completion: @escaping (UIAlertController) -> Void + ) { + if !ConnectivityManager.shared.isConnected { + let alertController = UIAlertController( + title: "Sem conexão", + message: "Conecte-se à internet para tentar novamente", + preferredStyle: .alert + ) + let alertAction = UIAlertAction( + title: "Ok", + style: .default + ) + alertController.addAction(alertAction) + completion(alertController) + } + } + + func login( + email: String?, + password: String? + ) async throws -> Data { + return await withCheckedContinuation { continuation in + AF.shared.request( + Endpoints.Auth.login, + method: .get, + parameters: makeParams(email: email, password: password), + headers: nil + ) { result in + switch result { + case .success(let data): + continuation.resume(returning: data) + case .failure(let error): + continuation.resume(with: .failure(error as! Never)) + } + } + } + } + + func makeParams( + email: String?, + password: String? + ) -> [String: String] { + guard + let email, + let password + else { return [:] } + + return [ + "email": email, + "password": password + ] + } + + func handleLoginSuccess(data: Data) throws { + let session = try JSONDecoder().decode(Session.self, from: data) + UserDefaultsManager.UserInfos.shared.save(session: session, user: nil) + } + + func validateButton( + email: String?, + enable: @escaping () -> Void, + disable: @escaping () -> Void + ) { + do { + try LuzLoginEmailValidator.validate(email) + enable() + } catch { + disable() + } + } +} diff --git a/CleanCodeApp/Modules/Features/Luz/ResetPassword/LuzResetPasswordFactory.swift b/CleanCodeApp/Modules/Features/Luz/ResetPassword/LuzResetPasswordFactory.swift new file mode 100644 index 0000000..b45774f --- /dev/null +++ b/CleanCodeApp/Modules/Features/Luz/ResetPassword/LuzResetPasswordFactory.swift @@ -0,0 +1,11 @@ +import UIKit + +enum LuzResetPasswordFactory { + static func make() -> UIViewController { + let storyboard = UIStoryboard(name: "LuzUser", bundle: nil) + let viewController = storyboard.instantiateViewController( + withIdentifier: "LuzResetPasswordViewController" + ) as! LuzResetPasswordViewController + return viewController + } +}