diff --git a/app/src/main/java/app/cashadvisor/authorization/data/NetworkToResetPasswordExceptionMapper.kt b/app/src/main/java/app/cashadvisor/authorization/data/NetworkToResetPasswordExceptionMapper.kt new file mode 100644 index 00000000..12fc4f0f --- /dev/null +++ b/app/src/main/java/app/cashadvisor/authorization/data/NetworkToResetPasswordExceptionMapper.kt @@ -0,0 +1,128 @@ +package app.cashadvisor.authorization.data + +import app.cashadvisor.authorization.data.models.response.customError.ErrorWrongConfirmationCodeResponse +import app.cashadvisor.common.data.models.ErrorResponse +import app.cashadvisor.common.utill.exceptions.LoginException +import app.cashadvisor.common.utill.exceptions.NetworkException +import app.cashadvisor.common.utill.exceptions.ResetPasswordException +import javax.inject.Inject +import kotlinx.serialization.json.Json + +class NetworkToResetPasswordExceptionMapper @Inject constructor( + private val json: Json +) { + fun handleConfirmResetPasswordWithCode(exception: NetworkException): ResetPasswordException { + return when (exception) { + is NetworkException.BadRequest -> { + val errorResponse = handleErrorResponse(exception.errorBody) + ResetPasswordException.ConfirmResetPasswordWithCode.BadRequestInvalidCodeOrMissingContentTypeHeader( + message = errorResponse.message, + statusCode = errorResponse.statusCode + ) + } + + is NetworkException.Unauthorized -> { + val errorResponse = + handleErrorResponse(exception.errorBody) + ResetPasswordException.ConfirmResetPasswordWithCode.UnauthorizedWrongConfirmationCode( + remainingAttempts = errorResponse.remainingAttempts, + lockDuration = errorResponse.lockDurationNanoseconds, + message = errorResponse.error, + statusCode = errorResponse.statusCode + ) + } + + is NetworkException.InternalServerError -> { + val errorResponse = handleErrorResponse(exception.errorBody) + ResetPasswordException.ConfirmResetPasswordWithCode + .InternalServerErrorFailedToConfirmResetPassword( + message = errorResponse.message, + statusCode = errorResponse.statusCode + ) + } + + else -> handleCommonException(exception) + } + + } + + fun handleConfirmEmailToResetPassword(exception: NetworkException): ResetPasswordException { + return when (exception) { + is NetworkException.BadRequest -> { + val errorResponse = handleErrorResponse(exception.errorBody) + ResetPasswordException.ConfirmEmailToResetPassword.BadRequestInvalidInputOrContentType( + message = errorResponse.message, + statusCode = errorResponse.statusCode + ) + } + + is NetworkException.InternalServerError -> { + val errorResponse = handleErrorResponse(exception.errorBody) + ResetPasswordException.ConfirmEmailToResetPassword.InternalServerErrorFailedToGenerateTokenOrSendEmail( + message = errorResponse.message, + statusCode = errorResponse.statusCode + ) + } + + else -> handleCommonException(exception) + } + + } + + fun handleSaveNewPassword(exception: NetworkException): ResetPasswordException { + return when (exception) { + is NetworkException.BadRequest -> { + val errorResponse = handleErrorResponse(exception.errorBody) + ResetPasswordException.SaveNewPassword + .BadRequestInvalidPasswordOrMissingContentTypeHeader( + message = errorResponse.message, + statusCode = errorResponse.statusCode + ) + } + + is NetworkException.Unauthorized -> { + val errorResponse = + handleErrorResponse(exception.errorBody) + ResetPasswordException.SaveNewPassword.UnauthorizedInvalidTokenOrMissingContentTypeHeader( + message = errorResponse.error, + statusCode = errorResponse.statusCode + ) + } + + is NetworkException.InternalServerError -> { + val errorResponse = handleErrorResponse(exception.errorBody) + ResetPasswordException.SaveNewPassword.InternalServerErrorFailedToResetPassword( + message = errorResponse.message, + statusCode = errorResponse.statusCode + ) + } + + else -> handleCommonException(exception) + } + } + + private fun handleCommonException(exception: NetworkException): ResetPasswordException { + return when (exception) { + is NetworkException.NoInternetConnection -> { + ResetPasswordException.NoConnection(exception.errorBody) + } + + is NetworkException.Undefined -> { + ResetPasswordException.Undefined() + } + + else -> { + ResetPasswordException.Undefined() + } + } + } + + private inline fun handleErrorResponse(errorMessage: String): T { + try { + return json.decodeFromString(errorMessage) + + } catch (e: Exception) { + throw LoginException.Undefined() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/app/cashadvisor/authorization/data/ResetDataMapper.kt b/app/src/main/java/app/cashadvisor/authorization/data/ResetDataMapper.kt new file mode 100644 index 00000000..20456500 --- /dev/null +++ b/app/src/main/java/app/cashadvisor/authorization/data/ResetDataMapper.kt @@ -0,0 +1,64 @@ +package app.cashadvisor.authorization.data + +import app.cashadvisor.authorization.data.models.ConfirmResetPasswordWithCodeInputDto +import app.cashadvisor.authorization.data.models.ResetPasswordInputDto +import app.cashadvisor.authorization.data.models.ConfirmResetPasswordWithCodeOutputDto +import app.cashadvisor.authorization.data.models.ResetPasswordOutputDto +import app.cashadvisor.authorization.data.models.SaveNewPasswordInputDto +import app.cashadvisor.authorization.data.models.SaveNewPasswordOutputDto +import app.cashadvisor.authorization.data.models.request.ResetPasswordRequest +import app.cashadvisor.authorization.data.models.request.ResetPasswordWithCodeRequest +import app.cashadvisor.authorization.data.models.request.SaveNewPasswordRequest +import app.cashadvisor.authorization.data.models.response.ConfirmResetPasswordResponse +import app.cashadvisor.authorization.data.models.response.ResetPasswordResponse +import app.cashadvisor.authorization.data.models.response.SaveNewPasswordResponse +import javax.inject.Inject + +class ResetDataMapper @Inject constructor() { + + fun toResetPasswordRequest(inputDto: ResetPasswordInputDto): ResetPasswordRequest { + return ResetPasswordRequest( + email = inputDto.email + + ) + } + + fun toConfirmResetPasswordWithCodeOutputDto(response: ConfirmResetPasswordResponse): + ConfirmResetPasswordWithCodeOutputDto { + return ConfirmResetPasswordWithCodeOutputDto( + message = response.message, + statusCode = response.statusCode + ) + } + + fun toResetPasswordWithCodeRequest(inputDto: ConfirmResetPasswordWithCodeInputDto): + ResetPasswordWithCodeRequest { + return ResetPasswordWithCodeRequest( + code = inputDto.code, + token = inputDto.token + ) + } + + fun toResetPasswordOutputDto(response: ResetPasswordResponse): ResetPasswordOutputDto { + return ResetPasswordOutputDto( + message = response.message, + token = response.token, + statusCode = response.statusCode + ) + } + + fun toSaveNewPasswordRequest(inputDto: SaveNewPasswordInputDto): SaveNewPasswordRequest { + return SaveNewPasswordRequest( + email = inputDto.email, + password = inputDto.password, + resetToken = inputDto.resetToken + ) + } + + fun toSaveNewPasswordOutputDto(response: SaveNewPasswordResponse): SaveNewPasswordOutputDto { + return SaveNewPasswordOutputDto( + message = response.message, + statusCode = response.statusCode + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/app/cashadvisor/authorization/data/ResetDomainMapper.kt b/app/src/main/java/app/cashadvisor/authorization/data/ResetDomainMapper.kt new file mode 100644 index 00000000..7d48704c --- /dev/null +++ b/app/src/main/java/app/cashadvisor/authorization/data/ResetDomainMapper.kt @@ -0,0 +1,66 @@ +package app.cashadvisor.authorization.data + +import app.cashadvisor.authorization.data.models.ConfirmResetPasswordWithCodeInputDto +import app.cashadvisor.authorization.data.models.ResetPasswordInputDto +import app.cashadvisor.authorization.data.models.ConfirmResetPasswordWithCodeOutputDto +import app.cashadvisor.authorization.data.models.ResetPasswordOutputDto +import app.cashadvisor.authorization.data.models.SaveNewPasswordInputDto +import app.cashadvisor.authorization.data.models.SaveNewPasswordOutputDto +import app.cashadvisor.authorization.domain.models.ConfirmCode +import app.cashadvisor.authorization.domain.models.ConfirmResetPasswordWithCode +import app.cashadvisor.authorization.domain.models.Email +import app.cashadvisor.authorization.domain.models.Password +import app.cashadvisor.authorization.domain.models.ResetPasswordData +import app.cashadvisor.authorization.domain.models.SaveNewPasswordData +import javax.inject.Inject + +class ResetDomainMapper @Inject constructor() { + + fun toResetPasswordInputDto(email: Email): ResetPasswordInputDto { + return ResetPasswordInputDto( + email = email.value + ) + } + + fun toConfirmResetPasswordByEmailWithCodeInputDto( + code: ConfirmCode, + token: String + ): ConfirmResetPasswordWithCodeInputDto { + return ConfirmResetPasswordWithCodeInputDto( + code = code.value, + token = token + ) + } + + fun toSaveNewPasswordInputDto( + email: Email, + password: Password, + resetToken: String + ): SaveNewPasswordInputDto { + return SaveNewPasswordInputDto( + email = email.value, + password = password.value, + resetToken = resetToken + ) + } + + fun toConfirmResetPasswordWithCode(data: ConfirmResetPasswordWithCodeOutputDto): ConfirmResetPasswordWithCode { + return ConfirmResetPasswordWithCode( + message = data.message, + ) + } + + fun toResetPasswordData(data: ResetPasswordOutputDto): ResetPasswordData { + return ResetPasswordData( + message = data.message, + statusCode = data.statusCode + ) + } + + fun toSaveNewPasswordData(data: SaveNewPasswordOutputDto): SaveNewPasswordData { + return SaveNewPasswordData( + message = data.message, + statusCode = data.statusCode + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/app/cashadvisor/authorization/data/api/ResetPasswordApiService.kt b/app/src/main/java/app/cashadvisor/authorization/data/api/ResetPasswordApiService.kt new file mode 100644 index 00000000..ff288d46 --- /dev/null +++ b/app/src/main/java/app/cashadvisor/authorization/data/api/ResetPasswordApiService.kt @@ -0,0 +1,27 @@ +package app.cashadvisor.authorization.data.api + +import app.cashadvisor.authorization.data.models.request.ResetPasswordWithCodeRequest +import app.cashadvisor.authorization.data.models.request.ResetPasswordRequest +import app.cashadvisor.authorization.data.models.request.SaveNewPasswordRequest +import app.cashadvisor.authorization.data.models.response.ConfirmResetPasswordResponse +import app.cashadvisor.authorization.data.models.response.ResetPasswordResponse +import app.cashadvisor.authorization.data.models.response.SaveNewPasswordResponse +import retrofit2.http.Body +import retrofit2.http.Headers +import retrofit2.http.POST + +interface ResetPasswordApiService { + + @Headers("Content-Type: application/json") + @POST("auth/login/reset/password") + suspend fun resetPassword(@Body passwordResetRequest: ResetPasswordRequest ):ResetPasswordResponse + + @Headers("Content-Type: application/json") + @POST("auth/login/reset/password/confirm") + suspend fun resetPasswordConfirm(@Body resetPasswordRequest: ResetPasswordWithCodeRequest):ConfirmResetPasswordResponse + + @Headers("Content-Type: application/json") + @POST("auth/login/reset/password") + suspend fun saveNewPassword(@Body saveNewPasswordRequest: SaveNewPasswordRequest):SaveNewPasswordResponse + +} \ No newline at end of file diff --git a/app/src/main/java/app/cashadvisor/authorization/data/api/ResetPasswordRemoteDataSource.kt b/app/src/main/java/app/cashadvisor/authorization/data/api/ResetPasswordRemoteDataSource.kt new file mode 100644 index 00000000..07681658 --- /dev/null +++ b/app/src/main/java/app/cashadvisor/authorization/data/api/ResetPasswordRemoteDataSource.kt @@ -0,0 +1,14 @@ +package app.cashadvisor.authorization.data.api + +import app.cashadvisor.authorization.data.models.ConfirmResetPasswordWithCodeInputDto +import app.cashadvisor.authorization.data.models.ConfirmResetPasswordWithCodeOutputDto +import app.cashadvisor.authorization.data.models.ResetPasswordInputDto +import app.cashadvisor.authorization.data.models.ResetPasswordOutputDto +import app.cashadvisor.authorization.data.models.SaveNewPasswordInputDto +import app.cashadvisor.authorization.data.models.SaveNewPasswordOutputDto + +interface ResetPasswordRemoteDataSource { + suspend fun confirmEmail(inputDto: ResetPasswordInputDto):ResetPasswordOutputDto + suspend fun confirmResetPasswordByEmailWithCode(inputDto: ConfirmResetPasswordWithCodeInputDto):ConfirmResetPasswordWithCodeOutputDto + suspend fun saveNewPassword(inputDto:SaveNewPasswordInputDto):SaveNewPasswordOutputDto +} \ No newline at end of file diff --git a/app/src/main/java/app/cashadvisor/authorization/data/impl/ResetPasswordRemoteDataSourceImpl.kt b/app/src/main/java/app/cashadvisor/authorization/data/impl/ResetPasswordRemoteDataSourceImpl.kt new file mode 100644 index 00000000..818a5947 --- /dev/null +++ b/app/src/main/java/app/cashadvisor/authorization/data/impl/ResetPasswordRemoteDataSourceImpl.kt @@ -0,0 +1,58 @@ +package app.cashadvisor.authorization.data.impl + +import app.cashadvisor.authorization.data.NetworkToResetPasswordExceptionMapper +import app.cashadvisor.authorization.data.ResetDataMapper +import app.cashadvisor.authorization.data.api.ResetPasswordApiService +import app.cashadvisor.authorization.data.api.ResetPasswordRemoteDataSource +import app.cashadvisor.authorization.data.models.ConfirmResetPasswordWithCodeInputDto +import app.cashadvisor.authorization.data.models.ResetPasswordInputDto +import app.cashadvisor.authorization.data.models.ConfirmResetPasswordWithCodeOutputDto +import app.cashadvisor.authorization.data.models.ResetPasswordOutputDto +import app.cashadvisor.authorization.data.models.SaveNewPasswordInputDto +import app.cashadvisor.authorization.data.models.SaveNewPasswordOutputDto +import app.cashadvisor.common.utill.exceptions.NetworkException +import app.cashadvisor.common.utill.extensions.logDebugMessage +import javax.inject.Inject + +class ResetPasswordRemoteDataSourceImpl @Inject constructor( + private val resetDataMapper: ResetDataMapper, + private val resetPasswordApiService: ResetPasswordApiService, + private val networkToResetPasswordExceptionMapper: + NetworkToResetPasswordExceptionMapper +) : ResetPasswordRemoteDataSource { + + override suspend fun confirmEmail(inputDto: ResetPasswordInputDto): ResetPasswordOutputDto { + return try { + val response = resetPasswordApiService.resetPassword( + passwordResetRequest = resetDataMapper.toResetPasswordRequest(inputDto) + ) + logDebugMessage(resetDataMapper.toResetPasswordRequest(inputDto).toString()) + resetDataMapper.toResetPasswordOutputDto(response) + } catch (exception: NetworkException) { + throw networkToResetPasswordExceptionMapper.handleConfirmEmailToResetPassword(exception) + } + } + + override suspend fun confirmResetPasswordByEmailWithCode(inputDto: ConfirmResetPasswordWithCodeInputDto): + ConfirmResetPasswordWithCodeOutputDto { + return try { + val response = resetPasswordApiService.resetPasswordConfirm( + resetPasswordRequest = resetDataMapper.toResetPasswordWithCodeRequest(inputDto) + ) + resetDataMapper.toConfirmResetPasswordWithCodeOutputDto(response) + } catch (exception: NetworkException) { + throw networkToResetPasswordExceptionMapper.handleConfirmResetPasswordWithCode(exception) + } + } + + override suspend fun saveNewPassword(inputDto: SaveNewPasswordInputDto): SaveNewPasswordOutputDto { + return try { + val response = resetPasswordApiService.saveNewPassword( + saveNewPasswordRequest = resetDataMapper.toSaveNewPasswordRequest(inputDto) + ) + resetDataMapper.toSaveNewPasswordOutputDto(response) + } catch (exception: NetworkException) { + throw networkToResetPasswordExceptionMapper.handleSaveNewPassword(exception) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/app/cashadvisor/authorization/data/impl/ResetPasswordRepositoryImpl.kt b/app/src/main/java/app/cashadvisor/authorization/data/impl/ResetPasswordRepositoryImpl.kt new file mode 100644 index 00000000..513dcb7f --- /dev/null +++ b/app/src/main/java/app/cashadvisor/authorization/data/impl/ResetPasswordRepositoryImpl.kt @@ -0,0 +1,192 @@ +package app.cashadvisor.authorization.data.impl + +import app.cashadvisor.authorization.data.ResetDomainMapper +import app.cashadvisor.authorization.data.api.ResetPasswordRemoteDataSource +import app.cashadvisor.authorization.di.ResetPasswordExceptionMapper +import app.cashadvisor.authorization.domain.api.ResetPasswordRepository +import app.cashadvisor.authorization.domain.models.ConfirmCode +import app.cashadvisor.authorization.domain.models.ConfirmResetPasswordWithCode +import app.cashadvisor.authorization.domain.models.Email +import app.cashadvisor.authorization.domain.models.Password +import app.cashadvisor.authorization.domain.models.ResetPasswordData +import app.cashadvisor.authorization.domain.models.ResetPasswordState +import app.cashadvisor.authorization.domain.models.SaveNewPasswordData +import app.cashadvisor.common.domain.BaseExceptionToErrorMapper +import app.cashadvisor.common.domain.Resource +import app.cashadvisor.common.domain.model.ErrorEntity +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update +import javax.inject.Inject + +class ResetPasswordRepositoryImpl @Inject constructor( + private val resetPasswordRemoteDataSource: ResetPasswordRemoteDataSource, + @ResetPasswordExceptionMapper private val exceptionToErrorMapper: BaseExceptionToErrorMapper, + private val resetDomainMapper: ResetDomainMapper + +) : ResetPasswordRepository { + private val _state: MutableStateFlow = + MutableStateFlow(ResetPasswordState()) + + private val currentState: ResetPasswordState + get() = _state.value + + override suspend fun confirmEmailForPasswordReset(email: Email): Resource { + return try { + val data = resetPasswordRemoteDataSource.confirmEmail( + inputDto = resetDomainMapper.toResetPasswordInputDto(email) + ) + + _state.update { + it.copy(state = ResetPasswordState.State.InProcess(codeToken = data.token)) + } + return when (data.statusCode) { + SUCCESS -> Resource.Success( + data = resetDomainMapper.toResetPasswordData(data) + ) + + BAD_REQUEST -> Resource.Error( + ErrorEntity.ConfirmEmailToResetPassword.InvalidInput(INVALID_INPUT) + ) + + INTERNAL_SERVER_ERROR -> Resource.Error( + ErrorEntity.ConfirmEmailToResetPassword.FailedToGenerateTokenOrSendEmail( + FAILED_TO_GENERATE + ) + ) + + else -> Resource.Error(ErrorEntity.UnknownError()) + } + } catch (exception: Exception) { + _state.update { it.copy(state = ResetPasswordState.State.Initial) } + Resource.Error( + exceptionToErrorMapper.handleException(exception) + ) + } + } + + override suspend fun resetPasswordConfirmWithCode(code: ConfirmCode): + Resource { + return try { + val token: String + when (val state = currentState.state) { + is ResetPasswordState.State.InProcess -> { + token = state.codeToken + } + + else -> { + return Resource.Error( + ErrorEntity.ConfirmResetPasswordByEmailWithCode.InvalidInput( + WRONG_STATE_ERROR + ) + ) + } + } + val data = resetPasswordRemoteDataSource.confirmResetPasswordByEmailWithCode( + inputDto = resetDomainMapper.toConfirmResetPasswordByEmailWithCodeInputDto( + code, + token + ) + ) + return when (data.statusCode) { + SUCCESS -> Resource.Success( + data = resetDomainMapper.toConfirmResetPasswordWithCode(data) + ) + + BAD_REQUEST -> Resource.Error( + ErrorEntity.ConfirmResetPasswordByEmailWithCode.InvalidInput(INVALID_INPUT) + ) + + UNAUTHORIZED -> Resource.Error( + ErrorEntity.ConfirmResetPasswordByEmailWithCode.WrongConfirmationCode( + WRONG_CONFIRMATION_CODE + ) + ) + + INTERNAL_SERVER_ERROR -> Resource.Error( + ErrorEntity.ConfirmResetPasswordByEmailWithCode.FailedToConfirmPasswordReset( + FAILED_TO_CONFIRM + ) + ) + + else -> Resource.Error(ErrorEntity.UnknownError()) + } + + + } catch (exception: Exception) { + Resource.Error( + exceptionToErrorMapper.handleException(exception) + ) + } + } + + override suspend fun saveNewPassword( + email: Email, + password: Password + ): Resource { + return try { + val token: String + when (val state = currentState.state) { + is ResetPasswordState.State.InProcess -> { + token = state.codeToken + } + + else -> { + return Resource.Error( + ErrorEntity.ConfirmResetPasswordByEmailWithCode.InvalidInput( + WRONG_STATE_ERROR + ) + ) + } + } + val data = resetPasswordRemoteDataSource.saveNewPassword( + resetDomainMapper.toSaveNewPasswordInputDto( + email, + password, + token + ) + ) + _state.update { it.copy(state = ResetPasswordState.State.Initial) } + return when (data.statusCode) { + SUCCESS -> Resource.Success( + data = resetDomainMapper.toSaveNewPasswordData(data) + ) + + BAD_REQUEST -> Resource.Error( + ErrorEntity.SaveNewPassword.InvalidInput(INVALID_INPUT) + ) + + UNAUTHORIZED -> Resource.Error( + ErrorEntity.SaveNewPassword.InvalidToken(INVALID_TOKEN) + ) + + INTERNAL_SERVER_ERROR -> Resource.Error( + ErrorEntity.SaveNewPassword.FailedToResetPassword(FAILED_TO_RESET_PASSWORD) + ) + + else -> Resource.Error(ErrorEntity.UnknownError()) + } + + } catch (exception: Exception) { + Resource.Error( + exceptionToErrorMapper.handleException(exception) + ) + } + } + + + + companion object { + const val WRONG_STATE_ERROR = "ResetPassword is not in progress" + const val WRONG_CONFIRMATION_CODE = "Wrong confirmation code" + const val INVALID_INPUT = "Invalid input or content type" + const val INVALID_TOKEN = "Invalid or expired reset token" + const val FAILED_TO_CONFIRM = "Failed to confirm password reset" + const val FAILED_TO_GENERATE = "Failed to generate token or send email" + const val FAILED_TO_RESET_PASSWORD = "Failed to reset password" + const val SUCCESS = 200 + const val BAD_REQUEST = 400 + const val UNAUTHORIZED = 401 + const val INTERNAL_SERVER_ERROR = 500 + + } +} \ No newline at end of file diff --git a/app/src/main/java/app/cashadvisor/authorization/data/models/ConfirmResetPasswordWithCodeInputDto.kt b/app/src/main/java/app/cashadvisor/authorization/data/models/ConfirmResetPasswordWithCodeInputDto.kt new file mode 100644 index 00000000..af485b79 --- /dev/null +++ b/app/src/main/java/app/cashadvisor/authorization/data/models/ConfirmResetPasswordWithCodeInputDto.kt @@ -0,0 +1,6 @@ +package app.cashadvisor.authorization.data.models + +data class ConfirmResetPasswordWithCodeInputDto( + val code:String, + val token:String +) diff --git a/app/src/main/java/app/cashadvisor/authorization/data/models/ConfirmResetPasswordWithCodeOutputDto.kt b/app/src/main/java/app/cashadvisor/authorization/data/models/ConfirmResetPasswordWithCodeOutputDto.kt new file mode 100644 index 00000000..04322724 --- /dev/null +++ b/app/src/main/java/app/cashadvisor/authorization/data/models/ConfirmResetPasswordWithCodeOutputDto.kt @@ -0,0 +1,7 @@ +package app.cashadvisor.authorization.data.models + + +data class ConfirmResetPasswordWithCodeOutputDto( + val message:String? = null, + val statusCode: Int +) diff --git a/app/src/main/java/app/cashadvisor/authorization/data/models/ResetPasswordInputDto.kt b/app/src/main/java/app/cashadvisor/authorization/data/models/ResetPasswordInputDto.kt new file mode 100644 index 00000000..f90d8e6f --- /dev/null +++ b/app/src/main/java/app/cashadvisor/authorization/data/models/ResetPasswordInputDto.kt @@ -0,0 +1,6 @@ +package app.cashadvisor.authorization.data.models + + +data class ResetPasswordInputDto( + val email:String +) diff --git a/app/src/main/java/app/cashadvisor/authorization/data/models/ResetPasswordOutputDto.kt b/app/src/main/java/app/cashadvisor/authorization/data/models/ResetPasswordOutputDto.kt new file mode 100644 index 00000000..0b16c695 --- /dev/null +++ b/app/src/main/java/app/cashadvisor/authorization/data/models/ResetPasswordOutputDto.kt @@ -0,0 +1,8 @@ +package app.cashadvisor.authorization.data.models + + +data class ResetPasswordOutputDto( + val message:String, + val token:String, + val statusCode: Int +) diff --git a/app/src/main/java/app/cashadvisor/authorization/data/models/SaveNewPasswordInputDto.kt b/app/src/main/java/app/cashadvisor/authorization/data/models/SaveNewPasswordInputDto.kt new file mode 100644 index 00000000..b7f2ee85 --- /dev/null +++ b/app/src/main/java/app/cashadvisor/authorization/data/models/SaveNewPasswordInputDto.kt @@ -0,0 +1,7 @@ +package app.cashadvisor.authorization.data.models + +data class SaveNewPasswordInputDto( + val email:String, + val password:String, + val resetToken:String +) \ No newline at end of file diff --git a/app/src/main/java/app/cashadvisor/authorization/data/models/SaveNewPasswordOutputDto.kt b/app/src/main/java/app/cashadvisor/authorization/data/models/SaveNewPasswordOutputDto.kt new file mode 100644 index 00000000..58a71afb --- /dev/null +++ b/app/src/main/java/app/cashadvisor/authorization/data/models/SaveNewPasswordOutputDto.kt @@ -0,0 +1,6 @@ +package app.cashadvisor.authorization.data.models + +data class SaveNewPasswordOutputDto( + val message:String, + val statusCode: Int +) diff --git a/app/src/main/java/app/cashadvisor/authorization/data/models/request/ConfirmLoginByEmailRequest.kt b/app/src/main/java/app/cashadvisor/authorization/data/models/request/ConfirmLoginByEmailRequest.kt index 716c37db..dc44bf0d 100644 --- a/app/src/main/java/app/cashadvisor/authorization/data/models/request/ConfirmLoginByEmailRequest.kt +++ b/app/src/main/java/app/cashadvisor/authorization/data/models/request/ConfirmLoginByEmailRequest.kt @@ -2,6 +2,7 @@ package app.cashadvisor.authorization.data.models.request import kotlinx.serialization.Serializable + @Serializable data class ConfirmLoginByEmailRequest( val email:String, diff --git a/app/src/main/java/app/cashadvisor/authorization/data/models/request/ResetPasswordRequest.kt b/app/src/main/java/app/cashadvisor/authorization/data/models/request/ResetPasswordRequest.kt new file mode 100644 index 00000000..6ef19ca1 --- /dev/null +++ b/app/src/main/java/app/cashadvisor/authorization/data/models/request/ResetPasswordRequest.kt @@ -0,0 +1,8 @@ +package app.cashadvisor.authorization.data.models.request + +import kotlinx.serialization.Serializable + +@Serializable +data class ResetPasswordRequest( + val email:String +) diff --git a/app/src/main/java/app/cashadvisor/authorization/data/models/request/ResetPasswordWithCodeRequest.kt b/app/src/main/java/app/cashadvisor/authorization/data/models/request/ResetPasswordWithCodeRequest.kt new file mode 100644 index 00000000..5580ba78 --- /dev/null +++ b/app/src/main/java/app/cashadvisor/authorization/data/models/request/ResetPasswordWithCodeRequest.kt @@ -0,0 +1,9 @@ +package app.cashadvisor.authorization.data.models.request + +import kotlinx.serialization.Serializable + +@Serializable +data class ResetPasswordWithCodeRequest( + val code: String, + val token: String +) diff --git a/app/src/main/java/app/cashadvisor/authorization/data/models/request/SaveNewPasswordRequest.kt b/app/src/main/java/app/cashadvisor/authorization/data/models/request/SaveNewPasswordRequest.kt new file mode 100644 index 00000000..23b9f25e --- /dev/null +++ b/app/src/main/java/app/cashadvisor/authorization/data/models/request/SaveNewPasswordRequest.kt @@ -0,0 +1,11 @@ +package app.cashadvisor.authorization.data.models.request + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class SaveNewPasswordRequest( + val email:String, + val password:String, + @SerialName("reset_token") val resetToken:String +) \ No newline at end of file diff --git a/app/src/main/java/app/cashadvisor/authorization/data/models/response/ConfirmResetPasswordResponse.kt b/app/src/main/java/app/cashadvisor/authorization/data/models/response/ConfirmResetPasswordResponse.kt new file mode 100644 index 00000000..e1f832b8 --- /dev/null +++ b/app/src/main/java/app/cashadvisor/authorization/data/models/response/ConfirmResetPasswordResponse.kt @@ -0,0 +1,10 @@ +package app.cashadvisor.authorization.data.models.response + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ConfirmResetPasswordResponse( + val message: String? = null, + @SerialName("status_code") val statusCode: Int = 0 +) diff --git a/app/src/main/java/app/cashadvisor/authorization/data/models/response/ResetPasswordResponse.kt b/app/src/main/java/app/cashadvisor/authorization/data/models/response/ResetPasswordResponse.kt new file mode 100644 index 00000000..10adeab3 --- /dev/null +++ b/app/src/main/java/app/cashadvisor/authorization/data/models/response/ResetPasswordResponse.kt @@ -0,0 +1,10 @@ +package app.cashadvisor.authorization.data.models.response + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ResetPasswordResponse( + val message:String, + val token:String, + @SerialName("status_code") val statusCode: Int = 0) diff --git a/app/src/main/java/app/cashadvisor/authorization/data/models/response/SaveNewPasswordResponse.kt b/app/src/main/java/app/cashadvisor/authorization/data/models/response/SaveNewPasswordResponse.kt new file mode 100644 index 00000000..31eba0d2 --- /dev/null +++ b/app/src/main/java/app/cashadvisor/authorization/data/models/response/SaveNewPasswordResponse.kt @@ -0,0 +1,11 @@ +package app.cashadvisor.authorization.data.models.response + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class SaveNewPasswordResponse( + val message:String, + @SerialName("status_code") val statusCode: Int = 0 + +) diff --git a/app/src/main/java/app/cashadvisor/authorization/di/AuthenticationDataModule.kt b/app/src/main/java/app/cashadvisor/authorization/di/AuthenticationDataModule.kt index 3353be28..41e9a2ed 100644 --- a/app/src/main/java/app/cashadvisor/authorization/di/AuthenticationDataModule.kt +++ b/app/src/main/java/app/cashadvisor/authorization/di/AuthenticationDataModule.kt @@ -2,14 +2,19 @@ package app.cashadvisor.authorization.di import app.cashadvisor.authorization.data.api.LoginRemoteDataSource import app.cashadvisor.authorization.data.api.RegisterRemoteDataSource +import app.cashadvisor.authorization.data.api.ResetPasswordRemoteDataSource import app.cashadvisor.authorization.data.impl.LoginRemoteDataSourceImpl import app.cashadvisor.authorization.data.impl.LoginRepositoryImpl import app.cashadvisor.authorization.data.impl.RegisterRemoteDataSourceImpl import app.cashadvisor.authorization.data.impl.RegisterRepositoryImpl +import app.cashadvisor.authorization.data.impl.ResetPasswordRemoteDataSourceImpl +import app.cashadvisor.authorization.data.impl.ResetPasswordRepositoryImpl import app.cashadvisor.authorization.domain.LoginExceptionToErrorMapper import app.cashadvisor.authorization.domain.RegisterExceptionToErrorMapper +import app.cashadvisor.authorization.domain.ResetPasswordExceptionToErrorMapper import app.cashadvisor.authorization.domain.api.LoginRepository import app.cashadvisor.authorization.domain.api.RegisterRepository +import app.cashadvisor.authorization.domain.api.ResetPasswordRepository import app.cashadvisor.common.domain.BaseExceptionToErrorMapper import dagger.Binds import dagger.Module @@ -26,6 +31,10 @@ annotation class RegisterExceptionMapper @Retention(AnnotationRetention.BINARY) annotation class LoginExceptionMapper +@Qualifier +@Retention(AnnotationRetention.BINARY) +annotation class ResetPasswordExceptionMapper + @Module @InstallIn(SingletonComponent::class) interface LoginAndRegisterDataModule { @@ -66,4 +75,25 @@ interface LoginAndRegisterDataModule { impl: LoginExceptionToErrorMapper ): BaseExceptionToErrorMapper +} +@Module +@InstallIn(SingletonComponent::class) +interface ResetPasswordDataModule{ + @Singleton + @Binds + fun bindResetPasswordDataSource( + impl: ResetPasswordRemoteDataSourceImpl + ):ResetPasswordRemoteDataSource + + @Singleton + @Binds + fun bindResetPasswordRepository( + impl:ResetPasswordRepositoryImpl + ):ResetPasswordRepository + + @ResetPasswordExceptionMapper + @Binds + fun bindResetPasswordExceptionToErrorMapper( + impl:ResetPasswordExceptionToErrorMapper + ):BaseExceptionToErrorMapper } \ No newline at end of file diff --git a/app/src/main/java/app/cashadvisor/authorization/di/AuthenticationDomainModule.kt b/app/src/main/java/app/cashadvisor/authorization/di/AuthenticationDomainModule.kt index fbdd845c..f1264610 100644 --- a/app/src/main/java/app/cashadvisor/authorization/di/AuthenticationDomainModule.kt +++ b/app/src/main/java/app/cashadvisor/authorization/di/AuthenticationDomainModule.kt @@ -3,9 +3,11 @@ package app.cashadvisor.authorization.di import app.cashadvisor.authorization.domain.api.InputValidationInteractor import app.cashadvisor.authorization.domain.api.LoginInteractor import app.cashadvisor.authorization.domain.api.RegisterInteractor +import app.cashadvisor.authorization.domain.api.ResetPasswordInteractor import app.cashadvisor.authorization.domain.impl.InputValidationInteractorImpl import app.cashadvisor.authorization.domain.impl.LoginInteractorImpl import app.cashadvisor.authorization.domain.impl.RegisterInteractorImpl +import app.cashadvisor.authorization.domain.impl.ResetPasswordInteractorImpl import dagger.Binds import dagger.Module import dagger.hilt.InstallIn @@ -30,4 +32,9 @@ interface AuthenticationDomainModule { impl: InputValidationInteractorImpl ): InputValidationInteractor + @Binds + fun bindResetPasswordInteractor( + impl: ResetPasswordInteractorImpl + ):ResetPasswordInteractor + } \ No newline at end of file diff --git a/app/src/main/java/app/cashadvisor/authorization/di/AuthenticationNetworkModule.kt b/app/src/main/java/app/cashadvisor/authorization/di/AuthenticationNetworkModule.kt index 3d5c4f73..df46d0b5 100644 --- a/app/src/main/java/app/cashadvisor/authorization/di/AuthenticationNetworkModule.kt +++ b/app/src/main/java/app/cashadvisor/authorization/di/AuthenticationNetworkModule.kt @@ -2,12 +2,13 @@ package app.cashadvisor.authorization.di import app.cashadvisor.authorization.data.api.LoginApiService import app.cashadvisor.authorization.data.api.RegisterApiService +import app.cashadvisor.authorization.data.api.ResetPasswordApiService import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent -import retrofit2.Retrofit import javax.inject.Singleton +import retrofit2.Retrofit @Module @@ -24,4 +25,10 @@ class AuthenticationNetworkModule { fun provideLoginApiService(retrofit: Retrofit): LoginApiService { return retrofit.create(LoginApiService::class.java) } + + @Provides + @Singleton + fun provideResetPasswordApiService(retrofit: Retrofit):ResetPasswordApiService{ + return retrofit.create(ResetPasswordApiService::class.java) + } } \ No newline at end of file diff --git a/app/src/main/java/app/cashadvisor/authorization/di/AuthorizationDataModule.kt b/app/src/main/java/app/cashadvisor/authorization/di/AuthorizationDataModule.kt index ae4c824b..e5c7679a 100644 --- a/app/src/main/java/app/cashadvisor/authorization/di/AuthorizationDataModule.kt +++ b/app/src/main/java/app/cashadvisor/authorization/di/AuthorizationDataModule.kt @@ -2,6 +2,7 @@ package app.cashadvisor.authorization.di import android.content.Context import android.content.SharedPreferences +import android.content.res.Resources import androidx.security.crypto.EncryptedSharedPreferences import androidx.security.crypto.MasterKeys import app.cashadvisor.authorization.data.impl.CredentialsRepositoryImpl @@ -46,6 +47,12 @@ class AuthorizationDataModule { key: String = CREDENTIALS_KEY, gson: Json ): CredentialsRepository = CredentialsRepositoryImpl(storage, key, gson) + + @Provides + @Singleton + fun provideResources(@ApplicationContext context: Context): Resources { + return context.resources + } companion object { private const val SECRET_SETTINGS = "secret_shared_prefs" private const val CREDENTIALS_KEY = "secret_credentials" diff --git a/app/src/main/java/app/cashadvisor/authorization/domain/ResetPasswordExceptionToErrorMapper.kt b/app/src/main/java/app/cashadvisor/authorization/domain/ResetPasswordExceptionToErrorMapper.kt new file mode 100644 index 00000000..eedd193f --- /dev/null +++ b/app/src/main/java/app/cashadvisor/authorization/domain/ResetPasswordExceptionToErrorMapper.kt @@ -0,0 +1,90 @@ +package app.cashadvisor.authorization.domain + +import app.cashadvisor.common.domain.BaseExceptionToErrorMapper +import app.cashadvisor.common.domain.model.ErrorEntity +import app.cashadvisor.common.utill.exceptions.ResetPasswordException +import javax.inject.Inject + +class ResetPasswordExceptionToErrorMapper @Inject constructor() : BaseExceptionToErrorMapper() { + override fun handleSpecificException(exception: Exception): ErrorEntity { + return when (exception) { + is ResetPasswordException.ConfirmEmailToResetPassword -> handleConfirmEmailToResetPasswordException( + exception + ) + + is ResetPasswordException.SaveNewPassword -> handleSaveNewPasswordException(exception) + is ResetPasswordException.ConfirmResetPasswordWithCode -> handleConfirmResetPasswordWithCode( + exception + ) + + else -> handleUnknownError(exception) + } + } + + private fun handleConfirmEmailToResetPasswordException(exception: ResetPasswordException.ConfirmEmailToResetPassword): ErrorEntity { + return when (exception) { + is ResetPasswordException.ConfirmEmailToResetPassword.BadRequestInvalidInputOrContentType -> { + ErrorEntity.ConfirmEmailToResetPassword.InvalidInput( + exception.message + ) + } + + is ResetPasswordException.ConfirmEmailToResetPassword.InternalServerErrorFailedToGenerateTokenOrSendEmail -> { + ErrorEntity.ConfirmEmailToResetPassword.FailedToGenerateTokenOrSendEmail( + exception.message + ) + } + } + } + + private fun handleSaveNewPasswordException(exception: ResetPasswordException.SaveNewPassword): ErrorEntity { + return when (exception) { + is ResetPasswordException.SaveNewPassword.BadRequestInvalidPasswordOrMissingContentTypeHeader -> { + ErrorEntity.SaveNewPassword.InvalidInput( + exception.message + ) + } + + is ResetPasswordException.SaveNewPassword.UnauthorizedInvalidTokenOrMissingContentTypeHeader -> { + ErrorEntity.SaveNewPassword.InvalidToken( + exception.message + ) + } + + is ResetPasswordException.SaveNewPassword.InternalServerErrorFailedToResetPassword -> { + ErrorEntity.SaveNewPassword.FailedToResetPassword( + exception.message + ) + } + } + + } + + private fun handleConfirmResetPasswordWithCode(exception: ResetPasswordException.ConfirmResetPasswordWithCode): ErrorEntity { + return when (exception) { + is ResetPasswordException.ConfirmResetPasswordWithCode.BadRequestInvalidCodeOrMissingContentTypeHeader -> { + ErrorEntity.ConfirmResetPasswordByEmailWithCode.InvalidInput( + exception.message + ) + } + + is ResetPasswordException.ConfirmResetPasswordWithCode.UnauthorizedWrongConfirmationCode -> { + ErrorEntity.ConfirmResetPasswordByEmailWithCode.WrongConfirmationCode( + exception.message, + exception.remainingAttempts, + exception.lockDuration + ) + } + + is ResetPasswordException.ConfirmResetPasswordWithCode.InternalServerErrorFailedToConfirmResetPassword -> { + ErrorEntity.ConfirmResetPasswordByEmailWithCode.FailedToConfirmPasswordReset( + exception.message + ) + } + + } + + } + + +} \ No newline at end of file diff --git a/app/src/main/java/app/cashadvisor/authorization/domain/api/ResetPasswordInteractor.kt b/app/src/main/java/app/cashadvisor/authorization/domain/api/ResetPasswordInteractor.kt new file mode 100644 index 00000000..7f5208c4 --- /dev/null +++ b/app/src/main/java/app/cashadvisor/authorization/domain/api/ResetPasswordInteractor.kt @@ -0,0 +1,17 @@ +package app.cashadvisor.authorization.domain.api + +import app.cashadvisor.authorization.domain.models.ConfirmCode +import app.cashadvisor.authorization.domain.models.Email +import app.cashadvisor.authorization.domain.models.Password +import app.cashadvisor.authorization.domain.models.ResetPasswordData +import app.cashadvisor.authorization.domain.models.SaveNewPasswordData +import app.cashadvisor.common.domain.Resource + +interface ResetPasswordInteractor { + suspend fun confirmEmailForPasswordReset(email: Email): Resource + + suspend fun resetPasswordConfirmWithCode(code: ConfirmCode): Resource + + suspend fun saveNewPassword(email: Email, password: Password): Resource + +} \ No newline at end of file diff --git a/app/src/main/java/app/cashadvisor/authorization/domain/api/ResetPasswordRepository.kt b/app/src/main/java/app/cashadvisor/authorization/domain/api/ResetPasswordRepository.kt new file mode 100644 index 00000000..75cb2b8a --- /dev/null +++ b/app/src/main/java/app/cashadvisor/authorization/domain/api/ResetPasswordRepository.kt @@ -0,0 +1,25 @@ +package app.cashadvisor.authorization.domain.api + +import app.cashadvisor.authorization.domain.models.ConfirmCode +import app.cashadvisor.authorization.domain.models.ConfirmResetPasswordWithCode +import app.cashadvisor.authorization.domain.models.Email +import app.cashadvisor.authorization.domain.models.Password +import app.cashadvisor.authorization.domain.models.ResetPasswordData +import app.cashadvisor.authorization.domain.models.SaveNewPasswordData +import app.cashadvisor.common.domain.Resource + +interface ResetPasswordRepository { + suspend fun confirmEmailForPasswordReset( + email:Email + ):Resource + + suspend fun resetPasswordConfirmWithCode( + code: ConfirmCode, + ):Resource + + suspend fun saveNewPassword( + email: Email, + password:Password, + ):Resource + +} \ No newline at end of file diff --git a/app/src/main/java/app/cashadvisor/authorization/domain/impl/ResetPasswordInteractorImpl.kt b/app/src/main/java/app/cashadvisor/authorization/domain/impl/ResetPasswordInteractorImpl.kt new file mode 100644 index 00000000..786d8e22 --- /dev/null +++ b/app/src/main/java/app/cashadvisor/authorization/domain/impl/ResetPasswordInteractorImpl.kt @@ -0,0 +1,62 @@ +package app.cashadvisor.authorization.domain.impl + +import app.cashadvisor.authorization.domain.api.ResetPasswordInteractor +import app.cashadvisor.authorization.domain.api.ResetPasswordRepository +import app.cashadvisor.authorization.domain.models.ConfirmCode +import app.cashadvisor.authorization.domain.models.Email +import app.cashadvisor.authorization.domain.models.Password +import app.cashadvisor.authorization.domain.models.ResetPasswordData +import app.cashadvisor.authorization.domain.models.SaveNewPasswordData +import app.cashadvisor.common.domain.Resource +import javax.inject.Inject + +class ResetPasswordInteractorImpl @Inject constructor( + private val resetPasswordRepository: ResetPasswordRepository, +) : ResetPasswordInteractor { + override suspend fun confirmEmailForPasswordReset(email: Email): Resource { + val result = resetPasswordRepository.confirmEmailForPasswordReset(email) + return when (result) { + is Resource.Success -> { + Resource.Success(data = result.data) + } + + is Resource.Error -> { + Resource.Error(error = result.error) + } + } + } + + override suspend fun resetPasswordConfirmWithCode(code: ConfirmCode): Resource { + val result = resetPasswordRepository.resetPasswordConfirmWithCode(code) + return when (result) { + is Resource.Success -> { + if (result.data.message != null) { + Resource.Success(data = result.data.message) + } else { + Resource.Success(data = "null") + } + } + + is Resource.Error -> { + Resource.Error(error = result.error) + } + } + + } + + override suspend fun saveNewPassword( + email: Email, + password: Password + ): Resource { + val result = resetPasswordRepository.saveNewPassword(email, password) + return when (result) { + is Resource.Success -> { + Resource.Success(data = result.data) + } + + is Resource.Error -> { + Resource.Error(error = result.error) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/app/cashadvisor/authorization/domain/models/ConfirmResetPasswordWithCode.kt b/app/src/main/java/app/cashadvisor/authorization/domain/models/ConfirmResetPasswordWithCode.kt new file mode 100644 index 00000000..c7db4859 --- /dev/null +++ b/app/src/main/java/app/cashadvisor/authorization/domain/models/ConfirmResetPasswordWithCode.kt @@ -0,0 +1,5 @@ +package app.cashadvisor.authorization.domain.models + +data class ConfirmResetPasswordWithCode( + val message:String? = null +) diff --git a/app/src/main/java/app/cashadvisor/authorization/domain/models/ResetPasswordData.kt b/app/src/main/java/app/cashadvisor/authorization/domain/models/ResetPasswordData.kt new file mode 100644 index 00000000..824194bd --- /dev/null +++ b/app/src/main/java/app/cashadvisor/authorization/domain/models/ResetPasswordData.kt @@ -0,0 +1,6 @@ +package app.cashadvisor.authorization.domain.models + +data class ResetPasswordData( + val message:String, + val statusCode: Int +) diff --git a/app/src/main/java/app/cashadvisor/authorization/domain/models/ResetPasswordState.kt b/app/src/main/java/app/cashadvisor/authorization/domain/models/ResetPasswordState.kt new file mode 100644 index 00000000..4100def2 --- /dev/null +++ b/app/src/main/java/app/cashadvisor/authorization/domain/models/ResetPasswordState.kt @@ -0,0 +1,10 @@ +package app.cashadvisor.authorization.domain.models + +data class ResetPasswordState( + val state: ResetPasswordState.State = ResetPasswordState.State.Initial +){ + sealed interface State { + data object Initial : State + data class InProcess(val codeToken: String) : State + } +} diff --git a/app/src/main/java/app/cashadvisor/authorization/domain/models/SaveNewPasswordData.kt b/app/src/main/java/app/cashadvisor/authorization/domain/models/SaveNewPasswordData.kt new file mode 100644 index 00000000..103b0b50 --- /dev/null +++ b/app/src/main/java/app/cashadvisor/authorization/domain/models/SaveNewPasswordData.kt @@ -0,0 +1,6 @@ +package app.cashadvisor.authorization.domain.models + +data class SaveNewPasswordData( + val message:String, + val statusCode: Int +) diff --git a/app/src/main/java/app/cashadvisor/authorization/domain/models/states/PasswordRecoveryScreenState.kt b/app/src/main/java/app/cashadvisor/authorization/domain/models/states/PasswordRecoveryScreenState.kt new file mode 100644 index 00000000..254686fc --- /dev/null +++ b/app/src/main/java/app/cashadvisor/authorization/domain/models/states/PasswordRecoveryScreenState.kt @@ -0,0 +1,25 @@ +package app.cashadvisor.authorization.domain.models.states + +import app.cashadvisor.authorization.presentation.viewmodel.models.RecoveryEmailValidationState +import app.cashadvisor.authorization.presentation.viewmodel.models.RecoveryPasswordValidationState + +sealed interface PasswordRecoveryScreenState{ + data class EmailInput( + val emailState: RecoveryEmailValidationState? = null, + val isLoginSuccessful: Boolean? = null, + val isBtnLoginEnabled: Boolean, + val isLoading: Boolean? = null + ): PasswordRecoveryScreenState + data class ConfirmationCode(val resendingCoolDownSec: String? = null) : + PasswordRecoveryScreenState + + data class PasswordInput( + val passwordState: RecoveryPasswordValidationState? = null, + val resetPasswordSuccessful:Boolean? = null, + val isBtnResetPasswordEnabled: Boolean, + val isLoading: Boolean? = null + ):PasswordRecoveryScreenState +} + + + diff --git a/app/src/main/java/app/cashadvisor/authorization/presentation/ui/PasswordRecoveryFragment.kt b/app/src/main/java/app/cashadvisor/authorization/presentation/ui/PasswordRecoveryFragment.kt index 6631c9d1..af0526c7 100644 --- a/app/src/main/java/app/cashadvisor/authorization/presentation/ui/PasswordRecoveryFragment.kt +++ b/app/src/main/java/app/cashadvisor/authorization/presentation/ui/PasswordRecoveryFragment.kt @@ -1,45 +1,376 @@ package app.cashadvisor.authorization.presentation.ui -import android.os.Bundle +import android.content.Context import android.view.LayoutInflater import android.view.View -import android.view.ViewGroup -import androidx.fragment.app.Fragment +import android.view.inputmethod.InputMethodManager +import androidx.core.content.res.ResourcesCompat +import androidx.core.widget.addTextChangedListener +import androidx.fragment.app.viewModels +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import androidx.navigation.fragment.findNavController -import app.cashadvisor.R +import app.cashadvisor.authorization.domain.models.states.PasswordRecoveryScreenState +import app.cashadvisor.authorization.presentation.ui.models.RecoveryScreenMessageContent +import app.cashadvisor.authorization.presentation.ui.models.RecoverySideEffect +import app.cashadvisor.authorization.presentation.viewmodel.PasswordRecoveryViewModel +import app.cashadvisor.authorization.presentation.viewmodel.models.RecoveryEmailValidationState +import app.cashadvisor.authorization.presentation.viewmodel.models.RecoveryPasswordValidationState +import app.cashadvisor.common.ui.BaseFragment import app.cashadvisor.databinding.FragmentPasswordRecoveryBinding +import app.cashadvisor.uikit.R +import app.cashadvisor.uikit.databinding.ItemDialogNoInternetBinding +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.google.android.material.snackbar.Snackbar +import com.google.android.material.textfield.TextInputEditText +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.launch -class PasswordRecoveryFragment : Fragment() { +@AndroidEntryPoint +class PasswordRecoveryFragment : + BaseFragment( + FragmentPasswordRecoveryBinding::inflate + ) { + override val viewModel: PasswordRecoveryViewModel by viewModels() + override fun onConfigureViews() { + with(binding) { + btnGetCode.setOnClickListener { + viewModel.requestRecovery() + } + btnSendNewPassword.setOnClickListener { + viewModel.setPassword(etPasswordInput.text.toString()) + } + etEmailInput.addTextChangedListener { + viewModel.emailInputListener(it) + } + etConfirmationCode.setCallback { + viewModel.setEmailConfirmCode(it) + } + etPasswordInput.addTextChangedListener { + viewModel.passwordInputListener(it) + } + tvCantGetCode.setOnClickListener { + //navigation to support screen. It`s not exist yet + } + } + } + + override fun onSubscribe() { + viewLifecycleOwner.lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.uiState.collect { uiState -> + updateUi(uiState) + + } + + } + } + viewLifecycleOwner.lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.sideEffect.collect { recoverySideEffect -> + handleSideEffects(recoverySideEffect) + + } + } + } + viewLifecycleOwner.lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.messageEvent.collect { screenMessageState -> + showMessages(screenMessageState) + + } + } + } + + } + + private fun updateUi(state: PasswordRecoveryScreenState) { + navigateBack(state) + with(binding) { + when (state) { + is PasswordRecoveryScreenState.EmailInput -> { + customSteps.changeSteps(maxSteps = 3, currentStep = 1) + tvSubtitle.text = getText(R.string.enter_account_data) + clEmailForms.visibility = View.VISIBLE + clConfirmationCode.visibility = View.GONE + clNewPassword.visibility = View.GONE + + + btnGetCode.isEnabled = state.isBtnLoginEnabled + manageEmailValidation(state.emailState) + + state.isLoginSuccessful?.let { isLoginSuccessful -> + if (isLoginSuccessful) { + hideKeyboard() + } else { + showErrorEditText(etEmailInput) + } + } + } + + is PasswordRecoveryScreenState.ConfirmationCode -> { + customSteps.changeSteps(maxSteps = 3, currentStep = 2) + tvSubtitle.text = getText(R.string.confirmation) + clEmailForms.visibility = View.GONE + clConfirmationCode.visibility = View.VISIBLE + clNewPassword.visibility = View.GONE + + if (state.resendingCoolDownSec.isNullOrBlank()) { + tvSendAgain.apply { + text = getString(R.string.send_confirmation_code_again) + setTextColor(resources.getColor(R.color.black, requireActivity().theme)) + setOnClickListener { viewModel.sendConfirmationCodeByEmail() } + } + } else { + tvSendAgain.apply { + text = getString( + R.string.send_confirmation_code_again_seconds, + state.resendingCoolDownSec + ) + setTextColor(resources.getColor(R.color.subcolour2, requireActivity().theme)) + setOnClickListener(null) + } + } + } + + is PasswordRecoveryScreenState.PasswordInput -> { + customSteps.changeSteps(maxSteps = 3, currentStep = 3) + tvSubtitle.text = getText(R.string.new_password) + clEmailForms.visibility = View.GONE + clConfirmationCode.visibility = View.GONE + clNewPassword.visibility = View.VISIBLE + + btnSendNewPassword.isEnabled = state.isBtnResetPasswordEnabled + managePasswordValidation(state.passwordState) + + state.resetPasswordSuccessful?.let { isResetPasswordSuccessful -> + if (isResetPasswordSuccessful) { + hideKeyboard() + } else { + showErrorEditText(etPasswordInput) + } + } + } + } + + + } + + } + + private fun handleSideEffects(sideEffect: RecoverySideEffect) { + when (sideEffect) { + is RecoverySideEffect.PasswordSuccessfullyConfirmed -> { + findNavController().navigate( + app.cashadvisor.R.id.action_passwordRecoveryFragment_to_entryVerificationFragment + ) + } + + is RecoverySideEffect.HideKeyboard -> { + hideKeyboard() + } + + is RecoverySideEffect.NoInternetConnection -> { + hideKeyboard() + showNoInternetDialog() + } + is RecoverySideEffect.ClearConfirmationCode -> { + binding.etConfirmationCode.setCode("") + } + else -> findNavController().navigateUp() + + } + } + + private fun navigateBack(state: PasswordRecoveryScreenState) { + viewModel.initOnBackPressedDispatcher(requireActivity().onBackPressedDispatcher) + with(binding) { + when (state) { + is PasswordRecoveryScreenState.ConfirmationCode -> { + btnBack.setOnClickListener { + viewModel.navigateBackToEmailState() + } + + } + + is PasswordRecoveryScreenState.EmailInput -> { + binding.btnBack.setOnClickListener { + findNavController().navigateUp() + } - private var _binding: FragmentPasswordRecoveryBinding? = null - private val binding get() = _binding!! + } + + is PasswordRecoveryScreenState.PasswordInput -> { + binding.btnBack.setOnClickListener { + viewModel.navigateBackToEmailState() + } + + } + } + } + } + private fun manageEmailValidation(state: RecoveryEmailValidationState?) { + state?.let { + with(binding) { + when (state) { + is RecoveryEmailValidationState.Error -> { + showErrorEditText(etEmailInput) + } - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - arguments?.let { + is RecoveryEmailValidationState.Success -> { + showSuccessEditText(etEmailInput) + } + is RecoveryEmailValidationState.Default -> { + showNeutralEditText(etEmailInput) + } + } + } } + } - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - _binding = FragmentPasswordRecoveryBinding.inflate(layoutInflater, container, false) - return binding.root + private fun managePasswordValidation(state: RecoveryPasswordValidationState?) { + state?.let { + with(binding) { + when (state) { + is RecoveryPasswordValidationState.Error -> { + showErrorEditText(binding.etPasswordInput) + tiPasswordInput.isHelperTextEnabled = true + tiPasswordInput.helperText = + getString(R.string.password_text_input_helper_text) + } + + is RecoveryPasswordValidationState.Default -> { + showNeutralEditText(binding.etPasswordInput) + tiPasswordInput.isHelperTextEnabled = false + } + + is RecoveryPasswordValidationState.Success -> { + showSuccessEditText(binding.etPasswordInput) + tiPasswordInput.isHelperTextEnabled = false + } + } + } + } } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) + private fun showMessages(content: RecoveryScreenMessageContent) { + with(binding) { + when (content) { + is RecoveryScreenMessageContent.EmailFormatError -> { + showSnackbar( + getString(R.string.wrong_email_format), + etEmailInput + ) + } + + is RecoveryScreenMessageContent.PasswordCountError -> { + showSnackbar( + getString(R.string.wrong_password_count), + etPasswordInput + ) + } + + is RecoveryScreenMessageContent.PasswordFormatError -> { + showSnackbar( + getString(R.string.wrong_password_format), + etPasswordInput + ) + } + + is RecoveryScreenMessageContent.LoginError -> { + showSnackbar( + content.message, + etEmailInput + ) + } + + is RecoveryScreenMessageContent.ConfirmationCodeMessage -> { + showSnackbar( + content.message, + etEmailInput + ) + } - binding.btnVerification.setOnClickListener { - findNavController().navigate(R.id.action_passwordRecoveryFragment_to_entryVerificationFragment) + is RecoveryScreenMessageContent.ResetPasswordError -> { + showSnackbar( + content.message, + etEmailInput + ) + } + } } } - override fun onDestroyView() { - super.onDestroyView() - _binding = null + private fun showErrorEditText(editText: TextInputEditText) { + editText.background = ResourcesCompat.getDrawable( + resources, + R.drawable.text_input_background_error, + requireActivity().theme + ) + } + + private fun showSuccessEditText(editText: TextInputEditText) { + editText.background = ResourcesCompat.getDrawable( + resources, + R.drawable.text_input_background_success, + requireActivity().theme + ) + } + + private fun showNeutralEditText(editText: TextInputEditText) { + editText.background = ResourcesCompat.getDrawable( + resources, + R.drawable.text_input_background_neutral, + requireActivity().theme + ) + } + + private fun showSnackbar(message: String, viewToFocus: TextInputEditText) { + hideKeyboard() + Snackbar.make(binding.root, message, LoginFragment.SNACKBAR_DURATION) + .setBackgroundTint(resources.getColor(R.color.black, requireActivity().theme)) + .setTextColor(resources.getColor(R.color.white, requireActivity().theme)) + .setActionTextColor(resources.getColor(R.color.white, requireActivity().theme)) + .setAction(getString(R.string.ok)) { + viewToFocus.requestFocus() + showKeyboard(viewToFocus) + } + .show() + } + + private fun hideKeyboard() { + val inputMethodManager = + requireActivity().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + inputMethodManager.hideSoftInputFromWindow(binding.root.windowToken, 0) + } + + private fun showKeyboard(view: View) { + val inputMethodManager = + requireActivity().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + inputMethodManager.showSoftInput(view, 0) + } + + private fun showNoInternetDialog() { + val inflater = LayoutInflater.from(requireContext()) + val dialogBinding = ItemDialogNoInternetBinding.inflate(inflater) + + val noInternetDialog = MaterialAlertDialogBuilder(requireContext()) + .setView(dialogBinding.root) + .setBackground( + ResourcesCompat.getDrawable( + resources, + R.drawable.dialog_no_internet_background, + requireActivity().theme + ) + ) + .show() + + dialogBinding.btnClose.setOnClickListener { + noInternetDialog.dismiss() + } } } \ No newline at end of file diff --git a/app/src/main/java/app/cashadvisor/authorization/presentation/ui/models/RecoveryScreenMessageContent.kt b/app/src/main/java/app/cashadvisor/authorization/presentation/ui/models/RecoveryScreenMessageContent.kt new file mode 100644 index 00000000..5ddbfa14 --- /dev/null +++ b/app/src/main/java/app/cashadvisor/authorization/presentation/ui/models/RecoveryScreenMessageContent.kt @@ -0,0 +1,11 @@ +package app.cashadvisor.authorization.presentation.ui.models + +sealed interface RecoveryScreenMessageContent { + + data object EmailFormatError: RecoveryScreenMessageContent + data object PasswordFormatError: RecoveryScreenMessageContent + data object PasswordCountError: RecoveryScreenMessageContent + data class LoginError(val message: String): RecoveryScreenMessageContent + data class ResetPasswordError(val message: String):RecoveryScreenMessageContent + data class ConfirmationCodeMessage(val message: String): RecoveryScreenMessageContent +} \ No newline at end of file diff --git a/app/src/main/java/app/cashadvisor/authorization/presentation/ui/models/RecoverySideEffect.kt b/app/src/main/java/app/cashadvisor/authorization/presentation/ui/models/RecoverySideEffect.kt new file mode 100644 index 00000000..22ff384a --- /dev/null +++ b/app/src/main/java/app/cashadvisor/authorization/presentation/ui/models/RecoverySideEffect.kt @@ -0,0 +1,11 @@ +package app.cashadvisor.authorization.presentation.ui.models + +sealed interface RecoverySideEffect { + data object NoInternetConnection: RecoverySideEffect + data object PasswordSuccessfullyConfirmed: RecoverySideEffect + data object HideKeyboard: RecoverySideEffect + data object ClearConfirmationCode:RecoverySideEffect + data object NavigateBackToLoginFragment:RecoverySideEffect + + +} \ No newline at end of file diff --git a/app/src/main/java/app/cashadvisor/authorization/presentation/viewmodel/PasswordRecoveryViewModel.kt b/app/src/main/java/app/cashadvisor/authorization/presentation/viewmodel/PasswordRecoveryViewModel.kt new file mode 100644 index 00000000..53a08fa0 --- /dev/null +++ b/app/src/main/java/app/cashadvisor/authorization/presentation/viewmodel/PasswordRecoveryViewModel.kt @@ -0,0 +1,472 @@ +package app.cashadvisor.authorization.presentation.viewmodel + +import android.content.res.Resources +import android.text.Editable +import androidx.activity.OnBackPressedDispatcher +import androidx.activity.addCallback +import androidx.lifecycle.viewModelScope +import app.cashadvisor.authorization.domain.api.InputValidationInteractor +import app.cashadvisor.authorization.domain.api.ResetPasswordInteractor +import app.cashadvisor.authorization.domain.models.ConfirmCode +import app.cashadvisor.authorization.domain.models.Email +import app.cashadvisor.authorization.domain.models.Password +import app.cashadvisor.authorization.domain.models.PasswordValidationError +import app.cashadvisor.authorization.domain.models.states.ConfirmCodeValidationState +import app.cashadvisor.authorization.domain.models.states.EmailValidationState +import app.cashadvisor.authorization.domain.models.states.PasswordRecoveryScreenState +import app.cashadvisor.authorization.domain.models.states.PasswordValidationState +import app.cashadvisor.authorization.presentation.ui.models.RecoveryScreenMessageContent +import app.cashadvisor.authorization.presentation.ui.models.RecoverySideEffect +import app.cashadvisor.authorization.presentation.viewmodel.models.RecoveryEmailValidationState +import app.cashadvisor.authorization.presentation.viewmodel.models.RecoveryPasswordValidationState +import app.cashadvisor.common.domain.Resource +import app.cashadvisor.common.domain.model.ErrorEntity +import app.cashadvisor.common.ui.BaseViewModel +import app.cashadvisor.common.utill.extensions.logDebugMessage +import app.cashadvisor.uikit.R +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class PasswordRecoveryViewModel @Inject constructor( + private val resetPasswordInteractor: ResetPasswordInteractor, + private val inputValidationInteractor: InputValidationInteractor, + private val resources: Resources +) : BaseViewModel() { + + private var emailInput = "" + private var confirmCode = "" + private var passwordInput = "" + + private var attemptsToSendConfirmationCode = 3 + + private var resendCountDownJob: Job? = null + + private val _uiState: MutableStateFlow = MutableStateFlow( + PasswordRecoveryScreenState.EmailInput( + emailState = RecoveryEmailValidationState.Default, + isBtnLoginEnabled = false + ) + ) + val uiState: StateFlow = _uiState.asStateFlow() + + private val _sideEffects: MutableSharedFlow = MutableSharedFlow() + val sideEffect: SharedFlow = _sideEffects.asSharedFlow() + + private val _messageEvent = MutableSharedFlow() + val messageEvent = _messageEvent.asSharedFlow() + + + fun requestRecovery( + + ) { + viewModelScope.launch { + val result = inputValidationInteractor.validateEmail(emailInput) + when (result) { + is EmailValidationState.Success -> { + _uiState.value = PasswordRecoveryScreenState.EmailInput( + emailState = RecoveryEmailValidationState.Success(result.email), + isBtnLoginEnabled = true + ) + recovery() + + + } + + is EmailValidationState.Error -> { + _uiState.value = PasswordRecoveryScreenState.EmailInput( + emailState = RecoveryEmailValidationState + .Error(result.email, result.emailValidationError), + isBtnLoginEnabled = false + ) + emailValidationErrorMessage(result) + } + } + } + } + + private fun recovery() { + + viewModelScope.launch(Dispatchers.IO) { + logDebugMessage(emailInput) + val result = resetPasswordInteractor.confirmEmailForPasswordReset( + Email(emailInput) + ) + when (result) { + is Resource.Success -> { + logDebugMessage("Message recovery ${result.data.message}") + viewModelScope.launch { + _uiState.value = PasswordRecoveryScreenState.ConfirmationCode() + _sideEffects.emit(RecoverySideEffect.HideKeyboard) + sendConfirmationCodeByEmail() + } + } + + is Resource.Error -> { + when (result.error) { + + is ErrorEntity.NetworksError.NoInternet -> { + logDebugMessage("NoInternet ${result.error.message}") + _sideEffects.emit(RecoverySideEffect.NoInternetConnection) + } + + is ErrorEntity.ConfirmEmailToResetPassword -> { + when (result.error) { + is ErrorEntity.ConfirmEmailToResetPassword.FailedToGenerateTokenOrSendEmail -> { + logDebugMessage("FailedToGenerateTokenOrSendEmail ${result.error.message}") + + } + + is ErrorEntity.ConfirmEmailToResetPassword.InvalidInput -> { + logDebugMessage("InvalidEmail ${result.error.message}") + _messageEvent.emit( + RecoveryScreenMessageContent + .LoginError(message = result.error.message) + ) + + } + } + } + + else -> { + logDebugMessage("Something went wrong ${result.error.message}") + _messageEvent.emit( + RecoveryScreenMessageContent.LoginError(message = result.error.message) + ) + } + } + } + } + } + + } + + fun setEmailConfirmCode(code: String) { + viewModelScope.launch { + val result = inputValidationInteractor.validateConfirmationCode(code) + when (result) { + is ConfirmCodeValidationState.Success -> { + confirmCode = code + sendEmailConfirmCode() + } + + is ConfirmCodeValidationState.Error -> { + + + } + } + } + } + + private fun sendEmailConfirmCode() { + viewModelScope.launch { + val result = + resetPasswordInteractor.resetPasswordConfirmWithCode(ConfirmCode(confirmCode)) + when (result) { + is Resource.Success -> { + viewModelScope.launch { + logDebugMessage(result.data) + resendCountDownJob?.cancel() + _uiState.value = PasswordRecoveryScreenState.PasswordInput( + passwordState = RecoveryPasswordValidationState.Default, + isBtnResetPasswordEnabled = false, + ) + } + } + + is Resource.Error -> { + _sideEffects.emit(RecoverySideEffect.ClearConfirmationCode) + logDebugMessage(result.error.message) + when (result.error) { + is ErrorEntity.ConfirmResetPasswordByEmailWithCode.InvalidInput -> { + _messageEvent.emit( + RecoveryScreenMessageContent + .ConfirmationCodeMessage( + message = + resources.getString(R.string.invalid_or_expired_token) + ) + ) + navigateBackToEmailState() + } + + is ErrorEntity.ConfirmResetPasswordByEmailWithCode.WrongConfirmationCode -> { + viewModelScope.launch { + val remainingAttempts = result.error.remainingAttempts + if (remainingAttempts != null) { + attemptsToSendConfirmationCode = remainingAttempts + } else { + attemptsToSendConfirmationCode -= 1 + } + if (attemptsToSendConfirmationCode > 0) { + _messageEvent.emit( + RecoveryScreenMessageContent + .ConfirmationCodeMessage( + message = + resources.getString(R.string.invalid_confirmation_code) + ) + ) + } else { + _messageEvent.emit( + RecoveryScreenMessageContent.ConfirmationCodeMessage( + message = + resources.getString(R.string.user_is_locked) + ) + ) + attemptsToSendConfirmationCode = 3 + + navigateBackToEmailState() + } + + + } + + } + + is ErrorEntity.ConfirmResetPasswordByEmailWithCode.FailedToConfirmPasswordReset -> { + _messageEvent.emit( + RecoveryScreenMessageContent.ConfirmationCodeMessage( + message = + resources.getString(R.string.failed_to_confirm_email_or_register_user) + ) + ) + navigateBackToEmailState() + } + + is ErrorEntity.NetworksError.NoInternet -> { + + _sideEffects.emit(RecoverySideEffect.NoInternetConnection) + + } + + else -> { + logDebugMessage( + resources.getString( + R.string.debug_message_something_went_wrong, + result.error.message + ) + ) + _messageEvent.emit( + RecoveryScreenMessageContent.ConfirmationCodeMessage( + message = + result.error.message + ) + ) + } + } + } + + } + } + } + + fun setPassword(password: String) { + viewModelScope.launch { + val result = inputValidationInteractor.validatePassword(password) + when (result) { + is PasswordValidationState.Success -> { + _uiState.value = PasswordRecoveryScreenState.PasswordInput( + passwordState = RecoveryPasswordValidationState.Success(result.password), + isBtnResetPasswordEnabled = true + ) + sendNewPassword() + } + + is PasswordValidationState.Error -> { + _uiState.value = PasswordRecoveryScreenState.PasswordInput( + passwordState = + RecoveryPasswordValidationState.Error(result.passwordValidationError), + isBtnResetPasswordEnabled = false + ) + passwordValidationErrorMessage(result) + } + } + } + } + + private fun sendNewPassword() { + viewModelScope.launch { + val result = + resetPasswordInteractor.saveNewPassword( + email = Email(emailInput), + password = Password(passwordInput) + ) + when (result) { + is Resource.Success -> { + viewModelScope.launch { + logDebugMessage( + resources.getString(R.string.debug_message_success_reset_password) + ) + _sideEffects.emit(RecoverySideEffect.PasswordSuccessfullyConfirmed) + } + } + + is Resource.Error -> { + logDebugMessage(result.error.message) + when (result.error) { + is ErrorEntity.SaveNewPassword.InvalidInput -> { + _messageEvent.emit( + RecoveryScreenMessageContent.ResetPasswordError(result.error.message) + ) + } + + is ErrorEntity.SaveNewPassword.InvalidToken -> { + _messageEvent.emit( + RecoveryScreenMessageContent + .ResetPasswordError(result.error.message) + ) + } + + is ErrorEntity.SaveNewPassword.FailedToResetPassword -> { + _messageEvent.emit( + RecoveryScreenMessageContent.ResetPasswordError(result.error.message) + ) + } + + is ErrorEntity.NetworksError.NoInternet -> { + _sideEffects.emit(RecoverySideEffect.NoInternetConnection) + } + + else -> { + _messageEvent.emit( + RecoveryScreenMessageContent.ResetPasswordError(result.error.message) + ) + } + + } + } + } + } + } + + private fun emailValidationErrorMessage( + emailValidationState: EmailValidationState, + ) { + viewModelScope.launch { + if (emailValidationState is EmailValidationState.Error) { + _messageEvent.emit(RecoveryScreenMessageContent.EmailFormatError) + + } + } + } + + private fun passwordValidationErrorMessage( + passwordValidationState: PasswordValidationState + ) { + viewModelScope.launch { + if (passwordValidationState is PasswordValidationState.Error) { + when (passwordValidationState.passwordValidationError) { + PasswordValidationError.PASSWORD_NOT_VALID -> { + _messageEvent.emit(RecoveryScreenMessageContent.PasswordFormatError) + } + + PasswordValidationError.PASSWORD_IS_NOT_LONG_ENOUGH -> { + _messageEvent.emit(RecoveryScreenMessageContent.PasswordCountError) + } + } + } + } + } + + fun sendConfirmationCodeByEmail() { + //add some method in future to send code to email + startCountDownToResendCode() + } + + private fun startCountDownToResendCode( + ) { + resendCountDownJob = viewModelScope.launch(Dispatchers.IO) { + var allTime = RESENDING_COOL_DOWN + val interval = COUNT_DOWN_INTERVAL + + while (allTime > 0) { + _uiState.value = PasswordRecoveryScreenState.ConfirmationCode( + resendingCoolDownSec = (allTime / 1000).toString() + ) + allTime -= interval + delay(interval) + } + _uiState.value = PasswordRecoveryScreenState.ConfirmationCode() + } + } + + fun navigateBackToEmailState() { + viewModelScope.launch { + resendCountDownJob?.cancel() + _uiState.value = PasswordRecoveryScreenState.EmailInput( + emailState = RecoveryEmailValidationState.Default, + isLoginSuccessful = null, + isBtnLoginEnabled = true + ) + } + } + private fun navigateBackToLoginFragment(){ + viewModelScope.launch { + _sideEffects.emit(RecoverySideEffect.NavigateBackToLoginFragment) + } + } + + fun emailInputListener( + emailInput: Editable? = null, + + ) { + if (emailInput.toString() == this.emailInput) return + + viewModelScope.launch { + emailInput?.let { + this@PasswordRecoveryViewModel.emailInput = it.toString() + } + + val isBtnLoginEnabled = + this@PasswordRecoveryViewModel.emailInput.isNotBlank() + + _uiState.value = PasswordRecoveryScreenState.EmailInput( + emailState = RecoveryEmailValidationState.Default, + isBtnLoginEnabled = isBtnLoginEnabled + ) + } + } + + fun passwordInputListener( + passwordInput: Editable? = null + ) { + if (passwordInput.toString() == this.passwordInput) return + + viewModelScope.launch { + passwordInput?.let { + this@PasswordRecoveryViewModel.passwordInput = passwordInput.toString() + } + val isBtnResetPasswordEnabled = + this@PasswordRecoveryViewModel.passwordInput.isNotBlank() + + _uiState.value = PasswordRecoveryScreenState.PasswordInput( + passwordState = RecoveryPasswordValidationState.Default, + isBtnResetPasswordEnabled = isBtnResetPasswordEnabled + ) + + } + } + + fun initOnBackPressedDispatcher(onBackPressedDispatcher:OnBackPressedDispatcher){ + onBackPressedDispatcher.addCallback { + when(_uiState.value){ + is PasswordRecoveryScreenState.EmailInput -> navigateBackToLoginFragment() + else -> navigateBackToEmailState() + } + } + } + + companion object { + const val RESENDING_COOL_DOWN = 30000L + const val COUNT_DOWN_INTERVAL = 1000L + } +} \ No newline at end of file diff --git a/app/src/main/java/app/cashadvisor/authorization/presentation/viewmodel/models/RecoveryEmailValidationState.kt b/app/src/main/java/app/cashadvisor/authorization/presentation/viewmodel/models/RecoveryEmailValidationState.kt new file mode 100644 index 00000000..dfa51a84 --- /dev/null +++ b/app/src/main/java/app/cashadvisor/authorization/presentation/viewmodel/models/RecoveryEmailValidationState.kt @@ -0,0 +1,13 @@ +package app.cashadvisor.authorization.presentation.viewmodel.models + +import app.cashadvisor.authorization.domain.models.Email +import app.cashadvisor.authorization.domain.models.EmailValidationError + +interface RecoveryEmailValidationState { + data class Success(val email: Email) : RecoveryEmailValidationState + data class Error( + val email: Email, + val emailValidationError: EmailValidationError + ) : RecoveryEmailValidationState + data object Default: RecoveryEmailValidationState +} \ No newline at end of file diff --git a/app/src/main/java/app/cashadvisor/authorization/presentation/viewmodel/models/RecoveryPasswordScreenEvent.kt b/app/src/main/java/app/cashadvisor/authorization/presentation/viewmodel/models/RecoveryPasswordScreenEvent.kt new file mode 100644 index 00000000..e6011e44 --- /dev/null +++ b/app/src/main/java/app/cashadvisor/authorization/presentation/viewmodel/models/RecoveryPasswordScreenEvent.kt @@ -0,0 +1,12 @@ +package app.cashadvisor.authorization.presentation.viewmodel.models + +import android.content.Context + +sealed interface RecoveryPasswordScreenEvent { + class SetEmail(val email: String) : RecoveryPasswordScreenEvent + data object Recovery: RecoveryPasswordScreenEvent + class SetEmailConfirmCode(val code:String, val context: Context) : RecoveryPasswordScreenEvent + class ConfirmEmail(val context: Context): RecoveryPasswordScreenEvent + class SetPassword(val password:String, val context: Context): RecoveryPasswordScreenEvent + class ConfirmNewPassword(val context: Context): RecoveryPasswordScreenEvent +} \ No newline at end of file diff --git a/app/src/main/java/app/cashadvisor/authorization/presentation/viewmodel/models/RecoveryPasswordValidationState.kt b/app/src/main/java/app/cashadvisor/authorization/presentation/viewmodel/models/RecoveryPasswordValidationState.kt new file mode 100644 index 00000000..ef916a02 --- /dev/null +++ b/app/src/main/java/app/cashadvisor/authorization/presentation/viewmodel/models/RecoveryPasswordValidationState.kt @@ -0,0 +1,13 @@ +package app.cashadvisor.authorization.presentation.viewmodel.models + +import app.cashadvisor.authorization.domain.models.Password +import app.cashadvisor.authorization.domain.models.PasswordValidationError + +interface RecoveryPasswordValidationState { + data class Success(val password: Password) : RecoveryPasswordValidationState + data class Error( + //val password: Password, + val passwordValidationError: PasswordValidationError + ) : RecoveryPasswordValidationState + data object Default : RecoveryPasswordValidationState +} \ No newline at end of file diff --git a/app/src/main/java/app/cashadvisor/common/di/CommonDataModule.kt b/app/src/main/java/app/cashadvisor/common/di/CommonDataModule.kt index 19fe4d74..ec5fd6e3 100644 --- a/app/src/main/java/app/cashadvisor/common/di/CommonDataModule.kt +++ b/app/src/main/java/app/cashadvisor/common/di/CommonDataModule.kt @@ -144,8 +144,8 @@ class NetworkModule { } companion object { - const val ENDPOINT_URL_PROD = "https://212.233.78.3:8080/v1/" - const val ENDPOINT_URL_STAGE = "https://212.233.78.3:8080/v1/" + const val ENDPOINT_URL_PROD = "https://212.233.99.233:8080/v1/" + const val ENDPOINT_URL_STAGE = "https://212.233.99.233:8080/v1/" const val PROD = "prod" } diff --git a/app/src/main/java/app/cashadvisor/common/domain/BaseExceptionToErrorMapper.kt b/app/src/main/java/app/cashadvisor/common/domain/BaseExceptionToErrorMapper.kt index 05136d70..148f44a2 100644 --- a/app/src/main/java/app/cashadvisor/common/domain/BaseExceptionToErrorMapper.kt +++ b/app/src/main/java/app/cashadvisor/common/domain/BaseExceptionToErrorMapper.kt @@ -4,6 +4,7 @@ import app.cashadvisor.common.domain.model.ErrorEntity import app.cashadvisor.common.utill.exceptions.LoginException import app.cashadvisor.common.utill.exceptions.NetworkException import app.cashadvisor.common.utill.exceptions.RegisterException +import app.cashadvisor.common.utill.exceptions.ResetPasswordException import app.cashadvisor.common.utill.extensions.logNetworkError import app.cashadvisor.profile.data.mapper.UserProfileException import java.net.ConnectException @@ -16,7 +17,9 @@ abstract class BaseExceptionToErrorMapper { fun handleException(exception: Exception): ErrorEntity { return when (exception) { - is ConnectException, is NetworkException, is LoginException.NoConnection, is RegisterException.NoConnection, is UserProfileException.NoConnection -> { + + is ConnectException, is NetworkException, is LoginException.NoConnection, + is RegisterException.NoConnection, is ResetPasswordException.NoConnection, is UserProfileException.NoConnection -> { logNetworkError(exception.message) handleNetworkError(exception) } diff --git a/app/src/main/java/app/cashadvisor/common/domain/model/ErrorEntity.kt b/app/src/main/java/app/cashadvisor/common/domain/model/ErrorEntity.kt index 2b80f4de..71afc41b 100644 --- a/app/src/main/java/app/cashadvisor/common/domain/model/ErrorEntity.kt +++ b/app/src/main/java/app/cashadvisor/common/domain/model/ErrorEntity.kt @@ -50,6 +50,43 @@ sealed class ErrorEntity(open val message: String) { RegisterConfirmationWithCode(message) } + sealed class ConfirmEmailToResetPassword(override val message: String) : + ErrorEntity(message) { + data class InvalidInput(override val message: String) : ConfirmEmailToResetPassword(message) + data class FailedToGenerateTokenOrSendEmail(override val message: String) : + ConfirmEmailToResetPassword(message) + + } + + sealed class ConfirmResetPasswordByEmailWithCode(override val message: String) : + ErrorEntity(message) { + data class InvalidInput(override val message: String) : + ConfirmResetPasswordByEmailWithCode(message) + + data class WrongConfirmationCode( + override val message: String, + val remainingAttempts:Int? = null, + val lockDuration:Long? = null + ) : ConfirmResetPasswordByEmailWithCode(message) + + data class FailedToConfirmPasswordReset(override val message: String) : + ConfirmResetPasswordByEmailWithCode(message) + } + + sealed class SaveNewPassword(override val message: String) : ErrorEntity(message) { + + data class InvalidInput(override val message: String) : + SaveNewPassword(message) + + data class InvalidToken(override val message: String) : + SaveNewPassword(message) + + data class FailedToResetPassword(override val message: String) : + SaveNewPassword(message) + } + + + sealed class Profile(override val message: String) : ErrorEntity(message) { data class InvalidContent(override val message: String) : Profile(message) data class UserNotAuthorized(override val message: String) : Profile(message) diff --git a/app/src/main/java/app/cashadvisor/common/utill/exceptions/ResetPasswordException.kt b/app/src/main/java/app/cashadvisor/common/utill/exceptions/ResetPasswordException.kt new file mode 100644 index 00000000..38e2de50 --- /dev/null +++ b/app/src/main/java/app/cashadvisor/common/utill/exceptions/ResetPasswordException.kt @@ -0,0 +1,76 @@ +package app.cashadvisor.common.utill.exceptions + +import java.io.IOException + +sealed class ResetPasswordException( + override val message: String +) : IOException(message) { + + data class NoConnection( + override val message: String = NO_INTERNET_CONNECTION + ) : ResetPasswordException(message = message) + + data class Undefined(override val message: String = UNDEFINED_MESSAGE) : + ResetPasswordException(message = message) + + sealed class ConfirmResetPasswordWithCode( + override val message: String + ):ResetPasswordException(message){ + class BadRequestInvalidCodeOrMissingContentTypeHeader( + override val message: String, + val statusCode: Int + ) : ConfirmResetPasswordWithCode(message = message) + + class UnauthorizedWrongConfirmationCode( + override val message: String, + val remainingAttempts:Int, + val lockDuration:Long, + val statusCode: Int + ):ConfirmResetPasswordWithCode(message = message) + + class InternalServerErrorFailedToConfirmResetPassword( + override val message: String, + val statusCode: Int + ) : ConfirmResetPasswordWithCode(message = message) + + } + + sealed class ConfirmEmailToResetPassword( + override val message: String + ):ResetPasswordException(message){ + class BadRequestInvalidInputOrContentType( + override val message: String, + val statusCode: Int + ) : ConfirmEmailToResetPassword(message = message) + + class InternalServerErrorFailedToGenerateTokenOrSendEmail( + override val message: String, + val statusCode: Int + ) : ConfirmEmailToResetPassword(message= message) + } + + sealed class SaveNewPassword( + message: String + ):ResetPasswordException(message){ + class BadRequestInvalidPasswordOrMissingContentTypeHeader( + override val message: String, + val statusCode: Int + ) : SaveNewPassword(message = message) + + class UnauthorizedInvalidTokenOrMissingContentTypeHeader( + override val message: String, + val statusCode: Int + ) : SaveNewPassword(message = message) + + class InternalServerErrorFailedToResetPassword( + override val message: String, + val statusCode: Int + ) : SaveNewPassword(message= message) + + } + + companion object { + const val NO_INTERNET_CONNECTION = "No internet connection" + const val UNDEFINED_MESSAGE = "Undefined" + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_password_recovery.xml b/app/src/main/res/layout/fragment_password_recovery.xml index d47c3e72..ced33311 100644 --- a/app/src/main/res/layout/fragment_password_recovery.xml +++ b/app/src/main/res/layout/fragment_password_recovery.xml @@ -1,30 +1,257 @@ - + + + + app:layout_constraintTop_toBottomOf="@id/custom_steps" /> + + + + + + + + + + +