Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
1804a22
ANDR-75: Создание модуля forgot password
2olka Jan 29, 2026
f58bd14
ANDR-75: Верстка модального окна
2olka Feb 5, 2026
e1c82b2
ANDR-75: Добавление таймера на модальное окно
2olka Feb 5, 2026
2993e68
ANDR-75: Верстка экрана забыли пароль
2olka Feb 6, 2026
9f2a6b5
ANDR-75: Вынос текста в стороковые ресурсы
2olka Feb 12, 2026
0eaaa8f
ANDR-75: presentation and finished ui
2olka Feb 12, 2026
0dfef85
ANDR-75: domain
2olka Feb 12, 2026
31e64a0
ANDR-75: data
2olka Feb 12, 2026
1fd0fab
ANDR-75: di
2olka Feb 12, 2026
e669e46
ANDR-75: ui
2olka Feb 12, 2026
6b21161
ANDR-75: Настройка отступов и цветов для PixelPerfect
2olka Feb 16, 2026
28d142f
ANDR-75: Настройка отступов и цветов в ModalWindow для PixelPerfect
2olka Feb 16, 2026
ea3533b
ANDR-75: исправление ошибок KtLint
2olka Feb 16, 2026
774c101
ANDR-75: исправление ошибок KtLint
2olka Feb 16, 2026
c5f71df
ANDR-75: исправление ошибок KtLint
2olka Feb 16, 2026
c07500c
ANDR-75: исправление ошибок Detect
2olka Feb 16, 2026
9f36d36
ANDR-75: исправление ошибок KtLint
2olka Feb 16, 2026
ccb606b
ANDR-75: исправление ошибок KtLint
2olka Feb 16, 2026
b6d2736
ANDR-75: исправление ошибок KtLint
2olka Feb 16, 2026
2735cad
ANDR-75: исправление ошибок KtLint
2olka Feb 16, 2026
d26ec9a
ANDR-75: исправление ошибок Detect
2olka Feb 16, 2026
8ef0512
ANDR-75: исправление замечаний
2olka Mar 10, 2026
e6a3556
Merge branch 'epic/ANDR-81' into feature/ANDR-75
2olka Mar 11, 2026
23a2ec4
ANDR-75: исправление замечаний Ktlint
2olka Mar 12, 2026
c8db0f1
ANDR-75: исправление замечаний Detect
2olka Mar 12, 2026
3287b33
ANDR-75: исправление замечаний Detect
2olka Mar 12, 2026
d23dec9
ANDR-75: исправление замечаний Detect
2olka Mar 12, 2026
a17515c
Исправление замечаний
2olka Mar 16, 2026
5f5269b
Merge branch 'epic/ANDR-81' into feature/ANDR-75
2olka Mar 16, 2026
0af4f77
Исправление замечаний
2olka Mar 16, 2026
8429541
Исправление замечаний
2olka Mar 16, 2026
0b87b98
Merge remote-tracking branch 'origin/feature/ANDR-75' into feature/AN…
2olka Mar 16, 2026
b017692
Исправление замечаний
2olka Mar 16, 2026
c632b36
Исправление замечаний
2olka Mar 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .idea/compiler.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions feature/forgot-password/impl/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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<EmailValidator> { EmailValidatorImpl() }
factory { ForgotPasswordScreenMapper(emailValidator = get()) }

viewModel {
ForgotPasswordViewModel(
forgotPasswordScreenMapper = get(),
sendResetLinkUseCase = get()
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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>(
ForgotPasswordState.Content(
email = "",
isLoading = false,
error = null,
emailValidationError = "",
emailValidationError = null,
isSuccessDialogVisible = false,
)
)

private val mutableUiState =
MutableStateFlow<ForgotPasswordScreenState>(ForgotPasswordScreenState.Initial)
val uiState: StateFlow<ForgotPasswordScreenState> = mutableUiState.asStateFlow()
val uiState: StateFlow<ForgotPasswordScreenState> = mutableState
.mapLatest { state -> forgotPasswordScreenMapper.getScreenState(state) }
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(STOP_TIMEOUT_MILLIS),
initialValue = ForgotPasswordScreenState.Initial
)

private val mutableCommands =
MutableSharedFlow<ForgotPasswordCommand>()
val commands: SharedFlow<ForgotPasswordCommand> = 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()
Expand All @@ -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,
)
}
}
Expand All @@ -77,30 +81,41 @@ 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)
}

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))
Expand All @@ -120,4 +135,11 @@ class ForgotPasswordViewModel(
mutableCommands.emit(command)
}
}
}

private val ForgotPasswordState.email: String
get() = when (this) {
is ForgotPasswordState.Content -> email
is ForgotPasswordState.Loading -> email
is ForgotPasswordState.Error -> email
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
)
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
}
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package ru.yeahub.impl.presentation.validator

interface EmailValidator {
fun validate(email: String): EmailValidationResult
fun isValid(email: String): Boolean
}
Original file line number Diff line number Diff line change
@@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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 ->
Expand All @@ -28,6 +28,6 @@ fun ForgotPasswordRoute(

ForgotPasswordScreen(
state = state,
onEvent = viewModel::handleEvents
onEvent = viewModel::onEvent
)
}
Loading
Loading