From 2c89e74e8858487d187621ea3a2c0208493e1a98 Mon Sep 17 00:00:00 2001 From: MODDII Date: Fri, 23 Jan 2026 20:49:41 +0300 Subject: [PATCH 1/5] =?UTF-8?q?=D0=A4=D0=B8=D1=87=D0=B0=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:=20?= =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D1=8B=20=D0=BC?= =?UTF-8?q?=D0=BE=D0=B4=D1=83=D0=BB=D0=B8=20api=20=D0=B8=20impl?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/gradle.xml | 9 ++-- feature/registration/api/.gitignore | 1 + feature/registration/api/build.gradle.kts | 42 +++++++++++++++++++ feature/registration/api/consumer-rules.pro | 0 feature/registration/api/proguard-rules.pro | 21 ++++++++++ .../api/src/main/AndroidManifest.xml | 4 ++ feature/registration/impl/.gitignore | 1 + feature/registration/impl/build.gradle.kts | 42 +++++++++++++++++++ feature/registration/impl/consumer-rules.pro | 0 feature/registration/impl/proguard-rules.pro | 21 ++++++++++ .../impl/src/main/AndroidManifest.xml | 4 ++ settings.gradle.kts | 3 +- 12 files changed, 144 insertions(+), 4 deletions(-) create mode 100644 feature/registration/api/.gitignore create mode 100644 feature/registration/api/build.gradle.kts create mode 100644 feature/registration/api/consumer-rules.pro create mode 100644 feature/registration/api/proguard-rules.pro create mode 100644 feature/registration/api/src/main/AndroidManifest.xml create mode 100644 feature/registration/impl/.gitignore create mode 100644 feature/registration/impl/build.gradle.kts create mode 100644 feature/registration/impl/consumer-rules.pro create mode 100644 feature/registration/impl/proguard-rules.pro create mode 100644 feature/registration/impl/src/main/AndroidManifest.xml diff --git a/.idea/gradle.xml b/.idea/gradle.xml index d6d139c0..60bb4b54 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -42,12 +42,15 @@ diff --git a/feature/registration/api/.gitignore b/feature/registration/api/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/feature/registration/api/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/feature/registration/api/build.gradle.kts b/feature/registration/api/build.gradle.kts new file mode 100644 index 00000000..e1aa6ead --- /dev/null +++ b/feature/registration/api/build.gradle.kts @@ -0,0 +1,42 @@ +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) +} + +android { + namespace = "ru.yeahub.api" + 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" + } +} + +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/registration/api/consumer-rules.pro b/feature/registration/api/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/feature/registration/api/proguard-rules.pro b/feature/registration/api/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/feature/registration/api/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/feature/registration/api/src/main/AndroidManifest.xml b/feature/registration/api/src/main/AndroidManifest.xml new file mode 100644 index 00000000..a5918e68 --- /dev/null +++ b/feature/registration/api/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/feature/registration/impl/.gitignore b/feature/registration/impl/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/feature/registration/impl/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/feature/registration/impl/build.gradle.kts b/feature/registration/impl/build.gradle.kts new file mode 100644 index 00000000..752c597e --- /dev/null +++ b/feature/registration/impl/build.gradle.kts @@ -0,0 +1,42 @@ +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) +} + +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" + } +} + +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/registration/impl/consumer-rules.pro b/feature/registration/impl/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/feature/registration/impl/proguard-rules.pro b/feature/registration/impl/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/feature/registration/impl/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/feature/registration/impl/src/main/AndroidManifest.xml b/feature/registration/impl/src/main/AndroidManifest.xml new file mode 100644 index 00000000..a5918e68 --- /dev/null +++ b/feature/registration/impl/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 616cc767..3fcb11e5 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -57,4 +57,5 @@ include(":feature:public-collections:api") include(":feature:questions-or-collections") include(":feature:questions-or-collections:api") include(":feature:questions-or-collections:impl") - +include(":feature:registration:api") +include(":feature:registration:impl") From 9aa84bc23c6fbdda5a7c03979c6b237283d77676 Mon Sep 17 00:00:00 2001 From: MODDII Date: Thu, 5 Feb 2026 18:19:23 +0300 Subject: [PATCH 2/5] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=20=D0=BC=D0=BE=D0=B4=D1=83=D0=BB=D1=8C=20authentication?= =?UTF-8?q?=20(api/impl)=20=D0=B2=20impl=20=D1=81=D0=B4=D0=B5=D0=BB=D0=B0?= =?UTF-8?q?=D0=BB=20=D1=80=D0=B5=D0=B7=D0=B4=D0=B5=D0=BB=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D0=B5=20=D0=BD=D0=B0=20forgot,=20login,=20registration.=20?= =?UTF-8?q?=D0=A1=D0=B2=D0=B5=D1=80=D1=81=D1=82=D0=B0=D0=BB=20=D1=8D=D0=BA?= =?UTF-8?q?=D1=80=D0=B0=D0=BD=20=D1=80=D0=B5=D0=B3=D0=B8=D1=81=D1=82=D1=80?= =?UTF-8?q?=D0=B0=D1=86=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/.gitignore | 0 .../api/build.gradle.kts | 2 +- .../api/consumer-rules.pro | 0 .../api/proguard-rules.pro | 0 .../api/src/main/AndroidManifest.xml | 0 .../impl/.gitignore | 0 feature/authentication/impl/build.gradle.kts | 71 ++++ .../impl/consumer-rules.pro | 0 .../impl/proguard-rules.pro | 0 .../impl/src/main/AndroidManifest.xml | 0 .../presentation/PasswordValidation.kt | 9 + .../presentation/RegistrationScreen.kt | 320 ++++++++++++++++++ .../presentation/RegistrationUiState.kt | 33 ++ .../presentation/RegistrationViewModel.kt | 82 +++++ feature/registration/impl/build.gradle.kts | 42 --- gradle/libs.versions.toml | 1 + settings.gradle.kts | 4 +- 17 files changed, 519 insertions(+), 45 deletions(-) rename feature/{registration => authentication}/api/.gitignore (100%) rename feature/{registration => authentication}/api/build.gradle.kts (95%) rename feature/{registration => authentication}/api/consumer-rules.pro (100%) rename feature/{registration => authentication}/api/proguard-rules.pro (100%) rename feature/{registration => authentication}/api/src/main/AndroidManifest.xml (100%) rename feature/{registration => authentication}/impl/.gitignore (100%) create mode 100644 feature/authentication/impl/build.gradle.kts rename feature/{registration => authentication}/impl/consumer-rules.pro (100%) rename feature/{registration => authentication}/impl/proguard-rules.pro (100%) rename feature/{registration => authentication}/impl/src/main/AndroidManifest.xml (100%) create mode 100644 feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/registration/presentation/PasswordValidation.kt create mode 100644 feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/registration/presentation/RegistrationScreen.kt create mode 100644 feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/registration/presentation/RegistrationUiState.kt create mode 100644 feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/registration/presentation/RegistrationViewModel.kt delete mode 100644 feature/registration/impl/build.gradle.kts diff --git a/feature/registration/api/.gitignore b/feature/authentication/api/.gitignore similarity index 100% rename from feature/registration/api/.gitignore rename to feature/authentication/api/.gitignore diff --git a/feature/registration/api/build.gradle.kts b/feature/authentication/api/build.gradle.kts similarity index 95% rename from feature/registration/api/build.gradle.kts rename to feature/authentication/api/build.gradle.kts index e1aa6ead..e10f3d40 100644 --- a/feature/registration/api/build.gradle.kts +++ b/feature/authentication/api/build.gradle.kts @@ -4,7 +4,7 @@ plugins { } android { - namespace = "ru.yeahub.api" + namespace = "ru.yeahub.authentication.api" compileSdk = 35 defaultConfig { diff --git a/feature/registration/api/consumer-rules.pro b/feature/authentication/api/consumer-rules.pro similarity index 100% rename from feature/registration/api/consumer-rules.pro rename to feature/authentication/api/consumer-rules.pro diff --git a/feature/registration/api/proguard-rules.pro b/feature/authentication/api/proguard-rules.pro similarity index 100% rename from feature/registration/api/proguard-rules.pro rename to feature/authentication/api/proguard-rules.pro diff --git a/feature/registration/api/src/main/AndroidManifest.xml b/feature/authentication/api/src/main/AndroidManifest.xml similarity index 100% rename from feature/registration/api/src/main/AndroidManifest.xml rename to feature/authentication/api/src/main/AndroidManifest.xml diff --git a/feature/registration/impl/.gitignore b/feature/authentication/impl/.gitignore similarity index 100% rename from feature/registration/impl/.gitignore rename to feature/authentication/impl/.gitignore diff --git a/feature/authentication/impl/build.gradle.kts b/feature/authentication/impl/build.gradle.kts new file mode 100644 index 00000000..2759ccdb --- /dev/null +++ b/feature/authentication/impl/build.gradle.kts @@ -0,0 +1,71 @@ +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.compose) +} + +android { + namespace = "ru.yeahub.authentication.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 + } + buildFeatures { + compose = true + } + testOptions { + unitTests.all { + it.useJUnitPlatform() + it.jvmArgs("-XX:+EnableDynamicAgentLoading") + } + } + kotlinOptions { + jvmTarget = "11" + } +} + +dependencies { + implementation(libs.androidx.compose.material.icons.extended) + implementation(project(":core:ui")) + implementation(project(":core:utils")) + implementation(project(":core:network-api")) + implementation(project(":core:navigation-api")) + implementation(libs.timber) + implementation(libs.androidx.core.ktx) + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.androidx.lifecycle.viewmodel.ktx) + implementation(libs.androidx.ui) + implementation(libs.androidx.ui.graphics) + implementation(libs.androidx.material3) + implementation(libs.androidx.runtime.android) + implementation(libs.androidx.ui.tooling.preview) + implementation(libs.koin.core) + implementation(libs.koin.android) + implementation(libs.koin.compose) + implementation(libs.compose.shimmer) + implementation(libs.androidx.navigation.compose) + debugImplementation(libs.androidx.ui.tooling) + debugImplementation(libs.androidx.ui.test.manifest) + testImplementation(libs.junit.jupiter) + testImplementation(platform(libs.junit.bom)) + testRuntimeOnly(libs.junit.platform.launcher) + testImplementation(libs.mockk) +} \ No newline at end of file diff --git a/feature/registration/impl/consumer-rules.pro b/feature/authentication/impl/consumer-rules.pro similarity index 100% rename from feature/registration/impl/consumer-rules.pro rename to feature/authentication/impl/consumer-rules.pro diff --git a/feature/registration/impl/proguard-rules.pro b/feature/authentication/impl/proguard-rules.pro similarity index 100% rename from feature/registration/impl/proguard-rules.pro rename to feature/authentication/impl/proguard-rules.pro diff --git a/feature/registration/impl/src/main/AndroidManifest.xml b/feature/authentication/impl/src/main/AndroidManifest.xml similarity index 100% rename from feature/registration/impl/src/main/AndroidManifest.xml rename to feature/authentication/impl/src/main/AndroidManifest.xml 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 new file mode 100644 index 00000000..ad73ebed --- /dev/null +++ b/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/registration/presentation/PasswordValidation.kt @@ -0,0 +1,9 @@ +package ru.yeahub.authentication.impl.registration.presentation + +internal fun isPasswordValid(password: String): Boolean { + if (password.length < 8) return false + val hasDigit = password.any(Char::isDigit) + val hasLower = password.any { it in 'a'..'z' } + val hasUpper = password.any { it in 'A'..'Z' } + return hasDigit && hasLower && hasUpper +} \ No newline at end of file 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 new file mode 100644 index 00000000..e8a37136 --- /dev/null +++ b/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/registration/presentation/RegistrationScreen.kt @@ -0,0 +1,320 @@ +package ru.yeahub.authentication.impl.registration.presentation + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +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.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.ClickableText +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Visibility +import androidx.compose.material.icons.filled.VisibilityOff +import androidx.compose.material3.Button +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.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +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 + +@Composable +fun RegistrationScreen( + state: RegistrationUiState, + onAction: (RegistrationAction) -> Unit, + onOpenPdPolicy: () -> Unit, + onOpenOffer: () -> Unit +) { + val linkColor = MaterialTheme.colorScheme.primary + + Scaffold { paddings -> + Column( + modifier = Modifier + .padding(paddings) + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(horizontal = 16.dp, vertical = 24.dp), + verticalArrangement = Arrangement.spacedBy(14.dp) + ) { + Text( + text = "Регистрация", + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.SemiBold + ) + + FormTextField( + title = "Никнейм", + placeholder = "Введите никнейм", + value = state.nickname, + onValueChange = { onAction(RegistrationAction.NicknameChanged(it)) }, + keyboardType = KeyboardType.Ascii + ) + + FormTextField( + title = "Электронная почта", + placeholder = "Введите электронную почту", + value = state.email, + onValueChange = { onAction(RegistrationAction.EmailChanged(it)) }, + keyboardType = KeyboardType.Email + ) + + FormPasswordField( + title = "Пароль", + placeholder = "Введите пароль", + value = state.password, + isVisible = state.isPasswordVisible, + onValueChange = { onAction(RegistrationAction.PasswordChanged(it)) }, + onToggleVisibility = { onAction(RegistrationAction.TogglePasswordVisible) } + ) + + FormPasswordField( + title = "Подтвердить пароль", + placeholder = "Введите пароль", + value = state.confirmPassword, + isVisible = state.isConfirmPasswordVisible, + onValueChange = { onAction(RegistrationAction.ConfirmPasswordChanged(it)) }, + onToggleVisibility = { onAction(RegistrationAction.ToggleConfirmPasswordVisible) } + ) + + ConsentRow( + checked = state.isPdAccepted, + onCheckedChange = { onAction(RegistrationAction.PdAcceptedChanged(it)) }, + text = pdConsentText(linkColor), + onLinkClicked = { tag -> + if (tag == "pd") onOpenPdPolicy() + }, + accentColor = linkColor + ) + + ConsentRow( + checked = state.isOfferAccepted, + onCheckedChange = { onAction(RegistrationAction.OfferAcceptedChanged(it)) }, + text = offerConsentText(linkColor), + onLinkClicked = { tag -> + if (tag == "offer") onOpenOffer() + }, + accentColor = linkColor + ) + + ConsentRow( + checked = state.isMailingAccepted, + onCheckedChange = { onAction(RegistrationAction.MailingAcceptedChanged(it)) }, + text = AnnotatedString("Даю согласие на получение рекламных и информационных рассылок"), + accentColor = linkColor, + onLinkClicked = { } + ) + + Spacer(Modifier.height(8.dp)) + + Button( + onClick = { onAction(RegistrationAction.SubmitClicked) }, + enabled = state.isSubmitEnabled, + modifier = Modifier.fillMaxWidth() + ) { + Text("Зарегистрироваться") + } + } + } +} + +@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 + ) + ) +} + +private fun pdConsentText(linkColor: Color): AnnotatedString = buildAnnotatedString { + append("Даю ") + pushStringAnnotation(tag = "pd", annotation = "pd") + pushStyle(SpanStyle(color = linkColor)) + append("Согласие на обработку персональных данных") + pop() + pop() + append(", в соответствии с Политикой в отношении ПД") +} + +private fun offerConsentText(linkColor: Color): AnnotatedString = buildAnnotatedString { + append("Подтверждаю что ознакомился(-ась) ") + append("с ") + pushStringAnnotation(tag = "offer", annotation = "offer") + pushStyle(SpanStyle(color = linkColor)) + append("Договором-офертой") + pop() + pop() +} + +@Composable +private fun ConsentRow( + checked: Boolean, + onCheckedChange: (Boolean) -> Unit, + text: AnnotatedString, + accentColor: Color, + onLinkClicked: (tag: String) -> Unit +) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.Top + ) { + Checkbox( + checked = checked, + onCheckedChange = onCheckedChange, + colors = CheckboxDefaults.colors( + checkedColor = accentColor, + checkmarkColor = MaterialTheme.colorScheme.onPrimary, + uncheckedColor = MaterialTheme.colorScheme.outline + ) + ) + Spacer(Modifier.width(8.dp)) + ClickableText( + text = text, + style = MaterialTheme.typography.bodySmall.copy( + color = MaterialTheme.colorScheme.onSurface, + textDecoration = null + ), + onClick = { offset -> + text.getStringAnnotations(start = offset, end = offset) + .firstOrNull() + ?.let { onLinkClicked(it.tag) } + ?: onCheckedChange(!checked) + } + ) + } +} + +@Preview(showBackground = true, widthDp = 360) +@Composable +private fun RegistrationScreenPreview() { + MaterialTheme { + RegistrationScreen( + state = RegistrationUiState( + nickname = "", + email = "", + password = "", + confirmPassword = "", + isPasswordVisible = false, + isConfirmPasswordVisible = false, + isPdAccepted = true, + isOfferAccepted = true, + isMailingAccepted = false, + isSubmitEnabled = false + ), + onAction = {}, + onOpenPdPolicy = {}, + onOpenOffer = {} + ) + } +} \ 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 new file mode 100644 index 00000000..b9809bd3 --- /dev/null +++ b/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/registration/presentation/RegistrationUiState.kt @@ -0,0 +1,33 @@ +package ru.yeahub.authentication.impl.registration.presentation + +data class RegistrationUiState( + val nickname: String, + val email: String, + val password: String, + val confirmPassword: String, + + val isPdAccepted: Boolean, + val isOfferAccepted: Boolean, + val isMailingAccepted: Boolean, + + val isPasswordVisible: Boolean, + val isConfirmPasswordVisible: Boolean, + + val isSubmitEnabled: Boolean +) + +sealed class RegistrationAction { + data class NicknameChanged(val value: String): RegistrationAction() + data class EmailChanged(val value: String): RegistrationAction() + data class PasswordChanged(val value: String): RegistrationAction() + data class ConfirmPasswordChanged(val value: String): RegistrationAction() + + data class PdAcceptedChanged(val value: Boolean): RegistrationAction() + data class OfferAcceptedChanged(val value: Boolean): RegistrationAction() + data class MailingAcceptedChanged(val value: Boolean): RegistrationAction() + + data object TogglePasswordVisible: RegistrationAction() + data object ToggleConfirmPasswordVisible: RegistrationAction() + + data object SubmitClicked: RegistrationAction() +} \ No newline at end of file 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 new file mode 100644 index 00000000..788493d4 --- /dev/null +++ b/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/registration/presentation/RegistrationViewModel.kt @@ -0,0 +1,82 @@ +package ru.yeahub.authentication.impl.registration.presentation + +import android.util.Patterns +import androidx.lifecycle.ViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update + +class RegistrationViewModel: ViewModel() { + + private val _state = MutableStateFlow( + RegistrationUiState( + nickname = "", + email = "", + password = "", + confirmPassword = "", + isPdAccepted = false, + isOfferAccepted = false, + isMailingAccepted = false, + isPasswordVisible = false, + isConfirmPasswordVisible = false, + isSubmitEnabled = false + ) + ) + val state: StateFlow = _state + + fun onAction(action: RegistrationAction) { + when(action) { + is RegistrationAction.ConfirmPasswordChanged -> { + _state.update { it.copy(confirmPassword = action.value).revalidate() } + } + + is RegistrationAction.EmailChanged -> { + _state.update { it.copy(email = action.value).revalidate() } + } + + is RegistrationAction.MailingAcceptedChanged -> { + _state.update { it.copy(isMailingAccepted = action.value).revalidate() } + } + + is RegistrationAction.NicknameChanged -> { + _state.update { it.copy(nickname = action.value).revalidate() } + } + + is RegistrationAction.OfferAcceptedChanged -> { + _state.update { it.copy(isOfferAccepted = action.value).revalidate() } + } + + is RegistrationAction.PasswordChanged -> { + _state.update { it.copy(password = action.value).revalidate() } + } + + is RegistrationAction.PdAcceptedChanged -> { + _state.update { it.copy(isPdAccepted = action.value).revalidate() } + } + + RegistrationAction.SubmitClicked -> { + + } + + RegistrationAction.ToggleConfirmPasswordVisible -> { + _state.update { it.copy(isConfirmPasswordVisible = !it.isConfirmPasswordVisible) } + } + + RegistrationAction.TogglePasswordVisible -> { + _state.update { it.copy(isPasswordVisible = !it.isPasswordVisible) } + } + } + } + + private fun RegistrationUiState.revalidate(): RegistrationUiState { + 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 + + return copy( + isSubmitEnabled = nicknameOk && emailOk && passOk && confirmOk && requiredConsentsOk + ) + } +} \ No newline at end of file diff --git a/feature/registration/impl/build.gradle.kts b/feature/registration/impl/build.gradle.kts deleted file mode 100644 index 752c597e..00000000 --- a/feature/registration/impl/build.gradle.kts +++ /dev/null @@ -1,42 +0,0 @@ -plugins { - alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) -} - -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" - } -} - -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/gradle/libs.versions.toml b/gradle/libs.versions.toml index 776d5c21..72143a64 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -32,6 +32,7 @@ uiToolingPreviewAndroid = "1.8.3" runtime = "1.9.0" [libraries] +androidx-compose-material-icons-extended = { module = "androidx.compose.material:material-icons-extended" } androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } androidx-lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "lifecycleRuntimeCompose" } diff --git a/settings.gradle.kts b/settings.gradle.kts index 3fcb11e5..261ce0c2 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -57,5 +57,5 @@ include(":feature:public-collections:api") include(":feature:questions-or-collections") include(":feature:questions-or-collections:api") include(":feature:questions-or-collections:impl") -include(":feature:registration:api") -include(":feature:registration:impl") +include(":feature:authentication:api") +include(":feature:authentication:impl") From 027dd92b65e354f80bc0bfbcb88e0716eac0f204 Mon Sep 17 00:00:00 2001 From: MODDII Date: Thu, 5 Feb 2026 19:17:12 +0300 Subject: [PATCH 3/5] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D1=8B=20=D0=B7=D0=B0=D0=BC=D0=B5=D1=87=D0=B0=D0=BD?= =?UTF-8?q?=D0=B8=D1=8F=20ktlint=20=D0=B8=20detekt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/PasswordValidation.kt | 4 ++- .../presentation/RegistrationScreen.kt | 16 ++++++------ .../presentation/RegistrationUiState.kt | 26 +++++++------------ .../presentation/RegistrationViewModel.kt | 8 +++--- 4 files changed, 24 insertions(+), 30 deletions(-) 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 index ad73ebed..7a82ef26 100644 --- 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 @@ -1,7 +1,9 @@ package ru.yeahub.authentication.impl.registration.presentation +private const val MIN_PASSWORD_LENGTH = 8 + internal fun isPasswordValid(password: String): Boolean { - if (password.length < 8) return false + if (password.length < MIN_PASSWORD_LENGTH) return false val hasDigit = password.any(Char::isDigit) val hasLower = password.any { it in 'a'..'z' } val hasUpper = password.any { it in 'A'..'Z' } 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 e8a37136..46758cd7 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 @@ -297,20 +297,20 @@ private fun ConsentRow( @Preview(showBackground = true, widthDp = 360) @Composable -private fun RegistrationScreenPreview() { +fun RegistrationScreenPreview() { MaterialTheme { RegistrationScreen( state = RegistrationUiState( - nickname = "", - email = "", - password = "", - confirmPassword = "", - isPasswordVisible = false, - isConfirmPasswordVisible = false, + nickname = "admin", + email = "admin@mail.ru", + password = "1234", + confirmPassword = "1234", + isPasswordVisible = true, + isConfirmPasswordVisible = true, isPdAccepted = true, isOfferAccepted = true, isMailingAccepted = false, - isSubmitEnabled = false + isSubmitEnabled = true ), onAction = {}, onOpenPdPolicy = {}, 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 b9809bd3..15e96754 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 @@ -5,29 +5,23 @@ data class RegistrationUiState( val email: String, val password: String, val confirmPassword: String, - val isPdAccepted: Boolean, val isOfferAccepted: Boolean, val isMailingAccepted: Boolean, - val isPasswordVisible: Boolean, val isConfirmPasswordVisible: Boolean, - val isSubmitEnabled: Boolean ) sealed class RegistrationAction { - data class NicknameChanged(val value: String): RegistrationAction() - data class EmailChanged(val value: String): RegistrationAction() - data class PasswordChanged(val value: String): RegistrationAction() - data class ConfirmPasswordChanged(val value: String): RegistrationAction() - - data class PdAcceptedChanged(val value: Boolean): RegistrationAction() - data class OfferAcceptedChanged(val value: Boolean): RegistrationAction() - data class MailingAcceptedChanged(val value: Boolean): RegistrationAction() - - data object TogglePasswordVisible: RegistrationAction() - data object ToggleConfirmPasswordVisible: RegistrationAction() - - data object SubmitClicked: RegistrationAction() + data class NicknameChanged(val value: String) : RegistrationAction() + data class EmailChanged(val value: String) : RegistrationAction() + data class PasswordChanged(val value: String) : RegistrationAction() + data class ConfirmPasswordChanged(val value: String) : RegistrationAction() + data class PdAcceptedChanged(val value: Boolean) : RegistrationAction() + data class OfferAcceptedChanged(val value: Boolean) : RegistrationAction() + data class MailingAcceptedChanged(val value: Boolean) : RegistrationAction() + data object TogglePasswordVisible : RegistrationAction() + data object ToggleConfirmPasswordVisible : RegistrationAction() + data object SubmitClicked : RegistrationAction() } \ No newline at end of file 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 788493d4..0af91624 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 @@ -6,7 +6,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update -class RegistrationViewModel: ViewModel() { +class RegistrationViewModel : ViewModel() { private val _state = MutableStateFlow( RegistrationUiState( @@ -25,7 +25,7 @@ class RegistrationViewModel: ViewModel() { val state: StateFlow = _state fun onAction(action: RegistrationAction) { - when(action) { + when (action) { is RegistrationAction.ConfirmPasswordChanged -> { _state.update { it.copy(confirmPassword = action.value).revalidate() } } @@ -54,9 +54,7 @@ class RegistrationViewModel: ViewModel() { _state.update { it.copy(isPdAccepted = action.value).revalidate() } } - RegistrationAction.SubmitClicked -> { - - } + RegistrationAction.SubmitClicked -> {} RegistrationAction.ToggleConfirmPasswordVisible -> { _state.update { it.copy(isConfirmPasswordVisible = !it.isConfirmPasswordVisible) } From 3e2a945b0de50be998b2d4eb7305ce43618d8c67 Mon Sep 17 00:00:00 2001 From: MODDII Date: Thu, 5 Feb 2026 19:48:24 +0300 Subject: [PATCH 4/5] =?UTF-8?q?=D0=9F=D0=BE=D0=B4=D0=BA=D0=BB=D1=8E=D1=87?= =?UTF-8?q?=D0=B8=D0=BB=20core=5Fui=20=D0=BA=20=D0=BC=D0=BE=D0=B4=D1=83?= =?UTF-8?q?=D0=BB=D1=8E.=20=D0=9F=D0=B5=D1=80=D0=B5=D0=B8=D1=81=D0=BF?= =?UTF-8?q?=D0=BE=D0=BB=D1=8C=D0=B7=D0=BE=D0=B2=D0=B0=D0=BB=20=D0=BD=D1=83?= =?UTF-8?q?=D0=B6=D0=BD=D1=83=D1=8E=20=D0=BA=D0=BD=D0=BE=D0=BF=D0=BA=D1=83?= =?UTF-8?q?=20=D0=B8=20=D0=BF=D0=BE=D0=B4=D0=BF=D1=80=D0=B0=D0=B8=D0=BB=20?= =?UTF-8?q?=D1=86=D0=B2=D0=B5=D1=82=20=D1=87=D0=B5=D0=BA=D0=B1=D0=BE=D0=BA?= =?UTF-8?q?=D1=81=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/gradle.xml | 6 +++--- feature/authentication/impl/build.gradle.kts | 1 + .../registration/presentation/RegistrationScreen.kt | 13 +++++++------ 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/.idea/gradle.xml b/.idea/gradle.xml index 60bb4b54..7a53b8e1 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -21,6 +21,9 @@