From 03206bca143d2a4741b2aa06c2fd5126b09dbc99 Mon Sep 17 00:00:00 2001 From: MODDII Date: Thu, 7 May 2026 15:15:34 +0300 Subject: [PATCH 01/14] =?UTF-8?q?ANDR/51:=20=D0=BD=D0=B0=D1=81=D1=82=D1=80?= =?UTF-8?q?=D0=BE=D0=B9=D0=BA=D0=B0=20=D1=81=D0=B5=D1=82=D0=B5=D0=B2=D0=BE?= =?UTF-8?q?=D0=B3=D0=BE=20=D1=81=D0=BB=D0=BE=D1=8F=20=D0=B8=20=D0=B2=D0=BD?= =?UTF-8?q?=D0=B5=D0=B4=D1=80=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=B7=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D1=81=D0=B8=D0=BC=D0=BE=D1=81=D1=82=D0=B5=D0=B9=20=D0=B4?= =?UTF-8?q?=D0=BB=D1=8F=20=D0=BC=D0=BE=D0=B4=D1=83=D0=BB=D1=8F=20=D0=B0?= =?UTF-8?q?=D1=83=D1=82=D0=B5=D0=BD=D1=82=D0=B8=D1=84=D0=B8=D0=BA=D0=B0?= =?UTF-8?q?=D1=86=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle.kts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 207c2966..1a061bd4 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -109,6 +109,8 @@ dependencies { implementation(project(":feature:questions-or-collections:impl")) implementation(project(":feature:public-collections:impl")) implementation(project(":feature:selection-specializations:impl")) + implementation(project(":feature:authentication:api")) + implementation(project(":feature:authentication:impl")) } tasks.withType { From c6d98773b679ca813a7eed16ecf4fa2eb00e6a96 Mon Sep 17 00:00:00 2001 From: MODDII Date: Thu, 7 May 2026 15:15:40 +0300 Subject: [PATCH 02/14] =?UTF-8?q?ANDR/51:=20=D1=80=D0=B5=D0=B3=D0=B8=D1=81?= =?UTF-8?q?=D1=82=D1=80=D0=B0=D1=86=D0=B8=D1=8F=20=D0=BC=D0=B0=D1=80=D1=88?= =?UTF-8?q?=D1=80=D1=83=D1=82=D0=BE=D0=B2=20=D0=B8=20=D0=BD=D0=B0=D1=81?= =?UTF-8?q?=D1=82=D1=80=D0=BE=D0=B9=D0=BA=D0=B0=20=D0=BD=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=B3=D0=B0=D1=86=D0=B8=D0=B8=20=D0=B4=D0=BB=D1=8F=20=D1=8D?= =?UTF-8?q?=D0=BA=D1=80=D0=B0=D0=BD=D0=B0=20=D1=80=D0=B5=D0=B3=D0=B8=D1=81?= =?UTF-8?q?=D1=82=D1=80=D0=B0=D1=86=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/ru/yeahub/navigation_api/FeatureRoute.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/navigation-api/src/main/java/ru/yeahub/navigation_api/FeatureRoute.kt b/core/navigation-api/src/main/java/ru/yeahub/navigation_api/FeatureRoute.kt index 446c60e6..a2436fe2 100644 --- a/core/navigation-api/src/main/java/ru/yeahub/navigation_api/FeatureRoute.kt +++ b/core/navigation-api/src/main/java/ru/yeahub/navigation_api/FeatureRoute.kt @@ -62,4 +62,7 @@ object FeatureRoute { object PublicCollectionsFeature { const val FEATURE_NAME = "public_collections" } + object RegistrationFeature { + const val FEATURE_NAME = "registration" + } } \ No newline at end of file From 71fb6ff2ee3502c83f9fa7ccdce64f588d7de9cb Mon Sep 17 00:00:00 2001 From: MODDII Date: Tue, 9 Jun 2026 18:44:41 +0300 Subject: [PATCH 03/14] =?UTF-8?q?ANDR-51:=20=D0=92=D1=80=D0=B5=D0=BC=D0=B5?= =?UTF-8?q?=D0=BD=D0=BD=D0=BE=D0=B5=20=D0=B8=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BC=D0=BE=D0=B4=D1=83=D0=BB?= =?UTF-8?q?=D1=8F=20forgot-password=20=D0=B8=20=D1=83=D0=B4=D0=B0=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BB=D0=BE=D0=BA=D0=B0=D0=BB=D1=8C?= =?UTF-8?q?=D0=BD=D1=8B=D1=85=20=D0=BD=D0=B0=D1=81=D1=82=D1=80=D0=BE=D0=B5?= =?UTF-8?q?=D0=BA=20IDE.=20=D0=97=D0=B0=D0=BA=D0=BE=D0=BC=D0=BC=D0=B5?= =?UTF-8?q?=D0=BD=D1=82=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D1=8B=20=D0=B2?= =?UTF-8?q?=D1=8B=D0=B7=D0=BE=D0=B2=D1=8B=20=D1=83=D0=B4=D0=B0=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=BD=D1=8B=D1=85=20UI-=D0=BA=D0=BE=D0=BC=D0=BF=D0=BE?= =?UTF-8?q?=D0=BD=D0=B5=D0=BD=D1=82=D0=BE=D0=B2=20=D0=B4=D0=BB=D1=8F=20?= =?UTF-8?q?=D0=BE=D0=B1=D0=B5=D1=81=D0=BF=D0=B5=D1=87=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D1=8F=20=D1=83=D1=81=D0=BF=D0=B5=D1=88=D0=BD=D0=BE=D0=B9=20?= =?UTF-8?q?=D1=81=D0=B1=D0=BE=D1=80=D0=BA=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/compiler.xml | 9 ------ .../ru/yeahub/impl/ui/ForgotPasswordRoute.kt | 14 ++++----- .../ru/yeahub/impl/ui/ForgotPasswordScreen.kt | 30 +++++++++---------- 3 files changed, 22 insertions(+), 31 deletions(-) delete mode 100644 .idea/compiler.xml diff --git a/.idea/compiler.xml b/.idea/compiler.xml deleted file mode 100644 index 76b52f20..00000000 --- a/.idea/compiler.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ 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..b675bd9f 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.compose.runtime.collectAsState +//import androidx.compose.runtime.getValue 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.collectAsState() LaunchedEffect(Unit) { viewModel.commands.collect { command -> @@ -26,8 +26,8 @@ fun ForgotPasswordRoute( } } - ForgotPasswordScreen( - state = state, - onEvent = viewModel::handleEvents - ) +// ForgotPasswordScreen( +// state = state, +// onEvent = viewModel::handleEvents +// ) } \ 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..0fd80a2c 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 @@ -17,8 +17,8 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import ru.yeahub.core_ui.component.PrimaryButton import ru.yeahub.core_ui.component.YeahubButtonColors -import ru.yeahub.core_ui.component.textInput.DefaultTextField -import ru.yeahub.core_ui.component.textInput.TextInputColorsDefaults +//import ru.yeahub.core_ui.component.textInput.DefaultTextField +//import ru.yeahub.core_ui.component.textInput.TextInputColorsDefaults import ru.yeahub.core_ui.theme.Theme import ru.yeahub.impl.R import ru.yeahub.impl.presentation.intents.ForgotPasswordEvent @@ -75,19 +75,19 @@ fun ForgotPasswordScreen( else -> false } - DefaultTextField( - value = email, - onValueChange = { onEvent(ForgotPasswordEvent.EmailChanged(it)) }, - label = stringResource(R.string.enter_email), - isEnabled = !isLoading, - isError = emailError != null, - showLeadingIcon = false, - onExpandedChange = { }, - modifier = Modifier - .fillMaxWidth() - .height(52.dp), - colors = TextInputColorsDefaults.defaultColors() - ) +// DefaultTextField( +// value = email, +// onValueChange = { onEvent(ForgotPasswordEvent.EmailChanged(it)) }, +// label = stringResource(R.string.enter_email), +// isEnabled = !isLoading, +// isError = emailError != null, +// showLeadingIcon = false, +// onExpandedChange = { }, +// modifier = Modifier +// .fillMaxWidth() +// .height(52.dp), +//// colors = TextInputColorsDefaults.defaultColors() +// ) if (emailError != null) { Spacer(Modifier.height(4.dp)) From 0edfb9975df82aad07909d2aa96c1d5dcf63e72a Mon Sep 17 00:00:00 2001 From: MODDII Date: Tue, 9 Jun 2026 18:45:19 +0300 Subject: [PATCH 04/14] =?UTF-8?q?ANDR-51:=20=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=B1=D0=B0=D0=B7=D0=BE=D0=B2?= =?UTF-8?q?=D1=8B=D1=85=20=D0=BA=D0=BE=D0=BC=D0=BF=D0=BE=D0=BD=D0=B5=D0=BD?= =?UTF-8?q?=D1=82=D0=BE=D0=B2=20Button.=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=20=D0=BF=D0=B0=D1=80=D0=B0=D0=BC=D0=B5=D1=82?= =?UTF-8?q?=D1=80=20isLoading=20=D0=B4=D0=BB=D1=8F=20=D0=BE=D1=82=D0=BE?= =?UTF-8?q?=D0=B1=D1=80=D0=B0=D0=B6=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=B8=D0=BD?= =?UTF-8?q?=D0=B4=D0=B8=D0=BA=D0=B0=D1=82=D0=BE=D1=80=D0=B0=20=D0=B7=D0=B0?= =?UTF-8?q?=D0=B3=D1=80=D1=83=D0=B7=D0=BA=D0=B8=20=D0=B8=20=D0=B0=D0=B2?= =?UTF-8?q?=D1=82=D0=BE=D0=BC=D0=B0=D1=82=D0=B8=D1=87=D0=B5=D1=81=D0=BA?= =?UTF-8?q?=D0=BE=D0=B9=20=D0=B1=D0=BB=D0=BE=D0=BA=D0=B8=D1=80=D0=BE=D0=B2?= =?UTF-8?q?=D0=BA=D0=B8=20=D0=BA=D0=BD=D0=BE=D0=BF=D0=BE=D0=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ru/yeahub/core_ui/component/Button.kt | 58 ++++++++++++------- 1 file changed, 38 insertions(+), 20 deletions(-) diff --git a/core/ui/src/main/java/ru/yeahub/core_ui/component/Button.kt b/core/ui/src/main/java/ru/yeahub/core_ui/component/Button.kt index 537fb1f8..89ee389b 100644 --- a/core/ui/src/main/java/ru/yeahub/core_ui/component/Button.kt +++ b/core/ui/src/main/java/ru/yeahub/core_ui/component/Button.kt @@ -9,8 +9,10 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface @@ -37,23 +39,25 @@ fun PrimaryButton( onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, + isLoading: Boolean = false, colors: YeahubButtonColors = YeahubButtonDefaults.primaryButtonColors(), border: BorderStroke? = null, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, shape: Shape = RoundedCornerShape(12.dp), contentPadding: PaddingValues = ButtonDefaults.ContentPadding, - content: @Composable RowScope.() -> Unit + content: @Composable RowScope.() -> Unit, ) { DefaultButton( onClick = onClick, modifier = modifier, enabled = enabled, + isLoading = isLoading, colors = colors, border = border, interactionSource = interactionSource, shape = shape, contentPadding = contentPadding, - content = content, + content = content ) } @@ -62,17 +66,19 @@ fun SecondaryButton( onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, + isLoading: Boolean = false, colors: YeahubButtonColors = YeahubButtonDefaults.secondaryButtonColors(), border: BorderStroke? = null, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, shape: Shape = RoundedCornerShape(12.dp), contentPadding: PaddingValues = ButtonDefaults.ContentPadding, - content: @Composable RowScope.() -> Unit + content: @Composable RowScope.() -> Unit, ) { DefaultButton( onClick = onClick, modifier = modifier, enabled = enabled, + isLoading = isLoading, colors = colors, border = border, interactionSource = interactionSource, @@ -87,6 +93,7 @@ fun OutlineButton( onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, + isLoading: Boolean = false, colors: YeahubButtonColors = YeahubButtonDefaults.outlinedButtonColors(), border: BorderStroke = YeahubButtonDefaults.outlineBorderDefaults(), interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, @@ -98,12 +105,13 @@ fun OutlineButton( onClick = onClick, modifier = modifier, enabled = enabled, + isLoading = isLoading, colors = colors, border = border, interactionSource = interactionSource, shape = shape, contentPadding = contentPadding, - content = content, + content = content ) } @@ -112,6 +120,7 @@ private fun DefaultButton( onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, + isLoading: Boolean = false, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, colors: YeahubButtonColors = YeahubButtonDefaults.primaryButtonColors(), border: BorderStroke? = null, @@ -119,18 +128,18 @@ private fun DefaultButton( contentPadding: PaddingValues = ButtonDefaults.ContentPadding, content: @Composable RowScope.() -> Unit ) { - val contentColor: Color by colors.contentColor(enabled) - val containerColor: Color by colors.containerColor(enabled) + val contentColor: Color by colors.contentColor(enabled && !isLoading) + val containerColor: Color by colors.containerColor(enabled && !isLoading) Surface( onClick = onClick, modifier = modifier, - enabled = enabled, + enabled = enabled && !isLoading, shape = shape, color = containerColor, contentColor = contentColor, border = border, - interactionSource = interactionSource, + interactionSource = interactionSource ) { CompositionLocalProvider( value = LocalContentColor provides contentColor @@ -139,9 +148,18 @@ private fun DefaultButton( modifier = Modifier .padding(contentPadding), horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically, - content = content, - ) + verticalAlignment = Alignment.CenterVertically + ) { + if (isLoading) { + CircularProgressIndicator( + modifier = Modifier.size(20.dp), + color = contentColor, + strokeWidth = 2.dp + ) + } else { + content() + } + } } } } @@ -158,7 +176,7 @@ object YeahubButtonDefaults { contentColor = contentColor, containerColor = containerColor, disabledContentColor = disabledContentColor, - disabledContainerColor = disabledContainerColor, + disabledContainerColor = disabledContainerColor ) } @@ -173,7 +191,7 @@ object YeahubButtonDefaults { contentColor = contentColor, containerColor = containerColor, disabledContentColor = disabledContentColor, - disabledContainerColor = disabledContainerColor, + disabledContainerColor = disabledContainerColor ) } @@ -188,7 +206,7 @@ object YeahubButtonDefaults { contentColor = contentColor, containerColor = containerColor, disabledContentColor = disabledContentColor, - disabledContainerColor = disabledContainerColor, + disabledContainerColor = disabledContainerColor ) } @@ -203,7 +221,7 @@ object YeahubButtonDefaults { contentColor = contentColor, containerColor = containerColor, disabledContentColor = disabledContentColor, - disabledContainerColor = disabledContainerColor, + disabledContainerColor = disabledContainerColor ) } @@ -212,13 +230,13 @@ object YeahubButtonDefaults { contentColor: Color = Theme.colors.red600, containerColor: Color = Color.Transparent, disabledContentColor: Color = Theme.colors.red200, - disabledContainerColor: Color = Color.Transparent + disabledContainerColor: Color = Color.Transparent, ): YeahubButtonColors { return YeahubButtonColors( contentColor = contentColor, containerColor = containerColor, disabledContentColor = disabledContentColor, - disabledContainerColor = disabledContainerColor, + disabledContainerColor = disabledContainerColor ) } @@ -229,7 +247,7 @@ object YeahubButtonDefaults { ): BorderStroke { return BorderStroke( width = width, - color = borderColor, + color = borderColor ) } } @@ -239,7 +257,7 @@ data class YeahubButtonColors( private val contentColor: Color, private val containerColor: Color, private val disabledContentColor: Color, - private val disabledContainerColor: Color + private val disabledContainerColor: Color, ) : ButtonColors { @Composable override fun containerColor(enabled: Boolean): State { @@ -290,7 +308,7 @@ fun ButtonPreviews() { } // Secondary Button - Text("Secondary Buttons", style = MaterialTheme.typography.titleMedium) + Text("Secondary Buttons", style = MaterialTheme.typography.titleMedium) SecondaryButton( onClick = {}, modifier = Modifier.fillMaxWidth(), From b54281314f9a8bd1a53eb755c1bfd7edb6e830fd Mon Sep 17 00:00:00 2001 From: MODDII Date: Tue, 9 Jun 2026 18:46:26 +0300 Subject: [PATCH 05/14] =?UTF-8?q?ANDR-51:=20=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20=D1=81=D0=B5=D1=82=D0=B5=D0=B2?= =?UTF-8?q?=D1=8B=D1=85=20=D0=B8=D0=BD=D1=82=D0=B5=D1=80=D1=84=D0=B5=D0=B9?= =?UTF-8?q?=D1=81=D0=BE=D0=B2=20=D0=B8=20=D1=80=D0=B5=D1=81=D1=83=D1=80?= =?UTF-8?q?=D1=81=D0=BE=D0=B2=20=D0=BD=D0=B0=D0=B2=D0=B8=D0=B3=D0=B0=D1=86?= =?UTF-8?q?=D0=B8=D0=B8.=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=20POST-=D0=BC=D0=B5=D1=82=D0=BE=D0=B4=20=D0=B4=D0=BB?= =?UTF-8?q?=D1=8F=20=D1=80=D0=B5=D0=B3=D0=B8=D1=81=D1=82=D1=80=D0=B0=D1=86?= =?UTF-8?q?=D0=B8=D0=B8=20=D0=B8=20=D0=B8=D0=BA=D0=BE=D0=BD=D0=BA=D0=B0=20?= =?UTF-8?q?=D0=BF=D1=80=D0=BE=D1=84=D0=B8=D0=BB=D1=8F=20=D0=B4=D0=BB=D1=8F?= =?UTF-8?q?=20=D0=BD=D0=B8=D0=B6=D0=BD=D0=B5=D0=B9=20=D0=BD=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D0=B3=D0=B0=D1=86=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/res/drawable/icon_tab_profile.xml | 14 ++++++++++++++ .../network_api/models/RegistrationRequestDto.kt | 3 +-- .../ru/yeahub/network_impl/RetrofitApiService.kt | 15 +++++++++++++++ 3 files changed, 30 insertions(+), 2 deletions(-) create mode 100644 core/navigation-impl/src/main/res/drawable/icon_tab_profile.xml diff --git a/core/navigation-impl/src/main/res/drawable/icon_tab_profile.xml b/core/navigation-impl/src/main/res/drawable/icon_tab_profile.xml new file mode 100644 index 00000000..43325fc7 --- /dev/null +++ b/core/navigation-impl/src/main/res/drawable/icon_tab_profile.xml @@ -0,0 +1,14 @@ + + + + diff --git a/core/network-api/src/main/java/ru/yeahub/network_api/models/RegistrationRequestDto.kt b/core/network-api/src/main/java/ru/yeahub/network_api/models/RegistrationRequestDto.kt index 08a043c8..2f51902a 100644 --- a/core/network-api/src/main/java/ru/yeahub/network_api/models/RegistrationRequestDto.kt +++ b/core/network-api/src/main/java/ru/yeahub/network_api/models/RegistrationRequestDto.kt @@ -1,8 +1,7 @@ package ru.yeahub.network_api.models data class RegistrationRequestDto( - val nickname: String, + val username: String, val email: String, val password: String, - val isMailingAccepted: Boolean, ) diff --git a/core/network-impl/src/main/java/ru/yeahub/network_impl/RetrofitApiService.kt b/core/network-impl/src/main/java/ru/yeahub/network_impl/RetrofitApiService.kt index 1b7deeee..01b8a166 100644 --- a/core/network-impl/src/main/java/ru/yeahub/network_impl/RetrofitApiService.kt +++ b/core/network-impl/src/main/java/ru/yeahub/network_impl/RetrofitApiService.kt @@ -1,18 +1,29 @@ package ru.yeahub.network_impl +import retrofit2.http.Body import retrofit2.http.GET +import retrofit2.http.POST import retrofit2.http.Path import retrofit2.http.Query import ru.yeahub.network_api.ApiService +import ru.yeahub.network_api.models.AuthUserDto import ru.yeahub.network_api.models.GetCollectionsResponse import ru.yeahub.network_api.models.GetPublicQuestionResponse import ru.yeahub.network_api.models.GetPublicQuestionsResponse import ru.yeahub.network_api.models.GetSkillsResponse import ru.yeahub.network_api.models.GetSpecializationResponse import ru.yeahub.network_api.models.GetSpecializationsResponse +import ru.yeahub.network_api.models.LoginRequestDto +import ru.yeahub.network_api.models.LoginResponseDto +import ru.yeahub.network_api.models.RegistrationRequestDto interface RetrofitApiService : ApiService { + @POST("auth/signUp") + override suspend fun register( + @Body request: RegistrationRequestDto + ) + @GET("questions/public-questions") override suspend fun getQuestions( @Query("page") page: Int, @@ -61,4 +72,8 @@ interface RetrofitApiService : ApiService { @Query("specializations") specializationsId: Long, @Query("isFree") isFree: Boolean ): GetCollectionsResponse + + // Пустые реализации для ApiService, если они не нужны в Retrofit-слое + override suspend fun login(request: LoginRequestDto): LoginResponseDto = TODO("Not required for this feature") + override suspend fun getProfile(): AuthUserDto = TODO("Not required for this feature") } From 0eb402109044a749f060b7fb0627d7a54c71a10c Mon Sep 17 00:00:00 2001 From: MODDII Date: Tue, 9 Jun 2026 18:47:31 +0300 Subject: [PATCH 06/14] =?UTF-8?q?ANDR-51:=20=D0=A0=D0=B5=D0=B0=D0=BB=D0=B8?= =?UTF-8?q?=D0=B7=D0=B0=D1=86=D0=B8=D1=8F=20=D1=80=D0=B5=D0=BF=D0=BE=D0=B7?= =?UTF-8?q?=D0=B8=D1=82=D0=BE=D1=80=D0=B8=D1=8F=20=D1=80=D0=B5=D0=B3=D0=B8?= =?UTF-8?q?=D1=81=D1=82=D1=80=D0=B0=D1=86=D0=B8=D0=B8=20=D0=B8=20=D0=BE?= =?UTF-8?q?=D0=B1=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D0=BA=D0=B8=20=D0=BE=D1=88?= =?UTF-8?q?=D0=B8=D0=B1=D0=BE=D0=BA.=20=D0=9D=D0=B0=D1=81=D1=82=D1=80?= =?UTF-8?q?=D0=BE=D0=B5=D0=BD=20=D0=BC=D0=B0=D0=BF=D0=BF=D0=B8=D0=BD=D0=B3?= =?UTF-8?q?=20=D0=BE=D1=82=D0=B2=D0=B5=D1=82=D0=BE=D0=B2=20=D0=B1=D1=8D?= =?UTF-8?q?=D0=BA=D0=B5=D0=BD=D0=B4=D0=B0=20=D1=81=20=D1=83=D1=87=D0=B5?= =?UTF-8?q?=D1=82=D0=BE=D0=BC=20=D1=81=D0=BF=D0=B5=D1=86=D0=B8=D1=84=D0=B8?= =?UTF-8?q?=D1=87=D0=BD=D1=8B=D1=85=20=D0=BA=D0=BE=D0=B4=D0=BE=D0=B2=20(40?= =?UTF-8?q?4,=20409)=20=D0=B8=20=D0=BA=D0=BB=D1=8E=D1=87=D0=B5=D0=B9=20?= =?UTF-8?q?=D1=81=D0=BE=D0=BE=D0=B1=D1=89=D0=B5=D0=BD=D0=B8=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mapper/RegistrationDomainToDataMapper.kt | 3 +- .../repository/RegistrationRepositoryImpl.kt | 33 ++++++++++++------- .../domain/entity/RegistrationException.kt | 10 +++--- 3 files changed, 26 insertions(+), 20 deletions(-) diff --git a/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/registration/data/mapper/RegistrationDomainToDataMapper.kt b/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/registration/data/mapper/RegistrationDomainToDataMapper.kt index 5380ccbe..b54170c4 100644 --- a/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/registration/data/mapper/RegistrationDomainToDataMapper.kt +++ b/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/registration/data/mapper/RegistrationDomainToDataMapper.kt @@ -7,10 +7,9 @@ class RegistrationDomainToDataMapper { fun map(model: RegistrationModel): RegistrationRequestDto { return RegistrationRequestDto( - nickname = model.nickname, + username = model.nickname, email = model.email, password = model.password, - isMailingAccepted = model.isMailingAccepted ) } } \ No newline at end of file diff --git a/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/registration/data/repository/RegistrationRepositoryImpl.kt b/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/registration/data/repository/RegistrationRepositoryImpl.kt index 27d5ca6a..9c2ae8fc 100644 --- a/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/registration/data/repository/RegistrationRepositoryImpl.kt +++ b/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/registration/data/repository/RegistrationRepositoryImpl.kt @@ -1,5 +1,6 @@ package ru.yeahub.authentication.impl.registration.data.repository +import com.google.gson.Gson import kotlinx.coroutines.CancellationException import retrofit2.HttpException import ru.yeahub.authentication.impl.registration.data.mapper.RegistrationDomainToDataMapper @@ -9,11 +10,13 @@ import ru.yeahub.authentication.impl.registration.domain.entity.RegistrationErro import ru.yeahub.authentication.impl.registration.domain.entity.RegistrationException import ru.yeahub.authentication.impl.registration.domain.entity.RegistrationModel import ru.yeahub.authentication.impl.registration.domain.repository.RegistrationRepositoryApi +import ru.yeahub.network_api.models.ErrorResponseDto import java.io.IOException class RegistrationRepositoryImpl( private val remoteDataSourceApi: RegistrationRemoteDataSourceApi, - private val mapper: RegistrationDomainToDataMapper + private val mapper: RegistrationDomainToDataMapper, + private val gson: Gson ) : RegistrationRepositoryApi { override suspend fun register(registrationModel: RegistrationModel) = @@ -24,7 +27,7 @@ class RegistrationRepositoryImpl( throw e } catch (e: IOException) { throw RegistrationException( - error = RegistrationError.Network, + error = RegistrationError.UnknownError, failure = Failure(cause = e) ) } catch (e: HttpException) { @@ -33,20 +36,26 @@ class RegistrationRepositoryImpl( private fun mapHttpException(e: HttpException): RegistrationException { val code = e.code() - val error = - when (code) { - HTTP_CONFLICT -> RegistrationError.EmailAlreadyExists - HTTP_BAD_REQUEST -> RegistrationError.InvalidCredentials - in HTTP_SERVER_ERROR_MIN..HTTP_SERVER_ERROR_MAX -> RegistrationError.Server - else -> RegistrationError.Unknown - } + val errorBody = e.response()?.errorBody()?.string() + val errorDto = errorBody?.let { + runCatching { gson.fromJson(it, ErrorResponseDto::class.java) }.getOrNull() + } + + val backendKey = errorDto?.message + + val error = when { + backendKey == BACKEND_KEY_USER_CONFLICT || code == HTTP_CONFLICT -> RegistrationError.Conflict + backendKey == BACKEND_KEY_USER_NOT_FOUND || code == HTTP_NOT_FOUND -> RegistrationError.NotFound + else -> RegistrationError.UnknownError + } + return RegistrationException(error = error, failure = Failure(cause = e, httpCode = code)) } private companion object { - private const val HTTP_BAD_REQUEST = 400 private const val HTTP_CONFLICT = 409 - private const val HTTP_SERVER_ERROR_MIN = 500 - private const val HTTP_SERVER_ERROR_MAX = 599 + private const val HTTP_NOT_FOUND = 404 + private const val BACKEND_KEY_USER_CONFLICT = "user.user.conflict" + private const val BACKEND_KEY_USER_NOT_FOUND = "user.user.id.not_found" } } \ No newline at end of file diff --git a/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/registration/domain/entity/RegistrationException.kt b/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/registration/domain/entity/RegistrationException.kt index cd69c63e..a0e6188a 100644 --- a/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/registration/domain/entity/RegistrationException.kt +++ b/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/registration/domain/entity/RegistrationException.kt @@ -6,12 +6,10 @@ data class Failure( ) sealed interface RegistrationError { - data object EmailAlreadyExists : RegistrationError - data object NickNameTaken : RegistrationError - data object InvalidCredentials : RegistrationError - data object Network : RegistrationError - data object Server : RegistrationError - data object Unknown : RegistrationError + data object Success : RegistrationError + data object NotFound : RegistrationError + data object Conflict : RegistrationError + data object UnknownError : RegistrationError } class RegistrationException( From 33443e9fbd763780cdce66b0014ff3694e9c66f0 Mon Sep 17 00:00:00 2001 From: MODDII Date: Tue, 9 Jun 2026 19:37:03 +0300 Subject: [PATCH 07/14] =?UTF-8?q?ANDR-51:=20=D0=A0=D0=B5=D0=B0=D0=BB=D0=B8?= =?UTF-8?q?=D0=B7=D0=B0=D1=86=D0=B8=D1=8F=20=D0=BF=D1=80=D0=B5=D0=B7=D0=B5?= =?UTF-8?q?=D0=BD=D1=82=D0=B0=D1=86=D0=B8=D0=BE=D0=BD=D0=BD=D0=BE=D0=B9=20?= =?UTF-8?q?=D0=BB=D0=BE=D0=B3=D0=B8=D0=BA=D0=B8=20=D1=80=D0=B5=D0=B3=D0=B8?= =?UTF-8?q?=D1=81=D1=82=D1=80=D0=B0=D1=86=D0=B8=D0=B8.=20=D0=94=D0=BE?= =?UTF-8?q?=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B0=20ViewModel,=20?= =?UTF-8?q?=D0=BA=D0=BE=D0=BC=D0=BF=D0=BB=D0=B5=D0=BA=D1=81=D0=BD=D0=B0?= =?UTF-8?q?=D1=8F=20=D0=B2=D0=B0=D0=BB=D0=B8=D0=B4=D0=B0=D1=86=D0=B8=D1=8F?= =?UTF-8?q?=20=D0=BF=D0=BE=D0=BB=D0=B5=D0=B9=20=D0=BD=D0=B0=20=D0=BE=D1=81?= =?UTF-8?q?=D0=BD=D0=BE=D0=B2=D0=B5=20=D0=B4=D0=BE=D0=BC=D0=B5=D0=BD=D0=BD?= =?UTF-8?q?=D1=8B=D1=85=20=D0=BF=D1=80=D0=B0=D0=B2=D0=B8=D0=BB=20=D0=B8=20?= =?UTF-8?q?=D0=BC=D0=B0=D0=BF=D0=BF=D0=B8=D0=BD=D0=B3=20=D1=81=D0=BE=D1=81?= =?UTF-8?q?=D1=82=D0=BE=D1=8F=D0=BD=D0=B8=D0=B9=20=D1=8D=D0=BA=D1=80=D0=B0?= =?UTF-8?q?=D0=BD=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/PasswordValidation.kt | 10 -- .../presentation/RegistrationUiState.kt | 14 +- .../presentation/RegistrationUiStateMapper.kt | 167 ++++++++++++++++-- .../presentation/RegistrationViewModel.kt | 114 +++--------- .../impl/src/main/res/values/strings.xml | 13 ++ 5 files changed, 203 insertions(+), 115 deletions(-) delete mode 100644 feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/registration/presentation/PasswordValidation.kt diff --git a/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/registration/presentation/PasswordValidation.kt b/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/registration/presentation/PasswordValidation.kt deleted file mode 100644 index 2109da95..00000000 --- a/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/registration/presentation/PasswordValidation.kt +++ /dev/null @@ -1,10 +0,0 @@ -package ru.yeahub.authentication.impl.registration.presentation - -private const val MIN_PASSWORD_LENGTH = 8 - -internal fun isPasswordValid(password: String): Boolean { - val isValid = password.matches( - Regex("^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[^A-Za-z\\d]).{$MIN_PASSWORD_LENGTH,}$") - ) - return isValid -} \ No newline at end of file diff --git a/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/registration/presentation/RegistrationUiState.kt b/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/registration/presentation/RegistrationUiState.kt index a710ea90..2900ca59 100644 --- a/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/registration/presentation/RegistrationUiState.kt +++ b/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/registration/presentation/RegistrationUiState.kt @@ -1,16 +1,25 @@ package ru.yeahub.authentication.impl.registration.presentation +import ru.yeahub.core_utils.common.TextOrResource + data class RegistrationFormState( val nickname: String, + val nicknameError: TextOrResource?, val email: String, + val emailError: TextOrResource?, val password: String, + val passwordError: TextOrResource?, val confirmPassword: String, + val confirmPasswordError: TextOrResource?, val isPdAccepted: Boolean, val isOfferAccepted: Boolean, val isMailingAccepted: Boolean, val isPasswordVisible: Boolean, val isConfirmPasswordVisible: Boolean, val isSubmitEnabled: Boolean, + val isEmailTouched: Boolean, + val isPasswordTouched: Boolean, + val isConfirmPasswordTouched: Boolean, ) sealed interface RegistrationUiState { @@ -21,7 +30,7 @@ sealed interface RegistrationUiState { data class Loading(override val formState: RegistrationFormState) : RegistrationUiState - data class Error(val message: String, override val formState: RegistrationFormState) : + data class Error(val message: TextOrResource? = null, override val formState: RegistrationFormState) : RegistrationUiState } @@ -36,4 +45,7 @@ sealed interface RegistrationAction { data object TogglePasswordVisible : RegistrationAction data object ToggleConfirmPasswordVisible : RegistrationAction data object SubmitClicked : RegistrationAction + data class EmailFocusChanged(val hasFocus: Boolean) : RegistrationAction + data class PasswordFocusChanged(val hasFocus: Boolean) : RegistrationAction + data class ConfirmPasswordFocusChanged(val hasFocus: Boolean) : RegistrationAction } diff --git a/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/registration/presentation/RegistrationUiStateMapper.kt b/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/registration/presentation/RegistrationUiStateMapper.kt index ba05e5cb..caa56468 100644 --- a/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/registration/presentation/RegistrationUiStateMapper.kt +++ b/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/registration/presentation/RegistrationUiStateMapper.kt @@ -1,46 +1,175 @@ package ru.yeahub.authentication.impl.registration.presentation -import android.util.Patterns +import ru.yeahub.authentication.impl.R +import ru.yeahub.authentication.impl.registration.domain.entity.RegistrationError +import ru.yeahub.authentication.impl.registration.domain.entity.RegistrationException +import ru.yeahub.core_utils.common.TextOrResource +import ru.yeahub.core_utils.validation.EmailValidator +import ru.yeahub.core_utils.validation.PasswordValidationError +import ru.yeahub.core_utils.validation.PasswordValidator class RegistrationUiStateMapper { fun getInitialFormState(): RegistrationFormState = RegistrationFormState( nickname = "", + nicknameError = null, email = "", + emailError = null, password = "", + passwordError = null, confirmPassword = "", + confirmPasswordError = null, isPdAccepted = false, isOfferAccepted = false, isMailingAccepted = false, isPasswordVisible = false, isConfirmPasswordVisible = false, isSubmitEnabled = false, + isEmailTouched = false, + isPasswordTouched = false, + isConfirmPasswordTouched = false, ) - fun getInitialState(): RegistrationUiState = RegistrationUiState.Content(getInitialFormState()) + fun mapToInitialState(): RegistrationUiState { + return RegistrationUiState.Content(getInitialFormState()) + } - fun getScreenState( - formState: RegistrationFormState, - isLoading: Boolean, - errorMessage: String? + fun mapToUpdatedState( + currentState: RegistrationUiState, + action: RegistrationAction ): RegistrationUiState { - val validatedForm = formState.revalidate() - return when { - isLoading -> RegistrationUiState.Loading(validatedForm) - errorMessage != null -> RegistrationUiState.Error(errorMessage, validatedForm) - else -> RegistrationUiState.Content(validatedForm) + val newForm = updateFormState(currentState.formState, action) + return RegistrationUiState.Content(validateForm(newForm)) + } + + private fun updateFormState( + currentForm: RegistrationFormState, + action: RegistrationAction + ): RegistrationFormState { + return when (action) { + is RegistrationAction.NicknameChanged, + is RegistrationAction.EmailChanged, + is RegistrationAction.PasswordChanged, + is RegistrationAction.ConfirmPasswordChanged -> handleFieldAction(currentForm, action) + + is RegistrationAction.EmailFocusChanged, + is RegistrationAction.PasswordFocusChanged, + is RegistrationAction.ConfirmPasswordFocusChanged -> handleFocusAction(currentForm, action) + + is RegistrationAction.PdAcceptedChanged, + is RegistrationAction.OfferAcceptedChanged, + is RegistrationAction.MailingAcceptedChanged -> handleConsentAction(currentForm, action) + + is RegistrationAction.TogglePasswordVisible, + is RegistrationAction.ToggleConfirmPasswordVisible -> handleToggleAction(currentForm, action) + + else -> currentForm } } - private fun RegistrationFormState.revalidate(): RegistrationFormState { - val nicknameOk = nickname.trim().isNotEmpty() - val emailOk = Patterns.EMAIL_ADDRESS.matcher(email.trim()).matches() - val passOk = isPasswordValid(password) - val confirmOk = password == confirmPassword && confirmPassword.isNotEmpty() - val requiredConsentsOk = isPdAccepted && isOfferAccepted + private fun handleFieldAction( + form: RegistrationFormState, + action: RegistrationAction + ): RegistrationFormState = when (action) { + is RegistrationAction.NicknameChanged -> form.copy(nickname = action.value) + is RegistrationAction.EmailChanged -> form.copy(email = action.value) + is RegistrationAction.PasswordChanged -> form.copy(password = action.value) + is RegistrationAction.ConfirmPasswordChanged -> form.copy(confirmPassword = action.value) + else -> form + } - return copy( - isSubmitEnabled = nicknameOk && emailOk && passOk && confirmOk && requiredConsentsOk + private fun handleFocusAction( + form: RegistrationFormState, + action: RegistrationAction + ): RegistrationFormState = when (action) { + is RegistrationAction.EmailFocusChanged -> + form.copy(isEmailTouched = !action.hasFocus && form.email.isNotEmpty()) + is RegistrationAction.PasswordFocusChanged -> + form.copy(isPasswordTouched = !action.hasFocus && form.password.isNotEmpty()) + is RegistrationAction.ConfirmPasswordFocusChanged -> + form.copy(isConfirmPasswordTouched = !action.hasFocus && form.confirmPassword.isNotEmpty()) + else -> form + } + + private fun handleConsentAction( + form: RegistrationFormState, + action: RegistrationAction + ): RegistrationFormState = when (action) { + is RegistrationAction.PdAcceptedChanged -> form.copy(isPdAccepted = action.value) + is RegistrationAction.OfferAcceptedChanged -> form.copy(isOfferAccepted = action.value) + is RegistrationAction.MailingAcceptedChanged -> form.copy(isMailingAccepted = action.value) + else -> form + } + + private fun handleToggleAction( + form: RegistrationFormState, + action: RegistrationAction + ): RegistrationFormState = when (action) { + is RegistrationAction.TogglePasswordVisible -> + form.copy(isPasswordVisible = !form.isPasswordVisible) + is RegistrationAction.ToggleConfirmPasswordVisible -> + form.copy(isConfirmPasswordVisible = !form.isConfirmPasswordVisible) + else -> form + } + + fun mapToLoadingState(currentState: RegistrationUiState): RegistrationUiState { + return RegistrationUiState.Loading(currentState.formState) + } + + fun mapToErrorState( + currentState: RegistrationUiState, + exception: RegistrationException + ): RegistrationUiState { + val errorResource = mapExceptionToResource(exception) + return RegistrationUiState.Error(errorResource, currentState.formState) + } + + fun mapExceptionToResource(exception: RegistrationException): TextOrResource { + return when (exception.error) { + RegistrationError.Conflict -> TextOrResource.Resource(R.string.error_user_already_exists) + RegistrationError.NotFound -> TextOrResource.Resource(R.string.error_resource_not_found) + else -> TextOrResource.Resource(R.string.login_unknown_error) + } + } + + private fun validateForm(form: RegistrationFormState): RegistrationFormState { + val isEmailValid = EmailValidator.isValid(form.email.trim()) + val passwordErrors = PasswordValidator.validate(form.password) + val isPassValid = passwordErrors.isEmpty() + val arePasswordsMatch = form.password == form.confirmPassword && form.confirmPassword.isNotEmpty() + val isNicknameOk = form.nickname.trim().isNotEmpty() + + val matchError = if (form.isConfirmPasswordTouched && form.confirmPassword.isNotEmpty() && !arePasswordsMatch) { + TextOrResource.Resource(R.string.error_passwords_not_match) + } else { + null + } + + return form.copy( + emailError = if (form.isEmailTouched && !isEmailValid) { + TextOrResource.Resource(R.string.error_email_invalid) + } else { + null + }, + passwordError = getPasswordError(form, passwordErrors) ?: matchError, + confirmPasswordError = matchError, + isSubmitEnabled = isNicknameOk && isEmailValid && isPassValid && + arePasswordsMatch && form.isPdAccepted && form.isOfferAccepted ) } + + private fun getPasswordError( + form: RegistrationFormState, + errors: Set + ): TextOrResource? { + if (!form.isPasswordTouched || form.password.isEmpty() || errors.isEmpty()) { + return null + } + return when (errors.first()) { + PasswordValidationError.TOO_SHORT -> TextOrResource.Resource(R.string.error_password_too_short) + PasswordValidationError.NO_UPPERCASE -> TextOrResource.Resource(R.string.error_password_no_uppercase) + PasswordValidationError.NO_DIGIT -> TextOrResource.Resource(R.string.error_password_no_digit) + PasswordValidationError.NO_SPECIAL_CHAR -> TextOrResource.Resource(R.string.error_password_no_special_char) + } + } } diff --git a/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/registration/presentation/RegistrationViewModel.kt b/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/registration/presentation/RegistrationViewModel.kt index e6b3eaa3..711f9103 100644 --- a/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/registration/presentation/RegistrationViewModel.kt +++ b/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/registration/presentation/RegistrationViewModel.kt @@ -2,117 +2,61 @@ package ru.yeahub.authentication.impl.registration.presentation import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.flow.update +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch -import ru.yeahub.authentication.impl.registration.domain.entity.RegistrationError import ru.yeahub.authentication.impl.registration.domain.entity.RegistrationException import ru.yeahub.authentication.impl.registration.domain.entity.RegistrationModel import ru.yeahub.authentication.impl.registration.domain.usecase.RegistrationUseCase +import ru.yeahub.core_utils.common.TextOrResource -private const val UI_STATE_STOP_TIMEOUT = 5000L +sealed interface RegistrationCommand { + data object NavigateToSuccess : RegistrationCommand + data class ShowError(val message: TextOrResource) : RegistrationCommand +} class RegistrationViewModel( private val registrationUseCase: RegistrationUseCase, private val mapper: RegistrationUiStateMapper ) : ViewModel() { - private val formData = MutableStateFlow(mapper.getInitialFormState()) - private val isLoading = MutableStateFlow(false) - private val error = MutableStateFlow(null) + private val _state = MutableStateFlow(mapper.mapToInitialState()) + val state: StateFlow = _state.asStateFlow() - val state: StateFlow = - combine(formData, isLoading, error) { form, loading, err -> - mapper.getScreenState(form, loading, err) - } - .stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(UI_STATE_STOP_TIMEOUT), - initialValue = mapper.getInitialState() - ) + private val _commands = MutableSharedFlow() + val commands: SharedFlow = _commands.asSharedFlow() fun onAction(action: RegistrationAction) { when (action) { - is RegistrationAction.ConfirmPasswordChanged -> { - updateForm { it.copy(confirmPassword = action.value) } - } - - is RegistrationAction.EmailChanged -> { - updateForm { it.copy(email = action.value) } - } - - is RegistrationAction.MailingAcceptedChanged -> { - updateForm { it.copy(isMailingAccepted = action.value) } - } - - is RegistrationAction.NicknameChanged -> { - updateForm { it.copy(nickname = action.value) } - } - - is RegistrationAction.OfferAcceptedChanged -> { - updateForm { it.copy(isOfferAccepted = action.value) } - } - - is RegistrationAction.PasswordChanged -> { - updateForm { it.copy(password = action.value) } + RegistrationAction.SubmitClicked -> submitRegistration() + else -> { + _state.value = mapper.mapToUpdatedState(_state.value, action) } - - is RegistrationAction.PdAcceptedChanged -> { - updateForm { it.copy(isPdAccepted = action.value) } - } - - RegistrationAction.SubmitClicked -> { - submitRegistration() - } - - RegistrationAction.ToggleConfirmPasswordVisible -> { - updateForm { it.copy(isConfirmPasswordVisible = !it.isConfirmPasswordVisible) } - } - - RegistrationAction.TogglePasswordVisible -> { - updateForm { it.copy(isPasswordVisible = !it.isPasswordVisible) } - } - } - } - - private fun updateForm(transform: (RegistrationFormState) -> RegistrationFormState) { - formData.update { transform(it) } - if (error.value != null) { - error.value = null } } private fun submitRegistration() { - isLoading.value = true - error.value = null + _state.value = mapper.mapToLoadingState(_state.value) viewModelScope.launch { try { - val currentForm = formData.value - val userModel = - RegistrationModel( - nickname = currentForm.nickname, - email = currentForm.email, - password = currentForm.password, - isMailingAccepted = currentForm.isMailingAccepted - ) + val currentForm = _state.value.formState + val userModel = RegistrationModel( + nickname = currentForm.nickname, + email = currentForm.email, + password = currentForm.password, + isMailingAccepted = currentForm.isMailingAccepted + ) registrationUseCase.invoke(userModel) - isLoading.value = false + _state.value = mapper.mapToInitialState() + _commands.emit(RegistrationCommand.NavigateToSuccess) } catch (e: RegistrationException) { - val errorMessage = - when (e.error) { - RegistrationError.EmailAlreadyExists -> "Такой Email уже существует" - RegistrationError.NickNameTaken -> "Никнейм занят" - RegistrationError.InvalidCredentials -> "Неверные данные" - RegistrationError.Network -> "Ошибка сети. Проверьте подключение" - RegistrationError.Server, RegistrationError.Unknown -> - "Произошла ошибка на сервере" - } - isLoading.value = false - error.value = errorMessage + val errorResource = mapper.mapExceptionToResource(e) + _state.value = mapper.mapToErrorState(_state.value, e) + _commands.emit(RegistrationCommand.ShowError(errorResource)) } } } diff --git a/feature/authentication/impl/src/main/res/values/strings.xml b/feature/authentication/impl/src/main/res/values/strings.xml index bc397f6c..f186b4c5 100644 --- a/feature/authentication/impl/src/main/res/values/strings.xml +++ b/feature/authentication/impl/src/main/res/values/strings.xml @@ -15,6 +15,10 @@ , в соответствии с Политикой в отношении ПД "Подтверждаю что ознакомился(-ась) с " Договором-офертой + + https://docs.google.com/document/d/1OX9Fc3HPhjL_U9xkF2P3vsSmM1fAdhmQ88J2NT0emFo/edit?tab=t.0 + https://docs.google.com/document/d/1tU9lgOu_W21DAoHOH0kQmTWq5hG3rvxv_Q_jzRi8Gh4/edit?tab=t.0 + Вход в личный кабинет Вход Забыли пароль? @@ -29,4 +33,13 @@ Произошла непредвиденная ошибка Показать пароль Скрыть пароль + + Email введен неверно + Минимум 8 символов + Нужна заглавная буква + Нужна цифра + Нужен спецсимвол + Пароли не совпадают + Пользователь уже существует + Ресурс не найден \ No newline at end of file From 4c61e078bce137943427424a97ffa7c83bc075bd Mon Sep 17 00:00:00 2001 From: MODDII Date: Tue, 9 Jun 2026 19:39:18 +0300 Subject: [PATCH 08/14] =?UTF-8?q?ANDR-51:=20=D0=97=D0=B0=D0=B2=D0=B5=D1=80?= =?UTF-8?q?=D1=88=D0=B5=D0=BD=D0=B8=D0=B5=20=D1=80=D0=B5=D0=B0=D0=BB=D0=B8?= =?UTF-8?q?=D0=B7=D0=B0=D1=86=D0=B8=D0=B8=20UI=20=D0=B8=20=D0=B8=D0=BD?= =?UTF-8?q?=D1=82=D0=B5=D0=B3=D1=80=D0=B0=D1=86=D0=B8=D1=8F=20=D1=84=D0=B8?= =?UTF-8?q?=D1=87=D0=B8=20=D0=B2=20=D0=BF=D1=80=D0=B8=D0=BB=D0=BE=D0=B6?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5.=20=D0=9D=D0=B0=D1=81=D1=82=D1=80?= =?UTF-8?q?=D0=BE=D0=B5=D0=BD=D0=B0=20=D0=BD=D0=B0=D0=B2=D0=B8=D0=B3=D0=B0?= =?UTF-8?q?=D1=86=D0=B8=D1=8F,=20=D1=80=D0=B5=D0=B3=D0=B8=D1=81=D1=82?= =?UTF-8?q?=D1=80=D0=B0=D1=86=D0=B8=D1=8F=20=D1=8D=D0=BA=D1=80=D0=B0=D0=BD?= =?UTF-8?q?=D0=B0=20=D0=B2=20NavigationFactory=20=D0=B8=20=D0=BF=D0=BE?= =?UTF-8?q?=D0=B4=D0=BA=D0=BB=D1=8E=D1=87=D0=B5=D0=BD=D0=B8=D0=B5=20DI-?= =?UTF-8?q?=D0=BC=D0=BE=D0=B4=D1=83=D0=BB=D0=B5=D0=B9.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/java/ru/yeahub/Application.kt | 4 +- .../yeahub/navigation_impl/AppNavigation.kt | 73 ++++-- .../navigation_impl/NavigationFactory.kt | 3 +- .../model/BottomNavigationItem.kt | 6 + .../registration/RegistrationFeatureImpl.kt | 48 ++++ .../di/RegistrationFeatureModule.kt | 7 +- .../presentation/RegistrationScreen.kt | 240 ++++++++---------- 7 files changed, 222 insertions(+), 159 deletions(-) create mode 100644 feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/registration/RegistrationFeatureImpl.kt diff --git a/app/src/main/java/ru/yeahub/Application.kt b/app/src/main/java/ru/yeahub/Application.kt index 792c54b3..0b1dd5b1 100644 --- a/app/src/main/java/ru/yeahub/Application.kt +++ b/app/src/main/java/ru/yeahub/Application.kt @@ -3,6 +3,7 @@ package ru.yeahub import android.app.Application import org.koin.android.ext.koin.androidContext import org.koin.core.context.startKoin +import ru.yeahub.authentication.impl.registration.di.registrationFeatureModule import ru.yeahub.detail_question.impl.di.detailQuestionFeatureModule import ru.yeahub.example_details.impl.detailsFeatureModule import ru.yeahub.example_home.impl.data.di.questionsMainFeatureModule @@ -53,7 +54,8 @@ class Application : Application() { CollectionsFeatureModule, detailQuestionFeatureModule, collectionsAndQuestionsFeatureModule, - specializationFeatureModule + specializationFeatureModule, + registrationFeatureModule ) } // проверка, что модули загружены diff --git a/core/navigation-impl/src/main/java/ru/yeahub/navigation_impl/AppNavigation.kt b/core/navigation-impl/src/main/java/ru/yeahub/navigation_impl/AppNavigation.kt index 35c59fef..25e156ed 100644 --- a/core/navigation-impl/src/main/java/ru/yeahub/navigation_impl/AppNavigation.kt +++ b/core/navigation-impl/src/main/java/ru/yeahub/navigation_impl/AppNavigation.kt @@ -34,6 +34,7 @@ import androidx.navigation.compose.rememberNavController import org.koin.compose.getKoin import ru.yeahub.core_ui.theme.Theme import ru.yeahub.navigation_api.FeatureApi +import ru.yeahub.navigation_api.FeatureRoute import ru.yeahub.navigation_api.NavigationPathManager import ru.yeahub.navigation_impl.model.BottomNavigationItem import timber.log.Timber @@ -70,7 +71,7 @@ fun AppNavigation( val features: Set = getKoin().getAll().toSet() Timber.d("AppNavigation onCreate: Loaded features: ${features.map { it.javaClass.simpleName }}") val navItems = getBottomNavItems() - + features.forEach { feature -> feature.initialize(pathManager) } @@ -78,16 +79,18 @@ fun AppNavigation( val navBackStackEntry by navController.currentBackStackEntryAsState() val currentRoute = navBackStackEntry?.destination?.route val selectedRoute = getSelectedRoute(currentRoute, navItems) - + currentRoute?.let { route -> pathManager.setCurrentPath(route) } Scaffold( modifier = modifier, + containerColor = Theme.colors.white900, bottomBar = { NavigationBar( - modifier = Modifier.clip(RoundedCornerShape(12.dp)) + modifier = Modifier + .clip(RoundedCornerShape(12.dp)) .height(100.dp), containerColor = Theme.colors.purple700 ) { @@ -120,7 +123,11 @@ fun AppNavigation( .fillMaxWidth() .background( color = animateColorAsState( - targetValue = if (isSelected) Theme.colors.white900 else Color.Transparent, + targetValue = if (isSelected) { + Theme.colors.white900 + } else { + Color.Transparent + }, animationSpec = tween(durationMillis = 80) ).value, shape = RoundedCornerShape(8.dp) @@ -139,28 +146,44 @@ fun AppNavigation( }, alwaysShowLabel = false ) - Timber.d("NavSelected", "$currentRoute") + Timber.d("NavSelected: $currentRoute") } } } ) { padding -> Box(modifier = Modifier.padding(padding)) { - NavHost( + AppNavHost( navController = navController, - startDestination = navItems[1].route, - modifier = Modifier, - ) { - registerDynamicNavigation( - features = features, - pathManager = pathManager, - navController = navController, - navGraphBuilder = this - ) - } + features = features, + pathManager = pathManager + ) } } } +/** + * Отдельный компонент для NavHost, чтобы избежать дублирования кода. + */ +@Composable +private fun AppNavHost( + navController: NavHostController, + features: Set, + pathManager: NavigationPathManager +) { + NavHost( + navController = navController, + startDestination = FeatureRoute.HomeFeature.FEATURE_NAME, + modifier = Modifier, + ) { + registerDynamicNavigation( + features = features, + pathManager = pathManager, + navController = navController, + navGraphBuilder = this + ) + } +} + /** * Обработка нажатий на нижнюю панель навигации. */ @@ -186,9 +209,9 @@ private fun handleBottomNavClick( // Навигируем на родительский маршрут другого таба Timber.d( "AppNavigation onClick: Navigating to different tab: " + - "${item.route} from: $currentRoute" + "${item.route} from: $currentRoute" ) - + // Устанавливаем новый корневой путь pathManager.setCurrentPath(item.route) navController.navigate(item.route) { @@ -210,21 +233,21 @@ private fun registerDynamicNavigation( ) { val rootFeatures = features.filter { it.isRootFeature() } val childFeatures = features.filter { !it.isRootFeature() } - + // Регистрируем корневые фичи rootFeatures.forEach { feature -> Timber.d( "AppNavigation registerGraph: Registering root feature: " + "${feature.javaClass.simpleName}" ) - + // Сбрасываем путь для корневой фичи pathManager.setCurrentPath("") pathManager.registerFeaturePath(feature.getFeatureName(), feature.getFeatureName()) - + feature.registerGraph(navGraphBuilder, navController, pathManager) } - + // Регистрируем дочерние фичи для каждой корневой фичи registerChildFeatures(childFeatures, rootFeatures, pathManager, navController, navGraphBuilder) } @@ -241,17 +264,17 @@ private fun registerChildFeatures( ) { childFeatures.forEach { childFeature -> val dependentRootFeatures = childFeature.getDependentRootFeatures(rootFeatures) - + // Если фича не указала зависимости, регистрируем для всех корневых фич val targetRootFeatures = if (dependentRootFeatures.isEmpty()) { rootFeatures } else { dependentRootFeatures } - + targetRootFeatures.forEach { rootFeature -> pathManager.setCurrentPath(rootFeature.getFeatureName()) - + // Регистрируем дочернюю фичу childFeature.registerGraph( navGraphBuilder = navGraphBuilder, diff --git a/core/navigation-impl/src/main/java/ru/yeahub/navigation_impl/NavigationFactory.kt b/core/navigation-impl/src/main/java/ru/yeahub/navigation_impl/NavigationFactory.kt index 946f2fe1..9c7b5fd2 100644 --- a/core/navigation-impl/src/main/java/ru/yeahub/navigation_impl/NavigationFactory.kt +++ b/core/navigation-impl/src/main/java/ru/yeahub/navigation_impl/NavigationFactory.kt @@ -6,13 +6,14 @@ import ru.yeahub.navigation_impl.model.BottomNavigationItem * Фабрика для создания навигационных элементов. */ fun getBottomNavItems(): List = listOf( + BottomNavigationItem.Profile, BottomNavigationItem.Collections, BottomNavigationItem.Home, BottomNavigationItem.Questions ) fun getSelectedRoute(currentRoute: String?, navItems: List): String = when { - currentRoute == null -> navItems[1].route + currentRoute == null -> navItems[2].route navItems.any { it.route == currentRoute } -> currentRoute else -> navItems.find { currentRoute.startsWith(it.route) }?.route ?: navItems.last().route } \ No newline at end of file diff --git a/core/navigation-impl/src/main/java/ru/yeahub/navigation_impl/model/BottomNavigationItem.kt b/core/navigation-impl/src/main/java/ru/yeahub/navigation_impl/model/BottomNavigationItem.kt index be7c35fd..692e957e 100644 --- a/core/navigation-impl/src/main/java/ru/yeahub/navigation_impl/model/BottomNavigationItem.kt +++ b/core/navigation-impl/src/main/java/ru/yeahub/navigation_impl/model/BottomNavigationItem.kt @@ -32,6 +32,12 @@ sealed class BottomNavigationItem( val label: String, @DrawableRes val icon: Int ) { + data object Profile : BottomNavigationItem( + route = FeatureRoute.RegistrationFeature.FEATURE_NAME, + label = "Профиль", + icon = R.drawable.icon_tab_profile + ) + data object Collections : BottomNavigationItem( route = FeatureRoute.CollectionsFeature.FEATURE_NAME, label = "Коллекции", diff --git a/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/registration/RegistrationFeatureImpl.kt b/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/registration/RegistrationFeatureImpl.kt new file mode 100644 index 00000000..72e803b6 --- /dev/null +++ b/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/registration/RegistrationFeatureImpl.kt @@ -0,0 +1,48 @@ +package ru.yeahub.authentication.impl.registration + +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.res.stringResource +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavHostController +import androidx.navigation.compose.composable +import ru.yeahub.authentication.impl.R +import ru.yeahub.authentication.impl.registration.presentation.RegistrationScreen +import ru.yeahub.navigation_api.FeatureApi +import ru.yeahub.navigation_api.FeatureRoute +import ru.yeahub.navigation_api.NavigationPathManager + +class RegistrationFeatureImpl : FeatureApi { + + override fun getFeatureName(): String = FeatureRoute.RegistrationFeature.FEATURE_NAME + + override fun isRootFeature(): Boolean = true + + override fun initialize(pathManager: NavigationPathManager) { + super.initialize(pathManager) + pathManager.registerFeaturePath(getFeatureName(), getFeatureName()) + } + + override fun registerGraph( + navGraphBuilder: NavGraphBuilder, + navController: NavHostController, + pathManager: NavigationPathManager, + modifier: Modifier + ) { + navGraphBuilder.composable(route = getFeatureName()) { + val uriHandler = LocalUriHandler.current + val pdUrl = stringResource(R.string.pd_offer_url) + val offerUrl = stringResource(R.string.public_offer_uri) + + RegistrationScreen( + onRegistrationSuccess = { + navController.navigate(FeatureRoute.HomeFeature.FEATURE_NAME) { + popUpTo(getFeatureName()) { inclusive = true } + } + }, + onOpenPdPolicy = { uriHandler.openUri(pdUrl) }, + onOpenOffer = { uriHandler.openUri(offerUrl) } + ) + } + } +} diff --git a/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/registration/di/RegistrationFeatureModule.kt b/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/registration/di/RegistrationFeatureModule.kt index 77aa3a8b..994b6cb4 100644 --- a/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/registration/di/RegistrationFeatureModule.kt +++ b/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/registration/di/RegistrationFeatureModule.kt @@ -1,5 +1,6 @@ package ru.yeahub.authentication.impl.registration.di +import com.google.gson.Gson import org.koin.androidx.viewmodel.dsl.viewModelOf import org.koin.core.module.dsl.bind import org.koin.core.module.dsl.factoryOf @@ -11,14 +12,18 @@ import ru.yeahub.authentication.impl.registration.data.repository.remote.Registr import ru.yeahub.authentication.impl.registration.data.repository.remote.RegistrationRemoteDataSourceImpl import ru.yeahub.authentication.impl.registration.domain.repository.RegistrationRepositoryApi import ru.yeahub.authentication.impl.registration.domain.usecase.RegistrationUseCase -import ru.yeahub.authentication.impl.registration.presentation.RegistrationUiStateMapper import ru.yeahub.authentication.impl.registration.presentation.RegistrationViewModel +import ru.yeahub.authentication.impl.registration.presentation.RegistrationUiStateMapper +import ru.yeahub.authentication.impl.registration.RegistrationFeatureImpl +import ru.yeahub.navigation_api.FeatureApi val registrationFeatureModule = module { + singleOf(::Gson) singleOf(::RegistrationDomainToDataMapper) singleOf(::RegistrationRemoteDataSourceImpl) { bind() } singleOf(::RegistrationRepositoryImpl) { bind() } factoryOf(::RegistrationUseCase) singleOf(::RegistrationUiStateMapper) viewModelOf(::RegistrationViewModel) + singleOf(::RegistrationFeatureImpl) { bind() } } diff --git a/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/registration/presentation/RegistrationScreen.kt b/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/registration/presentation/RegistrationScreen.kt index 3110da3a..93d598fb 100644 --- a/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/registration/presentation/RegistrationScreen.kt +++ b/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/registration/presentation/RegistrationScreen.kt @@ -18,99 +18,172 @@ import androidx.compose.material.icons.filled.Visibility import androidx.compose.material.icons.filled.VisibilityOff import androidx.compose.material3.Checkbox import androidx.compose.material3.CheckboxDefaults -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.OutlinedTextFieldDefaults import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import org.koin.androidx.compose.koinViewModel import ru.yeahub.authentication.impl.R import ru.yeahub.core_ui.component.PrimaryButton +import ru.yeahub.core_ui.component.PrimaryTextField import ru.yeahub.core_ui.theme.Theme +import ru.yeahub.core_ui.theme.YeaHubTheme @Composable fun RegistrationScreen( + viewModel: RegistrationViewModel = koinViewModel(), + onRegistrationSuccess: () -> Unit, + onOpenPdPolicy: () -> Unit, + onOpenOffer: () -> Unit +) { + val state by viewModel.state.collectAsState() + val context = androidx.compose.ui.platform.LocalContext.current + + LaunchedEffect(Unit) { + viewModel.commands.collect { command -> + when (command) { + is RegistrationCommand.NavigateToSuccess -> onRegistrationSuccess() + is RegistrationCommand.ShowError -> { + android.widget.Toast.makeText( + context, + command.message.getString(context), + android.widget.Toast.LENGTH_SHORT + ).show() + } + } + } + } + + RegistrationContent( + state = state, + onAction = viewModel::onAction, + onOpenPdPolicy = onOpenPdPolicy, + onOpenOffer = onOpenOffer + ) +} + +@Composable +fun RegistrationContent( state: RegistrationUiState, onAction: (RegistrationAction) -> Unit, onOpenPdPolicy: () -> Unit, onOpenOffer: () -> Unit ) { - val linkColor = MaterialTheme.colorScheme.primary + val linkColor = Theme.colors.purple700 val form = state.formState - Scaffold { paddings -> + Scaffold( + containerColor = Theme.colors.white900 + ) { paddings -> Column( modifier = Modifier .padding(paddings) .fillMaxSize() .verticalScroll(rememberScrollState()) - .padding(horizontal = 16.dp, vertical = 24.dp), + .padding(horizontal = 16.dp), verticalArrangement = Arrangement.spacedBy(14.dp) ) { Text( text = stringResource(R.string.registration_title), - style = MaterialTheme.typography.headlineSmall, - fontWeight = FontWeight.SemiBold + style = Theme.typography.body6, + color = Theme.colors.black900 ) - FormTextField( + PrimaryTextField( title = stringResource(R.string.nickname_title), placeholder = stringResource(R.string.nickname_placeholder), value = form.nickname, onValueChange = { onAction(RegistrationAction.NicknameChanged(it)) }, - keyboardType = KeyboardType.Ascii + error = form.nicknameError, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Ascii) ) - FormTextField( + PrimaryTextField( title = stringResource(R.string.email_title), placeholder = stringResource(R.string.email_placeholder), value = form.email, onValueChange = { onAction(RegistrationAction.EmailChanged(it)) }, - keyboardType = KeyboardType.Email + error = form.emailError, + onFocusChanged = { hasFocus -> + onAction(RegistrationAction.EmailFocusChanged(hasFocus)) + }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Email) ) - FormPasswordField( + PrimaryTextField( title = stringResource(R.string.password_title), placeholder = stringResource(R.string.password_placeholder), value = form.password, - isVisible = form.isPasswordVisible, onValueChange = { onAction(RegistrationAction.PasswordChanged(it)) }, - onToggleVisibility = { + error = form.passwordError, + onFocusChanged = { hasFocus -> + onAction(RegistrationAction.PasswordFocusChanged(hasFocus)) + }, + visualTransformation = if (form.isPasswordVisible) { + VisualTransformation.None + } else { + PasswordVisualTransformation() + }, + trailingIcon = rememberVectorPainter( + if (form.isPasswordVisible) { + Icons.Filled.VisibilityOff + } else { + Icons.Filled.Visibility + } + ), + onTrailingIconClick = { onAction(RegistrationAction.TogglePasswordVisible) - } + }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password) ) - FormPasswordField( + PrimaryTextField( title = stringResource(R.string.confirm_password_title), placeholder = stringResource(R.string.password_placeholder), value = form.confirmPassword, - isVisible = form.isConfirmPasswordVisible, onValueChange = { onAction(RegistrationAction.ConfirmPasswordChanged(it)) }, - onToggleVisibility = { + error = form.confirmPasswordError, + onFocusChanged = { hasFocus -> + onAction(RegistrationAction.ConfirmPasswordFocusChanged(hasFocus)) + }, + visualTransformation = if (form.isConfirmPasswordVisible) { + VisualTransformation.None + } else { + PasswordVisualTransformation() + }, + trailingIcon = rememberVectorPainter( + if (form.isConfirmPasswordVisible) { + Icons.Filled.VisibilityOff + } else { + Icons.Filled.Visibility + } + ), + onTrailingIconClick = { onAction(RegistrationAction.ToggleConfirmPasswordVisible) - } + }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password) ) ConsentRow( @@ -148,115 +221,13 @@ fun RegistrationScreen( PrimaryButton( onClick = { onAction(RegistrationAction.SubmitClicked) }, enabled = form.isSubmitEnabled, + isLoading = state is RegistrationUiState.Loading, modifier = Modifier.fillMaxWidth(), ) { Text(stringResource(R.string.registration_button)) } } } } -@Composable -private fun FormTextField( - title: String, - placeholder: String, - value: String, - onValueChange: (String) -> Unit, - keyboardType: KeyboardType, - modifier: Modifier = Modifier, -) { - Column(modifier = modifier, verticalArrangement = Arrangement.spacedBy(4.dp)) { - Text(text = title) - - OutlinedTextField( - modifier = Modifier.fillMaxWidth(), - value = value, - onValueChange = onValueChange, - placeholder = { - Text( - text = placeholder, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - }, - singleLine = true, - keyboardOptions = KeyboardOptions(keyboardType = keyboardType), - colors = - OutlinedTextFieldDefaults.colors( - focusedTextColor = - MaterialTheme.colorScheme.onSurfaceVariant, - unfocusedTextColor = - MaterialTheme.colorScheme.onSurfaceVariant - ) - ) - } -} - -@Composable -private fun FormPasswordField( - title: String, - placeholder: String, - value: String, - isVisible: Boolean, - onValueChange: (String) -> Unit, - onToggleVisibility: () -> Unit, - modifier: Modifier = Modifier, -) { - Column(modifier = modifier, verticalArrangement = Arrangement.spacedBy(4.dp)) { - Text(text = title) - - PasswordField( - placeholder = placeholder, - value = value, - isVisible = isVisible, - onValueChange = onValueChange, - onToggleVisibility = onToggleVisibility - ) - } -} - -@Composable -private fun PasswordField( - placeholder: String, - value: String, - isVisible: Boolean, - onValueChange: (String) -> Unit, - onToggleVisibility: () -> Unit, - modifier: Modifier = Modifier, -) { - OutlinedTextField( - modifier = modifier.fillMaxWidth(), - value = value, - onValueChange = onValueChange, - placeholder = { - Text(text = placeholder, color = MaterialTheme.colorScheme.onSurfaceVariant) - }, - singleLine = true, - visualTransformation = - if (isVisible) { - VisualTransformation.None - } else { - PasswordVisualTransformation() - }, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password), - trailingIcon = { - IconButton(onClick = onToggleVisibility) { - Icon( - imageVector = - if (isVisible) { - Icons.Filled.VisibilityOff - } else { - Icons.Filled.Visibility - }, - contentDescription = null - ) - } - }, - colors = - OutlinedTextFieldDefaults.colors( - focusedTextColor = MaterialTheme.colorScheme.onSurfaceVariant, - unfocusedTextColor = MaterialTheme.colorScheme.onSurfaceVariant - ) - ) -} - @Composable private fun pdConsentText(linkColor: Color): AnnotatedString = buildAnnotatedString { append(stringResource(R.string.pd_consent_prefix)) @@ -300,8 +271,8 @@ private fun ConsentRow( ClickableText( text = text, style = - MaterialTheme.typography.bodySmall.copy( - color = MaterialTheme.colorScheme.onSurface, + Theme.typography.body1.copy( + color = Theme.colors.black900, textDecoration = null ), onClick = { offset -> @@ -317,21 +288,28 @@ private fun ConsentRow( @Preview(showBackground = true) @Composable fun RegistrationScreenPreview() { - MaterialTheme { - RegistrationScreen( + YeaHubTheme { + RegistrationContent( state = RegistrationUiState.Content( RegistrationFormState( nickname = "admin", + nicknameError = null, email = "admin@mail.ru", + emailError = null, password = "1234", + passwordError = null, confirmPassword = "1234", + confirmPasswordError = null, isPasswordVisible = true, isConfirmPasswordVisible = true, isPdAccepted = true, isOfferAccepted = true, isMailingAccepted = false, - isSubmitEnabled = true + isSubmitEnabled = true, + isEmailTouched = true, + isPasswordTouched = true, + isConfirmPasswordTouched = true ) ), onAction = {}, From 0d5be7f732350ca7be07614281e449b5f37f468f Mon Sep 17 00:00:00 2001 From: MODDII Date: Tue, 9 Jun 2026 19:56:01 +0300 Subject: [PATCH 09/14] =?UTF-8?q?ANDR-51:=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20=D1=82=D0=B5=D1=81=D1=82=D0=BE?= =?UTF-8?q?=D0=B2=20=D0=B4=D0=BB=D1=8F=20=D1=84=D0=B8=D1=87=D0=B8=20=D1=80?= =?UTF-8?q?=D0=B5=D0=B3=D0=B8=D1=81=D1=82=D1=80=D0=B0=D1=86=D0=B8=D0=B8.?= =?UTF-8?q?=20Unit-=D1=82=D0=B5=D1=81=D1=82=D1=8B=20=D0=B4=D0=BB=D1=8F=20?= =?UTF-8?q?=D0=B2=D0=B5=D1=80=D0=B8=D1=84=D0=B8=D0=BA=D0=B0=D1=86=D0=B8?= =?UTF-8?q?=D0=B8=20=D0=BB=D0=BE=D0=B3=D0=B8=D0=BA=D0=B8=20=D1=80=D0=B5?= =?UTF-8?q?=D0=B3=D0=B8=D1=81=D1=82=D1=80=D0=B0=D1=86=D0=B8=D0=B8.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../RegistrationDomainToDataMapperTest.kt | 31 ++++ .../RegistrationUiStateMapperTest.kt | 158 ++++++++++++++++++ 2 files changed, 189 insertions(+) create mode 100644 feature/authentication/impl/src/test/java/ru/yeahub/authentication/impl/registration/data/mapper/RegistrationDomainToDataMapperTest.kt create mode 100644 feature/authentication/impl/src/test/java/ru/yeahub/authentication/impl/registration/presentation/RegistrationUiStateMapperTest.kt diff --git a/feature/authentication/impl/src/test/java/ru/yeahub/authentication/impl/registration/data/mapper/RegistrationDomainToDataMapperTest.kt b/feature/authentication/impl/src/test/java/ru/yeahub/authentication/impl/registration/data/mapper/RegistrationDomainToDataMapperTest.kt new file mode 100644 index 00000000..95b05b6c --- /dev/null +++ b/feature/authentication/impl/src/test/java/ru/yeahub/authentication/impl/registration/data/mapper/RegistrationDomainToDataMapperTest.kt @@ -0,0 +1,31 @@ +package ru.yeahub.authentication.impl.registration.data.mapper + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import ru.yeahub.authentication.impl.registration.domain.entity.RegistrationModel +import ru.yeahub.network_api.models.RegistrationRequestDto + +class RegistrationDomainToDataMapperTest { + + private val mapper = RegistrationDomainToDataMapper() + + @Test + fun `map RegistrationModel to RegistrationRequestDto correctly`() { + val model = RegistrationModel( + nickname = "testuser", + email = "test@mail.ru", + password = "Password123!", + isMailingAccepted = true + ) + + val expected = RegistrationRequestDto( + username = "testuser", + email = "test@mail.ru", + password = "Password123!" + ) + + val result = mapper.map(model) + + assertEquals(expected, result) + } +} diff --git a/feature/authentication/impl/src/test/java/ru/yeahub/authentication/impl/registration/presentation/RegistrationUiStateMapperTest.kt b/feature/authentication/impl/src/test/java/ru/yeahub/authentication/impl/registration/presentation/RegistrationUiStateMapperTest.kt new file mode 100644 index 00000000..8588321e --- /dev/null +++ b/feature/authentication/impl/src/test/java/ru/yeahub/authentication/impl/registration/presentation/RegistrationUiStateMapperTest.kt @@ -0,0 +1,158 @@ +package ru.yeahub.authentication.impl.registration.presentation + +import io.mockk.every +import io.mockk.mockkObject +import io.mockk.unmockkAll +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource +import ru.yeahub.authentication.impl.R +import ru.yeahub.core_utils.common.TextOrResource +import ru.yeahub.core_utils.validation.EmailValidator +import java.util.stream.Stream + +class RegistrationUiStateMapperTest { + + private val mapper = RegistrationUiStateMapper() + + @BeforeEach + fun setUp() { + mockkObject(EmailValidator) + every { EmailValidator.isValid(any()) } returns true + } + + @AfterEach + fun tearDown() { + unmockkAll() + } + + @Test + fun `mapToInitialState returns correct initial state`() { + val state = mapper.mapToInitialState() + + assertTrue(state is RegistrationUiState.Content) + val form = state.formState + assertEquals("", form.nickname) + assertEquals("", form.email) + assertEquals("", form.password) + assertEquals("", form.confirmPassword) + assertFalse(form.isPdAccepted) + assertFalse(form.isOfferAccepted) + assertFalse(form.isSubmitEnabled) + assertNull(form.emailError) + assertNull(form.nicknameError) + assertNull(form.passwordError) + assertNull(form.confirmPasswordError) + } + + @ParameterizedTest + @MethodSource("provideValidationCases") + fun `validateForm logic works correctly`( + nickname: String, + email: String, + isEmailValid: Boolean, + password: String, + confirmPassword: String, + isPdAccepted: Boolean, + isOfferAccepted: Boolean, + expectedSubmitEnabled: Boolean + ) { + // Arrange + every { EmailValidator.isValid(any()) } returns isEmailValid + + val form = mapper.getInitialFormState().copy( + nickname = nickname, + email = email, + password = password, + confirmPassword = confirmPassword, + isPdAccepted = isPdAccepted, + isOfferAccepted = isOfferAccepted + ) + val currentState = RegistrationUiState.Content(form) + + // Act - trigger validation by any action that updates state + val resultState = mapper.mapToUpdatedState(currentState, RegistrationAction.NicknameChanged(nickname)) + + // Assert + assertEquals(expectedSubmitEnabled, resultState.formState.isSubmitEnabled) + } + + companion object { + @JvmStatic + fun provideValidationCases(): Stream = Stream.of( + // nickname, email, isEmailValid, password, confirmPassword, isPdAccepted, isOfferAccepted, expectedSubmitEnabled + Arguments.of("user", "test@test.com", true, "Pass123!", "Pass123!", true, true, true), + Arguments.of("", "test@test.com", true, "Pass123!", "Pass123!", true, true, false), // Empty nickname + Arguments.of("user", "invalid", false, "Pass123!", "Pass123!", true, true, false), // Invalid email + Arguments.of("user", "test@test.com", true, "short", "short", true, true, false), // Short password + Arguments.of("user", "test@test.com", true, "Pass123!", "Mismatch!", true, true, false), // Password mismatch + Arguments.of("user", "test@test.com", true, "short", "short", true, true, false), // Too short + Arguments.of("user", "test@test.com", true, "nospecial123", "nospecial123", true, true, false), // No special + Arguments.of("user", "test@test.com", true, "NoDigit!", "NoDigit!", true, true, false), // No digit + Arguments.of("user", "test@test.com", true, "nouppercase1!", "nouppercase1!", true, true, false), // No uppercase + Arguments.of("user", "test@test.com", true, "Pass123!", "Pass123!", false, true, false), // PD not accepted + Arguments.of("user", "test@test.com", true, "Pass123!", "Pass123!", true, false, false) // Offer not accepted + ) + + @JvmStatic + fun providePasswordErrorCases(): Stream = Stream.of( + Arguments.of("short", R.string.error_password_too_short), + Arguments.of("NoSpecial123", R.string.error_password_no_special_char), + Arguments.of("NoDigit!", R.string.error_password_no_digit), + Arguments.of("nouppercase1!", R.string.error_password_no_uppercase) + ) + } + + @ParameterizedTest + @MethodSource("providePasswordErrorCases") + fun `validateForm returns correct error resource for specific password failures`( + password: String, + expectedErrorRes: Int + ) { + // Arrange + val form = mapper.getInitialFormState().copy( + password = password, + isPasswordTouched = true // Error only shows if touched + ) + val currentState = RegistrationUiState.Content(form) + + // Act + val resultState = mapper.mapToUpdatedState(currentState, RegistrationAction.PasswordChanged(password)) + + // Assert + val error = resultState.formState.passwordError as? TextOrResource.Resource + assertEquals(expectedErrorRes, error?.resource) + } + + @Test + fun `mapToLoadingState preserves form state`() { + val form = mapper.getInitialFormState().copy(nickname = "some user") + val currentState = RegistrationUiState.Content(form) + + val resultState = mapper.mapToLoadingState(currentState) + + assertTrue(resultState is RegistrationUiState.Loading) + assertEquals(form, resultState.formState) + } + + @Test + fun `handleFocusAction updates touched state correctly`() { + val form = mapper.getInitialFormState().copy(email = "test") + val currentState = RegistrationUiState.Content(form) + + // Email lost focus + val stateAfterFocusLost = mapper.mapToUpdatedState(currentState, RegistrationAction.EmailFocusChanged(false)) + assertTrue(stateAfterFocusLost.formState.isEmailTouched) + + // Email gained focus + val stateAfterFocusGained = mapper.mapToUpdatedState(stateAfterFocusLost, RegistrationAction.EmailFocusChanged(true)) + assertFalse(stateAfterFocusGained.formState.isEmailTouched) + } +} From 8d38a3f0323ef88477ab00bc1ebcda2bfd9b1587 Mon Sep 17 00:00:00 2001 From: MODDII Date: Tue, 9 Jun 2026 20:21:44 +0300 Subject: [PATCH 10/14] =?UTF-8?q?ANDR-51:=20=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BF=D1=80=D0=B5=D0=B2=D1=8C?= =?UTF-8?q?=D1=8E=20=D1=80=D0=B5=D0=B3=D0=B8=D1=81=D1=82=D1=80=D0=B0=D1=86?= =?UTF-8?q?=D0=B8=D0=B8=20=D0=B4=D0=BB=D1=8F=20=D0=B2=D1=81=D0=B5=D1=85=20?= =?UTF-8?q?=D1=84=D0=BE=D1=80=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/RegistrationScreen.kt | 115 ++++++++++++++++-- 1 file changed, 108 insertions(+), 7 deletions(-) diff --git a/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/registration/presentation/RegistrationScreen.kt b/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/registration/presentation/RegistrationScreen.kt index 93d598fb..da4bad11 100644 --- a/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/registration/presentation/RegistrationScreen.kt +++ b/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/registration/presentation/RegistrationScreen.kt @@ -285,27 +285,128 @@ private fun ConsentRow( } } -@Preview(showBackground = true) +@Preview(showBackground = true, name = "Empty State") @Composable -fun RegistrationScreenPreview() { +fun RegistrationScreenPreview_Empty() { + YeaHubTheme { + RegistrationContent( + state = RegistrationUiState.Content( + RegistrationFormState( + nickname = "", + nicknameError = null, + email = "", + emailError = null, + password = "", + passwordError = null, + confirmPassword = "", + confirmPasswordError = null, + isPdAccepted = false, + isOfferAccepted = false, + isMailingAccepted = false, + isPasswordVisible = false, + isConfirmPasswordVisible = false, + isSubmitEnabled = false, + isEmailTouched = false, + isPasswordTouched = false, + isConfirmPasswordTouched = false + ) + ), + onAction = {}, + onOpenPdPolicy = {}, + onOpenOffer = {} + ) + } +} + +@Preview(showBackground = true, name = "Valid State") +@Composable +fun RegistrationScreenPreview_Valid() { YeaHubTheme { RegistrationContent( state = RegistrationUiState.Content( RegistrationFormState( - nickname = "admin", + nickname = "JohnDoe", nicknameError = null, - email = "admin@mail.ru", + email = "john@example.com", emailError = null, - password = "1234", + password = "Password123!", passwordError = null, - confirmPassword = "1234", + confirmPassword = "Password123!", confirmPasswordError = null, + isPasswordVisible = false, + isConfirmPasswordVisible = false, + isPdAccepted = true, + isOfferAccepted = true, + isMailingAccepted = true, + isSubmitEnabled = true, + isEmailTouched = true, + isPasswordTouched = true, + isConfirmPasswordTouched = true + ) + ), + onAction = {}, + onOpenPdPolicy = {}, + onOpenOffer = {} + ) + } +} + +@Preview(showBackground = true, name = "Error State") +@Composable +fun RegistrationScreenPreview_Error() { + YeaHubTheme { + RegistrationContent( + state = + RegistrationUiState.Error( + formState = RegistrationFormState( + nickname = "", + nicknameError = ru.yeahub.core_utils.common.TextOrResource.Text("Имя пользователя не может быть пустым"), + email = "invalid-email", + emailError = ru.yeahub.core_utils.common.TextOrResource.Text("Некорректный email"), + password = "123", + passwordError = ru.yeahub.core_utils.common.TextOrResource.Text("Пароль слишком короткий"), + confirmPassword = "456", + confirmPasswordError = ru.yeahub.core_utils.common.TextOrResource.Text("Пароли не совпадают"), isPasswordVisible = true, isConfirmPasswordVisible = true, + isPdAccepted = false, + isOfferAccepted = false, + isMailingAccepted = false, + isSubmitEnabled = false, + isEmailTouched = true, + isPasswordTouched = true, + isConfirmPasswordTouched = true + ) + ), + onAction = {}, + onOpenPdPolicy = {}, + onOpenOffer = {} + ) + } +} + +@Preview(showBackground = true, name = "Loading State") +@Composable +fun RegistrationScreenPreview_Loading() { + YeaHubTheme { + RegistrationContent( + state = + RegistrationUiState.Loading( + RegistrationFormState( + nickname = "JohnDoe", + nicknameError = null, + email = "john@example.com", + emailError = null, + password = "Password123!", + passwordError = null, + confirmPassword = "Password123!", + confirmPasswordError = null, + isPasswordVisible = false, + isConfirmPasswordVisible = false, isPdAccepted = true, isOfferAccepted = true, - isMailingAccepted = false, + isMailingAccepted = true, isSubmitEnabled = true, isEmailTouched = true, isPasswordTouched = true, From e25549035eadea03b8ac5f95251e658d4a3e5b7f Mon Sep 17 00:00:00 2001 From: MODDII Date: Tue, 9 Jun 2026 20:45:36 +0300 Subject: [PATCH 11/14] =?UTF-8?q?ANDR-51:=20=D0=98=D1=81=D0=BF=D1=80=D0=B0?= =?UTF-8?q?=D0=B2=D0=BB=D0=B5=D0=BD=D1=8B=20=D0=BE=D1=88=D0=B8=D0=B1=D0=BA?= =?UTF-8?q?=D0=B8=20detekt=20=D0=B8=20ktlint?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/RegistrationScreen.kt | 9 +- .../RegistrationUiStateMapperTest.kt | 142 +++++++++++++++--- 2 files changed, 124 insertions(+), 27 deletions(-) diff --git a/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/registration/presentation/RegistrationScreen.kt b/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/registration/presentation/RegistrationScreen.kt index da4bad11..f514071d 100644 --- a/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/registration/presentation/RegistrationScreen.kt +++ b/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/registration/presentation/RegistrationScreen.kt @@ -43,6 +43,7 @@ import ru.yeahub.core_ui.component.PrimaryButton import ru.yeahub.core_ui.component.PrimaryTextField import ru.yeahub.core_ui.theme.Theme import ru.yeahub.core_ui.theme.YeaHubTheme +import ru.yeahub.core_utils.common.TextOrResource @Composable fun RegistrationScreen( @@ -361,13 +362,13 @@ fun RegistrationScreenPreview_Error() { RegistrationUiState.Error( formState = RegistrationFormState( nickname = "", - nicknameError = ru.yeahub.core_utils.common.TextOrResource.Text("Имя пользователя не может быть пустым"), + nicknameError = TextOrResource.Text("Имя пользователя не может быть пустым"), email = "invalid-email", - emailError = ru.yeahub.core_utils.common.TextOrResource.Text("Некорректный email"), + emailError = TextOrResource.Text("Некорректный email"), password = "123", - passwordError = ru.yeahub.core_utils.common.TextOrResource.Text("Пароль слишком короткий"), + passwordError = TextOrResource.Text("Пароль слишком короткий"), confirmPassword = "456", - confirmPasswordError = ru.yeahub.core_utils.common.TextOrResource.Text("Пароли не совпадают"), + confirmPasswordError = TextOrResource.Text("Пароли не совпадают"), isPasswordVisible = true, isConfirmPasswordVisible = true, isPdAccepted = false, diff --git a/feature/authentication/impl/src/test/java/ru/yeahub/authentication/impl/registration/presentation/RegistrationUiStateMapperTest.kt b/feature/authentication/impl/src/test/java/ru/yeahub/authentication/impl/registration/presentation/RegistrationUiStateMapperTest.kt index 8588321e..6211ee57 100644 --- a/feature/authentication/impl/src/test/java/ru/yeahub/authentication/impl/registration/presentation/RegistrationUiStateMapperTest.kt +++ b/feature/authentication/impl/src/test/java/ru/yeahub/authentication/impl/registration/presentation/RegistrationUiStateMapperTest.kt @@ -64,7 +64,6 @@ class RegistrationUiStateMapperTest { isOfferAccepted: Boolean, expectedSubmitEnabled: Boolean ) { - // Arrange every { EmailValidator.isValid(any()) } returns isEmailValid val form = mapper.getInitialFormState().copy( @@ -77,28 +76,124 @@ class RegistrationUiStateMapperTest { ) val currentState = RegistrationUiState.Content(form) - // Act - trigger validation by any action that updates state val resultState = mapper.mapToUpdatedState(currentState, RegistrationAction.NicknameChanged(nickname)) - // Assert assertEquals(expectedSubmitEnabled, resultState.formState.isSubmitEnabled) } companion object { @JvmStatic fun provideValidationCases(): Stream = Stream.of( - // nickname, email, isEmailValid, password, confirmPassword, isPdAccepted, isOfferAccepted, expectedSubmitEnabled - Arguments.of("user", "test@test.com", true, "Pass123!", "Pass123!", true, true, true), - Arguments.of("", "test@test.com", true, "Pass123!", "Pass123!", true, true, false), // Empty nickname - Arguments.of("user", "invalid", false, "Pass123!", "Pass123!", true, true, false), // Invalid email - Arguments.of("user", "test@test.com", true, "short", "short", true, true, false), // Short password - Arguments.of("user", "test@test.com", true, "Pass123!", "Mismatch!", true, true, false), // Password mismatch - Arguments.of("user", "test@test.com", true, "short", "short", true, true, false), // Too short - Arguments.of("user", "test@test.com", true, "nospecial123", "nospecial123", true, true, false), // No special - Arguments.of("user", "test@test.com", true, "NoDigit!", "NoDigit!", true, true, false), // No digit - Arguments.of("user", "test@test.com", true, "nouppercase1!", "nouppercase1!", true, true, false), // No uppercase - Arguments.of("user", "test@test.com", true, "Pass123!", "Pass123!", false, true, false), // PD not accepted - Arguments.of("user", "test@test.com", true, "Pass123!", "Pass123!", true, false, false) // Offer not accepted + Arguments.of( + "user", + "test@test.com", + true, + "Pass123!", + "Pass123!", + true, + true, + true + ), + Arguments.of( + "", + "test@test.com", + true, + "Pass123!", + "Pass123!", + true, + true, + false + ), + Arguments.of( + "user", + "invalid", + false, + "Pass123!", + "Pass123!", + true, + true, + false + ), + Arguments.of( + "user", + "test@test.com", + true, + "short", + "short", + true, + true, + false + ), + Arguments.of( + "user", + "test@test.com", + true, + "Pass123!", + "Mismatch!", + true, + true, + false + ), + Arguments.of( + "user", + "test@test.com", + true, + "short", + "short", + true, + true, + false + ), + Arguments.of( + "user", + "test@test.com", + true, + "nospecial123", + "nospecial123", + true, + true, + false + ), + Arguments.of( + "user", + "test@test.com", + true, + "NoDigit!", + "NoDigit!", + true, + true, + false + ), + Arguments.of( + "user", + "test@test.com", + true, + "nouppercase1!", + "nouppercase1!", + true, + true, + false + ), + Arguments.of( + "user", + "test@test.com", + true, + "Pass123!", + "Pass123!", + false, + true, + false + ), + Arguments.of( + "user", + "test@test.com", + true, + "Pass123!", + "Pass123!", + true, + false, + false + ) ) @JvmStatic @@ -116,17 +211,14 @@ class RegistrationUiStateMapperTest { password: String, expectedErrorRes: Int ) { - // Arrange val form = mapper.getInitialFormState().copy( password = password, - isPasswordTouched = true // Error only shows if touched + isPasswordTouched = true ) val currentState = RegistrationUiState.Content(form) - // Act val resultState = mapper.mapToUpdatedState(currentState, RegistrationAction.PasswordChanged(password)) - // Assert val error = resultState.formState.passwordError as? TextOrResource.Resource assertEquals(expectedErrorRes, error?.resource) } @@ -147,12 +239,16 @@ class RegistrationUiStateMapperTest { val form = mapper.getInitialFormState().copy(email = "test") val currentState = RegistrationUiState.Content(form) - // Email lost focus - val stateAfterFocusLost = mapper.mapToUpdatedState(currentState, RegistrationAction.EmailFocusChanged(false)) + val stateAfterFocusLost = mapper.mapToUpdatedState( + currentState, + RegistrationAction.EmailFocusChanged(false) + ) assertTrue(stateAfterFocusLost.formState.isEmailTouched) - // Email gained focus - val stateAfterFocusGained = mapper.mapToUpdatedState(stateAfterFocusLost, RegistrationAction.EmailFocusChanged(true)) + val stateAfterFocusGained = mapper.mapToUpdatedState( + stateAfterFocusLost, + RegistrationAction.EmailFocusChanged(true) + ) assertFalse(stateAfterFocusGained.formState.isEmailTouched) } } From 0c669cceb33a3f2aad471ef417cd878fc0bfc175 Mon Sep 17 00:00:00 2001 From: MODDII Date: Thu, 11 Jun 2026 19:51:12 +0300 Subject: [PATCH 12/14] =?UTF-8?q?ANDR-51=20=D0=A3=D0=B4=D0=B0=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20=D1=83=D1=81=D1=82=D0=B0=D1=80=D0=B5=D0=B2?= =?UTF-8?q?=D1=88=D0=B5=D0=B3=D0=BE=20=D0=BC=D0=BE=D0=B4=D1=83=D0=BB=D1=8F?= =?UTF-8?q?=20forgot-password=20=D0=B8=20=D0=BE=D1=87=D0=B8=D1=81=D1=82?= =?UTF-8?q?=D0=BA=D0=B0=20=D0=BA=D0=BE=D0=BD=D1=84=D0=B8=D0=B3=D1=83=D1=80?= =?UTF-8?q?=D0=B0=D1=86=D0=B8=D0=B8=20=D0=BF=D1=80=D0=BE=D0=B5=D0=BA=D1=82?= =?UTF-8?q?=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle.kts | 2 - feature/forgot-password/.gitignore | 1 - feature/forgot-password/api/.gitignore | 1 - feature/forgot-password/api/build.gradle.kts | 44 ----- .../forgot-password/api/consumer-rules.pro | 0 .../api/src/main/AndroidManifest.xml | 4 - feature/forgot-password/build.gradle.kts | 44 ----- feature/forgot-password/consumer-rules.pro | 0 feature/forgot-password/impl/.gitignore | 1 - feature/forgot-password/impl/build.gradle.kts | 61 ------ .../forgot-password/impl/consumer-rules.pro | 0 .../impl/src/main/AndroidManifest.xml | 4 - .../impl/data/dto/ForgotPasswordRequestDto.kt | 5 - .../data/dto/ForgotPasswordResponseDto.kt | 6 - .../impl/data/mapper/ForgotPasswordMapper.kt | 14 -- .../ru/yeahub/impl/data/remote/AuthApi.kt | 8 - .../ForgotPasswordRepositoryImpl.kt | 48 ----- .../yeahub/impl/di/ForgotPasswordFactory.kt | 23 --- .../impl/domain/ForgotPasswordRepository.kt | 6 - .../impl/domain/ForgotPasswordResult.kt | 6 - .../impl/domain/SendResetLinkUseCase.kt | 15 -- .../presentation/ForgotPasswordViewModel.kt | 123 ------------- .../intents/ForgotPasswordCommand.kt | 7 - .../intents/ForgotPasswordEvent.kt | 7 - .../presentation/mapper/EmailValidator.kt | 28 --- .../mapper/ForgotPasswordScreenMapper.kt | 41 ----- .../state/ForgotPasswordScreenState.kt | 14 -- .../presentation/state/ForgotPasswordState.kt | 9 - .../impl/ui/ForgotPasswordModalWindow.kt | 173 ------------------ .../ru/yeahub/impl/ui/ForgotPasswordRoute.kt | 33 ---- .../ru/yeahub/impl/ui/ForgotPasswordScreen.kt | 173 ------------------ .../impl/src/main/res/drawable/close.jpg | Bin 7063 -> 0 bytes .../impl/src/main/res/drawable/letter.jpg | Bin 13437 -> 0 bytes .../impl/src/main/res/values/strings.xml | 16 -- .../src/main/AndroidManifest.xml | 4 - settings.gradle.kts | 4 +- 36 files changed, 1 insertion(+), 924 deletions(-) delete mode 100644 feature/forgot-password/.gitignore delete mode 100644 feature/forgot-password/api/.gitignore delete mode 100644 feature/forgot-password/api/build.gradle.kts delete mode 100644 feature/forgot-password/api/consumer-rules.pro delete mode 100644 feature/forgot-password/api/src/main/AndroidManifest.xml delete mode 100644 feature/forgot-password/build.gradle.kts delete mode 100644 feature/forgot-password/consumer-rules.pro delete mode 100644 feature/forgot-password/impl/.gitignore delete mode 100644 feature/forgot-password/impl/build.gradle.kts delete mode 100644 feature/forgot-password/impl/consumer-rules.pro delete mode 100644 feature/forgot-password/impl/src/main/AndroidManifest.xml delete mode 100644 feature/forgot-password/impl/src/main/java/ru/yeahub/impl/data/dto/ForgotPasswordRequestDto.kt delete mode 100644 feature/forgot-password/impl/src/main/java/ru/yeahub/impl/data/dto/ForgotPasswordResponseDto.kt delete mode 100644 feature/forgot-password/impl/src/main/java/ru/yeahub/impl/data/mapper/ForgotPasswordMapper.kt delete mode 100644 feature/forgot-password/impl/src/main/java/ru/yeahub/impl/data/remote/AuthApi.kt delete mode 100644 feature/forgot-password/impl/src/main/java/ru/yeahub/impl/data/repository/ForgotPasswordRepositoryImpl.kt delete mode 100644 feature/forgot-password/impl/src/main/java/ru/yeahub/impl/di/ForgotPasswordFactory.kt delete mode 100644 feature/forgot-password/impl/src/main/java/ru/yeahub/impl/domain/ForgotPasswordRepository.kt delete mode 100644 feature/forgot-password/impl/src/main/java/ru/yeahub/impl/domain/ForgotPasswordResult.kt delete mode 100644 feature/forgot-password/impl/src/main/java/ru/yeahub/impl/domain/SendResetLinkUseCase.kt delete mode 100644 feature/forgot-password/impl/src/main/java/ru/yeahub/impl/presentation/ForgotPasswordViewModel.kt delete mode 100644 feature/forgot-password/impl/src/main/java/ru/yeahub/impl/presentation/intents/ForgotPasswordCommand.kt delete mode 100644 feature/forgot-password/impl/src/main/java/ru/yeahub/impl/presentation/intents/ForgotPasswordEvent.kt delete mode 100644 feature/forgot-password/impl/src/main/java/ru/yeahub/impl/presentation/mapper/EmailValidator.kt delete mode 100644 feature/forgot-password/impl/src/main/java/ru/yeahub/impl/presentation/mapper/ForgotPasswordScreenMapper.kt delete mode 100644 feature/forgot-password/impl/src/main/java/ru/yeahub/impl/presentation/state/ForgotPasswordScreenState.kt delete mode 100644 feature/forgot-password/impl/src/main/java/ru/yeahub/impl/presentation/state/ForgotPasswordState.kt delete mode 100644 feature/forgot-password/impl/src/main/java/ru/yeahub/impl/ui/ForgotPasswordModalWindow.kt delete mode 100644 feature/forgot-password/impl/src/main/java/ru/yeahub/impl/ui/ForgotPasswordRoute.kt delete mode 100644 feature/forgot-password/impl/src/main/java/ru/yeahub/impl/ui/ForgotPasswordScreen.kt delete mode 100644 feature/forgot-password/impl/src/main/res/drawable/close.jpg delete mode 100644 feature/forgot-password/impl/src/main/res/drawable/letter.jpg delete mode 100644 feature/forgot-password/impl/src/main/res/values/strings.xml delete mode 100644 feature/forgot-password/src/main/AndroidManifest.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 1a061bd4..9aa5c368 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -98,8 +98,6 @@ dependencies { implementation(project(":feature:questions-or-collections:api")) implementation(project(":feature:public-collections:api")) implementation(project(":feature:selection-specializations:api")) -// implementation(project(":feature:forgot-password:api")) -// implementation(project(":feature:forgot-password:impl")) implementation(project(":feature:example-home:impl")) implementation(project(":feature:example-profile:impl")) implementation(project(":feature:example-questions:impl")) diff --git a/feature/forgot-password/.gitignore b/feature/forgot-password/.gitignore deleted file mode 100644 index 42afabfd..00000000 --- a/feature/forgot-password/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build \ No newline at end of file diff --git a/feature/forgot-password/api/.gitignore b/feature/forgot-password/api/.gitignore deleted file mode 100644 index 42afabfd..00000000 --- a/feature/forgot-password/api/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build \ No newline at end of file diff --git a/feature/forgot-password/api/build.gradle.kts b/feature/forgot-password/api/build.gradle.kts deleted file mode 100644 index b4a39edf..00000000 --- a/feature/forgot-password/api/build.gradle.kts +++ /dev/null @@ -1,44 +0,0 @@ -plugins { - alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) -} - -android { - namespace = "ru.yeahub.api" - compileSdk { - version = release(35) - } - - defaultConfig { - minSdk = 24 - - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - consumerProguardFiles("consumer-rules.pro") - } - - buildTypes { - release { - isMinifyEnabled = false - proguardFiles( - getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro" - ) - } - } - compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 - } - kotlinOptions { - jvmTarget = "11" - } -} - -dependencies { - implementation(libs.androidx.core.ktx) - implementation(libs.androidx.appcompat) - implementation(libs.material) - testImplementation(libs.junit) - androidTestImplementation(libs.androidx.junit) - androidTestImplementation(libs.androidx.espresso.core) -} \ No newline at end of file diff --git a/feature/forgot-password/api/consumer-rules.pro b/feature/forgot-password/api/consumer-rules.pro deleted file mode 100644 index e69de29b..00000000 diff --git a/feature/forgot-password/api/src/main/AndroidManifest.xml b/feature/forgot-password/api/src/main/AndroidManifest.xml deleted file mode 100644 index a5918e68..00000000 --- a/feature/forgot-password/api/src/main/AndroidManifest.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/feature/forgot-password/build.gradle.kts b/feature/forgot-password/build.gradle.kts deleted file mode 100644 index c6d924a4..00000000 --- a/feature/forgot-password/build.gradle.kts +++ /dev/null @@ -1,44 +0,0 @@ -plugins { - alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) -} - -android { - namespace = "ru.yeahub.forgot_password" - compileSdk { - version = release(35) - } - - defaultConfig { - minSdk = 24 - - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - consumerProguardFiles("consumer-rules.pro") - } - - buildTypes { - release { - isMinifyEnabled = false - proguardFiles( - getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro" - ) - } - } - compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 - } - kotlinOptions { - jvmTarget = "11" - } -} - -dependencies { - implementation(libs.androidx.core.ktx) - implementation(libs.androidx.appcompat) - implementation(libs.material) - testImplementation(libs.junit) - androidTestImplementation(libs.androidx.junit) - androidTestImplementation(libs.androidx.espresso.core) -} \ No newline at end of file diff --git a/feature/forgot-password/consumer-rules.pro b/feature/forgot-password/consumer-rules.pro deleted file mode 100644 index e69de29b..00000000 diff --git a/feature/forgot-password/impl/.gitignore b/feature/forgot-password/impl/.gitignore deleted file mode 100644 index 42afabfd..00000000 --- a/feature/forgot-password/impl/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build \ No newline at end of file diff --git a/feature/forgot-password/impl/build.gradle.kts b/feature/forgot-password/impl/build.gradle.kts deleted file mode 100644 index 290ccdc7..00000000 --- a/feature/forgot-password/impl/build.gradle.kts +++ /dev/null @@ -1,61 +0,0 @@ -plugins { - alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) - alias(libs.plugins.kotlin.compose) -} - -android { - namespace = "ru.yeahub.impl" - compileSdk = 35 - - defaultConfig { - minSdk = 24 - - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - consumerProguardFiles("consumer-rules.pro") - } - - buildTypes { - release { - isMinifyEnabled = false - proguardFiles( - getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro" - ) - } - } - compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 - } - kotlinOptions { - jvmTarget = "11" - } - buildFeatures { - compose = true - } - composeOptions { - kotlinCompilerExtensionVersion = "1.5.8" - } -} - -dependencies { - implementation(libs.androidx.core.ktx) - implementation(libs.androidx.appcompat) - implementation(libs.material) - implementation(libs.androidx.compose.runtime) - implementation(libs.androidx.compose.ui) - implementation(libs.androidx.compose.foundation) - implementation(libs.androidx.compose.material3) - implementation(libs.timber) - implementation(project(":core:ui")) - - // Compose Preview dependencies - implementation(libs.androidx.ui.tooling.preview) - debugImplementation(libs.androidx.ui.tooling) - debugImplementation(libs.androidx.ui.test.manifest) - - testImplementation(libs.junit) - androidTestImplementation(libs.androidx.junit) - androidTestImplementation(libs.androidx.espresso.core) -} \ No newline at end of file diff --git a/feature/forgot-password/impl/consumer-rules.pro b/feature/forgot-password/impl/consumer-rules.pro deleted file mode 100644 index e69de29b..00000000 diff --git a/feature/forgot-password/impl/src/main/AndroidManifest.xml b/feature/forgot-password/impl/src/main/AndroidManifest.xml deleted file mode 100644 index a5918e68..00000000 --- a/feature/forgot-password/impl/src/main/AndroidManifest.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/data/dto/ForgotPasswordRequestDto.kt b/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/data/dto/ForgotPasswordRequestDto.kt deleted file mode 100644 index 0517819a..00000000 --- a/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/data/dto/ForgotPasswordRequestDto.kt +++ /dev/null @@ -1,5 +0,0 @@ -package ru.yeahub.impl.data.dto - -data class ForgotPasswordRequestDto( - val email: String -) \ No newline at end of file diff --git a/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/data/dto/ForgotPasswordResponseDto.kt b/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/data/dto/ForgotPasswordResponseDto.kt deleted file mode 100644 index cfc5132c..00000000 --- a/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/data/dto/ForgotPasswordResponseDto.kt +++ /dev/null @@ -1,6 +0,0 @@ -package ru.yeahub.impl.data.dto - -data class ForgotPasswordResponseDto( - val ok: Boolean, - val message: String -) \ No newline at end of file diff --git a/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/data/mapper/ForgotPasswordMapper.kt b/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/data/mapper/ForgotPasswordMapper.kt deleted file mode 100644 index ce20fabe..00000000 --- a/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/data/mapper/ForgotPasswordMapper.kt +++ /dev/null @@ -1,14 +0,0 @@ -package ru.yeahub.impl.data.mapper - -import ru.yeahub.impl.data.dto.ForgotPasswordResponseDto -import ru.yeahub.impl.domain.ForgotPasswordResult - -class ForgotPasswordMapper { - fun toDomain(responseDto: ForgotPasswordResponseDto): ForgotPasswordResult { - return if (responseDto.ok) { - ForgotPasswordResult.Success - } else { - ForgotPasswordResult.Error(responseDto.message) - } - } -} \ No newline at end of file diff --git a/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/data/remote/AuthApi.kt b/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/data/remote/AuthApi.kt deleted file mode 100644 index b673889e..00000000 --- a/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/data/remote/AuthApi.kt +++ /dev/null @@ -1,8 +0,0 @@ -package ru.yeahub.impl.data.remote - -import ru.yeahub.impl.data.dto.ForgotPasswordRequestDto -import ru.yeahub.impl.data.dto.ForgotPasswordResponseDto - -interface AuthApi { - suspend fun forgotPassword(request: ForgotPasswordRequestDto): ForgotPasswordResponseDto -} \ No newline at end of file diff --git a/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/data/repository/ForgotPasswordRepositoryImpl.kt b/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/data/repository/ForgotPasswordRepositoryImpl.kt deleted file mode 100644 index 118a9cae..00000000 --- a/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/data/repository/ForgotPasswordRepositoryImpl.kt +++ /dev/null @@ -1,48 +0,0 @@ -package ru.yeahub.impl.data.repository - -import kotlinx.coroutines.CancellationException -import java.io.IOException -import ru.yeahub.impl.data.dto.ForgotPasswordRequestDto -import ru.yeahub.impl.data.mapper.ForgotPasswordMapper -import ru.yeahub.impl.data.remote.AuthApi -import ru.yeahub.impl.domain.ForgotPasswordRepository -import ru.yeahub.impl.domain.ForgotPasswordResult -import timber.log.Timber - -class ForgotPasswordRepositoryImpl( - private val api: AuthApi, - private val mapper: ForgotPasswordMapper -) : ForgotPasswordRepository { - - @Suppress("TooGenericExceptionCaught") - override suspend fun sendResetLink(email: String): ForgotPasswordResult { - return try { - val dto = api.forgotPassword(ForgotPasswordRequestDto(email)) - mapper.toDomain(dto) - } catch (e: CancellationException) { - throw e - } catch (e: IOException) { - Timber.e( - "Network error sending reset link for email: $email," + - "error: ${e.message}" - ) - ForgotPasswordResult.Error( - "Ошибка сети. Проверьте подключение к интернету." - ) - } catch (e: IllegalArgumentException) { - Timber.e( - "Invalid data error sending reset link for email: $email, " + - "error: ${e.message}" - ) - ForgotPasswordResult.Error( - "Некорректные данные. Проверьте введенный email." - ) - } catch (e: Exception) { - Timber.e( - "Unexpected error sending reset link for email: $email," + - "error: ${e.message}" - ) - ForgotPasswordResult.Error("Неизвестная ошибка. Попробуйте позже.") - } - } -} \ No newline at end of file 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/domain/ForgotPasswordRepository.kt b/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/domain/ForgotPasswordRepository.kt deleted file mode 100644 index 07aef5e8..00000000 --- a/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/domain/ForgotPasswordRepository.kt +++ /dev/null @@ -1,6 +0,0 @@ -package ru.yeahub.impl.domain - -interface ForgotPasswordRepository { - - suspend fun sendResetLink(email: String): ForgotPasswordResult -} \ No newline at end of file diff --git a/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/domain/ForgotPasswordResult.kt b/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/domain/ForgotPasswordResult.kt deleted file mode 100644 index 4220d03e..00000000 --- a/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/domain/ForgotPasswordResult.kt +++ /dev/null @@ -1,6 +0,0 @@ -package ru.yeahub.impl.domain - -sealed interface ForgotPasswordResult { - data object Success : ForgotPasswordResult - data class Error(val message: String) : ForgotPasswordResult -} \ No newline at end of file diff --git a/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/domain/SendResetLinkUseCase.kt b/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/domain/SendResetLinkUseCase.kt deleted file mode 100644 index c141eb30..00000000 --- a/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/domain/SendResetLinkUseCase.kt +++ /dev/null @@ -1,15 +0,0 @@ -package ru.yeahub.impl.domain - -class SendResetLinkUseCase( - private val repository: ForgotPasswordRepository -) { - suspend operator fun invoke(email: String): ForgotPasswordResult { - val errorMessage = when { - email.isBlank() -> "Введите email" - !email.contains("@") -> "Некорректный email" - else -> null - } - return errorMessage?.let { ForgotPasswordResult.Error(it) } - ?: repository.sendResetLink(email.trim()) - } -} \ 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 deleted file mode 100644 index 346a31fc..00000000 --- a/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/presentation/ForgotPasswordViewModel.kt +++ /dev/null @@ -1,123 +0,0 @@ -package ru.yeahub.impl.presentation - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharedFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -import ru.yeahub.impl.domain.ForgotPasswordResult -import ru.yeahub.impl.domain.SendResetLinkUseCase -import ru.yeahub.impl.presentation.intents.ForgotPasswordCommand -import ru.yeahub.impl.presentation.intents.ForgotPasswordEvent -import ru.yeahub.impl.presentation.mapper.ForgotPasswordScreenMapper -import ru.yeahub.impl.presentation.state.ForgotPasswordScreenState -import ru.yeahub.impl.presentation.state.ForgotPasswordState - -class ForgotPasswordViewModel( - private val forgotPasswordScreenMapper: ForgotPasswordScreenMapper, - private val sendResetLinkUseCase: SendResetLinkUseCase -) : ViewModel() { - - private val mutableState = MutableStateFlow( - ForgotPasswordState( - email = "", - isLoading = false, - error = null, - emailValidationError = "", - isSuccessDialogVisible = false, - ) - ) - - private val mutableUiState = - MutableStateFlow(ForgotPasswordScreenState.Initial) - val uiState: StateFlow = mutableUiState.asStateFlow() - - private val mutableCommands = - MutableSharedFlow() - val commands: SharedFlow = mutableCommands - - init { - viewModelScope.launch { - mutableState.collect { state -> - mutableUiState.value = forgotPasswordScreenMapper.getScreenState(state) - } - } - } - - fun handleEvents(event: ForgotPasswordEvent) { - when (event) { - is ForgotPasswordEvent.EmailChanged -> onEmailChanged(event.value) - is ForgotPasswordEvent.SubmitClicked -> onSubmit() - is ForgotPasswordEvent.BackClicked -> handleBackClick() - } - } - - private fun onEmailChanged(value: String) { - mutableState.update { currentState -> - val validationError = if (value.isNotBlank()) { - forgotPasswordScreenMapper.validateEmail(value) - } else { - null - } - currentState.copy( - email = value, - emailValidationError = validationError, - error = null - ) - } - } - - private fun onSubmit() { - val currentState = mutableState.value - val email = currentState.email.trim() - - if (!forgotPasswordScreenMapper.canSubmit(email)) { - mutableState.update { - it.copy(emailValidationError = forgotPasswordScreenMapper.validateEmail(email)) - } - return - } - - mutableState.update { it.copy(isLoading = true, error = null) } - - viewModelScope.launch { - when (val result = sendResetLinkUseCase(email)) { - is ForgotPasswordResult.Success -> { - mutableState.update { - it.copy( - isLoading = false, - isSuccessDialogVisible = true - ) - } - emitCommand(ForgotPasswordCommand.NavigateToCheckEmail) - } - - is ForgotPasswordResult.Error -> { - mutableState.update { - it.copy( - isLoading = false, - error = result.message - ) - } - emitCommand(ForgotPasswordCommand.ShowSnackbar(result.message)) - } - } - } - } - - private fun handleBackClick() { - viewModelScope.launch { - mutableCommands.emit(ForgotPasswordCommand.NavigateBack) - } - } - - private fun emitCommand(command: ForgotPasswordCommand) { - viewModelScope.launch { - mutableCommands.emit(command) - } - } -} \ No newline at end of file diff --git a/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/presentation/intents/ForgotPasswordCommand.kt b/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/presentation/intents/ForgotPasswordCommand.kt deleted file mode 100644 index 091724ec..00000000 --- a/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/presentation/intents/ForgotPasswordCommand.kt +++ /dev/null @@ -1,7 +0,0 @@ -package ru.yeahub.impl.presentation.intents - -sealed interface ForgotPasswordCommand { - data class ShowSnackbar(val message: String) : ForgotPasswordCommand - data object NavigateBack : ForgotPasswordCommand - data object NavigateToCheckEmail : ForgotPasswordCommand -} \ No newline at end of file diff --git a/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/presentation/intents/ForgotPasswordEvent.kt b/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/presentation/intents/ForgotPasswordEvent.kt deleted file mode 100644 index 72324f87..00000000 --- a/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/presentation/intents/ForgotPasswordEvent.kt +++ /dev/null @@ -1,7 +0,0 @@ -package ru.yeahub.impl.presentation.intents - -sealed interface ForgotPasswordEvent { - data class EmailChanged(val value: String) : ForgotPasswordEvent - data object SubmitClicked : ForgotPasswordEvent - data object BackClicked : ForgotPasswordEvent -} \ No newline at end of file diff --git a/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/presentation/mapper/EmailValidator.kt b/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/presentation/mapper/EmailValidator.kt deleted file mode 100644 index 19d9b3e7..00000000 --- a/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/presentation/mapper/EmailValidator.kt +++ /dev/null @@ -1,28 +0,0 @@ -package ru.yeahub.impl.presentation.mapper - -import android.util.Patterns - -sealed interface EmailValidationResult { - data object Valid : EmailValidationResult - data object Empty : EmailValidationResult - data class Invalid(val errorMessage: String) : EmailValidationResult -} - -class EmailValidator { - - 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 - } - } - - 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/presentation/mapper/ForgotPasswordScreenMapper.kt b/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/presentation/mapper/ForgotPasswordScreenMapper.kt deleted file mode 100644 index 8dd1cbac..00000000 --- a/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/presentation/mapper/ForgotPasswordScreenMapper.kt +++ /dev/null @@ -1,41 +0,0 @@ -package ru.yeahub.impl.presentation.mapper - -import ru.yeahub.impl.presentation.state.ForgotPasswordScreenState -import ru.yeahub.impl.presentation.state.ForgotPasswordState - -class ForgotPasswordScreenMapper( - private val emailValidator: EmailValidator, -) { - fun getScreenState(state: ForgotPasswordState): ForgotPasswordScreenState { - return when { - state.error != null -> { - ForgotPasswordScreenState.Error(message = state.error) - } - - state.email.isBlank() && state.emailValidationError == null -> { - ForgotPasswordScreenState.Initial - } - - else -> { - ForgotPasswordScreenState.Content( - email = state.email, - isLoading = state.isLoading, - emailError = state.emailValidationError, - isSent = state.isSuccessDialogVisible, - ) - } - } - } - - fun validateEmail(email: String): String? { - return when (val result = emailValidator.validate(email)) { - is EmailValidationResult.Valid -> null - is EmailValidationResult.Empty -> null - is EmailValidationResult.Invalid -> result.errorMessage - } - } - - fun canSubmit(email: String): Boolean { - return emailValidator.isValid(email) - } -} diff --git a/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/presentation/state/ForgotPasswordScreenState.kt b/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/presentation/state/ForgotPasswordScreenState.kt deleted file mode 100644 index 11735ce2..00000000 --- a/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/presentation/state/ForgotPasswordScreenState.kt +++ /dev/null @@ -1,14 +0,0 @@ -package ru.yeahub.impl.presentation.state - -sealed class ForgotPasswordScreenState { - data object Initial : ForgotPasswordScreenState() - data class Content( - val email: String, - val isLoading: Boolean, - val emailError: String?, - val isSent: Boolean, - ) : ForgotPasswordScreenState() { - val isEmailValid: Boolean = emailError == null && email.isNotBlank() - } - data class Error(val message: String?) : ForgotPasswordScreenState() -} \ No newline at end of file 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 deleted file mode 100644 index 78486f01..00000000 --- a/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/presentation/state/ForgotPasswordState.kt +++ /dev/null @@ -1,9 +0,0 @@ -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 diff --git a/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/ui/ForgotPasswordModalWindow.kt b/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/ui/ForgotPasswordModalWindow.kt deleted file mode 100644 index 4fbefe02..00000000 --- a/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/ui/ForgotPasswordModalWindow.kt +++ /dev/null @@ -1,173 +0,0 @@ -@file:OptIn(ExperimentalMaterial3Api::class) - -package ru.yeahub.impl.ui - -import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.Image -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.Dialog -import kotlinx.coroutines.delay -import ru.yeahub.core_ui.example.staticPreview.StaticPreview -import ru.yeahub.core_ui.theme.Theme -import ru.yeahub.impl.R - -@Composable -fun InstructionsSentDialog( - onDismiss: () -> Unit, - onResend: () -> Unit, - modifier: Modifier = Modifier, -) { - Dialog(onDismissRequest = onDismiss) { - InstructionsSentDialogContent( - onDismiss = onDismiss, - onResend = onResend, - modifier = modifier - ) - } -} - -@Composable -private fun InstructionsSentDialogContent( - onDismiss: () -> Unit, - onResend: () -> Unit, - modifier: Modifier = Modifier, -) { - val color = Theme.colors.purple700 - val shape = RoundedCornerShape(16.dp) - - var secondsLeft by remember { mutableStateOf(60) } - - LaunchedEffect(Unit) { - while (secondsLeft > 0) { - delay(1_000) - secondsLeft-- - } - } - Surface( - modifier = modifier - .fillMaxWidth() - .padding(horizontal = 24.dp), - shape = shape, - color = Color.White, - tonalElevation = 0.dp, - shadowElevation = 0.dp, - border = BorderStroke(1.dp, color), - ) { - Box( - modifier = Modifier - .clip(shape) - .padding(16.dp), - contentAlignment = Alignment.TopEnd - ) { - IconButton( - onClick = onDismiss, - modifier = Modifier - .size(32.dp) - ) { - Image( - painter = painterResource(id = R.drawable.close), - contentDescription = stringResource(R.string.close), - modifier = Modifier.size(24.dp) - ) - } - - Column( - modifier = Modifier - .fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Image( - painter = painterResource(id = R.drawable.letter), - contentDescription = stringResource(R.string.letter), - modifier = Modifier - .size(116.dp) - .padding(vertical = 2.dp) - ) - - Text( - text = stringResource(R.string.instructions_letter), - style = Theme.typography.body5Accent, - textAlign = TextAlign.Center - ) - - Spacer(Modifier.height(6.dp)) - - Text( - text = stringResource(R.string.instruction), - style = Theme.typography.body2, - color = Theme.colors.black900, - textAlign = TextAlign.Center, - lineHeight = MaterialTheme.typography.bodyMedium.lineHeight - ) - - Spacer(Modifier.height(14.dp)) - - if (secondsLeft > 0) { - Text( - text = stringResource(R.string.resend, secondsLeft), - color = Theme.colors.black300, - style = Theme.typography.body2 - ) - } else { - Text( - text = stringResource(R.string.resend_button), - color = color, - style = Theme.typography.body2, - modifier = Modifier - .clip(RoundedCornerShape(10.dp)) - .clickable(onClick = onResend) - .padding(horizontal = 12.dp, vertical = 10.dp) - ) - } - - Spacer(Modifier.height(8.dp)) - - Text( - text = stringResource(R.string.ok), - color = color, - style = Theme.typography.body2, - modifier = Modifier - .clip(RoundedCornerShape(10.dp)) - .clickable(onClick = onDismiss) - .padding(horizontal = 12.dp, vertical = 4.dp) - ) - } - } - } -} - -@StaticPreview -@Composable -fun InstructionsSentDialogPreview() { - InstructionsSentDialogContent( - onDismiss = {}, - onResend = {} - ) -} 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 deleted file mode 100644 index b675bd9f..00000000 --- a/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/ui/ForgotPasswordRoute.kt +++ /dev/null @@ -1,33 +0,0 @@ -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 ru.yeahub.impl.presentation.ForgotPasswordViewModel -import ru.yeahub.impl.presentation.intents.ForgotPasswordCommand - -@Composable -fun ForgotPasswordRoute( - viewModel: ForgotPasswordViewModel, - onBack: () -> Unit, - onCheckEmail: () -> Unit, - showSnackbar: suspend (String) -> Unit -) { -// val state by viewModel.uiState.collectAsState() - - LaunchedEffect(Unit) { - viewModel.commands.collect { command -> - when (command) { - is ForgotPasswordCommand.NavigateBack -> onBack() - is ForgotPasswordCommand.NavigateToCheckEmail -> onCheckEmail() - is ForgotPasswordCommand.ShowSnackbar -> showSnackbar(command.message) - } - } - } - -// ForgotPasswordScreen( -// state = state, -// onEvent = viewModel::handleEvents -// ) -} \ 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 deleted file mode 100644 index 0fd80a2c..00000000 --- a/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/ui/ForgotPasswordScreen.kt +++ /dev/null @@ -1,173 +0,0 @@ -@file:OptIn(ExperimentalMaterial3Api::class) - -package ru.yeahub.impl.ui - -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import ru.yeahub.core_ui.component.PrimaryButton -import ru.yeahub.core_ui.component.YeahubButtonColors -//import ru.yeahub.core_ui.component.textInput.DefaultTextField -//import ru.yeahub.core_ui.component.textInput.TextInputColorsDefaults -import ru.yeahub.core_ui.theme.Theme -import ru.yeahub.impl.R -import ru.yeahub.impl.presentation.intents.ForgotPasswordEvent -import ru.yeahub.impl.presentation.state.ForgotPasswordScreenState - -@Composable -fun ForgotPasswordScreen( - modifier: Modifier = Modifier, - state: ForgotPasswordScreenState, - onEvent: (ForgotPasswordEvent) -> Unit -) { - Column( - modifier = modifier - .fillMaxSize() - .padding(horizontal = 16.dp) - .padding(top = 32.dp) - ) { - Text( - text = stringResource(R.string.title_forgot_password), - style = Theme.typography.head5 - ) - - Spacer(Modifier.height(6.dp)) - - Text( - text = stringResource(R.string.enter_email_instruction), - style = Theme.typography.body2Accent, - color = Theme.colors.black500 - ) - - Spacer(Modifier.height(96.dp)) - - Text( - text = stringResource(R.string.email), - style = Theme.typography.body3Accent - ) - - 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 - else -> null - } - val isEmailValid = when (state) { - is ForgotPasswordScreenState.Content -> state.isEmailValid - else -> false - } - -// DefaultTextField( -// value = email, -// onValueChange = { onEvent(ForgotPasswordEvent.EmailChanged(it)) }, -// label = stringResource(R.string.enter_email), -// isEnabled = !isLoading, -// isError = emailError != null, -// showLeadingIcon = false, -// onExpandedChange = { }, -// modifier = Modifier -// .fillMaxWidth() -// .height(52.dp), -//// colors = TextInputColorsDefaults.defaultColors() -// ) - - if (emailError != null) { - Spacer(Modifier.height(4.dp)) - Text( - text = emailError, - style = Theme.typography.body3, - color = Theme.colors.red500 - ) - } - - Spacer(Modifier.height(20.dp)) - - PrimaryButton( - onClick = { onEvent(ForgotPasswordEvent.SubmitClicked) }, - enabled = isEmailValid && !isLoading, - modifier = Modifier - .fillMaxWidth() - .height(48.dp), - colors = YeahubButtonColors( - contentColor = Theme.colors.white900, - containerColor = Theme.colors.purple700, - disabledContentColor = Theme.colors.black200, - disabledContainerColor = Theme.colors.black50 - ) - ) { - Text( - text = stringResource(R.string.send_button), - style = Theme.typography.body4 - ) - } - } -} - -@Preview(showBackground = true, name = "Initial State") -@Composable -fun ForgotPasswordScreenPreview_Initial() { - ForgotPasswordScreen( - state = ForgotPasswordScreenState.Initial, - onEvent = {} - ) -} - -@Preview(showBackground = true, name = "Valid Email") -@Composable -fun ForgotPasswordScreenPreview_Valid() { - ForgotPasswordScreen( - state = ForgotPasswordScreenState.Content( - email = "user@example.com", - isLoading = false, - emailError = null, - isSent = false - ), - onEvent = {} - ) -} - -@Preview(showBackground = true, name = "Invalid Email") -@Composable -fun ForgotPasswordScreenPreview_Invalid() { - ForgotPasswordScreen( - state = ForgotPasswordScreenState.Content( - email = "invalid-email", - isLoading = false, - emailError = "Введите корректный email", - isSent = false - ), - onEvent = {} - ) -} - -@Preview(showBackground = true, name = "Loading") -@Composable -fun ForgotPasswordScreenPreview_Loading() { - ForgotPasswordScreen( - state = ForgotPasswordScreenState.Content( - email = "user@example.com", - isLoading = true, - emailError = null, - isSent = false - ), - onEvent = {} - ) -} diff --git a/feature/forgot-password/impl/src/main/res/drawable/close.jpg b/feature/forgot-password/impl/src/main/res/drawable/close.jpg deleted file mode 100644 index 4de6c0819bb288f701c9ad81b8a31a84f4a91db9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7063 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 DANVS# diff --git a/feature/forgot-password/impl/src/main/res/values/strings.xml b/feature/forgot-password/impl/src/main/res/values/strings.xml deleted file mode 100644 index 6dfa844e..00000000 --- a/feature/forgot-password/impl/src/main/res/values/strings.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - Close - Letter - Мы отправили письмо\nс инструкциями - Если вы не получили письмо\nс инструкциями, проверьте, пожалуйста,\nпапку «Спам» или попробуйте отправить\nзапрос еще раз - Отправить повторно через %1$d c - Отправить повторно - Хорошо - Забыли пароль? - Для восстановления пароля введите адрес\nэл.почты, на который вы регистрировались.\nМы отправим письмо для восстановления пароля - Электронная почта - Введите электронную почту - Отправить - Не удалось отправить письмо - \ No newline at end of file diff --git a/feature/forgot-password/src/main/AndroidManifest.xml b/feature/forgot-password/src/main/AndroidManifest.xml deleted file mode 100644 index a5918e68..00000000 --- a/feature/forgot-password/src/main/AndroidManifest.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 002e2400..cbecfd9e 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -62,6 +62,4 @@ include(":feature:authentication:impl") include(":feature:enter-and-registration") include(":feature:enter-and-registration:api") include(":feature:enter-and-registration:impl") -//include(":feature:forgot-password") -//include(":feature:forgot-password:api") -//include(":feature:forgot-password:impl") + From cae0c9126404703d0efd44e25fefe23cd4fd1a3e Mon Sep 17 00:00:00 2001 From: MODDII Date: Thu, 11 Jun 2026 20:33:05 +0300 Subject: [PATCH 13/14] =?UTF-8?q?ANDR-51=20=D0=90=D0=B4=D0=B0=D0=BF=D1=82?= =?UTF-8?q?=D0=B0=D1=86=D0=B8=D1=8F=20=D1=87=D0=B5=D0=BA=D0=B1=D0=BE=D0=BA?= =?UTF-8?q?=D1=81=D0=BE=D0=B2=20=D0=B8=20=D0=B2=D0=B5=D1=80=D1=81=D1=82?= =?UTF-8?q?=D0=BA=D0=B8=20=D1=81=D0=BE=D0=B3=D0=BB=D0=B0=D1=81=D0=B8=D0=B9?= =?UTF-8?q?=20=D0=BD=D0=B0=20=D1=8D=D0=BA=D1=80=D0=B0=D0=BD=D0=B5=20=D1=80?= =?UTF-8?q?=D0=B5=D0=B3=D0=B8=D1=81=D1=82=D1=80=D0=B0=D1=86=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/RegistrationScreen.kt | 85 +++++++++++-------- 1 file changed, 48 insertions(+), 37 deletions(-) diff --git a/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/registration/presentation/RegistrationScreen.kt b/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/registration/presentation/RegistrationScreen.kt index f514071d..6bd0f300 100644 --- a/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/registration/presentation/RegistrationScreen.kt +++ b/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/registration/presentation/RegistrationScreen.kt @@ -18,12 +18,15 @@ import androidx.compose.material.icons.filled.Visibility import androidx.compose.material.icons.filled.VisibilityOff import androidx.compose.material3.Checkbox import androidx.compose.material3.CheckboxDefaults +import androidx.compose.material3.LocalMinimumInteractiveComponentSize import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.ui.unit.Dp import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -187,35 +190,40 @@ fun RegistrationContent( keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password) ) - ConsentRow( - checked = form.isPdAccepted, - onCheckedChange = { - onAction(RegistrationAction.PdAcceptedChanged(it)) - }, - text = pdConsentText(linkColor), - onLinkClicked = { tag -> if (tag == "pd") onOpenPdPolicy() } - ) + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + ConsentRow( + checked = form.isPdAccepted, + onCheckedChange = { + onAction(RegistrationAction.PdAcceptedChanged(it)) + }, + text = pdConsentText(linkColor), + onLinkClicked = { tag -> if (tag == "pd") onOpenPdPolicy() } + ) - ConsentRow( - checked = form.isOfferAccepted, - onCheckedChange = { - onAction(RegistrationAction.OfferAcceptedChanged(it)) - }, - text = offerConsentText(linkColor), - onLinkClicked = { tag -> if (tag == "offer") onOpenOffer() } - ) + ConsentRow( + checked = form.isOfferAccepted, + onCheckedChange = { + onAction(RegistrationAction.OfferAcceptedChanged(it)) + }, + text = offerConsentText(linkColor), + onLinkClicked = { tag -> if (tag == "offer") onOpenOffer() } + ) - ConsentRow( - checked = form.isMailingAccepted, - onCheckedChange = { - onAction(RegistrationAction.MailingAcceptedChanged(it)) - }, - text = - AnnotatedString( - stringResource(R.string.marketing_opt_in_text) - ), - onLinkClicked = {} - ) + ConsentRow( + checked = form.isMailingAccepted, + onCheckedChange = { + onAction(RegistrationAction.MailingAcceptedChanged(it)) + }, + text = + AnnotatedString( + stringResource(R.string.marketing_opt_in_text) + ), + onLinkClicked = {} + ) + } Spacer(Modifier.height(8.dp)) @@ -258,16 +266,19 @@ private fun ConsentRow( onLinkClicked: (tag: String) -> Unit ) { Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.Top) { - Checkbox( - checked = checked, - onCheckedChange = onCheckedChange, - colors = - CheckboxDefaults.colors( - checkedColor = Theme.colors.purple700, - uncheckedColor = Theme.colors.purple200, - checkmarkColor = Theme.colors.white900 - ) - ) + CompositionLocalProvider(LocalMinimumInteractiveComponentSize provides Dp.Unspecified) { + Checkbox( + checked = checked, + onCheckedChange = onCheckedChange, + modifier = Modifier.padding(top = 2.dp), + colors = + CheckboxDefaults.colors( + checkedColor = Theme.colors.purple700, + uncheckedColor = Theme.colors.black200, + checkmarkColor = Theme.colors.white900 + ) + ) + } Spacer(Modifier.width(8.dp)) ClickableText( text = text, From d8efb51d1f13555afb13be4adb7d3d111d457b6e Mon Sep 17 00:00:00 2001 From: MODDII Date: Thu, 25 Jun 2026 14:14:52 +0300 Subject: [PATCH 14/14] =?UTF-8?q?ANDR-51:=20=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20=D1=82=D0=B5=D1=81=D1=82=D0=BE?= =?UTF-8?q?=D0=B2=20=D0=BD=D0=B0=20=D0=BC=D0=B0=D0=BF=D0=BF=D0=B5=D1=80=20?= =?UTF-8?q?=D1=81=D0=BE=D0=B3=D0=BB=D0=B0=D1=81=D0=BD=D0=BE=20=D1=81=D1=82?= =?UTF-8?q?=D0=B8=D0=BB=D1=8E=20=D0=BD=D0=B0=D0=BF=D0=B8=D1=81=D0=B0=D0=BD?= =?UTF-8?q?=D0=B8=D1=8F=20=D1=82=D0=B5=D1=81=D1=82=D0=BE=D0=B2=20=D0=BD?= =?UTF-8?q?=D0=B0=20=D0=BF=D1=80=D0=BE=D0=B5=D0=BA=D1=82=D0=B5.=20=D0=9E?= =?UTF-8?q?=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20=D1=81?= =?UTF-8?q?=D0=B0=D0=BC=D0=BE=D0=B3=D0=BE=20=D0=BC=D0=B0=D0=BF=D0=BF=D0=B5?= =?UTF-8?q?=D1=80=D0=B0.=20=D0=A2=D0=B5=D0=BF=D0=B5=D1=80=D1=8C=20=D0=B2?= =?UTF-8?q?=D1=81=D0=B5=20=D1=81=D0=BE=D0=B1=D1=8B=D1=82=D0=B8=D1=8F=20?= =?UTF-8?q?=D0=BE=D0=B1=D1=80=D0=B0=D0=B1=D0=B0=D1=82=D1=8B=D0=B2=D0=B0?= =?UTF-8?q?=D1=8E=D1=82=D1=81=D1=8F=20=D0=B1=D0=B5=D0=B7=20else?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/RegistrationUiStateMapper.kt | 46 +- .../RegistrationUiStateMapperTest.kt | 549 +++++++++++------- 2 files changed, 391 insertions(+), 204 deletions(-) diff --git a/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/registration/presentation/RegistrationUiStateMapper.kt b/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/registration/presentation/RegistrationUiStateMapper.kt index caa56468..1e06ac94 100644 --- a/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/registration/presentation/RegistrationUiStateMapper.kt +++ b/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/registration/presentation/RegistrationUiStateMapper.kt @@ -63,7 +63,7 @@ class RegistrationUiStateMapper { is RegistrationAction.TogglePasswordVisible, is RegistrationAction.ToggleConfirmPasswordVisible -> handleToggleAction(currentForm, action) - else -> currentForm + is RegistrationAction.SubmitClicked -> currentForm } } @@ -75,7 +75,15 @@ class RegistrationUiStateMapper { is RegistrationAction.EmailChanged -> form.copy(email = action.value) is RegistrationAction.PasswordChanged -> form.copy(password = action.value) is RegistrationAction.ConfirmPasswordChanged -> form.copy(confirmPassword = action.value) - else -> form + is RegistrationAction.PdAcceptedChanged, + is RegistrationAction.OfferAcceptedChanged, + is RegistrationAction.MailingAcceptedChanged, + is RegistrationAction.TogglePasswordVisible, + is RegistrationAction.ToggleConfirmPasswordVisible, + is RegistrationAction.SubmitClicked, + is RegistrationAction.EmailFocusChanged, + is RegistrationAction.PasswordFocusChanged, + is RegistrationAction.ConfirmPasswordFocusChanged -> form } private fun handleFocusAction( @@ -88,7 +96,16 @@ class RegistrationUiStateMapper { form.copy(isPasswordTouched = !action.hasFocus && form.password.isNotEmpty()) is RegistrationAction.ConfirmPasswordFocusChanged -> form.copy(isConfirmPasswordTouched = !action.hasFocus && form.confirmPassword.isNotEmpty()) - else -> form + is RegistrationAction.NicknameChanged, + is RegistrationAction.EmailChanged, + is RegistrationAction.PasswordChanged, + is RegistrationAction.ConfirmPasswordChanged, + is RegistrationAction.PdAcceptedChanged, + is RegistrationAction.OfferAcceptedChanged, + is RegistrationAction.MailingAcceptedChanged, + is RegistrationAction.TogglePasswordVisible, + is RegistrationAction.ToggleConfirmPasswordVisible, + is RegistrationAction.SubmitClicked -> form } private fun handleConsentAction( @@ -98,7 +115,16 @@ class RegistrationUiStateMapper { is RegistrationAction.PdAcceptedChanged -> form.copy(isPdAccepted = action.value) is RegistrationAction.OfferAcceptedChanged -> form.copy(isOfferAccepted = action.value) is RegistrationAction.MailingAcceptedChanged -> form.copy(isMailingAccepted = action.value) - else -> form + is RegistrationAction.NicknameChanged, + is RegistrationAction.EmailChanged, + is RegistrationAction.PasswordChanged, + is RegistrationAction.ConfirmPasswordChanged, + is RegistrationAction.TogglePasswordVisible, + is RegistrationAction.ToggleConfirmPasswordVisible, + is RegistrationAction.SubmitClicked, + is RegistrationAction.EmailFocusChanged, + is RegistrationAction.PasswordFocusChanged, + is RegistrationAction.ConfirmPasswordFocusChanged -> form } private fun handleToggleAction( @@ -109,7 +135,17 @@ class RegistrationUiStateMapper { form.copy(isPasswordVisible = !form.isPasswordVisible) is RegistrationAction.ToggleConfirmPasswordVisible -> form.copy(isConfirmPasswordVisible = !form.isConfirmPasswordVisible) - else -> form + is RegistrationAction.NicknameChanged, + is RegistrationAction.EmailChanged, + is RegistrationAction.PasswordChanged, + is RegistrationAction.ConfirmPasswordChanged, + is RegistrationAction.PdAcceptedChanged, + is RegistrationAction.OfferAcceptedChanged, + is RegistrationAction.MailingAcceptedChanged, + is RegistrationAction.SubmitClicked, + is RegistrationAction.EmailFocusChanged, + is RegistrationAction.PasswordFocusChanged, + is RegistrationAction.ConfirmPasswordFocusChanged -> form } fun mapToLoadingState(currentState: RegistrationUiState): RegistrationUiState { diff --git a/feature/authentication/impl/src/test/java/ru/yeahub/authentication/impl/registration/presentation/RegistrationUiStateMapperTest.kt b/feature/authentication/impl/src/test/java/ru/yeahub/authentication/impl/registration/presentation/RegistrationUiStateMapperTest.kt index 6211ee57..f9795519 100644 --- a/feature/authentication/impl/src/test/java/ru/yeahub/authentication/impl/registration/presentation/RegistrationUiStateMapperTest.kt +++ b/feature/authentication/impl/src/test/java/ru/yeahub/authentication/impl/registration/presentation/RegistrationUiStateMapperTest.kt @@ -5,15 +5,16 @@ import io.mockk.mockkObject import io.mockk.unmockkAll import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Assertions.assertFalse -import org.junit.jupiter.api.Assertions.assertNull import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtensionContext import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.Arguments -import org.junit.jupiter.params.provider.MethodSource +import org.junit.jupiter.params.provider.ArgumentsProvider +import org.junit.jupiter.params.provider.ArgumentsSource import ru.yeahub.authentication.impl.R +import ru.yeahub.authentication.impl.registration.domain.entity.RegistrationError +import ru.yeahub.authentication.impl.registration.domain.entity.RegistrationException import ru.yeahub.core_utils.common.TextOrResource import ru.yeahub.core_utils.validation.EmailValidator import java.util.stream.Stream @@ -33,222 +34,372 @@ class RegistrationUiStateMapperTest { unmockkAll() } - @Test - fun `mapToInitialState returns correct initial state`() { - val state = mapper.mapToInitialState() - - assertTrue(state is RegistrationUiState.Content) - val form = state.formState - assertEquals("", form.nickname) - assertEquals("", form.email) - assertEquals("", form.password) - assertEquals("", form.confirmPassword) - assertFalse(form.isPdAccepted) - assertFalse(form.isOfferAccepted) - assertFalse(form.isSubmitEnabled) - assertNull(form.emailError) - assertNull(form.nicknameError) - assertNull(form.passwordError) - assertNull(form.confirmPasswordError) + @ParameterizedTest + @ArgumentsSource(UpdatedStateArgumentsProvider::class) + fun `should update form state by action correctly`(testCase: UpdatedStateTestCase) { + val initialForm = testCase.setupForm(mapper.getInitialFormState()) + val currentState = RegistrationUiState.Content(initialForm) + + val result = mapper.mapToUpdatedState(currentState, testCase.action) + + assertEquals(testCase.expectedForm, result.formState) } @ParameterizedTest - @MethodSource("provideValidationCases") - fun `validateForm logic works correctly`( - nickname: String, - email: String, - isEmailValid: Boolean, - password: String, - confirmPassword: String, - isPdAccepted: Boolean, - isOfferAccepted: Boolean, - expectedSubmitEnabled: Boolean - ) { - every { EmailValidator.isValid(any()) } returns isEmailValid - + @ArgumentsSource(ValidationArgumentsProvider::class) + fun `should validate form and return correct isSubmitEnabled`(testCase: ValidationTestCase) { + every { EmailValidator.isValid(any()) } returns testCase.isEmailValid + val form = mapper.getInitialFormState().copy( - nickname = nickname, - email = email, - password = password, - confirmPassword = confirmPassword, - isPdAccepted = isPdAccepted, - isOfferAccepted = isOfferAccepted + nickname = testCase.nickname, + email = testCase.email, + password = testCase.password, + confirmPassword = testCase.confirmPassword, + isPdAccepted = testCase.isPdAccepted, + isOfferAccepted = testCase.isOfferAccepted, ) val currentState = RegistrationUiState.Content(form) - val resultState = mapper.mapToUpdatedState(currentState, RegistrationAction.NicknameChanged(nickname)) - - assertEquals(expectedSubmitEnabled, resultState.formState.isSubmitEnabled) - } - - companion object { - @JvmStatic - fun provideValidationCases(): Stream = Stream.of( - Arguments.of( - "user", - "test@test.com", - true, - "Pass123!", - "Pass123!", - true, - true, - true - ), - Arguments.of( - "", - "test@test.com", - true, - "Pass123!", - "Pass123!", - true, - true, - false - ), - Arguments.of( - "user", - "invalid", - false, - "Pass123!", - "Pass123!", - true, - true, - false - ), - Arguments.of( - "user", - "test@test.com", - true, - "short", - "short", - true, - true, - false - ), - Arguments.of( - "user", - "test@test.com", - true, - "Pass123!", - "Mismatch!", - true, - true, - false - ), - Arguments.of( - "user", - "test@test.com", - true, - "short", - "short", - true, - true, - false - ), - Arguments.of( - "user", - "test@test.com", - true, - "nospecial123", - "nospecial123", - true, - true, - false - ), - Arguments.of( - "user", - "test@test.com", - true, - "NoDigit!", - "NoDigit!", - true, - true, - false - ), - Arguments.of( - "user", - "test@test.com", - true, - "nouppercase1!", - "nouppercase1!", - true, - true, - false - ), - Arguments.of( - "user", - "test@test.com", - true, - "Pass123!", - "Pass123!", - false, - true, - false - ), - Arguments.of( - "user", - "test@test.com", - true, - "Pass123!", - "Pass123!", - true, - false, - false - ) + val result = mapper.mapToUpdatedState( + currentState, + testCase.action, ) - @JvmStatic - fun providePasswordErrorCases(): Stream = Stream.of( - Arguments.of("short", R.string.error_password_too_short), - Arguments.of("NoSpecial123", R.string.error_password_no_special_char), - Arguments.of("NoDigit!", R.string.error_password_no_digit), - Arguments.of("nouppercase1!", R.string.error_password_no_uppercase) - ) + assertEquals(testCase.expectedSubmitEnabled, result.formState.isSubmitEnabled) } @ParameterizedTest - @MethodSource("providePasswordErrorCases") - fun `validateForm returns correct error resource for specific password failures`( - password: String, - expectedErrorRes: Int - ) { - val form = mapper.getInitialFormState().copy( - password = password, - isPasswordTouched = true - ) - val currentState = RegistrationUiState.Content(form) + @ArgumentsSource(ErrorStateArgumentsProvider::class) + fun `should map exception to correct error resource`(testCase: ErrorStateTestCase) { + val currentState = RegistrationUiState.Content(mapper.getInitialFormState()) - val resultState = mapper.mapToUpdatedState(currentState, RegistrationAction.PasswordChanged(password)) + val result = mapper.mapToErrorState(currentState, testCase.exception) - val error = resultState.formState.passwordError as? TextOrResource.Resource - assertEquals(expectedErrorRes, error?.resource) + assertTrue(result is RegistrationUiState.Error) + val errorMessage = (result as RegistrationUiState.Error).message as TextOrResource.Resource + assertEquals(testCase.expectedErrorRes, errorMessage.resource) } - @Test - fun `mapToLoadingState preserves form state`() { - val form = mapper.getInitialFormState().copy(nickname = "some user") - val currentState = RegistrationUiState.Content(form) + data class UpdatedStateTestCase( + val name: String, + val action: RegistrationAction, + val setupForm: (RegistrationFormState) -> RegistrationFormState = { it }, + val expectedForm: RegistrationFormState, + ) { + override fun toString(): String = name + } - val resultState = mapper.mapToLoadingState(currentState) + data class ValidationTestCase( + val name: String, + val nickname: String, + val email: String, + val isEmailValid: Boolean, + val password: String, + val confirmPassword: String, + val isPdAccepted: Boolean, + val isOfferAccepted: Boolean, + val action: RegistrationAction, + val expectedSubmitEnabled: Boolean, + ) { + override fun toString(): String = name + } - assertTrue(resultState is RegistrationUiState.Loading) - assertEquals(form, resultState.formState) + data class ErrorStateTestCase( + val name: String, + val exception: RegistrationException, + val expectedErrorRes: Int, + ) { + override fun toString(): String = name } - @Test - fun `handleFocusAction updates touched state correctly`() { - val form = mapper.getInitialFormState().copy(email = "test") - val currentState = RegistrationUiState.Content(form) + class UpdatedStateArgumentsProvider : ArgumentsProvider { + override fun provideArguments(context: ExtensionContext?): Stream { + return Stream.of( + Arguments.of( + UpdatedStateTestCase( + name = "NicknameChanged обновляет nickname", + action = RegistrationAction.NicknameChanged("John"), + expectedForm = initialFormWith(nickname = "John"), + ) + ), + Arguments.of( + UpdatedStateTestCase( + name = "EmailChanged обновляет email", + action = RegistrationAction.EmailChanged("test@mail.ru"), + expectedForm = initialFormWith(email = "test@mail.ru"), + ) + ), + Arguments.of( + UpdatedStateTestCase( + name = "PasswordChanged обновляет password", + action = RegistrationAction.PasswordChanged("Pass123!"), + expectedForm = initialFormWith(password = "Pass123!"), + ) + ), + Arguments.of( + UpdatedStateTestCase( + name = "ConfirmPasswordChanged обновляет confirmPassword", + action = RegistrationAction.ConfirmPasswordChanged("Pass123!"), + expectedForm = initialFormWith(confirmPassword = "Pass123!"), + ) + ), + Arguments.of( + UpdatedStateTestCase( + name = "EmailFocusChanged(false) при непустом email ставит isEmailTouched = true", + action = RegistrationAction.EmailFocusChanged(false), + setupForm = { it.copy(email = "test@mail.ru") }, + expectedForm = initialFormWith( + email = "test@mail.ru", + isEmailTouched = true, + ), + ) + ), + Arguments.of( + UpdatedStateTestCase( + name = "EmailFocusChanged(true) не ставит isEmailTouched", + action = RegistrationAction.EmailFocusChanged(true), + setupForm = { it.copy(email = "test@mail.ru", isEmailTouched = true) }, + expectedForm = initialFormWith(email = "test@mail.ru"), + ) + ), + Arguments.of( + UpdatedStateTestCase( + name = "PasswordFocusChanged(false) при непустом пароле ставит isPasswordTouched = true", + action = RegistrationAction.PasswordFocusChanged(false), + setupForm = { it.copy(password = "Pass123!") }, + expectedForm = initialFormWith( + password = "Pass123!", + isPasswordTouched = true, + ), + ) + ), + Arguments.of( + UpdatedStateTestCase( + name = "ConfirmPasswordFocusChanged(false) при непустом " + + "пароле ставит isConfirmPasswordTouched = true", + action = RegistrationAction.ConfirmPasswordFocusChanged(false), + setupForm = { it.copy(password = "Pass123!", confirmPassword = "Pass123!") }, + expectedForm = initialFormWith( + password = "Pass123!", + confirmPassword = "Pass123!", + isConfirmPasswordTouched = true, + ), + ) + ), + Arguments.of( + UpdatedStateTestCase( + name = "PdAcceptedChanged(true) обновляет isPdAccepted", + action = RegistrationAction.PdAcceptedChanged(true), + expectedForm = initialFormWith(isPdAccepted = true), + ) + ), + Arguments.of( + UpdatedStateTestCase( + name = "OfferAcceptedChanged(true) обновляет isOfferAccepted", + action = RegistrationAction.OfferAcceptedChanged(true), + expectedForm = initialFormWith(isOfferAccepted = true), + ) + ), + Arguments.of( + UpdatedStateTestCase( + name = "MailingAcceptedChanged(true) обновляет isMailingAccepted", + action = RegistrationAction.MailingAcceptedChanged(true), + expectedForm = initialFormWith(isMailingAccepted = true), + ) + ), + Arguments.of( + UpdatedStateTestCase( + name = "TogglePasswordVisible переключает isPasswordVisible на true", + action = RegistrationAction.TogglePasswordVisible, + expectedForm = initialFormWith(isPasswordVisible = true), + ) + ), + Arguments.of( + UpdatedStateTestCase( + name = "ToggleConfirmPasswordVisible переключает isConfirmPasswordVisible на true", + action = RegistrationAction.ToggleConfirmPasswordVisible, + expectedForm = initialFormWith(isConfirmPasswordVisible = true), + ) + ), + Arguments.of( + UpdatedStateTestCase( + name = "SubmitClicked не меняет форму", + action = RegistrationAction.SubmitClicked, + expectedForm = initialFormWith(), + ) + ), + ) + } + } - val stateAfterFocusLost = mapper.mapToUpdatedState( - currentState, - RegistrationAction.EmailFocusChanged(false) - ) - assertTrue(stateAfterFocusLost.formState.isEmailTouched) + class ValidationArgumentsProvider : ArgumentsProvider { + override fun provideArguments(context: ExtensionContext?): Stream { + return Stream.of( + Arguments.of( + ValidationTestCase( + name = "Все поля валидны — submit разрешён", + nickname = "user", + email = "test@test.com", + isEmailValid = true, + password = "Pass123!", + confirmPassword = "Pass123!", + isPdAccepted = true, + isOfferAccepted = true, + expectedSubmitEnabled = true, + action = RegistrationAction.NicknameChanged("user") + ) + ), + Arguments.of( + ValidationTestCase( + name = "Пустой nickname — submit запрещён", + nickname = "", + email = "test@test.com", + isEmailValid = true, + password = "Pass123!", + confirmPassword = "Pass123!", + isPdAccepted = true, + isOfferAccepted = true, + expectedSubmitEnabled = false, + action = RegistrationAction.NicknameChanged("") + ) + ), + Arguments.of( + ValidationTestCase( + name = "Невалидный email — submit запрещён", + nickname = "user", + email = "invalid", + isEmailValid = false, + password = "Pass123!", + confirmPassword = "Pass123!", + isPdAccepted = true, + isOfferAccepted = true, + expectedSubmitEnabled = false, + action = RegistrationAction.EmailChanged("user") + ) + ), + Arguments.of( + ValidationTestCase( + name = "Слабый пароль — submit запрещён", + nickname = "user", + email = "test@test.com", + isEmailValid = true, + password = "short", + confirmPassword = "short", + isPdAccepted = true, + isOfferAccepted = true, + expectedSubmitEnabled = false, + action = RegistrationAction.PasswordChanged("1234") + ) + ), + Arguments.of( + ValidationTestCase( + name = "Пароли не совпадают — submit запрещён", + nickname = "user", + email = "test@test.com", + isEmailValid = true, + password = "Pass123!", + confirmPassword = "Mismatch!", + isPdAccepted = true, + isOfferAccepted = true, + expectedSubmitEnabled = false, + action = RegistrationAction.ConfirmPasswordChanged("1234") + ) + ), + Arguments.of( + ValidationTestCase( + name = "ПД не принят — submit запрещён", + nickname = "user", + email = "test@test.com", + isEmailValid = true, + password = "Pass123!", + confirmPassword = "Pass123!", + isPdAccepted = false, + isOfferAccepted = true, + expectedSubmitEnabled = false, + action = RegistrationAction.PdAcceptedChanged(false) + ) + ), + Arguments.of( + ValidationTestCase( + name = "Оферта не принята — submit запрещён", + nickname = "user", + email = "test@test.com", + isEmailValid = true, + password = "Pass123!", + confirmPassword = "Pass123!", + isPdAccepted = true, + isOfferAccepted = false, + expectedSubmitEnabled = false, + action = RegistrationAction.OfferAcceptedChanged(false) + ) + ), + ) + } + } - val stateAfterFocusGained = mapper.mapToUpdatedState( - stateAfterFocusLost, - RegistrationAction.EmailFocusChanged(true) - ) - assertFalse(stateAfterFocusGained.formState.isEmailTouched) + class ErrorStateArgumentsProvider : ArgumentsProvider { + override fun provideArguments(context: ExtensionContext?): Stream { + return Stream.of( + Arguments.of( + ErrorStateTestCase( + name = "Conflict → error_user_already_exists", + exception = RegistrationException(RegistrationError.Conflict), + expectedErrorRes = R.string.error_user_already_exists, + ) + ), + Arguments.of( + ErrorStateTestCase( + name = "NotFound → error_resource_not_found", + exception = RegistrationException(RegistrationError.NotFound), + expectedErrorRes = R.string.error_resource_not_found, + ) + ), + Arguments.of( + ErrorStateTestCase( + name = "UnknownError → login_unknown_error", + exception = RegistrationException(RegistrationError.UnknownError), + expectedErrorRes = R.string.login_unknown_error, + ) + ), + ) + } } } + +private fun initialFormWith( + nickname: String = "", + nicknameError: TextOrResource? = null, + email: String = "", + emailError: TextOrResource? = null, + password: String = "", + passwordError: TextOrResource? = null, + confirmPassword: String = "", + confirmPasswordError: TextOrResource? = null, + isPdAccepted: Boolean = false, + isOfferAccepted: Boolean = false, + isMailingAccepted: Boolean = false, + isPasswordVisible: Boolean = false, + isConfirmPasswordVisible: Boolean = false, + isSubmitEnabled: Boolean = false, + isEmailTouched: Boolean = false, + isPasswordTouched: Boolean = false, + isConfirmPasswordTouched: Boolean = false, +): RegistrationFormState = RegistrationFormState( + nickname = nickname, + nicknameError = nicknameError, + email = email, + emailError = emailError, + password = password, + passwordError = passwordError, + confirmPassword = confirmPassword, + confirmPasswordError = confirmPasswordError, + isPdAccepted = isPdAccepted, + isOfferAccepted = isOfferAccepted, + isMailingAccepted = isMailingAccepted, + isPasswordVisible = isPasswordVisible, + isConfirmPasswordVisible = isConfirmPasswordVisible, + isSubmitEnabled = isSubmitEnabled, + isEmailTouched = isEmailTouched, + isPasswordTouched = isPasswordTouched, + isConfirmPasswordTouched = isConfirmPasswordTouched, +)