Implement error messages for login & registration

This commit is contained in:
William Brawner 2022-06-07 21:14:20 -06:00
parent d72be0abd9
commit 5e1744f7d6
7 changed files with 174 additions and 79 deletions

View file

@ -30,7 +30,7 @@ class DataStore : ObservableObject {
}
}
@Published var overview: AsyncData<BudgetOverview> = .empty
@Published var showBudgetSelection: Bool = true
@Published var showBudgetSelection: Bool = false
@Published var editingBudget: Bool = false
@Published var editingCategory: Bool = false
@Published var editingRecurringTransaction: Bool = false
@ -81,6 +81,9 @@ class DataStore : ObservableObject {
do {
let budgets = try await self.apiService.getBudgets(count: count, page: page).sorted(by: { $0.name < $1.name })
self.budgets = .success(budgets)
if budgets.isEmpty {
showBudgetSelection = true
}
if self.budget != .empty {
return
}
@ -93,6 +96,7 @@ class DataStore : ObservableObject {
}
} catch {
self.budgets = .error(error)
showBudgetSelection = true
}
}
@ -479,6 +483,8 @@ class DataStore : ObservableObject {
switch currentUser {
case .empty, .loading:
self.showLogin = true
case .error(_, let user):
self.showLogin = user == nil
default:
self.showLogin = false
}
@ -508,7 +514,15 @@ class DataStore : ObservableObject {
}
@Published var showLogin: Bool = true
func clearUserError() {
self.currentUser = .empty
}
func login(username: String, password: String) async {
if baseUrl.isEmpty {
self.currentUser = .error(NetworkError.invalidUrl)
return
}
self.currentUser = .loading
do {
let response = try await self.apiService.login(username: username, password: password)
@ -527,9 +541,24 @@ class DataStore : ObservableObject {
}
func register(username: String, email: String, password: String, confirmPassword: String) async {
// TODO: Validate other fields as well
if baseUrl.isEmpty {
self.currentUser = .error(NetworkError.invalidUrl)
return
}
if username.isEmpty {
self.currentUser = .error(UsernameError.empty)
return
}
if !email.isEmpty && (!email.contains("@") || !email.contains(".")) {
self.currentUser = .error(EmailError.invalid)
return
}
if password.isEmpty {
self.currentUser = .error(PasswordError.empty)
return
}
if !password.elementsEqual(confirmPassword) {
// TODO: Show error message to user
self.currentUser = .error(PasswordError.notMatching)
return
}
do {
@ -541,6 +570,7 @@ class DataStore : ObservableObject {
default:
print(error.localizedDescription)
}
self.currentUser = .error(error)
return
}
await self.login(username: username, password: password)
@ -568,7 +598,6 @@ class DataStore : ObservableObject {
func loadProfile() async {
guard let userId = self.userId, !userId.isEmpty else {
self.currentUser = .error(UserStatus.unauthenticated)
return
}
do {
@ -659,7 +688,7 @@ class DataStore : ObservableObject {
}
enum UsernameError: String, Error {
case empty = "cannot_be_empty"
case empty = "username_cannot_be_empty"
case unavailable = "username_taken"
case unknown = "unknown_error"
}
@ -671,15 +700,7 @@ enum EmailError: String, Error {
}
enum PasswordError: String, Error {
case empty = "cannot_be_empty"
case empty = "password_cannot_be_empty"
case notMatching = "passwords_dont_match"
case unknown = "unknown_error"
}
enum UserStatus: Error, Equatable {
case unauthenticated
case authenticating
case failedAuthentication
case authenticated
case passwordMismatch
}

View file

@ -8,28 +8,46 @@
import SwiftUI
import Combine
import TwigsCore
struct LoginView: View {
@State var username: String = ""
@State var password: String = ""
@EnvironmentObject var dataStore: DataStore
var loading: Bool {
switch dataStore.user {
case .loading:
return true
default:
return false
var error: String? {
if case let .error(error as NetworkError, _) = dataStore.currentUser {
switch error {
case .badRequest(let reason):
if reason == nil || reason?.isEmpty == true {
return "unknown_error"
}
return reason
case .server:
return "server_error"
case .invalidUrl, .notFound:
return "server_invalid"
case .unauthorized:
return "credentials_invalid"
default:
return "unknown_error"
}
}
return nil
}
var body: some View {
LoadingView(
isShowing: .constant(loading),
loadingText: "loading_login"
) {
switch dataStore.currentUser {
case .loading:
ActivityIndicator(isAnimating: .constant(true), style: .medium)
default:
NavigationView {
VStack {
Text("info_login")
if let error = self.error {
Text(LocalizedStringKey(error))
.multilineTextAlignment(.center)
.foregroundColor(.red)
}
TextField(LocalizedStringKey("prompt_server"), text: self.$dataStore.baseUrl)
.autocapitalization(.none)
.textFieldStyle(RoundedBorderTextFieldStyle())
@ -49,19 +67,35 @@ struct LoginView: View {
await self.dataStore.login(username: self.username, password: self.password)
}
}
Button("action_login", action: {
Button(action: {
Task {
await self.dataStore.login(username: self.username, password: self.password)
}
}).buttonStyle(DefaultButtonStyle())
}, label: {
Text("action_login")
.frame(maxWidth: .infinity)
})
.buttonStyle(.borderedProminent)
Spacer()
Text("info_register")
NavigationLink(destination: RegistrationView(username: self.$username, password: self.$password)) {
Text("action_register")
.buttonStyle(DefaultButtonStyle())
HStack {
Text("info_register")
NavigationLink(
destination: RegistrationView(username: self.$username, password: self.$password)
.navigationTitle("action_register")
.onAppear {
dataStore.clearUserError()
}
.onDisappear {
dataStore.clearUserError()
}
) {
Text("action_register")
.buttonStyle(DefaultButtonStyle())
}
}
}.padding()
}.navigationBarHidden(true)
.navigationTitle("action_login")
}
}
}

View file

@ -7,6 +7,7 @@
//
import SwiftUI
import TwigsCore
struct RegistrationView: View {
@Binding var username: String
@ -14,27 +15,75 @@ struct RegistrationView: View {
@Binding var password: String
@State var confirmedPassword: String = ""
@EnvironmentObject var dataStore: DataStore
var error: String? {
if case let .error(error as NetworkError, _) = dataStore.currentUser {
switch error {
case .badRequest(let reason):
if reason == nil || reason?.isEmpty == true {
return "unknown_error"
}
return reason
case .server:
return "server_error"
case .invalidUrl, .notFound:
return "server_invalid"
case .unauthorized:
return "credentials_invalid"
default:
return "unknown_error"
}
}
if case let .error(error as UsernameError, _) = dataStore.currentUser {
return error.rawValue
}
if case let .error(error as EmailError, _) = dataStore.currentUser {
return error.rawValue
}
if case let .error(error as PasswordError, _) = dataStore.currentUser {
return error.rawValue
}
return nil
}
var body: some View {
VStack {
TextField(LocalizedStringKey("prompt_server"), text: self.$dataStore.baseUrl)
.textFieldStyle(RoundedBorderTextFieldStyle())
.textContentType(.URL)
TextField("prompt_username", text: self.$username)
.autocapitalization(UITextAutocapitalizationType.none)
.textFieldStyle(RoundedBorderTextFieldStyle())
.textContentType(UITextContentType.username)
TextField("prompt_email", text: self.$email)
.textContentType(UITextContentType.emailAddress)
.autocapitalization(UITextAutocapitalizationType.none)
.textFieldStyle(RoundedBorderTextFieldStyle())
SecureField("prompt_password", text: self.$password)
.textFieldStyle(RoundedBorderTextFieldStyle())
.textContentType(UITextContentType.newPassword)
SecureField("prompt_confirm_password", text: self.$confirmedPassword)
.textFieldStyle(RoundedBorderTextFieldStyle())
.textContentType(UITextContentType.newPassword)
.onSubmit {
switch dataStore.currentUser {
case .loading:
ActivityIndicator(isAnimating: .constant(true), style: .medium)
default:
VStack {
if let error = self.error {
Text(LocalizedStringKey(error))
.multilineTextAlignment(.center)
.foregroundColor(.red)
}
TextField(LocalizedStringKey("prompt_server"), text: self.$dataStore.baseUrl)
.textFieldStyle(RoundedBorderTextFieldStyle())
.textContentType(.URL)
TextField("prompt_username", text: self.$username)
.autocapitalization(UITextAutocapitalizationType.none)
.textFieldStyle(RoundedBorderTextFieldStyle())
.textContentType(UITextContentType.username)
TextField("prompt_email", text: self.$email)
.textContentType(UITextContentType.emailAddress)
.autocapitalization(UITextAutocapitalizationType.none)
.textFieldStyle(RoundedBorderTextFieldStyle())
SecureField("prompt_password", text: self.$password)
.textFieldStyle(RoundedBorderTextFieldStyle())
.textContentType(UITextContentType.newPassword)
SecureField("prompt_confirm_password", text: self.$confirmedPassword)
.textFieldStyle(RoundedBorderTextFieldStyle())
.textContentType(UITextContentType.newPassword)
.onSubmit {
Task {
await self.dataStore.register(
username: self.username,
email: self.email,
password: self.password,
confirmPassword: self.confirmedPassword
)
}
}
Button(action: {
Task {
await self.dataStore.register(
username: self.username,
@ -43,18 +92,13 @@ struct RegistrationView: View {
confirmPassword: self.confirmedPassword
)
}
}
Button("action_register", action: {
Task {
await self.dataStore.register(
username: self.username,
email: self.email,
password: self.password,
confirmPassword: self.confirmedPassword
)
}
}).buttonStyle(DefaultButtonStyle())
}.padding()
}, label: {
Text("action_register")
.frame(maxWidth: .infinity)
})
.buttonStyle(.borderedProminent)
}.padding()
}
}
}

View file

@ -63,19 +63,9 @@ struct SidebarBudgetView: View {
var body: some View {
mainView
.sheet(isPresented: $dataStore.showLogin,
onDismiss: {
Task {
await self.dataStore.getBudgets()
}
},
content: {
LoginView()
.environmentObject(dataStore)
.onDisappear {
Task {
await self.dataStore.getBudgets()
}
}
})
.interactiveDismissDisabled(true)
}

View file

@ -26,9 +26,7 @@ struct TabbedBudgetView: View {
Button("budgets", action: {
self.dataStore.showBudgetSelection = true
}).padding()
}, trailing: Button("logout", action: {
self.dataStore.logout()
}).padding())
})
}
.tabItem {
Image(systemName: "chart.line.uptrend.xyaxis.circle.fill")

View file

@ -135,8 +135,12 @@
"onDate" = "On Date";
"email_invalid" = "Invalid email address";
"cannot_be_empty" = "This cannot be empty";
"username_cannot_be_empty" = "Username cannot be empty";
"password_cannot_be_empty" = "Password cannot be empty";
"username_taken" = "Username taken";
"email_taken" = "Email taken";
"passwords_dont_match" = "Password don't match"
"passwords_dont_match" = "Password don't match";
"unknown_error" = "An unknown error has occurred";
"server_error" = "Server error";
"server_invalid" = "Invalid server URL";
"credentials_invalid" = "Invalid username/password";

View file

@ -136,8 +136,12 @@
"onDate" = "Fecha";
"email_invalid" = "Correo electrónico inválido";
"cannot_be_empty" = "No se puede omitir";
"username_cannot_be_empty" = "Nombre de usuario no se puede omitir";
"password_cannot_be_empty" = "Contraseña no se puede omitir";
"username_taken" = "Nombre de usuario no disponible";
"email_taken" = "Correo electrónico no disponible";
"passwords_dont_match" = "Contraseñas no coinciden"
"passwords_dont_match" = "Contraseñas no coinciden";
"unknown_error" = "Se ocurrió un error";
"server_error" = "Error de servidor";
"server_invalid" = "URL de servidor inválido";
"credentials_invalid" = "Usuario/contraseña inválidos";