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
+ }
+}