diff --git a/BaseStyle/BaseStyle.xcodeproj/project.pbxproj b/BaseStyle/BaseStyle.xcodeproj/project.pbxproj index ab254dc13..6f9a37e6b 100644 --- a/BaseStyle/BaseStyle.xcodeproj/project.pbxproj +++ b/BaseStyle/BaseStyle.xcodeproj/project.pbxproj @@ -25,11 +25,9 @@ D82C65552C46886E009947DC /* Roboto-Bold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = D82C65502C46886E009947DC /* Roboto-Bold.ttf */; }; D82C65562C46886E009947DC /* Roboto-Medium.ttf in Resources */ = {isa = PBXBuildFile; fileRef = D82C65512C46886E009947DC /* Roboto-Medium.ttf */; }; D8302D9C2B9EE1D2005ACA13 /* PrimaryFloatingButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8302D9B2B9EE1D2005ACA13 /* PrimaryFloatingButton.swift */; }; - D86632962C2410BB009D3EF5 /* OtpTextInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D86632952C2410BB009D3EF5 /* OtpTextInputView.swift */; }; D887213F2B99992A009DC5BE /* LoaderViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D887213E2B99992A009DC5BE /* LoaderViewModel.swift */; }; D89C933F2BC3C0F800FACD16 /* ForwardIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = D89C933E2BC3C0F800FACD16 /* ForwardIcon.swift */; }; D89C93462BC42DE500FACD16 /* MailComposeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D89C93452BC42DE500FACD16 /* MailComposeView.swift */; }; - D89DBE2D2B88828300E5F1BD /* Countries.json in Resources */ = {isa = PBXBuildFile; fileRef = D89DBE2C2B88828300E5F1BD /* Countries.json */; }; D89DBE332B888F2D00E5F1BD /* SearchBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = D89DBE322B888F2D00E5F1BD /* SearchBar.swift */; }; D89DBE352B88A05F00E5F1BD /* UIApplication+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = D89DBE342B88A05F00E5F1BD /* UIApplication+Extension.swift */; }; D89DBE3E2B8C67CE00E5F1BD /* String+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = D89DBE3D2B8C67CE00E5F1BD /* String+Extension.swift */; }; @@ -90,11 +88,9 @@ D82C65502C46886E009947DC /* Roboto-Bold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Roboto-Bold.ttf"; sourceTree = ""; }; D82C65512C46886E009947DC /* Roboto-Medium.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Roboto-Medium.ttf"; sourceTree = ""; }; D8302D9B2B9EE1D2005ACA13 /* PrimaryFloatingButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrimaryFloatingButton.swift; sourceTree = ""; }; - D86632952C2410BB009D3EF5 /* OtpTextInputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OtpTextInputView.swift; sourceTree = ""; }; D887213E2B99992A009DC5BE /* LoaderViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = LoaderViewModel.swift; path = BaseStyle/Views/LoaderViewModel.swift; sourceTree = SOURCE_ROOT; }; D89C933E2BC3C0F800FACD16 /* ForwardIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForwardIcon.swift; sourceTree = ""; }; D89C93452BC42DE500FACD16 /* MailComposeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MailComposeView.swift; sourceTree = ""; }; - D89DBE2C2B88828300E5F1BD /* Countries.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = Countries.json; sourceTree = ""; }; D89DBE322B888F2D00E5F1BD /* SearchBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchBar.swift; sourceTree = ""; }; D89DBE342B88A05F00E5F1BD /* UIApplication+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIApplication+Extension.swift"; sourceTree = ""; }; D89DBE3D2B8C67CE00E5F1BD /* String+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Extension.swift"; sourceTree = ""; }; @@ -233,7 +229,6 @@ children = ( D8D42A562B85CD42009B345D /* BaseAssets.xcassets */, D8D42A892B85D525009B345D /* AppColors.swift */, - D89DBE2C2B88828300E5F1BD /* Countries.json */, D8D42A582B85CDB2009B345D /* Fonts */, ); path = Resource; @@ -309,7 +304,6 @@ D8D42AAF2B872E44009B345D /* LoaderView.swift */, D8D14A4F2BA090F000F45FF2 /* ShareSheetView.swift */, D89C93452BC42DE500FACD16 /* MailComposeView.swift */, - D86632952C2410BB009D3EF5 /* OtpTextInputView.swift */, D82174BD2BBAD86D00DB42C3 /* ProfileImageView.swift */, D8E244C02B986CD800C6C82A /* ImagePickerView.swift */, ); @@ -412,7 +406,6 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - D89DBE2D2B88828300E5F1BD /* Countries.json in Resources */, D82C65532C46886E009947DC /* Lato-Medium.ttf in Resources */, D82C65552C46886E009947DC /* Roboto-Bold.ttf in Resources */, D82C65562C46886E009947DC /* Roboto-Medium.ttf in Resources */, @@ -526,7 +519,6 @@ 21D26E232CA199630090488B /* CapsuleButton.swift in Sources */, D89C93462BC42DE500FACD16 /* MailComposeView.swift in Sources */, 213A1F2A2C52335D00BF9800 /* CheckmarkButton.swift in Sources */, - D86632962C2410BB009D3EF5 /* OtpTextInputView.swift in Sources */, D8E244C12B986CD800C6C82A /* ImagePickerView.swift in Sources */, D8D42A862B85D08F009B345D /* Font+Extension.swift in Sources */, D8EB0ED82CAD8C9F00AC6A44 /* ErrorView.swift in Sources */, diff --git a/BaseStyle/BaseStyle/CustomUI/Buttons/PrimaryFloatingButton.swift b/BaseStyle/BaseStyle/CustomUI/Buttons/PrimaryFloatingButton.swift index 30ae25822..c806fae32 100644 --- a/BaseStyle/BaseStyle/CustomUI/Buttons/PrimaryFloatingButton.swift +++ b/BaseStyle/BaseStyle/CustomUI/Buttons/PrimaryFloatingButton.swift @@ -11,13 +11,19 @@ public struct PrimaryFloatingButton: View { private let text: String private let bottomPadding: CGFloat + private var bgColor: Color + private var textColor: Color private let isEnabled: Bool private let showLoader: Bool private let onClick: (() -> Void)? - public init(text: String, bottomPadding: CGFloat = 24, isEnabled: Bool = true, showLoader: Bool = false, onClick: (() -> Void)? = nil) { + public init(text: String, bottomPadding: CGFloat = 24, textColor: Color = primaryLightText, + bgColor: Color = primaryColor, isEnabled: Bool = true, + showLoader: Bool = false, onClick: (() -> Void)? = nil) { self.text = text self.bottomPadding = bottomPadding + self.textColor = textColor + self.bgColor = bgColor self.isEnabled = isEnabled self.showLoader = showLoader self.onClick = onClick @@ -27,7 +33,9 @@ public struct PrimaryFloatingButton: View { VStack(alignment: .center, spacing: 0) { VSpacer(10) - PrimaryButton(text: text, isEnabled: !showLoader && isEnabled, showLoader: showLoader, onClick: onClick) + PrimaryButton(text: text, textColor: textColor, bgColor: bgColor, + isEnabled: !showLoader && isEnabled, + showLoader: showLoader, onClick: onClick) } .padding(.horizontal, 16) .padding(.bottom, bottomPadding) diff --git a/BaseStyle/BaseStyle/CustomUI/OtpTextInputView.swift b/BaseStyle/BaseStyle/CustomUI/OtpTextInputView.swift deleted file mode 100644 index e67cd94e1..000000000 --- a/BaseStyle/BaseStyle/CustomUI/OtpTextInputView.swift +++ /dev/null @@ -1,61 +0,0 @@ -// -// OtpTextInputView.swift -// BaseStyle -// -// Created by Amisha Italiya on 20/06/24. -// - -import SwiftUI - -public struct OtpTextInputView: View { - - private let OTP_TOTAL_CHARACTERS = 6 - - @Binding var text: String - - let placeholder: String - let isFocused: FocusState.Binding - let keyboardType: UIKeyboardType - let alignment: TextAlignment - - var onOtpVerify: (() -> Void)? - - public init(text: Binding, placeholder: String = "", isFocused: FocusState.Binding, - keyboardType: UIKeyboardType = .numberPad, alignment: TextAlignment = .center, - onOtpVerify: ( () -> Void)? = nil) { - self._text = text - self.placeholder = placeholder - self.isFocused = isFocused - self.keyboardType = keyboardType - self.alignment = alignment - self.onOtpVerify = onOtpVerify - } - - public var body: some View { - TextField(placeholder.localized, text: $text) - .kerning(16) - .focused(isFocused) - .tint(primaryColor) - .font(.Header2()) - .keyboardType(keyboardType) - .foregroundStyle(primaryText) - .multilineTextAlignment(alignment) - .textContentType(.oneTimeCode) - .autocorrectionDisabled() - .onChange(of: text) { newValue in - if newValue.count == OTP_TOTAL_CHARACTERS { - onOtpVerify?() - UIApplication.shared.endEditing() - } - } - .textInputAutocapitalization(.never) - .onAppear { - if text.isEmpty { - isFocused.wrappedValue = true - } else { - isFocused.wrappedValue = false - UIApplication.shared.endEditing() - } - } - } -} diff --git a/BaseStyle/BaseStyle/Extension/String+Extension.swift b/BaseStyle/BaseStyle/Extension/String+Extension.swift index c2e196c3b..cf1f64f7b 100644 --- a/BaseStyle/BaseStyle/Extension/String+Extension.swift +++ b/BaseStyle/BaseStyle/Extension/String+Extension.swift @@ -21,10 +21,6 @@ extension String { return emailPred.evaluate(with: self) } - public func getNumbersOnly() -> String { - self.filter("0123456789".contains) - } - public func randomString(length: Int) -> String { let letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" return String((0.. AppUser { - if let storedUser = try await store.fetchUserBy(id: user.id) { - return storedUser + if var fetchedUser = try await store.fetchUserBy(id: user.id) { + LogD("UserRepository: \(#function) User already exists in Firestore.") + if !fetchedUser.isActive { + fetchedUser.isActive = true + return try await updateUser(user: fetchedUser) + } + return fetchedUser } else { - _ = try await store.addUser(user: user) + LogD("UserRepository: \(#function) User does not exist. Adding new user.") + try await store.addUser(user: user) return user } } diff --git a/Data/Data/Router/AppRoute.swift b/Data/Data/Router/AppRoute.swift index 16c5eafc7..f0ee5c4df 100644 --- a/Data/Data/Router/AppRoute.swift +++ b/Data/Data/Router/AppRoute.swift @@ -15,8 +15,7 @@ public enum AppRoute: Hashable { case OnboardView case LoginView - case PhoneLoginView - case VerifyOTPView(phoneNumber: String, dialCode: String, verificationId: String) + case EmailLoginView(onDismiss: (() -> Void)? = nil) case ProfileView case HomeView @@ -44,7 +43,7 @@ public enum AppRoute: Hashable { case ChooseMultiplePayerView(groupId: String, selectedPayers: [String: Double], amount: Double, onPayerSelection: (([String: Double]) -> Void)) // MARK: - Activity Tab - case ActivityLogView + case ActivityHomeView // MARK: - Account Tab case AccountHomeView @@ -55,10 +54,8 @@ public enum AppRoute: Hashable { "onboardView" case .LoginView: "loginView" - case .PhoneLoginView: - "phoneLoginView" - case .VerifyOTPView: - "verifyOTPView" + case .EmailLoginView: + "EmailLoginView" case .ProfileView: "userProfileView" case .HomeView: @@ -67,8 +64,8 @@ public enum AppRoute: Hashable { case .FriendsHomeView: "friendsHomeView" - case .ActivityLogView: - "activityLogView" + case .ActivityHomeView: + "activityHomeView" case .ExpenseDetailView: "expenseDetailView" diff --git a/Data/Data/Store/UserStore.swift b/Data/Data/Store/UserStore.swift index 1102ec85b..06b308aa7 100644 --- a/Data/Data/Store/UserStore.swift +++ b/Data/Data/Store/UserStore.swift @@ -34,7 +34,15 @@ class UserStore: ObservableObject { func fetchUserBy(id: String) async throws -> AppUser? { let snapshot = try await usersCollection.document(id).getDocument(source: .server) - return try snapshot.data(as: AppUser.self) + + if snapshot.exists { + let fetchedUser = try snapshot.data(as: AppUser.self) + LogD("UserStore: \(#function) User fetched successfully.") + return fetchedUser + } else { + LogE("UserStore: \(#function) snapshot is nil for requested user.") + return nil + } } func fetchLatestUserBy(id: String, completion: @escaping (AppUser?) -> Void) { diff --git a/Splito.xcodeproj/project.pbxproj b/Splito.xcodeproj/project.pbxproj index 7b1fe2bd2..6ff85bb49 100644 --- a/Splito.xcodeproj/project.pbxproj +++ b/Splito.xcodeproj/project.pbxproj @@ -27,6 +27,8 @@ 21BA6D4A2C3BB63B0020ED04 /* ChooseMultiplePayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21BA6D492C3BB63B0020ED04 /* ChooseMultiplePayerView.swift */; }; 21BA6D4D2C3BB6AD0020ED04 /* ChooseMultiplePayerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21BA6D4C2C3BB6AD0020ED04 /* ChooseMultiplePayerViewModel.swift */; }; 21BA6D4F2C3BBB1B0020ED04 /* ChoosePayerRouteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21BA6D4E2C3BBB1B0020ED04 /* ChoosePayerRouteView.swift */; }; + 21D8CF3F2CFF080300463E4D /* EmailLoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21D8CF3E2CFF080300463E4D /* EmailLoginView.swift */; }; + 21D8CF412CFF080F00463E4D /* EmailLoginViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21D8CF402CFF080F00463E4D /* EmailLoginViewModel.swift */; }; 21F27BDE2C36768D00196D62 /* ExpenseSplitOptionsTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21F27BDD2C36768D00196D62 /* ExpenseSplitOptionsTabView.swift */; }; 741540F86E36400CE27B1FAD /* Pods_SplitoTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 701193A10871F36C3EDB356C /* Pods_SplitoTests.framework */; }; D826C0E22BDBD65600AAA449 /* GroupBalancesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D826C0E12BDBD65600AAA449 /* GroupBalancesView.swift */; }; @@ -68,10 +70,6 @@ D89C933B2BC0204100FACD16 /* AccountHomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D89C933A2BC0204100FACD16 /* AccountHomeView.swift */; }; D89C933D2BC020B200FACD16 /* AccountHomeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D89C933C2BC020B200FACD16 /* AccountHomeViewModel.swift */; }; D89DBE1F2B87327400E5F1BD /* SignInWithAppleDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D89DBE1E2B87327400E5F1BD /* SignInWithAppleDelegate.swift */; }; - D89DBE232B875C2200E5F1BD /* PhoneLoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D89DBE222B875C2200E5F1BD /* PhoneLoginView.swift */; }; - D89DBE252B875C3000E5F1BD /* PhoneLoginViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D89DBE242B875C3000E5F1BD /* PhoneLoginViewModel.swift */; }; - D89DBE382B88A6A800E5F1BD /* VerifyOtpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D89DBE372B88A6A800E5F1BD /* VerifyOtpView.swift */; }; - D89DBE3A2B88A6B400E5F1BD /* VerifyOtpViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D89DBE392B88A6B400E5F1BD /* VerifyOtpViewModel.swift */; }; D89DBE422B8CA72700E5F1BD /* HomeRouteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D89DBE412B8CA72700E5F1BD /* HomeRouteView.swift */; }; D89DBE5B2B8DE97000E5F1BD /* GroupRouteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D89DBE5A2B8DE97000E5F1BD /* GroupRouteView.swift */; }; D89DBE5F2B8DE98600E5F1BD /* AccountRouteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D89DBE5E2B8DE98600E5F1BD /* AccountRouteView.swift */; }; @@ -115,7 +113,6 @@ D8E244BB2B9843A100C6C82A /* CreateGroupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8E244BA2B9843A100C6C82A /* CreateGroupView.swift */; }; D8E244BD2B98444900C6C82A /* CreateGroupViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8E244BC2B98444900C6C82A /* CreateGroupViewModel.swift */; }; D8E244BF2B98592C00C6C82A /* GroupHomeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8E244BE2B98592C00C6C82A /* GroupHomeViewModel.swift */; }; - D8EB0ED52CAD884A00AC6A44 /* AppRoute.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8EB0ED32CAD884A00AC6A44 /* AppRoute.swift */; }; D8EB0ED62CAD884A00AC6A44 /* RouterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8EB0ED22CAD884A00AC6A44 /* RouterView.swift */; }; E2D6A34174334B81314FAF12 /* Pods_Splito.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 43FFDB1561C565EE6E3DC86A /* Pods_Splito.framework */; }; /* End PBXBuildFile section */ @@ -174,6 +171,8 @@ 21BA6D492C3BB63B0020ED04 /* ChooseMultiplePayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChooseMultiplePayerView.swift; sourceTree = ""; }; 21BA6D4C2C3BB6AD0020ED04 /* ChooseMultiplePayerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChooseMultiplePayerViewModel.swift; sourceTree = ""; }; 21BA6D4E2C3BBB1B0020ED04 /* ChoosePayerRouteView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChoosePayerRouteView.swift; sourceTree = ""; }; + 21D8CF3E2CFF080300463E4D /* EmailLoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmailLoginView.swift; sourceTree = ""; }; + 21D8CF402CFF080F00463E4D /* EmailLoginViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmailLoginViewModel.swift; sourceTree = ""; }; 21F27BDD2C36768D00196D62 /* ExpenseSplitOptionsTabView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpenseSplitOptionsTabView.swift; sourceTree = ""; }; 43FFDB1561C565EE6E3DC86A /* Pods_Splito.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Splito.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 44312D517F674E980CBAE838 /* Pods-Splito.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Splito.debug.xcconfig"; path = "Target Support Files/Pods-Splito/Pods-Splito.debug.xcconfig"; sourceTree = ""; }; @@ -229,10 +228,6 @@ D89C933C2BC020B200FACD16 /* AccountHomeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountHomeViewModel.swift; sourceTree = ""; }; D89DBE1E2B87327400E5F1BD /* SignInWithAppleDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInWithAppleDelegate.swift; sourceTree = ""; }; D89DBE202B87433B00E5F1BD /* Splito.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Splito.entitlements; sourceTree = ""; }; - D89DBE222B875C2200E5F1BD /* PhoneLoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhoneLoginView.swift; sourceTree = ""; }; - D89DBE242B875C3000E5F1BD /* PhoneLoginViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhoneLoginViewModel.swift; sourceTree = ""; }; - D89DBE372B88A6A800E5F1BD /* VerifyOtpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VerifyOtpView.swift; sourceTree = ""; }; - D89DBE392B88A6B400E5F1BD /* VerifyOtpViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VerifyOtpViewModel.swift; sourceTree = ""; }; D89DBE412B8CA72700E5F1BD /* HomeRouteView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeRouteView.swift; sourceTree = ""; }; D89DBE5A2B8DE97000E5F1BD /* GroupRouteView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupRouteView.swift; sourceTree = ""; }; D89DBE5E2B8DE98600E5F1BD /* AccountRouteView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountRouteView.swift; sourceTree = ""; }; @@ -276,7 +271,6 @@ D8E244BC2B98444900C6C82A /* CreateGroupViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateGroupViewModel.swift; sourceTree = ""; }; D8E244BE2B98592C00C6C82A /* GroupHomeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupHomeViewModel.swift; sourceTree = ""; }; D8EB0ED22CAD884A00AC6A44 /* RouterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RouterView.swift; sourceTree = ""; }; - D8EB0ED32CAD884A00AC6A44 /* AppRoute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppRoute.swift; sourceTree = ""; }; DFDBE222145831CBD9B686FF /* Pods-SplitoTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SplitoTests.release.xcconfig"; path = "Target Support Files/Pods-SplitoTests/Pods-SplitoTests.release.xcconfig"; sourceTree = ""; }; F76BDF4C42A2EF9A9F45B069 /* Pods-Splito-SplitoUITests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Splito-SplitoUITests.debug.xcconfig"; path = "Target Support Files/Pods-Splito-SplitoUITests/Pods-Splito-SplitoUITests.debug.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -362,6 +356,15 @@ path = Payer; sourceTree = ""; }; + 21D8CF3D2CFF07E800463E4D /* EmailLogin */ = { + isa = PBXGroup; + children = ( + 21D8CF3E2CFF080300463E4D /* EmailLoginView.swift */, + 21D8CF402CFF080F00463E4D /* EmailLoginViewModel.swift */, + ); + path = EmailLogin; + sourceTree = ""; + }; 7530D0A01F0D8D2C363B2A6E /* Frameworks */ = { isa = PBXGroup; children = ( @@ -615,25 +618,6 @@ path = Resource; sourceTree = ""; }; - D89DBE212B875C1000E5F1BD /* PhoneLogin */ = { - isa = PBXGroup; - children = ( - D89DBE222B875C2200E5F1BD /* PhoneLoginView.swift */, - D89DBE242B875C3000E5F1BD /* PhoneLoginViewModel.swift */, - D89DBE362B88A69E00E5F1BD /* VerifyOtp */, - ); - path = PhoneLogin; - sourceTree = ""; - }; - D89DBE362B88A69E00E5F1BD /* VerifyOtp */ = { - isa = PBXGroup; - children = ( - D89DBE372B88A6A800E5F1BD /* VerifyOtpView.swift */, - D89DBE392B88A6B400E5F1BD /* VerifyOtpViewModel.swift */, - ); - path = VerifyOtp; - sourceTree = ""; - }; D89DBE552B8DE8C600E5F1BD /* Groups */ = { isa = PBXGroup; children = ( @@ -736,7 +720,7 @@ D8AC26ED2B84B12800CEAAD3 /* LoginView.swift */, D8AC26EC2B84B12800CEAAD3 /* LoginViewModel.swift */, D89DBE1E2B87327400E5F1BD /* SignInWithAppleDelegate.swift */, - D89DBE212B875C1000E5F1BD /* PhoneLogin */, + 21D8CF3D2CFF07E800463E4D /* EmailLogin */, ); path = Login; sourceTree = ""; @@ -766,7 +750,6 @@ isa = PBXGroup; children = ( D8EB0ED22CAD884A00AC6A44 /* RouterView.swift */, - D8EB0ED32CAD884A00AC6A44 /* AppRoute.swift */, ); path = Router; sourceTree = ""; @@ -1064,6 +1047,7 @@ D83344612C0EF1F100CD9F05 /* GroupTotalsView.swift in Sources */, D88721472B9B2C97009DC5BE /* GroupListViewModel.swift in Sources */, D8D14A622BA2DCE700F45FF2 /* UserProfileViewModel.swift in Sources */, + 21D8CF412CFF080F00463E4D /* EmailLoginViewModel.swift in Sources */, 2177692B2C203160009B3B37 /* GroupTransactionDetailView.swift in Sources */, D8A7CA6E2BA483F60014EC67 /* GroupHomeView.swift in Sources */, D85E86ED2BB41B87002EDF76 /* SelectGroupView.swift in Sources */, @@ -1082,7 +1066,6 @@ D8D6C3A02B79F8110023CF08 /* AppDelegate.swift in Sources */, D8AC26F42B84B12800CEAAD3 /* OnboardViewModel.swift in Sources */, D826C0E42BDBD66300AAA449 /* GroupBalancesViewModel.swift in Sources */, - D89DBE3A2B88A6B400E5F1BD /* VerifyOtpViewModel.swift in Sources */, D89DBE5F2B8DE98600E5F1BD /* AccountRouteView.swift in Sources */, D8CD952C2BD65F1900407B47 /* ExpenseSplitOptionsView.swift in Sources */, 21559CAC2CBD25B30039F127 /* ActivityLogRouteView.swift in Sources */, @@ -1112,17 +1095,14 @@ D89DBE5B2B8DE97000E5F1BD /* GroupRouteView.swift in Sources */, D8AC27152B84B73000CEAAD3 /* PageControl.swift in Sources */, D8E244BB2B9843A100C6C82A /* CreateGroupView.swift in Sources */, - D89DBE382B88A6A800E5F1BD /* VerifyOtpView.swift in Sources */, D89DBE422B8CA72700E5F1BD /* HomeRouteView.swift in Sources */, 21B1C09E2C1C5AA30098B4FD /* GroupExpenseListView.swift in Sources */, - D89DBE252B875C3000E5F1BD /* PhoneLoginViewModel.swift in Sources */, D8D14A602BA2DCDB00F45FF2 /* UserProfileView.swift in Sources */, + 21D8CF3F2CFF080300463E4D /* EmailLoginView.swift in Sources */, D88721452B9B2C78009DC5BE /* GroupListView.swift in Sources */, D86BB33F2C05ED9D00463E6C /* MainRouteViewModel.swift in Sources */, D8D14A582BA189F800F45FF2 /* JoinMemberViewModel.swift in Sources */, - D8EB0ED52CAD884A00AC6A44 /* AppRoute.swift in Sources */, D8EB0ED62CAD884A00AC6A44 /* RouterView.swift in Sources */, - D89DBE232B875C2200E5F1BD /* PhoneLoginView.swift in Sources */, 21F27BDE2C36768D00196D62 /* ExpenseSplitOptionsTabView.swift in Sources */, D85E86E92BB3FD49002EDF76 /* ChoosePayerView.swift in Sources */, D833446E2C0F2D4C00CD9F05 /* GroupWhoGettingPaidViewModel.swift in Sources */, diff --git a/Splito/Localization/Localizable.xcstrings b/Splito/Localization/Localizable.xcstrings index 948ac5d07..f6dfacca8 100644 --- a/Splito/Localization/Localizable.xcstrings +++ b/Splito/Localization/Localizable.xcstrings @@ -12,12 +12,6 @@ }, " ₹ 0.00" : { - }, - " and " : { - - }, - " Enter mobile number" : { - }, " from the group" : { "extractionState" : "manual" @@ -134,21 +128,21 @@ }, "0" : { - }, - "00:%@" : { - }, "1.23" : { "extractionState" : "manual" - }, - "6 digits" : { - }, "About" : { }, "Account" : { + }, + "Account Disabled" : { + "extractionState" : "manual" + }, + "Account Not Found" : { + "extractionState" : "manual" }, "Acknowledgements" : { "extractionState" : "manual" @@ -205,6 +199,9 @@ "Amount" : { "extractionState" : "manual" }, + "An email has been sent to %@ with instructions to reset your password." : { + "extractionState" : "manual" + }, "Ana borrows $10 from Bob" : { }, @@ -252,9 +249,6 @@ }, "Bob borrows $10 from Charlie" : { - }, - "By entering your number, you’re agreeing to our " : { - }, "Camera access is required to take picture for expenses" : { "extractionState" : "manual" @@ -283,8 +277,11 @@ "Contact Us" : { "extractionState" : "manual" }, - "Countries" : { - + "Continue your journey" : { + "extractionState" : "manual" + }, + "Create account" : { + "extractionState" : "manual" }, "Create group" : { @@ -318,10 +315,19 @@ }, "Edit expense" : { + }, + "email" : { + "extractionState" : "manual" }, "Email" : { "extractionState" : "manual" }, + "Email Already in Use" : { + "extractionState" : "manual" + }, + "Email sent" : { + "extractionState" : "manual" + }, "Email sent successfully!" : { "extractionState" : "manual" }, @@ -331,9 +337,6 @@ "Enter a reason for this payment" : { "extractionState" : "manual" }, - "Enter a valid phone number." : { - "extractionState" : "manual" - }, "Enter code" : { "extractionState" : "manual" }, @@ -345,6 +348,9 @@ }, "Enter the percentage split that's fair for your situation." : { "extractionState" : "manual" + }, + "Enter your email" : { + }, "Enter your email address" : { "extractionState" : "manual" @@ -358,10 +364,10 @@ "Enter your note here..." : { "extractionState" : "manual" }, - "Enter your phone number" : { - "extractionState" : "manual" + "Enter your password" : { + }, - "Entered number is too long, Please check the phone number." : { + "Enter your phone number" : { "extractionState" : "manual" }, "equally" : { @@ -385,10 +391,10 @@ "First Name" : { "extractionState" : "manual" }, - "get back" : { - "extractionState" : "manual" + "Forgot password?" : { + }, - "Get OTP" : { + "get back" : { "extractionState" : "manual" }, "Get Started" : { @@ -436,7 +442,10 @@ "In a group with \"simplify debts\" enabled, Splito will tell Ana to repay Charlie $10. Bob does nothing. This is the most efficient way for the group to settle up. With simplify debts enabled, it's normal to owe someone who didn't directly loan you money." : { }, - "Invalid OTP" : { + "Incorrect email or password" : { + "extractionState" : "manual" + }, + "Invalid Email" : { "extractionState" : "manual" }, "It seems that everything has settled down in all groups." : { @@ -444,6 +453,9 @@ }, "Join group" : { + }, + "Key" : { + "extractionState" : "manual" }, "Last Name" : { "extractionState" : "manual" @@ -478,6 +490,9 @@ "Let's split the expense! Use invite code %@ to join the %@ group, don't have an app then please download it." : { "extractionState" : "manual" }, + "Login" : { + "extractionState" : "manual" + }, "Looks like there are no outstanding settlements in any of your groups yet." : { }, @@ -492,6 +507,9 @@ }, "Multiple people" : { + }, + "No account found with the provided email address. Please sign up." : { + "extractionState" : "manual" }, "No activity yet!" : { @@ -562,6 +580,15 @@ "paid" : { "extractionState" : "manual" }, + "password" : { + "extractionState" : "manual" + }, + "Password" : { + + }, + "Password must be at least 6 characters long." : { + "extractionState" : "manual" + }, "Payment deleted successfully." : { "extractionState" : "manual" }, @@ -586,6 +613,9 @@ "Please enter a cost for your expense first!" : { "extractionState" : "manual" }, + "Please enter a valid email address." : { + "extractionState" : "manual" + }, "Please enter valid email" : { "extractionState" : "manual" }, @@ -598,17 +628,11 @@ "Please select group to get payer list." : { "extractionState" : "manual" }, - "Please, enter a valid OTP code." : { - "extractionState" : "manual" - }, "Privacy Policy" : { "extractionState" : "manual" }, "Privacy policy cannot be accessed." : { "extractionState" : "manual" - }, - "privacy policy." : { - }, "Rate Splito" : { "extractionState" : "manual" @@ -640,19 +664,6 @@ "requires recent authentication" : { "extractionState" : "manual" }, - "Resend code" : { - - }, - "Resend code " : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Resend code " - } - } - } - }, "Restore expense" : { "extractionState" : "manual" }, @@ -674,9 +685,6 @@ "Retry" : { "extractionState" : "manual" }, - "Search" : { - "extractionState" : "manual" - }, "Search expenses" : { "extractionState" : "manual" }, @@ -719,13 +727,16 @@ "short" : { "extractionState" : "manual" }, + "Sign in to access your account and enjoy all its features." : { + "extractionState" : "manual" + }, "Sign in with Apple" : { "extractionState" : "manual" }, - "Sign in with Google" : { + "Sign in with Email" : { "extractionState" : "manual" }, - "Sign in with Phone Number" : { + "Sign in with Google" : { "extractionState" : "manual" }, "Sign Out" : { @@ -778,9 +789,6 @@ }, "Take a picture" : { - }, - "terms of service" : { - }, "The amounts do not add up to the total cost of %@. You are %@ by %@." : { "extractionState" : "manual" @@ -791,6 +799,15 @@ "The code you've entered is not exists." : { "extractionState" : "manual" }, + "The email address is already associated with an existing account. Please use a different email or log in to your existing account." : { + "extractionState" : "manual" + }, + "The email address is not valid. Please check and try again." : { + "extractionState" : "manual" + }, + "The email or password you entered is incorrect. Please try again." : { + "extractionState" : "manual" + }, "The group associated with this expense has been deleted, so it cannot be restored." : { "extractionState" : "manual" }, @@ -809,6 +826,9 @@ "There are no groups available to search." : { "extractionState" : "manual" }, + "This account has been disabled. Please contact support." : { + "extractionState" : "manual" + }, "This code will be active for 2 days." : { }, @@ -875,6 +895,9 @@ "Totals" : { "extractionState" : "manual" }, + "Unable to send a password reset email. Please try again later." : { + "extractionState" : "manual" + }, "Unequally" : { "extractionState" : "manual" }, @@ -887,24 +910,9 @@ "Use settle up to divide costs with your friends or colleagues. It's easy and ensures everyone pays or receives their fair share.\\n\\nLet's get started splitting bills easily with friends." : { "extractionState" : "manual" }, - "Verification code" : { - "extractionState" : "manual" - }, - "Verify" : { - "extractionState" : "manual" - }, "Warning" : { "extractionState" : "manual" }, - "We’ll verify your phone number with a verification code." : { - "extractionState" : "manual" - }, - "We’ve sent a verification code to your phone %@." : { - "extractionState" : "manual" - }, - "What’s your phone number?" : { - "extractionState" : "manual" - }, "Whoops!" : { "extractionState" : "manual" }, diff --git a/Splito/Resource/Assets.xcassets/Images/Login/EmailIcon.imageset/Contents.json b/Splito/Resource/Assets.xcassets/Images/Login/EmailIcon.imageset/Contents.json new file mode 100644 index 000000000..b5fc2a922 --- /dev/null +++ b/Splito/Resource/Assets.xcassets/Images/Login/EmailIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "email_svgrepo.com.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Splito/Resource/Assets.xcassets/Images/Login/EmailIcon.imageset/email_svgrepo.com.svg b/Splito/Resource/Assets.xcassets/Images/Login/EmailIcon.imageset/email_svgrepo.com.svg new file mode 100644 index 000000000..a4e3b0cbe --- /dev/null +++ b/Splito/Resource/Assets.xcassets/Images/Login/EmailIcon.imageset/email_svgrepo.com.svg @@ -0,0 +1,4 @@ + + + + diff --git a/Splito/Resource/Assets.xcassets/Images/Login/LoginAppLogo.imageset/Contents.json b/Splito/Resource/Assets.xcassets/Images/Login/LoginAppLogo.imageset/Contents.json index 8ac482c18..ad42a58df 100644 --- a/Splito/Resource/Assets.xcassets/Images/Login/LoginAppLogo.imageset/Contents.json +++ b/Splito/Resource/Assets.xcassets/Images/Login/LoginAppLogo.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "user.svg", + "filename" : "logo.svg", "idiom" : "universal" } ], diff --git a/Splito/Resource/Assets.xcassets/Images/Login/LoginAppLogo.imageset/logo.svg b/Splito/Resource/Assets.xcassets/Images/Login/LoginAppLogo.imageset/logo.svg new file mode 100644 index 000000000..510a1d9a7 --- /dev/null +++ b/Splito/Resource/Assets.xcassets/Images/Login/LoginAppLogo.imageset/logo.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/Splito/Resource/Assets.xcassets/Images/Login/LoginAppLogo.imageset/user.svg b/Splito/Resource/Assets.xcassets/Images/Login/LoginAppLogo.imageset/user.svg deleted file mode 100644 index c8201c3c0..000000000 --- a/Splito/Resource/Assets.xcassets/Images/Login/LoginAppLogo.imageset/user.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/Splito/UI/Home/Account/User Profile/UserProfileView.swift b/Splito/UI/Home/Account/User Profile/UserProfileView.swift index 3083d212f..f0eceeace 100644 --- a/Splito/UI/Home/Account/User Profile/UserProfileView.swift +++ b/Splito/UI/Home/Account/User Profile/UserProfileView.swift @@ -72,11 +72,6 @@ struct UserProfileView: View { ImagePickerView(cropOption: .square, sourceType: !viewModel.sourceTypeIsCamera ? .photoLibrary : .camera, image: $viewModel.profileImage, isPresented: $viewModel.showImagePicker) } - .sheet(isPresented: $viewModel.showOTPView) { - VerifyOtpView(viewModel: VerifyOtpViewModel(phoneNumber: viewModel.phoneNumber, - verificationId: viewModel.verificationId, - onLoginSuccess: viewModel.otpPublisher.send(_:))) - } } } @@ -130,17 +125,17 @@ private struct UserDetailList: View { } var isEmailDisable: Bool { - userLoginType == .Google + userLoginType == .Google || userLoginType == .Email } var body: some View { VStack(spacing: 24) { ForEach(profileOptions.indices, id: \.self) { index in UserDetailCell(titleText: titles[index], focused: $focusedField, - isDisabled: userLoginType == .Phone ? profileOptions[index].isDisabled : (profileOptions[index] == .email ? isEmailDisable : false), + isDisabled: (profileOptions[index] == .email ? isEmailDisable : false), placeholder: profileOptions[index].placeholder, subtitleText: profileOptions[index].subtitle, - validationEnabled: profileOptions[index].validationType == .email || profileOptions[index].validationType == .phone || profileOptions[index].validationType == .firstName, + validationEnabled: profileOptions[index].validationType == .email || profileOptions[index].validationType == .firstName, fieldType: profileOptions[index].fieldTypes, keyboardType: profileOptions[index].keyboardType, validationType: profileOptions[index].validationType, diff --git a/Splito/UI/Home/Account/User Profile/UserProfileViewModel.swift b/Splito/UI/Home/Account/User Profile/UserProfileViewModel.swift index e836173ef..523d97eb8 100644 --- a/Splito/UI/Home/Account/User Profile/UserProfileViewModel.swift +++ b/Splito/UI/Home/Account/User Profile/UserProfileViewModel.swift @@ -5,9 +5,7 @@ // Created by Amisha Italiya on 14/03/24. // -import SwiftUI import Data -import Combine import BaseStyle import AVFoundation import FirebaseAuth @@ -27,7 +25,7 @@ public class UserProfileViewModel: BaseViewModel, ObservableObject { @Published var lastName: String = "" @Published var email: String = "" @Published var phoneNumber: String = "" - @Published var userLoginType: LoginType = .Phone + @Published var userLoginType: LoginType = .Email @Published var profileImage: UIImage? @Published var profileImageUrl: String? @@ -39,17 +37,13 @@ public class UserProfileViewModel: BaseViewModel, ObservableObject { @Published var isOpenFromOnboard: Bool @Published var isSaveInProgress = false @Published var isDeleteInProgress = false - @Published var showOTPView = false - var verificationId = "" private var currentNonce: String = "" private lazy var appleSignInDelegates: SignInWithAppleDelegates! = nil private let router: Router? private var onDismiss: (() -> Void)? - var otpPublisher = PassthroughSubject() - init(router: Router?, isOpenFromOnboard: Bool, onDismiss: (() -> Void)?) { self.router = router self.onDismiss = onDismiss @@ -259,8 +253,8 @@ extension UserProfileViewModel { case .Google: handleGoogleLogin(completion: completion) - case .Phone: - handlePhoneLogin(completion: completion) + case .Email: + handleEmailLogin(completion: completion) } } @@ -308,50 +302,27 @@ extension UserProfileViewModel { } } - private func handlePhoneLogin(completion: @escaping (AuthCredential?) -> Void) { - guard let phoneNumber = preference.user?.phoneNumber else { - self.isDeleteInProgress = false - LogE("UserProfileViewModel: \(#function) No phone number found for phone login.") - return - } + private func handleEmailLogin(completion: @escaping (AuthCredential?) -> Void) { + let alert = UIAlertController(title: "Re-authenticate", message: "Please enter your password", preferredStyle: .alert) - FirebaseProvider.phoneAuthProvider - .verifyPhoneNumber(phoneNumber, uiDelegate: nil) { [weak self] verificationID, error in - guard let self = self else { return } - self.isDeleteInProgress = false - if let error { - self.handleFirebaseAuthErrors(error) - } else { - self.phoneNumber = phoneNumber - self.verificationId = verificationID ?? "" - self.showOTPView = true - - self.otpPublisher - .sink { otp in - guard !otp.isEmpty else { return } - self.showOTPView = false - - let credential = FirebaseProvider.phoneAuthProvider - .credential(withVerificationID: self.verificationId, verificationCode: otp) - completion(credential) - } - .store(in: &self.cancelable) - } - } - } - - private func handleFirebaseAuthErrors(_ error: Error) { - if (error as NSError).code == FirebaseAuth.AuthErrorCode.webContextCancelled.rawValue { - showAlertFor(message: "Something went wrong! Please try after some time.") - } else if (error as NSError).code == FirebaseAuth.AuthErrorCode.tooManyRequests.rawValue { - showAlertFor(message: "Too many attempts, please try after some time.") - } else if (error as NSError).code == FirebaseAuth.AuthErrorCode.missingPhoneNumber.rawValue { - showAlertFor(message: "Enter a valid phone number.") - } else if (error as NSError).code == FirebaseAuth.AuthErrorCode.invalidPhoneNumber.rawValue { - showAlertFor(message: "Enter a valid phone number.") - } else { - LogE("UserProfileViewModel: \(#function) Phone login fail with error: \(error).") - showAlertFor(title: "Authentication failed", message: "Apologies, we were not able to complete the authentication process. Please try again later.") + alert.addTextField { textField in + textField.placeholder = "Password" + textField.isSecureTextEntry = true } + alert.addAction(UIAlertAction(title: "Cancel", style: .cancel) { _ in + self.isDeleteInProgress = false + }) + alert.addAction(UIAlertAction(title: "Confirm", style: .default) { _ in + guard let password = alert.textFields?.first?.text, + let email = self.preference.user?.emailId else { + self.isDeleteInProgress = false + LogE("UserProfileViewModel: \(#function) No email found for email login.") + return + } + let credential = EmailAuthProvider.credential(withEmail: email, password: password) + completion(credential) + }) + + TopViewController.shared.topViewController()?.present(alert, animated: true) } } diff --git a/Splito/UI/Home/Expense/AddExpenseView.swift b/Splito/UI/Home/Expense/AddExpenseView.swift index d28fb7efc..7d2420afb 100644 --- a/Splito/UI/Home/Expense/AddExpenseView.swift +++ b/Splito/UI/Home/Expense/AddExpenseView.swift @@ -232,14 +232,14 @@ struct AddNoteImageFooterView: View { DatePickerView(date: $date) ImageAttachmentView(image: image, imageUrl: imageUrl, handleImageBtnTap: handleImageTap) + .confirmationDialog("", isPresented: $showImagePickerOptions, titleVisibility: .hidden) { + ImagePickerOptionsView(image: image, imageUrl: imageUrl, handleActionSelection: handleActionSelection) + } NoteButtonView(isNoteEmpty: isNoteEmpty, handleNoteBtnTap: handleNoteBtnTap) } .padding(.vertical, 12) .padding(.horizontal, 16) - .confirmationDialog("", isPresented: $showImagePickerOptions, titleVisibility: .hidden) { - ImagePickerOptionsView(image: image, imageUrl: imageUrl, handleActionSelection: handleActionSelection) - } } } diff --git a/Splito/UI/Home/Groups/Add Member/JoinMemberView.swift b/Splito/UI/Home/Groups/Add Member/JoinMemberView.swift index 1818afb23..f0438096d 100644 --- a/Splito/UI/Home/Groups/Add Member/JoinMemberView.swift +++ b/Splito/UI/Home/Groups/Add Member/JoinMemberView.swift @@ -27,8 +27,7 @@ struct JoinMemberView: View { .foregroundStyle(primaryText) .multilineTextAlignment(.center) - OtpTextInputView(text: $viewModel.code, placeholder: "AF0R00", isFocused: $isFocused, - keyboardType: .alphabet) { + JoinMemberTextInputView(text: $viewModel.code, placeholder: "AF0R00", isFocused: $isFocused) { viewModel.handleJoinMemberAction { isSucceed in if isSucceed { dismiss() } } @@ -72,3 +71,47 @@ struct JoinMemberView: View { } } } + +public struct JoinMemberTextInputView: View { + + private let CODE_TOTAL_CHARACTERS = 6 + + @Binding var text: String + + let placeholder: String + let isFocused: FocusState.Binding + + var onCodeChange: (() -> Void) + + public var body: some View { + TextField(placeholder.localized, text: $text) + .kerning(16) + .focused(isFocused) + .tint(primaryColor) + .font(.Header2()) + .keyboardType(.alphabet) + .foregroundStyle(primaryText) + .multilineTextAlignment(.center) + .textContentType(.oneTimeCode) + .autocorrectionDisabled() + .onChange(of: text) { newValue in + // Restrict the length of text + if newValue.count > CODE_TOTAL_CHARACTERS { + text = String(newValue.prefix(CODE_TOTAL_CHARACTERS)) + return + } + + // Validate input characters by allowing only alphanumeric + text = newValue.filter { $0.isLetter || $0.isNumber } + + if newValue.count == CODE_TOTAL_CHARACTERS { + onCodeChange() + isFocused.wrappedValue = false + } + } + .textInputAutocapitalization(.never) + .onAppear { + isFocused.wrappedValue = true + } + } +} diff --git a/Splito/UI/Home/Groups/GroupListViewModel.swift b/Splito/UI/Home/Groups/GroupListViewModel.swift index 9d7f53b68..f23a2d2b3 100644 --- a/Splito/UI/Home/Groups/GroupListViewModel.swift +++ b/Splito/UI/Home/Groups/GroupListViewModel.swift @@ -231,7 +231,9 @@ extension GroupListViewModel { } func handleSearchBarTap() { - if combinedGroups.isEmpty { + if (combinedGroups.isEmpty) || + (selectedTab == .unsettled && combinedGroups.filter({ $0.userBalance != 0 }).isEmpty) || + (selectedTab == .settled && combinedGroups.filter({ $0.userBalance == 0 }).isEmpty) { showToastFor(toast: .init(type: .info, title: "No groups yet", message: "There are no groups available to search.")) } else { withAnimation { diff --git a/Splito/UI/Home/HomeRouteView.swift b/Splito/UI/Home/HomeRouteView.swift index 65deb8ed7..e0d254226 100644 --- a/Splito/UI/Home/HomeRouteView.swift +++ b/Splito/UI/Home/HomeRouteView.swift @@ -48,11 +48,15 @@ struct HomeRouteView: View { } } .tint(primaryText) - .onAppear(perform: viewModel.openUserProfileIfNeeded) + .onAppear(perform: viewModel.openProfileOrOnboardFlow) .sheet(isPresented: $viewModel.openProfileView) { UserProfileView(viewModel: UserProfileViewModel(router: nil, isOpenFromOnboard: true, onDismiss: viewModel.dismissProfileView)) .interactiveDismissDisabled() } + .fullScreenCover(isPresented: $viewModel.openOnboardFlow) { + OnboardRouteView(onDismiss: viewModel.dismissOnboardFlow) + .interactiveDismissDisabled() + } .onReceive(NotificationCenter.default.publisher(for: .showActivityLog)) { notification in if let activityId = notification.userInfo?["activityId"] as? String { viewModel.switchToActivityLog(activityId: activityId) diff --git a/Splito/UI/Home/HomeRouteViewModel.swift b/Splito/UI/Home/HomeRouteViewModel.swift index a9661a8de..0ce66c26d 100644 --- a/Splito/UI/Home/HomeRouteViewModel.swift +++ b/Splito/UI/Home/HomeRouteViewModel.swift @@ -14,15 +14,16 @@ class HomeRouteViewModel: ObservableObject { @Published var isTabBarVisible: Bool = true @Published var openProfileView: Bool = false + @Published var openOnboardFlow: Bool = false @Published var selectedTab: Int = 0 @Published var activityLogId: String? - func openUserProfileIfNeeded() { - if preference.isVerifiedUser { - if preference.user == nil || (preference.user?.firstName == nil) || (preference.user?.firstName == "") { - openProfileView = true - } + func openProfileOrOnboardFlow() { + if preference.user == nil { + openOnboardFlow = true + } else if preference.isVerifiedUser && (preference.user?.firstName == nil || preference.user?.firstName == "") { + openProfileView = true } } @@ -34,6 +35,10 @@ class HomeRouteViewModel: ObservableObject { openProfileView = false } + func dismissOnboardFlow() { + openOnboardFlow = false + } + func switchToActivityLog(activityId: String) { activityLogId = activityId selectedTab = 1 diff --git a/Splito/UI/Login/EmailLogin/EmailLoginView.swift b/Splito/UI/Login/EmailLogin/EmailLoginView.swift new file mode 100644 index 000000000..fea392228 --- /dev/null +++ b/Splito/UI/Login/EmailLogin/EmailLoginView.swift @@ -0,0 +1,186 @@ +// +// EmailLoginView.swift +// Splito +// +// Created by Amisha Italiya on 03/12/24. +// + +import SwiftUI +import BaseStyle + +struct EmailLoginView: View { + + @StateObject var viewModel: EmailLoginViewModel + + @FocusState private var focusedField: EmailLoginViewModel.EmailLoginField? + + var body: some View { + GeometryReader { proxy in + VStack(alignment: .leading, spacing: 0) { + ScrollView { + VStack(alignment: .leading, spacing: 0) { + AppLogoView(geometry: .constant(proxy)) + + Group { + LoginTitleView(titleText: "Continue your journey") + + VSpacer(16) + + LoginSubtitleView(subtitleText: "Sign in to access your account and enjoy all its features.") + + VSpacer(24) + + EmailInputFieldView(email: $viewModel.email, focusedField: $focusedField) + + VSpacer(16) + + PasswordInputFieldView(password: $viewModel.password, focusedField: $focusedField) + + ForgotPasswordView(onForgotPasswordClick: viewModel.onForgotPasswordClick) + + Spacer() + } + .padding(.horizontal, 16) + .frame(maxWidth: isIpad ? 600 : nil, alignment: .leading) + .frame(maxWidth: .infinity, alignment: .center) + + VStack(spacing: 0) { + PrimaryFloatingButton(text: "Login", bottomPadding: 6, + isEnabled: !viewModel.email.isEmpty && !viewModel.password.isEmpty, + showLoader: viewModel.isLoginInProgress, onClick: viewModel.onLoginClick) + + PrimaryFloatingButton(text: "Create account", textColor: primaryDarkColor, bgColor: container2Color, + showLoader: viewModel.isSignupInProgress, onClick: viewModel.onCreateAccountClick) + } + .frame(maxWidth: isIpad ? 600 : nil, alignment: .leading) + .frame(maxWidth: .infinity, alignment: .center) + } + .frame(minHeight: proxy.size.height, maxHeight: .infinity, alignment: .center) + .ignoresSafeArea(.keyboard) + } + .scrollIndicators(.hidden) + .scrollBounceBehavior(.basedOnSize) + } + } + .background(surfaceColor) + .alertView.alert(isPresented: $viewModel.showAlert, alertStruct: viewModel.alert) + .ignoresSafeArea(edges: .top) + .toolbar(.hidden, for: .navigationBar) + .overlay(alignment: .topLeading) { + BackButton(onClick: viewModel.navigateToRoot) + } + .onTapGesture { + UIApplication.shared.endEditing() + } + .onAppear { + focusedField = .email + } + } +} + +private struct EmailInputFieldView: View { + + @Binding var email: String + var focusedField: FocusState.Binding + + var body: some View { + VStack(alignment: .leading, spacing: 8) { + Text("Email") + .font(.body3()) + .foregroundStyle(secondaryText) + + TextField("Enter your email", text: $email) + .keyboardType(.emailAddress) + .font(.subTitle3()) + .foregroundStyle(primaryText) + .tint(primaryColor) + .autocapitalization(.none) + .focused(focusedField, equals: .email) + .submitLabel(.next) + .onSubmit { + focusedField.wrappedValue = .password + } + .padding(.vertical, 12) + .padding(.horizontal, 16) + .overlay { + RoundedRectangle(cornerRadius: 12) + .stroke(outlineColor, lineWidth: 1) + } + } + } +} + +private struct PasswordInputFieldView: View { + + @Binding var password: String + var focusedField: FocusState.Binding + + @State private var isSecured = true + @State private var visibleInput: String = "" + + var body: some View { + VStack(alignment: .leading, spacing: 8) { + Text("Password") + .font(.body3()) + .foregroundStyle(secondaryText) + + ZStack(alignment: .trailing) { + TextField("Enter your password", text: $visibleInput) + .onChange(of: visibleInput) { newValue in + guard isSecured else { password = newValue; return } + if newValue.count >= password.count { + let newItem = newValue.filter { $0 != Character("•") } + password.append(newItem) + } else { + password.removeLast() + } + visibleInput = String(newValue.map { _ in Character("•") }) + } + .font(.subTitle3()) + .foregroundStyle(primaryText) + .tint(primaryColor) + .autocapitalization(.none) + .submitLabel(.done) + .padding(.vertical, 12) + .padding(.horizontal, 16) + + Image(systemName: isSecured ? "eye" : "eye.slash") + .resizable() + .scaledToFit() + .frame(width: 15, height: 15) + .foregroundStyle(lowestText) + .fontWeight(.black) + .padding(.vertical, 15) + .padding(.horizontal, 19) + .onTapGestureForced { + isSecured.toggle() + visibleInput = isSecured ? String(password.map { _ in Character("•") }) : password + } + } + .focused(focusedField, equals: .password) + .overlay { + RoundedRectangle(cornerRadius: 12) + .stroke(outlineColor, lineWidth: 1) + } + } + } +} + +private struct ForgotPasswordView: View { + + let onForgotPasswordClick: () -> Void + + var body: some View { + HStack { + Spacer() + + Button(action: onForgotPasswordClick) { + Text("Forgot password?") + .font(.caption1()) + .foregroundStyle(disableText) + } + } + .padding(.top, 8) + .padding(.bottom, 24) + } +} diff --git a/Splito/UI/Login/EmailLogin/EmailLoginViewModel.swift b/Splito/UI/Login/EmailLogin/EmailLoginViewModel.swift new file mode 100644 index 000000000..11b6e5beb --- /dev/null +++ b/Splito/UI/Login/EmailLogin/EmailLoginViewModel.swift @@ -0,0 +1,157 @@ +// +// EmailLoginViewModel.swift +// Splito +// +// Created by Amisha Italiya on 03/12/24. +// + +import Data +import Foundation +import FirebaseAuth + +public class EmailLoginViewModel: BaseViewModel, ObservableObject { + + @Inject private var preference: SplitoPreference + @Inject private var userRepository: UserRepository + + @Published private(set) var isLoginInProgress = false + @Published private(set) var isSignupInProgress = false + + @Published var email = "" + @Published var password = "" + + private let router: Router + private var onDismiss: (() -> Void)? + + init(router: Router, onDismiss: (() -> Void)? = nil) { + self.router = router + self.onDismiss = onDismiss + } + + // MARK: - User Actions + func onCreateAccountClick() { + guard validateEmailAndPassword() else { return } + + isSignupInProgress = true + FirebaseAuth.Auth.auth().createUser(withEmail: email, password: password) { [weak self] result, error in + guard let self else { return } + isSignupInProgress = false + self.handleAuthResponse(result: result, error: error, isLogin: false) + } + } + + func onLoginClick() { + guard validateEmailAndPassword() else { return } + + isLoginInProgress = true + FirebaseAuth.Auth.auth().signIn(withEmail: email, password: password) { [weak self] result, error in + guard let self else { return } + isLoginInProgress = false + self.handleAuthResponse(result: result, error: error, isLogin: true) + } + } + + private func handleAuthResponse(result: AuthDataResult?, error: Error?, isLogin: Bool) { + if let error { + LogE("EmailLoginViewModel: \(#function) Error during \(isLogin ? "login" : "sign up"): \(error)") + handleFirebaseAuthErrors(error) + } else if let result { + let user = AppUser(id: result.user.uid, firstName: "", lastName: "", + emailId: email, phoneNumber: nil, loginType: .Email) + Task { + await storeUser(user: user) + } + LogD("EmailLoginViewModel: \(#function) User \(isLogin ? "logged in" : "signed up") successfully.") + } else { + self.alert = .init(message: "Contact Support") + self.showAlert = true + } + } + + private func validateEmailAndPassword() -> Bool { + if !email.isValidEmail { + showAlertFor(title: "Error", message: "Please enter a valid email address.") + return false + } + if password.count < 6 { + showAlertFor(title: "Error", message: "Password must be at least 6 characters long.") + return false + } + + return true + } + + private func storeUser(user: AppUser) async { + do { + let user = try await userRepository.storeUser(user: user) + preference.isVerifiedUser = true + preference.user = user + if onDismiss != nil { + onDismiss?() + } else { + navigateToRoot() + } + LogD("EmailLoginViewModel: \(#function) User stored successfully.") + } catch { + LogE("EmailLoginViewModel: \(#function) Failed to store user: \(error).") + alert = .init(message: "Something went wrong! Please try after some time.") + showAlert = true + } + } + + func onForgotPasswordClick() { + guard email.isValidEmail else { + showAlertFor(title: "Invalid Email", message: "Please enter a valid email address.") + return + } + + FirebaseAuth.Auth.auth().sendPasswordReset(withEmail: email) { [weak self] error in + guard let self else { return } + if let error { + LogE("EmailLoginViewModel: \(#function) Failed to send password reset email: \(error)") + self.handleFirebaseAuthErrors(error, isPasswordReset: true) + } else { + self.showAlertFor(title: "Email sent", message: "An email has been sent to \(email) with instructions to reset your password.") + } + } + } + + func navigateToRoot() { + router.popToRoot() + } + + // MARK: - Error handling + private func handleFirebaseAuthErrors(_ error: Error, isPasswordReset: Bool = false) { + guard let authErrorCode = FirebaseAuth.AuthErrorCode(rawValue: (error as NSError).code) else { + showAlertFor(title: "Error", message: "Something went wrong! Please try after some time.") + return + } + + switch authErrorCode { + case .webContextCancelled: + showAlertFor(message: "Something went wrong! Please try after some time.") + case .tooManyRequests: + showAlertFor(message: "Too many attempts, please try after some time.") + case .invalidEmail: + showAlertFor(title: "Invalid Email", message: "The email address is not valid. Please check and try again.") + case .emailAlreadyInUse: + showAlertFor(title: "Email Already in Use", message: "The email address is already associated with an existing account. Please use a different email or log in to your existing account.") + case .userNotFound: + showAlertFor(title: "Account Not Found", message: "No account found with the provided email address. Please sign up.") + case .userDisabled: + showAlertFor(title: "Account Disabled", message: "This account has been disabled. Please contact support.") + case .invalidCredential: + showAlertFor(title: "Incorrect email or password", message: "The email or password you entered is incorrect. Please try again.") + default: + LogE("EmailLoginViewModel: \(#function) \((isPasswordReset) ? "Password reset" : "Email login") fail with error: \(error).") + isPasswordReset ? showAlertFor(title: "Error", message: "Unable to send a password reset email. Please try again later.") : showAlertFor(title: "Authentication failed", message: "Apologies, we were not able to complete the authentication process. Please try again later.") + } + } +} + +extension EmailLoginViewModel { + enum EmailLoginField: Hashable { + case email + case password + } +} diff --git a/Splito/UI/Login/LoginView.swift b/Splito/UI/Login/LoginView.swift index 480f73aea..393ceab9f 100644 --- a/Splito/UI/Login/LoginView.swift +++ b/Splito/UI/Login/LoginView.swift @@ -40,7 +40,7 @@ struct LoginView: View { showAppleLoading: viewModel.showAppleLoading, onGoogleLoginClick: viewModel.onGoogleLoginClick, onAppleLoginClick: viewModel.onAppleLoginClick, - onPhoneLoginClick: viewModel.onPhoneLoginClick) + onEmailLoginClick: viewModel.onEmailLoginClick) VSpacer(24) } @@ -59,7 +59,7 @@ private struct LoginOptionsView: View { let onGoogleLoginClick: () -> Void let onAppleLoginClick: () -> Void - let onPhoneLoginClick: () -> Void + let onEmailLoginClick: () -> Void var body: some View { VStack(spacing: 8) { @@ -68,9 +68,8 @@ private struct LoginOptionsView: View { LoginOptionsButtonView(systemImage: ("apple.logo", primaryText, (14, 16)), buttonName: "Sign in with Apple", showLoader: showAppleLoading, onClick: onAppleLoginClick) - LoginOptionsButtonView(systemImage: ("phone.fill", primaryLightText, (12, 12)), - buttonName: "Sign in with Phone Number", bgColor: primaryColor, - buttonTextColor: primaryLightText, showLoader: false, onClick: onPhoneLoginClick) + LoginOptionsButtonView(image: .emailIcon, buttonName: "Sign in with Email", bgColor: primaryColor, + buttonTextColor: primaryLightText, showLoader: false, onClick: onEmailLoginClick) } .padding(.horizontal, 16) .frame(maxWidth: isIpad ? 600 : nil, alignment: .center) @@ -158,7 +157,7 @@ struct AppLogoView: View { .scaledToFit() .frame(width: width * 0.2 + 200, height: geometry.size.height * 0.1 + 120, alignment: .center) .padding(.top, 88) - .padding(.bottom, 40) + .padding(.bottom, 16) Spacer() } @@ -175,7 +174,7 @@ struct LoginTitleView: View { var body: some View { HStack { - Text(titleText) + Text(titleText.localized) .font(.Header1()) .foregroundStyle(primaryText) @@ -190,11 +189,12 @@ struct LoginSubtitleView: View { var body: some View { HStack { - Text(subtitleText) + Text(subtitleText.localized) .font(.subTitle1()) .foregroundStyle(disableText) .tracking(-0.2) .lineSpacing(4) + .fixedSize(horizontal: false, vertical: true) Spacer() } diff --git a/Splito/UI/Login/LoginViewModel.swift b/Splito/UI/Login/LoginViewModel.swift index 780529b8c..3765ba775 100644 --- a/Splito/UI/Login/LoginViewModel.swift +++ b/Splito/UI/Login/LoginViewModel.swift @@ -23,9 +23,11 @@ public class LoginViewModel: BaseViewModel, ObservableObject { private var currentNonce: String = "" private var appleSignInDelegates: SignInWithAppleDelegates? private let router: Router + private var onDismiss: (() -> Void)? - init(router: Router) { + init(router: Router, onDismiss: (() -> Void)? = nil) { self.router = router + self.onDismiss = onDismiss } // MARK: - Data Loading @@ -117,18 +119,19 @@ public class LoginViewModel: BaseViewModel, ObservableObject { LogD("LoginViewModel: \(#function) User stored successfully.") } catch { LogE("LoginViewModel: \(#function) Failed to store user: \(error).") - self.alert = .init(message: error.localizedDescription) + self.alert = .init(message: "Something went wrong! Please try after some time.") self.showAlert = true } } private func onLoginSuccess() { preference.isVerifiedUser = true + onDismiss?() } // MARK: - User Actions - func onPhoneLoginClick() { - router.push(.PhoneLoginView) + func onEmailLoginClick() { + router.push(.EmailLoginView(onDismiss: onDismiss)) } } diff --git a/Splito/UI/Login/PhoneLogin/PhoneLoginView.swift b/Splito/UI/Login/PhoneLogin/PhoneLoginView.swift deleted file mode 100644 index 0ace1840d..000000000 --- a/Splito/UI/Login/PhoneLogin/PhoneLoginView.swift +++ /dev/null @@ -1,268 +0,0 @@ -// -// PhoneLoginView.swift -// Splito -// -// Created by Amisha Italiya on 22/02/24. -// - -import Data -import SwiftUI -import BaseStyle - -public struct PhoneLoginView: View { - - @StateObject var viewModel: PhoneLoginViewModel - - public var body: some View { - GeometryReader { proxy in - VStack(alignment: .leading, spacing: 0) { - ScrollView { - VStack(alignment: .leading, spacing: 0) { - AppLogoView(geometry: .constant(proxy)) - - Group { - LoginTitleView(titleText: "What’s your phone number?") - - VSpacer(16) - - LoginSubtitleView(subtitleText: "We’ll verify your phone number with a verification code.") - - VSpacer(40) - - HStack(spacing: 0) { - Spacer() - - PhoneLoginContentView(phoneNumber: $viewModel.phoneNumber, countries: $viewModel.countries, - selectedCountry: $viewModel.currentCountry, showLoader: viewModel.showLoader) - } - } - .padding(.horizontal, 16) - .frame(maxWidth: isIpad ? 600 : nil, alignment: .leading) - .frame(maxWidth: .infinity, alignment: .center) - } - } - .scrollIndicators(.hidden) - .scrollBounceBehavior(.basedOnSize) - - GetOtpBtnView(phoneNumber: $viewModel.phoneNumber, showLoader: viewModel.showLoader, onNext: viewModel.verifyAndSendOtp, handlePrivacyPolicyTap: viewModel.handlePrivacyPolicyTap) - } - } - .background(surfaceColor) - .alertView.alert(isPresented: $viewModel.showAlert, alertStruct: viewModel.alert) - .ignoresSafeArea(edges: .top) - .toolbar(.hidden, for: .navigationBar) - .overlay(alignment: .topLeading) { - BackButton(onClick: viewModel.handleBackBtnTap) - } - .onTapGesture { - UIApplication.shared.endEditing() - } - } -} - -private struct GetOtpBtnView: View { - let MIN_NUMBER_LENGTH: Int = 4 - let MAX_NUMBER_LENGTH: Int = 20 - - @Binding var phoneNumber: String - - let showLoader: Bool - - let onNext: () -> Void - let handlePrivacyPolicyTap: () -> Void - - var body: some View { - VStack(spacing: 0) { - Group { - Text("By entering your number, you’re agreeing to our ") - .foregroundColor(disableText) - + - Text("terms of service") - .foregroundColor(primaryText) - .underline() - + - Text(" and ") - .foregroundColor(disableText) - + - Text("privacy policy.") - .underline() - .foregroundColor(primaryText) - } - .font(.caption1()) - .padding(.bottom, 24) - .frame(maxWidth: .infinity, alignment: .leading) - .onTapGesture(perform: handlePrivacyPolicyTap) - - PrimaryButton(text: "Get OTP", isEnabled: (phoneNumber.count >= MIN_NUMBER_LENGTH && phoneNumber.count <= MAX_NUMBER_LENGTH), showLoader: showLoader, onClick: onNext) - - VSpacer(24) - } - .padding(.horizontal, 16) - .frame(maxWidth: isIpad ? 600 : nil, alignment: .center) - .frame(maxWidth: .infinity, alignment: .center) - } - - func abc() -> some View { - return HStack {} - } -} - -private struct PhoneLoginContentView: View { - - @Binding var phoneNumber: String - @Binding var countries: [Country] - @Binding var selectedCountry: Country - - let showLoader: Bool - - @State var showCountryPicker = false - @FocusState var isFocused: Bool - - var body: some View { - VStack(spacing: 0) { - HStack(spacing: 0) { - HStack(spacing: 8) { - Text(selectedCountry.dialCode) - .font(.subTitle1()) - .foregroundStyle(primaryText) - .tracking(-0.2) - - ScrollToTopButton(icon: "chevron.down", iconColor: primaryText, - bgColor: surfaceColor, size: (12, 7.5), padding: 3) { - showCountryPicker = true - } - } - .onTapGestureForced { - showCountryPicker = true - } - - Divider() - .frame(height: 50) - .background(dividerColor) - .padding(.horizontal, 16) - - ZStack(alignment: .leading) { - if phoneNumber.isEmpty { - Text(" Enter mobile number") - .font(.subTitle3()) - .foregroundStyle(disableText) - } - TextField("", text: $phoneNumber) - .font(.Header2()) - .textContentType(.telephoneNumber) - .keyboardType(.phonePad) - .foregroundStyle(primaryText) - .disabled(showLoader) - .tint(primaryColor) - .focused($isFocused) - .onAppear { - isFocused = true - } - } - } - .frame(maxWidth: .infinity, alignment: .leading) - - Spacer() - } - .sheet(isPresented: $showCountryPicker) { - PhoneLoginCountryPicker(countries: $countries, selectedCountry: $selectedCountry, isPresented: $showCountryPicker) - } - } -} - -private struct PhoneLoginCountryPicker: View { - - @Binding var countries: [Country] - @Binding var selectedCountry: Country - @Binding var isPresented: Bool - - @State private var searchCountry: String = "" - @FocusState private var isFocused: Bool - - private var filteredCountries: [Country] { - countries.filter { country in - searchCountry.isEmpty ? true : country.name.lowercased().contains(searchCountry.lowercased()) || - country.dialCode.lowercased().contains(searchCountry.lowercased()) - } - } - - var body: some View { - VStack(spacing: 0) { - Text("Countries") - .font(.Header4()) - .foregroundStyle(primaryText) - .padding(.top, 24) - .padding(.bottom, 16) - .frame(maxWidth: .infinity, alignment: .center) - .onTapGestureForced { - isFocused = false - } - - SearchBar(text: $searchCountry, isFocused: $isFocused, placeholder: "Search") - .padding(.vertical, -7) - .padding(.horizontal, 3) - .overlay(content: { - RoundedRectangle(cornerRadius: 12) - .stroke(outlineColor, lineWidth: 1) - }) - .focused($isFocused) - .onAppear { - isFocused = true - } - .padding([.horizontal, .bottom], 16) - - if filteredCountries.isEmpty { - CountryNotFoundView(searchCountry: searchCountry) - } else { - List(filteredCountries) { country in - PhoneLoginCountryCell(country: country) { - selectedCountry = country - isPresented = false - } - } - .listStyle(PlainListStyle()) - } - } - } -} - -private struct CountryNotFoundView: View { - - let searchCountry: String - - var body: some View { - VStack(spacing: 0) { - Spacer() - - Text("No results found for \"\(searchCountry)\"!") - .font(.subTitle1()) - .foregroundStyle(disableText) - .padding(.bottom, 60) - - Spacer() - } - .onTapGestureForced { - UIApplication.shared.endEditing() - } - } -} - -private struct PhoneLoginCountryCell: View { - - let country: Country - let onCellSelect: () -> Void - - var body: some View { - Button(action: onCellSelect) { - HStack(spacing: 0) { - Text(country.flag + " " + country.name) - .font(.body1(16)) - .foregroundStyle(primaryText) - Spacer() - Text(country.dialCode) - .font(.body1(16)) - .foregroundStyle(primaryText) - } - } - } -} diff --git a/Splito/UI/Login/PhoneLogin/PhoneLoginViewModel.swift b/Splito/UI/Login/PhoneLogin/PhoneLoginViewModel.swift deleted file mode 100644 index 0397c6dc4..000000000 --- a/Splito/UI/Login/PhoneLogin/PhoneLoginViewModel.swift +++ /dev/null @@ -1,95 +0,0 @@ -// -// PhoneLoginViewModel.swift -// Splito -// -// Created by Amisha Italiya on 22/02/24. -// - -import Data -import FirebaseAuth -import BaseStyle - -public class PhoneLoginViewModel: BaseViewModel, ObservableObject { - - let MAX_NUMBER_LENGTH: Int = 20 - - @Published var countries = [Country]() - @Published var currentCountry: Country - - @Published private(set) var verificationId = "" - @Published private(set) var showLoader: Bool = false - - @Published var phoneNumber = "" { - didSet { - guard phoneNumber.count < MAX_NUMBER_LENGTH else { - showAlertFor(message: "Entered number is too long, Please check the phone number.") - phoneNumber = oldValue - return - } - } - } - - private let router: Router - - init(router: Router) { - self.router = router - let allCountries = JSONUtils.readJSONFromFile(fileName: "Countries", type: [Country].self, bundle: .baseBundle) ?? [] - let currentLocal = Locale.current.region?.identifier - self.countries = allCountries - self.currentCountry = allCountries.first(where: {$0.isoCode == currentLocal}) ?? (allCountries.first ?? Country(name: "India", dialCode: "+91", isoCode: "IN")) - super.init() - } - - // MARK: - Data Loading - func verifyAndSendOtp() { - showLoader = true - FirebaseProvider.phoneAuthProvider - .verifyPhoneNumber((currentCountry.dialCode + phoneNumber.getNumbersOnly()), uiDelegate: nil) { [weak self] (verificationID, error) in - self?.showLoader = false - if let error { - self?.handleFirebaseAuthErrors(error) - } else { - self?.verificationId = verificationID ?? "" - self?.openVerifyOtpView() - } - } - } - - // MARK: - User Actions - func handleBackBtnTap() { - router.pop() - } - - func handlePrivacyPolicyTap() { - if let url = URL(string: Constants.privacyPolicyURL) { - if UIApplication.shared.canOpenURL(url) { - UIApplication.shared.open(url, options: [:]) - } else { - showToastFor(toast: ToastPrompt(type: .error, title: "Error", message: "Privacy policy cannot be accessed.")) - } - } - } -} - -// MARK: - Helper Methods -extension PhoneLoginViewModel { - - private func openVerifyOtpView() { - router.push(.VerifyOTPView(phoneNumber: phoneNumber, dialCode: currentCountry.dialCode, verificationId: verificationId)) - } - - private func handleFirebaseAuthErrors(_ error: Error) { - if (error as NSError).code == FirebaseAuth.AuthErrorCode.webContextCancelled.rawValue { - showAlertFor(message: "Something went wrong! Please try after some time.") - } else if (error as NSError).code == FirebaseAuth.AuthErrorCode.tooManyRequests.rawValue { - showAlertFor(message: "Too many attempts, please try after some time.") - } else if (error as NSError).code == FirebaseAuth.AuthErrorCode.missingPhoneNumber.rawValue { - showAlertFor(message: "Enter a valid phone number.") - } else if (error as NSError).code == FirebaseAuth.AuthErrorCode.invalidPhoneNumber.rawValue { - showAlertFor(message: "Enter a valid phone number.") - } else { - LogE("PhoneLoginViewModel: \(#function) Phone login fail with error: \(error).") - showAlertFor(title: "Authentication failed", message: "Apologies, we were not able to complete the authentication process. Please try again later.") - } - } -} diff --git a/Splito/UI/Login/PhoneLogin/VerifyOtp/VerifyOtpView.swift b/Splito/UI/Login/PhoneLogin/VerifyOtp/VerifyOtpView.swift deleted file mode 100644 index 43d32d00c..000000000 --- a/Splito/UI/Login/PhoneLogin/VerifyOtp/VerifyOtpView.swift +++ /dev/null @@ -1,147 +0,0 @@ -// -// VerifyOtpView.swift -// Splito -// -// Created by Amisha Italiya on 23/02/24. -// - -import SwiftUI -import BaseStyle - -public struct VerifyOtpView: View { - - @StateObject var viewModel: VerifyOtpViewModel - - public var body: some View { - GeometryReader { proxy in - VStack(alignment: .leading, spacing: 0) { - ScrollView { - VStack(alignment: .leading, spacing: 0) { - AppLogoView(geometry: .constant(proxy)) - - Group { - LoginTitleView(titleText: "Verification code") - - VSpacer(16) - - LoginSubtitleView(subtitleText: "We’ve sent a verification code to your phone \(viewModel.hiddenPhoneNumber).") - - VSpacer(40) - - HStack { - OtpTextFieldView(otp: $viewModel.otp, onVerify: { - viewModel.verifyOTP() - UIApplication.shared.endEditing() - }) - - Spacer() - } - } - .padding(.horizontal, 16) - .frame(maxWidth: isIpad ? 600 : nil, alignment: .leading) - .frame(maxWidth: .infinity, alignment: .center) - } - } - .scrollIndicators(.hidden) - .scrollBounceBehavior(.basedOnSize) - - PhoneLoginOtpBtnView( - otp: $viewModel.otp, - resendOtpCount: $viewModel.resendOtpCount, - showLoader: viewModel.showLoader, - onVerify: { - viewModel.verifyOTP() - UIApplication.shared.endEditing() - }, - onResendOtp: viewModel.resendOtp - ) - } - } - .background(surfaceColor) - .alertView.alert(isPresented: $viewModel.showAlert, alertStruct: viewModel.alert) - .toastView(toast: $viewModel.toast) - .onTapGesture { - UIApplication.shared.endEditing() - } - .onDisappear { - viewModel.resendTimer?.invalidate() - } - .ignoresSafeArea(edges: .top) - .toolbar(.hidden, for: .navigationBar) - .overlay(alignment: .topLeading) { - BackButton(onClick: viewModel.handleBackBtnTap) - } - } -} - -private struct PhoneLoginOtpBtnView: View { - - @Binding var otp: String - @Binding var resendOtpCount: Int - let showLoader: Bool - - let onVerify: () -> Void - let onResendOtp: () -> Void - - var body: some View { - VStack(spacing: 0) { - if resendOtpCount > 0 { - Group { - Text("Resend code ") - .foregroundColor(secondaryText) - + Text("00:\(String(format: "%02d", resendOtpCount))") - .foregroundColor(primaryText) - } - .font(.caption1()) - } else { - VStack(spacing: 0) { - Button(action: onResendOtp) { - Text("Resend code") - .font(.buttonText()) - .foregroundStyle(primaryColor) - } - - Divider() - .frame(height: 1) - .background(primaryColor) - } - .fixedSize(horizontal: true, vertical: false) - } - - VSpacer(12) - - PrimaryButton(text: "Verify", isEnabled: !otp.isEmpty, showLoader: showLoader, onClick: onVerify) - - VSpacer(24) - } - .padding(.horizontal, 16) - .frame(maxWidth: isIpad ? 600 : nil, alignment: .leading) - .frame(maxWidth: .infinity, alignment: .center) - } -} - -private struct OtpTextFieldView: View { - - @Binding var otp: String - - let onVerify: () -> Void - - @FocusState private var isFocused: Bool - - var body: some View { - HStack(spacing: 0) { - Text("6 digits") - .font(.subTitle1()) - .foregroundStyle(primaryText) - .tracking(-0.2) - - Divider() - .frame(height: 50) - .background(dividerColor) - .padding(.horizontal, 16) - - OtpTextInputView(text: $otp, placeholder: "000000", isFocused: $isFocused, alignment: .leading, onOtpVerify: onVerify) - } - .frame(maxWidth: .infinity, alignment: .leading) - } -} diff --git a/Splito/UI/Login/PhoneLogin/VerifyOtp/VerifyOtpViewModel.swift b/Splito/UI/Login/PhoneLogin/VerifyOtp/VerifyOtpViewModel.swift deleted file mode 100644 index 15421a0c7..000000000 --- a/Splito/UI/Login/PhoneLogin/VerifyOtp/VerifyOtpViewModel.swift +++ /dev/null @@ -1,146 +0,0 @@ -// -// VerifyOtpViewModel.swift -// Splito -// -// Created by Amisha Italiya on 23/02/24. -// - -import Data -import SwiftUI -import FirebaseAuth - -public class VerifyOtpViewModel: BaseViewModel, ObservableObject { - - @Inject var preference: SplitoPreference - @Inject var userRepository: UserRepository - - @Published var otp = "" - @Published var resendOtpCount: Int = 30 - @Published private(set) var showLoader: Bool = false - @Published private(set) var resendTimer: Timer? - - var hiddenPhoneNumber: String { - let count = phoneNumber.count - guard count > 4 else { return phoneNumber } - let middleNumbers = String(repeating: "*", count: count - 4) - return "\(phoneNumber.prefix(2))\(middleNumbers)\(phoneNumber.suffix(2))" - } - - private let router: Router? - private let dialCode: String - private var phoneNumber: String - private var verificationId: String - private var isFromPhoneLogin = false - - private var onLoginSuccess: ((String) -> Void)? - - init(router: Router? = nil, phoneNumber: String, dialCode: String = "", - verificationId: String, onLoginSuccess: ((String) -> Void)? = nil) { - self.router = router - self.phoneNumber = phoneNumber - self.dialCode = dialCode - self.verificationId = verificationId - self.onLoginSuccess = onLoginSuccess - super.init() - runTimer() - isFromPhoneLogin = onLoginSuccess == nil - } - - // MARK: - Data Loading - func verifyOTP() { - guard !otp.isEmpty else { return } - - let credential = FirebaseProvider.phoneAuthProvider.credential(withVerificationID: verificationId, - verificationCode: otp) - showLoader = true - FirebaseProvider.auth.signIn(with: credential) {[weak self] (result, _) in - self?.showLoader = false - if let result { - guard let self else { return } - self.resendTimer?.invalidate() - let user = AppUser(id: result.user.uid, firstName: nil, lastName: nil, emailId: nil, - phoneNumber: result.user.phoneNumber, loginType: .Phone) - Task { - await self.storeUser(user: user) - } - } else { - self?.onLoginError() - } - } - } - - func resendOtp() { - showLoader = true - FirebaseProvider.phoneAuthProvider.verifyPhoneNumber((dialCode + phoneNumber), uiDelegate: nil) { [weak self] (verificationID, error) in - guard let self else { return } - self.showLoader = false - if error != nil { - if (error! as NSError).code == FirebaseAuth.AuthErrorCode.webContextCancelled.rawValue { - self.showAlertFor(message: "Something went wrong! Please try after some time.") - } else if (error! as NSError).code == FirebaseAuth.AuthErrorCode.tooManyRequests.rawValue { - self.showAlertFor(title: "Warning !!!", message: "Too many attempts, please try after some time.") - } else if (error! as NSError).code == FirebaseAuth.AuthErrorCode.missingPhoneNumber.rawValue || (error! as NSError).code == FirebaseAuth.AuthErrorCode.invalidPhoneNumber.rawValue { - self.showAlertFor(message: "Enter a valid phone number") - } else { - LogE("VerifyOtpViewModel: \(#function) Phone login fail with error: \(error.debugDescription)") - self.showAlertFor(title: "Authentication failed", - message: "Apologies, we were not able to complete the authentication process. Please try again later.") - } - } else { - self.verificationId = verificationID ?? "" - self.runTimer() - } - } - } - - // MARK: - User Actions - func handleBackBtnTap() { - router?.pop() - } - - func editButtonAction() { - router?.pop() - } -} - -// MARK: - Helper Methods -extension VerifyOtpViewModel { - private func runTimer() { - resendOtpCount = 30 - resendTimer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(update), userInfo: nil, repeats: true) - } - - @objc func update() { - if resendOtpCount > 0 { - resendOtpCount -= 1 - } else { - resendTimer?.invalidate() - } - } - - private func onLoginError() { - showAlertFor(title: "Invalid OTP", message: "Please, enter a valid OTP code.") - } - - private func storeUser(user: AppUser) async { - do { - let user = try await userRepository.storeUser(user: user) - self.preference.isVerifiedUser = true - self.preference.user = user - self.onVerificationSuccess() - LogD("VerifyOtpViewModel: \(#function) User stored successfully.") - } catch { - LogE("VerifyOtpViewModel: \(#function) Failed to store user: \(error).") - self.alert = .init(message: error.localizedDescription) - self.showAlert = true - } - } - - private func onVerificationSuccess() { - if onLoginSuccess == nil { - router?.popToRoot() - } else { - onLoginSuccess?(otp) - } - } -} diff --git a/Splito/UI/Onboard/OnboardRouteView.swift b/Splito/UI/Onboard/OnboardRouteView.swift index acfef08f5..9b13c24f1 100644 --- a/Splito/UI/Onboard/OnboardRouteView.swift +++ b/Splito/UI/Onboard/OnboardRouteView.swift @@ -15,23 +15,23 @@ struct OnboardRouteView: View { @Inject var preference: SplitoPreference + var onDismiss: (() -> Void)? + var body: some View { RouterView(router: router) { route in switch route { case .OnboardView: OnboardView(viewModel: OnboardViewModel(router: router)) case .LoginView: - LoginView(viewModel: LoginViewModel(router: router)) - case .PhoneLoginView: - PhoneLoginView(viewModel: PhoneLoginViewModel(router: router)) - case .VerifyOTPView(let phoneNumber, let dialCode, let verificationId): - VerifyOtpView(viewModel: VerifyOtpViewModel(router: router, phoneNumber: phoneNumber, dialCode: dialCode, verificationId: verificationId)) + LoginView(viewModel: LoginViewModel(router: router, onDismiss: onDismiss)) + case .EmailLoginView(let onDismiss): + EmailLoginView(viewModel: EmailLoginViewModel(router: router, onDismiss: onDismiss)) default: EmptyRouteView(routeName: self) } } .onAppear { - if preference.isOnboardShown, !preference.isVerifiedUser { + if preference.isOnboardShown && (!preference.isVerifiedUser || preference.user == nil) { router.updateRoot(root: .LoginView) } } diff --git a/Splito/UI/Router/AppRoute.swift b/Splito/UI/Router/AppRoute.swift deleted file mode 100644 index 0d4093acc..000000000 --- a/Splito/UI/Router/AppRoute.swift +++ /dev/null @@ -1,116 +0,0 @@ -// -// AppRoute.swift -// Data -// -// Created by Amisha Italiya on 27/02/24. -// - -import Foundation -import Data - -public enum AppRoute: Hashable { - - public static func == (lhs: AppRoute, rhs: AppRoute) -> Bool { - return lhs.key == rhs.key - } - - case OnboardView - case LoginView - case PhoneLoginView - case VerifyOTPView(phoneNumber: String, dialCode: String, verificationId: String) - case ProfileView - case HomeView - - // MARK: - Friends Tab - case FriendsHomeView - - // MARK: - Groups Tab - case GroupListView - case GroupHomeView(groupId: String) - case CreateGroupView(group: Groups?) - case InviteMemberView(groupId: String) - case JoinMemberView - case GroupSettingView(groupId: String) - case GroupSettleUpView(groupId: String) - case GroupWhoIsPayingView(groupId: String, isPaymentSettled: Bool) - case GroupWhoGettingPaidView(groupId: String, selectedMemberId: String) - case GroupPaymentView(transactionId: String?, groupId: String, payerId: String, receiverId: String, amount: Double) - case TransactionListView(groupId: String) - case TransactionDetailView(transactionId: String, groupId: String) - - // MARK: - Expense Button - case AddExpenseView(groupId: String, expenseId: String?) - case ExpenseDetailView(groupId: String, expenseId: String) - case ChoosePayerView(groupId: String, amount: Double, selectedPayer: [String: Double], onPayerSelection: (([String: Double]) -> Void)) - case ChooseMultiplePayerView(groupId: String, selectedPayers: [String: Double], amount: Double, onPayerSelection: (([String: Double]) -> Void)) - - // MARK: - Activity Tab - case ActivityHomeView - - // MARK: - Account Tab - case AccountHomeView - - var key: String { - switch self { - case .OnboardView: - "onboardView" - case .LoginView: - "loginView" - case .PhoneLoginView: - "phoneLoginView" - case .VerifyOTPView: - "verifyOTPView" - case .ProfileView: - "userProfileView" - case .HomeView: - "homeView" - - case .FriendsHomeView: - "friendsHomeView" - - case .ActivityHomeView: - "activityHomeView" - case .ExpenseDetailView: - "expenseDetailView" - - case .GroupListView: - "groupListView" - case .GroupHomeView: - "groupHomeView" - case .CreateGroupView: - "createGroupView" - case .InviteMemberView: - "inviteMemberView" - case .JoinMemberView: - "joinMemberView" - case .GroupSettingView: - "groupSettingView" - case .GroupSettleUpView: - "groupSettleUpView" - case .GroupWhoIsPayingView: - "groupWhoIsPayingView" - case .GroupWhoGettingPaidView: - "groupWhoGettingPaidView" - case .GroupPaymentView: - "groupPaymentView" - case .TransactionListView: - "transactionListView" - case .TransactionDetailView: - "transactionDetailView" - - case .AddExpenseView: - "addExpenseView" - case .ChoosePayerView: - "choosePayerView" - case .ChooseMultiplePayerView: - "chooseMultiplePayerView" - - case .AccountHomeView: - "accountHomeView" - } - } - - public func hash(into hasher: inout Hasher) { - hasher.combine(key) - } -}