diff --git a/Localization/en.lproj/Localizable.strings b/Localization/en.lproj/Localizable.strings index 8d221241..801c3b78 100644 --- a/Localization/en.lproj/Localizable.strings +++ b/Localization/en.lproj/Localizable.strings @@ -28,9 +28,7 @@ "username" = "Username"; "display_name" = "Display Name"; -"email_login" = "Enter email"; "enter_your_pin_received_by_mail" = "Enter pin sent to email"; -"check_spam" = "Didn’t receive the email? Check spam"; "pin" = "Pin"; "invalid_email" = "Please enter a valid email"; @@ -287,3 +285,29 @@ "FollowRecommendations.YouMayKnow" = "You may know"; "Notifications.FollowRecommendations" = "We found some people you may know, add them and get chatting today."; + +"Authentication.Skip" = "Skip"; + +"Authentication.Email" = "Email"; +"Authentication.Pin" = "Login Code"; +"Authentication.Pin.Description" = "Didn't receive the email? Check spam"; + +"Authentication.Name" = "Name"; +"Authentication.Name.Description" = "What's your name?"; + +"Authentication.Username" = "Username"; +"Authentication.Username.Description" = "Choose a username for people to find you on Soapbox."; + +"Authentication.Error.InvalidName" = "Please enter a name."; + +"Authentication.ProfilePhoto" = "Profile Photo"; +"Authentication.ProfilePhoto.Description" = "Set your profile photo so your friends will recognize you."; + +"Authentication.Permissions" = "Permissions"; +"Authentication.Permissions.Description" = "Soapbox is an audio-first app, we'll need few permissions from you."; + +"Authentication.Permissions.Microphone" = "Microphone"; +"Authentication.Permissions.Microphone.Description" = "So your friends can hear your beautiful voice."; + +"Authentication.Permissions.Notifications" = "Notifications"; +"Authentication.Permissions.Notifications.Description" = "So you'll know when your friends are online and chatting."; diff --git a/Podfile b/Podfile index cf42e43e..35ab6b6d 100644 --- a/Podfile +++ b/Podfile @@ -11,7 +11,6 @@ target 'Soapbox' do pod 'DrawerView', git: 'git@github.com:SoapboxSocial/DrawerView.git', commit: 'b0f4ad7ac60e0a5cfe089e55995115ccb7893d01' pod 'NotificationBannerSwift' pod 'KeychainAccess' - pod 'SwiftConfettiView' pod 'UIWindowTransitions' pod 'AlamofireImage' pod 'GSImageViewerController' diff --git a/Podfile.lock b/Podfile.lock index f2ddf41c..0d75daf6 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -15,7 +15,6 @@ PODS: - SnapKit (~> 5.0.1) - Siren (5.8.1) - SnapKit (5.0.1) - - SwiftConfettiView (0.1.0) - Swifter (2.5.0) - SwiftProtobuf (1.17.0) - UIWindowTransitions (1.0.0) @@ -32,7 +31,6 @@ DEPENDENCIES: - KeychainAccess - NotificationBannerSwift - Siren - - SwiftConfettiView - "Swifter (from `git@github.com:mattdonnelly/Swifter.git`)" - SwiftProtobuf - UIWindowTransitions @@ -51,7 +49,6 @@ SPEC REPOS: - NotificationBannerSwift - Siren - SnapKit - - SwiftConfettiView - SwiftProtobuf - UIWindowTransitions @@ -84,11 +81,10 @@ SPEC CHECKSUMS: NotificationBannerSwift: 7021be2338f8f29cf424b0aca43da462bf9e2a1a Siren: de768099aff1f1c9acda4247064788a4940b4bf3 SnapKit: 97b92857e3df3a0c71833cce143274bf6ef8e5eb - SwiftConfettiView: 1bdbc2e6913b6b1c76d8070802b212508ad5c352 Swifter: 4c2f8cf320c2f751c10128f841d8e9bd8609d988 SwiftProtobuf: 9c85136c6ba74b0a1b84279dbf0f6db8efb714e0 UIWindowTransitions: 2237083382e11f100db7de934041bba0c4669c86 -PODFILE CHECKSUM: a3326625314027ca9400b4e1af13d652f700ef54 +PODFILE CHECKSUM: c10badbc8db03cfc239205760cb09131995a79fe COCOAPODS: 1.10.1 diff --git a/Resources/Settings.bundle/Acknowledgements.plist b/Resources/Settings.bundle/Acknowledgements.plist index 20f87c2a..ca34043b 100644 --- a/Resources/Settings.bundle/Acknowledgements.plist +++ b/Resources/Settings.bundle/Acknowledgements.plist @@ -404,37 +404,6 @@ THE SOFTWARE. Type PSGroupSpecifier - - FooterText - MIT License - -Copyright (c) 2019 Uğur Ethem AYDIN - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - - License - MIT - Title - SwiftConfettiView - Type - PSGroupSpecifier - FooterText Apache License diff --git a/Sources/API/APIClient.swift b/Sources/API/APIClient.swift index ec56f273..7b0e5c75 100644 --- a/Sources/API/APIClient.swift +++ b/Sources/API/APIClient.swift @@ -97,20 +97,12 @@ class APIClient: Client { } } - func register(token: String, username: String, displayName: String, image: UIImage, callback: @escaping (Result<(User, Int), Error>) -> Void) { - AF.upload( - multipartFormData: { multipartFormData in - guard let imgData = image.jpegData(compressionQuality: 0.5) else { - return callback(.failure(.preprocessing)) - } - - multipartFormData.append(imgData, withName: "profile", fileName: "profile", mimeType: "image/jpg") - - multipartFormData.append(displayName.data(using: String.Encoding.utf8)!, withName: "display_name") - multipartFormData.append(username.data(using: String.Encoding.utf8)!, withName: "username") - multipartFormData.append(token.data(using: String.Encoding.utf8)!, withName: "token") - }, - to: Configuration.rootURL.appendingPathComponent("/v1/login/register") + func register(token: String, username: String, displayName: String, callback: @escaping (Result<(User, Int), Error>) -> Void) { + AF.request( + Configuration.rootURL.appendingPathComponent("/v1/login/register"), + method: .post, + parameters: ["display_name": displayName, "username": username, "token": token], + encoding: URLEncoding.default ) .validate() .response { result in @@ -129,6 +121,28 @@ class APIClient: Client { } } + func edit(image: UIImage, callback: @escaping (Result) -> Void) { + AF.upload( + multipartFormData: { multipartFormData in + guard let imgData = image.jpegData(compressionQuality: 0.5) else { + return callback(.failure(.preprocessing)) + } + + multipartFormData.append(imgData, withName: "profile", fileName: "profile", mimeType: "image/jpg") + }, + to: Configuration.rootURL.appendingPathComponent("/v1/users/upload"), + headers: ["Authorization": token!] + ) + .validate() + .response { result in + if let error = self.validate(result) { + return callback(.failure(error)) + } + + callback(.success(())) + } + } + func completeRegistration(callback: @escaping (Result) -> Void) { post(path: "/v1/login/register/completed", callback: callback) } diff --git a/Sources/NotificationManager.swift b/Sources/NotificationManager.swift index 914b0148..b1baecc1 100644 --- a/Sources/NotificationManager.swift +++ b/Sources/NotificationManager.swift @@ -26,8 +26,10 @@ class NotificationManager { delegate?.deviceTokenFailedToSet() } - func requestAuthorization() { + func requestAuthorization(callback: ((Bool) -> Void)? = nil) { UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { granted, _ in + callback?(granted) + guard granted else { DispatchQueue.main.async { self.delegate?.deviceTokenFailedToSet() diff --git a/Sources/Scenes/Authentication/AuthenticationInteractor.swift b/Sources/Scenes/Authentication/AuthenticationInteractor.swift index b2ab2f96..fe6736eb 100644 --- a/Sources/Scenes/Authentication/AuthenticationInteractor.swift +++ b/Sources/Scenes/Authentication/AuthenticationInteractor.swift @@ -6,7 +6,6 @@ import UIWindowTransitions protocol AuthenticationInteractorOutput { func present(error: AuthenticationInteractor.AuthenticationError) func present(state: AuthenticationInteractor.AuthenticationState) - func presentLoggedInView() } class AuthenticationInteractor: NSObject, AuthenticationViewControllerOutput { @@ -14,13 +13,14 @@ class AuthenticationInteractor: NSObject, AuthenticationViewControllerOutput { private let api: APIClient private var token: String? + private var displayName: String! enum AuthenticationState: Int { - case getStarted, login, pin, registration, requestNotifications, follow, success + case start, login, pin, name, username, profilePhoto, permissions, invite, completed } enum AuthenticationError { - case invalidEmail, invalidPin, invalidUsername, usernameTaken, missingProfileImage, general, registerWithEmailDisabled + case invalidEmail, invalidPin, invalidUsername, invalidDisplayName, usernameTaken, general, registerWithEmailDisabled } init(output: AuthenticationInteractorOutput, api: APIClient) { @@ -64,7 +64,7 @@ class AuthenticationInteractor: NSObject, AuthenticationViewControllerOutput { authController.performRequests() } - func submitPin(pin: String?) { + func submit(pin: String?) { guard let input = pin else { return output.present(error: .invalidPin) } @@ -91,28 +91,38 @@ class AuthenticationInteractor: NSObject, AuthenticationViewControllerOutput { self.store(token: self.token!, expires: expires, user: user) - NotificationManager.shared.requestAuthorization() - DispatchQueue.main.async { - self.output.presentLoggedInView() + self.output.present(state: .invite) } case .register: - self.output.present(state: .registration) + self.output.present(state: .name) } } } } - func register(username: String?, displayName: String?, image: UIImage?) { - guard let usernameInput = username, isValidUsername(usernameInput) else { - return output.present(error: .invalidUsername) + func submit(displayName: String?) { + guard let input = displayName else { + output.present(error: .invalidDisplayName) + return } - guard let profileImage = image else { - return output.present(error: .missingProfileImage) + let name = input.trimmingCharacters(in: .whitespacesAndNewlines) + if input == "" { + output.present(error: .invalidDisplayName) + return } - api.register(token: token!, username: usernameInput, displayName: displayName ?? usernameInput, image: profileImage) { result in + self.displayName = name + output.present(state: .username) + } + + func register(withUsername username: String?) { + guard let usernameInput = username, isValidUsername(usernameInput) else { + return output.present(error: .invalidUsername) + } + + api.register(token: token!, username: usernameInput, displayName: displayName) { result in switch result { case let .failure(error): switch error { @@ -128,15 +138,23 @@ class AuthenticationInteractor: NSObject, AuthenticationViewControllerOutput { case let .success((user, expires)): self.store(token: self.token!, expires: expires, user: user) DispatchQueue.main.async { - self.output.present(state: .requestNotifications) - - NotificationManager.shared.delegate = self - NotificationManager.shared.requestAuthorization() + self.output.present(state: .profilePhoto) } } } } + func submit(image: UIImage) { + api.edit(image: image, callback: { result in + switch result { + case let .failure(error): + return + case let .success: + return + } + }) + } + func follow(users: [Int]) { if users.count == 0 { return registrationCompleted() @@ -158,7 +176,7 @@ class AuthenticationInteractor: NSObject, AuthenticationViewControllerOutput { } }) - output.present(state: .success) +// output.present(state: .success) } private func isValidUsername(_ username: String) -> Bool { @@ -193,16 +211,6 @@ class AuthenticationInteractor: NSObject, AuthenticationViewControllerOutput { } } -extension AuthenticationInteractor: NotificationManagerDelegate { - func deviceTokenFailedToSet() { - output.present(state: .follow) - } - - func deviceTokenWasSet() { - output.present(state: .follow) - } -} - extension AuthenticationInteractor: ASAuthorizationControllerDelegate { func authorizationController(controller _: ASAuthorizationController, didCompleteWithError error: Error) { output.present(error: .general) @@ -238,9 +246,7 @@ extension AuthenticationInteractor: ASAuthorizationControllerDelegate { NotificationManager.shared.requestAuthorization() - DispatchQueue.main.async { - self.output.presentLoggedInView() - } + self.output.present(state: .completed) case .register: guard let token = response.3 else { return self.output.present(error: .general) @@ -248,7 +254,7 @@ extension AuthenticationInteractor: ASAuthorizationControllerDelegate { self.token = token - self.output.present(state: .registration) + self.output.present(state: .name) } } }) diff --git a/Sources/Scenes/Authentication/AuthenticationPresenter.swift b/Sources/Scenes/Authentication/AuthenticationPresenter.swift index 87e62cfe..93df2d44 100644 --- a/Sources/Scenes/Authentication/AuthenticationPresenter.swift +++ b/Sources/Scenes/Authentication/AuthenticationPresenter.swift @@ -28,8 +28,12 @@ class AuthenticationPresenter: AuthenticationInteractorOutput { output.displayError(.normal, title: NSLocalizedString("incorrect_pin", comment: ""), description: nil) case .invalidUsername: output.displayError(.normal, title: NSLocalizedString("invalid_username", comment: ""), description: nil) - case .missingProfileImage: - output.displayError(.normal, title: NSLocalizedString("pick_profile_image", comment: ""), description: nil) + case .invalidDisplayName: + output.displayError( + .normal, + title: NSLocalizedString("Authentication.Error.InvalidName", comment: ""), + description: nil + ) case .usernameTaken: output.displayError(.normal, title: NSLocalizedString("username_already_exists", comment: ""), description: nil) case .registerWithEmailDisabled: @@ -39,19 +43,5 @@ class AuthenticationPresenter: AuthenticationInteractorOutput { func present(state: AuthenticationInteractor.AuthenticationState) { output.transitionTo(state: state) - - if state == .success { - DispatchQueue.main.asyncAfter(deadline: .now() + 2.1) { - self.presentLoggedInView() - } - } - } - - func presentLoggedInView() { - let delegate = UIApplication.shared.delegate as! AppDelegate - delegate.window!.set( - rootViewController: delegate.createLoggedIn(), - options: UIWindow.TransitionOptions(direction: .fade, style: .easeOut) - ) } } diff --git a/Sources/Scenes/Authentication/AuthenticationViewController.swift b/Sources/Scenes/Authentication/AuthenticationViewController.swift index 6904c8e9..8bceb1ca 100644 --- a/Sources/Scenes/Authentication/AuthenticationViewController.swift +++ b/Sources/Scenes/Authentication/AuthenticationViewController.swift @@ -1,27 +1,109 @@ import UIKit -protocol AuthenticationViewControllerWithInput { - func enableSubmit() -} - protocol AuthenticationViewControllerOutput { func login(email: String?) func loginWithApple() - func submitPin(pin: String?) - func register(username: String?, displayName: String?, image: UIImage?) + func submit(pin: String?) + func submit(displayName: String?) + func submit(image: UIImage) + func register(withUsername username: String?) func follow(users: [Int]) } +protocol AuthenticationStepViewController where Self: UIViewController { + var hasBackButton: Bool { get } + + var hasSkipButton: Bool { get } + + var stepDescription: String? { get } + + func enableSubmit() +} + class AuthenticationViewController: UIPageViewController { var output: AuthenticationViewControllerOutput! - private var orderedViewControllers = [UIViewController]() - - private var state = AuthenticationInteractor.AuthenticationState.getStarted + private var orderedViewControllers = [AuthenticationStepViewController]() + + private var state = AuthenticationInteractor.AuthenticationState.start + + private let titleLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.font = .rounded(forTextStyle: .title3, weight: .bold) + label.textColor = .white + label.textAlignment = .center + return label + }() + + private let descriptionLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.font = .rounded(forTextStyle: .body, weight: .semibold) + label.textColor = .white + label.textAlignment = .center + label.numberOfLines = 0 + return label + }() + + private let backButton: UIButton = { + let button = UIButton() + button.setImage(UIImage(systemName: "chevron.backward"), for: .normal) + button.backgroundColor = .lightBrandColor + button.imageView?.tintColor = .white + button.translatesAutoresizingMaskIntoConstraints = false + button.isHidden = true + button.addTarget(self, action: #selector(didTapBackButton), for: .touchUpInside) + return button + }() + + private let skipButton: UIButton = { + let button = UIButton() + button.translatesAutoresizingMaskIntoConstraints = false + button.isHidden = true + button.setTitle(NSLocalizedString("Authentication.Skip", comment: ""), for: .normal) + button.titleLabel?.font = .rounded(forTextStyle: .body, weight: .semibold) + button.addTarget(self, action: #selector(didTapSkipButton), for: .touchUpInside) + return button + }() + + override var preferredStatusBarStyle: UIStatusBarStyle { + return .lightContent + } init() { super.init(transitionStyle: .scroll, navigationOrientation: .horizontal) + view.addSubview(titleLabel) + view.addSubview(descriptionLabel) + view.addSubview(backButton) + view.addSubview(skipButton) + + NSLayoutConstraint.activate([ + backButton.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 10), + backButton.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 20), + backButton.heightAnchor.constraint(equalToConstant: 40), + backButton.widthAnchor.constraint(equalToConstant: 40), + ]) + + backButton.layer.cornerRadius = 20 + + NSLayoutConstraint.activate([ + skipButton.centerYAnchor.constraint(equalTo: backButton.centerYAnchor), + skipButton.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor, constant: -20), + ]) + + NSLayoutConstraint.activate([ + titleLabel.centerYAnchor.constraint(equalTo: backButton.centerYAnchor), + titleLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor), + ]) + + NSLayoutConstraint.activate([ + descriptionLabel.topAnchor.constraint(equalTo: backButton.bottomAnchor, constant: 20), + descriptionLabel.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 20), + descriptionLabel.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -20), + ]) + let start = AuthenticationStartViewController() start.delegate = self orderedViewControllers.append(start) @@ -34,21 +116,44 @@ class AuthenticationViewController: UIPageViewController { pin.delegate = self orderedViewControllers.append(pin) - let registration = AuthenticationRegistrationViewController() - registration.delegate = self - orderedViewControllers.append(registration) + let name = AuthenticationNameViewController() + name.delegate = self + orderedViewControllers.append(name) + + let username = AuthenticationUsernameViewController() + username.delegate = self + orderedViewControllers.append(username) - orderedViewControllers.append(AuthenticationRequestNotificationsViewController()) + let photo = AuthenticationProfilePhotoViewController() + photo.delegate = self + orderedViewControllers.append(photo) - let follow = AuthenticationFollowViewController() - follow.delegate = self - orderedViewControllers.append(follow) + let permissions = AuthenticationPermissionsViewController() + permissions.delegate = self + orderedViewControllers.append(permissions) - orderedViewControllers.append(AuthenticationSuccessViewController()) + let invite = AuthenticationInviteFriendsViewController() + orderedViewControllers.append(invite) setViewControllers([orderedViewControllers[0]], direction: .forward, animated: false) } + @objc private func didTapBackButton() { + guard let state = AuthenticationInteractor.AuthenticationState(rawValue: state.rawValue - 1) else { + return + } + + transitionTo(state: state) + } + + @objc private func didTapSkipButton() { + guard let state = AuthenticationInteractor.AuthenticationState(rawValue: state.rawValue + 1) else { + return + } + + transitionTo(state: state) + } + required init?(coder _: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -64,31 +169,27 @@ class AuthenticationViewController: UIPageViewController { return false } - didSubmit(pin: pin) + didSubmit(withText: pin) return true } } extension AuthenticationViewController: AuthenticationPresenterOutput { func displayEmailRegistrationDisabledError() { - if let controller = orderedViewControllers[state.rawValue] as? AuthenticationViewControllerWithInput { - controller.enableSubmit() - } + orderedViewControllers[state.rawValue].enableSubmit() let banner = NotificationBanner(title: NSLocalizedString("register_with_email_disabled_use_apple", comment: ""), style: .danger, type: .normal) banner.onTap = { DispatchQueue.main.async { - self.transitionTo(state: .getStarted) + self.transitionTo(state: .start) } } banner.show() } - func displayError(_ style: NotificationBanner.BannerType, title: String, description: String?) { - if let controller = orderedViewControllers[state.rawValue] as? AuthenticationViewControllerWithInput { - controller.enableSubmit() - } + func displayError(_ style: NotificationBanner.BannerType, title: String, description: String? = nil) { + orderedViewControllers[state.rawValue].enableSubmit() let banner = NotificationBanner(title: title, subtitle: description, style: .danger, type: style) banner.show() @@ -102,7 +203,34 @@ extension AuthenticationViewController: AuthenticationPresenterOutput { self.state = state - setViewControllers([orderedViewControllers[state.rawValue]], direction: direction, animated: true) + if state == .completed { + return transitionToHome() + } + + let view = orderedViewControllers[state.rawValue] + + UIView.animate(withDuration: 0.3, animations: { + self.backButton.isHidden = !view.hasBackButton + self.skipButton.isHidden = !view.hasSkipButton + + self.titleLabel.text = view.title + self.descriptionLabel.text = view.stepDescription + }) + + setViewControllers([view], direction: direction, animated: true) + } + + private func transitionToHome() { + guard let delegate = UIApplication.shared.delegate as? AppDelegate else { + return + } + + // @TODO CALL REGISTRATION COMPLETED + + delegate.window!.set( + rootViewController: delegate.createLoggedIn(), + options: UIWindow.TransitionOptions(direction: .fade, style: .easeOut) + ) } } @@ -116,26 +244,31 @@ extension AuthenticationViewController: AuthenticationStartViewControllerDelegat } } -extension AuthenticationViewController: AuthenticationEmailViewControllerDelegate { - func didSubmit(email: String?) { - output.login(email: email) - } -} - -extension AuthenticationViewController: AuthenticationPinViewControllerDelegate { - func didSubmit(pin: String?) { - output.submitPin(pin: pin) +extension AuthenticationViewController: AuthenticationTextInputViewControllerDelegate { + func didSubmit(withText text: String?) { + switch state { + case .login: + output.login(email: text) + case .pin: + output.submit(pin: text) + case .name: + output.submit(displayName: text) + case .username: + output.register(withUsername: text) + default: + return + } } } -extension AuthenticationViewController: AuthenticationRegistrationViewControllerDelegate { - func didSubmit(username: String?, displayName: String?, image: UIImage?) { - output.register(username: username, displayName: displayName, image: image) +extension AuthenticationViewController: AuthenticationProfilePhotoViewControllerDelegate { + func didUpload(image: UIImage) { + output.submit(image: image) } } -extension AuthenticationViewController: AuthenticationFollowViewControllerDelegate { - func didSubmit(users: [Int]) { - output.follow(users: users) +extension AuthenticationViewController: AuthenticationPermissionsViewControllerDelegate { + func didFinishPermissions() { + transitionTo(state: .invite) } } diff --git a/Sources/Scenes/Authentication/ViewControllers/AuthenticationEmailViewController.swift b/Sources/Scenes/Authentication/ViewControllers/AuthenticationEmailViewController.swift index 431280d8..4d0a08c3 100644 --- a/Sources/Scenes/Authentication/ViewControllers/AuthenticationEmailViewController.swift +++ b/Sources/Scenes/Authentication/ViewControllers/AuthenticationEmailViewController.swift @@ -1,69 +1,18 @@ import UIKit -protocol AuthenticationEmailViewControllerDelegate { - func didSubmit(email: String?) -} +class AuthenticationEmailViewController: AuthenticationTextInputViewController { + override var hasBackButton: Bool { + return true + } + + override init() { + super.init() -class AuthenticationEmailViewController: ViewControllerWithKeyboardConstraint { - var delegate: AuthenticationEmailViewControllerDelegate? + title = NSLocalizedString("Authentication.Email", comment: "") - private let textField: TextField = { - let textField = TextField(frame: .zero, theme: .light) - textField.placeholder = "Email" - textField.translatesAutoresizingMaskIntoConstraints = false textField.keyboardType = .emailAddress textField.textContentType = .emailAddress - textField.autocorrectionType = .no - textField.autocapitalizationType = .none - return textField - }() - - private let submitButton: Button = { - let button = Button(size: .large) - button.translatesAutoresizingMaskIntoConstraints = false - button.setTitle(NSLocalizedString("next", comment: ""), for: .normal) - button.addTarget(self, action: #selector(didSubmit), for: .touchUpInside) - button.backgroundColor = UIColor.white.withAlphaComponent(0.3) - return button - }() - - private let label: UILabel = { - let label = UILabel() - label.translatesAutoresizingMaskIntoConstraints = false - label.textColor = .white - label.text = NSLocalizedString("email_login", comment: "") - label.font = .rounded(forTextStyle: .title1, weight: .bold) - return label - }() - - init() { - super.init(nibName: nil, bundle: nil) - - view.addSubview(label) - - view.addSubview(textField) - view.addSubview(submitButton) - - bottomLayoutConstraint = submitButton.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -(view.frame.size.height / 3)) - bottomLayoutConstraint.isActive = true - - NSLayoutConstraint.activate([ - label.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 20), - label.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -20), - label.bottomAnchor.constraint(equalTo: textField.topAnchor, constant: -20), - ]) - - NSLayoutConstraint.activate([ - textField.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 20), - textField.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -20), - textField.heightAnchor.constraint(equalToConstant: 56), - textField.bottomAnchor.constraint(equalTo: submitButton.topAnchor, constant: -10), - ]) - - NSLayoutConstraint.activate([ - submitButton.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 20), - submitButton.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -20), - ]) + textField.placeholder = NSLocalizedString("Authentication.Email", comment: "") } required init?(coder _: NSCoder) { @@ -74,15 +23,4 @@ class AuthenticationEmailViewController: ViewControllerWithKeyboardConstraint { super.viewDidDisappear(animated) view.endEditing(true) } - - @objc private func didSubmit() { - submitButton.isEnabled = false - delegate?.didSubmit(email: textField.text) - } -} - -extension AuthenticationEmailViewController: AuthenticationViewControllerWithInput { - func enableSubmit() { - submitButton.isEnabled = true - } } diff --git a/Sources/Scenes/Authentication/ViewControllers/AuthenticationFollowViewController.swift b/Sources/Scenes/Authentication/ViewControllers/AuthenticationFollowViewController.swift deleted file mode 100644 index 1c81851f..00000000 --- a/Sources/Scenes/Authentication/ViewControllers/AuthenticationFollowViewController.swift +++ /dev/null @@ -1,170 +0,0 @@ -import UIKit - -protocol AuthenticationFollowViewControllerDelegate { - func didSubmit(users: [Int]) -} - -class AuthenticationFollowViewController: UIViewController { - var delegate: AuthenticationFollowViewControllerDelegate? - - private let label: UILabel = { - let label = UILabel() - label.translatesAutoresizingMaskIntoConstraints = false - label.font = .rounded(forTextStyle: .title1, weight: .bold) - label.text = NSLocalizedString("follow_users_to_start_talking", comment: "") - label.textColor = .white - label.numberOfLines = 0 - return label - }() - - private let followButton: Button = { - let button = Button(size: .large) - button.translatesAutoresizingMaskIntoConstraints = false - button.setTitle(NSLocalizedString("follow", comment: ""), for: .normal) - button.addTarget(self, action: #selector(didSubmit), for: .touchUpInside) - button.backgroundColor = UIColor.white.withAlphaComponent(0.3) - return button - }() - - private var users = [APIClient.User]() - - private var list: UICollectionView! - - private var selected = [Int]() - - override func viewDidLoad() { - super.viewDidLoad() - - view.addSubview(label) - - NSLayoutConstraint.activate([ - label.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 20), - label.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -20), - label.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20), - ]) - - view.addSubview(followButton) - - NSLayoutConstraint.activate([ - followButton.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 20), - followButton.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -20), - followButton.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -10), - ]) - - let layout = UICollectionViewFlowLayout.basicUserBubbleLayout(itemsPerRow: 4, width: view.frame.size.width) - layout.sectionInset.bottom = view.safeAreaInsets.bottom - - list = UICollectionView(frame: .zero, collectionViewLayout: layout) - list.dataSource = self - list.delegate = self - list.allowsMultipleSelection = true - list.translatesAutoresizingMaskIntoConstraints = false - list.register(cellWithClass: SelectableImageTextCell.self) - list.backgroundColor = .clear - view.addSubview(list) - - NSLayoutConstraint.activate([ - list.topAnchor.constraint(equalTo: label.bottomAnchor, constant: 20), - list.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 20), - list.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -20), - list.bottomAnchor.constraint(equalTo: followButton.topAnchor), - ]) - - APIClient().search("*", types: [.users], limit: 48, offset: 0, callback: { [self] result in - switch result { - case .failure: - break - case let .success(response): - if let users = response.users { - self.users = users - - for i in 0 ..< min(4, users.count) { - self.selected.append(users[i].id) - } - - self.list.reloadData() - } - } - }) - } - - @objc private func didSubmit() { - delegate?.didSubmit(users: selected) - } -} - -extension AuthenticationFollowViewController: UICollectionViewDelegate { - func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool { - let item = collectionView.cellForItem(at: indexPath) - if item?.isSelected ?? false { - return false - } - - return true - } - - func collectionView(_ collectionView: UICollectionView, shouldDeselectItemAt indexPath: IndexPath) -> Bool { - let item = collectionView.cellForItem(at: indexPath) - if item?.isSelected ?? false { - return true - } - - return false - } - - func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { - guard let cell = collectionView.cellForItem(at: indexPath) as? SelectableImageTextCell else { - return - } - - cell.selectedView.isHidden = false - - let user = users[indexPath.item] - selected.append(user.id) - - followButton.setTitle(NSLocalizedString("follow", comment: ""), for: .normal) - } - - func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) { - guard let cell = collectionView.cellForItem(at: indexPath) as? SelectableImageTextCell else { - return - } - - cell.selectedView.isHidden = true - - let user = users[indexPath.item] - selected.removeAll(where: { $0 == user.id }) - - if selected.count == 0 { - followButton.setTitle(NSLocalizedString("skip", comment: ""), for: .normal) - } - } -} - -extension AuthenticationFollowViewController: UICollectionViewDataSource { - func collectionView(_: UICollectionView, numberOfItemsInSection _: Int) -> Int { - return users.count - } - - func collectionView(_: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { - let cell = list.dequeueReusableCell(withClass: SelectableImageTextCell.self, for: indexPath) - - let user = users[indexPath.item] - - cell.image.backgroundColor = .lightBrandColor - if let image = user.image, image != "" { - cell.image.af.setImage(withURL: Configuration.cdn.appendingPathComponent("/images/" + image)) - } - - cell.title.text = user.displayName.firstName() - cell.title.textColor = .white - - if selected.contains(user.id) { - cell.selectedView.isHidden = false - } else { - cell.selectedView.isHidden = true - } - - return cell - } -} diff --git a/Sources/Scenes/Authentication/ViewControllers/AuthenticationInviteFriendsViewController.swift b/Sources/Scenes/Authentication/ViewControllers/AuthenticationInviteFriendsViewController.swift new file mode 100644 index 00000000..a955229d --- /dev/null +++ b/Sources/Scenes/Authentication/ViewControllers/AuthenticationInviteFriendsViewController.swift @@ -0,0 +1,17 @@ +import UIKit + +class AuthenticationInviteFriendsViewController: UIViewController, AuthenticationStepViewController { + var stepDescription: String? { + return nil + } + + var hasSkipButton: Bool { + return true + } + + var hasBackButton: Bool { + return false + } + + func enableSubmit() {} +} diff --git a/Sources/Scenes/Authentication/ViewControllers/AuthenticationNameViewController.swift b/Sources/Scenes/Authentication/ViewControllers/AuthenticationNameViewController.swift new file mode 100644 index 00000000..7f3167f0 --- /dev/null +++ b/Sources/Scenes/Authentication/ViewControllers/AuthenticationNameViewController.swift @@ -0,0 +1,18 @@ +import UIKit + +class AuthenticationNameViewController: AuthenticationTextInputViewController { + override var stepDescription: String? { + return NSLocalizedString("Authentication.Name.Description", comment: "") + } + + override init() { + super.init() + + title = NSLocalizedString("Authentication.Name", comment: "") + textField.placeholder = NSLocalizedString("Authentication.Name", comment: "") + } + + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/Sources/Scenes/Authentication/ViewControllers/AuthenticationPermissionsViewController.swift b/Sources/Scenes/Authentication/ViewControllers/AuthenticationPermissionsViewController.swift new file mode 100644 index 00000000..dbcaef0c --- /dev/null +++ b/Sources/Scenes/Authentication/ViewControllers/AuthenticationPermissionsViewController.swift @@ -0,0 +1,122 @@ +import AVFoundation +import UIKit + +protocol AuthenticationPermissionsViewControllerDelegate: AnyObject { + func didFinishPermissions() +} + +class AuthenticationPermissionsViewController: UIViewController, AuthenticationStepViewController { + var hasBackButton: Bool { + return false + } + + var hasSkipButton: Bool { + return false + } + + var stepDescription: String? { + return NSLocalizedString("Authentication.Permissions.Description", comment: "") + } + + var delegate: AuthenticationPermissionsViewControllerDelegate? + + private let microphoneButton: PermissionButton = { + let button = PermissionButton() + button.title.text = NSLocalizedString("Authentication.Permissions.Microphone", comment: "") + button.emoji.text = "🎙" + button.descriptionLabel.text = NSLocalizedString("Authentication.Permissions.Microphone.Description", comment: "") + return button + }() + + private let notificationsButton: PermissionButton = { + let button = PermissionButton() + button.title.text = NSLocalizedString("Authentication.Permissions.Notifications", comment: "") + button.emoji.text = "🔔" + button.descriptionLabel.text = NSLocalizedString("Authentication.Permissions.Notifications.Description", comment: "") + return button + }() + + let submitButton: Button = { + let button = Button(size: .large) + button.translatesAutoresizingMaskIntoConstraints = false + button.setTitle(NSLocalizedString("next", comment: ""), for: .normal) + button.backgroundColor = UIColor.white.withAlphaComponent(0.3) + button.isEnabled = false + button.addTarget(self, action: #selector(didSubmit), for: .touchUpInside) + return button + }() + + init() { + super.init(nibName: nil, bundle: nil) + + title = NSLocalizedString("Authentication.Permissions", comment: "") + + let stack = UIStackView() + stack.translatesAutoresizingMaskIntoConstraints = false + stack.spacing = 30 + stack.distribution = .fill + stack.alignment = .fill + stack.axis = .vertical + view.addSubview(stack) + + microphoneButton.addTarget(self, action: #selector(micPermissions), for: .touchUpInside) + stack.addArrangedSubview(microphoneButton) + + notificationsButton.addTarget(self, action: #selector(notificationPermissions), for: .touchUpInside) + stack.addArrangedSubview(notificationsButton) + + NSLayoutConstraint.activate([ + stack.centerYAnchor.constraint(equalTo: view.centerYAnchor), + stack.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 20), + stack.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -20), + ]) + + view.addSubview(submitButton) + + NSLayoutConstraint.activate([ + submitButton.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 20), + submitButton.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -20), + submitButton.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -20), + ]) + } + + func enableSubmit() { + // do nothing + } + + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func micPermissions() { + AVAudioSession.sharedInstance().requestRecordPermission { _ in + DispatchQueue.main.async { + self.microphoneButton.isSelected = true + self.enableNext() + } + } + } + + @objc private func notificationPermissions() { + NotificationManager.shared.requestAuthorization(callback: { _ in + DispatchQueue.main.async { + self.notificationsButton.isSelected = true + self.enableNext() + } + }) + } + + @objc private func didSubmit() { + delegate?.didFinishPermissions() + } + + private func enableNext() { + if !notificationsButton.isSelected || !microphoneButton.isSelected { + return + } + + UIView.animate(withDuration: 0.3, animations: { + self.submitButton.isEnabled = true + }) + } +} diff --git a/Sources/Scenes/Authentication/ViewControllers/AuthenticationPinViewController.swift b/Sources/Scenes/Authentication/ViewControllers/AuthenticationPinViewController.swift index 1ad40812..fc9019a5 100644 --- a/Sources/Scenes/Authentication/ViewControllers/AuthenticationPinViewController.swift +++ b/Sources/Scenes/Authentication/ViewControllers/AuthenticationPinViewController.swift @@ -1,83 +1,22 @@ import UIKit -protocol AuthenticationPinViewControllerDelegate { - func didSubmit(pin: String?) -} - -class AuthenticationPinViewController: ViewControllerWithKeyboardConstraint { - var delegate: AuthenticationPinViewControllerDelegate? - - private let textField: TextField = { - let textField = TextField(frame: .zero, theme: .light) - textField.placeholder = NSLocalizedString("pin", comment: "") - textField.translatesAutoresizingMaskIntoConstraints = false - textField.keyboardType = .numberPad - textField.textContentType = .oneTimeCode - return textField - }() - - private let submitButton: Button = { - let button = Button(size: .large) - button.translatesAutoresizingMaskIntoConstraints = false - button.setTitle(NSLocalizedString("next", comment: ""), for: .normal) - button.addTarget(self, action: #selector(didSubmit), for: .touchUpInside) - button.backgroundColor = UIColor.white.withAlphaComponent(0.3) - return button - }() - - private let label: UILabel = { - let label = UILabel() - label.translatesAutoresizingMaskIntoConstraints = false - label.textColor = .white - label.text = NSLocalizedString("enter_your_pin_received_by_mail", comment: "") - label.font = .rounded(forTextStyle: .title1, weight: .bold) - return label - }() - - private let note: UILabel = { - let label = UILabel() - label.translatesAutoresizingMaskIntoConstraints = false - label.textColor = .white - label.text = NSLocalizedString("check_spam", comment: "") - label.font = .rounded(forTextStyle: .subheadline, weight: .semibold) - label.textAlignment = .center - return label - }() - - init() { - super.init(nibName: nil, bundle: nil) - - view.addSubview(label) - view.addSubview(textField) - view.addSubview(submitButton) - view.addSubview(note) - - bottomLayoutConstraint = submitButton.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -(view.frame.size.height / 3)) - bottomLayoutConstraint.isActive = true +class AuthenticationPinViewController: AuthenticationTextInputViewController { + override var stepDescription: String? { + return NSLocalizedString("Authentication.Pin.Description", comment: "") + } - NSLayoutConstraint.activate([ - label.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 20), - label.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -20), - label.bottomAnchor.constraint(equalTo: textField.topAnchor, constant: -20), - ]) + override var hasBackButton: Bool { + return true + } - NSLayoutConstraint.activate([ - textField.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 20), - textField.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -20), - textField.heightAnchor.constraint(equalToConstant: 56), - textField.bottomAnchor.constraint(equalTo: note.topAnchor, constant: -20), - ]) + override init() { + super.init() - NSLayoutConstraint.activate([ - note.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 20), - note.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -20), - note.bottomAnchor.constraint(equalTo: submitButton.topAnchor, constant: -20), - ]) + title = NSLocalizedString("Authentication.Pin", comment: "") - NSLayoutConstraint.activate([ - submitButton.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 20), - submitButton.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -20), - ]) + textField.keyboardType = .numberPad + textField.textContentType = .oneTimeCode + textField.placeholder = NSLocalizedString("Authentication.Pin", comment: "") } required init?(coder _: NSCoder) { @@ -88,15 +27,4 @@ class AuthenticationPinViewController: ViewControllerWithKeyboardConstraint { super.viewDidDisappear(animated) view.endEditing(true) } - - @objc private func didSubmit() { - submitButton.isEnabled = false - delegate?.didSubmit(pin: textField.text) - } -} - -extension AuthenticationPinViewController: AuthenticationViewControllerWithInput { - func enableSubmit() { - submitButton.isEnabled = true - } } diff --git a/Sources/Scenes/Authentication/ViewControllers/AuthenticationProfilePhotoViewController.swift b/Sources/Scenes/Authentication/ViewControllers/AuthenticationProfilePhotoViewController.swift new file mode 100644 index 00000000..9c7d427a --- /dev/null +++ b/Sources/Scenes/Authentication/ViewControllers/AuthenticationProfilePhotoViewController.swift @@ -0,0 +1,143 @@ +import UIKit + +private class ImageButton: UIButton { + init() { + super.init(frame: .zero) + translatesAutoresizingMaskIntoConstraints = false + + setImage( + UIImage(systemName: "camera.fill", withConfiguration: UIImage.SymbolConfiguration(pointSize: 47))!, + for: .normal + ) + + tintColor = .brandColor + backgroundColor = .white + } + + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layoutSubviews() { + super.layoutSubviews() + layer.cornerRadius = frame.size.width / 2 + } +} + +protocol AuthenticationProfilePhotoViewControllerDelegate: AnyObject { + func didUpload(image: UIImage) +} + +class AuthenticationProfilePhotoViewController: UIViewController, AuthenticationStepViewController { + weak var delegate: AuthenticationProfilePhotoViewControllerDelegate? + + var hasBackButton: Bool { + return false + } + + var hasSkipButton: Bool { + return true + } + + var stepDescription: String? { + return NSLocalizedString("Authentication.ProfilePhoto.Description", comment: "") + } + + var image: UIImage? + + private let imageButton: ImageButton = { + let button = ImageButton() + button.addTarget(self, action: #selector(didTapImageButton), for: .touchUpInside) + return button + }() + + let submitButton: ButtonWithLoadingIndicator = { + let button = ButtonWithLoadingIndicator(size: .large) + button.translatesAutoresizingMaskIntoConstraints = false + button.setTitle(NSLocalizedString("next", comment: ""), for: .normal) + button.backgroundColor = UIColor.white.withAlphaComponent(0.3) + button.isEnabled = false + button.addTarget(self, action: #selector(didSubmit), for: .touchUpInside) + return button + }() + + private let imagePicker: ImagePicker + + init() { + imagePicker = ImagePicker() + + super.init(nibName: nil, bundle: nil) + + imagePicker.delegate = self + + title = NSLocalizedString("Authentication.ProfilePhoto", comment: "") + + let circleView = UIView() + circleView.translatesAutoresizingMaskIntoConstraints = false + circleView.clipsToBounds = true + view.addSubview(circleView) + + circleView.addSubview(imageButton) + + NSLayoutConstraint.activate([ + imageButton.centerYAnchor.constraint(equalTo: circleView.centerYAnchor), + imageButton.centerXAnchor.constraint(equalTo: circleView.centerXAnchor), + imageButton.heightAnchor.constraint(equalToConstant: 180), + imageButton.widthAnchor.constraint(equalToConstant: 180), + ]) + + NSLayoutConstraint.activate([ + circleView.centerYAnchor.constraint(equalTo: view.centerYAnchor), + circleView.centerXAnchor.constraint(equalTo: view.centerXAnchor), + circleView.heightAnchor.constraint(equalToConstant: 180), + circleView.widthAnchor.constraint(equalToConstant: 180), + ]) + + circleView.layer.cornerRadius = 180 / 2 + + view.addSubview(submitButton) + + NSLayoutConstraint.activate([ + submitButton.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 20), + submitButton.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -20), + submitButton.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -20), + ]) + } + + func enableSubmit() { + submitButton.isLoading = false + } + + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func didTapImageButton() { + imagePicker.present(self) + } + + @objc private func didSubmit() { + guard let image = image else { + return + } + + delegate?.didUpload(image: image) + } +} + +extension AuthenticationProfilePhotoViewController: ImagePickerDelegate { + func didSelect(image: UIImage?) { + guard let profilePhoto = image else { + return + } + + imageButton.setImage(profilePhoto, for: .normal) + imageButton.contentHorizontalAlignment = .fill + imageButton.contentVerticalAlignment = .fill + imageButton.contentMode = .scaleAspectFill + + self.image = profilePhoto + + submitButton.isLoading = true + } +} diff --git a/Sources/Scenes/Authentication/ViewControllers/AuthenticationRegistrationViewController.swift b/Sources/Scenes/Authentication/ViewControllers/AuthenticationRegistrationViewController.swift deleted file mode 100644 index 65768c05..00000000 --- a/Sources/Scenes/Authentication/ViewControllers/AuthenticationRegistrationViewController.swift +++ /dev/null @@ -1,127 +0,0 @@ -import UIKit - -protocol AuthenticationRegistrationViewControllerDelegate { - func didSubmit(username: String?, displayName: String?, image: UIImage?) -} - -class AuthenticationRegistrationViewController: ViewControllerWithKeyboardConstraint { - var delegate: AuthenticationRegistrationViewControllerDelegate? - - private let usernameTextField: TextField = { - let textField = TextField(frame: .zero, theme: .light) - textField.translatesAutoresizingMaskIntoConstraints = false - textField.placeholder = NSLocalizedString("username", comment: "") - textField.autocorrectionType = .no - textField.autocapitalizationType = .none - return textField - }() - - private let displayNameTextField: TextField = { - let textField = TextField(frame: .zero, theme: .light) - textField.translatesAutoresizingMaskIntoConstraints = false - textField.placeholder = NSLocalizedString("display_name", comment: "") - textField.autocorrectionType = .no - textField.autocapitalizationType = .none - return textField - }() - - private let submitButton: Button = { - let button = Button(size: .large) - button.translatesAutoresizingMaskIntoConstraints = false - button.setTitle(NSLocalizedString("submit", comment: ""), for: .normal) - button.addTarget(self, action: #selector(didSubmit), for: .touchUpInside) - button.backgroundColor = UIColor.white.withAlphaComponent(0.3) - return button - }() - - private var imageButton: EditImageButton! - - private var imagePicker = ImagePicker() - - private var image: UIImage? - - private let label: UILabel = { - let label = UILabel() - label.translatesAutoresizingMaskIntoConstraints = false - label.textColor = .white - label.text = NSLocalizedString("create_account", comment: "") - label.font = .rounded(forTextStyle: .title1, weight: .bold) - return label - }() - - init() { - super.init(nibName: nil, bundle: nil) - - imageButton = EditImageButton() - imageButton.addTarget(self, action: #selector(selectImage)) - - view.addSubview(usernameTextField) - view.addSubview(displayNameTextField) - view.addSubview(submitButton) - view.addSubview(imageButton) - view.addSubview(label) - - imagePicker.delegate = self - - bottomLayoutConstraint = submitButton.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -(view.frame.size.height / 3)) - bottomLayoutConstraint.isActive = true - - NSLayoutConstraint.activate([ - imageButton.centerXAnchor.constraint(equalTo: view.centerXAnchor), - imageButton.bottomAnchor.constraint(equalTo: label.topAnchor, constant: -40), - imageButton.heightAnchor.constraint(equalToConstant: 80), - imageButton.widthAnchor.constraint(equalToConstant: 80), - ]) - - NSLayoutConstraint.activate([ - label.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 20), - label.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -20), - label.bottomAnchor.constraint(equalTo: usernameTextField.topAnchor, constant: -20), - ]) - - NSLayoutConstraint.activate([ - usernameTextField.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 20), - usernameTextField.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -20), - usernameTextField.heightAnchor.constraint(equalToConstant: 56), - usernameTextField.bottomAnchor.constraint(equalTo: displayNameTextField.topAnchor, constant: -20), - ]) - - NSLayoutConstraint.activate([ - displayNameTextField.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 20), - displayNameTextField.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -20), - displayNameTextField.heightAnchor.constraint(equalToConstant: 56), - displayNameTextField.bottomAnchor.constraint(equalTo: submitButton.topAnchor, constant: -20), - ]) - - NSLayoutConstraint.activate([ - submitButton.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 20), - submitButton.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -20), - ]) - } - - @objc private func didSubmit() { - delegate?.didSubmit(username: usernameTextField.text, displayName: displayNameTextField.text, image: image) - } - - @objc private func selectImage() { - imagePicker.present(self) - } - - required init?(coder _: NSCoder) { - fatalError("init(coder:) has not been implemented") - } -} - -extension AuthenticationRegistrationViewController: ImagePickerDelegate { - func didSelect(image: UIImage?) { - guard image != nil else { return } - imageButton.image = image - self.image = image - } -} - -extension AuthenticationRegistrationViewController: AuthenticationViewControllerWithInput { - func enableSubmit() { - submitButton.isEnabled = true - } -} diff --git a/Sources/Scenes/Authentication/ViewControllers/AuthenticationRequestNotificationsViewController.swift b/Sources/Scenes/Authentication/ViewControllers/AuthenticationRequestNotificationsViewController.swift deleted file mode 100644 index 343a19bd..00000000 --- a/Sources/Scenes/Authentication/ViewControllers/AuthenticationRequestNotificationsViewController.swift +++ /dev/null @@ -1,34 +0,0 @@ -import UIKit - -class AuthenticationRequestNotificationsViewController: UIViewController { - private let label: UILabel = { - let label = UILabel() - label.translatesAutoresizingMaskIntoConstraints = false - label.font = .rounded(forTextStyle: .title1, weight: .bold) - label.text = NSLocalizedString("enable_notifications", comment: "") - label.textColor = .white - label.numberOfLines = 0 - return label - }() - - override func viewDidLoad() { - super.viewDidLoad() - - let dude = UIImageView(image: UIImage(named: "bluedude.notifications")) - dude.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(dude) - - NSLayoutConstraint.activate([ - dude.rightAnchor.constraint(equalTo: view.rightAnchor), - dude.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -20), - ]) - - view.addSubview(label) - - NSLayoutConstraint.activate([ - label.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 20), - label.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -20), - label.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20), - ]) - } -} diff --git a/Sources/Scenes/Authentication/ViewControllers/AuthenticationStartViewController.swift b/Sources/Scenes/Authentication/ViewControllers/AuthenticationStartViewController.swift index 7ab8a99d..8d94777d 100644 --- a/Sources/Scenes/Authentication/ViewControllers/AuthenticationStartViewController.swift +++ b/Sources/Scenes/Authentication/ViewControllers/AuthenticationStartViewController.swift @@ -6,7 +6,19 @@ protocol AuthenticationStartViewControllerDelegate { func didRequestSignInWithApple() } -class AuthenticationStartViewController: UIViewController { +class AuthenticationStartViewController: UIViewController, AuthenticationStepViewController { + var stepDescription: String? { + return "" + } + + var hasBackButton: Bool { + return false + } + + var hasSkipButton: Bool { + return false + } + var delegate: AuthenticationStartViewControllerDelegate? private let terms: UITextView = { @@ -16,7 +28,7 @@ class AuthenticationStartViewController: UIViewController { text.isEditable = false text.backgroundColor = .clear text.contentInset = .zero - text.font = .rounded(forTextStyle: .caption1, weight: .medium) + text.font = .rounded(forTextStyle: .subheadline, weight: .semibold) text.linkTextAttributes = [ NSAttributedString.Key.foregroundColor: UIColor.white, ] @@ -45,26 +57,6 @@ class AuthenticationStartViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() - let logo = UIImageView(image: UIImage(named: "soapbar")) - logo.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(logo) - - let logoPlaceholder = UIView() - logoPlaceholder.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(logoPlaceholder) - - let greenDude = UIImageView(image: UIImage(named: "greendude")) - greenDude.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(greenDude) - - let blueDude = UIImageView(image: UIImage(named: "bluedude")) - blueDude.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(blueDude) - - let pinkDude = UIImageView(image: UIImage(named: "pinkdude")) - pinkDude.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(pinkDude) - view.addSubview(signInWithApple) view.addSubview(submitButton) @@ -72,33 +64,6 @@ class AuthenticationStartViewController: UIViewController { terms.textAlignment = .center view.addSubview(terms) - NSLayoutConstraint.activate([ - greenDude.leftAnchor.constraint(equalTo: view.leftAnchor), - greenDude.bottomAnchor.constraint(equalTo: submitButton.topAnchor, constant: -20), - ]) - - NSLayoutConstraint.activate([ - blueDude.rightAnchor.constraint(equalTo: view.rightAnchor), - blueDude.bottomAnchor.constraint(equalTo: greenDude.centerYAnchor), - ]) - - NSLayoutConstraint.activate([ - pinkDude.leftAnchor.constraint(equalTo: view.leftAnchor), - pinkDude.topAnchor.constraint(equalTo: view.topAnchor), - ]) - - NSLayoutConstraint.activate([ - logoPlaceholder.leftAnchor.constraint(equalTo: view.leftAnchor), - logoPlaceholder.rightAnchor.constraint(equalTo: view.rightAnchor), - logoPlaceholder.topAnchor.constraint(equalTo: pinkDude.bottomAnchor), - logoPlaceholder.bottomAnchor.constraint(equalTo: blueDude.topAnchor), - ]) - - NSLayoutConstraint.activate([ - logo.centerXAnchor.constraint(equalTo: logoPlaceholder.centerXAnchor), - logo.centerYAnchor.constraint(equalTo: logoPlaceholder.centerYAnchor), - ]) - NSLayoutConstraint.activate([ signInWithApple.heightAnchor.constraint(equalTo: submitButton.heightAnchor), signInWithApple.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 20), @@ -119,6 +84,10 @@ class AuthenticationStartViewController: UIViewController { ]) } + func enableSubmit() { + // do nothing + } + private func termsNoticeAttributedString() -> NSMutableAttributedString { let notice = NSLocalizedString("login_terms_notice", comment: "") let termsText = NSLocalizedString("terms", comment: "") diff --git a/Sources/Scenes/Authentication/ViewControllers/AuthenticationSuccessViewController.swift b/Sources/Scenes/Authentication/ViewControllers/AuthenticationSuccessViewController.swift deleted file mode 100644 index 0abba573..00000000 --- a/Sources/Scenes/Authentication/ViewControllers/AuthenticationSuccessViewController.swift +++ /dev/null @@ -1,38 +0,0 @@ -import SwiftConfettiView -import UIKit - -class AuthenticationSuccessViewController: UIViewController { - private let label: UILabel = { - let label = UILabel() - label.translatesAutoresizingMaskIntoConstraints = false - label.font = .rounded(forTextStyle: .title1, weight: .bold) - label.textColor = .white - label.text = NSLocalizedString("welcome", comment: "") - return label - }() - - private var confettiView: SwiftConfettiView! - - override func viewDidLoad() { - super.viewDidLoad() - - view.addSubview(label) - - NSLayoutConstraint.activate([ - label.centerYAnchor.constraint(equalTo: view.centerYAnchor), - label.centerXAnchor.constraint(equalTo: view.centerXAnchor), - ]) - - confettiView = SwiftConfettiView(frame: view.bounds) - view.addSubview(confettiView) - confettiView.startConfetti() - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - self.confettiView.stopConfetti() - } - } -} diff --git a/Sources/Scenes/Authentication/ViewControllers/AuthenticationTextInputViewController.swift b/Sources/Scenes/Authentication/ViewControllers/AuthenticationTextInputViewController.swift new file mode 100644 index 00000000..c7f65fa0 --- /dev/null +++ b/Sources/Scenes/Authentication/ViewControllers/AuthenticationTextInputViewController.swift @@ -0,0 +1,94 @@ +import UIKit + +protocol AuthenticationTextInputViewControllerDelegate: AnyObject { + func didSubmit(withText text: String?) +} + +class AuthenticationTextInputViewController: ViewControllerWithKeyboardConstraint, AuthenticationStepViewController { + weak var delegate: AuthenticationTextInputViewControllerDelegate? + + var stepDescription: String? { + return nil + } + + var hasBackButton: Bool { + return false + } + + var hasSkipButton: Bool { + return true + } + + let textField: TextField = { + let textField = TextField(frame: .zero, theme: .light) + textField.translatesAutoresizingMaskIntoConstraints = false + textField.autocorrectionType = .no + textField.autocapitalizationType = .none + return textField + }() + + let submitButton: ButtonWithLoadingIndicator = { + let button = ButtonWithLoadingIndicator(size: .large) + button.translatesAutoresizingMaskIntoConstraints = false + button.setTitle(NSLocalizedString("next", comment: ""), for: .normal) + button.backgroundColor = UIColor.white.withAlphaComponent(0.3) + button.isEnabled = false + button.addTarget(self, action: #selector(didSubmit), for: .touchUpInside) + return button + }() + + init() { + super.init(nibName: nil, bundle: nil) + + view.addSubview(textField) + view.addSubview(submitButton) + + bottomLayoutConstraint = submitButton.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -(view.frame.size.height / 3)) + bottomLayoutConstraint.isActive = true + + textField.delegate = self + + NSLayoutConstraint.activate([ + textField.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 20), + textField.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -20), + textField.heightAnchor.constraint(equalToConstant: 56), + textField.bottomAnchor.constraint(equalTo: submitButton.topAnchor, constant: -10), + ]) + + NSLayoutConstraint.activate([ + submitButton.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 20), + submitButton.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -20), + ]) + } + + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + view.endEditing(true) + } + + func enableSubmit() { + submitButton.isLoading = false + } + + @objc private func didSubmit() { + submitButton.isLoading = true + + delegate?.didSubmit(withText: textField.text) + } +} + +extension AuthenticationTextInputViewController: UITextFieldDelegate { + func textFieldDidChangeSelection(_ textField: UITextField) { + UIView.animate(withDuration: 0.3, animations: { + if textField.text != "" { + self.submitButton.isEnabled = true + } else { + self.submitButton.isEnabled = false + } + }) + } +} diff --git a/Sources/Scenes/Authentication/ViewControllers/AuthenticationUsernameViewController.swift b/Sources/Scenes/Authentication/ViewControllers/AuthenticationUsernameViewController.swift new file mode 100644 index 00000000..fc1c63cc --- /dev/null +++ b/Sources/Scenes/Authentication/ViewControllers/AuthenticationUsernameViewController.swift @@ -0,0 +1,22 @@ +import UIKit + +class AuthenticationUsernameViewController: AuthenticationTextInputViewController { + override var stepDescription: String? { + return NSLocalizedString("Authentication.Username.Description", comment: "") + } + + override var hasBackButton: Bool { + return true + } + + override init() { + super.init() + + title = NSLocalizedString("Authentication.Username", comment: "") + textField.placeholder = NSLocalizedString("Authentication.Username", comment: "") + } + + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/Sources/UI/Authentication/PermissionButton.swift b/Sources/UI/Authentication/PermissionButton.swift new file mode 100644 index 00000000..f6d0e9ab --- /dev/null +++ b/Sources/UI/Authentication/PermissionButton.swift @@ -0,0 +1,121 @@ +import UIKit + +class PermissionButton: UIButton { + let emoji: UILabel = { + let label = UILabel() + label.isUserInteractionEnabled = false + label.textAlignment = .center + label.translatesAutoresizingMaskIntoConstraints = false + label.font = .rounded(forTextStyle: .title1, weight: .semibold) + return label + }() + + let title: UILabel = { + let label = UILabel() + label.isUserInteractionEnabled = false + label.textColor = .white + label.translatesAutoresizingMaskIntoConstraints = false + label.font = .rounded(forTextStyle: .headline, weight: .semibold) + return label + }() + + let descriptionLabel: UILabel = { + let label = UILabel() + label.isUserInteractionEnabled = false + label.textColor = .white + label.font = .rounded(forTextStyle: .subheadline, weight: .regular) + label.numberOfLines = 0 + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + let checkmark: UIView = { + let view = UIView() + view.isUserInteractionEnabled = false + view.translatesAutoresizingMaskIntoConstraints = false + view.backgroundColor = .brandColor + + view.layer.borderWidth = 1.0 + view.layer.borderColor = UIColor.white.cgColor + + let image = UIImageView(image: UIImage( + systemName: "checkmark", + withConfiguration: UIImage.SymbolConfiguration(pointSize: 17, weight: .heavy) + )) + image.tintColor = .brandColor + image.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(image) + + NSLayoutConstraint.activate([ + image.centerXAnchor.constraint(equalTo: view.centerXAnchor), + image.centerYAnchor.constraint(equalTo: view.centerYAnchor), + image.heightAnchor.constraint(equalToConstant: 20), + image.widthAnchor.constraint(equalToConstant: 20), + ]) + + return view + }() + + override var isSelected: Bool { + didSet { + UIView.animate(withDuration: 0.3, animations: { + if self.isSelected { + self.checkmark.backgroundColor = .white + } else { + self.checkmark.backgroundColor = .brandColor + } + }) + } + } + + init() { + super.init(frame: .zero) + translatesAutoresizingMaskIntoConstraints = false + + addSubview(emoji) + addSubview(checkmark) + + let stack = UIStackView() + stack.translatesAutoresizingMaskIntoConstraints = false + stack.spacing = 5 + stack.distribution = .fill + stack.alignment = .fill + stack.axis = .vertical + stack.isUserInteractionEnabled = false + addSubview(stack) + + stack.addArrangedSubview(title) + stack.addArrangedSubview(descriptionLabel) + + addSubview(stack) + + NSLayoutConstraint.activate([ + emoji.leftAnchor.constraint(equalTo: leftAnchor), + emoji.centerYAnchor.constraint(equalTo: stack.centerYAnchor), + emoji.widthAnchor.constraint(equalTo: emoji.heightAnchor), + ]) + + NSLayoutConstraint.activate([ + checkmark.rightAnchor.constraint(equalTo: rightAnchor), + checkmark.centerYAnchor.constraint(equalTo: stack.centerYAnchor), + checkmark.widthAnchor.constraint(equalToConstant: 32), + checkmark.heightAnchor.constraint(equalToConstant: 32), + ]) + + NSLayoutConstraint.activate([ + stack.topAnchor.constraint(equalTo: topAnchor), + stack.bottomAnchor.constraint(equalTo: bottomAnchor), + stack.leftAnchor.constraint(equalTo: emoji.rightAnchor, constant: 20), + stack.rightAnchor.constraint(equalTo: checkmark.rightAnchor, constant: -20), + ]) + } + + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layoutSubviews() { + super.layoutSubviews() + checkmark.layer.cornerRadius = checkmark.frame.size.width / 2 + } +}