Implement error messages for login & registration
This commit is contained in:
parent
d72be0abd9
commit
5e1744f7d6
7 changed files with 174 additions and 79 deletions
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
Loading…
Reference in a new issue