diff --git a/.idea/compiler.xml b/.idea/compiler.xml index 76b52f20..dd57af9e 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -2,6 +2,7 @@ + diff --git a/feature/forgot-password/impl/build.gradle.kts b/feature/forgot-password/impl/build.gradle.kts index 290ccdc7..aa9c142d 100644 --- a/feature/forgot-password/impl/build.gradle.kts +++ b/feature/forgot-password/impl/build.gradle.kts @@ -49,6 +49,9 @@ dependencies { implementation(libs.androidx.compose.material3) implementation(libs.timber) implementation(project(":core:ui")) + implementation(libs.koin.core) + implementation(libs.koin.android) + implementation(libs.koin.compose) // Compose Preview dependencies implementation(libs.androidx.ui.tooling.preview) diff --git a/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/di/ForgotPasswordFactory.kt b/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/di/ForgotPasswordFactory.kt deleted file mode 100644 index f7202c10..00000000 --- a/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/di/ForgotPasswordFactory.kt +++ /dev/null @@ -1,23 +0,0 @@ -package ru.yeahub.impl.di - -import ru.yeahub.impl.data.mapper.ForgotPasswordMapper -import ru.yeahub.impl.data.remote.AuthApi -import ru.yeahub.impl.data.repository.ForgotPasswordRepositoryImpl -import ru.yeahub.impl.domain.SendResetLinkUseCase -import ru.yeahub.impl.presentation.ForgotPasswordViewModel -import ru.yeahub.impl.presentation.mapper.EmailValidator -import ru.yeahub.impl.presentation.mapper.ForgotPasswordScreenMapper - -object ForgotPasswordFactory { - - fun createViewModel( - api: AuthApi, - mapper: ForgotPasswordMapper, - emailValidator: EmailValidator, - ): ForgotPasswordViewModel { - val forgotPasswordRepository = ForgotPasswordRepositoryImpl(api, mapper) - val sendResetLinkUseCase = SendResetLinkUseCase(forgotPasswordRepository) - val forgotPasswordScreenMapper = ForgotPasswordScreenMapper(emailValidator) - return ForgotPasswordViewModel(forgotPasswordScreenMapper, sendResetLinkUseCase) - } -} \ No newline at end of file diff --git a/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/di/ForgotPasswordViewModelModule.kt b/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/di/ForgotPasswordViewModelModule.kt new file mode 100644 index 00000000..b28ab660 --- /dev/null +++ b/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/di/ForgotPasswordViewModelModule.kt @@ -0,0 +1,20 @@ +package ru.yeahub.impl.di + +import org.koin.androidx.viewmodel.dsl.viewModel +import org.koin.dsl.module +import ru.yeahub.impl.presentation.ForgotPasswordViewModel +import ru.yeahub.impl.presentation.validator.EmailValidator +import ru.yeahub.impl.presentation.validator.EmailValidatorImpl +import ru.yeahub.impl.presentation.mapper.ForgotPasswordScreenMapper + +internal val forgotPasswordViewModelModule = module { + factory { EmailValidatorImpl() } + factory { ForgotPasswordScreenMapper(emailValidator = get()) } + + viewModel { + ForgotPasswordViewModel( + forgotPasswordScreenMapper = get(), + sendResetLinkUseCase = get() + ) + } +} \ No newline at end of file diff --git a/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/presentation/ForgotPasswordViewModel.kt b/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/presentation/ForgotPasswordViewModel.kt index 346a31fc..909178d3 100644 --- a/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/presentation/ForgotPasswordViewModel.kt +++ b/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/presentation/ForgotPasswordViewModel.kt @@ -2,11 +2,14 @@ package ru.yeahub.impl.presentation import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import ru.yeahub.impl.domain.ForgotPasswordResult @@ -17,38 +20,38 @@ import ru.yeahub.impl.presentation.mapper.ForgotPasswordScreenMapper import ru.yeahub.impl.presentation.state.ForgotPasswordScreenState import ru.yeahub.impl.presentation.state.ForgotPasswordState +@OptIn(ExperimentalCoroutinesApi::class) class ForgotPasswordViewModel( private val forgotPasswordScreenMapper: ForgotPasswordScreenMapper, private val sendResetLinkUseCase: SendResetLinkUseCase ) : ViewModel() { - private val mutableState = MutableStateFlow( - ForgotPasswordState( + private companion object { + const val STOP_TIMEOUT_MILLIS = 5_000L + } + + private val mutableState = + MutableStateFlow( + ForgotPasswordState.Content( email = "", - isLoading = false, - error = null, - emailValidationError = "", + emailValidationError = null, isSuccessDialogVisible = false, ) ) - private val mutableUiState = - MutableStateFlow(ForgotPasswordScreenState.Initial) - val uiState: StateFlow = mutableUiState.asStateFlow() + val uiState: StateFlow = mutableState + .mapLatest { state -> forgotPasswordScreenMapper.getScreenState(state) } + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(STOP_TIMEOUT_MILLIS), + initialValue = ForgotPasswordScreenState.Initial + ) private val mutableCommands = MutableSharedFlow() val commands: SharedFlow = mutableCommands - init { - viewModelScope.launch { - mutableState.collect { state -> - mutableUiState.value = forgotPasswordScreenMapper.getScreenState(state) - } - } - } - - fun handleEvents(event: ForgotPasswordEvent) { + fun onEvent(event: ForgotPasswordEvent) { when (event) { is ForgotPasswordEvent.EmailChanged -> onEmailChanged(event.value) is ForgotPasswordEvent.SubmitClicked -> onSubmit() @@ -57,16 +60,17 @@ class ForgotPasswordViewModel( } private fun onEmailChanged(value: String) { - mutableState.update { currentState -> - val validationError = if (value.isNotBlank()) { - forgotPasswordScreenMapper.validateEmail(value) - } else { - null - } - currentState.copy( + val validationError = if (value.isNotBlank()) { + forgotPasswordScreenMapper.validateEmail(value) + } else { + null + } + + mutableState.update { + ForgotPasswordState.Content( email = value, emailValidationError = validationError, - error = null + isSuccessDialogVisible = false, ) } } @@ -77,20 +81,30 @@ class ForgotPasswordViewModel( if (!forgotPasswordScreenMapper.canSubmit(email)) { mutableState.update { - it.copy(emailValidationError = forgotPasswordScreenMapper.validateEmail(email)) + ForgotPasswordState.Content( + email = email, + emailValidationError = forgotPasswordScreenMapper.validateEmail(email), + isSuccessDialogVisible = false, + ) } return } - mutableState.update { it.copy(isLoading = true, error = null) } + mutableState.update { + ForgotPasswordState.Loading( + email = email, + emailValidationError = null, + ) + } viewModelScope.launch { when (val result = sendResetLinkUseCase(email)) { is ForgotPasswordResult.Success -> { mutableState.update { - it.copy( - isLoading = false, - isSuccessDialogVisible = true + ForgotPasswordState.Content( + email = email, + emailValidationError = null, + isSuccessDialogVisible = true, ) } emitCommand(ForgotPasswordCommand.NavigateToCheckEmail) @@ -98,9 +112,10 @@ class ForgotPasswordViewModel( is ForgotPasswordResult.Error -> { mutableState.update { - it.copy( - isLoading = false, - error = result.message + ForgotPasswordState.Error( + email = email, + error = result.message, + emailValidationError = null, ) } emitCommand(ForgotPasswordCommand.ShowSnackbar(result.message)) @@ -120,4 +135,11 @@ class ForgotPasswordViewModel( mutableCommands.emit(command) } } -} \ No newline at end of file + + private val ForgotPasswordState.email: String + get() = when (this) { + is ForgotPasswordState.Content -> email + is ForgotPasswordState.Loading -> email + is ForgotPasswordState.Error -> email + } +} diff --git a/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/presentation/mapper/ForgotPasswordScreenMapper.kt b/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/presentation/mapper/ForgotPasswordScreenMapper.kt index 8dd1cbac..b628e7c6 100644 --- a/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/presentation/mapper/ForgotPasswordScreenMapper.kt +++ b/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/presentation/mapper/ForgotPasswordScreenMapper.kt @@ -2,26 +2,37 @@ package ru.yeahub.impl.presentation.mapper import ru.yeahub.impl.presentation.state.ForgotPasswordScreenState import ru.yeahub.impl.presentation.state.ForgotPasswordState +import ru.yeahub.impl.presentation.validator.EmailValidationResult +import ru.yeahub.impl.presentation.validator.EmailValidator class ForgotPasswordScreenMapper( private val emailValidator: EmailValidator, ) { fun getScreenState(state: ForgotPasswordState): ForgotPasswordScreenState { - return when { - state.error != null -> { + return when (state) { + is ForgotPasswordState.Error -> { ForgotPasswordScreenState.Error(message = state.error) } - state.email.isBlank() && state.emailValidationError == null -> { - ForgotPasswordScreenState.Initial + is ForgotPasswordState.Content -> { + if (state.email.isBlank() && state.emailValidationError == null) { + ForgotPasswordScreenState.Initial + } else { + ForgotPasswordScreenState.Content( + email = state.email, + isLoading = false, + emailError = state.emailValidationError, + isSent = state.isSuccessDialogVisible, + ) + } } - else -> { + is ForgotPasswordState.Loading -> { ForgotPasswordScreenState.Content( email = state.email, - isLoading = state.isLoading, + isLoading = true, emailError = state.emailValidationError, - isSent = state.isSuccessDialogVisible, + isSent = false, ) } } diff --git a/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/presentation/state/ForgotPasswordState.kt b/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/presentation/state/ForgotPasswordState.kt index 78486f01..9e3906fd 100644 --- a/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/presentation/state/ForgotPasswordState.kt +++ b/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/presentation/state/ForgotPasswordState.kt @@ -1,9 +1,20 @@ package ru.yeahub.impl.presentation.state -data class ForgotPasswordState( - val email: String, - val isLoading: Boolean, - val error: String?, - val emailValidationError: String?, - val isSuccessDialogVisible: Boolean, -) \ No newline at end of file +sealed interface ForgotPasswordState { + data class Content( + val email: String, + val emailValidationError: String?, + val isSuccessDialogVisible: Boolean, + ) : ForgotPasswordState + + data class Loading( + val email: String, + val emailValidationError: String?, + ) : ForgotPasswordState + + data class Error( + val email: String, + val error: String, + val emailValidationError: String?, + ) : ForgotPasswordState +} \ No newline at end of file diff --git a/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/presentation/validator/EmailValidationResult.kt b/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/presentation/validator/EmailValidationResult.kt new file mode 100644 index 00000000..f87b3977 --- /dev/null +++ b/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/presentation/validator/EmailValidationResult.kt @@ -0,0 +1,7 @@ +package ru.yeahub.impl.presentation.validator + +sealed interface EmailValidationResult { + data object Valid : EmailValidationResult + data object Empty : EmailValidationResult + data class Invalid(val errorMessage: String) : EmailValidationResult +} \ No newline at end of file diff --git a/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/presentation/validator/EmailValidator.kt b/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/presentation/validator/EmailValidator.kt new file mode 100644 index 00000000..f7bfeee6 --- /dev/null +++ b/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/presentation/validator/EmailValidator.kt @@ -0,0 +1,6 @@ +package ru.yeahub.impl.presentation.validator + +interface EmailValidator { + fun validate(email: String): EmailValidationResult + fun isValid(email: String): Boolean +} \ No newline at end of file diff --git a/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/presentation/validator/EmailValidatorImpl.kt b/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/presentation/validator/EmailValidatorImpl.kt new file mode 100644 index 00000000..b78b7c86 --- /dev/null +++ b/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/presentation/validator/EmailValidatorImpl.kt @@ -0,0 +1,23 @@ +package ru.yeahub.impl.presentation.validator + +import android.util.Patterns + +class EmailValidatorImpl : EmailValidator { + + override fun validate(email: String): EmailValidationResult { + val trimmedEmail = email.trim() + + return when { + trimmedEmail.isBlank() -> EmailValidationResult.Empty + !Patterns.EMAIL_ADDRESS.matcher(trimmedEmail).matches() -> { + EmailValidationResult.Invalid("Введите корректный email") + } + + else -> EmailValidationResult.Valid + } + } + + override fun isValid(email: String): Boolean { + return validate(email) is EmailValidationResult.Valid + } +} \ No newline at end of file diff --git a/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/ui/ForgotPasswordRoute.kt b/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/ui/ForgotPasswordRoute.kt index 7264ad8e..c07c1acc 100644 --- a/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/ui/ForgotPasswordRoute.kt +++ b/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/ui/ForgotPasswordRoute.kt @@ -2,8 +2,8 @@ package ru.yeahub.impl.ui import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.lifecycle.compose.collectAsStateWithLifecycle import ru.yeahub.impl.presentation.ForgotPasswordViewModel import ru.yeahub.impl.presentation.intents.ForgotPasswordCommand @@ -14,7 +14,7 @@ fun ForgotPasswordRoute( onCheckEmail: () -> Unit, showSnackbar: suspend (String) -> Unit ) { - val state by viewModel.uiState.collectAsState() + val state by viewModel.uiState.collectAsStateWithLifecycle() LaunchedEffect(Unit) { viewModel.commands.collect { command -> @@ -28,6 +28,6 @@ fun ForgotPasswordRoute( ForgotPasswordScreen( state = state, - onEvent = viewModel::handleEvents + onEvent = viewModel::onEvent ) } \ No newline at end of file diff --git a/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/ui/ForgotPasswordScreen.kt b/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/ui/ForgotPasswordScreen.kt index e8e55718..42d4acf0 100644 --- a/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/ui/ForgotPasswordScreen.kt +++ b/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/ui/ForgotPasswordScreen.kt @@ -58,22 +58,15 @@ fun ForgotPasswordScreen( Spacer(Modifier.height(6.dp)) - val email = when (state) { - is ForgotPasswordScreenState.Content -> state.email - else -> "" - } - val isLoading = when (state) { - is ForgotPasswordScreenState.Content -> state.isLoading - else -> false - } - val emailError = when (state) { - is ForgotPasswordScreenState.Content -> state.emailError + val contentState = when (state) { + is ForgotPasswordScreenState.Content -> state else -> null } - val isEmailValid = when (state) { - is ForgotPasswordScreenState.Content -> state.isEmailValid - else -> false - } + + val email = contentState?.email.orEmpty() + val isLoading = contentState?.isLoading ?: false + val emailError = contentState?.emailError + val isEmailValid = contentState?.isEmailValid ?: false DefaultTextField( value = email, @@ -149,7 +142,7 @@ fun ForgotPasswordScreenPreview_Valid() { fun ForgotPasswordScreenPreview_Invalid() { ForgotPasswordScreen( state = ForgotPasswordScreenState.Content( - email = "invalid-email", + email = "invalid email", isLoading = false, emailError = "Введите корректный email", isSent = false