From 431bfa2ca90ce2ad9af6d48be9097f0d38a73d57 Mon Sep 17 00:00:00 2001 From: vincent serem Date: Wed, 25 Mar 2026 09:34:45 +1100 Subject: [PATCH 1/3] feat: Improved login and registration flow with form validation and enhanced UI feedback - Added `TextInputLayout` with error messaging to `LoginActivity` and `RegisterActivity` for real-time validation feedback. - Implemented text change listeners to clear validation errors dynamically. - Refactored loading states to properly toggle button enablement and progress bar visibility during API calls. - Improved error handling by parsing `ApiErrorResponse` and providing more descriptive error messages. - Updated layouts (`activity_login.xml`, `account_creation.xml`) to include `TextInputLayout` containers and fix autofill hints. - Added explicit password validation for the login process. --- .../guardian/view/general/LoginActivity.kt | 81 +++++++++++------ .../guardian/view/general/RegisterActivity.kt | 91 ++++++++++++------- app/src/main/res/layout/account_creation.xml | 91 +++++++------------ app/src/main/res/layout/activity_login.xml | 18 ++-- 4 files changed, 156 insertions(+), 125 deletions(-) diff --git a/app/src/main/java/deakin/gopher/guardian/view/general/LoginActivity.kt b/app/src/main/java/deakin/gopher/guardian/view/general/LoginActivity.kt index c03d645d6..660de7c99 100644 --- a/app/src/main/java/deakin/gopher/guardian/view/general/LoginActivity.kt +++ b/app/src/main/java/deakin/gopher/guardian/view/general/LoginActivity.kt @@ -10,6 +10,7 @@ import android.widget.ProgressBar import android.widget.TextView import android.widget.Toast import androidx.appcompat.app.AlertDialog +import androidx.core.widget.addTextChangedListener import com.google.android.gms.auth.api.signin.GoogleSignIn import com.google.android.gms.auth.api.signin.GoogleSignInAccount import com.google.android.gms.auth.api.signin.GoogleSignInClient @@ -17,6 +18,7 @@ import com.google.android.gms.auth.api.signin.GoogleSignInOptions import com.google.android.gms.common.SignInButton import com.google.android.gms.common.api.ApiException import com.google.android.gms.tasks.Task +import com.google.android.material.textfield.TextInputLayout import com.google.firebase.auth.FirebaseAuth import com.google.firebase.auth.GoogleAuthProvider import com.google.gson.Gson @@ -44,6 +46,8 @@ class LoginActivity : BaseActivity() { setContentView(R.layout.activity_login) val mEmail: EditText = findViewById(R.id.Email) val mPassword: EditText = findViewById(R.id.password) + val emailLayout: TextInputLayout = findViewById(R.id.emailTextInputLayout) + val passwordLayout: TextInputLayout = findViewById(R.id.passwordTextInputLayout) val progressBar: ProgressBar = findViewById(R.id.progressBar) val loginButton: Button = findViewById(R.id.loginBtn) val loginGoogleButton: SignInButton = findViewById(R.id.loginGoogleBtn) @@ -56,23 +60,30 @@ class LoginActivity : BaseActivity() { .build() gsoClient = GoogleSignIn.getClient(this, gso) + mEmail.addTextChangedListener { emailLayout.error = null } + mPassword.addTextChangedListener { passwordLayout.error = null } + loginButton.setOnClickListener { - progressBar.show() val emailInput = mEmail.text.toString().trim { it <= ' ' } val passwordInput = mPassword.text.toString().trim { it <= ' ' } - val loginValidationError = validateInputs(emailInput) + val loginValidationError = validateInputs(emailInput, passwordInput) if (loginValidationError != null) { - progressBar.hide() - Toast.makeText( - applicationContext, - loginValidationError.messageResoureId, - Toast.LENGTH_LONG, - ).show() + when (loginValidationError) { + LoginValidationError.EmptyEmail, LoginValidationError.InvalidEmail -> { + emailLayout.error = getString(loginValidationError.messageResoureId) + } + LoginValidationError.EmptyPassword -> { + passwordLayout.error = getString(loginValidationError.messageResoureId) + } + else -> showMessage(getString(loginValidationError.messageResoureId)) + } return@setOnClickListener } + setLoading(true, loginButton, progressBar) + val call = ApiClient.apiService.login(emailInput, passwordInput) call.enqueue( @@ -81,21 +92,15 @@ class LoginActivity : BaseActivity() { call: Call, response: Response, ) { - progressBar.hide() + setLoading(false, loginButton, progressBar) if (response.isSuccessful && response.body() != null) { - // Handle successful login val user = response.body()!!.user val token = response.body()!!.token SessionManager.createLoginSession(user, token) NavigationService(this@LoginActivity).toPinCodeActivity(user.role) } else { - // Handle error - val errorResponse = - Gson().fromJson( - response.errorBody()?.string(), - ApiErrorResponse::class.java, - ) - showMessage(errorResponse.apiError ?: response.message()) + val errorResponse = parseError(response) + showMessage(errorResponse ?: response.message()) } } @@ -103,9 +108,8 @@ class LoginActivity : BaseActivity() { call: Call, t: Throwable, ) { - // Handle failure - progressBar.hide() - showMessage(getString(R.string.toast_login_error, t.message)) + setLoading(false, loginButton, progressBar) + showMessage(getString(R.string.toast_login_error, t.localizedMessage)) } }, ) @@ -143,7 +147,29 @@ class LoginActivity : BaseActivity() { } } - private fun validateInputs(rawEmail: String?): LoginValidationError? { + private fun setLoading(isLoading: Boolean, button: Button, progressBar: ProgressBar) { + if (isLoading) { + button.isEnabled = false + progressBar.show() + } else { + button.isEnabled = true + progressBar.hide() + } + } + + private fun parseError(response: Response<*>): String? { + return try { + val errorResponse = Gson().fromJson( + response.errorBody()?.string(), + ApiErrorResponse::class.java, + ) + errorResponse.apiError + } catch (e: Exception) { + null + } + } + + private fun validateInputs(rawEmail: String?, rawPassword: String?): LoginValidationError? { if (rawEmail.isNullOrEmpty()) { return LoginValidationError.EmptyEmail } @@ -153,6 +179,10 @@ class LoginActivity : BaseActivity() { return LoginValidationError.InvalidEmail } + if (rawPassword.isNullOrEmpty()) { + return LoginValidationError.EmptyPassword + } + return null } @@ -170,13 +200,8 @@ class LoginActivity : BaseActivity() { ?: getString(R.string.toast_reset_link_sent_to_your_email), ) } else { - // Handle error - val errorResponse = - Gson().fromJson( - response.errorBody()?.string(), - ApiErrorResponse::class.java, - ) - showMessage(errorResponse.apiError ?: response.message()) + val errorResponse = parseError(response) + showMessage(errorResponse ?: response.message()) } } diff --git a/app/src/main/java/deakin/gopher/guardian/view/general/RegisterActivity.kt b/app/src/main/java/deakin/gopher/guardian/view/general/RegisterActivity.kt index 41fcbeaa0..a28a553e3 100644 --- a/app/src/main/java/deakin/gopher/guardian/view/general/RegisterActivity.kt +++ b/app/src/main/java/deakin/gopher/guardian/view/general/RegisterActivity.kt @@ -1,15 +1,18 @@ package deakin.gopher.guardian.view.general import android.os.Bundle -import android.view.View import android.widget.Button import android.widget.EditText import android.widget.ProgressBar import android.widget.Toast import androidx.core.view.isVisible +import androidx.core.widget.addTextChangedListener import com.google.android.material.button.MaterialButton import com.google.android.material.button.MaterialButtonToggleGroup +import com.google.android.material.textfield.TextInputLayout +import com.google.gson.Gson import deakin.gopher.guardian.R +import deakin.gopher.guardian.model.ApiErrorResponse import deakin.gopher.guardian.model.RegistrationStatusMessage import deakin.gopher.guardian.model.login.EmailAddress import deakin.gopher.guardian.model.login.Password @@ -30,17 +33,29 @@ class RegisterActivity : BaseActivity() { val mEmail: EditText = findViewById(R.id.Email) val mPassword: EditText = findViewById(R.id.password) val passwordConfirmation: EditText = findViewById(R.id.passwordConfirm) + val nameLayout: TextInputLayout = findViewById(R.id.nameTextInputLayout) + val emailLayout: TextInputLayout = findViewById(R.id.emailTextInputLayout) + val passwordLayout: TextInputLayout = findViewById(R.id.passwordTextInputLayout) + val confirmPasswordLayout: TextInputLayout = findViewById(R.id.confirmPasswordTextInputLayout) val roleButton: MaterialButtonToggleGroup = findViewById(R.id.role_toggle_group) val backToLoginButton: Button = findViewById(R.id.backToLoginButton) val mRegisterBtn: Button = findViewById(R.id.registerBtn) val progressBar: ProgressBar = findViewById(R.id.progressBar) + mFullName.addTextChangedListener { nameLayout.error = null } + mEmail.addTextChangedListener { emailLayout.error = null } + mPassword.addTextChangedListener { passwordLayout.error = null } + passwordConfirmation.addTextChangedListener { confirmPasswordLayout.error = null } + roleButton.addOnButtonCheckedListener { _, _, _ -> + // Optional: clear role error if you add one to the layout + } + mRegisterBtn.setOnClickListener { val emailInput = mEmail.text.toString().trim { it <= ' ' } val passwordInput = mPassword.text.toString().trim { it <= ' ' } val passwordConfirmInput = passwordConfirmation.text.toString().trim { it <= ' ' } val nameInput = mFullName.text.toString().trim { it <= ' ' } - val roleInput = roleButton.checkedButtonId + val roleId = roleButton.checkedButtonId val registrationError = validateInputs( @@ -48,28 +63,32 @@ class RegisterActivity : BaseActivity() { passwordInput, passwordConfirmInput, nameInput, - roleInput, + roleId, ) if (registrationError != null) { - Toast.makeText( - applicationContext, - registrationError.messageResourceId, - Toast.LENGTH_LONG, - ).show() + when (registrationError) { + RegistrationError.EmptyName -> nameLayout.error = getString(registrationError.messageResourceId) + RegistrationError.EmptyEmail, RegistrationError.InvalidEmail -> emailLayout.error = getString(registrationError.messageResourceId) + RegistrationError.EmptyPassword, RegistrationError.PasswordTooShort -> passwordLayout.error = getString(registrationError.messageResourceId) + RegistrationError.EmptyConfirmedPassword, RegistrationError.PasswordsFailConfirmation -> confirmPasswordLayout.error = getString(registrationError.messageResourceId) + RegistrationError.EmptyRole -> showMessage(getString(registrationError.messageResourceId)) + } return@setOnClickListener } - progressBar.visibility = View.VISIBLE + setLoading(true, mRegisterBtn, progressBar) - val emailAddress = EmailAddress(emailInput) - val password = Password(passwordInput) - val role = findViewById(roleInput).text.toString().lowercase() + val role = when (roleId) { + R.id.button_caretaker -> "caretaker" + R.id.button_nurse -> "nurse" + else -> "" + } val request = RegisterRequest( - email = emailAddress.emailAddress, - password = password.password, + email = emailInput, + password = passwordInput, name = nameInput, role = role, ) @@ -82,18 +101,13 @@ class RegisterActivity : BaseActivity() { call: Call, response: Response, ) { - progressBar.isVisible = false + setLoading(false, mRegisterBtn, progressBar) if (response.isSuccessful) { - // Handle successful registration - showMessage(RegistrationStatusMessage.Success.toString()) + showMessage(getString(R.string.registration_success)) NavigationService(this@RegisterActivity).toLogin() } else { - // Handle error - showMessage( - RegistrationStatusMessage.Failure.toString() + " : ${ - response.errorBody() - }", - ) + val errorMsg = parseError(response) + showMessage(getString(R.string.registration_failure) + (if (errorMsg != null) ": $errorMsg" else "")) } } @@ -101,9 +115,8 @@ class RegisterActivity : BaseActivity() { call: Call, t: Throwable, ) { - // Handle failure - progressBar.isVisible = false - showMessage(RegistrationStatusMessage.Failure.toString() + ": ${t.message}") + setLoading(false, mRegisterBtn, progressBar) + showMessage(getString(R.string.registration_failure) + ": ${t.localizedMessage}") } }, ) @@ -114,6 +127,23 @@ class RegisterActivity : BaseActivity() { } } + private fun setLoading(isLoading: Boolean, button: Button, progressBar: ProgressBar) { + button.isEnabled = !isLoading + progressBar.isVisible = isLoading + } + + private fun parseError(response: Response<*>): String? { + return try { + val errorResponse = Gson().fromJson( + response.errorBody()?.string(), + ApiErrorResponse::class.java, + ) + errorResponse.apiError + } catch (e: Exception) { + null + } + } + private fun validateInputs( rawEmail: String?, rawPassword: String?, @@ -121,6 +151,9 @@ class RegisterActivity : BaseActivity() { rawName: String?, roleInput: Int, ): RegistrationError? { + if (rawName.isNullOrEmpty()) { + return RegistrationError.EmptyName + } if (rawEmail.isNullOrEmpty()) { return RegistrationError.EmptyEmail } @@ -147,11 +180,7 @@ class RegisterActivity : BaseActivity() { return RegistrationError.PasswordsFailConfirmation } - if (rawName.isNullOrEmpty()) { - return RegistrationError.EmptyName - } - - if (roleInput == View.NO_ID) { + if (roleInput == android.view.View.NO_ID) { return RegistrationError.EmptyRole } diff --git a/app/src/main/res/layout/account_creation.xml b/app/src/main/res/layout/account_creation.xml index c26e201f3..473db4575 100644 --- a/app/src/main/res/layout/account_creation.xml +++ b/app/src/main/res/layout/account_creation.xml @@ -4,7 +4,7 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" - tools:context="deakin.gopher.guardian.view.general.LoginActivity"> + tools:context="deakin.gopher.guardian.view.general.RegisterActivity"> + android:padding="16dp"> + app:srcCompat="@drawable/agedcare_icon" /> + android:layout_height="wrap_content" + app:errorEnabled="true"> + android:layout_height="wrap_content" + app:errorEnabled="true"> + android:paddingHorizontal="16dp" /> + android:inputType="textPassword" /> + android:inputType="textPassword" /> + android:text="@string/register_role_selection_prompt" /> @@ -199,41 +182,33 @@ android:id="@+id/registerBtn" android:layout_width="200dp" android:layout_height="50dp" + android:layout_marginTop="16dp" android:backgroundTint="@color/TG_blue" android:fontFamily="@font/poppins_bold" android:text="@string/create_account" android:textAllCaps="false" android:textColor="@color/white" - app:cornerRadius="24dp" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintHorizontal_bias="0.497" - app:layout_constraintStart_toStartOf="parent" - tools:layout_editor_absoluteY="372dp" /> + app:cornerRadius="24dp" />