diff --git a/Package.swift b/Package.swift index 25ebb908a90ad60198cd069fbab071902387730d..45958a19c19a494cb6979c9dac8b55ca1905e56f 100644 --- a/Package.swift +++ b/Package.swift @@ -28,6 +28,7 @@ let package = Package( .library(name: "PushFeature", targets: ["PushFeature"]), .library(name: "SFTPFeature", targets: ["SFTPFeature"]), .library(name: "CrashService", targets: ["CrashService"]), + .library(name: "TermsFeature", targets: ["TermsFeature"]), .library(name: "Presentation", targets: ["Presentation"]), .library(name: "ToastFeature", targets: ["ToastFeature"]), .library(name: "BackupFeature", targets: ["BackupFeature"]), @@ -150,6 +151,7 @@ let package = Package( .target(name: "MenuFeature"), .target(name: "PushFeature"), .target(name: "SFTPFeature"), + .target(name: "TermsFeature"), .target(name: "ToastFeature"), .target(name: "CrashService"), .target(name: "BackupFeature"), @@ -507,6 +509,14 @@ let package = Package( .target(name: "DependencyInjection"), ] ), + .target( + name: "TermsFeature", + dependencies: [ + .target(name: "Theme"), + .target(name: "Shared"), + .target(name: "Defaults") + ] + ), .target( name: "RequestsFeature", dependencies: [ diff --git a/Sources/App/DependencyRegistrator.swift b/Sources/App/DependencyRegistrator.swift index 05d03f78e33764ef8a3f3cd325b5fa1b9d958c18..e24711457082d4ee7ba1bb51005e73762f656c10 100644 --- a/Sources/App/DependencyRegistrator.swift +++ b/Sources/App/DependencyRegistrator.swift @@ -34,6 +34,7 @@ import DependencyInjection import ScanFeature import ChatFeature import MenuFeature +import TermsFeature import BackupFeature import SearchFeature import LaunchFeature @@ -111,8 +112,16 @@ struct DependencyRegistrator { // MARK: Coordinators + container.register( + TermsCoordinator.live( + usernameFactory: OnboardingUsernameController.init(_:), + chatListFactory: ChatListController.init + ) + ) + container.register( LaunchCoordinator( + termsFactory: TermsConditionsController.init(_:), searchFactory: SearchContainerController.init, requestsFactory: RequestsContainerController.init, chatListFactory: ChatListController.init, @@ -206,6 +215,7 @@ struct DependencyRegistrator { searchFactory: SearchContainerController.init, welcomeFactory: OnboardingWelcomeController.init, chatListFactory: ChatListController.init, + termsFactory: TermsConditionsController.init(_:), usernameFactory: OnboardingUsernameController.init(_:), restoreListFactory: RestoreListController.init(_:), successFactory: OnboardingSuccessController.init(_:), diff --git a/Sources/Defaults/KeyObject.swift b/Sources/Defaults/KeyObject.swift index 7757f7ed4fa550c478736b76cd7c1036ef4fde34..0ade4e83639f54a5181b4292936a2d9dee049f60 100644 --- a/Sources/Defaults/KeyObject.swift +++ b/Sources/Defaults/KeyObject.swift @@ -21,6 +21,7 @@ public enum Key: String { // MARK: General case theme + case acceptedTerms // MARK: Requests diff --git a/Sources/LaunchFeature/LaunchCoordinator.swift b/Sources/LaunchFeature/LaunchCoordinator.swift index 28ac7f90d6833c85d43cf5dfc2fd56a0bc4dad23..37035773d6cfd875d5acf6de309b4ac84d72ae6f 100644 --- a/Sources/LaunchFeature/LaunchCoordinator.swift +++ b/Sources/LaunchFeature/LaunchCoordinator.swift @@ -5,6 +5,7 @@ import Presentation public protocol LaunchCoordinating { func toChats(from: UIViewController) + func toTerms(from: UIViewController) func toRequests(from: UIViewController) func toSearch(searching: String, from: UIViewController) func toOnboarding(with: String, from: UIViewController) @@ -15,6 +16,7 @@ public protocol LaunchCoordinating { public struct LaunchCoordinator: LaunchCoordinating { var replacePresenter: Presenting = ReplacePresenter() + var termsFactory: (String?) -> UIViewController var searchFactory: (String) -> UIViewController var requestsFactory: () -> UIViewController var chatListFactory: () -> UIViewController @@ -23,6 +25,7 @@ public struct LaunchCoordinator: LaunchCoordinating { var groupChatFactory: (GroupInfo) -> UIViewController public init( + termsFactory: @escaping (String?) -> UIViewController, searchFactory: @escaping (String) -> UIViewController, requestsFactory: @escaping () -> UIViewController, chatListFactory: @escaping () -> UIViewController, @@ -30,6 +33,7 @@ public struct LaunchCoordinator: LaunchCoordinating { singleChatFactory: @escaping (Contact) -> UIViewController, groupChatFactory: @escaping (GroupInfo) -> UIViewController ) { + self.termsFactory = termsFactory self.searchFactory = searchFactory self.requestsFactory = requestsFactory self.chatListFactory = chatListFactory @@ -46,6 +50,11 @@ public extension LaunchCoordinator { replacePresenter.present(chatListScreen, screen, from: parent) } + func toTerms(from parent: UIViewController) { + let screen = termsFactory(nil) + replacePresenter.present(screen, from: parent) + } + func toChats(from parent: UIViewController) { let screen = chatListFactory() replacePresenter.present(screen, from: parent) diff --git a/Sources/OnboardingFeature/Controllers/OnboardingStartController.swift b/Sources/OnboardingFeature/Controllers/OnboardingStartController.swift index d95169bbd9c5ce078687a77c96768d91e9d2336d..7ef1bae86c8297f1b674f486e93955587afe284b 100644 --- a/Sources/OnboardingFeature/Controllers/OnboardingStartController.swift +++ b/Sources/OnboardingFeature/Controllers/OnboardingStartController.swift @@ -52,7 +52,7 @@ public final class OnboardingStartController: UIViewController { super.viewDidLoad() screenView.startButton.publisher(for: .touchUpInside) - .sink { [unowned self] in coordinator.toUsername(with: ndf, from: self) } + .sink { [unowned self] in coordinator.toTerms(ndf: ndf, from: self) } .store(in: &cancellables) } } diff --git a/Sources/OnboardingFeature/Controllers/OnboardingUsernameController.swift b/Sources/OnboardingFeature/Controllers/OnboardingUsernameController.swift index 3bc4493ebd99899a4b9ea646964816ff0140009e..b1d3feb5cf5047be6cab2948051f4392973c85cc 100644 --- a/Sources/OnboardingFeature/Controllers/OnboardingUsernameController.swift +++ b/Sources/OnboardingFeature/Controllers/OnboardingUsernameController.swift @@ -36,6 +36,7 @@ public final class OnboardingUsernameController: UIViewController { public override func viewDidLoad() { super.viewDidLoad() + setupNavigationBar() setupScrollView() setupBindings() @@ -48,6 +49,26 @@ public final class OnboardingUsernameController: UIViewController { } } + private func setupNavigationBar() { + navigationItem.backButtonTitle = "" + + let backButton = UIButton() + backButton.setImage(Asset.navigationBarBack.image, for: .normal) + backButton.tintColor = Asset.neutralActive.color + backButton.imageView?.contentMode = .center + backButton.snp.makeConstraints { $0.width.equalTo(50) } + backButton + .publisher(for: .touchUpInside) + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigationController?.popViewController(animated: true) + }.store(in: &cancellables) + + navigationItem.leftBarButtonItem = UIBarButtonItem( + customView: UIStackView(arrangedSubviews: [backButton]) + ) + } + private func setupScrollView() { scrollViewController.scrollView.backgroundColor = .white diff --git a/Sources/OnboardingFeature/Coordinator/OnboardingCoordinator.swift b/Sources/OnboardingFeature/Coordinator/OnboardingCoordinator.swift index 59bf66c4e95c27f4a151472e473f657c88c13e1e..9ea57da2c51a02679f40c9132b88498fb139fa68 100644 --- a/Sources/OnboardingFeature/Coordinator/OnboardingCoordinator.swift +++ b/Sources/OnboardingFeature/Coordinator/OnboardingCoordinator.swift @@ -11,6 +11,7 @@ public protocol OnboardingCoordinating { func toEmail(from: UIViewController) func toPhone(from: UIViewController) func toWelcome(from: UIViewController) + func toTerms(ndf: String, from: UIViewController) func toUsername(with: String, from: UIViewController) func toRestoreList(with: String, from: UIViewController) func toDrawer(_: UIViewController, from: UIViewController) @@ -46,6 +47,7 @@ public struct OnboardingCoordinator: OnboardingCoordinating { var chatListFactory: () -> UIViewController var usernameFactory: (String) -> UIViewController var restoreListFactory: (String) -> UIViewController + var termsFactory: (String?) -> UIViewController var successFactory: (OnboardingSuccessModel) -> UIViewController var countriesFactory: (@escaping (Country) -> Void) -> UIViewController var phoneConfirmationFactory: (AttributeConfirmation, @escaping AttributeControllerClosure) -> UIViewController @@ -57,6 +59,7 @@ public struct OnboardingCoordinator: OnboardingCoordinating { searchFactory: @escaping (String?) -> UIViewController, welcomeFactory: @escaping () -> UIViewController, chatListFactory: @escaping () -> UIViewController, + termsFactory: @escaping (String?) -> UIViewController, usernameFactory: @escaping (String) -> UIViewController, restoreListFactory: @escaping (String) -> UIViewController, successFactory: @escaping (OnboardingSuccessModel) -> UIViewController, @@ -65,6 +68,7 @@ public struct OnboardingCoordinator: OnboardingCoordinating { emailConfirmationFactory: @escaping (AttributeConfirmation, @escaping AttributeControllerClosure) -> UIViewController ) { self.emailFactory = emailFactory + self.termsFactory = termsFactory self.phoneFactory = phoneFactory self.searchFactory = searchFactory self.welcomeFactory = welcomeFactory @@ -79,6 +83,14 @@ public struct OnboardingCoordinator: OnboardingCoordinating { } public extension OnboardingCoordinator { + func toTerms( + ndf: String, + from parent: UIViewController + ) { + let screen = termsFactory(ndf) + pushPresenter.present(screen, from: parent) + } + func toEmail(from parent: UIViewController) { let screen = emailFactory() replacePresenter.present(screen, from: parent) diff --git a/Sources/Shared/AutoGenerated/Strings.swift b/Sources/Shared/AutoGenerated/Strings.swift index 1f65eac1dc1fc32bef4a548cfee612b192f07f3f..03ada00df323371aae8d22c05b50903f9da768d6 100644 --- a/Sources/Shared/AutoGenerated/Strings.swift +++ b/Sources/Shared/AutoGenerated/Strings.swift @@ -1224,6 +1224,17 @@ public enum Localized { } } + public enum Terms { + /// Accept and proceed + public static let accept = Localized.tr("Localizable", "terms.accept") + /// By enabling the checkbox on the left, you agree with the terms and conditions. + public static let radio = Localized.tr("Localizable", "terms.radio") + /// Show terms and conditions + public static let show = Localized.tr("Localizable", "terms.show") + /// Terms #&# Conditions + public static let title = Localized.tr("Localizable", "terms.title") + } + public enum Ud { /// There are no users with that %@. public static func noneFound(_ p1: Any) -> String { diff --git a/Sources/Shared/Resources/en.lproj/Localizable.strings b/Sources/Shared/Resources/en.lproj/Localizable.strings index 4172e91f1c19da1b3d304f289c43c7dd7cd55e63..033e7a6f2ff8bd973d4eaf2cec0755295c8936b5 100644 --- a/Sources/Shared/Resources/en.lproj/Localizable.strings +++ b/Sources/Shared/Resources/en.lproj/Localizable.strings @@ -646,6 +646,17 @@ "backup.SFTP" = "SFTP"; +// Terms & Conditions + +"terms.title" += "Terms #&# Conditions"; +"terms.radio" += "By enabling the checkbox on the left, you agree with the terms and conditions."; +"terms.accept" += "Accept and proceed"; +"terms.show" += "Show terms and conditions"; + // Settings - Delete Account "settings.delete.title" diff --git a/Sources/TermsFeature/RadioButton.swift b/Sources/TermsFeature/RadioButton.swift new file mode 100644 index 0000000000000000000000000000000000000000..43b873ac9939ac8297e466ef4494cf9c78d9512a --- /dev/null +++ b/Sources/TermsFeature/RadioButton.swift @@ -0,0 +1,53 @@ +import UIKit +import Shared + +final class RadioButton: UIControl { + private let filledView = UIView() + private let containerView = UIView() + + init() { + super.init(frame: .zero) + + containerView.layer.borderWidth = 1 + containerView.layer.cornerRadius = 15 + containerView.layer.masksToBounds = true + containerView.layer.borderColor = UIColor.gray.cgColor + + filledView.isHidden = true + filledView.layer.cornerRadius = 10 + filledView.layer.masksToBounds = true + filledView.backgroundColor = Asset.brandPrimary.color + + containerView.isUserInteractionEnabled = false + filledView.isUserInteractionEnabled = false + + addSubview(containerView) + containerView.addSubview(filledView) + + setupConstraints() + } + + required init?(coder: NSCoder) { nil } + + func set(enabled: Bool) { + filledView.isHidden = !enabled + } + + private func setupConstraints() { + containerView.snp.makeConstraints { + $0.width.equalTo(30) + $0.height.equalTo(30) + $0.top.equalToSuperview().offset(5) + $0.left.equalToSuperview().offset(5) + $0.right.equalToSuperview().offset(-5) + $0.bottom.equalToSuperview().offset(-5) + } + + filledView.snp.makeConstraints { + $0.top.equalToSuperview().offset(5) + $0.left.equalToSuperview().offset(5) + $0.right.equalToSuperview().offset(-5) + $0.bottom.equalToSuperview().offset(-5) + } + } +} diff --git a/Sources/TermsFeature/RadioTextComponent.swift b/Sources/TermsFeature/RadioTextComponent.swift new file mode 100644 index 0000000000000000000000000000000000000000..64c7b1cfb3d80ccc85284f7544e286666faeb639 --- /dev/null +++ b/Sources/TermsFeature/RadioTextComponent.swift @@ -0,0 +1,40 @@ +import UIKit +import Shared + +final class RadioTextComponent: UIView { + let titleLabel = UILabel() + let radioButton = RadioButton() + + var isEnabled: Bool = false { + didSet { radioButton.set(enabled: isEnabled) } + } + + init() { + super.init(frame: .zero) + + titleLabel.numberOfLines = 0 + titleLabel.textColor = Asset.neutralBody.color + titleLabel.font = Fonts.Mulish.regular.font(size: 13.0) + + addSubview(titleLabel) + addSubview(radioButton) + + setupConstraints() + } + + required init?(coder: NSCoder) { nil } + + private func setupConstraints() { + titleLabel.snp.makeConstraints { + $0.left.equalTo(radioButton.snp.right).offset(7) + $0.centerY.equalTo(radioButton) + $0.right.equalToSuperview() + } + + radioButton.snp.makeConstraints { + $0.left.equalToSuperview() + $0.top.greaterThanOrEqualToSuperview() + $0.bottom.equalToSuperview() + } + } +} diff --git a/Sources/TermsFeature/TermsConditionsController.swift b/Sources/TermsFeature/TermsConditionsController.swift new file mode 100644 index 0000000000000000000000000000000000000000..ea017dd0f01aa1b6a86be0a02f15677d061a9ed1 --- /dev/null +++ b/Sources/TermsFeature/TermsConditionsController.swift @@ -0,0 +1,85 @@ +import UIKit +import Theme +import Shared +import Combine +import Defaults +import DependencyInjection + +public final class TermsConditionsController: UIViewController { + @Dependency var coordinator: TermsCoordinator + @Dependency var statusBarController: StatusBarStyleControlling + + @KeyObject(.acceptedTerms, defaultValue: false) var didAcceptTerms: Bool + + lazy private var screenView = TermsConditionsView() + + private let ndf: String? + private var cancellables = Set<AnyCancellable>() + + public init(_ ndf: String?) { + self.ndf = ndf + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { nil } + + public override func loadView() { + view = screenView + } + + public override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + statusBarController.style.send(.darkContent) + navigationController?.navigationBar.customize(translucent: true) + } + + public override func viewDidLoad() { + super.viewDidLoad() + + navigationItem.backButtonTitle = "" + + let backButton = UIButton() + backButton.setImage(Asset.navigationBarBack.image, for: .normal) + backButton.tintColor = Asset.neutralActive.color + backButton.imageView?.contentMode = .center + backButton.snp.makeConstraints { $0.width.equalTo(50) } + backButton + .publisher(for: .touchUpInside) + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + navigationController?.popViewController(animated: true) + }.store(in: &cancellables) + + navigationItem.leftBarButtonItem = UIBarButtonItem( + customView: UIStackView(arrangedSubviews: [backButton]) + ) + + screenView.radioComponent + .radioButton.publisher(for: .touchUpInside) + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + screenView.radioComponent.isEnabled.toggle() + screenView.nextButton.isEnabled = screenView.radioComponent.isEnabled + }.store(in: &cancellables) + + screenView.nextButton + .publisher(for: .touchUpInside) + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + didAcceptTerms = true + + if let ndf = ndf { + coordinator.presentUsername(ndf, self) + } else { + coordinator.presentChatList(self) + } + }.store(in: &cancellables) + + screenView.showTermsButton + .publisher(for: .touchUpInside) + .receive(on: DispatchQueue.main) + .sink { _ in + // TODO + }.store(in: &cancellables) + } +} diff --git a/Sources/TermsFeature/TermsConditionsView.swift b/Sources/TermsFeature/TermsConditionsView.swift new file mode 100644 index 0000000000000000000000000000000000000000..fb7d1198f586cd12ecc3e1b0e1479c5cfba84a41 --- /dev/null +++ b/Sources/TermsFeature/TermsConditionsView.swift @@ -0,0 +1,72 @@ +import UIKit +import Shared + +final class TermsConditionsView: UIView { + let titleLabel = UILabel() + let nextButton = CapsuleButton() + let showTermsButton = CapsuleButton() + let radioComponent = RadioTextComponent() + + init() { + super.init(frame: .zero) + backgroundColor = Asset.neutralWhite.color + + let attString = NSMutableAttributedString(string: Localized.Terms.title) + let paragraph = NSMutableParagraphStyle() + paragraph.alignment = .left + paragraph.lineHeightMultiple = 1.15 + + attString.addAttribute(.paragraphStyle, value: paragraph) + attString.addAttribute(.foregroundColor, value: Asset.neutralActive.color) + attString.addAttribute(.font, value: Fonts.Mulish.bold.font(size: 34.0) as Any) + + attString.addAttributes(attributes: [ + .font: Fonts.Mulish.bold.font(size: 34.0) as Any, + .foregroundColor: Asset.brandPrimary.color + ], betweenCharacters: "#") + + titleLabel.numberOfLines = 0 + titleLabel.attributedText = attString + + radioComponent.titleLabel.text = Localized.Terms.radio + + nextButton.isEnabled = false + nextButton.set(style: .brandColored, title: Localized.Terms.accept) + showTermsButton.set(style: .seeThrough, title: Localized.Terms.show) + + addSubview(titleLabel) + addSubview(nextButton) + addSubview(radioComponent) + addSubview(showTermsButton) + + setupConstraints() + } + + required init?(coder: NSCoder) { nil } + + private func setupConstraints() { + titleLabel.snp.makeConstraints { + $0.top.equalTo(safeAreaLayoutGuide).offset(30) + $0.left.equalToSuperview().offset(38) + $0.right.equalToSuperview().offset(-44) + } + + radioComponent.snp.makeConstraints { + $0.left.equalToSuperview().offset(40) + $0.right.equalToSuperview().offset(-40) + $0.bottom.equalTo(nextButton.snp.top).offset(-20) + } + + nextButton.snp.makeConstraints { + $0.left.equalToSuperview().offset(40) + $0.right.equalToSuperview().offset(-40) + $0.bottom.equalTo(showTermsButton.snp.top).offset(-10) + } + + showTermsButton.snp.makeConstraints { + $0.left.equalToSuperview().offset(40) + $0.right.equalToSuperview().offset(-40) + $0.bottom.equalTo(safeAreaLayoutGuide).offset(-40) + } + } +} diff --git a/Sources/TermsFeature/TermsCoordinator.swift b/Sources/TermsFeature/TermsCoordinator.swift new file mode 100644 index 0000000000000000000000000000000000000000..daff90e4b1b81b18e75e3a6e34557a3478e26f57 --- /dev/null +++ b/Sources/TermsFeature/TermsCoordinator.swift @@ -0,0 +1,25 @@ +import UIKit +import Presentation + +public struct TermsCoordinator { + var presentChatList: (UIViewController) -> Void + var presentUsername: (String, UIViewController) -> Void +} + +public extension TermsCoordinator { + static func live( + usernameFactory: @escaping (String) -> UIViewController, + chatListFactory: @escaping () -> UIViewController + ) -> Self { + .init( + presentChatList: { parent in + let presenter = ReplacePresenter() + presenter.present(chatListFactory(), from: parent) + }, + presentUsername: { ndf, parent in + let presenter = PushPresenter() + presenter.present(usernameFactory(ndf), from: parent) + } + ) + } +}