From 010ba51ba62eb7a2c2423a18a0544814ff8fb91c Mon Sep 17 00:00:00 2001 From: Shashank Kaushik Date: Sat, 15 Jun 2024 22:37:45 +0530 Subject: [PATCH] Checkbox for user age instead of a birth date --- Style/Sources/Style/Assets.swift | 3 + .../blue_checked_icon.imageset/Contents.json | 12 ++ .../blue_checked_icon.pdf | Bin 0 -> 2766 bytes .../Contents.json | 12 ++ .../blue_unchecked_icon.pdf | Bin 0 -> 1068 bytes .../red_unchecked_icon.imageset/Contents.json | 12 ++ .../red_unchecked_icon.pdf | Bin 0 -> 1154 bytes .../Components/Views/AgeConsentView.swift | 110 ++++++++++++++++++ animeal/res/en.lproj/Localizable.strings | 1 + .../Profile/UserValidationModel.swift | 2 +- animeal/src/Common/Strings.swift | 2 + .../Model/Items/ProfileModelAction.swift | 8 +- .../Model/Items/ProfileModelItem.swift | 18 ++- .../Model/Items/ProfileModelItemError.swift | 4 + .../Flows/Profile/Model/ProfileModel.swift | 4 +- .../UseCases/FetchProfileItemsUseCase.swift | 12 +- .../src/Flows/Profile/ProfileContract.swift | 2 +- .../Flows/Profile/ProfileViewController.swift | 41 +++---- .../src/Flows/Profile/ProfileViewModel.swift | 26 +++-- .../Profile/View/Items/ProfileViewItem.swift | 30 ++++- .../View/Items/ProfileViewTextEvent.swift | 1 + .../ProfileViewItem+TextInputViewModel.swift | 2 +- .../Utils/Mappers/ProfileViewItemMapper.swift | 32 ++--- .../Generated/AutoMockable.generated.swift | 12 +- 24 files changed, 267 insertions(+), 79 deletions(-) create mode 100644 Style/Sources/Style/Resources/Images/Images.xcassets/Profile/blue_checked_icon.imageset/Contents.json create mode 100644 Style/Sources/Style/Resources/Images/Images.xcassets/Profile/blue_checked_icon.imageset/blue_checked_icon.pdf create mode 100644 Style/Sources/Style/Resources/Images/Images.xcassets/Profile/blue_unchecked_icon.imageset/Contents.json create mode 100644 Style/Sources/Style/Resources/Images/Images.xcassets/Profile/blue_unchecked_icon.imageset/blue_unchecked_icon.pdf create mode 100644 Style/Sources/Style/Resources/Images/Images.xcassets/Profile/red_unchecked_icon.imageset/Contents.json create mode 100644 Style/Sources/Style/Resources/Images/Images.xcassets/Profile/red_unchecked_icon.imageset/red_unchecked_icon.pdf create mode 100644 UIComponents/Sources/UIComponents/Components/Views/AgeConsentView.swift diff --git a/Style/Sources/Style/Assets.swift b/Style/Sources/Style/Assets.swift index a518fc0f..af93f857 100644 --- a/Style/Sources/Style/Assets.swift +++ b/Style/Sources/Style/Assets.swift @@ -73,7 +73,10 @@ public enum Asset { public static let linkedinIcon = ImageAsset(name: "linkedin_icon") public static let websiteIcon = ImageAsset(name: "website_icon") public static let onboardingFeed = ImageAsset(name: "onboarding_feed") + public static let blueCheckedIcon = ImageAsset(name: "blue_checked_icon") + public static let blueUncheckedIcon = ImageAsset(name: "blue_unchecked_icon") public static let calendar = ImageAsset(name: "calendar") + public static let redUncheckedIcon = ImageAsset(name: "red_unchecked_icon") public static let signInApple = ImageAsset(name: "sign_in_apple") public static let signInFacebook = ImageAsset(name: "sign_in_facebook") public static let signInMobile = ImageAsset(name: "sign_in_mobile") diff --git a/Style/Sources/Style/Resources/Images/Images.xcassets/Profile/blue_checked_icon.imageset/Contents.json b/Style/Sources/Style/Resources/Images/Images.xcassets/Profile/blue_checked_icon.imageset/Contents.json new file mode 100644 index 00000000..224ce972 --- /dev/null +++ b/Style/Sources/Style/Resources/Images/Images.xcassets/Profile/blue_checked_icon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "blue_checked_icon.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Style/Sources/Style/Resources/Images/Images.xcassets/Profile/blue_checked_icon.imageset/blue_checked_icon.pdf b/Style/Sources/Style/Resources/Images/Images.xcassets/Profile/blue_checked_icon.imageset/blue_checked_icon.pdf new file mode 100644 index 0000000000000000000000000000000000000000..684c216b043274a8c44aea9b8cd0035679e63439 GIT binary patch literal 2766 zcmeHJ%}?7f6uyp=S#xw?Clg#rvl@I{tO^*{w| zoLOjESG#RTK>wRHEn@`dps&u$W}DZtES`JoqmflL-`y!~^rEUa7MsbY+SP?@(1hE0 zc2+&nTgnDZ4?p3<7UJn3%(9FL`W;k(WTR@CZ%8nazgC5uP0#3f-u){Y@{{mZ+^Ifx zv;8%Y##fi-w(zYr>O(U>Qd}Zf{G8pxj%rGy&$^0i6yF2S8oMI0cms)UKro$T%yOoGhcQ zKZ1I!AVv=?dk{35BeD+Y*}*lm;SlFa@fwF~@<8rMNxr@}c414VV-;fOBD+7q*txCk zDTcHYJL{l8t_G2bXKZ4O{5fF(N7MNT`xt~9hM-xS3;+t_nxLs+A2m8@Z`vQ3$Z}z` z8fI9z1S`{&aLQv`*J$X_HwruYC~S<2&puPwh;=}hn;(VUsp0=s*so+crm4yDC>1Wt z;p*+QFWb0uXS#v=5Hs61kn<-sovU0)y?)O1XMUMCKlIG>KK$)&=)zDjP(f&x+`BGZoQZ82czczc+*f@*+ zuTATeX8X>VU%;C?;&#;C2<_a6mUDx01gUI4KuP7auP=e%7Ba>#Gy^>LGWbh z?-C4by@2px^Ad`I8ywD}2tz2mJ`^#pOV#;mEo*}GewqNehfTGSNAz&DI5HFmv#J7q ekv<)-V7~e-kqaQVQF*O2J0ePAa&T~db@2yG*izg8 literal 0 HcmV?d00001 diff --git a/Style/Sources/Style/Resources/Images/Images.xcassets/Profile/blue_unchecked_icon.imageset/Contents.json b/Style/Sources/Style/Resources/Images/Images.xcassets/Profile/blue_unchecked_icon.imageset/Contents.json new file mode 100644 index 00000000..ca64b143 --- /dev/null +++ b/Style/Sources/Style/Resources/Images/Images.xcassets/Profile/blue_unchecked_icon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "blue_unchecked_icon.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Style/Sources/Style/Resources/Images/Images.xcassets/Profile/blue_unchecked_icon.imageset/blue_unchecked_icon.pdf b/Style/Sources/Style/Resources/Images/Images.xcassets/Profile/blue_unchecked_icon.imageset/blue_unchecked_icon.pdf new file mode 100644 index 0000000000000000000000000000000000000000..eebb3b5f7a29fda571a684e1edae37949f82082c GIT binary patch literal 1068 zcmY!laBvK;I`dFTEr~!5FAK2q*+Jp}3?dH8Gc~f-!f>!K}ju0xs`=an0v0)VV$B zO?LE)oo^S)D+C2?U{<~G{=G$Kn%J5ro2p*Vof~Uuxo_UTx=$}3zuj5+v&4Pwr>lQ= zt$Uj{|5(3j6mL)N{j+s5r0*}=IeSt5pO8s90;l|p7Ihn2Em2#Qx{}#9$jL+KlvJq+ zgLcQJ+r}qE1&!E#i?D8r6n*&U+`HE&V>iUcOUPRr+`E&g)IhhAJnJkVHT_rc=>$FAE{&y~9&6#>& zhpohlS5~G1Yfm+1_JlfJ)$xle%}VH<`*iBdt6$$XaLX>1Xm;1?2vE3wo9ov$`DGJ= zI2YYy;chu|zA>kG&Fe(D(~46LUpOYPykW(YEy_xNIykRN-|ouos(ZukeD-zGe1^pW zfvjC7&K1VzCd)Mx-&8RT)b*C#l2ji!L;R_PTF^ApqzfXc=g;mpkZ=3@TkXR-fmyA# zRcxm7?4;56jLyT3L&LakPtlm z>U(FV0G+A;@p}^2U0nAd!g9{l0-3bB+AyY$33?UO>=%A`HH^k6r zZeameRZ^6gnUh+?1&TdS7hrH`6zAurYAR@Crf5O}ML#G%zeEA#VQ>KJ2WM8L0$mTz Uyop66VDDI%m~*MBy862T0OWCEm;e9( literal 0 HcmV?d00001 diff --git a/Style/Sources/Style/Resources/Images/Images.xcassets/Profile/red_unchecked_icon.imageset/Contents.json b/Style/Sources/Style/Resources/Images/Images.xcassets/Profile/red_unchecked_icon.imageset/Contents.json new file mode 100644 index 00000000..67f3275c --- /dev/null +++ b/Style/Sources/Style/Resources/Images/Images.xcassets/Profile/red_unchecked_icon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "red_unchecked_icon.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Style/Sources/Style/Resources/Images/Images.xcassets/Profile/red_unchecked_icon.imageset/red_unchecked_icon.pdf b/Style/Sources/Style/Resources/Images/Images.xcassets/Profile/red_unchecked_icon.imageset/red_unchecked_icon.pdf new file mode 100644 index 0000000000000000000000000000000000000000..7d2ffb7a4176a325b6a4b4ad7a441730b1ecbfd5 GIT binary patch literal 1154 zcmY!laBvK;I`dFTEr~!5FAK2q*+Jp}3?dH8Gc~g0XgrZ=R^3K+AW#qiZHUx?%iy zzplbYHZhq4TvZ+8Nw&yTm$o4;>&`rp<2 zciz|gQSe8n@N;yw?wZqw4?HQ(NbL9ivb1$p;FRL~=VD#%9`!VLey^9dzfH`w-Q?Ov z;it|+G;ZAL-S_;T5)gv#ur7rx&0ec4(Kk4I$En6q?`Zu>e+J(Ci&lP zpKGrf6&y9ykE78$hp$O3Fc?L{h8XZd!-TNnI`m@fy?z-a{`~|n(M~b8VzL*&DO%va>8LH2@=iH*6^8WZ- zx5`qUC5ausLY+AO3IG4J@6mVt-D@3xgAxoh#$ibZlolXK%EZDPo}f(NqQHcv@0gdD zUs9|P9ScoHA(aKG3i<(w>9BO=o0^iD=#*cf5UpUKX8;BWhM@&a$k1HR*wWI{z(@gR zQc!AferZv1YOw++X@k-vD49Cv=ar=90c`}OYnWg_Vi8bG!4xWll=?wJ@LZwqotXl3 zsshLdL2!GV^Gktx4Kdtb49Nwsz%WBG3FN_IxP>5xK|Jo9Sdy5NpN^`vq9`?u%Rs@L z3*vncsbFSmYHX?i6ovvr0|hWkArCHO40I<5AcV{efc`-fGBQUOvH)s9Q)OX}Fsq~} zF*7H%hzk^Zo-V-P&?wH&P1RJ;$V}0M1d4u8etwAp$iv_O)(_6CN(H(eTqGnGm4Llt NX>81;s_N?R1^}7clKB7t literal 0 HcmV?d00001 diff --git a/UIComponents/Sources/UIComponents/Components/Views/AgeConsentView.swift b/UIComponents/Sources/UIComponents/Components/Views/AgeConsentView.swift new file mode 100644 index 00000000..175b9b5d --- /dev/null +++ b/UIComponents/Sources/UIComponents/Components/Views/AgeConsentView.swift @@ -0,0 +1,110 @@ +// +// AgeConsentView.swift +// +// +// Created by Shashank Kaushik on 26/05/24. +// + +import Combine +import Style +import UIKit + +public final class AgeConsentView: UIStackView { + private let checkboxImageView = UIImageView() + private let label = UILabel() + private var viewModel: AgeConsentViewModel? { + didSet { + updateUI(viewModel?.state ?? .unchecked) + } + } + + public var onTap: ((Bool) -> (Void))? + + // MARK: - Initialization + override init(frame: CGRect) { + super.init(frame: frame) + setup() + } + + required init(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public func configure(_ viewModel: AgeConsentViewModel) { + self.viewModel = viewModel + } +} + +private extension AgeConsentView { + // MARK: - Setup + func setup() { + axis = .horizontal + spacing = 8.0 + distribution = .fill + alignment = .leading + translatesAutoresizingMaskIntoConstraints = false + + label.numberOfLines = 0 + label.lineBreakMode = .byWordWrapping + label.font = designEngine.fonts.primary.semibold(14.0) + checkboxImageView.contentMode = .scaleAspectFit + + addArrangedSubview(checkboxImageView) + addArrangedSubview(label) + + isUserInteractionEnabled = true + let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTapGesture)) + addGestureRecognizer(tapGesture) + } + + @objc func handleTapGesture() { + viewModel?.toggleState() + onTap?(viewModel?.state == .checked) + } + + private func updateUI(_ newState: CheckBoxState) { + label.text = viewModel?.title + switch newState { + case .checked: + checkboxImageView.image = Asset.Images.blueCheckedIcon.image + label.textColor = designEngine.colors.textPrimary + case .unchecked: + checkboxImageView.image = Asset.Images.blueUncheckedIcon.image + label.textColor = designEngine.colors.textPrimary + } + } +} + +public extension AgeConsentView { + + class AgeConsentViewModel: ObservableObject, AgeConsentViewModelProtocol { + public var title: String + @Published public var state: CheckBoxState + + var cancellables = Set() + + public init(state: CheckBoxState, title: String) { + self.state = state + self.title = title + } + + func toggleState() { + switch state { + case .checked: + state = .unchecked + case .unchecked: + state = .checked + } + } + } +} + +public enum CheckBoxState { + case checked + case unchecked +} + +public protocol AgeConsentViewModelProtocol { + var title: String { get } + var state: CheckBoxState { get } +} diff --git a/animeal/res/en.lproj/Localizable.strings b/animeal/res/en.lproj/Localizable.strings index 064ca546..f5e9f980 100644 --- a/animeal/res/en.lproj/Localizable.strings +++ b/animeal/res/en.lproj/Localizable.strings @@ -29,6 +29,7 @@ "profile.errors.incorrectFormat" = "Format is incorrect"; "profile.errors.incorrectCharactersLength" = "Length must be between 2 and 35"; "profile.errors.incorrectCharacters" = "Must contain only letters"; +"profile.consent" = "You acknowledge that you are atleast 15 years old"; "phone.title" = "Please, enter your phone"; "phone.fields.phoneTitlle" = "Phone Number"; "phone.fields.passwordTitlle" = "Password"; diff --git a/animeal/src/Business/Services/Profile/UserValidationModel.swift b/animeal/src/Business/Services/Profile/UserValidationModel.swift index 34f01ade..874b0acd 100644 --- a/animeal/src/Business/Services/Profile/UserValidationModel.swift +++ b/animeal/src/Business/Services/Profile/UserValidationModel.swift @@ -36,7 +36,7 @@ final class UserValidationModel: UserProfileValidationModel { emailVerified = attributes[.emailVerified] .map { Bool($0) ?? false } ?? false - areAllNecessaryFieldsFilled = [UserProfileAttributeKey.name, .familyName, .email, .birthDate, .phoneNumber] + areAllNecessaryFieldsFilled = [UserProfileAttributeKey.name, .familyName, .email, .phoneNumber] .allSatisfy { attributes[$0]?.isEmpty == false } } } diff --git a/animeal/src/Common/Strings.swift b/animeal/src/Common/Strings.swift index 8c8f8cab..368f2c63 100644 --- a/animeal/src/Common/Strings.swift +++ b/animeal/src/Common/Strings.swift @@ -194,6 +194,8 @@ internal enum L10n { internal static let birthDate = L10n.tr("Localizable", "profile.birthDate", fallback: "Birthdate") /// Cancel internal static let cancel = L10n.tr("Localizable", "profile.cancel", fallback: "Cancel") + /// You acknowledge that you are atleast 15 years old + internal static let consent = L10n.tr("Localizable", "profile.consent", fallback: "You acknowledge that you are atleast 15 years old") /// Done internal static let done = L10n.tr("Localizable", "profile.done", fallback: "Done") /// Edit diff --git a/animeal/src/Flows/Profile/Model/Items/ProfileModelAction.swift b/animeal/src/Flows/Profile/Model/Items/ProfileModelAction.swift index 125e083d..d7c11432 100644 --- a/animeal/src/Flows/Profile/Model/Items/ProfileModelAction.swift +++ b/animeal/src/Flows/Profile/Model/Items/ProfileModelAction.swift @@ -165,9 +165,9 @@ final class ProfileModelEditAction: ProfileModelAction { let items = await state.items let editableItems = items.toEditable { item in switch item.type { - case .name, .surname, .email, .birthday: + case .name, .surname, .email: return false - case .phone: + case .phone, .birthday: return true } } @@ -218,9 +218,9 @@ final class ProfileModelSaveAction: ProfileModelAction { let items = await state.items let editableItems = items.toEditable { item in switch item.type { - case .name, .surname, .email, .birthday: + case .name, .surname, .email: return false - case .phone: + case .phone, .birthday: return true } } diff --git a/animeal/src/Flows/Profile/Model/Items/ProfileModelItem.swift b/animeal/src/Flows/Profile/Model/Items/ProfileModelItem.swift index 09e578d5..80d73a4c 100644 --- a/animeal/src/Flows/Profile/Model/Items/ProfileModelItem.swift +++ b/animeal/src/Flows/Profile/Model/Items/ProfileModelItem.swift @@ -50,8 +50,17 @@ struct ProfileModelItem: Hashable, ProfileModelValidatable { _value = transform(newValue) } } + var selected: Bool { + get { + style == .readonly ? true : _selected ?? false + } + set { + _selected = style == .readonly ? true : newValue + } + } var date: Date? { transformDate(text) } private var _value: String? + private var _selected: Bool? init( identifier: String, @@ -80,7 +89,7 @@ struct ProfileModelItem: Hashable, ProfileModelValidatable { case .phone(let region): return try validatePhone(region) case .birthday: - return try validateDate() + return try validateCheckBox() } } @@ -167,6 +176,13 @@ extension ProfileModelItem { return region.phoneNumberCode + text } + func validateCheckBox() throws -> String { + guard selected == true else { + throw ProfileModelItemAgeError(selected: false) + } + return "" + } + func validateDate() throws -> String { let text = try validateForEmptiness() diff --git a/animeal/src/Flows/Profile/Model/Items/ProfileModelItemError.swift b/animeal/src/Flows/Profile/Model/Items/ProfileModelItemError.swift index 465f0be8..31de159e 100644 --- a/animeal/src/Flows/Profile/Model/Items/ProfileModelItemError.swift +++ b/animeal/src/Flows/Profile/Model/Items/ProfileModelItemError.swift @@ -4,3 +4,7 @@ struct ProfileModelItemError: LocalizedError { let itemIdentifier: String var errorDescription: String? } + +struct ProfileModelItemAgeError: LocalizedError { + let selected: Bool +} diff --git a/animeal/src/Flows/Profile/Model/ProfileModel.swift b/animeal/src/Flows/Profile/Model/ProfileModel.swift index 9fb535d6..74747518 100644 --- a/animeal/src/Flows/Profile/Model/ProfileModel.swift +++ b/animeal/src/Flows/Profile/Model/ProfileModel.swift @@ -52,8 +52,8 @@ final class ProfileModel: ProfileModelProtocol { await fetchItems(identifier) } - func updateItem(_ text: String?, forIdentifier identifier: String) async { - await updateItems(text, forIdentifier: identifier) + func updateItem(_ text: String?, _ selected: Bool, forIdentifier identifier: String) async { + await updateItems(text, selected, forIdentifier: identifier) } func validateItems() async -> Bool { diff --git a/animeal/src/Flows/Profile/Model/UseCases/FetchProfileItemsUseCase.swift b/animeal/src/Flows/Profile/Model/UseCases/FetchProfileItemsUseCase.swift index 2060c1ac..b8d8d609 100644 --- a/animeal/src/Flows/Profile/Model/UseCases/FetchProfileItemsUseCase.swift +++ b/animeal/src/Flows/Profile/Model/UseCases/FetchProfileItemsUseCase.swift @@ -17,7 +17,7 @@ protocol FetchProfileItemRequiredActionUseCase { } protocol UpdateProfileItemsUseCaseLogic { - func callAsFunction(_ text: String?, forIdentifier identifier: String) async + func callAsFunction(_ text: String?, _ selected: Bool, forIdentifier identifier: String) async } protocol ValidateProfileItemsUseCaseLogic { @@ -113,13 +113,6 @@ extension FetchProfileItemsUseCase: FetchProfileItemsUseCaseLogic { } else { item.text = nil } - case .birthday where isFacebook: - dateFormatter.dateFormat = "MM/dd/yyyy" - if let birthDate = knownAttributes[key]?.value, - let date = dateFormatter.date(from: birthDate) { - dateFormatter.dateFormat = "dd/MM/yyyy" - item.text = dateFormatter.string(from: date) - } default: item.text = knownAttributes[key]?.value } @@ -165,7 +158,7 @@ extension FetchProfileItemsUseCase: UpdateProfileItemsUseCaseLogic { /// - Parameters: /// - text: the value of the profile item. for name is the updated name, for email it's the updated email. /// - identifier: Identifer of the particular profile item which was updated. - func callAsFunction(_ text: String?, forIdentifier identifier: String) async { + func callAsFunction(_ text: String?, _ selected: Bool, forIdentifier identifier: String) async { var items = await state.items guard let itemIndex = items.firstIndex(where: { $0.identifier == identifier }) @@ -173,6 +166,7 @@ extension FetchProfileItemsUseCase: UpdateProfileItemsUseCaseLogic { var value = items[itemIndex] value.text = text + value.selected = selected items[itemIndex] = value var changedItemsTypes = await state.changedItemsTypes diff --git a/animeal/src/Flows/Profile/ProfileContract.swift b/animeal/src/Flows/Profile/ProfileContract.swift index 89ff90d2..3c521c24 100644 --- a/animeal/src/Flows/Profile/ProfileContract.swift +++ b/animeal/src/Flows/Profile/ProfileContract.swift @@ -43,7 +43,7 @@ protocol ProfileModelProtocol { func fetchCachedItems() async throws -> [ProfileModelItem] func fetchItems() async throws -> [ProfileModelItem] func fetchItem(_ identifier: String) async -> ProfileModelItem? - func updateItem(_ text: String?, forIdentifier identifier: String) async + func updateItem(_ text: String?, _ selected: Bool, forIdentifier identifier: String) async func validateItems() async -> Bool func fetchRequiredAction(forIdentifier identifier: String) async -> PhoneModelRequiredAction? diff --git a/animeal/src/Flows/Profile/ProfileViewController.swift b/animeal/src/Flows/Profile/ProfileViewController.swift index 5e5dfc47..01074977 100644 --- a/animeal/src/Flows/Profile/ProfileViewController.swift +++ b/animeal/src/Flows/Profile/ProfileViewController.swift @@ -151,15 +151,16 @@ private extension ProfileViewController { } // MARK: - Configuration - func createViewItems(_ viewItems: [ProfileViewItem]) { + func createViewItems(_ viewItems: [ProfileViewItemProtocol]) { inputsContentView.arrangedSubviews.forEach { $0.removeFromSuperview() } viewItems.forEach { item in switch item.type { case .phone: + guard let model = (item as? ProfileTextFieldViewItem)?.phoneModel else { return } let inputView = PhoneInputView() - inputView.configure(item.phoneModel) + inputView.configure(model) inputView.codeWasTapped = { [weak self] _ in self?.viewModel.handleActionEvent( ProfileViewActionEvent.itemWasTapped(item.identifier) @@ -196,22 +197,18 @@ private extension ProfileViewController { ) } inputsContentView.addArrangedSubview(inputView) - case .birthday: - let inputView = DateInputView() - inputView.configure(item.dateModel) - inputsContentView.addArrangedSubview(inputView) - inputView.valueWasChanged = { [weak self] textInput, date in - let result = self?.viewModel.handleItemEvent(.changeDate(item.identifier, date)) - textInput.text = result?.formattedText - } - inputView.didEndEditing = { [weak self] textInput in - self?.viewModel.handleItemEvent( - .changeText(.endEditing(item.identifier, textInput.text)) - ) + case .birthday: + guard let model: AgeConsentView.AgeConsentViewModel = (item as? ProfileAgeConsentViewItem)?.ageConsentModel else { return } + let ageConsentView = AgeConsentView() + ageConsentView.onTap = { [weak self] selected in + self?.viewModel.handleItemEvent(.clickCheckBox(item.identifier ,selected)) } + ageConsentView.configure(model) + inputsContentView.addArrangedSubview(ageConsentView) default: + guard let model = (item as? ProfileTextFieldViewItem)?.model else { return } let inputView = DefaultInputView() - inputView.configure(item.model) + inputView.configure(model) inputsContentView.addArrangedSubview(inputView) inputView.shouldChangeCharacters = { [weak self] textInput, range, string in let text = textInput.text @@ -236,7 +233,7 @@ private extension ProfileViewController { } } - func updateViewItems(_ viewItems: [ProfileViewItem]) { + func updateViewItems(_ viewItems: [ProfileViewItemProtocol]) { let identifiedViewInputs = inputsContentView.arrangedSubviews .compactMap { $0 as? TextInputDecoratable } .reduce([String: TextInputDecoratable]()) { partialResult, input in @@ -245,18 +242,22 @@ private extension ProfileViewController { return result } + let ageConsentView = inputsContentView.arrangedSubviews + .compactMap { $0 as? AgeConsentView }.first + viewItems.forEach { viewItem in switch viewItem.type { case .phone: guard let inputView = identifiedViewInputs[viewItem.identifier] as? PhoneInputView else { return } - inputView.configure(viewItem.phoneModel) + inputView.configure((viewItem as! ProfileTextFieldViewItem).phoneModel) case .birthday: - guard let inputView = identifiedViewInputs[viewItem.identifier] as? DateInputView else { return } - inputView.configure(viewItem.dateModel) + guard let inputView = ageConsentView, let model = (viewItem as? ProfileAgeConsentViewItem)?.ageConsentModel else { return } + inputView.configure(model) default: guard let inputView = identifiedViewInputs[viewItem.identifier] as? DefaultInputView else { return } - inputView.configure(viewItem.model) + inputView.configure((viewItem as! ProfileTextFieldViewItem).model) } } + } } diff --git a/animeal/src/Flows/Profile/ProfileViewModel.swift b/animeal/src/Flows/Profile/ProfileViewModel.swift index a68284dd..fb11fbb2 100644 --- a/animeal/src/Flows/Profile/ProfileViewModel.swift +++ b/animeal/src/Flows/Profile/ProfileViewModel.swift @@ -6,7 +6,7 @@ import Common final class ProfileViewModel: ProfileViewModelProtocol { // MARK: - Private properties - private var viewItems: [ProfileViewItem] + private var viewItems: [ProfileViewItemProtocol] private var modelActions: [ProfileModelAction] // MARK: - Dependencies @@ -114,8 +114,14 @@ final class ProfileViewModel: ProfileViewModelProtocol { return handleTextEvent(textEvent) case let .changeDate(identifier, date): let text = dateFormatter.string(from: date) - Task { [weak self] in await self?.model.updateItem(text, forIdentifier: identifier) } + Task { [weak self] in await self?.model.updateItem(text, false, forIdentifier: identifier) } return ProfileViewText(caretOffset: text.count, formattedText: text) + case let .clickCheckBox(identifier, selected): + Task { [weak self] in + await self?.model.updateItem(nil, selected, forIdentifier: identifier) + await self?.validateItems() + } + return ProfileViewText(caretOffset: .zero, formattedText: nil) } } @@ -127,10 +133,10 @@ final class ProfileViewModel: ProfileViewModelProtocol { formattedText: text ) case let .didChange(identifier, text): - guard let formatter = viewItems.first(where: { $0.identifier == identifier })?.formatter + guard let formatter = viewItems.compactMap({ $0 as? ProfileTextFieldViewItem }).first(where: { $0.identifier == identifier })?.formatter else { Task { [weak self] in - await self?.model.updateItem(text, forIdentifier: identifier) + await self?.model.updateItem(text, false, forIdentifier: identifier) await self?.updateViewActions() } return ProfileViewText( @@ -140,7 +146,7 @@ final class ProfileViewModel: ProfileViewModelProtocol { } let unformattedText = formatter.unformat(text ?? .empty) Task { [weak self] in - await self?.model.updateItem(unformattedText, forIdentifier: identifier) + await self?.model.updateItem(unformattedText, false, forIdentifier: identifier) await self?.updateViewActions() } return ProfileViewText( @@ -148,7 +154,7 @@ final class ProfileViewModel: ProfileViewModelProtocol { formattedText: text ) case let .shouldChangeCharactersIn(identifier, text, range, replacementString): - guard let formatter = viewItems.first(where: { $0.identifier == identifier })?.formatter + guard let formatter = viewItems.compactMap({ $0 as? ProfileTextFieldViewItem }).first(where: { $0.identifier == identifier })?.formatter else { let text = (text ?? .empty) + replacementString return ProfileViewText( @@ -166,10 +172,10 @@ final class ProfileViewModel: ProfileViewModelProtocol { formattedText: result.formattedText ) case let .endEditing(identifier, text): - guard let formatter = viewItems.first(where: { $0.identifier == identifier })?.formatter + guard let formatter = viewItems.compactMap({ $0 as? ProfileTextFieldViewItem }).first(where: { $0.identifier == identifier })?.formatter else { Task { [weak self] in - await self?.model.updateItem(text, forIdentifier: identifier) + await self?.model.updateItem(text, false, forIdentifier: identifier) await self?.validateItems() } return ProfileViewText( @@ -179,7 +185,7 @@ final class ProfileViewModel: ProfileViewModelProtocol { } let unformattedText = formatter.unformat(text ?? .empty) Task { [weak self] in - await self?.model.updateItem(unformattedText, forIdentifier: identifier) + await self?.model.updateItem(unformattedText, false, forIdentifier: identifier) await self?.validateItems() } @@ -232,7 +238,7 @@ final class ProfileViewModel: ProfileViewModelProtocol { let completion: () -> Void = { Task { @MainActor [weak self] in guard let self else { return } - await self.model.updateItem(nil, forIdentifier: identifier) + await self.model.updateItem(nil, false, forIdentifier: identifier) self.updateViewItems(animated: false, resetPreviousItems: false) { [weak self] in try await self?.model.fetchCachedItems() ?? [] } diff --git a/animeal/src/Flows/Profile/View/Items/ProfileViewItem.swift b/animeal/src/Flows/Profile/View/Items/ProfileViewItem.swift index 8ca2099c..2e7d2e34 100644 --- a/animeal/src/Flows/Profile/View/Items/ProfileViewItem.swift +++ b/animeal/src/Flows/Profile/View/Items/ProfileViewItem.swift @@ -5,12 +5,29 @@ import Foundation import Common import UIComponents +protocol ProfileViewItemProtocol { + var identifier: String { get } + var type: ProfileItemType { get } + var state: ProfileItemState { get } + var isEditable: Bool { get } + var title: String { get } +} + +protocol ProfileViewTextFieldProtocol: ProfileViewItemProtocol { + var formatter: DefaultTextInputFormatter? { get } + var content: TextFieldContainerView.Model { get } +} + +protocol ProfileViewAgeConsentProtocol: ProfileViewItemProtocol { + var ageConsentModel: AgeConsentView.AgeConsentViewModel { get } +} + struct ProfileViewItemsSnapshot { let resetPreviousItems: Bool - let viewItems: [ProfileViewItem] + let viewItems: [ProfileViewItemProtocol] } -struct ProfileViewItem { +struct ProfileTextFieldViewItem: ProfileViewTextFieldProtocol { let identifier: String let type: ProfileItemType let state: ProfileItemState @@ -19,3 +36,12 @@ struct ProfileViewItem { let title: String let content: TextFieldContainerView.Model } + +struct ProfileAgeConsentViewItem: ProfileViewAgeConsentProtocol { + let identifier: String + let type: ProfileItemType + let state: ProfileItemState + let isEditable: Bool + let title: String + let ageConsentModel: AgeConsentView.AgeConsentViewModel +} diff --git a/animeal/src/Flows/Profile/View/Items/ProfileViewTextEvent.swift b/animeal/src/Flows/Profile/View/Items/ProfileViewTextEvent.swift index 50dc8b39..d4e23e29 100644 --- a/animeal/src/Flows/Profile/View/Items/ProfileViewTextEvent.swift +++ b/animeal/src/Flows/Profile/View/Items/ProfileViewTextEvent.swift @@ -10,4 +10,5 @@ enum ProfileViewTextEvent { enum ProfileViewItemEvent { case changeText(ProfileViewTextEvent) case changeDate(String, Date) + case clickCheckBox(String, Bool) } diff --git a/animeal/src/Flows/Profile/View/Utils/Extensions/ProfileViewItem+TextInputViewModel.swift b/animeal/src/Flows/Profile/View/Utils/Extensions/ProfileViewItem+TextInputViewModel.swift index 5afcc466..7d2c07b4 100644 --- a/animeal/src/Flows/Profile/View/Utils/Extensions/ProfileViewItem+TextInputViewModel.swift +++ b/animeal/src/Flows/Profile/View/Utils/Extensions/ProfileViewItem+TextInputViewModel.swift @@ -4,7 +4,7 @@ import UIKit // SDK import UIComponents -extension ProfileViewItem { +extension ProfileTextFieldViewItem { var phoneModel: PhoneInputView.Model { switch state { case .normal: diff --git a/animeal/src/Flows/Profile/View/Utils/Mappers/ProfileViewItemMapper.swift b/animeal/src/Flows/Profile/View/Utils/Mappers/ProfileViewItemMapper.swift index e0facddc..13f5b084 100644 --- a/animeal/src/Flows/Profile/View/Utils/Mappers/ProfileViewItemMapper.swift +++ b/animeal/src/Flows/Profile/View/Utils/Mappers/ProfileViewItemMapper.swift @@ -8,16 +8,16 @@ import Style // sourcery: AutoMockable protocol ProfileViewItemMappable { - func mapItem(_ input: ProfileModelItem) -> ProfileViewItem - func mapItems(_ input: [ProfileModelItem]) -> [ProfileViewItem] + func mapItem(_ input: ProfileModelItem) -> ProfileViewItemProtocol + func mapItems(_ input: [ProfileModelItem]) -> [ProfileViewItemProtocol] } struct ProfileViewItemMapper: ProfileViewItemMappable { - func mapItem(_ input: ProfileModelItem) -> ProfileViewItem { + func mapItem(_ input: ProfileModelItem) -> ProfileViewItemProtocol { switch input.type { case .phone(let region): guard let placeholder = region.phoneNumberPlaceholder else { - let viewItem = ProfileViewItem( + let viewItem = ProfileTextFieldViewItem( identifier: input.identifier, type: input.type, state: input.state, @@ -37,7 +37,7 @@ struct ProfileViewItemMapper: ProfileViewItemMappable { let formatter = DefaultTextInputFormatter.phoneNumberFormatter( placeholder ) - let viewItem = ProfileViewItem( + let viewItem = ProfileTextFieldViewItem( identifier: input.identifier, type: input.type, state: input.state, @@ -54,30 +54,18 @@ struct ProfileViewItemMapper: ProfileViewItemMappable { ) return viewItem case .birthday: - let viewItem = ProfileViewItem( + let state: CheckBoxState = input.selected ? .checked : .unchecked + let viewItem = ProfileAgeConsentViewItem( identifier: input.identifier, type: input.type, state: input.state, - formatter: nil, isEditable: input.isEditable, title: input.type.title, - content: DateTextContentView.Model( - placeholder: input.type.title, - text: input.text, - date: input.date, - isEditable: input.isEditable, - rightActions: [ - .init( - identifier: UUID().uuidString, - icon: Asset.Images.calendar.image, - action: nil - ) - ] - ) + ageConsentModel: AgeConsentView.AgeConsentViewModel(state: state, title: L10n.Profile.consent) ) return viewItem default: - let viewItem = ProfileViewItem( + let viewItem = ProfileTextFieldViewItem( identifier: input.identifier, type: input.type, state: input.state, @@ -94,7 +82,7 @@ struct ProfileViewItemMapper: ProfileViewItemMappable { } } - func mapItems(_ input: [ProfileModelItem]) -> [ProfileViewItem] { + func mapItems(_ input: [ProfileModelItem]) -> [ProfileViewItemProtocol] { return input.map(mapItem) } } diff --git a/animealTests/Sourcery/Generated/AutoMockable.generated.swift b/animealTests/Sourcery/Generated/AutoMockable.generated.swift index fd5a89c9..c6bc1000 100644 --- a/animealTests/Sourcery/Generated/AutoMockable.generated.swift +++ b/animealTests/Sourcery/Generated/AutoMockable.generated.swift @@ -1332,10 +1332,10 @@ class ProfileViewItemMappableMock: ProfileViewItemMappable { } var mapItemReceivedInput: ProfileModelItem? var mapItemReceivedInvocations: [ProfileModelItem] = [] - var mapItemReturnValue: ProfileViewItem! - var mapItemClosure: ((ProfileModelItem) -> ProfileViewItem)? + var mapItemReturnValue: ProfileViewItemProtocol! + var mapItemClosure: ((ProfileModelItem) -> ProfileViewItemProtocol)? - func mapItem(_ input: ProfileModelItem) -> ProfileViewItem { + func mapItem(_ input: ProfileModelItem) -> ProfileViewItemProtocol { mapItemCallsCount += 1 mapItemReceivedInput = input mapItemReceivedInvocations.append(input) @@ -1354,10 +1354,10 @@ class ProfileViewItemMappableMock: ProfileViewItemMappable { } var mapItemsReceivedInput: [ProfileModelItem]? var mapItemsReceivedInvocations: [[ProfileModelItem]] = [] - var mapItemsReturnValue: [ProfileViewItem]! - var mapItemsClosure: (([ProfileModelItem]) -> [ProfileViewItem])? + var mapItemsReturnValue: [ProfileViewItemProtocol]! + var mapItemsClosure: (([ProfileModelItem]) -> [ProfileViewItemProtocol])? - func mapItems(_ input: [ProfileModelItem]) -> [ProfileViewItem] { + func mapItems(_ input: [ProfileModelItem]) -> [ProfileViewItemProtocol] { mapItemsCallsCount += 1 mapItemsReceivedInput = input mapItemsReceivedInvocations.append(input)