diff --git a/.github/workflows/launch_unit_tests.yml b/.github/workflows/launch_unit_tests.yml new file mode 100644 index 00000000..eec2a29e --- /dev/null +++ b/.github/workflows/launch_unit_tests.yml @@ -0,0 +1,27 @@ +# This is a basic workflow to help you get started with Actions + +name: Unit tests + +# Controls when the workflow will run +on: + # Triggers the workflow on pull request events for the master branch + pull_request: + branches: [ master ] + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + # This workflow contains a single job called "build" + build: + # The type of runner that the job will run on + runs-on: ubuntu-latest + + # Steps represent a sequence of tasks that will be executed as part of the job + steps: + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it + - uses: actions/checkout@v2 + + - name: Run Unit Tests + run: ./gradlew test diff --git a/app/src/main/java/by/alexandr7035/affinidi_id/data/ApiService.kt b/app/src/main/java/by/alexandr7035/affinidi_id/data/ApiService.kt index 373215a1..99edb216 100644 --- a/app/src/main/java/by/alexandr7035/affinidi_id/data/ApiService.kt +++ b/app/src/main/java/by/alexandr7035/affinidi_id/data/ApiService.kt @@ -1,5 +1,7 @@ package by.alexandr7035.affinidi_id.data +import by.alexandr7035.affinidi_id.data.model.reset_password.ConfirmResetPasswordRequest +import by.alexandr7035.affinidi_id.data.model.reset_password.InitializeResetPasswordRequest import by.alexandr7035.affinidi_id.data.model.sign_in.SignInRequest import by.alexandr7035.affinidi_id.data.model.sign_in.SignInResponse import by.alexandr7035.affinidi_id.data.model.sign_up.ConfirmSignUpRequest @@ -25,4 +27,12 @@ interface ApiService { @POST("api/v1/users/logout") suspend fun logOut(@Header("Authorization") accessToken: String): Response + + // This request doesn't return anything but sends OTP to user's email + @POST("api/v1/users/forgot-password") + suspend fun initializePasswordReset(@Body body: InitializeResetPasswordRequest): Response + + // This request doesn't return anything. 204 code for success + @POST("api/v1/users/forgot-password/confirm") + suspend fun confirmPasswordReset(@Body body: ConfirmResetPasswordRequest): Response } \ No newline at end of file diff --git a/app/src/main/java/by/alexandr7035/affinidi_id/data/LoginRepository.kt b/app/src/main/java/by/alexandr7035/affinidi_id/data/LoginRepository.kt new file mode 100644 index 00000000..1eeab060 --- /dev/null +++ b/app/src/main/java/by/alexandr7035/affinidi_id/data/LoginRepository.kt @@ -0,0 +1,17 @@ +package by.alexandr7035.affinidi_id.data + +import by.alexandr7035.affinidi_id.data.model.log_out.LogOutModel +import by.alexandr7035.affinidi_id.data.model.sign_in.SignInModel + +interface LoginRepository { + suspend fun signIn( + userName: String, + password: String, + ): SignInModel + + fun checkIfAuthorized(): Boolean + + fun saveUserName(userName: String) + + suspend fun logOut(): LogOutModel +} \ No newline at end of file diff --git a/app/src/main/java/by/alexandr7035/affinidi_id/data/AuthRepository.kt b/app/src/main/java/by/alexandr7035/affinidi_id/data/RegistrationRepository.kt similarity index 57% rename from app/src/main/java/by/alexandr7035/affinidi_id/data/AuthRepository.kt rename to app/src/main/java/by/alexandr7035/affinidi_id/data/RegistrationRepository.kt index 838e7135..ee808bb8 100644 --- a/app/src/main/java/by/alexandr7035/affinidi_id/data/AuthRepository.kt +++ b/app/src/main/java/by/alexandr7035/affinidi_id/data/RegistrationRepository.kt @@ -1,11 +1,9 @@ package by.alexandr7035.affinidi_id.data -import by.alexandr7035.affinidi_id.data.model.log_out.LogOutModel -import by.alexandr7035.affinidi_id.data.model.sign_in.SignInModel import by.alexandr7035.affinidi_id.data.model.sign_up.SignUpConfirmationModel import by.alexandr7035.affinidi_id.data.model.sign_up.SignUpModel -interface AuthRepository { +interface RegistrationRepository { suspend fun signUp( userName: String, password: String, @@ -16,14 +14,6 @@ interface AuthRepository { confirmationCode: String ): SignUpConfirmationModel - suspend fun signIn( - userName: String, - password: String, - ): SignInModel - - fun checkIfAuthorized(): Boolean - + // FIXME refactoring fun saveUserName(userName: String) - - suspend fun logOut(): LogOutModel } \ No newline at end of file diff --git a/app/src/main/java/by/alexandr7035/affinidi_id/data/ResetPasswordRepository.kt b/app/src/main/java/by/alexandr7035/affinidi_id/data/ResetPasswordRepository.kt new file mode 100644 index 00000000..2be2e311 --- /dev/null +++ b/app/src/main/java/by/alexandr7035/affinidi_id/data/ResetPasswordRepository.kt @@ -0,0 +1,10 @@ +package by.alexandr7035.affinidi_id.data + +import by.alexandr7035.affinidi_id.data.model.reset_password.ConfirmPasswordResetModel +import by.alexandr7035.affinidi_id.data.model.reset_password.InitializePasswordResetModel + +interface ResetPasswordRepository { + suspend fun initializePasswordReset(userName: String): InitializePasswordResetModel + + suspend fun confirmResetPassword(userName: String, newPassword: String, confirmationCode: String): ConfirmPasswordResetModel +} \ No newline at end of file diff --git a/app/src/main/java/by/alexandr7035/affinidi_id/data/helpers/validation/InputValidationHelper.kt b/app/src/main/java/by/alexandr7035/affinidi_id/data/helpers/validation/InputValidationHelper.kt new file mode 100644 index 00000000..d07afbde --- /dev/null +++ b/app/src/main/java/by/alexandr7035/affinidi_id/data/helpers/validation/InputValidationHelper.kt @@ -0,0 +1,9 @@ +package by.alexandr7035.affinidi_id.data.helpers.validation + +interface InputValidationHelper { + fun validateUserName(userName: String): InputValidationResult + + fun validatePassword(password: String): InputValidationResult + + fun validateConfirmationCode(confirmationCode: String): InputValidationResult +} \ No newline at end of file diff --git a/app/src/main/java/by/alexandr7035/affinidi_id/data/helpers/validation/InputValidationHelperImpl.kt b/app/src/main/java/by/alexandr7035/affinidi_id/data/helpers/validation/InputValidationHelperImpl.kt new file mode 100644 index 00000000..b4db48d3 --- /dev/null +++ b/app/src/main/java/by/alexandr7035/affinidi_id/data/helpers/validation/InputValidationHelperImpl.kt @@ -0,0 +1,31 @@ +package by.alexandr7035.affinidi_id.data.helpers.validation + +import android.util.Patterns + +class InputValidationHelperImpl(minPasswordLength: Int): InputValidationHelper { + + private val passwordPattern = "^(?=.*[A-Z])(?=.*[a-z])(?=.*\\d).{$minPasswordLength,}\$".toPattern() + + override fun validateUserName(userName: String): InputValidationResult { + return when { + userName.isBlank() -> InputValidationResult.EMPTY_FIELD + ! Patterns.EMAIL_ADDRESS.matcher(userName).matches() -> InputValidationResult.WRONG_FORMAT + else -> InputValidationResult.NO_ERRORS + } + } + + override fun validatePassword(password: String): InputValidationResult { + return when { + password.isBlank() -> InputValidationResult.EMPTY_FIELD + ! passwordPattern.matcher(password).matches() -> InputValidationResult.WRONG_FORMAT + else -> InputValidationResult.NO_ERRORS + } + } + + override fun validateConfirmationCode(confirmationCode: String): InputValidationResult { + return when { + confirmationCode.isBlank() -> InputValidationResult.EMPTY_FIELD + else -> InputValidationResult.NO_ERRORS + } + } +} \ No newline at end of file diff --git a/app/src/main/java/by/alexandr7035/affinidi_id/data/helpers/validation/InputValidationResult.kt b/app/src/main/java/by/alexandr7035/affinidi_id/data/helpers/validation/InputValidationResult.kt new file mode 100644 index 00000000..4051695c --- /dev/null +++ b/app/src/main/java/by/alexandr7035/affinidi_id/data/helpers/validation/InputValidationResult.kt @@ -0,0 +1,7 @@ +package by.alexandr7035.affinidi_id.data.helpers.validation + +enum class InputValidationResult { + EMPTY_FIELD, + WRONG_FORMAT, + NO_ERRORS +} \ No newline at end of file diff --git a/app/src/main/java/by/alexandr7035/affinidi_id/data/implementation/AuthRepositoryImpl.kt b/app/src/main/java/by/alexandr7035/affinidi_id/data/implementation/LoginRepositoryImpl.kt similarity index 56% rename from app/src/main/java/by/alexandr7035/affinidi_id/data/implementation/AuthRepositoryImpl.kt rename to app/src/main/java/by/alexandr7035/affinidi_id/data/implementation/LoginRepositoryImpl.kt index 17b48ec5..1b343efe 100644 --- a/app/src/main/java/by/alexandr7035/affinidi_id/data/implementation/AuthRepositoryImpl.kt +++ b/app/src/main/java/by/alexandr7035/affinidi_id/data/implementation/LoginRepositoryImpl.kt @@ -5,102 +5,19 @@ import by.alexandr7035.affinidi_id.core.ErrorType import by.alexandr7035.affinidi_id.core.extensions.debug import by.alexandr7035.affinidi_id.data.ApiService import by.alexandr7035.affinidi_id.data.AuthDataStorage -import by.alexandr7035.affinidi_id.data.AuthRepository +import by.alexandr7035.affinidi_id.data.LoginRepository import by.alexandr7035.affinidi_id.data.model.log_out.LogOutModel import by.alexandr7035.affinidi_id.data.model.sign_in.SignInModel import by.alexandr7035.affinidi_id.data.model.sign_in.SignInRequest import by.alexandr7035.affinidi_id.data.model.sign_in.SignInResponse -import by.alexandr7035.affinidi_id.data.model.sign_up.* import timber.log.Timber import java.lang.Exception import javax.inject.Inject -class AuthRepositoryImpl @Inject constructor( +class LoginRepositoryImpl @Inject constructor( private val apiService: ApiService, private val authDataStorage: AuthDataStorage -): AuthRepository { - override suspend fun signUp(userName: String, password: String): SignUpModel { - - try { - - val res = apiService.signUp( - SignUpRequest( - userName = userName, - password = password, - // Use with default params - signUpOptions = SignUpOptions(), - signUpMessageParams = SignUpMessageParams() - ) - ) - - return if (res.isSuccessful) { - // Get token for signup confirmation and return - SignUpModel.Success(res.body() as String) - } else { - Timber.debug("RES FAILED") - when (res.code()) { - 409 -> { - SignUpModel.Fail(ErrorType.USER_ALREADY_EXISTS) - } - else -> { - SignUpModel.Fail(ErrorType.UNKNOWN_ERROR) - } - } - } - } - // Handled in ErrorInterceptor - catch (appError: AppError) { - Timber.debug("RES FAILED ${appError.errorType.name}") - return SignUpModel.Fail(appError.errorType) - } - // Unknown exception - catch (e: Exception) { - e.printStackTrace() - return SignUpModel.Fail(ErrorType.UNKNOWN_ERROR) - } - } - - override suspend fun confirmSignUp(confirmationToken: String, confirmationCode: String): SignUpConfirmationModel { - try { - val res = apiService.confirmSignUp(ConfirmSignUpRequest( - token = confirmationToken, - confirmationCode = confirmationCode - )) - - if (res.isSuccessful) { - val data = res.body() as ConfirmSignUpResponse - - // TODO think if should do this through fragment - viewmodel - repo chain - saveAuthData(userDid = data.userDid, accessToken = data.accessToken) - - return SignUpConfirmationModel.Success( - accessToken = data.accessToken, - userDid = data.userDid - ) - } - else { - return when (res.code()) { - 400 -> { - SignUpConfirmationModel.Fail(ErrorType.WRONG_CONFIRMATION_CODE) - } - else -> { - SignUpConfirmationModel.Fail(ErrorType.UNKNOWN_ERROR) - } - } - } - } - // Handled in ErrorInterceptor - catch (appError: AppError) { - return SignUpConfirmationModel.Fail(appError.errorType) - } - // Unknown exception - catch (e: Exception) { - return SignUpConfirmationModel.Fail(ErrorType.UNKNOWN_ERROR) - } - - } - - +): LoginRepository { override suspend fun signIn(userName: String, password: String): SignInModel { try { @@ -130,7 +47,7 @@ class AuthRepositoryImpl @Inject constructor( } // Handled in ErrorInterceptor catch (appError: AppError) { - return SignInModel.Fail(appError.errorType) + return SignInModel.Fail(appError.errorType) } // Unknown exception catch (e: Exception) { @@ -146,11 +63,6 @@ class AuthRepositoryImpl @Inject constructor( authDataStorage.saveUserName(userName) } - private fun saveAuthData(userDid: String, accessToken: String) { - authDataStorage.saveDid(userDid) - authDataStorage.saveAccessToken(accessToken) - } - override suspend fun logOut(): LogOutModel { try { val res = apiService.logOut(authDataStorage.getAccessToken() ?: "") @@ -193,4 +105,9 @@ class AuthRepositoryImpl @Inject constructor( return LogOutModel.Fail(ErrorType.UNKNOWN_ERROR) } } + + private fun saveAuthData(userDid: String, accessToken: String) { + authDataStorage.saveDid(userDid) + authDataStorage.saveAccessToken(accessToken) + } } \ No newline at end of file diff --git a/app/src/main/java/by/alexandr7035/affinidi_id/data/implementation/RegistrationRepositoryImpl.kt b/app/src/main/java/by/alexandr7035/affinidi_id/data/implementation/RegistrationRepositoryImpl.kt new file mode 100644 index 00000000..6a7ae8cf --- /dev/null +++ b/app/src/main/java/by/alexandr7035/affinidi_id/data/implementation/RegistrationRepositoryImpl.kt @@ -0,0 +1,106 @@ +package by.alexandr7035.affinidi_id.data.implementation + +import by.alexandr7035.affinidi_id.core.AppError +import by.alexandr7035.affinidi_id.core.ErrorType +import by.alexandr7035.affinidi_id.core.extensions.debug +import by.alexandr7035.affinidi_id.data.ApiService +import by.alexandr7035.affinidi_id.data.AuthDataStorage +import by.alexandr7035.affinidi_id.data.RegistrationRepository +import by.alexandr7035.affinidi_id.data.model.sign_up.* +import timber.log.Timber +import javax.inject.Inject + +class RegistrationRepositoryImpl @Inject constructor( + private val apiService: ApiService, + private val authDataStorage: AuthDataStorage +): RegistrationRepository { + override suspend fun signUp(userName: String, password: String): SignUpModel { + + try { + + val res = apiService.signUp( + SignUpRequest( + userName = userName, + password = password, + // Use with default params + signUpOptions = SignUpOptions(), + signUpMessageParams = SignUpMessageParams() + ) + ) + + return if (res.isSuccessful) { + // Get token for signup confirmation and return + SignUpModel.Success(res.body() as String) + } else { + Timber.debug("RES FAILED") + when (res.code()) { + 409 -> { + SignUpModel.Fail(ErrorType.USER_ALREADY_EXISTS) + } + else -> { + SignUpModel.Fail(ErrorType.UNKNOWN_ERROR) + } + } + } + } + // Handled in ErrorInterceptor + catch (appError: AppError) { + Timber.debug("RES FAILED ${appError.errorType.name}") + return SignUpModel.Fail(appError.errorType) + } + // Unknown exception + catch (e: Exception) { + e.printStackTrace() + return SignUpModel.Fail(ErrorType.UNKNOWN_ERROR) + } + } + + override suspend fun confirmSignUp(confirmationToken: String, confirmationCode: String): SignUpConfirmationModel { + try { + val res = apiService.confirmSignUp(ConfirmSignUpRequest( + token = confirmationToken, + confirmationCode = confirmationCode + )) + + if (res.isSuccessful) { + val data = res.body() as ConfirmSignUpResponse + + // TODO think if should do this through fragment - viewmodel - repo chain + saveAuthData(userDid = data.userDid, accessToken = data.accessToken) + + return SignUpConfirmationModel.Success( + accessToken = data.accessToken, + userDid = data.userDid + ) + } + else { + return when (res.code()) { + 400 -> { + SignUpConfirmationModel.Fail(ErrorType.WRONG_CONFIRMATION_CODE) + } + else -> { + SignUpConfirmationModel.Fail(ErrorType.UNKNOWN_ERROR) + } + } + } + } + // Handled in ErrorInterceptor + catch (appError: AppError) { + return SignUpConfirmationModel.Fail(appError.errorType) + } + // Unknown exception + catch (e: Exception) { + return SignUpConfirmationModel.Fail(ErrorType.UNKNOWN_ERROR) + } + + } + + override fun saveUserName(userName: String) { + authDataStorage.saveUserName(userName) + } + + private fun saveAuthData(userDid: String, accessToken: String) { + authDataStorage.saveDid(userDid) + authDataStorage.saveAccessToken(accessToken) + } +} \ No newline at end of file diff --git a/app/src/main/java/by/alexandr7035/affinidi_id/data/implementation/ResetPasswordRepositoryImpl.kt b/app/src/main/java/by/alexandr7035/affinidi_id/data/implementation/ResetPasswordRepositoryImpl.kt new file mode 100644 index 00000000..1db4b09b --- /dev/null +++ b/app/src/main/java/by/alexandr7035/affinidi_id/data/implementation/ResetPasswordRepositoryImpl.kt @@ -0,0 +1,77 @@ +package by.alexandr7035.affinidi_id.data.implementation + +import by.alexandr7035.affinidi_id.core.AppError +import by.alexandr7035.affinidi_id.core.ErrorType +import by.alexandr7035.affinidi_id.core.extensions.debug +import by.alexandr7035.affinidi_id.data.ApiService +import by.alexandr7035.affinidi_id.data.ResetPasswordRepository +import by.alexandr7035.affinidi_id.data.model.reset_password.ConfirmPasswordResetModel +import by.alexandr7035.affinidi_id.data.model.reset_password.ConfirmResetPasswordRequest +import by.alexandr7035.affinidi_id.data.model.reset_password.InitializePasswordResetModel +import by.alexandr7035.affinidi_id.data.model.reset_password.InitializeResetPasswordRequest +import by.alexandr7035.affinidi_id.data.model.sign_up.SignUpModel +import timber.log.Timber +import java.lang.Exception +import javax.inject.Inject + +class ResetPasswordRepositoryImpl @Inject constructor(private val apiService: ApiService): ResetPasswordRepository{ + override suspend fun initializePasswordReset(userName: String): InitializePasswordResetModel { + try { + val res = apiService.initializePasswordReset(InitializeResetPasswordRequest(userName)) + + return if (res.isSuccessful) { + InitializePasswordResetModel.Success(userName) + } else { + when (res.code()) { + // FIXME wrong errortype for 400 + 400 -> InitializePasswordResetModel.Fail(ErrorType.WRONG_CONFIRMATION_CODE) + 404 -> InitializePasswordResetModel.Fail(ErrorType.USER_DOES_NOT_EXIST) + else -> InitializePasswordResetModel.Fail(ErrorType.UNKNOWN_ERROR) + } + } + } + // Handled in ErrorInterceptor + catch (appError: AppError) { + return InitializePasswordResetModel.Fail(appError.errorType) + } + // Unknown exception + catch (e: Exception) { + e.printStackTrace() + return InitializePasswordResetModel.Fail(ErrorType.UNKNOWN_ERROR) + } + } + + + override suspend fun confirmResetPassword(userName: String, newPassword: String, confirmationCode: String): ConfirmPasswordResetModel { + try { + val res = apiService.confirmPasswordReset(ConfirmResetPasswordRequest( + userName = userName, + newPassword = newPassword, + confirmationCode = confirmationCode + )) + + return if (res.isSuccessful) { + ConfirmPasswordResetModel.Success() + } else { + when (res.code()) { + 400 -> ConfirmPasswordResetModel.Fail(ErrorType.WRONG_CONFIRMATION_CODE) + // We can also get 404 error which means user doesn't exist. + // On code confirmation stage it's unintended behavior + // So we throw unknown error in this case to show it on UI + else -> ConfirmPasswordResetModel.Fail(ErrorType.UNKNOWN_ERROR) + // TODO handle "too many requests" here + } + } + } + // Handled in ErrorInterceptor + catch (appError: AppError) { + return ConfirmPasswordResetModel.Fail(appError.errorType) + } + // Unknown exception + catch (e: Exception) { + e.printStackTrace() + return ConfirmPasswordResetModel.Fail(ErrorType.UNKNOWN_ERROR) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/by/alexandr7035/affinidi_id/data/model/reset_password/ConfirmPasswordResetModel.kt b/app/src/main/java/by/alexandr7035/affinidi_id/data/model/reset_password/ConfirmPasswordResetModel.kt new file mode 100644 index 00000000..5ee715f9 --- /dev/null +++ b/app/src/main/java/by/alexandr7035/affinidi_id/data/model/reset_password/ConfirmPasswordResetModel.kt @@ -0,0 +1,9 @@ +package by.alexandr7035.affinidi_id.data.model.reset_password + +import by.alexandr7035.affinidi_id.core.ErrorType + +abstract class ConfirmPasswordResetModel { + class Success(): ConfirmPasswordResetModel() + + class Fail(val errorType: ErrorType): ConfirmPasswordResetModel() +} \ No newline at end of file diff --git a/app/src/main/java/by/alexandr7035/affinidi_id/data/model/reset_password/ConfirmResetPasswordRequest.kt b/app/src/main/java/by/alexandr7035/affinidi_id/data/model/reset_password/ConfirmResetPasswordRequest.kt new file mode 100644 index 00000000..433fe9a8 --- /dev/null +++ b/app/src/main/java/by/alexandr7035/affinidi_id/data/model/reset_password/ConfirmResetPasswordRequest.kt @@ -0,0 +1,12 @@ +package by.alexandr7035.affinidi_id.data.model.reset_password + +import com.google.gson.annotations.SerializedName + +data class ConfirmResetPasswordRequest( + @SerializedName("username") + val userName: String, + @SerializedName("otp") + val confirmationCode: String, + @SerializedName("newPassword") + val newPassword: String +) \ No newline at end of file diff --git a/app/src/main/java/by/alexandr7035/affinidi_id/data/model/reset_password/InitializePasswordResetModel.kt b/app/src/main/java/by/alexandr7035/affinidi_id/data/model/reset_password/InitializePasswordResetModel.kt new file mode 100644 index 00000000..93fa4cd8 --- /dev/null +++ b/app/src/main/java/by/alexandr7035/affinidi_id/data/model/reset_password/InitializePasswordResetModel.kt @@ -0,0 +1,9 @@ +package by.alexandr7035.affinidi_id.data.model.reset_password + +import by.alexandr7035.affinidi_id.core.ErrorType + +abstract class InitializePasswordResetModel { + data class Success(val userName: String): InitializePasswordResetModel() + + data class Fail(val errorType: ErrorType): InitializePasswordResetModel() +} \ No newline at end of file diff --git a/app/src/main/java/by/alexandr7035/affinidi_id/data/model/reset_password/InitializeResetPasswordRequest.kt b/app/src/main/java/by/alexandr7035/affinidi_id/data/model/reset_password/InitializeResetPasswordRequest.kt new file mode 100644 index 00000000..884ed886 --- /dev/null +++ b/app/src/main/java/by/alexandr7035/affinidi_id/data/model/reset_password/InitializeResetPasswordRequest.kt @@ -0,0 +1,8 @@ +package by.alexandr7035.affinidi_id.data.model.reset_password + +import com.google.gson.annotations.SerializedName + +data class InitializeResetPasswordRequest( + @SerializedName("username") + val userName: String +) \ No newline at end of file diff --git a/app/src/main/java/by/alexandr7035/affinidi_id/data/model/sign_up/SignUpResponse.kt b/app/src/main/java/by/alexandr7035/affinidi_id/data/model/sign_up/SignUpResponse.kt deleted file mode 100644 index 8ec6b677..00000000 --- a/app/src/main/java/by/alexandr7035/affinidi_id/data/model/sign_up/SignUpResponse.kt +++ /dev/null @@ -1,4 +0,0 @@ -package by.alexandr7035.affinidi_id.data.model.sign_up - -class SignUpResponse { -} \ No newline at end of file diff --git a/app/src/main/java/by/alexandr7035/affinidi_id/di/AppModule.kt b/app/src/main/java/by/alexandr7035/affinidi_id/di/AppModule.kt index d82e4720..315f80de 100644 --- a/app/src/main/java/by/alexandr7035/affinidi_id/di/AppModule.kt +++ b/app/src/main/java/by/alexandr7035/affinidi_id/di/AppModule.kt @@ -4,10 +4,9 @@ import android.app.Application import by.alexandr7035.affinidi_id.core.network.AuthInterceptor import by.alexandr7035.affinidi_id.core.network.ErrorInterceptor import by.alexandr7035.affinidi_id.data.* -import by.alexandr7035.affinidi_id.data.implementation.AuthDataStorageImpl -import by.alexandr7035.affinidi_id.data.implementation.AuthRepositoryImpl -import by.alexandr7035.affinidi_id.data.implementation.DicebearAvatarsHelperImpl -import by.alexandr7035.affinidi_id.data.implementation.ProfileRepositoryImpl +import by.alexandr7035.affinidi_id.data.helpers.validation.InputValidationHelper +import by.alexandr7035.affinidi_id.data.helpers.validation.InputValidationHelperImpl +import by.alexandr7035.affinidi_id.data.implementation.* import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -54,8 +53,14 @@ object AppModule { @Provides @Singleton - fun provieAuthRepository(apiService: ApiService, authDataStorage: AuthDataStorage): AuthRepository { - return AuthRepositoryImpl(apiService, authDataStorage) + fun provideRegistrationRepository(apiService: ApiService, authDataStorage: AuthDataStorage): RegistrationRepository { + return RegistrationRepositoryImpl(apiService, authDataStorage) + } + + @Provides + @Singleton + fun provideLoginRepository(apiService: ApiService, authDataStorage: AuthDataStorage): LoginRepository { + return LoginRepositoryImpl(apiService, authDataStorage) } @Provides @@ -64,6 +69,12 @@ object AppModule { return ProfileRepositoryImpl(authDataStorage, avatarsHelper, apiService) } + @Provides + @Singleton + fun provideResetPasswordRepository(apiService: ApiService): ResetPasswordRepository { + return ResetPasswordRepositoryImpl(apiService) + } + @Provides @Singleton @@ -75,4 +86,9 @@ object AppModule { fun provideDicebearAvatarsHelper(): DicebearAvatarsHelper { return DicebearAvatarsHelperImpl() } + + @Provides + fun provideInputValidationHelper(): InputValidationHelper { + return InputValidationHelperImpl(minPasswordLength = 8) + } } \ No newline at end of file diff --git a/app/src/main/java/by/alexandr7035/affinidi_id/presentation/MainViewModel.kt b/app/src/main/java/by/alexandr7035/affinidi_id/presentation/MainViewModel.kt index f7745a26..e07e6ef0 100644 --- a/app/src/main/java/by/alexandr7035/affinidi_id/presentation/MainViewModel.kt +++ b/app/src/main/java/by/alexandr7035/affinidi_id/presentation/MainViewModel.kt @@ -1,13 +1,13 @@ package by.alexandr7035.affinidi_id.presentation import androidx.lifecycle.ViewModel -import by.alexandr7035.affinidi_id.data.AuthRepository +import by.alexandr7035.affinidi_id.data.LoginRepository import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject @HiltViewModel -class MainViewModel @Inject constructor(private val authRepository: AuthRepository): ViewModel() { +class MainViewModel @Inject constructor(private val repository: LoginRepository): ViewModel() { fun checkIfAuthorized(): Boolean { - return authRepository.checkIfAuthorized() + return repository.checkIfAuthorized() } } \ No newline at end of file diff --git a/app/src/main/java/by/alexandr7035/affinidi_id/presentation/helpers/InputValidationResult.kt b/app/src/main/java/by/alexandr7035/affinidi_id/presentation/helpers/InputValidationResult.kt deleted file mode 100644 index 13993dd3..00000000 --- a/app/src/main/java/by/alexandr7035/affinidi_id/presentation/helpers/InputValidationResult.kt +++ /dev/null @@ -1,7 +0,0 @@ -package by.alexandr7035.affinidi_id.presentation.helpers - -enum class InputValidationResult { - PASSWORD_IS_EMPTY, - PASSWORD_WRONG_FORMAT, - NO_ERRORS -} \ No newline at end of file diff --git a/app/src/main/java/by/alexandr7035/affinidi_id/presentation/helpers/InputValidator.kt b/app/src/main/java/by/alexandr7035/affinidi_id/presentation/helpers/InputValidator.kt deleted file mode 100644 index e0953f92..00000000 --- a/app/src/main/java/by/alexandr7035/affinidi_id/presentation/helpers/InputValidator.kt +++ /dev/null @@ -1,5 +0,0 @@ -package by.alexandr7035.affinidi_id.presentation.helpers - -interface InputValidator { - fun validatePassword(password: String): InputValidationResult -} \ No newline at end of file diff --git a/app/src/main/java/by/alexandr7035/affinidi_id/presentation/helpers/InputValidatorImpl.kt b/app/src/main/java/by/alexandr7035/affinidi_id/presentation/helpers/InputValidatorImpl.kt deleted file mode 100644 index dba6221c..00000000 --- a/app/src/main/java/by/alexandr7035/affinidi_id/presentation/helpers/InputValidatorImpl.kt +++ /dev/null @@ -1,12 +0,0 @@ -package by.alexandr7035.affinidi_id.presentation.helpers - -class InputValidatorImpl: InputValidator { - override fun validatePassword(password: String): InputValidationResult { - // TODO format - return if (password.isBlank()) { - InputValidationResult.PASSWORD_IS_EMPTY - } else { - InputValidationResult.NO_ERRORS - } - } -} \ No newline at end of file diff --git a/app/src/main/java/by/alexandr7035/affinidi_id/presentation/login/LoginFragment.kt b/app/src/main/java/by/alexandr7035/affinidi_id/presentation/login/LoginFragment.kt index e7ede401..66967932 100644 --- a/app/src/main/java/by/alexandr7035/affinidi_id/presentation/login/LoginFragment.kt +++ b/app/src/main/java/by/alexandr7035/affinidi_id/presentation/login/LoginFragment.kt @@ -6,7 +6,6 @@ import android.text.method.LinkMovementMethod import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.Toast import androidx.core.content.ContextCompat import androidx.core.view.isVisible import androidx.core.widget.doOnTextChanged @@ -19,11 +18,9 @@ import by.alexandr7035.affinidi_id.core.extensions.clearError import by.alexandr7035.affinidi_id.core.extensions.getClickableSpannable import by.alexandr7035.affinidi_id.core.extensions.navigateSafe import by.alexandr7035.affinidi_id.core.extensions.showErrorDialog +import by.alexandr7035.affinidi_id.data.helpers.validation.InputValidationResult import by.alexandr7035.affinidi_id.data.model.sign_in.SignInModel import by.alexandr7035.affinidi_id.databinding.FragmentLoginBinding -import by.alexandr7035.affinidi_id.presentation.helpers.InputValidationResult -import by.alexandr7035.affinidi_id.presentation.helpers.InputValidatorImpl -import by.alexandr7035.affinidi_id.presentation.registration.RegistrationFragmentDirections import by.kirich1409.viewbindingdelegate.viewBinding import dagger.hilt.android.AndroidEntryPoint @@ -112,7 +109,6 @@ class LoginFragment : Fragment() { } }) -// binding.goToSignUpBtn.text = val goToSignUpText = getString(R.string.go_to_sign_up) val spannable = goToSignUpText.getClickableSpannable( @@ -129,36 +125,47 @@ class LoginFragment : Fragment() { movementMethod = LinkMovementMethod.getInstance() highlightColor = Color.TRANSPARENT } + + binding.forgotPasswordBtn.setOnClickListener { + findNavController() + .navigateSafe(LoginFragmentDirections.actionLoginFragmentToResetPasswordGraph()) + } } private fun chekIfFormIsValid(): Boolean { - // FIXME move from here to viewmodel (?) - val validator = InputValidatorImpl() + var formIsValid = true + + val userName = binding.userNameEditText.text.toString() + when (viewModel.validateUserName(userName)) { + InputValidationResult.EMPTY_FIELD -> { + binding.userNameField.error = getString(R.string.error_empty_field) + formIsValid = false + } - var isValid = true + InputValidationResult.WRONG_FORMAT -> { + binding.userNameField.error = getString(R.string.error_invalid_user_name) + formIsValid = false + } - // FIXME use validator - if (binding.userNameEditText.text!!.isEmpty()) { - binding.userNameField.error = getString(R.string.error_empty_field) - isValid = false + InputValidationResult.NO_ERRORS -> {} } val password = binding.passwordEditText.text.toString() - - when (validator.validatePassword(password)) { - InputValidationResult.PASSWORD_IS_EMPTY -> { + when (viewModel.validatePassword(password)) { + InputValidationResult.EMPTY_FIELD -> { binding.passwordField.error = getString(R.string.error_empty_field) - isValid = false + formIsValid = false } - InputValidationResult.PASSWORD_WRONG_FORMAT -> { + InputValidationResult.WRONG_FORMAT -> { binding.passwordField.error = getString(R.string.error_wromg_password_format) - isValid = false + formIsValid = false } + + InputValidationResult.NO_ERRORS -> {} } - return isValid + return formIsValid } - } \ No newline at end of file diff --git a/app/src/main/java/by/alexandr7035/affinidi_id/presentation/login/LoginViewModel.kt b/app/src/main/java/by/alexandr7035/affinidi_id/presentation/login/LoginViewModel.kt index e03461c8..947a29a8 100644 --- a/app/src/main/java/by/alexandr7035/affinidi_id/presentation/login/LoginViewModel.kt +++ b/app/src/main/java/by/alexandr7035/affinidi_id/presentation/login/LoginViewModel.kt @@ -1,10 +1,11 @@ package by.alexandr7035.affinidi_id.presentation.login -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import by.alexandr7035.affinidi_id.core.livedata.SingleLiveEvent -import by.alexandr7035.affinidi_id.data.AuthRepository +import by.alexandr7035.affinidi_id.data.LoginRepository +import by.alexandr7035.affinidi_id.data.helpers.validation.InputValidationHelper +import by.alexandr7035.affinidi_id.data.helpers.validation.InputValidationResult import by.alexandr7035.affinidi_id.data.model.sign_in.SignInModel import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers @@ -13,7 +14,10 @@ import kotlinx.coroutines.withContext import javax.inject.Inject @HiltViewModel -class LoginViewModel @Inject constructor(private val repository: AuthRepository): ViewModel() { +class LoginViewModel @Inject constructor( + private val repository: LoginRepository, + private val inputValidationHelper: InputValidationHelper +) : ViewModel() { val signInLiveData = SingleLiveEvent() fun signIn(userName: String, password: String) { @@ -29,4 +33,12 @@ class LoginViewModel @Inject constructor(private val repository: AuthRepository) fun saveUserName(userName: String) { repository.saveUserName(userName) } + + fun validateUserName(userName: String): InputValidationResult { + return inputValidationHelper.validateUserName(userName) + } + + fun validatePassword(password: String): InputValidationResult { + return inputValidationHelper.validatePassword(password) + } } \ No newline at end of file diff --git a/app/src/main/java/by/alexandr7035/affinidi_id/presentation/logout/LogoutViewModel.kt b/app/src/main/java/by/alexandr7035/affinidi_id/presentation/logout/LogoutViewModel.kt index e31be5e1..0410d465 100644 --- a/app/src/main/java/by/alexandr7035/affinidi_id/presentation/logout/LogoutViewModel.kt +++ b/app/src/main/java/by/alexandr7035/affinidi_id/presentation/logout/LogoutViewModel.kt @@ -3,7 +3,7 @@ package by.alexandr7035.affinidi_id.presentation.logout import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import by.alexandr7035.affinidi_id.data.AuthRepository +import by.alexandr7035.affinidi_id.data.LoginRepository import by.alexandr7035.affinidi_id.data.model.log_out.LogOutModel import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers @@ -12,7 +12,7 @@ import kotlinx.coroutines.withContext import javax.inject.Inject @HiltViewModel -class LogoutViewModel @Inject constructor(private val repository: AuthRepository): ViewModel() { +class LogoutViewModel @Inject constructor(private val repository: LoginRepository): ViewModel() { var logOutLiveData = MutableLiveData() fun logOut() { diff --git a/app/src/main/java/by/alexandr7035/affinidi_id/presentation/registration/RegistrationFragment.kt b/app/src/main/java/by/alexandr7035/affinidi_id/presentation/registration/RegistrationFragment.kt index eacf5242..b6dc59bb 100644 --- a/app/src/main/java/by/alexandr7035/affinidi_id/presentation/registration/RegistrationFragment.kt +++ b/app/src/main/java/by/alexandr7035/affinidi_id/presentation/registration/RegistrationFragment.kt @@ -18,11 +18,10 @@ import by.alexandr7035.affinidi_id.core.extensions.clearError import by.alexandr7035.affinidi_id.core.extensions.getClickableSpannable import by.alexandr7035.affinidi_id.core.extensions.navigateSafe import by.alexandr7035.affinidi_id.core.extensions.showErrorDialog +import by.alexandr7035.affinidi_id.data.helpers.validation.InputValidationResult import by.alexandr7035.affinidi_id.data.model.sign_up.SignUpConfirmationModel import by.alexandr7035.affinidi_id.data.model.sign_up.SignUpModel import by.alexandr7035.affinidi_id.databinding.FragmentRegistrationBinding -import by.alexandr7035.affinidi_id.presentation.helpers.InputValidationResult -import by.alexandr7035.affinidi_id.presentation.helpers.InputValidatorImpl import by.kirich1409.viewbindingdelegate.viewBinding import dagger.hilt.android.AndroidEntryPoint @@ -44,8 +43,6 @@ class RegistrationFragment : Fragment() { findNavController().navigateUp() } - val formValidator = InputValidatorImpl() - binding.signUpBtn.setOnClickListener { if (chekIfFormIsValid()) { binding.loginProgressView.isVisible = true @@ -95,6 +92,7 @@ class RegistrationFragment : Fragment() { }) + // TODO dry binding.userNameEditText.doOnTextChanged { text, start, before, count -> if (text?.isNotEmpty() == true) { binding.userNameField.clearError() @@ -173,16 +171,21 @@ class RegistrationFragment : Fragment() { } private fun chekIfFormIsValid(): Boolean { + var formIsValid = true - // FIXME move from here to viewmodel (?) - val validator = InputValidatorImpl() + val userName = binding.userNameEditText.text.toString() + when (viewModel.validateUserName(userName)) { + InputValidationResult.EMPTY_FIELD -> { + binding.userNameField.error = getString(R.string.error_empty_field) + formIsValid = false + } - var isValid = true + InputValidationResult.WRONG_FORMAT -> { + binding.userNameField.error = getString(R.string.error_invalid_user_name) + formIsValid = false + } - // FIXME use validator - if (binding.userNameEditText.text!!.isEmpty()) { - binding.userNameField.error = getString(R.string.error_empty_field) - isValid = false + InputValidationResult.NO_ERRORS -> {} } val password = binding.passwordSetEditText.text.toString() @@ -191,35 +194,39 @@ class RegistrationFragment : Fragment() { if (password != passwordConfirmation) { binding.passwordConfirmField.error = getString(R.string.error_passwords_not_match) binding.passwordSetField.error = getString(R.string.error_passwords_not_match) - isValid = false + formIsValid = false } - when (validator.validatePassword(password)) { - InputValidationResult.PASSWORD_IS_EMPTY -> { + when (viewModel.validatePassword(password)) { + InputValidationResult.EMPTY_FIELD -> { binding.passwordSetField.error = getString(R.string.error_empty_field) - isValid = false + formIsValid = false } - InputValidationResult.PASSWORD_WRONG_FORMAT -> { + InputValidationResult.WRONG_FORMAT -> { binding.passwordSetField.error = getString(R.string.error_wromg_password_format) - isValid = false + formIsValid = false } + + InputValidationResult.NO_ERRORS -> {} } - when (validator.validatePassword(passwordConfirmation)) { - InputValidationResult.PASSWORD_IS_EMPTY -> { + when (viewModel.validatePassword(passwordConfirmation)) { + InputValidationResult.EMPTY_FIELD -> { binding.passwordConfirmField.error = getString(R.string.error_empty_field) - isValid = false + formIsValid = false } - InputValidationResult.PASSWORD_WRONG_FORMAT -> { + InputValidationResult.WRONG_FORMAT -> { binding.passwordConfirmField.error = getString(R.string.error_wromg_password_format) - isValid = false + formIsValid = false } + + InputValidationResult.NO_ERRORS -> {} } - return isValid + return formIsValid } } \ No newline at end of file diff --git a/app/src/main/java/by/alexandr7035/affinidi_id/presentation/registration/RegistrationViewModel.kt b/app/src/main/java/by/alexandr7035/affinidi_id/presentation/registration/RegistrationViewModel.kt index 2ed0359b..16debfe0 100644 --- a/app/src/main/java/by/alexandr7035/affinidi_id/presentation/registration/RegistrationViewModel.kt +++ b/app/src/main/java/by/alexandr7035/affinidi_id/presentation/registration/RegistrationViewModel.kt @@ -1,10 +1,11 @@ package by.alexandr7035.affinidi_id.presentation.registration -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import by.alexandr7035.affinidi_id.core.livedata.SingleLiveEvent -import by.alexandr7035.affinidi_id.data.AuthRepository +import by.alexandr7035.affinidi_id.data.RegistrationRepository +import by.alexandr7035.affinidi_id.data.helpers.validation.InputValidationHelper +import by.alexandr7035.affinidi_id.data.helpers.validation.InputValidationResult import by.alexandr7035.affinidi_id.data.model.sign_up.SignUpConfirmationModel import by.alexandr7035.affinidi_id.data.model.sign_up.SignUpModel import dagger.hilt.android.lifecycle.HiltViewModel @@ -14,7 +15,10 @@ import kotlinx.coroutines.withContext import javax.inject.Inject @HiltViewModel -class RegistrationViewModel @Inject constructor(private val repository: AuthRepository): ViewModel() { +class RegistrationViewModel @Inject constructor( + private val repository: RegistrationRepository, + private val inputValidationHelper: InputValidationHelper +): ViewModel() { val signUpLiveData = SingleLiveEvent() val signUpConfirmationLiveData = SingleLiveEvent() @@ -42,4 +46,13 @@ class RegistrationViewModel @Inject constructor(private val repository: AuthRepo fun saveUserName(userName: String) { repository.saveUserName(userName) } + + fun validateUserName(userName: String): InputValidationResult { + return inputValidationHelper.validateUserName(userName) + } + + fun validatePassword(password: String): InputValidationResult { + return inputValidationHelper.validatePassword(password) + } + } \ No newline at end of file diff --git a/app/src/main/java/by/alexandr7035/affinidi_id/presentation/reset_password/ResetPasswordConfirmationFragment.kt b/app/src/main/java/by/alexandr7035/affinidi_id/presentation/reset_password/ResetPasswordConfirmationFragment.kt new file mode 100644 index 00000000..7fb31a48 --- /dev/null +++ b/app/src/main/java/by/alexandr7035/affinidi_id/presentation/reset_password/ResetPasswordConfirmationFragment.kt @@ -0,0 +1,112 @@ +package by.alexandr7035.affinidi_id.presentation.reset_password + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.view.isVisible +import androidx.core.widget.doOnTextChanged +import androidx.fragment.app.Fragment +import androidx.navigation.fragment.findNavController +import androidx.navigation.fragment.navArgs +import androidx.navigation.navGraphViewModels +import by.alexandr7035.affinidi_id.R +import by.alexandr7035.affinidi_id.core.ErrorType +import by.alexandr7035.affinidi_id.core.extensions.clearError +import by.alexandr7035.affinidi_id.core.extensions.navigateSafe +import by.alexandr7035.affinidi_id.core.extensions.showErrorDialog +import by.alexandr7035.affinidi_id.core.extensions.showToast +import by.alexandr7035.affinidi_id.data.helpers.validation.InputValidationResult +import by.alexandr7035.affinidi_id.data.model.reset_password.ConfirmPasswordResetModel +import by.alexandr7035.affinidi_id.databinding.FragmentResetPasswordConfirmationBinding +import by.kirich1409.viewbindingdelegate.viewBinding +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class ResetPasswordConfirmationFragment : Fragment() { + + private val binding by viewBinding(FragmentResetPasswordConfirmationBinding::bind) + private val viewModel by navGraphViewModels(R.id.resetPasswordGraph) { defaultViewModelProviderFactory } + private val safeArgs by navArgs() + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + // Inflate the layout for this fragment + return inflater.inflate(R.layout.fragment_reset_password_confirmation, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.confirmationCodeEditText.doOnTextChanged { text, start, before, count -> + if (text?.isNotEmpty() == true) { + binding.confirmationCodeField.clearError() + } + } + + binding.confirmBtn.setOnClickListener { + if (chekIfFormIsValid()) { + binding.progressView.isVisible = true + + viewModel.confirmPasswordReset( + username = safeArgs.userName, + newPassword = safeArgs.newPassword, + confirmationCode = binding.confirmationCodeEditText.text.toString() + ) + } + } + + + viewModel.confirmPasswordResetLiveData.observe(viewLifecycleOwner, { result -> + binding.progressView.isVisible = false + + when (result) { + is ConfirmPasswordResetModel.Success -> { + findNavController().navigateSafe(ResetPasswordConfirmationFragmentDirections + .actionGlobalLoginFragment()) + + // TODO show success dialog + requireContext().showToast(getString(R.string.password_changed_successfully)) + } + + is ConfirmPasswordResetModel.Fail -> { + when (result.errorType) { + ErrorType.WRONG_CONFIRMATION_CODE -> { + binding.confirmationCodeField.error = getString(R.string.error_wrong_confirmation_code) + } + + ErrorType.FAILED_CONNECTION -> { + showErrorDialog( + getString(R.string.error_failed_connection_title), + getString(R.string.error_failed_connection) + ) + } + + else -> { + showErrorDialog( + getString(R.string.error_unknown_title), + getString(R.string.error_unknown) + ) + } + } + } + } + }) + } + + private fun chekIfFormIsValid(): Boolean { + var formIsValid = true + + val code = binding.confirmationCodeEditText.text.toString() + when (viewModel.validateConfirmationCode(code)) { + InputValidationResult.EMPTY_FIELD -> { + binding.confirmationCodeField.error = getString(R.string.error_empty_field) + formIsValid = false + } + + InputValidationResult.NO_ERRORS -> { + } + } + + return formIsValid + } +} \ No newline at end of file diff --git a/app/src/main/java/by/alexandr7035/affinidi_id/presentation/reset_password/ResetPasswordSetPasswordFragment.kt b/app/src/main/java/by/alexandr7035/affinidi_id/presentation/reset_password/ResetPasswordSetPasswordFragment.kt new file mode 100644 index 00000000..9373361e --- /dev/null +++ b/app/src/main/java/by/alexandr7035/affinidi_id/presentation/reset_password/ResetPasswordSetPasswordFragment.kt @@ -0,0 +1,106 @@ +package by.alexandr7035.affinidi_id.presentation.reset_password + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.widget.doOnTextChanged +import androidx.fragment.app.Fragment +import androidx.navigation.fragment.findNavController +import androidx.navigation.fragment.navArgs +import androidx.navigation.navGraphViewModels +import by.alexandr7035.affinidi_id.R +import by.alexandr7035.affinidi_id.core.extensions.clearError +import by.alexandr7035.affinidi_id.core.extensions.navigateSafe +import by.alexandr7035.affinidi_id.data.helpers.validation.InputValidationResult +import by.alexandr7035.affinidi_id.databinding.FragmentResetPasswordSetPasswordBinding +import by.kirich1409.viewbindingdelegate.viewBinding +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class ResetPasswordSetPasswordFragment : Fragment() { + private val binding by viewBinding(FragmentResetPasswordSetPasswordBinding::bind) + private val viewModel by navGraphViewModels(R.id.resetPasswordGraph) { defaultViewModelProviderFactory } + private val safeArgs by navArgs() + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + // Inflate the layout for this fragment + return inflater.inflate(R.layout.fragment_reset_password_set_password, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.passwordSetEditText.doOnTextChanged { text, start, before, count -> + if (text?.isNotEmpty() == true) { + binding.passwordSetField.clearError() + } + } + + binding.passwordConfirmEditText.doOnTextChanged { text, start, before, count -> + if (text?.isNotEmpty() == true) { + binding.passwordConfirmField.clearError() + } + } + + binding.continueBtn.setOnClickListener { + if (chekIfFormIsValid()) { + findNavController().navigateSafe( + ResetPasswordSetPasswordFragmentDirections + .actionResetPasswordSetPasswordFragmentToResetPasswordConfirmationFragment( + safeArgs.userName, + binding.passwordSetEditText.text.toString() + ) + ) + } + } + } + + + private fun chekIfFormIsValid(): Boolean { + var formIsValid = true + + val password = binding.passwordSetEditText.text.toString() + val passwordConfirmation = binding.passwordConfirmEditText.text.toString() + + if (password != passwordConfirmation) { + binding.passwordConfirmField.error = getString(R.string.error_passwords_not_match) + binding.passwordSetField.error = getString(R.string.error_passwords_not_match) + formIsValid = false + } + + + when (viewModel.validatePassword(password)) { + InputValidationResult.EMPTY_FIELD -> { + binding.passwordSetField.error = getString(R.string.error_empty_field) + formIsValid = false + } + + InputValidationResult.WRONG_FORMAT -> { + binding.passwordSetField.error = getString(R.string.error_wromg_password_format) + formIsValid = false + } + + InputValidationResult.NO_ERRORS -> { + } + } + + when (viewModel.validatePassword(passwordConfirmation)) { + InputValidationResult.EMPTY_FIELD -> { + binding.passwordConfirmField.error = getString(R.string.error_empty_field) + formIsValid = false + } + + InputValidationResult.WRONG_FORMAT -> { + binding.passwordConfirmField.error = getString(R.string.error_wromg_password_format) + formIsValid = false + } + + InputValidationResult.NO_ERRORS -> { + } + } + + return formIsValid + } + +} \ No newline at end of file diff --git a/app/src/main/java/by/alexandr7035/affinidi_id/presentation/reset_password/ResetPasswordSetUsernameFragment.kt b/app/src/main/java/by/alexandr7035/affinidi_id/presentation/reset_password/ResetPasswordSetUsernameFragment.kt new file mode 100644 index 00000000..b8621541 --- /dev/null +++ b/app/src/main/java/by/alexandr7035/affinidi_id/presentation/reset_password/ResetPasswordSetUsernameFragment.kt @@ -0,0 +1,113 @@ +package by.alexandr7035.affinidi_id.presentation.reset_password + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.view.isVisible +import androidx.core.widget.doOnTextChanged +import androidx.fragment.app.Fragment +import androidx.navigation.fragment.findNavController +import androidx.navigation.navGraphViewModels +import by.alexandr7035.affinidi_id.R +import by.alexandr7035.affinidi_id.core.ErrorType +import by.alexandr7035.affinidi_id.core.extensions.clearError +import by.alexandr7035.affinidi_id.core.extensions.navigateSafe +import by.alexandr7035.affinidi_id.core.extensions.showErrorDialog +import by.alexandr7035.affinidi_id.core.extensions.showToast +import by.alexandr7035.affinidi_id.data.helpers.validation.InputValidationResult +import by.alexandr7035.affinidi_id.data.model.reset_password.InitializePasswordResetModel +import by.alexandr7035.affinidi_id.databinding.FragmentResetPasswordSetUsernameBinding +import by.kirich1409.viewbindingdelegate.viewBinding +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class ResetPasswordSetUsernameFragment : Fragment() { + + private val binding by viewBinding(FragmentResetPasswordSetUsernameBinding::bind) + private val viewModel by navGraphViewModels(R.id.resetPasswordGraph) { defaultViewModelProviderFactory } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + // Inflate the layout for this fragment + return inflater.inflate(R.layout.fragment_reset_password_set_username, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.toolbar.setNavigationOnClickListener { + findNavController().navigateUp() + } + + binding.userNameEditText.doOnTextChanged { text, start, before, count -> + if (text?.isNotEmpty() == true) { + binding.userNameField.clearError() + } + } + + binding.continueBtn.setOnClickListener { + if (chekIfFormIsValid()) { + binding.progressView.isVisible = true + + val userName = binding.userNameEditText.text.toString() + viewModel.initializePasswordReset(userName) + } + } + + viewModel.initializePasswordResetLiveData.observe(viewLifecycleOwner, { result -> + binding.progressView.isVisible = false + + when (result) { + is InitializePasswordResetModel.Success -> { + findNavController().navigateSafe(ResetPasswordSetUsernameFragmentDirections + .actionResetPasswordSetUsernameFragmentToResetPasswordSetPasswordFragment(result.userName)) + } + + is InitializePasswordResetModel.Fail -> { + when (result.errorType) { + ErrorType.USER_DOES_NOT_EXIST -> { + binding.userNameField.error = getString(R.string.error_user_not_found) + } + ErrorType.FAILED_CONNECTION -> { + showErrorDialog( + getString(R.string.error_failed_connection_title), + getString(R.string.error_failed_connection) + ) + } + // Including unknown error + else -> { + showErrorDialog( + getString(R.string.error_unknown_title), + getString(R.string.error_unknown) + ) + } + } + } + } + }) + } + + private fun chekIfFormIsValid(): Boolean { + + var formIsValid = true + + val userName = binding.userNameEditText.text.toString() + when (viewModel.validateUserName(userName)) { + InputValidationResult.EMPTY_FIELD -> { + binding.userNameField.error = getString(R.string.error_empty_field) + formIsValid = false + } + + InputValidationResult.WRONG_FORMAT -> { + binding.userNameField.error = getString(R.string.error_invalid_user_name) + formIsValid = false + } + + InputValidationResult.NO_ERRORS -> { + } + } + + return formIsValid + } + +} \ No newline at end of file diff --git a/app/src/main/java/by/alexandr7035/affinidi_id/presentation/reset_password/ResetPasswordViewModel.kt b/app/src/main/java/by/alexandr7035/affinidi_id/presentation/reset_password/ResetPasswordViewModel.kt new file mode 100644 index 00000000..59f29c23 --- /dev/null +++ b/app/src/main/java/by/alexandr7035/affinidi_id/presentation/reset_password/ResetPasswordViewModel.kt @@ -0,0 +1,53 @@ +package by.alexandr7035.affinidi_id.presentation.reset_password + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import by.alexandr7035.affinidi_id.core.livedata.SingleLiveEvent +import by.alexandr7035.affinidi_id.data.ResetPasswordRepository +import by.alexandr7035.affinidi_id.data.helpers.validation.InputValidationHelper +import by.alexandr7035.affinidi_id.data.helpers.validation.InputValidationResult +import by.alexandr7035.affinidi_id.data.model.reset_password.ConfirmPasswordResetModel +import by.alexandr7035.affinidi_id.data.model.reset_password.InitializePasswordResetModel +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import javax.inject.Inject + +@HiltViewModel +class ResetPasswordViewModel @Inject constructor(private val resetPasswordRepository: ResetPasswordRepository, private val inputValidationHelper: InputValidationHelper): ViewModel() { + val initializePasswordResetLiveData = SingleLiveEvent() + val confirmPasswordResetLiveData = SingleLiveEvent() + + fun initializePasswordReset(username: String) { + viewModelScope.launch(Dispatchers.IO) { + val result = resetPasswordRepository.initializePasswordReset(username) + + withContext(Dispatchers.Main) { + initializePasswordResetLiveData.value = result + } + } + } + + fun confirmPasswordReset(username: String, newPassword: String, confirmationCode: String) { + viewModelScope.launch(Dispatchers.IO) { + val result = resetPasswordRepository.confirmResetPassword(username, newPassword, confirmationCode) + + withContext(Dispatchers.Main) { + confirmPasswordResetLiveData.value = result + } + } + } + + fun validateUserName(username: String): InputValidationResult { + return inputValidationHelper.validateUserName(username) + } + + fun validatePassword(password: String): InputValidationResult { + return inputValidationHelper.validatePassword(password) + } + + fun validateConfirmationCode(code: String): InputValidationResult { + return inputValidationHelper.validateConfirmationCode(code) + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_login.xml b/app/src/main/res/layout/fragment_login.xml index fdb8c8b9..cf5e3ec1 100644 --- a/app/src/main/res/layout/fragment_login.xml +++ b/app/src/main/res/layout/fragment_login.xml @@ -72,11 +72,21 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/sign_in" - android:layout_marginTop="8dp" - app:layout_constraintTop_toBottomOf="@id/passwordField" + app:layout_constraintTop_toBottomOf="@id/forgotPasswordBtn" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent"/> + + + + + + + + + + + + + + + + + + +