diff --git a/app/build.gradle.kts b/app/build.gradle.kts index e07f40939ab3a37cfe763f4e5a1ed25a97bc3401..2ee2962cb09d26470512fe163883f2bd43f8a553 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -67,4 +67,5 @@ dependencies { androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) implementation("com.github.PhilJay:MPAndroidChart:v3.1.0") + implementation("org.jsoup:jsoup:1.14.3") } \ No newline at end of file diff --git a/app/src/main/java/com/example/abe/data/network/Retrofit.kt b/app/src/main/java/com/example/abe/data/network/Retrofit.kt index 0c04ca9e9f849b8a9551df61dad4aa025707f694..992568b5827e4826620040170b84655550e69947 100644 --- a/app/src/main/java/com/example/abe/data/network/Retrofit.kt +++ b/app/src/main/java/com/example/abe/data/network/Retrofit.kt @@ -60,12 +60,12 @@ class Retrofit { callback.onSuccess(it) } } else { - callback.onFailure("Login failed") + callback.onFailure("Login failed, incorrect email or password") } } override fun onFailure(call: Call<LoginResponse>, t: Throwable) { - callback.onFailure("Failed to send request") + callback.onFailure("Failed to send login request") } }) } diff --git a/app/src/main/java/com/example/abe/ui/form_transaction/FormTransaction.kt b/app/src/main/java/com/example/abe/ui/form_transaction/FormTransaction.kt index 688600c39d361493b7765218bfca9e39048a8bf2..386b625c8015d68f32910c51e5614c7e847810b5 100644 --- a/app/src/main/java/com/example/abe/ui/form_transaction/FormTransaction.kt +++ b/app/src/main/java/com/example/abe/ui/form_transaction/FormTransaction.kt @@ -30,6 +30,7 @@ import androidx.navigation.fragment.findNavController import com.example.abe.ABEApplication import com.example.abe.R import com.example.abe.databinding.FragmentFormTransactionBinding +import com.example.abe.utils.isNumericValid import com.google.android.gms.location.FusedLocationProviderClient import com.google.android.gms.location.LocationServices import com.google.android.material.textfield.TextInputLayout @@ -51,7 +52,6 @@ class FormTransaction : Fragment() { private lateinit var user: String private var id: Int? = null - private var location: String? = null override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? @@ -147,7 +147,11 @@ class FormTransaction : Fragment() { private fun amountFocusListener() { binding.formAmountEditText.setOnFocusChangeListener { _, focused -> - if (!focused) setHelperText(binding.formAmountContainer, binding.formAmountEditText) + if (!focused) setHelperText( + binding.formAmountContainer, + binding.formAmountEditText, + true + ) } } @@ -175,24 +179,30 @@ class FormTransaction : Fragment() { } } - private fun setHelperText(container: TextInputLayout, editText: EditText) { - container.helperText = - if (editText.text.toString().isEmpty()) "This field is required" else null + private fun setHelperText( + container: TextInputLayout, + editText: EditText, + isNumeric: Boolean = false + ) { + if (editText.text.toString().isEmpty()) { + container.helperText = "This field is required" + } else if (isNumeric && !isNumericValid(editText.text.toString())) { + container.helperText = "This field must be number" + } else { + container.helperText = null + } } private fun saveButtonListener() { binding.btnSave.setOnClickListener { setHelperText(binding.formTitleContainer, binding.formTitleEditText) - setHelperText(binding.formAmountContainer, binding.formAmountEditText) + setHelperText(binding.formAmountContainer, binding.formAmountEditText, true) setHelperText(binding.formCategoryContainer, binding.categoryAutocomplete) - if (binding.formLocationEditText.text.toString().isEmpty()) { - binding.formLocationEditText.setText(location) - } - if (binding.formTitleEditText.text.toString() .isNotEmpty() && binding.formAmountEditText.text.toString() - .isNotEmpty() && binding.categoryAutocomplete.text.toString().isNotEmpty() + .isNotEmpty() && binding.categoryAutocomplete.text.toString() + .isNotEmpty() && isNumericValid(binding.formAmountEditText.text.toString()) ) { if (id != null) { viewModel.updateTransaction(id!!) @@ -280,11 +290,7 @@ class FormTransaction : Fragment() { viewModel.latitude.value = latitude viewModel.longitude.value = longitude if (list != null) { - // TODO: check why this sometimes doesn't update the ui viewModel.location.value = (list[0].getAddressLine(0)) - location = (list[0].getAddressLine(0)) -// Toast.makeText(requireActivity(), viewModel.location.value, Toast.LENGTH_SHORT) -// .show() } } diff --git a/app/src/main/java/com/example/abe/ui/login/LoginActivity.kt b/app/src/main/java/com/example/abe/ui/login/LoginActivity.kt index b1472f0f28cd403dd902636d32d19f82c8f3e6ec..157d02343ae6efd2c70b84c5acf2136a67c638e3 100644 --- a/app/src/main/java/com/example/abe/ui/login/LoginActivity.kt +++ b/app/src/main/java/com/example/abe/ui/login/LoginActivity.kt @@ -3,11 +3,12 @@ package com.example.abe.ui.login import android.content.Context import android.content.Intent import android.os.Bundle -import android.util.Log +import android.util.Patterns import android.view.View +import android.widget.EditText +import android.widget.Toast import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity -import androidx.lifecycle.asLiveData import androidx.lifecycle.lifecycleScope import com.example.abe.MainActivity import com.example.abe.R @@ -17,7 +18,7 @@ import com.example.abe.data.network.LoginResultCallback import com.example.abe.data.network.Retrofit import com.example.abe.databinding.ActivityLoginBinding import com.example.abe.utils.isConnected -import kotlinx.coroutines.flow.firstOrNull +import com.google.android.material.textfield.TextInputLayout import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach @@ -29,14 +30,17 @@ class LoginActivity : AppCompatActivity(), LoginResultCallback { private fun attemptLogin(email: String, password: String) { val retrofit = Retrofit() - retrofit.login(email, password, this) + if (email != "" && password != "") { + retrofit.login(email, password, this) + } else { + Toast.makeText(applicationContext, "Please fill in your email and password", Toast.LENGTH_SHORT).show() + } } private lateinit var connectivityObserver: ConnectivityObserver private var networkState: ConnectivityObserver.NetworkState? = null override fun onSuccess(loginResponse: com.example.abe.data.network.LoginResponse) { - // Handle successful login println("Login successful: $loginResponse") val sharedPref = @@ -53,8 +57,7 @@ class LoginActivity : AppCompatActivity(), LoginResultCallback { } override fun onFailure(errorMessage: String) { - // Handle login failure - println(errorMessage) + Toast.makeText(applicationContext, errorMessage, Toast.LENGTH_SHORT).show() } override fun onCreate(savedInstanceState: Bundle?) { @@ -84,9 +87,15 @@ class LoginActivity : AppCompatActivity(), LoginResultCallback { }.launchIn(lifecycleScope) binding.btnSignIn.setOnClickListener { + setHelperText(binding.formEmailContainer, binding.emailInput, true) + setHelperText(binding.formPasswordContainer, binding.passwordInput, false) + email = binding.emailInput.text.toString() val password: String = binding.passwordInput.text.toString() + if (email.isEmpty() || password.isEmpty()) { + return@setOnClickListener + } if (!isConnected(networkState)) { binding.loginLayout.visibility = View.GONE binding.noNetworkLayout.visibility = View.VISIBLE @@ -99,5 +108,34 @@ class LoginActivity : AppCompatActivity(), LoginResultCallback { binding.noNetworkLayout.visibility = View.GONE binding.loginLayout.visibility = View.VISIBLE } + + emailFocusListener() + passwordFocusListener() + } + + private fun emailFocusListener() { + binding.emailInput.setOnFocusChangeListener { _, focused -> + if (!focused) setHelperText(binding.formEmailContainer, binding.emailInput, true) + } + } + + private fun passwordFocusListener() { + binding.passwordInput.setOnFocusChangeListener { _, focused -> + if (!focused) setHelperText(binding.formPasswordContainer, binding.passwordInput, false) + } + } + + private fun isValidEmail(email: String): Boolean { + return Patterns.EMAIL_ADDRESS.matcher(email).matches() + } + + private fun setHelperText(container: TextInputLayout, editText: EditText, isEmail: Boolean) { + if (editText.text.toString().isEmpty()) { + container.helperText = "This field is required" + } else if (isEmail && !isValidEmail(editText.text.toString())) { + container.helperText = "Invalid email address" + } else { + container.helperText = null + } } } diff --git a/app/src/main/java/com/example/abe/utils/Utils.kt b/app/src/main/java/com/example/abe/utils/Utils.kt index d68b03eaef07b4a4338b6459ba5c344cfe3adaaa..d3ff7f6ebeb93595dbe210d65b39eef651f4e1f2 100644 --- a/app/src/main/java/com/example/abe/utils/Utils.kt +++ b/app/src/main/java/com/example/abe/utils/Utils.kt @@ -1,7 +1,17 @@ package com.example.abe.utils import com.example.abe.connection.ConnectivityObserver +import org.jsoup.Jsoup +import org.jsoup.safety.Safelist fun isConnected(networkState: ConnectivityObserver.NetworkState?): Boolean { return (networkState != null && (networkState == ConnectivityObserver.NetworkState.AVAILABLE || networkState == ConnectivityObserver.NetworkState.LOSING)) } + +fun isNumericValid(input: String): Boolean { + return input.matches("\\d+".toRegex()) +} + +fun sanitizeHtml(input: String): String { + return Jsoup.clean(input, Safelist.basic()) +} diff --git a/app/src/main/res/layout/activity_login.xml b/app/src/main/res/layout/activity_login.xml index 48f3fa71a778687cb4c4e7d311426835aceebaa6..bb24e754c314385258772648f15159cab3fa595a 100644 --- a/app/src/main/res/layout/activity_login.xml +++ b/app/src/main/res/layout/activity_login.xml @@ -17,52 +17,78 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"> - <EditText - android:id="@+id/emailInput" - android:layout_width="300dp" + <com.google.android.material.textfield.TextInputLayout + android:id="@+id/formEmailContainer" + style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense" + android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginBottom="16dp" - android:ems="10" - android:hint="Email" - android:inputType="textEmailAddress" - android:outlineAmbientShadowColor="#000000" - android:outlineProvider="background" - app:layout_constraintBottom_toTopOf="@+id/passwordInput" + android:layout_marginHorizontal="20dp" + android:layout_marginTop="15dp" + app:helperTextTextColor="@color/destructive" + app:layout_constraintBottom_toTopOf="@+id/formPasswordContainer" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0.5" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/appName" app:layout_constraintVertical_bias="0.5" - app:layout_constraintVertical_chainStyle="packed" /> + app:layout_constraintVertical_chainStyle="packed" + > + + <com.google.android.material.textfield.TextInputEditText + android:id="@+id/emailInput" + android:layout_width="300dp" + android:layout_height="wrap_content" + android:ems="10" + android:maxLength="255" + android:hint="Email" + android:inputType="textEmailAddress" + android:outlineAmbientShadowColor="#000000" + android:outlineProvider="background" /> + </com.google.android.material.textfield.TextInputLayout> - <EditText - android:id="@+id/passwordInput" - android:layout_width="300dp" + <com.google.android.material.textfield.TextInputLayout + android:id="@+id/formPasswordContainer" + style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense" + android:layout_width="match_parent" android:layout_height="wrap_content" - android:ems="10" - android:hint="Password" - android:inputType="textPassword" + android:layout_marginHorizontal="20dp" + android:layout_marginTop="5dp" + app:helperTextTextColor="@color/destructive" app:layout_constraintBottom_toTopOf="@+id/btnSignIn" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0.5" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/emailInput" - app:layout_constraintVertical_bias="0.5" /> + app:layout_constraintTop_toBottomOf="@+id/formEmailContainer" + app:layout_constraintVertical_bias="0.5" + > + + <com.google.android.material.textfield.TextInputEditText + android:id="@+id/passwordInput" + android:layout_width="300dp" + android:layout_height="wrap_content" + android:maxLength="255" + android:ems="10" + android:hint="Password" + android:inputType="textPassword" + /> + + </com.google.android.material.textfield.TextInputLayout> <Button android:id="@+id/btnSignIn" - android:layout_width="300dp" - android:layout_height="wrap_content" + style="@style/Widget.Material3.Button" + android:layout_width="0dp" + android:layout_marginHorizontal="20dp" + android:layout_height="match_parent" android:layout_marginTop="36dp" - android:background="#9759C4" - android:clickable="true" android:text="Sign in" + android:textSize="20sp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0.5" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/passwordInput" + app:layout_constraintTop_toBottomOf="@+id/formPasswordContainer" app:layout_constraintVertical_bias="0.5" /> <TextView @@ -93,7 +119,7 @@ android:textColor="#9759C4" android:textSize="24sp" android:textStyle="bold" - app:layout_constraintBottom_toTopOf="@+id/emailInput" + app:layout_constraintBottom_toTopOf="@+id/formEmailContainer" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0.5" app:layout_constraintStart_toStartOf="parent" diff --git a/app/src/main/res/layout/fragment_form_transaction.xml b/app/src/main/res/layout/fragment_form_transaction.xml index 1f190286bd265c2a82573b58e8ce08c6b8535ed6..5728f18d2cccff73365964db6ff6b8fe7c7e9866 100644 --- a/app/src/main/res/layout/fragment_form_transaction.xml +++ b/app/src/main/res/layout/fragment_form_transaction.xml @@ -29,6 +29,7 @@ android:id="@+id/formTitleEditText" android:layout_width="match_parent" android:layout_height="match_parent" + android:maxLength="255" android:hint="@string/title" android:text="@={viewModel.title}" android:inputType="text" @@ -52,6 +53,7 @@ android:id="@+id/formAmountEditText" android:layout_width="match_parent" android:layout_height="match_parent" + android:maxLength="10" android:hint="@string/amount" android:text="@={viewModel.amount}" android:inputType="number" @@ -114,11 +116,12 @@ <com.google.android.material.textfield.TextInputEditText android:id="@+id/formLocationEditText" android:layout_width="match_parent" - android:layout_height="match_parent" + android:layout_height="wrap_content" android:hint="@string/location" android:inputType="none" - android:lines="2" - android:text="@={viewModel.location}" /> + android:text="@={viewModel.location}" + android:focusable="false" + android:focusableInTouchMode="false"/> </com.google.android.material.textfield.TextInputLayout> diff --git a/app/src/main/res/navigation/mobile_navigation.xml b/app/src/main/res/navigation/mobile_navigation.xml index 2d8908aa4dc9a1eee92e068e6dc6dbeca2db928a..6b817e8f061acda53c67e1547f0709d95b6e4fce 100644 --- a/app/src/main/res/navigation/mobile_navigation.xml +++ b/app/src/main/res/navigation/mobile_navigation.xml @@ -21,7 +21,7 @@ <fragment android:id="@+id/navigation_form_transaction" - android:label="Add Transaction" + android:label="Form Transaction" android:name="com.example.abe.ui.form_transaction.FormTransaction" tools:layout="@layout/fragment_form_transaction" />