add username and name to sign up form for ios

This commit is contained in:
Satindar Dhillon
2022-07-29 11:02:36 -07:00
parent 4e7010f7c1
commit 8b45095466
5 changed files with 403 additions and 264 deletions

View File

@ -224,7 +224,7 @@ struct CreateProfileView: View {
}
}
private extension PotentialUsernameStatus {
extension PotentialUsernameStatus {
var message: String? {
switch self {
case .tooShort:

View File

@ -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()
}
}
}

View File

@ -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")
}
}

View File

@ -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)
}
}
}

View File

@ -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")
}
}