From fdf1aab494ec24e6394216f89572696dfc0b70f0 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Mon, 25 Jul 2022 13:54:13 -0700 Subject: [PATCH 01/17] bump apple apps to v1.13.0 --- apple/Omnivore.xcodeproj/project.pbxproj | 36 ++++++++++++------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/apple/Omnivore.xcodeproj/project.pbxproj b/apple/Omnivore.xcodeproj/project.pbxproj index 97423c746..e35874bbe 100644 --- a/apple/Omnivore.xcodeproj/project.pbxproj +++ b/apple/Omnivore.xcodeproj/project.pbxproj @@ -1229,7 +1229,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 66; + CURRENT_PROJECT_VERSION = 68; DEVELOPMENT_TEAM = QJF2XZ86HB; ENABLE_HARDENED_RUNTIME = YES; INFOPLIST_FILE = InfoPlists/ShareExtensionMac.plist; @@ -1239,7 +1239,7 @@ "@executable_path/../../../../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 12.0; - MARKETING_VERSION = 1.12.1; + MARKETING_VERSION = 1.13.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "app.omnivore.app.ShareExtension-Mac"; @@ -1261,7 +1261,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 66; + CURRENT_PROJECT_VERSION = 68; DEVELOPMENT_TEAM = QJF2XZ86HB; ENABLE_HARDENED_RUNTIME = YES; INFOPLIST_FILE = InfoPlists/ShareExtensionMac.plist; @@ -1271,7 +1271,7 @@ "@executable_path/../../../../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 12.0; - MARKETING_VERSION = 1.12.1; + MARKETING_VERSION = 1.13.0; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "app.omnivore.app.ShareExtension-Mac"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -1343,7 +1343,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 66; + CURRENT_PROJECT_VERSION = 68; DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_TEAM = QJF2XZ86HB; ENABLE_HARDENED_RUNTIME = YES; @@ -1354,7 +1354,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 12.0; - MARKETING_VERSION = 1.12.1; + MARKETING_VERSION = 1.13.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = app.omnivore.app; @@ -1377,7 +1377,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 66; + CURRENT_PROJECT_VERSION = 68; DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_TEAM = QJF2XZ86HB; ENABLE_HARDENED_RUNTIME = YES; @@ -1388,7 +1388,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 12.0; - MARKETING_VERSION = 1.12.1; + MARKETING_VERSION = 1.13.0; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = app.omnivore.app; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -1443,7 +1443,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.12.1; + MARKETING_VERSION = 1.13.0; PRODUCT_BUNDLE_IDENTIFIER = app.omnivore.app; PRODUCT_NAME = Omnivore; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1475,7 +1475,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.12.1; + MARKETING_VERSION = 1.13.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; OTHER_LDFLAGS = ( @@ -1514,7 +1514,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.12.1; + MARKETING_VERSION = 1.13.0; MTL_FAST_MATH = YES; OTHER_LDFLAGS = ( "-framework", @@ -1540,7 +1540,7 @@ CODE_SIGN_ENTITLEMENTS = "Entitlements/SafariExtension-Mac.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 66; + CURRENT_PROJECT_VERSION = 68; DEVELOPMENT_TEAM = QJF2XZ86HB; ENABLE_HARDENED_RUNTIME = YES; GENERATE_INFOPLIST_FILE = YES; @@ -1553,7 +1553,7 @@ "@executable_path/../../../../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 12.0; - MARKETING_VERSION = 1.12.1; + MARKETING_VERSION = 1.13.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; OTHER_LDFLAGS = ( @@ -1579,7 +1579,7 @@ CODE_SIGN_ENTITLEMENTS = "Entitlements/SafariExtension-Mac.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 66; + CURRENT_PROJECT_VERSION = 68; DEVELOPMENT_TEAM = QJF2XZ86HB; ENABLE_HARDENED_RUNTIME = YES; GENERATE_INFOPLIST_FILE = YES; @@ -1592,7 +1592,7 @@ "@executable_path/../../../../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 12.0; - MARKETING_VERSION = 1.12.1; + MARKETING_VERSION = 1.13.0; MTL_FAST_MATH = YES; OTHER_LDFLAGS = ( "-framework", @@ -1679,7 +1679,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.12.1; + MARKETING_VERSION = 1.13.0; PRODUCT_BUNDLE_IDENTIFIER = "app.omnivore.app.share-extension"; PRODUCT_NAME = ShareExtension; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1734,7 +1734,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.12.1; + MARKETING_VERSION = 1.13.0; PRODUCT_BUNDLE_IDENTIFIER = app.omnivore.app; PRODUCT_NAME = Omnivore; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1763,7 +1763,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.12.1; + MARKETING_VERSION = 1.13.0; PRODUCT_BUNDLE_IDENTIFIER = "app.omnivore.app.share-extension"; PRODUCT_NAME = ShareExtension; PROVISIONING_PROFILE_SPECIFIER = ""; From 0c16f5e9daab3dfde74c2217bd4c0c2655f00702 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Mon, 25 Jul 2022 15:33:46 -0700 Subject: [PATCH 02/17] add autofill entitlement to ios --- apple/Entitlements/Omnivore.entitlements | 16 ++++++++++------ apple/Omnivore.xcodeproj/project.pbxproj | 4 ++++ 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/apple/Entitlements/Omnivore.entitlements b/apple/Entitlements/Omnivore.entitlements index b53defce6..270cdb0d4 100644 --- a/apple/Entitlements/Omnivore.entitlements +++ b/apple/Entitlements/Omnivore.entitlements @@ -8,6 +8,16 @@ Default + com.apple.developer.associated-domains + + applinks:omnivore.app + applinks:dev.omnivore.app + applinks:demo.omnivore.app + webcredentials:omnivore.app + webcredentials:dev.omnivore.app + + com.apple.developer.authentication-services.autofill-credential-provider + com.apple.security.application-groups group.app.omnivoreapp @@ -16,11 +26,5 @@ $(AppIdentifierPrefix)app.omnivore.shared - com.apple.developer.associated-domains - - applinks:omnivore.app - applinks:dev.omnivore.app - applinks:demo.omnivore.app - diff --git a/apple/Omnivore.xcodeproj/project.pbxproj b/apple/Omnivore.xcodeproj/project.pbxproj index e35874bbe..fff87604d 100644 --- a/apple/Omnivore.xcodeproj/project.pbxproj +++ b/apple/Omnivore.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 040333C9288F50B5008F0BDF /* AuthenticationServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 040333C8288F50B5008F0BDF /* AuthenticationServices.framework */; }; 0411792A26A22860004AE24F /* MacAppSmokeTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0411792926A22860004AE24F /* MacAppSmokeTest.swift */; }; 0418837E2742E99F003E0001 /* Intercom in Frameworks */ = {isa = PBXBuildFile; productRef = 0418837D2742E99F003E0001 /* Intercom */; }; 041883802742FCF2003E0001 /* Utils in Frameworks */ = {isa = PBXBuildFile; productRef = 0418837F2742FCF2003E0001 /* Utils */; }; @@ -174,6 +175,7 @@ /* Begin PBXFileReference section */ 03BE831A6CCC63104CB366AE /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 040333C8288F50B5008F0BDF /* AuthenticationServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AuthenticationServices.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS15.4.sdk/System/Library/Frameworks/AuthenticationServices.framework; sourceTree = DEVELOPER_DIR; }; 0411792726A22860004AE24F /* UnitTests-MacApp.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "UnitTests-MacApp.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 0411792926A22860004AE24F /* MacAppSmokeTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MacAppSmokeTest.swift; sourceTree = ""; }; 0411792B26A22860004AE24F /* MacUnitTests.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = MacUnitTests.plist; sourceTree = ""; }; @@ -302,6 +304,7 @@ buildActionMask = 2147483647; files = ( D2BC24479582C1B97F67646A /* App in Frameworks */, + 040333C9288F50B5008F0BDF /* AuthenticationServices.framework in Frameworks */, 0418837E2742E99F003E0001 /* Intercom in Frameworks */, 045B1681279147E7005047F7 /* FirebaseMessaging in Frameworks */, 041883802742FCF2003E0001 /* Utils in Frameworks */, @@ -423,6 +426,7 @@ 4214F6EE2714D2260096B644 /* Frameworks */ = { isa = PBXGroup; children = ( + 040333C8288F50B5008F0BDF /* AuthenticationServices.framework */, ); name = Frameworks; sourceTree = ""; From 3dd249564e6dd4dcb9f69f9efc2da9d8a4488e56 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Tue, 26 Jul 2022 15:21:28 -0700 Subject: [PATCH 03/17] stub in email auth modal --- .../Sources/App/Views/WelcomeView.swift | 40 +++++++++++++++---- 1 file changed, 33 insertions(+), 7 deletions(-) diff --git a/apple/OmnivoreKit/Sources/App/Views/WelcomeView.swift b/apple/OmnivoreKit/Sources/App/Views/WelcomeView.swift index f7361e714..289506352 100644 --- a/apple/OmnivoreKit/Sources/App/Views/WelcomeView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/WelcomeView.swift @@ -17,6 +17,7 @@ struct WelcomeView: View { @State private var showTermsLinks = false @State private var showTermsModal = false @State private var showPrivacyModal = false + @State private var showEmailLoginModal = false @State private var showAboutPage = false @State private var selectedEnvironment = AppEnvironment.initialAppEnvironment @State private var containerSize: CGSize = .zero @@ -138,11 +139,7 @@ struct WelcomeView: View { var authProviderButtonStack: some View { let useHorizontalLayout = containerSize.width > 500 - let buttonGroup = Group { - AppleSignInButton { - viewModel.handleAppleSignInCompletion(result: $0, authenticator: authenticator) - } - + let googleButton = Group { if AppKeys.sharedInstance?.iosClientGoogleId != nil { GoogleAuthButton { Task { @@ -152,12 +149,38 @@ struct WelcomeView: View { } } + let appleButton = AppleSignInButton { + viewModel.handleAppleSignInCompletion(result: $0, authenticator: authenticator) + } + + let emailButton = Button( + action: { showEmailLoginModal = true }, + label: { + Text("Continue with Email") + .foregroundColor(.appGrayTextContrast) + .underline() + } + ) + .padding(.vertical) + return VStack(alignment: .center, spacing: 16) { if useHorizontalLayout { - HStack { buttonGroup } + VStack(alignment: .leading, spacing: 0) { + HStack { + appleButton + googleButton + } + emailButton + } } else { - buttonGroup + VStack(alignment: .leading, spacing: 0) { + VStack(alignment: .leading, spacing: 16) { + appleButton + googleButton + } + emailButton + } } if let loginError = viewModel.loginError { @@ -206,6 +229,9 @@ struct WelcomeView: View { Spacer() } .padding() + .sheet(isPresented: $showEmailLoginModal) { + Text("Email Login") + } .sheet(isPresented: $showDebugModal) { DebugMenuView(selectedEnvironment: $selectedEnvironment) } From 463c0059fa8e34143b2bca1e3a85f0b44354a245 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Tue, 26 Jul 2022 22:27:16 -0700 Subject: [PATCH 04/17] add emailLoginView --- .../Views/Registration/EmailLoginView.swift | 94 +++++++++++++++++++ .../Sources/App/Views/WelcomeView.swift | 2 +- 2 files changed, 95 insertions(+), 1 deletion(-) create mode 100644 apple/OmnivoreKit/Sources/App/Views/Registration/EmailLoginView.swift diff --git a/apple/OmnivoreKit/Sources/App/Views/Registration/EmailLoginView.swift b/apple/OmnivoreKit/Sources/App/Views/Registration/EmailLoginView.swift new file mode 100644 index 000000000..3c273784b --- /dev/null +++ b/apple/OmnivoreKit/Sources/App/Views/Registration/EmailLoginView.swift @@ -0,0 +1,94 @@ +import Models +import Services +import SwiftUI +import Utils +import Views + +@MainActor final class EmailLoginViewModel: ObservableObject { + @Published var loginError: LoginError? + + func submitCredentials( + email: String, + password: String, + authenticator: Authenticator + ) { + print(email) + print(password) + print(authenticator.hasValidAuthToken) + } +} + +struct EmailLoginView: View { + @Environment(\.horizontalSizeClass) var horizontalSizeClass + @Environment(\.presentationMode) private var presentationMode + @EnvironmentObject var authenticator: Authenticator + @EnvironmentObject var dataService: DataService + @StateObject private var viewModel = EmailLoginViewModel() + + @State private var email = "" + @State private var password = "" + + var body: some View { + NavigationView { + VStack(spacing: 0) { + VStack(spacing: 28) { + ScrollView(showsIndicators: false) { + if horizontalSizeClass == .regular { + Spacer(minLength: 150) + } + VStack(alignment: .center, spacing: 16) { + VStack(spacing: 16) { + VStack(alignment: .leading, spacing: 6) { + Text("Email") + .font(.appFootnote) + .foregroundColor(.appGrayText) + TextField("", text: $email) + .textContentType(.emailAddress) + } + + VStack(alignment: .leading, spacing: 6) { + Text("Password") + .font(.appFootnote) + .foregroundColor(.appGrayText) + TextField("", text: $password) + .textContentType(.password) + } + + Button( + action: { + viewModel.submitCredentials( + email: email, + password: password, + authenticator: authenticator + ) + }, + label: { Text("Submit") } + ) + .buttonStyle(SolidCapsuleButtonStyle(color: .appDeepBackground, width: 300)) + + if let loginError = viewModel.loginError { + LoginErrorMessageView(loginError: loginError) + } + } + .textFieldStyle(StandardTextFieldStyle()) + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + + Spacer() + } + } + .frame(maxWidth: 300) + .navigationTitle("Sign In") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .barTrailing) { + Button( + action: { presentationMode.wrappedValue.dismiss() }, + label: { Image(systemName: "xmark").foregroundColor(.appGrayTextContrast) } + ) + } + } + } + } +} diff --git a/apple/OmnivoreKit/Sources/App/Views/WelcomeView.swift b/apple/OmnivoreKit/Sources/App/Views/WelcomeView.swift index 289506352..3202c6734 100644 --- a/apple/OmnivoreKit/Sources/App/Views/WelcomeView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/WelcomeView.swift @@ -230,7 +230,7 @@ struct WelcomeView: View { } .padding() .sheet(isPresented: $showEmailLoginModal) { - Text("Email Login") + EmailLoginView() } .sheet(isPresented: $showDebugModal) { DebugMenuView(selectedEnvironment: $selectedEnvironment) From f44f768af7c127b575ae6a27163a896d781c1df5 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Tue, 26 Jul 2022 23:02:57 -0700 Subject: [PATCH 05/17] stub in network calls to ios email login --- .../Views/Registration/EmailLoginView.swift | 22 +++++++++------- .../Authentication/AccountCreator.swift | 21 ++++++++++++++++ .../Services/Authentication/AuthModels.swift | 5 ++++ .../VerifyAuthProviderToken.swift | 25 +++++++++++++++++++ 4 files changed, 64 insertions(+), 9 deletions(-) diff --git a/apple/OmnivoreKit/Sources/App/Views/Registration/EmailLoginView.swift b/apple/OmnivoreKit/Sources/App/Views/Registration/EmailLoginView.swift index 3c273784b..fde8b988e 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Registration/EmailLoginView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Registration/EmailLoginView.swift @@ -11,10 +11,12 @@ import Views email: String, password: String, authenticator: Authenticator - ) { - print(email) - print(password) - print(authenticator.hasValidAuthToken) + ) async { + do { + try await authenticator.submitEmailLogin(email: email, password: password) + } catch { + loginError = error as? LoginError + } } } @@ -56,11 +58,13 @@ struct EmailLoginView: View { Button( action: { - viewModel.submitCredentials( - email: email, - password: password, - authenticator: authenticator - ) + Task { + await viewModel.submitCredentials( + email: email, + password: password, + authenticator: authenticator + ) + } }, label: { Text("Submit") } ) diff --git a/apple/OmnivoreKit/Sources/Services/Authentication/AccountCreator.swift b/apple/OmnivoreKit/Sources/Services/Authentication/AccountCreator.swift index 9ef9d3321..e58b9b1e9 100644 --- a/apple/OmnivoreKit/Sources/Services/Authentication/AccountCreator.swift +++ b/apple/OmnivoreKit/Sources/Services/Authentication/AccountCreator.swift @@ -45,3 +45,24 @@ extension Authenticator { } } } + +// email auth +public extension Authenticator { + func submitEmailLogin( + email: String, + password: String + ) async throws { + do { + let params = EmailSignInParams(email: email, password: password) + let authPayload = try await networker.submitEmailLogin(params: params) + try ValetKey.authCookieString.setValue(authPayload.commentedAuthCookieString) + try ValetKey.authToken.setValue(authPayload.authToken) + DispatchQueue.main.async { + self.isLoggedIn = true + } + } catch { + let serverError = (error as? ServerError) ?? ServerError.unknown + throw LoginError.make(serverError: serverError) + } + } +} diff --git a/apple/OmnivoreKit/Sources/Services/Authentication/AuthModels.swift b/apple/OmnivoreKit/Sources/Services/Authentication/AuthModels.swift index fafdee599..e3149c4dd 100644 --- a/apple/OmnivoreKit/Sources/Services/Authentication/AuthModels.swift +++ b/apple/OmnivoreKit/Sources/Services/Authentication/AuthModels.swift @@ -18,6 +18,11 @@ struct SignInParams: Encodable { let provider: AuthProvider } +struct EmailSignInParams: Encodable { + let email: String + let password: String +} + enum AuthProvider: String, Encodable { case apple = "APPLE" case google = "GOOGLE" diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Networking/ServerResources/VerifyAuthProviderToken.swift b/apple/OmnivoreKit/Sources/Services/DataService/Networking/ServerResources/VerifyAuthProviderToken.swift index febc2f0cf..9ed72444c 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Networking/ServerResources/VerifyAuthProviderToken.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Networking/ServerResources/VerifyAuthProviderToken.swift @@ -36,4 +36,29 @@ extension Networker { } } } + + func submitEmailLogin(params: EmailSignInParams) async throws -> AuthPayload { + let encodedParams = (try? JSONEncoder().encode(params)) ?? Data() + + let urlRequest = URLRequest.create( + baseURL: appEnvironment.serverBaseURL, + urlPath: "/api/mobile-auth/email-login", + requestMethod: .post(params: encodedParams) + ) + + let resource = ServerResource( + urlRequest: urlRequest, + decode: AuthPayload.decode + ) + + do { + return try await urlSession.performRequest(resource: resource) + } catch { + if let error = error as? ServerError { + throw LoginError.make(serverError: error) + } else { + throw LoginError.unknown + } + } + } } From 6a100051ac73706f3ea8893ed791aa3e9d2b070c Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Tue, 26 Jul 2022 23:40:44 -0700 Subject: [PATCH 06/17] add mobile email sign in route to api --- .../Views/Registration/RegistrationView.swift | 4 +- .../Models/ErrorModels/LoginError.swift | 1 + .../Services/Authentication/AuthModels.swift | 2 + .../Services/Authentication/GoogleAuth.swift | 2 +- .../DataService/Errors/ServerError.swift | 4 ++ .../VerifyAuthProviderToken.swift | 2 +- .../LoginErrorMessageView.swift | 2 + .../routers/auth/mobile/mobile_auth_router.ts | 11 +++- .../api/src/routers/auth/mobile/sign_in.ts | 51 +++++++++++++++++++ 9 files changed, 74 insertions(+), 5 deletions(-) diff --git a/apple/OmnivoreKit/Sources/App/Views/Registration/RegistrationView.swift b/apple/OmnivoreKit/Sources/App/Views/Registration/RegistrationView.swift index f365a467e..8bd5754ab 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Registration/RegistrationView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Registration/RegistrationView.swift @@ -20,7 +20,7 @@ import Views Task { await handleAppleToken(payload: payload, authenticator: authenticator) } case let .failure(error): switch error { - case .unauthorized, .unknown: + case .unauthorized, .unknown, .pendingEmailVerification: break case .network: loginError = error @@ -34,7 +34,7 @@ import Views } catch { let submitTokenError = (error as? LoginError) ?? .unknown switch submitTokenError { - case .unauthorized, .unknown: + case .unauthorized, .unknown, .pendingEmailVerification: await handleAppleSignUp(authenticator: authenticator, payload: payload) case .network: loginError = submitTokenError diff --git a/apple/OmnivoreKit/Sources/Models/ErrorModels/LoginError.swift b/apple/OmnivoreKit/Sources/Models/ErrorModels/LoginError.swift index 8be887841..438fbb983 100644 --- a/apple/OmnivoreKit/Sources/Models/ErrorModels/LoginError.swift +++ b/apple/OmnivoreKit/Sources/Models/ErrorModels/LoginError.swift @@ -4,4 +4,5 @@ public enum LoginError: Error { case unauthorized case network case unknown + case pendingEmailVerification } diff --git a/apple/OmnivoreKit/Sources/Services/Authentication/AuthModels.swift b/apple/OmnivoreKit/Sources/Services/Authentication/AuthModels.swift index e3149c4dd..97e165415 100644 --- a/apple/OmnivoreKit/Sources/Services/Authentication/AuthModels.swift +++ b/apple/OmnivoreKit/Sources/Services/Authentication/AuthModels.swift @@ -58,6 +58,8 @@ extension LoginError { return .unauthorized case .unknown: return .unknown + case .pendingEmailVerification: + return .pendingEmailVerification } } } diff --git a/apple/OmnivoreKit/Sources/Services/Authentication/GoogleAuth.swift b/apple/OmnivoreKit/Sources/Services/Authentication/GoogleAuth.swift index 9c3f081ab..2fa15f0f2 100644 --- a/apple/OmnivoreKit/Sources/Services/Authentication/GoogleAuth.swift +++ b/apple/OmnivoreKit/Sources/Services/Authentication/GoogleAuth.swift @@ -31,7 +31,7 @@ extension Authenticator { switch loginError { case .unauthorized, .unknown: return await createPendingUser(idToken: idToken) - case .network: + case .network, .pendingEmailVerification: return .loginError(error: .network) } } diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Errors/ServerError.swift b/apple/OmnivoreKit/Sources/Services/DataService/Errors/ServerError.swift index c3781974f..601b2e4cf 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Errors/ServerError.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Errors/ServerError.swift @@ -10,11 +10,15 @@ public enum ServerError: String, Error { case unauthenticated case timeout case unknown + case pendingEmailVerification } extension ServerError { init(serverResponse: ServerResponse) { switch serverResponse.httpUrlResponse?.statusCode { + case 418?: + self = .pendingEmailVerification + return case 401?, 403?: self = .unauthenticated return diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Networking/ServerResources/VerifyAuthProviderToken.swift b/apple/OmnivoreKit/Sources/Services/DataService/Networking/ServerResources/VerifyAuthProviderToken.swift index 9ed72444c..3943d5807 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Networking/ServerResources/VerifyAuthProviderToken.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Networking/ServerResources/VerifyAuthProviderToken.swift @@ -42,7 +42,7 @@ extension Networker { let urlRequest = URLRequest.create( baseURL: appEnvironment.serverBaseURL, - urlPath: "/api/mobile-auth/email-login", + urlPath: "/api/mobile-auth/email-sign-in", requestMethod: .post(params: encodedParams) ) diff --git a/apple/OmnivoreKit/Sources/Views/RegistrationViews/LoginErrorMessageView.swift b/apple/OmnivoreKit/Sources/Views/RegistrationViews/LoginErrorMessageView.swift index 4e3ccf36c..fcb06e2fb 100644 --- a/apple/OmnivoreKit/Sources/Views/RegistrationViews/LoginErrorMessageView.swift +++ b/apple/OmnivoreKit/Sources/Views/RegistrationViews/LoginErrorMessageView.swift @@ -25,6 +25,8 @@ private extension LoginError { return LocalText.networkError case .unknown: return LocalText.genericError + case .pendingEmailVerification: + return "Please check your email for a verification message." } } } diff --git a/packages/api/src/routers/auth/mobile/mobile_auth_router.ts b/packages/api/src/routers/auth/mobile/mobile_auth_router.ts index 6ff44219b..eb85649dd 100644 --- a/packages/api/src/routers/auth/mobile/mobile_auth_router.ts +++ b/packages/api/src/routers/auth/mobile/mobile_auth_router.ts @@ -2,7 +2,10 @@ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ import express from 'express' -import { createMobileSignInResponse } from './sign_in' +import { + createMobileSignInResponse, + createMobileEmailSignInResponse, +} from './sign_in' import { createMobileSignUpResponse } from './sign_up' import { createMobileAccountCreationResponse } from './account_creation' @@ -15,6 +18,12 @@ export function mobileAuthRouter() { res.status(payload.statusCode).json(payload.json) }) + router.post('/email-sign-in', async (req, res) => { + const { email, password } = req.body + const payload = await createMobileEmailSignInResponse(email, password) + res.status(payload.statusCode).json(payload.json) + }) + router.post('/sign-up', async (req, res) => { const { token, provider, name } = req.body const payload = await createMobileSignUpResponse(token, provider, name) diff --git a/packages/api/src/routers/auth/mobile/sign_in.ts b/packages/api/src/routers/auth/mobile/sign_in.ts index c3d5112f0..8902ff482 100644 --- a/packages/api/src/routers/auth/mobile/sign_in.ts +++ b/packages/api/src/routers/auth/mobile/sign_in.ts @@ -8,6 +8,11 @@ import { } from '../auth_types' import { createMobileAuthPayload } from '../jwt_helpers' import UserModel from '../../../datalayer/user' +import { initModels } from '../../../server' +import { sendConfirmationEmail } from '../../../services/send_emails' +import { kx } from '../../../datalayer/knex_config' +import { StatusType } from '../../../datalayer/user/model' +import { comparePassword } from '../../../utils/auth' export async function createMobileSignInResponse( token?: string, @@ -31,6 +36,52 @@ export async function createMobileSignInResponse( } } +export async function createMobileEmailSignInResponse( + email?: string, + password?: string +): Promise { + try { + if (!email || !password) { + throw new Error('Missing username or password') + } + + const models = initModels(kx, false) + const user = await models.user.getWhere({ + email, + }) + + if (!user?.id || !user?.password) { + throw new Error('user not found') + } + + const validPassword = await comparePassword(password, user.password) + if (!validPassword) { + throw new Error('password is invalid') + } + + if (user.status === StatusType.Pending && user.email) { + await sendConfirmationEmail({ + id: user.id, + email: user.email, + name: user.name, + }) + return { + statusCode: 418, + json: { errorCodes: ['PENDING_VERIFICATION'] }, + } + } + + const mobileAuthPayload = await createMobileAuthPayload(user.id) + + return { + statusCode: 200, + json: mobileAuthPayload, + } + } catch (e) { + return authFailedPayload + } +} + const authFailedPayload = { statusCode: 403, json: { errorCodes: ['AUTH_FAILED'] }, From 7dc1722ad2b4a4ee995ccf8061f68510264c110a Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Thu, 28 Jul 2022 14:17:03 -0700 Subject: [PATCH 07/17] change background color of sign in modal (ios) --- .../Views/Registration/EmailLoginView.swift | 64 ++++++++++++------- .../Sources/Models/AppEnvironment.swift | 13 ---- 2 files changed, 42 insertions(+), 35 deletions(-) diff --git a/apple/OmnivoreKit/Sources/App/Views/Registration/EmailLoginView.swift b/apple/OmnivoreKit/Sources/App/Views/Registration/EmailLoginView.swift index fde8b988e..9df216ec8 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Registration/EmailLoginView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Registration/EmailLoginView.swift @@ -32,29 +32,36 @@ struct EmailLoginView: View { var body: some View { NavigationView { - VStack(spacing: 0) { - VStack(spacing: 28) { - ScrollView(showsIndicators: false) { - if horizontalSizeClass == .regular { - Spacer(minLength: 150) - } - VStack(alignment: .center, spacing: 16) { - VStack(spacing: 16) { + ZStack { + Color.appBackground.edgesIgnoringSafeArea(.all) + VStack(spacing: 0) { + VStack(spacing: 28) { + ScrollView(showsIndicators: false) { + if horizontalSizeClass == .regular { + Spacer(minLength: 150) + } + VStack { VStack(alignment: .leading, spacing: 6) { Text("Email") .font(.appFootnote) .foregroundColor(.appGrayText) TextField("", text: $email) .textContentType(.emailAddress) + .textInputAutocapitalization(.never) + .disableAutocorrection(true) } + .padding(.bottom, 8) VStack(alignment: .leading, spacing: 6) { Text("Password") .font(.appFootnote) .foregroundColor(.appGrayText) - TextField("", text: $password) + SecureField("", text: $password) .textContentType(.password) + .textInputAutocapitalization(.never) + .disableAutocorrection(true) } + .padding(.bottom, 16) Button( action: { @@ -73,24 +80,37 @@ struct EmailLoginView: View { if let loginError = viewModel.loginError { LoginErrorMessageView(loginError: loginError) } + + HStack { + Button( + action: { print("switch to email signup") }, + label: { + Text("Don't have an account?") + .foregroundColor(.appGrayTextContrast) + .underline() + } + ) + .padding(.vertical) + Spacer() + } } .textFieldStyle(StandardTextFieldStyle()) } - } - .frame(maxWidth: .infinity, maxHeight: .infinity) + .frame(maxWidth: .infinity, maxHeight: .infinity) - Spacer() + Spacer() + } } - } - .frame(maxWidth: 300) - .navigationTitle("Sign In") - .navigationBarTitleDisplayMode(.inline) - .toolbar { - ToolbarItem(placement: .barTrailing) { - Button( - action: { presentationMode.wrappedValue.dismiss() }, - label: { Image(systemName: "xmark").foregroundColor(.appGrayTextContrast) } - ) + .frame(maxWidth: 300) + .navigationTitle("Sign In") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .barTrailing) { + Button( + action: { presentationMode.wrappedValue.dismiss() }, + label: { Image(systemName: "xmark").foregroundColor(.appGrayTextContrast) } + ) + } } } } diff --git a/apple/OmnivoreKit/Sources/Models/AppEnvironment.swift b/apple/OmnivoreKit/Sources/Models/AppEnvironment.swift index e49cb03f9..b6c94f86e 100644 --- a/apple/OmnivoreKit/Sources/Models/AppEnvironment.swift +++ b/apple/OmnivoreKit/Sources/Models/AppEnvironment.swift @@ -63,17 +63,4 @@ public extension AppEnvironment { return URL(string: "http://localhost:3000")! } } - - var highlightsServerBaseURL: URL { - switch self { - case .dev: - return URL(string: devHighlightsServerURL)! - case .demo: - return URL(string: demoHighlightsServerURL)! - case .prod: - return URL(string: prodHighlightsServerURL)! - case .test, .local: - return URL(string: "http://localhost:8080")! - } - } } From 4e7010f7c179d93566c5e57f57e18dbaffd4b5a0 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Thu, 28 Jul 2022 15:32:47 -0700 Subject: [PATCH 08/17] stub in signup view and toggle when button tapped --- .../Views/Registration/EmailAuthView.swift | 263 ++++++++++++++++++ .../Views/Registration/EmailLoginView.swift | 118 -------- .../Sources/App/Views/WelcomeView.swift | 2 +- .../components/templates/auth/ResetSent.tsx | 30 +- 4 files changed, 280 insertions(+), 133 deletions(-) create mode 100644 apple/OmnivoreKit/Sources/App/Views/Registration/EmailAuthView.swift delete mode 100644 apple/OmnivoreKit/Sources/App/Views/Registration/EmailLoginView.swift diff --git a/apple/OmnivoreKit/Sources/App/Views/Registration/EmailAuthView.swift b/apple/OmnivoreKit/Sources/App/Views/Registration/EmailAuthView.swift new file mode 100644 index 000000000..9bbd0f154 --- /dev/null +++ b/apple/OmnivoreKit/Sources/App/Views/Registration/EmailAuthView.swift @@ -0,0 +1,263 @@ +import Models +import Services +import SwiftUI +import Utils +import Views + +enum EmailAuthState { + case signIn + case signUp + case loading +} + +@MainActor final class EmailAuthViewModel: ObservableObject { + @Published var loginError: LoginError? + @Published var emailAuthState = EmailAuthState.loading + + func loadAuthState() { + // check tokens here to determine pending/active/no user + emailAuthState = .signUp + } + + func submitCredentials( + email: String, + password: String, + authenticator: Authenticator + ) async { + do { + try await authenticator.submitEmailLogin(email: email, password: password) + } catch { + loginError = error as? LoginError + } + } +} + +struct EmailAuthView: View { + @Environment(\.presentationMode) private var presentationMode + @StateObject private var viewModel = EmailAuthViewModel() + + @ViewBuilder var primaryContent: some View { + switch viewModel.emailAuthState { + case .signUp: + EmailSignupFormView(viewModel: viewModel) + case .signIn: + EmailLoginFormView(viewModel: viewModel) + case .loading: + VStack { + Spacer() + ProgressView() + Spacer() + } + } + } + + var body: some View { + NavigationView { + ZStack { + Color.appBackground.edgesIgnoringSafeArea(.all) + primaryContent + .frame(maxWidth: 300) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .barTrailing) { + Button( + action: { presentationMode.wrappedValue.dismiss() }, + label: { Image(systemName: "xmark").foregroundColor(.appGrayTextContrast) } + ) + } + } + } + } + .task { + viewModel.loadAuthState() + } + } +} + +struct EmailLoginFormView: View { + enum FocusedField { + case email, password + } + + @Environment(\.horizontalSizeClass) var horizontalSizeClass + @EnvironmentObject var authenticator: Authenticator + @ObservedObject var viewModel: EmailAuthViewModel + + @FocusState private var focusedField: FocusedField? + @State private var email = "" + @State private var password = "" + + var body: some View { + VStack(spacing: 0) { + VStack(spacing: 28) { + ScrollView(showsIndicators: false) { + if horizontalSizeClass == .regular { + Spacer(minLength: 150) + } + VStack { + VStack(alignment: .leading, spacing: 6) { + Text("Email") + .font(.appFootnote) + .foregroundColor(.appGrayText) + TextField("", text: $email) + .textContentType(.emailAddress) + .textInputAutocapitalization(.never) + .disableAutocorrection(true) + .focused($focusedField, equals: .email) + } + .padding(.bottom, 8) + + VStack(alignment: .leading, spacing: 6) { + Text("Password") + .font(.appFootnote) + .foregroundColor(.appGrayText) + SecureField("", text: $password) + .textContentType(.password) + .textInputAutocapitalization(.never) + .disableAutocorrection(true) + .focused($focusedField, equals: .password) + } + .padding(.bottom, 16) + + Button( + action: { + Task { + await viewModel.submitCredentials( + email: email, + password: password, + authenticator: authenticator + ) + } + }, + label: { Text("Submit") } + ) + .buttonStyle(SolidCapsuleButtonStyle(color: .appDeepBackground, width: 300)) + + if let loginError = viewModel.loginError { + LoginErrorMessageView(loginError: loginError) + } + + HStack { + Button( + action: { viewModel.emailAuthState = .signUp }, + label: { + Text("Don't have an account?") + .foregroundColor(.appGrayTextContrast) + .underline() + } + ) + .padding(.vertical) + Spacer() + } + } + .textFieldStyle(StandardTextFieldStyle()) + .onSubmit { + if focusedField == .email { + focusedField = .password + } else { + focusedField = nil + } + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + + Spacer() + } + } + .navigationTitle("Sign In") + } +} + +struct EmailSignupFormView: View { + enum FocusedField { + case email, password, fullName, username + } + + @Environment(\.horizontalSizeClass) var horizontalSizeClass + @EnvironmentObject var authenticator: Authenticator + @ObservedObject var viewModel: EmailAuthViewModel + + @FocusState private var focusedField: FocusedField? + @State private var email = "" + @State private var password = "" + + var body: some View { + VStack(spacing: 0) { + VStack(spacing: 28) { + ScrollView(showsIndicators: false) { + if horizontalSizeClass == .regular { + Spacer(minLength: 150) + } + VStack { + VStack(alignment: .leading, spacing: 6) { + Text("Email") + .font(.appFootnote) + .foregroundColor(.appGrayText) + TextField("", text: $email) + .textContentType(.emailAddress) + .textInputAutocapitalization(.never) + .disableAutocorrection(true) + .focused($focusedField, equals: .email) + } + .padding(.bottom, 8) + + VStack(alignment: .leading, spacing: 6) { + Text("Password") + .font(.appFootnote) + .foregroundColor(.appGrayText) + SecureField("", text: $password) + .textContentType(.password) + .textInputAutocapitalization(.never) + .disableAutocorrection(true) + .focused($focusedField, equals: .password) + } + .padding(.bottom, 16) + + Button( + action: { + Task { + await viewModel.submitCredentials( + email: email, + password: password, + authenticator: authenticator + ) + } + }, + label: { Text("Submit") } + ) + .buttonStyle(SolidCapsuleButtonStyle(color: .appDeepBackground, width: 300)) + + if let loginError = viewModel.loginError { + LoginErrorMessageView(loginError: loginError) + } + + HStack { + Button( + action: { viewModel.emailAuthState = .signIn }, + label: { + Text("Already have an account?") + .foregroundColor(.appGrayTextContrast) + .underline() + } + ) + .padding(.vertical) + Spacer() + } + } + .textFieldStyle(StandardTextFieldStyle()) + .onSubmit { + if focusedField == .email { + focusedField = .password + } else { + focusedField = nil + } + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + + Spacer() + } + } + .navigationTitle("Sign Up") + } +} diff --git a/apple/OmnivoreKit/Sources/App/Views/Registration/EmailLoginView.swift b/apple/OmnivoreKit/Sources/App/Views/Registration/EmailLoginView.swift deleted file mode 100644 index 9df216ec8..000000000 --- a/apple/OmnivoreKit/Sources/App/Views/Registration/EmailLoginView.swift +++ /dev/null @@ -1,118 +0,0 @@ -import Models -import Services -import SwiftUI -import Utils -import Views - -@MainActor final class EmailLoginViewModel: ObservableObject { - @Published var loginError: LoginError? - - func submitCredentials( - email: String, - password: String, - authenticator: Authenticator - ) async { - do { - try await authenticator.submitEmailLogin(email: email, password: password) - } catch { - loginError = error as? LoginError - } - } -} - -struct EmailLoginView: View { - @Environment(\.horizontalSizeClass) var horizontalSizeClass - @Environment(\.presentationMode) private var presentationMode - @EnvironmentObject var authenticator: Authenticator - @EnvironmentObject var dataService: DataService - @StateObject private var viewModel = EmailLoginViewModel() - - @State private var email = "" - @State private var password = "" - - var body: some View { - NavigationView { - ZStack { - Color.appBackground.edgesIgnoringSafeArea(.all) - VStack(spacing: 0) { - VStack(spacing: 28) { - ScrollView(showsIndicators: false) { - if horizontalSizeClass == .regular { - Spacer(minLength: 150) - } - VStack { - VStack(alignment: .leading, spacing: 6) { - Text("Email") - .font(.appFootnote) - .foregroundColor(.appGrayText) - TextField("", text: $email) - .textContentType(.emailAddress) - .textInputAutocapitalization(.never) - .disableAutocorrection(true) - } - .padding(.bottom, 8) - - VStack(alignment: .leading, spacing: 6) { - Text("Password") - .font(.appFootnote) - .foregroundColor(.appGrayText) - SecureField("", text: $password) - .textContentType(.password) - .textInputAutocapitalization(.never) - .disableAutocorrection(true) - } - .padding(.bottom, 16) - - Button( - action: { - Task { - await viewModel.submitCredentials( - email: email, - password: password, - authenticator: authenticator - ) - } - }, - label: { Text("Submit") } - ) - .buttonStyle(SolidCapsuleButtonStyle(color: .appDeepBackground, width: 300)) - - if let loginError = viewModel.loginError { - LoginErrorMessageView(loginError: loginError) - } - - HStack { - Button( - action: { print("switch to email signup") }, - label: { - Text("Don't have an account?") - .foregroundColor(.appGrayTextContrast) - .underline() - } - ) - .padding(.vertical) - Spacer() - } - } - .textFieldStyle(StandardTextFieldStyle()) - } - .frame(maxWidth: .infinity, maxHeight: .infinity) - - Spacer() - } - } - .frame(maxWidth: 300) - .navigationTitle("Sign In") - .navigationBarTitleDisplayMode(.inline) - .toolbar { - ToolbarItem(placement: .barTrailing) { - Button( - action: { presentationMode.wrappedValue.dismiss() }, - label: { Image(systemName: "xmark").foregroundColor(.appGrayTextContrast) } - ) - } - } - } - } - } -} diff --git a/apple/OmnivoreKit/Sources/App/Views/WelcomeView.swift b/apple/OmnivoreKit/Sources/App/Views/WelcomeView.swift index 3202c6734..c3deec02a 100644 --- a/apple/OmnivoreKit/Sources/App/Views/WelcomeView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/WelcomeView.swift @@ -230,7 +230,7 @@ struct WelcomeView: View { } .padding() .sheet(isPresented: $showEmailLoginModal) { - EmailLoginView() + EmailAuthView() } .sheet(isPresented: $showDebugModal) { DebugMenuView(selectedEnvironment: $selectedEnvironment) diff --git a/packages/web/components/templates/auth/ResetSent.tsx b/packages/web/components/templates/auth/ResetSent.tsx index 6c64dc3af..8c95ce973 100644 --- a/packages/web/components/templates/auth/ResetSent.tsx +++ b/packages/web/components/templates/auth/ResetSent.tsx @@ -11,23 +11,25 @@ export function ResetSent(props: LoginFormProps): JSX.Element { width: '100vw', height: '100vh', bg: '$omnivoreYellow', - overflowY: 'clip' + overflowY: 'clip', }} > - -

Reset email sent

- - If there is an account assosciated with the email specified we sent a - password reset link. Click the link to reset your password. You may need - to check your spam folder. + +

Reset email sent

+ + If there is an account associated with the email specified we sent a + password reset link. Click the link to reset your password. You may + need to check your spam folder. +
-
- + ) } From 8b45095466b662fa95f6010ea8ecf98c6f2a8250 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Fri, 29 Jul 2022 11:02:36 -0700 Subject: [PATCH 09/17] add username and name to sign up form for ios --- .../Registration/CreateProfileView.swift | 2 +- .../EmailAuth/EmailAuthView.swift | 68 +++++ .../EmailAuth/EmailLoginFormView.swift | 116 ++++++++ .../EmailAuth/EmailSignupFormView.swift | 218 +++++++++++++++ .../Views/Registration/EmailAuthView.swift | 263 ------------------ 5 files changed, 403 insertions(+), 264 deletions(-) create mode 100644 apple/OmnivoreKit/Sources/App/Views/Registration/EmailAuth/EmailAuthView.swift create mode 100644 apple/OmnivoreKit/Sources/App/Views/Registration/EmailAuth/EmailLoginFormView.swift create mode 100644 apple/OmnivoreKit/Sources/App/Views/Registration/EmailAuth/EmailSignupFormView.swift delete mode 100644 apple/OmnivoreKit/Sources/App/Views/Registration/EmailAuthView.swift diff --git a/apple/OmnivoreKit/Sources/App/Views/Registration/CreateProfileView.swift b/apple/OmnivoreKit/Sources/App/Views/Registration/CreateProfileView.swift index d06681338..4164550fb 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Registration/CreateProfileView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Registration/CreateProfileView.swift @@ -224,7 +224,7 @@ struct CreateProfileView: View { } } -private extension PotentialUsernameStatus { +extension PotentialUsernameStatus { var message: String? { switch self { case .tooShort: diff --git a/apple/OmnivoreKit/Sources/App/Views/Registration/EmailAuth/EmailAuthView.swift b/apple/OmnivoreKit/Sources/App/Views/Registration/EmailAuth/EmailAuthView.swift new file mode 100644 index 000000000..18b69768d --- /dev/null +++ b/apple/OmnivoreKit/Sources/App/Views/Registration/EmailAuth/EmailAuthView.swift @@ -0,0 +1,68 @@ +import Combine +import Models +import Services +import SwiftUI +import Utils +import Views + +enum EmailAuthState { + case signIn + case signUp + case loading +} + +@MainActor final class EmailAuthViewModel: ObservableObject { + @Published var loginError: LoginError? + @Published var emailAuthState = EmailAuthState.loading + @Published var potentialUsernameStatus = PotentialUsernameStatus.noUsername + @Published var potentialUsername = "" + + var subscriptions = Set() + + func loadAuthState() { + // check tokens here to determine pending/active/no user + emailAuthState = .signIn + } +} + +struct EmailAuthView: View { + @Environment(\.presentationMode) private var presentationMode + @StateObject private var viewModel = EmailAuthViewModel() + + @ViewBuilder var primaryContent: some View { + switch viewModel.emailAuthState { + case .signUp: + EmailSignupFormView(viewModel: viewModel) + case .signIn: + EmailLoginFormView(viewModel: viewModel) + case .loading: + VStack { + Spacer() + ProgressView() + Spacer() + } + } + } + + var body: some View { + NavigationView { + ZStack { + Color.appBackground.edgesIgnoringSafeArea(.all) + primaryContent + .frame(maxWidth: 300) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .barTrailing) { + Button( + action: { presentationMode.wrappedValue.dismiss() }, + label: { Image(systemName: "xmark").foregroundColor(.appGrayTextContrast) } + ) + } + } + } + } + .task { + viewModel.loadAuthState() + } + } +} diff --git a/apple/OmnivoreKit/Sources/App/Views/Registration/EmailAuth/EmailLoginFormView.swift b/apple/OmnivoreKit/Sources/App/Views/Registration/EmailAuth/EmailLoginFormView.swift new file mode 100644 index 000000000..40756a017 --- /dev/null +++ b/apple/OmnivoreKit/Sources/App/Views/Registration/EmailAuth/EmailLoginFormView.swift @@ -0,0 +1,116 @@ +import Models +import Services +import SwiftUI +import Utils +import Views + +extension EmailAuthViewModel { + func submitCredentials( + email: String, + password: String, + authenticator: Authenticator + ) async { + do { + try await authenticator.submitEmailLogin(email: email, password: password) + } catch { + loginError = error as? LoginError + } + } +} + +struct EmailLoginFormView: View { + enum FocusedField { + case email, password + } + + @Environment(\.horizontalSizeClass) var horizontalSizeClass + @EnvironmentObject var authenticator: Authenticator + @ObservedObject var viewModel: EmailAuthViewModel + + @FocusState private var focusedField: FocusedField? + @State private var email = "" + @State private var password = "" + + var body: some View { + VStack(spacing: 0) { + VStack(spacing: 28) { + ScrollView(showsIndicators: false) { + if horizontalSizeClass == .regular { + Spacer(minLength: 150) + } + VStack { + VStack(alignment: .leading, spacing: 6) { + Text("Email") + .font(.appFootnote) + .foregroundColor(.appGrayText) + TextField("", text: $email) + .keyboardType(.emailAddress) + .textContentType(.emailAddress) + .textInputAutocapitalization(.never) + .disableAutocorrection(true) + .focused($focusedField, equals: .email) + .submitLabel(.next) + } + .padding(.bottom, 8) + + VStack(alignment: .leading, spacing: 6) { + Text("Password") + .font(.appFootnote) + .foregroundColor(.appGrayText) + SecureField("", text: $password) + .textContentType(.password) + .textInputAutocapitalization(.never) + .disableAutocorrection(true) + .focused($focusedField, equals: .password) + .submitLabel(.done) + } + .padding(.bottom, 16) + + Button( + action: { + Task { + await viewModel.submitCredentials( + email: email, + password: password, + authenticator: authenticator + ) + } + }, + label: { Text("Submit") } + ) + .buttonStyle(SolidCapsuleButtonStyle(color: .appDeepBackground, width: 300)) + + if let loginError = viewModel.loginError { + LoginErrorMessageView(loginError: loginError) + } + + HStack { + Button( + action: { viewModel.emailAuthState = .signUp }, + label: { + Text("Don't have an account?") + .foregroundColor(.appGrayTextContrast) + .underline() + } + ) + .padding(.vertical) + Spacer() + } + } + .textFieldStyle(StandardTextFieldStyle()) + .onSubmit { + if focusedField == .email { + focusedField = .password + } else { + focusedField = nil + } + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + + Spacer() + } + } + .navigationTitle("Sign In") + } +} diff --git a/apple/OmnivoreKit/Sources/App/Views/Registration/EmailAuth/EmailSignupFormView.swift b/apple/OmnivoreKit/Sources/App/Views/Registration/EmailAuth/EmailSignupFormView.swift new file mode 100644 index 000000000..53215af01 --- /dev/null +++ b/apple/OmnivoreKit/Sources/App/Views/Registration/EmailAuth/EmailSignupFormView.swift @@ -0,0 +1,218 @@ +import Combine +import Models +import Services +import SwiftUI +import Utils +import Views + +extension EmailAuthViewModel { + func signUp( + email: String, + password: String, + username _: String, + fullName _: String, + authenticator: Authenticator + ) async { + do { + // TODO: add function to sign up + try await authenticator.submitEmailLogin(email: email, password: password) + } catch { + loginError = error as? LoginError + } + } + + func validateUsername(username: String, dataService: DataService) { + if let status = PotentialUsernameStatus.validationError(username: username.lowercased()) { + potentialUsernameStatus = status + return + } + + Task { + do { + try await dataService.validateUsernamePublisher(username: username) + potentialUsernameStatus = .available + } catch { + let usernameError = (error as? UsernameAvailabilityError) ?? .unknown + switch usernameError { + case .tooShort: + potentialUsernameStatus = .tooShort + case .tooLong: + potentialUsernameStatus = .tooLong + case .invalidPattern: + potentialUsernameStatus = .invalidPattern + case .nameUnavailable: + potentialUsernameStatus = .unavailable + case .internalServer, .unknown: + loginError = .unknown + case .network: + loginError = .network + } + } + } + } + + func configureUsernameValidation(dataService: DataService) { + $potentialUsername + .debounce(for: .seconds(0.5), scheduler: DispatchQueue.main) + .sink(receiveValue: { [weak self] username in + self?.validateUsername(username: username, dataService: dataService) + }) + .store(in: &subscriptions) + } +} + +struct EmailSignupFormView: View { + enum FocusedField { + case email, password, fullName, username + } + + @Environment(\.horizontalSizeClass) var horizontalSizeClass + @EnvironmentObject var authenticator: Authenticator + @EnvironmentObject var dataService: DataService + @ObservedObject var viewModel: EmailAuthViewModel + + @FocusState private var focusedField: FocusedField? + @State private var email = "" + @State private var password = "" + @State private var name = "" + @State private var username = "" + + var body: some View { + VStack(spacing: 0) { + VStack(spacing: 28) { + ScrollView(showsIndicators: false) { + if horizontalSizeClass == .regular { + Spacer(minLength: 150) + } + VStack { + // Email + VStack(alignment: .leading, spacing: 6) { + Text("Email") + .font(.appFootnote) + .foregroundColor(.appGrayText) + TextField("", text: $email) + .textContentType(.emailAddress) + .keyboardType(.emailAddress) + .textInputAutocapitalization(.never) + .disableAutocorrection(true) + .focused($focusedField, equals: .email) + .submitLabel(.next) + } + .padding(.bottom, 8) + + // Password + VStack(alignment: .leading, spacing: 6) { + Text("Password") + .font(.appFootnote) + .foregroundColor(.appGrayText) + SecureField("", text: $password) + .textContentType(.password) + .textInputAutocapitalization(.never) + .disableAutocorrection(true) + .focused($focusedField, equals: .password) + .submitLabel(.next) + } + .padding(.bottom, 8) + + // Full Name + VStack(alignment: .leading, spacing: 6) { + Text("Full Name") + .font(.appFootnote) + .foregroundColor(.appGrayText) + TextField("", text: $name) + .textContentType(.name) + .keyboardType(.alphabet) + .disableAutocorrection(true) + .focused($focusedField, equals: .fullName) + .submitLabel(.next) + } + .padding(.bottom, 8) + + // Username + VStack(alignment: .leading, spacing: 6) { + HStack { + VStack(alignment: .leading, spacing: 6) { + Text("Username") + .font(.appFootnote) + .foregroundColor(.appGrayText) + TextField("", text: $viewModel.potentialUsername) + .textInputAutocapitalization(.never) + .textContentType(.username) + .disableAutocorrection(true) + .keyboardType(.alphabet) + .focused($focusedField, equals: .username) + .submitLabel(.done) + } + + if viewModel.potentialUsernameStatus == .available { + Image(systemName: "checkmark.circle.fill") + .font(.appBody) + .foregroundColor(.green) + } + } + if let message = viewModel.potentialUsernameStatus.message { + Text(message) + .font(.appCaption) + .foregroundColor(.red) + } + } + .padding(.bottom, 16) + .animation(.default, value: 0.35) + + Button( + action: { + Task { + await viewModel.signUp( + email: email, + password: password, + username: username, + fullName: name, + authenticator: authenticator + ) + } + }, + label: { Text("Submit") } + ) + .buttonStyle(SolidCapsuleButtonStyle(color: .appDeepBackground, width: 300)) + + if let loginError = viewModel.loginError { + LoginErrorMessageView(loginError: loginError) + } + + HStack { + Button( + action: { viewModel.emailAuthState = .signIn }, + label: { + Text("Already have an account?") + .foregroundColor(.appGrayTextContrast) + .underline() + } + ) + .padding(.vertical) + Spacer() + } + } + .textFieldStyle(StandardTextFieldStyle()) + .onSubmit { + if focusedField == .email { + focusedField = .password + } else if focusedField == .password { + focusedField = .fullName + } else if focusedField == .fullName { + focusedField = .username + } else { + focusedField = nil + } + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + + Spacer() + } + } + .navigationTitle("Sign Up") + .task { + viewModel.configureUsernameValidation(dataService: dataService) + } + } +} diff --git a/apple/OmnivoreKit/Sources/App/Views/Registration/EmailAuthView.swift b/apple/OmnivoreKit/Sources/App/Views/Registration/EmailAuthView.swift deleted file mode 100644 index 9bbd0f154..000000000 --- a/apple/OmnivoreKit/Sources/App/Views/Registration/EmailAuthView.swift +++ /dev/null @@ -1,263 +0,0 @@ -import Models -import Services -import SwiftUI -import Utils -import Views - -enum EmailAuthState { - case signIn - case signUp - case loading -} - -@MainActor final class EmailAuthViewModel: ObservableObject { - @Published var loginError: LoginError? - @Published var emailAuthState = EmailAuthState.loading - - func loadAuthState() { - // check tokens here to determine pending/active/no user - emailAuthState = .signUp - } - - func submitCredentials( - email: String, - password: String, - authenticator: Authenticator - ) async { - do { - try await authenticator.submitEmailLogin(email: email, password: password) - } catch { - loginError = error as? LoginError - } - } -} - -struct EmailAuthView: View { - @Environment(\.presentationMode) private var presentationMode - @StateObject private var viewModel = EmailAuthViewModel() - - @ViewBuilder var primaryContent: some View { - switch viewModel.emailAuthState { - case .signUp: - EmailSignupFormView(viewModel: viewModel) - case .signIn: - EmailLoginFormView(viewModel: viewModel) - case .loading: - VStack { - Spacer() - ProgressView() - Spacer() - } - } - } - - var body: some View { - NavigationView { - ZStack { - Color.appBackground.edgesIgnoringSafeArea(.all) - primaryContent - .frame(maxWidth: 300) - .navigationBarTitleDisplayMode(.inline) - .toolbar { - ToolbarItem(placement: .barTrailing) { - Button( - action: { presentationMode.wrappedValue.dismiss() }, - label: { Image(systemName: "xmark").foregroundColor(.appGrayTextContrast) } - ) - } - } - } - } - .task { - viewModel.loadAuthState() - } - } -} - -struct EmailLoginFormView: View { - enum FocusedField { - case email, password - } - - @Environment(\.horizontalSizeClass) var horizontalSizeClass - @EnvironmentObject var authenticator: Authenticator - @ObservedObject var viewModel: EmailAuthViewModel - - @FocusState private var focusedField: FocusedField? - @State private var email = "" - @State private var password = "" - - var body: some View { - VStack(spacing: 0) { - VStack(spacing: 28) { - ScrollView(showsIndicators: false) { - if horizontalSizeClass == .regular { - Spacer(minLength: 150) - } - VStack { - VStack(alignment: .leading, spacing: 6) { - Text("Email") - .font(.appFootnote) - .foregroundColor(.appGrayText) - TextField("", text: $email) - .textContentType(.emailAddress) - .textInputAutocapitalization(.never) - .disableAutocorrection(true) - .focused($focusedField, equals: .email) - } - .padding(.bottom, 8) - - VStack(alignment: .leading, spacing: 6) { - Text("Password") - .font(.appFootnote) - .foregroundColor(.appGrayText) - SecureField("", text: $password) - .textContentType(.password) - .textInputAutocapitalization(.never) - .disableAutocorrection(true) - .focused($focusedField, equals: .password) - } - .padding(.bottom, 16) - - Button( - action: { - Task { - await viewModel.submitCredentials( - email: email, - password: password, - authenticator: authenticator - ) - } - }, - label: { Text("Submit") } - ) - .buttonStyle(SolidCapsuleButtonStyle(color: .appDeepBackground, width: 300)) - - if let loginError = viewModel.loginError { - LoginErrorMessageView(loginError: loginError) - } - - HStack { - Button( - action: { viewModel.emailAuthState = .signUp }, - label: { - Text("Don't have an account?") - .foregroundColor(.appGrayTextContrast) - .underline() - } - ) - .padding(.vertical) - Spacer() - } - } - .textFieldStyle(StandardTextFieldStyle()) - .onSubmit { - if focusedField == .email { - focusedField = .password - } else { - focusedField = nil - } - } - } - .frame(maxWidth: .infinity, maxHeight: .infinity) - - Spacer() - } - } - .navigationTitle("Sign In") - } -} - -struct EmailSignupFormView: View { - enum FocusedField { - case email, password, fullName, username - } - - @Environment(\.horizontalSizeClass) var horizontalSizeClass - @EnvironmentObject var authenticator: Authenticator - @ObservedObject var viewModel: EmailAuthViewModel - - @FocusState private var focusedField: FocusedField? - @State private var email = "" - @State private var password = "" - - var body: some View { - VStack(spacing: 0) { - VStack(spacing: 28) { - ScrollView(showsIndicators: false) { - if horizontalSizeClass == .regular { - Spacer(minLength: 150) - } - VStack { - VStack(alignment: .leading, spacing: 6) { - Text("Email") - .font(.appFootnote) - .foregroundColor(.appGrayText) - TextField("", text: $email) - .textContentType(.emailAddress) - .textInputAutocapitalization(.never) - .disableAutocorrection(true) - .focused($focusedField, equals: .email) - } - .padding(.bottom, 8) - - VStack(alignment: .leading, spacing: 6) { - Text("Password") - .font(.appFootnote) - .foregroundColor(.appGrayText) - SecureField("", text: $password) - .textContentType(.password) - .textInputAutocapitalization(.never) - .disableAutocorrection(true) - .focused($focusedField, equals: .password) - } - .padding(.bottom, 16) - - Button( - action: { - Task { - await viewModel.submitCredentials( - email: email, - password: password, - authenticator: authenticator - ) - } - }, - label: { Text("Submit") } - ) - .buttonStyle(SolidCapsuleButtonStyle(color: .appDeepBackground, width: 300)) - - if let loginError = viewModel.loginError { - LoginErrorMessageView(loginError: loginError) - } - - HStack { - Button( - action: { viewModel.emailAuthState = .signIn }, - label: { - Text("Already have an account?") - .foregroundColor(.appGrayTextContrast) - .underline() - } - ) - .padding(.vertical) - Spacer() - } - } - .textFieldStyle(StandardTextFieldStyle()) - .onSubmit { - if focusedField == .email { - focusedField = .password - } else { - focusedField = nil - } - } - } - .frame(maxWidth: .infinity, maxHeight: .infinity) - - Spacer() - } - } - .navigationTitle("Sign Up") - } -} From c01d9a70214b21edd8b965427e11cf1702aa32a2 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Fri, 29 Jul 2022 12:06:07 -0700 Subject: [PATCH 10/17] server returns a pendingEmailVerification boolean in auth payload --- .../Registration/EmailAuth/EmailAuthView.swift | 3 +++ .../EmailAuth/EmailLoginFormView.swift | 8 +++++++- .../Authentication/AccountCreator.swift | 17 ++++++++++++----- .../Services/Authentication/AuthModels.swift | 5 +++++ .../DataService/Networking/ServerResource.swift | 4 +--- .../VerifyAuthProviderToken.swift | 6 +++--- packages/api/src/routers/auth/mobile/sign_in.ts | 4 ++-- 7 files changed, 33 insertions(+), 14 deletions(-) diff --git a/apple/OmnivoreKit/Sources/App/Views/Registration/EmailAuth/EmailAuthView.swift b/apple/OmnivoreKit/Sources/App/Views/Registration/EmailAuth/EmailAuthView.swift index 18b69768d..df8f5f10e 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Registration/EmailAuth/EmailAuthView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Registration/EmailAuth/EmailAuthView.swift @@ -9,6 +9,7 @@ enum EmailAuthState { case signIn case signUp case loading + case pendingEmailVerification } @MainActor final class EmailAuthViewModel: ObservableObject { @@ -35,6 +36,8 @@ struct EmailAuthView: View { EmailSignupFormView(viewModel: viewModel) case .signIn: EmailLoginFormView(viewModel: viewModel) + case .pendingEmailVerification: + Text("Verify Your email") case .loading: VStack { Spacer() diff --git a/apple/OmnivoreKit/Sources/App/Views/Registration/EmailAuth/EmailLoginFormView.swift b/apple/OmnivoreKit/Sources/App/Views/Registration/EmailAuth/EmailLoginFormView.swift index 40756a017..3c8c0991d 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Registration/EmailAuth/EmailLoginFormView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Registration/EmailAuth/EmailLoginFormView.swift @@ -13,7 +13,13 @@ extension EmailAuthViewModel { do { try await authenticator.submitEmailLogin(email: email, password: password) } catch { - loginError = error as? LoginError + if let newLoginError = error as? LoginError { + if newLoginError == .pendingEmailVerification { + emailAuthState = .pendingEmailVerification + } else { + loginError = newLoginError + } + } } } } diff --git a/apple/OmnivoreKit/Sources/Services/Authentication/AccountCreator.swift b/apple/OmnivoreKit/Sources/Services/Authentication/AccountCreator.swift index e58b9b1e9..88f57583d 100644 --- a/apple/OmnivoreKit/Sources/Services/Authentication/AccountCreator.swift +++ b/apple/OmnivoreKit/Sources/Services/Authentication/AccountCreator.swift @@ -54,11 +54,18 @@ public extension Authenticator { ) async throws { do { let params = EmailSignInParams(email: email, password: password) - let authPayload = try await networker.submitEmailLogin(params: params) - try ValetKey.authCookieString.setValue(authPayload.commentedAuthCookieString) - try ValetKey.authToken.setValue(authPayload.authToken) - DispatchQueue.main.async { - self.isLoggedIn = true + let emailAuthPayload = try await networker.submitEmailLogin(params: params) + + if let authPayload = emailAuthPayload.authPayload { + try ValetKey.authCookieString.setValue(authPayload.commentedAuthCookieString) + try ValetKey.authToken.setValue(authPayload.authToken) + DispatchQueue.main.async { + self.isLoggedIn = true + } + } else if emailAuthPayload.errorCodes != nil { + throw ServerError.pendingEmailVerification + } else { + throw ServerError.unknown } } catch { let serverError = (error as? ServerError) ?? ServerError.unknown diff --git a/apple/OmnivoreKit/Sources/Services/Authentication/AuthModels.swift b/apple/OmnivoreKit/Sources/Services/Authentication/AuthModels.swift index 97e165415..ac4135f1f 100644 --- a/apple/OmnivoreKit/Sources/Services/Authentication/AuthModels.swift +++ b/apple/OmnivoreKit/Sources/Services/Authentication/AuthModels.swift @@ -33,6 +33,11 @@ struct AuthPayload: Decodable { let authToken: String } +struct EmailAuthPayload: Decodable { + let authPayload: AuthPayload? + let pendingEmailVerification: Bool? +} + struct CreateAccountParams: Encodable { let pendingUserToken: String let userProfile: UserProfile diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Networking/ServerResource.swift b/apple/OmnivoreKit/Sources/Services/DataService/Networking/ServerResource.swift index 331bac709..4bf7f11df 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Networking/ServerResource.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Networking/ServerResource.swift @@ -42,10 +42,9 @@ extension URLSession { do { let (data, response) = try await data(for: resource.urlRequest) let serverResponse = ServerResponse(data: data, response: response) + NetworkRequestLogger.log(request: resource.urlRequest, serverResponse: serverResponse) if let httpResponse = response as? HTTPURLResponse, 200 ..< 300 ~= httpResponse.statusCode { - NetworkRequestLogger.log(request: resource.urlRequest, serverResponse: serverResponse) - if let decodedValue = resource.decode(serverResponse) { return decodedValue } @@ -56,7 +55,6 @@ extension URLSession { } } catch { let serverResponse = ServerResponse(error: error) - NetworkRequestLogger.log(request: resource.urlRequest, serverResponse: serverResponse) throw ServerError(serverResponse: serverResponse) } } diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Networking/ServerResources/VerifyAuthProviderToken.swift b/apple/OmnivoreKit/Sources/Services/DataService/Networking/ServerResources/VerifyAuthProviderToken.swift index 3943d5807..382eb7b4a 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Networking/ServerResources/VerifyAuthProviderToken.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Networking/ServerResources/VerifyAuthProviderToken.swift @@ -37,7 +37,7 @@ extension Networker { } } - func submitEmailLogin(params: EmailSignInParams) async throws -> AuthPayload { + func submitEmailLogin(params: EmailSignInParams) async throws -> EmailAuthPayload { let encodedParams = (try? JSONEncoder().encode(params)) ?? Data() let urlRequest = URLRequest.create( @@ -46,9 +46,9 @@ extension Networker { requestMethod: .post(params: encodedParams) ) - let resource = ServerResource( + let resource = ServerResource( urlRequest: urlRequest, - decode: AuthPayload.decode + decode: EmailAuthPayload.decode ) do { diff --git a/packages/api/src/routers/auth/mobile/sign_in.ts b/packages/api/src/routers/auth/mobile/sign_in.ts index 8902ff482..699c38aa0 100644 --- a/packages/api/src/routers/auth/mobile/sign_in.ts +++ b/packages/api/src/routers/auth/mobile/sign_in.ts @@ -66,8 +66,8 @@ export async function createMobileEmailSignInResponse( name: user.name, }) return { - statusCode: 418, - json: { errorCodes: ['PENDING_VERIFICATION'] }, + statusCode: 200, + json: { pendingEmailVerification: true }, } } From 0178e4a1376e5ec076700b49829f25f72d6c4558 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Sat, 30 Jul 2022 09:10:41 -0700 Subject: [PATCH 11/17] add ios network code to submit email sign up request --- .../EmailAuth/EmailAuthView.swift | 8 +++++- .../EmailAuth/EmailSignupFormView.swift | 13 +++++++--- .../Authentication/AccountCreator.swift | 18 ++++++++++++- .../Services/Authentication/AuthModels.swift | 11 ++++++++ .../VerifyAuthProviderToken.swift | 25 +++++++++++++++++++ .../Sources/Services/Keychain/ValetKey.swift | 5 ++++ 6 files changed, 74 insertions(+), 6 deletions(-) diff --git a/apple/OmnivoreKit/Sources/App/Views/Registration/EmailAuth/EmailAuthView.swift b/apple/OmnivoreKit/Sources/App/Views/Registration/EmailAuth/EmailAuthView.swift index df8f5f10e..8d9d01068 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Registration/EmailAuth/EmailAuthView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Registration/EmailAuth/EmailAuthView.swift @@ -22,7 +22,13 @@ enum EmailAuthState { func loadAuthState() { // check tokens here to determine pending/active/no user - emailAuthState = .signIn + if PublicValet.hasPendingEmailVerificationToken { + // TODO: make network request to check status + // if it's now active then log in the user\ + emailAuthState = .pendingEmailVerification + } else { + emailAuthState = .signIn + } } } diff --git a/apple/OmnivoreKit/Sources/App/Views/Registration/EmailAuth/EmailSignupFormView.swift b/apple/OmnivoreKit/Sources/App/Views/Registration/EmailAuth/EmailSignupFormView.swift index 53215af01..ff62bd52d 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Registration/EmailAuth/EmailSignupFormView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Registration/EmailAuth/EmailSignupFormView.swift @@ -9,13 +9,18 @@ extension EmailAuthViewModel { func signUp( email: String, password: String, - username _: String, - fullName _: String, + username: String, + fullName: String, authenticator: Authenticator ) async { do { - // TODO: add function to sign up - try await authenticator.submitEmailLogin(email: email, password: password) + try await authenticator.submitUserSignUp( + email: email, + password: password, + username: username, + name: fullName + ) + emailAuthState = .pendingEmailVerification } catch { loginError = error as? LoginError } diff --git a/apple/OmnivoreKit/Sources/Services/Authentication/AccountCreator.swift b/apple/OmnivoreKit/Sources/Services/Authentication/AccountCreator.swift index 88f57583d..140ff5a4c 100644 --- a/apple/OmnivoreKit/Sources/Services/Authentication/AccountCreator.swift +++ b/apple/OmnivoreKit/Sources/Services/Authentication/AccountCreator.swift @@ -62,7 +62,7 @@ public extension Authenticator { DispatchQueue.main.async { self.isLoggedIn = true } - } else if emailAuthPayload.errorCodes != nil { + } else if emailAuthPayload.pendingEmailVerification == true { throw ServerError.pendingEmailVerification } else { throw ServerError.unknown @@ -72,4 +72,20 @@ public extension Authenticator { throw LoginError.make(serverError: serverError) } } + + func submitUserSignUp( + email: String, + password: String, + username: String, + name: String + ) async throws { + do { + let params = EmailSignUpParams(email: email, password: password, username: username, name: name) + let authPayload = try await networker.submitEmailSignUp(params: params) + try ValetKey.authTokenWithPendingEmail.setValue(authPayload.pendingEmailVerificationToken) + } catch { + let serverError = (error as? ServerError) ?? ServerError.unknown + throw LoginError.make(serverError: serverError) + } + } } diff --git a/apple/OmnivoreKit/Sources/Services/Authentication/AuthModels.swift b/apple/OmnivoreKit/Sources/Services/Authentication/AuthModels.swift index ac4135f1f..e20acb7f1 100644 --- a/apple/OmnivoreKit/Sources/Services/Authentication/AuthModels.swift +++ b/apple/OmnivoreKit/Sources/Services/Authentication/AuthModels.swift @@ -23,6 +23,13 @@ struct EmailSignInParams: Encodable { let password: String } +struct EmailSignUpParams: Encodable { + let email: String + let password: String + let username: String + let name: String +} + enum AuthProvider: String, Encodable { case apple = "APPLE" case google = "GOOGLE" @@ -48,6 +55,10 @@ struct PendingUserAuthPayload: Decodable { let pendingUserProfile: UserProfile } +struct PendingEmailVerificationAuthPayload: Decodable { + let pendingEmailVerificationToken: String +} + extension AuthPayload { var commentedAuthCookieString: String { authCookieString.replacingOccurrences(of: "HttpOnly", with: "comment=ios-webview-cookie; HttpOnly") diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Networking/ServerResources/VerifyAuthProviderToken.swift b/apple/OmnivoreKit/Sources/Services/DataService/Networking/ServerResources/VerifyAuthProviderToken.swift index 382eb7b4a..c469dd955 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Networking/ServerResources/VerifyAuthProviderToken.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Networking/ServerResources/VerifyAuthProviderToken.swift @@ -61,4 +61,29 @@ extension Networker { } } } + + func submitEmailSignUp(params: EmailSignUpParams) async throws -> PendingEmailVerificationAuthPayload { + let encodedParams = (try? JSONEncoder().encode(params)) ?? Data() + + let urlRequest = URLRequest.create( + baseURL: appEnvironment.serverBaseURL, + urlPath: "/api/mobile-auth/email-sign-up", + requestMethod: .post(params: encodedParams) + ) + + let resource = ServerResource( + urlRequest: urlRequest, + decode: PendingEmailVerificationAuthPayload.decode + ) + + do { + return try await urlSession.performRequest(resource: resource) + } catch { + if let error = error as? ServerError { + throw LoginError.make(serverError: error) + } else { + throw LoginError.unknown + } + } + } } diff --git a/apple/OmnivoreKit/Sources/Services/Keychain/ValetKey.swift b/apple/OmnivoreKit/Sources/Services/Keychain/ValetKey.swift index e4daea999..752279604 100644 --- a/apple/OmnivoreKit/Sources/Services/Keychain/ValetKey.swift +++ b/apple/OmnivoreKit/Sources/Services/Keychain/ValetKey.swift @@ -10,10 +10,15 @@ public enum PublicValet { public static var authToken: String? { ValetKey.authToken.value() } + + public static var hasPendingEmailVerificationToken: Bool { + ValetKey.authTokenWithPendingEmail.exists + } } enum ValetKey: String { case authToken = "app.omnivore.valet.auth-token" + case authTokenWithPendingEmail = "app.omnivore.valet.auth-token-with-pending-email" case authCookieString = "app.omnivore.valet.auth-cookie-raw-string" case appEnvironmentString = "app.omnivore.valet.app-environment" } From c076dd0f721f6f925fa2fc71361ce147c70ed06d Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Mon, 1 Aug 2022 10:42:39 -0700 Subject: [PATCH 12/17] add mobile email sign up endpoint --- .../routers/auth/mobile/mobile_auth_router.ts | 16 ++++++++- .../api/src/routers/auth/mobile/sign_up.ts | 34 +++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/packages/api/src/routers/auth/mobile/mobile_auth_router.ts b/packages/api/src/routers/auth/mobile/mobile_auth_router.ts index eb85649dd..138aae9c8 100644 --- a/packages/api/src/routers/auth/mobile/mobile_auth_router.ts +++ b/packages/api/src/routers/auth/mobile/mobile_auth_router.ts @@ -6,7 +6,10 @@ import { createMobileSignInResponse, createMobileEmailSignInResponse, } from './sign_in' -import { createMobileSignUpResponse } from './sign_up' +import { + createMobileSignUpResponse, + createMobileEmailSignUpResponse, +} from './sign_up' import { createMobileAccountCreationResponse } from './account_creation' export function mobileAuthRouter() { @@ -24,6 +27,17 @@ export function mobileAuthRouter() { res.status(payload.statusCode).json(payload.json) }) + router.post('/email-sign-up', async (req, res) => { + const { email, password, username, name } = req.body + const payload = await createMobileEmailSignUpResponse( + email, + password, + username, + name + ) + res.status(payload.statusCode).json(payload.json) + }) + router.post('/sign-up', async (req, res) => { const { token, provider, name } = req.body const payload = await createMobileSignUpResponse(token, provider, name) diff --git a/packages/api/src/routers/auth/mobile/sign_up.ts b/packages/api/src/routers/auth/mobile/sign_up.ts index 6cda2bf3f..ef36f481c 100644 --- a/packages/api/src/routers/auth/mobile/sign_up.ts +++ b/packages/api/src/routers/auth/mobile/sign_up.ts @@ -9,6 +9,8 @@ import { } from '../auth_types' import { createPendingUserToken, suggestedUsername } from '../jwt_helpers' import UserModel from '../../../datalayer/user' +import { hashPassword } from '../../../utils/auth' +import { createUser } from '../../../services/create_user' export async function createMobileSignUpResponse( token?: string, @@ -41,6 +43,38 @@ export async function createMobileSignUpResponse( } } +export async function createMobileEmailSignUpResponse( + email?: string, + password?: string, + username?: string, + name?: string +): Promise { + try { + if (!email || !password || !username || !name) { + throw new Error('Missing username, password, name, or username') + } + + const hashedPassword = await hashPassword(password) + + await createUser({ + email, + provider: 'EMAIL', + sourceUserId: email, + name, + username: username.toLowerCase(), + password: hashedPassword, + pendingConfirmation: true, + }) + + return { + statusCode: 200, + json: {}, + } + } catch (e) { + return signUpFailedPayload + } +} + const signUpFailedPayload = { statusCode: 403, json: { errorCodes: ['AUTH_FAILED'] }, From 66eb9bf7ee2b43fdf5be773de6e723c7cc62d127 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Mon, 1 Aug 2022 22:18:37 -0700 Subject: [PATCH 13/17] handle auth payload from email sign in correctly --- .../Registration/EmailAuth/EmailAuthView.swift | 16 +--------------- .../EmailAuth/EmailSignupFormView.swift | 4 +--- .../Services/Authentication/AccountCreator.swift | 6 +++--- .../Services/Authentication/AuthModels.swift | 3 ++- .../VerifyAuthProviderToken.swift | 8 ++++---- .../Sources/Services/Keychain/ValetKey.swift | 5 ----- 6 files changed, 11 insertions(+), 31 deletions(-) diff --git a/apple/OmnivoreKit/Sources/App/Views/Registration/EmailAuth/EmailAuthView.swift b/apple/OmnivoreKit/Sources/App/Views/Registration/EmailAuth/EmailAuthView.swift index 8d9d01068..bf770c5af 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Registration/EmailAuth/EmailAuthView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Registration/EmailAuth/EmailAuthView.swift @@ -14,22 +14,11 @@ enum EmailAuthState { @MainActor final class EmailAuthViewModel: ObservableObject { @Published var loginError: LoginError? - @Published var emailAuthState = EmailAuthState.loading + @Published var emailAuthState = EmailAuthState.signIn @Published var potentialUsernameStatus = PotentialUsernameStatus.noUsername @Published var potentialUsername = "" var subscriptions = Set() - - func loadAuthState() { - // check tokens here to determine pending/active/no user - if PublicValet.hasPendingEmailVerificationToken { - // TODO: make network request to check status - // if it's now active then log in the user\ - emailAuthState = .pendingEmailVerification - } else { - emailAuthState = .signIn - } - } } struct EmailAuthView: View { @@ -70,8 +59,5 @@ struct EmailAuthView: View { } } } - .task { - viewModel.loadAuthState() - } } } diff --git a/apple/OmnivoreKit/Sources/App/Views/Registration/EmailAuth/EmailSignupFormView.swift b/apple/OmnivoreKit/Sources/App/Views/Registration/EmailAuth/EmailSignupFormView.swift index ff62bd52d..c3c66706d 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Registration/EmailAuth/EmailSignupFormView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Registration/EmailAuth/EmailSignupFormView.swift @@ -9,7 +9,6 @@ extension EmailAuthViewModel { func signUp( email: String, password: String, - username: String, fullName: String, authenticator: Authenticator ) async { @@ -17,7 +16,7 @@ extension EmailAuthViewModel { try await authenticator.submitUserSignUp( email: email, password: password, - username: username, + username: potentialUsername, name: fullName ) emailAuthState = .pendingEmailVerification @@ -170,7 +169,6 @@ struct EmailSignupFormView: View { await viewModel.signUp( email: email, password: password, - username: username, fullName: name, authenticator: authenticator ) diff --git a/apple/OmnivoreKit/Sources/Services/Authentication/AccountCreator.swift b/apple/OmnivoreKit/Sources/Services/Authentication/AccountCreator.swift index 140ff5a4c..73e31dbb7 100644 --- a/apple/OmnivoreKit/Sources/Services/Authentication/AccountCreator.swift +++ b/apple/OmnivoreKit/Sources/Services/Authentication/AccountCreator.swift @@ -56,7 +56,8 @@ public extension Authenticator { let params = EmailSignInParams(email: email, password: password) let emailAuthPayload = try await networker.submitEmailLogin(params: params) - if let authPayload = emailAuthPayload.authPayload { + if let authCookieString = emailAuthPayload.authCookieString, let authToken = emailAuthPayload.authToken { + let authPayload = AuthPayload(authCookieString: authCookieString, authToken: authToken) try ValetKey.authCookieString.setValue(authPayload.commentedAuthCookieString) try ValetKey.authToken.setValue(authPayload.authToken) DispatchQueue.main.async { @@ -81,8 +82,7 @@ public extension Authenticator { ) async throws { do { let params = EmailSignUpParams(email: email, password: password, username: username, name: name) - let authPayload = try await networker.submitEmailSignUp(params: params) - try ValetKey.authTokenWithPendingEmail.setValue(authPayload.pendingEmailVerificationToken) + try await networker.submitEmailSignUp(params: params) } catch { let serverError = (error as? ServerError) ?? ServerError.unknown throw LoginError.make(serverError: serverError) diff --git a/apple/OmnivoreKit/Sources/Services/Authentication/AuthModels.swift b/apple/OmnivoreKit/Sources/Services/Authentication/AuthModels.swift index e20acb7f1..454e06b1e 100644 --- a/apple/OmnivoreKit/Sources/Services/Authentication/AuthModels.swift +++ b/apple/OmnivoreKit/Sources/Services/Authentication/AuthModels.swift @@ -41,7 +41,8 @@ struct AuthPayload: Decodable { } struct EmailAuthPayload: Decodable { - let authPayload: AuthPayload? + let authCookieString: String? + let authToken: String? let pendingEmailVerification: Bool? } diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Networking/ServerResources/VerifyAuthProviderToken.swift b/apple/OmnivoreKit/Sources/Services/DataService/Networking/ServerResources/VerifyAuthProviderToken.swift index c469dd955..0a2e4e4d7 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Networking/ServerResources/VerifyAuthProviderToken.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Networking/ServerResources/VerifyAuthProviderToken.swift @@ -62,7 +62,7 @@ extension Networker { } } - func submitEmailSignUp(params: EmailSignUpParams) async throws -> PendingEmailVerificationAuthPayload { + func submitEmailSignUp(params: EmailSignUpParams) async throws { let encodedParams = (try? JSONEncoder().encode(params)) ?? Data() let urlRequest = URLRequest.create( @@ -71,13 +71,13 @@ extension Networker { requestMethod: .post(params: encodedParams) ) - let resource = ServerResource( + let resource = ServerResource( urlRequest: urlRequest, - decode: PendingEmailVerificationAuthPayload.decode + decode: EmptyResponse.decode ) do { - return try await urlSession.performRequest(resource: resource) + _ = try await urlSession.performRequest(resource: resource) } catch { if let error = error as? ServerError { throw LoginError.make(serverError: error) diff --git a/apple/OmnivoreKit/Sources/Services/Keychain/ValetKey.swift b/apple/OmnivoreKit/Sources/Services/Keychain/ValetKey.swift index 752279604..e4daea999 100644 --- a/apple/OmnivoreKit/Sources/Services/Keychain/ValetKey.swift +++ b/apple/OmnivoreKit/Sources/Services/Keychain/ValetKey.swift @@ -10,15 +10,10 @@ public enum PublicValet { public static var authToken: String? { ValetKey.authToken.value() } - - public static var hasPendingEmailVerificationToken: Bool { - ValetKey.authTokenWithPendingEmail.exists - } } enum ValetKey: String { case authToken = "app.omnivore.valet.auth-token" - case authTokenWithPendingEmail = "app.omnivore.valet.auth-token-with-pending-email" case authCookieString = "app.omnivore.valet.auth-cookie-raw-string" case appEnvironmentString = "app.omnivore.valet.app-environment" } From 44a3ef7c24710ddc5ad06ad959bfdf445aa5c4a6 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Tue, 2 Aug 2022 07:09:23 -0700 Subject: [PATCH 14/17] add a EmailPendingVerificationView view --- .../EmailAuth/EmailAuthView.swift | 6 +- .../EmailAuth/EmailLoginFormView.swift | 89 ++++++++++++++++--- .../EmailAuth/EmailSignupFormView.swift | 2 +- 3 files changed, 81 insertions(+), 16 deletions(-) diff --git a/apple/OmnivoreKit/Sources/App/Views/Registration/EmailAuth/EmailAuthView.swift b/apple/OmnivoreKit/Sources/App/Views/Registration/EmailAuth/EmailAuthView.swift index bf770c5af..0a5905809 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Registration/EmailAuth/EmailAuthView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Registration/EmailAuth/EmailAuthView.swift @@ -9,7 +9,7 @@ enum EmailAuthState { case signIn case signUp case loading - case pendingEmailVerification + case pendingEmailVerification(email: String, password: String) } @MainActor final class EmailAuthViewModel: ObservableObject { @@ -31,8 +31,8 @@ struct EmailAuthView: View { EmailSignupFormView(viewModel: viewModel) case .signIn: EmailLoginFormView(viewModel: viewModel) - case .pendingEmailVerification: - Text("Verify Your email") + case let .pendingEmailVerification(email, password): + EmailPendingVerificationView(email: email, password: password, viewModel: viewModel) case .loading: VStack { Spacer() diff --git a/apple/OmnivoreKit/Sources/App/Views/Registration/EmailAuth/EmailLoginFormView.swift b/apple/OmnivoreKit/Sources/App/Views/Registration/EmailAuth/EmailLoginFormView.swift index 3c8c0991d..0b12960c1 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Registration/EmailAuth/EmailLoginFormView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Registration/EmailAuth/EmailLoginFormView.swift @@ -15,7 +15,7 @@ extension EmailAuthViewModel { } catch { if let newLoginError = error as? LoginError { if newLoginError == .pendingEmailVerification { - emailAuthState = .pendingEmailVerification + emailAuthState = .pendingEmailVerification(email: email, password: password) } else { loginError = newLoginError } @@ -30,6 +30,7 @@ struct EmailLoginFormView: View { } @Environment(\.horizontalSizeClass) var horizontalSizeClass + @Environment(\.openURL) var openURL @EnvironmentObject var authenticator: Authenticator @ObservedObject var viewModel: EmailAuthViewModel @@ -90,17 +91,34 @@ struct EmailLoginFormView: View { LoginErrorMessageView(loginError: loginError) } - HStack { - Button( - action: { viewModel.emailAuthState = .signUp }, - label: { - Text("Don't have an account?") - .foregroundColor(.appGrayTextContrast) - .underline() - } - ) - .padding(.vertical) - Spacer() + VStack(spacing: 0) { + HStack { + Button( + action: { viewModel.emailAuthState = .signUp }, + label: { + Text("Don't have an account?") + .foregroundColor(.appGrayTextContrast) + .underline() + } + ) + .padding(.vertical, 8) + Spacer() + } + + HStack { + Button( + action: { + openURL(URL(string: "https://omnivore.app/auth/forgot-password")!) + }, + label: { + Text("Forgot your password?") + .foregroundColor(.appGrayTextContrast) + .underline() + } + ) + .padding(.vertical, 8) + Spacer() + } } } .textFieldStyle(StandardTextFieldStyle()) @@ -120,3 +138,50 @@ struct EmailLoginFormView: View { .navigationTitle("Sign In") } } + +struct EmailPendingVerificationView: View { + let email: String + let password: String + + @ObservedObject var viewModel: EmailAuthViewModel + @EnvironmentObject var authenticator: Authenticator + + var verificationMessage: String { + "We've sent a verification email to \(email). Please verify your email and then tap the button below." + } + + var body: some View { + VStack(alignment: .leading, spacing: 8) { + Text(verificationMessage) + .font(.appBody) + + Button( + action: { + Task { + await viewModel.submitCredentials( + email: email, + password: password, + authenticator: authenticator + ) + } + }, + label: { Text("Check Status") } + ) + .buttonStyle(SolidCapsuleButtonStyle(color: .appDeepBackground, width: 300)) + + HStack { + Button( + action: { viewModel.emailAuthState = .signUp }, + label: { + Text("Use a different email?") + .foregroundColor(.appGrayTextContrast) + .underline() + } + ) + .padding(.vertical) + Spacer() + } + } + .navigationTitle("Verify Email") + } +} diff --git a/apple/OmnivoreKit/Sources/App/Views/Registration/EmailAuth/EmailSignupFormView.swift b/apple/OmnivoreKit/Sources/App/Views/Registration/EmailAuth/EmailSignupFormView.swift index c3c66706d..fcedb925e 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Registration/EmailAuth/EmailSignupFormView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Registration/EmailAuth/EmailSignupFormView.swift @@ -19,7 +19,7 @@ extension EmailAuthViewModel { username: potentialUsername, name: fullName ) - emailAuthState = .pendingEmailVerification + emailAuthState = .pendingEmailVerification(email: email, password: password) } catch { loginError = error as? LoginError } From f08029643bc0d6491522f81d4fb9b1510ea91697 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Tue, 2 Aug 2022 09:34:37 -0700 Subject: [PATCH 15/17] use newPassword for text field type on sign up form (iOS) --- .../App/Views/Registration/EmailAuth/EmailSignupFormView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apple/OmnivoreKit/Sources/App/Views/Registration/EmailAuth/EmailSignupFormView.swift b/apple/OmnivoreKit/Sources/App/Views/Registration/EmailAuth/EmailSignupFormView.swift index fcedb925e..73e481a44 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Registration/EmailAuth/EmailSignupFormView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Registration/EmailAuth/EmailSignupFormView.swift @@ -110,7 +110,7 @@ struct EmailSignupFormView: View { .font(.appFootnote) .foregroundColor(.appGrayText) SecureField("", text: $password) - .textContentType(.password) + .textContentType(.newPassword) .textInputAutocapitalization(.never) .disableAutocorrection(true) .focused($focusedField, equals: .password) From 38fcbeb9a161f3c6ade19ab0a167ee3e2325218e Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Tue, 2 Aug 2022 15:24:52 -0700 Subject: [PATCH 16/17] hide nav title if text field is focused on email auth view --- .../App/Views/Registration/EmailAuth/EmailLoginFormView.swift | 2 +- .../App/Views/Registration/EmailAuth/EmailSignupFormView.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apple/OmnivoreKit/Sources/App/Views/Registration/EmailAuth/EmailLoginFormView.swift b/apple/OmnivoreKit/Sources/App/Views/Registration/EmailAuth/EmailLoginFormView.swift index 0b12960c1..eceda710a 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Registration/EmailAuth/EmailLoginFormView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Registration/EmailAuth/EmailLoginFormView.swift @@ -135,7 +135,7 @@ struct EmailLoginFormView: View { Spacer() } } - .navigationTitle("Sign In") + .navigationTitle(focusedField == nil ? "Sign In" : "") } } diff --git a/apple/OmnivoreKit/Sources/App/Views/Registration/EmailAuth/EmailSignupFormView.swift b/apple/OmnivoreKit/Sources/App/Views/Registration/EmailAuth/EmailSignupFormView.swift index 73e481a44..abd5786dc 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Registration/EmailAuth/EmailSignupFormView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Registration/EmailAuth/EmailSignupFormView.swift @@ -213,7 +213,7 @@ struct EmailSignupFormView: View { Spacer() } } - .navigationTitle("Sign Up") + .navigationTitle(focusedField == nil ? "Sign Up" : "") .task { viewModel.configureUsernameValidation(dataService: dataService) } From a0c7d4e3b5644a7faa3935d331641c09c703a652 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Tue, 2 Aug 2022 19:43:47 -0700 Subject: [PATCH 17/17] call focused field as first modifier --- .../Registration/EmailAuth/EmailSignupFormView.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apple/OmnivoreKit/Sources/App/Views/Registration/EmailAuth/EmailSignupFormView.swift b/apple/OmnivoreKit/Sources/App/Views/Registration/EmailAuth/EmailSignupFormView.swift index abd5786dc..4c25b8305 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Registration/EmailAuth/EmailSignupFormView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Registration/EmailAuth/EmailSignupFormView.swift @@ -95,11 +95,11 @@ struct EmailSignupFormView: View { .font(.appFootnote) .foregroundColor(.appGrayText) TextField("", text: $email) + .focused($focusedField, equals: .email) .textContentType(.emailAddress) .keyboardType(.emailAddress) .textInputAutocapitalization(.never) .disableAutocorrection(true) - .focused($focusedField, equals: .email) .submitLabel(.next) } .padding(.bottom, 8) @@ -110,10 +110,10 @@ struct EmailSignupFormView: View { .font(.appFootnote) .foregroundColor(.appGrayText) SecureField("", text: $password) + .focused($focusedField, equals: .password) .textContentType(.newPassword) .textInputAutocapitalization(.never) .disableAutocorrection(true) - .focused($focusedField, equals: .password) .submitLabel(.next) } .padding(.bottom, 8) @@ -124,10 +124,10 @@ struct EmailSignupFormView: View { .font(.appFootnote) .foregroundColor(.appGrayText) TextField("", text: $name) + .focused($focusedField, equals: .fullName) .textContentType(.name) .keyboardType(.alphabet) .disableAutocorrection(true) - .focused($focusedField, equals: .fullName) .submitLabel(.next) } .padding(.bottom, 8) @@ -140,11 +140,11 @@ struct EmailSignupFormView: View { .font(.appFootnote) .foregroundColor(.appGrayText) TextField("", text: $viewModel.potentialUsername) + .focused($focusedField, equals: .username) .textInputAutocapitalization(.never) .textContentType(.username) .disableAutocorrection(true) .keyboardType(.alphabet) - .focused($focusedField, equals: .username) .submitLabel(.done) }