add username and name to sign up form for ios
This commit is contained in:
@ -224,7 +224,7 @@ struct CreateProfileView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private extension PotentialUsernameStatus {
|
||||
extension PotentialUsernameStatus {
|
||||
var message: String? {
|
||||
switch self {
|
||||
case .tooShort:
|
||||
|
||||
@ -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<AnyCancellable>()
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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")
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user