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/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 {
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-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
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/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")
}
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(),
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/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/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/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(
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/RegistrationScreen.kt b/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/registration/presentation/RegistrationScreen.kt
index 3110da3a..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
@@ -18,99 +18,173 @@ 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
+import ru.yeahub.core_utils.common.TextOrResource
@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 +222,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 +272,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 ->
@@ -314,24 +286,132 @@ private fun ConsentRow(
}
}
-@Preview(showBackground = true)
+@Preview(showBackground = true, name = "Empty State")
@Composable
-fun RegistrationScreenPreview() {
- MaterialTheme {
- RegistrationScreen(
+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",
- email = "admin@mail.ru",
- password = "1234",
- confirmPassword = "1234",
+ 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 = 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 = TextOrResource.Text("Имя пользователя не может быть пустым"),
+ email = "invalid-email",
+ emailError = TextOrResource.Text("Некорректный email"),
+ password = "123",
+ passwordError = TextOrResource.Text("Пароль слишком короткий"),
+ confirmPassword = "456",
+ confirmPasswordError = 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,
- isSubmitEnabled = true
+ isMailingAccepted = true,
+ isSubmitEnabled = true,
+ isEmailTouched = true,
+ isPasswordTouched = true,
+ isConfirmPasswordTouched = true
)
),
onAction = {},
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
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..6211ee57
--- /dev/null
+++ b/feature/authentication/impl/src/test/java/ru/yeahub/authentication/impl/registration/presentation/RegistrationUiStateMapperTest.kt
@@ -0,0 +1,254 @@
+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
+ ) {
+ 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)
+
+ val resultState = mapper.mapToUpdatedState(currentState, RegistrationAction.NicknameChanged(nickname))
+
+ assertEquals(expectedSubmitEnabled, resultState.formState.isSubmitEnabled)
+ }
+
+ companion object {
+ @JvmStatic
+ fun provideValidationCases(): Stream = Stream.of(
+ Arguments.of(
+ "user",
+ "test@test.com",
+ true,
+ "Pass123!",
+ "Pass123!",
+ true,
+ true,
+ true
+ ),
+ Arguments.of(
+ "",
+ "test@test.com",
+ true,
+ "Pass123!",
+ "Pass123!",
+ true,
+ true,
+ false
+ ),
+ Arguments.of(
+ "user",
+ "invalid",
+ false,
+ "Pass123!",
+ "Pass123!",
+ true,
+ true,
+ false
+ ),
+ Arguments.of(
+ "user",
+ "test@test.com",
+ true,
+ "short",
+ "short",
+ true,
+ true,
+ false
+ ),
+ Arguments.of(
+ "user",
+ "test@test.com",
+ true,
+ "Pass123!",
+ "Mismatch!",
+ true,
+ true,
+ false
+ ),
+ Arguments.of(
+ "user",
+ "test@test.com",
+ true,
+ "short",
+ "short",
+ true,
+ true,
+ false
+ ),
+ Arguments.of(
+ "user",
+ "test@test.com",
+ true,
+ "nospecial123",
+ "nospecial123",
+ true,
+ true,
+ false
+ ),
+ Arguments.of(
+ "user",
+ "test@test.com",
+ true,
+ "NoDigit!",
+ "NoDigit!",
+ true,
+ true,
+ false
+ ),
+ Arguments.of(
+ "user",
+ "test@test.com",
+ true,
+ "nouppercase1!",
+ "nouppercase1!",
+ true,
+ true,
+ false
+ ),
+ Arguments.of(
+ "user",
+ "test@test.com",
+ true,
+ "Pass123!",
+ "Pass123!",
+ false,
+ true,
+ false
+ ),
+ Arguments.of(
+ "user",
+ "test@test.com",
+ true,
+ "Pass123!",
+ "Pass123!",
+ true,
+ false,
+ false
+ )
+ )
+
+ @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
+ ) {
+ val form = mapper.getInitialFormState().copy(
+ password = password,
+ isPasswordTouched = true
+ )
+ val currentState = RegistrationUiState.Content(form)
+
+ val resultState = mapper.mapToUpdatedState(currentState, RegistrationAction.PasswordChanged(password))
+
+ 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)
+
+ val stateAfterFocusLost = mapper.mapToUpdatedState(
+ currentState,
+ RegistrationAction.EmailFocusChanged(false)
+ )
+ assertTrue(stateAfterFocusLost.formState.isEmailTouched)
+
+ val stateAfterFocusGained = mapper.mapToUpdatedState(
+ stateAfterFocusLost,
+ RegistrationAction.EmailFocusChanged(true)
+ )
+ assertFalse(stateAfterFocusGained.formState.isEmailTouched)
+ }
+}
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))