From 03206bca143d2a4741b2aa06c2fd5126b09dbc99 Mon Sep 17 00:00:00 2001 From: MODDII Date: Thu, 7 May 2026 15:15:34 +0300 Subject: [PATCH 01/11] =?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/11] =?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/11] =?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/11] =?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/11] =?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/11] =?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/11] =?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/11] =?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/11] =?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/11] =?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/11] =?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) } }