mirror of
https://github.com/omnivore-app/omnivore.git
synced 2026-03-11 08:54:26 +00:00
Merge pull request #1024 from omnivore-app/feature/email-login-ios
Email Login (iOS)
This commit is contained in:
commit
5da9939267
21 changed files with 792 additions and 67 deletions
|
|
@ -8,6 +8,16 @@
|
|||
<array>
|
||||
<string>Default</string>
|
||||
</array>
|
||||
<key>com.apple.developer.associated-domains</key>
|
||||
<array>
|
||||
<string>applinks:omnivore.app</string>
|
||||
<string>applinks:dev.omnivore.app</string>
|
||||
<string>applinks:demo.omnivore.app</string>
|
||||
<string>webcredentials:omnivore.app</string>
|
||||
<string>webcredentials:dev.omnivore.app</string>
|
||||
</array>
|
||||
<key>com.apple.developer.authentication-services.autofill-credential-provider</key>
|
||||
<true/>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.app.omnivoreapp</string>
|
||||
|
|
@ -16,11 +26,5 @@
|
|||
<array>
|
||||
<string>$(AppIdentifierPrefix)app.omnivore.shared</string>
|
||||
</array>
|
||||
<key>com.apple.developer.associated-domains</key>
|
||||
<array>
|
||||
<string>applinks:omnivore.app</string>
|
||||
<string>applinks:dev.omnivore.app</string>
|
||||
<string>applinks:demo.omnivore.app</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
|
|||
|
|
@ -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 = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
0411792B26A22860004AE24F /* MacUnitTests.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = MacUnitTests.plist; sourceTree = "<group>"; };
|
||||
|
|
@ -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 = "<group>";
|
||||
|
|
@ -1229,7 +1233,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 +1243,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 +1265,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 +1275,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 +1347,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 +1358,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 +1381,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 +1392,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 +1447,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 +1479,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 +1518,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 +1544,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 +1557,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 +1583,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 +1596,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 +1683,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 +1738,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 +1767,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 = "";
|
||||
|
|
|
|||
|
|
@ -224,7 +224,7 @@ struct CreateProfileView: View {
|
|||
}
|
||||
}
|
||||
|
||||
private extension PotentialUsernameStatus {
|
||||
extension PotentialUsernameStatus {
|
||||
var message: String? {
|
||||
switch self {
|
||||
case .tooShort:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,63 @@
|
|||
import Combine
|
||||
import Models
|
||||
import Services
|
||||
import SwiftUI
|
||||
import Utils
|
||||
import Views
|
||||
|
||||
enum EmailAuthState {
|
||||
case signIn
|
||||
case signUp
|
||||
case loading
|
||||
case pendingEmailVerification(email: String, password: String)
|
||||
}
|
||||
|
||||
@MainActor final class EmailAuthViewModel: ObservableObject {
|
||||
@Published var loginError: LoginError?
|
||||
@Published var emailAuthState = EmailAuthState.signIn
|
||||
@Published var potentialUsernameStatus = PotentialUsernameStatus.noUsername
|
||||
@Published var potentialUsername = ""
|
||||
|
||||
var subscriptions = Set<AnyCancellable>()
|
||||
}
|
||||
|
||||
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 let .pendingEmailVerification(email, password):
|
||||
EmailPendingVerificationView(email: email, password: password, 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) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,187 @@
|
|||
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 {
|
||||
if let newLoginError = error as? LoginError {
|
||||
if newLoginError == .pendingEmailVerification {
|
||||
emailAuthState = .pendingEmailVerification(email: email, password: password)
|
||||
} else {
|
||||
loginError = newLoginError
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct EmailLoginFormView: View {
|
||||
enum FocusedField {
|
||||
case email, password
|
||||
}
|
||||
|
||||
@Environment(\.horizontalSizeClass) var horizontalSizeClass
|
||||
@Environment(\.openURL) var openURL
|
||||
@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)
|
||||
}
|
||||
|
||||
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())
|
||||
.onSubmit {
|
||||
if focusedField == .email {
|
||||
focusedField = .password
|
||||
} else {
|
||||
focusedField = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
.navigationTitle(focusedField == nil ? "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")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,221 @@
|
|||
import Combine
|
||||
import Models
|
||||
import Services
|
||||
import SwiftUI
|
||||
import Utils
|
||||
import Views
|
||||
|
||||
extension EmailAuthViewModel {
|
||||
func signUp(
|
||||
email: String,
|
||||
password: String,
|
||||
fullName: String,
|
||||
authenticator: Authenticator
|
||||
) async {
|
||||
do {
|
||||
try await authenticator.submitUserSignUp(
|
||||
email: email,
|
||||
password: password,
|
||||
username: potentialUsername,
|
||||
name: fullName
|
||||
)
|
||||
emailAuthState = .pendingEmailVerification(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)
|
||||
.focused($focusedField, equals: .email)
|
||||
.textContentType(.emailAddress)
|
||||
.keyboardType(.emailAddress)
|
||||
.textInputAutocapitalization(.never)
|
||||
.disableAutocorrection(true)
|
||||
.submitLabel(.next)
|
||||
}
|
||||
.padding(.bottom, 8)
|
||||
|
||||
// Password
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Text("Password")
|
||||
.font(.appFootnote)
|
||||
.foregroundColor(.appGrayText)
|
||||
SecureField("", text: $password)
|
||||
.focused($focusedField, equals: .password)
|
||||
.textContentType(.newPassword)
|
||||
.textInputAutocapitalization(.never)
|
||||
.disableAutocorrection(true)
|
||||
.submitLabel(.next)
|
||||
}
|
||||
.padding(.bottom, 8)
|
||||
|
||||
// Full Name
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Text("Full Name")
|
||||
.font(.appFootnote)
|
||||
.foregroundColor(.appGrayText)
|
||||
TextField("", text: $name)
|
||||
.focused($focusedField, equals: .fullName)
|
||||
.textContentType(.name)
|
||||
.keyboardType(.alphabet)
|
||||
.disableAutocorrection(true)
|
||||
.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)
|
||||
.focused($focusedField, equals: .username)
|
||||
.textInputAutocapitalization(.never)
|
||||
.textContentType(.username)
|
||||
.disableAutocorrection(true)
|
||||
.keyboardType(.alphabet)
|
||||
.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,
|
||||
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(focusedField == nil ? "Sign Up" : "")
|
||||
.task {
|
||||
viewModel.configureUsernameValidation(dataService: dataService)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
EmailAuthView()
|
||||
}
|
||||
.sheet(isPresented: $showDebugModal) {
|
||||
DebugMenuView(selectedEnvironment: $selectedEnvironment)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")!
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,4 +4,5 @@ public enum LoginError: Error {
|
|||
case unauthorized
|
||||
case network
|
||||
case unknown
|
||||
case pendingEmailVerification
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,3 +45,47 @@ extension Authenticator {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// email auth
|
||||
public extension Authenticator {
|
||||
func submitEmailLogin(
|
||||
email: String,
|
||||
password: String
|
||||
) async throws {
|
||||
do {
|
||||
let params = EmailSignInParams(email: email, password: password)
|
||||
let emailAuthPayload = try await networker.submitEmailLogin(params: params)
|
||||
|
||||
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 {
|
||||
self.isLoggedIn = true
|
||||
}
|
||||
} else if emailAuthPayload.pendingEmailVerification == true {
|
||||
throw ServerError.pendingEmailVerification
|
||||
} else {
|
||||
throw ServerError.unknown
|
||||
}
|
||||
} catch {
|
||||
let serverError = (error as? ServerError) ?? ServerError.unknown
|
||||
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)
|
||||
try await networker.submitEmailSignUp(params: params)
|
||||
} catch {
|
||||
let serverError = (error as? ServerError) ?? ServerError.unknown
|
||||
throw LoginError.make(serverError: serverError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,18 @@ struct SignInParams: Encodable {
|
|||
let provider: AuthProvider
|
||||
}
|
||||
|
||||
struct EmailSignInParams: Encodable {
|
||||
let email: String
|
||||
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"
|
||||
|
|
@ -28,6 +40,12 @@ struct AuthPayload: Decodable {
|
|||
let authToken: String
|
||||
}
|
||||
|
||||
struct EmailAuthPayload: Decodable {
|
||||
let authCookieString: String?
|
||||
let authToken: String?
|
||||
let pendingEmailVerification: Bool?
|
||||
}
|
||||
|
||||
struct CreateAccountParams: Encodable {
|
||||
let pendingUserToken: String
|
||||
let userProfile: UserProfile
|
||||
|
|
@ -38,6 +56,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")
|
||||
|
|
@ -53,6 +75,8 @@ extension LoginError {
|
|||
return .unauthorized
|
||||
case .unknown:
|
||||
return .unknown
|
||||
case .pendingEmailVerification:
|
||||
return .pendingEmailVerification
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,4 +36,54 @@ extension Networker {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func submitEmailLogin(params: EmailSignInParams) async throws -> EmailAuthPayload {
|
||||
let encodedParams = (try? JSONEncoder().encode(params)) ?? Data()
|
||||
|
||||
let urlRequest = URLRequest.create(
|
||||
baseURL: appEnvironment.serverBaseURL,
|
||||
urlPath: "/api/mobile-auth/email-sign-in",
|
||||
requestMethod: .post(params: encodedParams)
|
||||
)
|
||||
|
||||
let resource = ServerResource<EmailAuthPayload>(
|
||||
urlRequest: urlRequest,
|
||||
decode: EmailAuthPayload.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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func submitEmailSignUp(params: EmailSignUpParams) async throws {
|
||||
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<EmptyResponse>(
|
||||
urlRequest: urlRequest,
|
||||
decode: EmptyResponse.decode
|
||||
)
|
||||
|
||||
do {
|
||||
_ = try await urlSession.performRequest(resource: resource)
|
||||
} catch {
|
||||
if let error = error as? ServerError {
|
||||
throw LoginError.make(serverError: error)
|
||||
} else {
|
||||
throw LoginError.unknown
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,8 +2,14 @@
|
|||
/* 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 { createMobileSignUpResponse } from './sign_up'
|
||||
import {
|
||||
createMobileSignInResponse,
|
||||
createMobileEmailSignInResponse,
|
||||
} from './sign_in'
|
||||
import {
|
||||
createMobileSignUpResponse,
|
||||
createMobileEmailSignUpResponse,
|
||||
} from './sign_up'
|
||||
import { createMobileAccountCreationResponse } from './account_creation'
|
||||
|
||||
export function mobileAuthRouter() {
|
||||
|
|
@ -15,6 +21,23 @@ 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('/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)
|
||||
|
|
|
|||
|
|
@ -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<JsonResponsePayload> {
|
||||
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: 200,
|
||||
json: { pendingEmailVerification: true },
|
||||
}
|
||||
}
|
||||
|
||||
const mobileAuthPayload = await createMobileAuthPayload(user.id)
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
json: mobileAuthPayload,
|
||||
}
|
||||
} catch (e) {
|
||||
return authFailedPayload
|
||||
}
|
||||
}
|
||||
|
||||
const authFailedPayload = {
|
||||
statusCode: 403,
|
||||
json: { errorCodes: ['AUTH_FAILED'] },
|
||||
|
|
|
|||
|
|
@ -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<JsonResponsePayload> {
|
||||
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'] },
|
||||
|
|
|
|||
|
|
@ -11,23 +11,25 @@ export function ResetSent(props: LoginFormProps): JSX.Element {
|
|||
width: '100vw',
|
||||
height: '100vh',
|
||||
bg: '$omnivoreYellow',
|
||||
overflowY: 'clip'
|
||||
overflowY: 'clip',
|
||||
}}
|
||||
>
|
||||
<Box css={{
|
||||
width: '100%',
|
||||
margin: '40px',
|
||||
color: '$omnivoreGray',
|
||||
'@xl': { margin: '138px' },
|
||||
}}>
|
||||
<h1>Reset email sent</h1>
|
||||
<Box>
|
||||
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.
|
||||
<Box
|
||||
css={{
|
||||
width: '100%',
|
||||
margin: '40px',
|
||||
color: '$omnivoreGray',
|
||||
'@xl': { margin: '138px' },
|
||||
}}
|
||||
>
|
||||
<h1>Reset email sent</h1>
|
||||
<Box>
|
||||
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.
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</HStack>
|
||||
</HStack>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue