From 5e1744f7d6d37b40580164a94a8cd59db4973390 Mon Sep 17 00:00:00 2001 From: Billy Brawner Date: Tue, 7 Jun 2022 21:14:20 -0600 Subject: [PATCH] Implement error messages for login & registration --- Twigs/DataStore.swift | 49 +++++++++---- Twigs/LoginView.swift | 66 +++++++++++++----- Twigs/RegistrationView.swift | 108 ++++++++++++++++++++--------- Twigs/SidebarBudgetView.swift | 10 --- Twigs/TabbedBudgetView.swift | 4 +- Twigs/en.lproj/Localizable.strings | 8 ++- Twigs/es.lproj/Localizable.strings | 8 ++- 7 files changed, 174 insertions(+), 79 deletions(-) diff --git a/Twigs/DataStore.swift b/Twigs/DataStore.swift index 8e985e7..ee82255 100644 --- a/Twigs/DataStore.swift +++ b/Twigs/DataStore.swift @@ -30,7 +30,7 @@ class DataStore : ObservableObject { } } @Published var overview: AsyncData = .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 -} diff --git a/Twigs/LoginView.swift b/Twigs/LoginView.swift index 5e49317..246796d 100644 --- a/Twigs/LoginView.swift +++ b/Twigs/LoginView.swift @@ -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") } } } diff --git a/Twigs/RegistrationView.swift b/Twigs/RegistrationView.swift index df5c462..450b364 100644 --- a/Twigs/RegistrationView.swift +++ b/Twigs/RegistrationView.swift @@ -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() + } } } diff --git a/Twigs/SidebarBudgetView.swift b/Twigs/SidebarBudgetView.swift index 45b0b97..8e3cc20 100644 --- a/Twigs/SidebarBudgetView.swift +++ b/Twigs/SidebarBudgetView.swift @@ -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) } diff --git a/Twigs/TabbedBudgetView.swift b/Twigs/TabbedBudgetView.swift index 9939c52..52393e5 100644 --- a/Twigs/TabbedBudgetView.swift +++ b/Twigs/TabbedBudgetView.swift @@ -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") diff --git a/Twigs/en.lproj/Localizable.strings b/Twigs/en.lproj/Localizable.strings index 9df2660..c3deee5 100644 --- a/Twigs/en.lproj/Localizable.strings +++ b/Twigs/en.lproj/Localizable.strings @@ -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"; diff --git a/Twigs/es.lproj/Localizable.strings b/Twigs/es.lproj/Localizable.strings index dafecaa..b715c45 100644 --- a/Twigs/es.lproj/Localizable.strings +++ b/Twigs/es.lproj/Localizable.strings @@ -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";