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 60939a41..654934c9 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -99,8 +99,6 @@ dependencies {
implementation(project(":feature:questions-or-collections:api"))
implementation(project(":feature:public-collections:api"))
implementation(project(":feature:selection-specializations:api"))
-// implementation(project(":feature:forgot-password:api"))
-// implementation(project(":feature:forgot-password:impl"))
implementation(project(":feature:example-home:impl"))
implementation(project(":feature:example-profile:impl"))
implementation(project(":feature:example-questions:impl"))
@@ -110,6 +108,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 303b420d..544806a5 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.datastore_impl.di.datastoreModule
import ru.yeahub.detail_question.impl.di.detailQuestionFeatureModule
import ru.yeahub.example_details.impl.detailsFeatureModule
@@ -55,7 +56,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..6bd0f300 100644
--- a/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/registration/presentation/RegistrationScreen.kt
+++ b/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/registration/presentation/RegistrationScreen.kt
@@ -18,245 +18,225 @@ 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.LocalMinimumInteractiveComponentSize
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.unit.Dp
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
+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)
- }
- )
-
- ConsentRow(
- checked = form.isPdAccepted,
- onCheckedChange = {
- onAction(RegistrationAction.PdAcceptedChanged(it))
},
- text = pdConsentText(linkColor),
- onLinkClicked = { tag -> if (tag == "pd") onOpenPdPolicy() }
+ keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password)
)
- ConsentRow(
- checked = form.isOfferAccepted,
- onCheckedChange = {
- onAction(RegistrationAction.OfferAcceptedChanged(it))
- },
- text = offerConsentText(linkColor),
- onLinkClicked = { tag -> if (tag == "offer") onOpenOffer() }
- )
+ Column(
+ modifier = Modifier.fillMaxWidth(),
+ verticalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ ConsentRow(
+ checked = form.isPdAccepted,
+ onCheckedChange = {
+ onAction(RegistrationAction.PdAcceptedChanged(it))
+ },
+ text = pdConsentText(linkColor),
+ onLinkClicked = { tag -> if (tag == "pd") onOpenPdPolicy() }
+ )
- ConsentRow(
- checked = form.isMailingAccepted,
- onCheckedChange = {
- onAction(RegistrationAction.MailingAcceptedChanged(it))
- },
- text =
- AnnotatedString(
- stringResource(R.string.marketing_opt_in_text)
- ),
- onLinkClicked = {}
- )
+ ConsentRow(
+ checked = form.isOfferAccepted,
+ onCheckedChange = {
+ onAction(RegistrationAction.OfferAcceptedChanged(it))
+ },
+ text = offerConsentText(linkColor),
+ onLinkClicked = { tag -> if (tag == "offer") onOpenOffer() }
+ )
+
+ ConsentRow(
+ checked = form.isMailingAccepted,
+ onCheckedChange = {
+ onAction(RegistrationAction.MailingAcceptedChanged(it))
+ },
+ text =
+ AnnotatedString(
+ stringResource(R.string.marketing_opt_in_text)
+ ),
+ onLinkClicked = {}
+ )
+ }
Spacer(Modifier.height(8.dp))
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))
@@ -286,22 +266,25 @@ private fun ConsentRow(
onLinkClicked: (tag: String) -> Unit
) {
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.Top) {
- Checkbox(
- checked = checked,
- onCheckedChange = onCheckedChange,
- colors =
- CheckboxDefaults.colors(
- checkedColor = Theme.colors.purple700,
- uncheckedColor = Theme.colors.purple200,
- checkmarkColor = Theme.colors.white900
- )
- )
+ CompositionLocalProvider(LocalMinimumInteractiveComponentSize provides Dp.Unspecified) {
+ Checkbox(
+ checked = checked,
+ onCheckedChange = onCheckedChange,
+ modifier = Modifier.padding(top = 2.dp),
+ colors =
+ CheckboxDefaults.colors(
+ checkedColor = Theme.colors.purple700,
+ uncheckedColor = Theme.colors.black200,
+ checkmarkColor = Theme.colors.white900
+ )
+ )
+ }
Spacer(Modifier.width(8.dp))
ClickableText(
text = text,
style =
- MaterialTheme.typography.bodySmall.copy(
- color = MaterialTheme.colorScheme.onSurface,
+ Theme.typography.body1.copy(
+ color = Theme.colors.black900,
textDecoration = null
),
onClick = { offset ->
@@ -314,24 +297,132 @@ private fun ConsentRow(
}
}
-@Preview(showBackground = true)
+@Preview(showBackground = true, name = "Empty State")
+@Composable
+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() {
- MaterialTheme {
- RegistrationScreen(
+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..1e06ac94 100644
--- a/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/registration/presentation/RegistrationUiStateMapper.kt
+++ b/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/registration/presentation/RegistrationUiStateMapper.kt
@@ -1,46 +1,211 @@
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)
+
+ is RegistrationAction.SubmitClicked -> 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)
+ is RegistrationAction.PdAcceptedChanged,
+ is RegistrationAction.OfferAcceptedChanged,
+ is RegistrationAction.MailingAcceptedChanged,
+ is RegistrationAction.TogglePasswordVisible,
+ is RegistrationAction.ToggleConfirmPasswordVisible,
+ is RegistrationAction.SubmitClicked,
+ is RegistrationAction.EmailFocusChanged,
+ is RegistrationAction.PasswordFocusChanged,
+ is RegistrationAction.ConfirmPasswordFocusChanged -> form
+ }
- 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())
+ is RegistrationAction.NicknameChanged,
+ is RegistrationAction.EmailChanged,
+ is RegistrationAction.PasswordChanged,
+ is RegistrationAction.ConfirmPasswordChanged,
+ is RegistrationAction.PdAcceptedChanged,
+ is RegistrationAction.OfferAcceptedChanged,
+ is RegistrationAction.MailingAcceptedChanged,
+ is RegistrationAction.TogglePasswordVisible,
+ is RegistrationAction.ToggleConfirmPasswordVisible,
+ is RegistrationAction.SubmitClicked -> form
+ }
+
+ private fun handleConsentAction(
+ 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)
+ is RegistrationAction.NicknameChanged,
+ is RegistrationAction.EmailChanged,
+ is RegistrationAction.PasswordChanged,
+ is RegistrationAction.ConfirmPasswordChanged,
+ is RegistrationAction.TogglePasswordVisible,
+ is RegistrationAction.ToggleConfirmPasswordVisible,
+ is RegistrationAction.SubmitClicked,
+ is RegistrationAction.EmailFocusChanged,
+ is RegistrationAction.PasswordFocusChanged,
+ is RegistrationAction.ConfirmPasswordFocusChanged -> form
+ }
+
+ private fun handleToggleAction(
+ form: RegistrationFormState,
+ action: RegistrationAction
+ ): RegistrationFormState = when (action) {
+ is RegistrationAction.TogglePasswordVisible ->
+ form.copy(isPasswordVisible = !form.isPasswordVisible)
+ is RegistrationAction.ToggleConfirmPasswordVisible ->
+ form.copy(isConfirmPasswordVisible = !form.isConfirmPasswordVisible)
+ is RegistrationAction.NicknameChanged,
+ is RegistrationAction.EmailChanged,
+ is RegistrationAction.PasswordChanged,
+ is RegistrationAction.ConfirmPasswordChanged,
+ is RegistrationAction.PdAcceptedChanged,
+ is RegistrationAction.OfferAcceptedChanged,
+ is RegistrationAction.MailingAcceptedChanged,
+ is RegistrationAction.SubmitClicked,
+ is RegistrationAction.EmailFocusChanged,
+ is RegistrationAction.PasswordFocusChanged,
+ is RegistrationAction.ConfirmPasswordFocusChanged -> form
+ }
+
+ fun mapToLoadingState(currentState: RegistrationUiState): RegistrationUiState {
+ 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 2972c407..b85e4da8 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,5 +33,14 @@
Произошла непредвиденная ошибка
Показать пароль
Скрыть пароль
+
+ 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..f9795519
--- /dev/null
+++ b/feature/authentication/impl/src/test/java/ru/yeahub/authentication/impl/registration/presentation/RegistrationUiStateMapperTest.kt
@@ -0,0 +1,405 @@
+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.assertTrue
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.extension.ExtensionContext
+import org.junit.jupiter.params.ParameterizedTest
+import org.junit.jupiter.params.provider.Arguments
+import org.junit.jupiter.params.provider.ArgumentsProvider
+import org.junit.jupiter.params.provider.ArgumentsSource
+import ru.yeahub.authentication.impl.R
+import ru.yeahub.authentication.impl.registration.domain.entity.RegistrationError
+import ru.yeahub.authentication.impl.registration.domain.entity.RegistrationException
+import ru.yeahub.core_utils.common.TextOrResource
+import ru.yeahub.core_utils.validation.EmailValidator
+import java.util.stream.Stream
+
+class RegistrationUiStateMapperTest {
+
+ private val mapper = RegistrationUiStateMapper()
+
+ @BeforeEach
+ fun setUp() {
+ mockkObject(EmailValidator)
+ every { EmailValidator.isValid(any()) } returns true
+ }
+
+ @AfterEach
+ fun tearDown() {
+ unmockkAll()
+ }
+
+ @ParameterizedTest
+ @ArgumentsSource(UpdatedStateArgumentsProvider::class)
+ fun `should update form state by action correctly`(testCase: UpdatedStateTestCase) {
+ val initialForm = testCase.setupForm(mapper.getInitialFormState())
+ val currentState = RegistrationUiState.Content(initialForm)
+
+ val result = mapper.mapToUpdatedState(currentState, testCase.action)
+
+ assertEquals(testCase.expectedForm, result.formState)
+ }
+
+ @ParameterizedTest
+ @ArgumentsSource(ValidationArgumentsProvider::class)
+ fun `should validate form and return correct isSubmitEnabled`(testCase: ValidationTestCase) {
+ every { EmailValidator.isValid(any()) } returns testCase.isEmailValid
+
+ val form = mapper.getInitialFormState().copy(
+ nickname = testCase.nickname,
+ email = testCase.email,
+ password = testCase.password,
+ confirmPassword = testCase.confirmPassword,
+ isPdAccepted = testCase.isPdAccepted,
+ isOfferAccepted = testCase.isOfferAccepted,
+ )
+ val currentState = RegistrationUiState.Content(form)
+
+ val result = mapper.mapToUpdatedState(
+ currentState,
+ testCase.action,
+ )
+
+ assertEquals(testCase.expectedSubmitEnabled, result.formState.isSubmitEnabled)
+ }
+
+ @ParameterizedTest
+ @ArgumentsSource(ErrorStateArgumentsProvider::class)
+ fun `should map exception to correct error resource`(testCase: ErrorStateTestCase) {
+ val currentState = RegistrationUiState.Content(mapper.getInitialFormState())
+
+ val result = mapper.mapToErrorState(currentState, testCase.exception)
+
+ assertTrue(result is RegistrationUiState.Error)
+ val errorMessage = (result as RegistrationUiState.Error).message as TextOrResource.Resource
+ assertEquals(testCase.expectedErrorRes, errorMessage.resource)
+ }
+
+ data class UpdatedStateTestCase(
+ val name: String,
+ val action: RegistrationAction,
+ val setupForm: (RegistrationFormState) -> RegistrationFormState = { it },
+ val expectedForm: RegistrationFormState,
+ ) {
+ override fun toString(): String = name
+ }
+
+ data class ValidationTestCase(
+ val name: String,
+ val nickname: String,
+ val email: String,
+ val isEmailValid: Boolean,
+ val password: String,
+ val confirmPassword: String,
+ val isPdAccepted: Boolean,
+ val isOfferAccepted: Boolean,
+ val action: RegistrationAction,
+ val expectedSubmitEnabled: Boolean,
+ ) {
+ override fun toString(): String = name
+ }
+
+ data class ErrorStateTestCase(
+ val name: String,
+ val exception: RegistrationException,
+ val expectedErrorRes: Int,
+ ) {
+ override fun toString(): String = name
+ }
+
+ class UpdatedStateArgumentsProvider : ArgumentsProvider {
+ override fun provideArguments(context: ExtensionContext?): Stream {
+ return Stream.of(
+ Arguments.of(
+ UpdatedStateTestCase(
+ name = "NicknameChanged обновляет nickname",
+ action = RegistrationAction.NicknameChanged("John"),
+ expectedForm = initialFormWith(nickname = "John"),
+ )
+ ),
+ Arguments.of(
+ UpdatedStateTestCase(
+ name = "EmailChanged обновляет email",
+ action = RegistrationAction.EmailChanged("test@mail.ru"),
+ expectedForm = initialFormWith(email = "test@mail.ru"),
+ )
+ ),
+ Arguments.of(
+ UpdatedStateTestCase(
+ name = "PasswordChanged обновляет password",
+ action = RegistrationAction.PasswordChanged("Pass123!"),
+ expectedForm = initialFormWith(password = "Pass123!"),
+ )
+ ),
+ Arguments.of(
+ UpdatedStateTestCase(
+ name = "ConfirmPasswordChanged обновляет confirmPassword",
+ action = RegistrationAction.ConfirmPasswordChanged("Pass123!"),
+ expectedForm = initialFormWith(confirmPassword = "Pass123!"),
+ )
+ ),
+ Arguments.of(
+ UpdatedStateTestCase(
+ name = "EmailFocusChanged(false) при непустом email ставит isEmailTouched = true",
+ action = RegistrationAction.EmailFocusChanged(false),
+ setupForm = { it.copy(email = "test@mail.ru") },
+ expectedForm = initialFormWith(
+ email = "test@mail.ru",
+ isEmailTouched = true,
+ ),
+ )
+ ),
+ Arguments.of(
+ UpdatedStateTestCase(
+ name = "EmailFocusChanged(true) не ставит isEmailTouched",
+ action = RegistrationAction.EmailFocusChanged(true),
+ setupForm = { it.copy(email = "test@mail.ru", isEmailTouched = true) },
+ expectedForm = initialFormWith(email = "test@mail.ru"),
+ )
+ ),
+ Arguments.of(
+ UpdatedStateTestCase(
+ name = "PasswordFocusChanged(false) при непустом пароле ставит isPasswordTouched = true",
+ action = RegistrationAction.PasswordFocusChanged(false),
+ setupForm = { it.copy(password = "Pass123!") },
+ expectedForm = initialFormWith(
+ password = "Pass123!",
+ isPasswordTouched = true,
+ ),
+ )
+ ),
+ Arguments.of(
+ UpdatedStateTestCase(
+ name = "ConfirmPasswordFocusChanged(false) при непустом " +
+ "пароле ставит isConfirmPasswordTouched = true",
+ action = RegistrationAction.ConfirmPasswordFocusChanged(false),
+ setupForm = { it.copy(password = "Pass123!", confirmPassword = "Pass123!") },
+ expectedForm = initialFormWith(
+ password = "Pass123!",
+ confirmPassword = "Pass123!",
+ isConfirmPasswordTouched = true,
+ ),
+ )
+ ),
+ Arguments.of(
+ UpdatedStateTestCase(
+ name = "PdAcceptedChanged(true) обновляет isPdAccepted",
+ action = RegistrationAction.PdAcceptedChanged(true),
+ expectedForm = initialFormWith(isPdAccepted = true),
+ )
+ ),
+ Arguments.of(
+ UpdatedStateTestCase(
+ name = "OfferAcceptedChanged(true) обновляет isOfferAccepted",
+ action = RegistrationAction.OfferAcceptedChanged(true),
+ expectedForm = initialFormWith(isOfferAccepted = true),
+ )
+ ),
+ Arguments.of(
+ UpdatedStateTestCase(
+ name = "MailingAcceptedChanged(true) обновляет isMailingAccepted",
+ action = RegistrationAction.MailingAcceptedChanged(true),
+ expectedForm = initialFormWith(isMailingAccepted = true),
+ )
+ ),
+ Arguments.of(
+ UpdatedStateTestCase(
+ name = "TogglePasswordVisible переключает isPasswordVisible на true",
+ action = RegistrationAction.TogglePasswordVisible,
+ expectedForm = initialFormWith(isPasswordVisible = true),
+ )
+ ),
+ Arguments.of(
+ UpdatedStateTestCase(
+ name = "ToggleConfirmPasswordVisible переключает isConfirmPasswordVisible на true",
+ action = RegistrationAction.ToggleConfirmPasswordVisible,
+ expectedForm = initialFormWith(isConfirmPasswordVisible = true),
+ )
+ ),
+ Arguments.of(
+ UpdatedStateTestCase(
+ name = "SubmitClicked не меняет форму",
+ action = RegistrationAction.SubmitClicked,
+ expectedForm = initialFormWith(),
+ )
+ ),
+ )
+ }
+ }
+
+ class ValidationArgumentsProvider : ArgumentsProvider {
+ override fun provideArguments(context: ExtensionContext?): Stream {
+ return Stream.of(
+ Arguments.of(
+ ValidationTestCase(
+ name = "Все поля валидны — submit разрешён",
+ nickname = "user",
+ email = "test@test.com",
+ isEmailValid = true,
+ password = "Pass123!",
+ confirmPassword = "Pass123!",
+ isPdAccepted = true,
+ isOfferAccepted = true,
+ expectedSubmitEnabled = true,
+ action = RegistrationAction.NicknameChanged("user")
+ )
+ ),
+ Arguments.of(
+ ValidationTestCase(
+ name = "Пустой nickname — submit запрещён",
+ nickname = "",
+ email = "test@test.com",
+ isEmailValid = true,
+ password = "Pass123!",
+ confirmPassword = "Pass123!",
+ isPdAccepted = true,
+ isOfferAccepted = true,
+ expectedSubmitEnabled = false,
+ action = RegistrationAction.NicknameChanged("")
+ )
+ ),
+ Arguments.of(
+ ValidationTestCase(
+ name = "Невалидный email — submit запрещён",
+ nickname = "user",
+ email = "invalid",
+ isEmailValid = false,
+ password = "Pass123!",
+ confirmPassword = "Pass123!",
+ isPdAccepted = true,
+ isOfferAccepted = true,
+ expectedSubmitEnabled = false,
+ action = RegistrationAction.EmailChanged("user")
+ )
+ ),
+ Arguments.of(
+ ValidationTestCase(
+ name = "Слабый пароль — submit запрещён",
+ nickname = "user",
+ email = "test@test.com",
+ isEmailValid = true,
+ password = "short",
+ confirmPassword = "short",
+ isPdAccepted = true,
+ isOfferAccepted = true,
+ expectedSubmitEnabled = false,
+ action = RegistrationAction.PasswordChanged("1234")
+ )
+ ),
+ Arguments.of(
+ ValidationTestCase(
+ name = "Пароли не совпадают — submit запрещён",
+ nickname = "user",
+ email = "test@test.com",
+ isEmailValid = true,
+ password = "Pass123!",
+ confirmPassword = "Mismatch!",
+ isPdAccepted = true,
+ isOfferAccepted = true,
+ expectedSubmitEnabled = false,
+ action = RegistrationAction.ConfirmPasswordChanged("1234")
+ )
+ ),
+ Arguments.of(
+ ValidationTestCase(
+ name = "ПД не принят — submit запрещён",
+ nickname = "user",
+ email = "test@test.com",
+ isEmailValid = true,
+ password = "Pass123!",
+ confirmPassword = "Pass123!",
+ isPdAccepted = false,
+ isOfferAccepted = true,
+ expectedSubmitEnabled = false,
+ action = RegistrationAction.PdAcceptedChanged(false)
+ )
+ ),
+ Arguments.of(
+ ValidationTestCase(
+ name = "Оферта не принята — submit запрещён",
+ nickname = "user",
+ email = "test@test.com",
+ isEmailValid = true,
+ password = "Pass123!",
+ confirmPassword = "Pass123!",
+ isPdAccepted = true,
+ isOfferAccepted = false,
+ expectedSubmitEnabled = false,
+ action = RegistrationAction.OfferAcceptedChanged(false)
+ )
+ ),
+ )
+ }
+ }
+
+ class ErrorStateArgumentsProvider : ArgumentsProvider {
+ override fun provideArguments(context: ExtensionContext?): Stream {
+ return Stream.of(
+ Arguments.of(
+ ErrorStateTestCase(
+ name = "Conflict → error_user_already_exists",
+ exception = RegistrationException(RegistrationError.Conflict),
+ expectedErrorRes = R.string.error_user_already_exists,
+ )
+ ),
+ Arguments.of(
+ ErrorStateTestCase(
+ name = "NotFound → error_resource_not_found",
+ exception = RegistrationException(RegistrationError.NotFound),
+ expectedErrorRes = R.string.error_resource_not_found,
+ )
+ ),
+ Arguments.of(
+ ErrorStateTestCase(
+ name = "UnknownError → login_unknown_error",
+ exception = RegistrationException(RegistrationError.UnknownError),
+ expectedErrorRes = R.string.login_unknown_error,
+ )
+ ),
+ )
+ }
+ }
+}
+
+private fun initialFormWith(
+ nickname: String = "",
+ nicknameError: TextOrResource? = null,
+ email: String = "",
+ emailError: TextOrResource? = null,
+ password: String = "",
+ passwordError: TextOrResource? = null,
+ confirmPassword: String = "",
+ confirmPasswordError: TextOrResource? = null,
+ isPdAccepted: Boolean = false,
+ isOfferAccepted: Boolean = false,
+ isMailingAccepted: Boolean = false,
+ isPasswordVisible: Boolean = false,
+ isConfirmPasswordVisible: Boolean = false,
+ isSubmitEnabled: Boolean = false,
+ isEmailTouched: Boolean = false,
+ isPasswordTouched: Boolean = false,
+ isConfirmPasswordTouched: Boolean = false,
+): RegistrationFormState = RegistrationFormState(
+ nickname = nickname,
+ nicknameError = nicknameError,
+ email = email,
+ emailError = emailError,
+ password = password,
+ passwordError = passwordError,
+ confirmPassword = confirmPassword,
+ confirmPasswordError = confirmPasswordError,
+ isPdAccepted = isPdAccepted,
+ isOfferAccepted = isOfferAccepted,
+ isMailingAccepted = isMailingAccepted,
+ isPasswordVisible = isPasswordVisible,
+ isConfirmPasswordVisible = isConfirmPasswordVisible,
+ isSubmitEnabled = isSubmitEnabled,
+ isEmailTouched = isEmailTouched,
+ isPasswordTouched = isPasswordTouched,
+ isConfirmPasswordTouched = isConfirmPasswordTouched,
+)
diff --git a/feature/forgot-password/.gitignore b/feature/forgot-password/.gitignore
deleted file mode 100644
index 42afabfd..00000000
--- a/feature/forgot-password/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-/build
\ No newline at end of file
diff --git a/feature/forgot-password/api/.gitignore b/feature/forgot-password/api/.gitignore
deleted file mode 100644
index 42afabfd..00000000
--- a/feature/forgot-password/api/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-/build
\ No newline at end of file
diff --git a/feature/forgot-password/api/build.gradle.kts b/feature/forgot-password/api/build.gradle.kts
deleted file mode 100644
index b4a39edf..00000000
--- a/feature/forgot-password/api/build.gradle.kts
+++ /dev/null
@@ -1,44 +0,0 @@
-plugins {
- alias(libs.plugins.android.library)
- alias(libs.plugins.kotlin.android)
-}
-
-android {
- namespace = "ru.yeahub.api"
- compileSdk {
- version = release(35)
- }
-
- defaultConfig {
- minSdk = 24
-
- testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
- consumerProguardFiles("consumer-rules.pro")
- }
-
- buildTypes {
- release {
- isMinifyEnabled = false
- proguardFiles(
- getDefaultProguardFile("proguard-android-optimize.txt"),
- "proguard-rules.pro"
- )
- }
- }
- compileOptions {
- sourceCompatibility = JavaVersion.VERSION_11
- targetCompatibility = JavaVersion.VERSION_11
- }
- kotlinOptions {
- jvmTarget = "11"
- }
-}
-
-dependencies {
- implementation(libs.androidx.core.ktx)
- implementation(libs.androidx.appcompat)
- implementation(libs.material)
- testImplementation(libs.junit)
- androidTestImplementation(libs.androidx.junit)
- androidTestImplementation(libs.androidx.espresso.core)
-}
\ No newline at end of file
diff --git a/feature/forgot-password/api/consumer-rules.pro b/feature/forgot-password/api/consumer-rules.pro
deleted file mode 100644
index e69de29b..00000000
diff --git a/feature/forgot-password/api/src/main/AndroidManifest.xml b/feature/forgot-password/api/src/main/AndroidManifest.xml
deleted file mode 100644
index a5918e68..00000000
--- a/feature/forgot-password/api/src/main/AndroidManifest.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
-
-
\ No newline at end of file
diff --git a/feature/forgot-password/build.gradle.kts b/feature/forgot-password/build.gradle.kts
deleted file mode 100644
index c6d924a4..00000000
--- a/feature/forgot-password/build.gradle.kts
+++ /dev/null
@@ -1,44 +0,0 @@
-plugins {
- alias(libs.plugins.android.library)
- alias(libs.plugins.kotlin.android)
-}
-
-android {
- namespace = "ru.yeahub.forgot_password"
- compileSdk {
- version = release(35)
- }
-
- defaultConfig {
- minSdk = 24
-
- testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
- consumerProguardFiles("consumer-rules.pro")
- }
-
- buildTypes {
- release {
- isMinifyEnabled = false
- proguardFiles(
- getDefaultProguardFile("proguard-android-optimize.txt"),
- "proguard-rules.pro"
- )
- }
- }
- compileOptions {
- sourceCompatibility = JavaVersion.VERSION_11
- targetCompatibility = JavaVersion.VERSION_11
- }
- kotlinOptions {
- jvmTarget = "11"
- }
-}
-
-dependencies {
- implementation(libs.androidx.core.ktx)
- implementation(libs.androidx.appcompat)
- implementation(libs.material)
- testImplementation(libs.junit)
- androidTestImplementation(libs.androidx.junit)
- androidTestImplementation(libs.androidx.espresso.core)
-}
\ No newline at end of file
diff --git a/feature/forgot-password/consumer-rules.pro b/feature/forgot-password/consumer-rules.pro
deleted file mode 100644
index e69de29b..00000000
diff --git a/feature/forgot-password/impl/.gitignore b/feature/forgot-password/impl/.gitignore
deleted file mode 100644
index 42afabfd..00000000
--- a/feature/forgot-password/impl/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-/build
\ No newline at end of file
diff --git a/feature/forgot-password/impl/build.gradle.kts b/feature/forgot-password/impl/build.gradle.kts
deleted file mode 100644
index 290ccdc7..00000000
--- a/feature/forgot-password/impl/build.gradle.kts
+++ /dev/null
@@ -1,61 +0,0 @@
-plugins {
- alias(libs.plugins.android.library)
- alias(libs.plugins.kotlin.android)
- alias(libs.plugins.kotlin.compose)
-}
-
-android {
- namespace = "ru.yeahub.impl"
- compileSdk = 35
-
- defaultConfig {
- minSdk = 24
-
- testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
- consumerProguardFiles("consumer-rules.pro")
- }
-
- buildTypes {
- release {
- isMinifyEnabled = false
- proguardFiles(
- getDefaultProguardFile("proguard-android-optimize.txt"),
- "proguard-rules.pro"
- )
- }
- }
- compileOptions {
- sourceCompatibility = JavaVersion.VERSION_11
- targetCompatibility = JavaVersion.VERSION_11
- }
- kotlinOptions {
- jvmTarget = "11"
- }
- buildFeatures {
- compose = true
- }
- composeOptions {
- kotlinCompilerExtensionVersion = "1.5.8"
- }
-}
-
-dependencies {
- implementation(libs.androidx.core.ktx)
- implementation(libs.androidx.appcompat)
- implementation(libs.material)
- implementation(libs.androidx.compose.runtime)
- implementation(libs.androidx.compose.ui)
- implementation(libs.androidx.compose.foundation)
- implementation(libs.androidx.compose.material3)
- implementation(libs.timber)
- implementation(project(":core:ui"))
-
- // Compose Preview dependencies
- implementation(libs.androidx.ui.tooling.preview)
- debugImplementation(libs.androidx.ui.tooling)
- debugImplementation(libs.androidx.ui.test.manifest)
-
- testImplementation(libs.junit)
- androidTestImplementation(libs.androidx.junit)
- androidTestImplementation(libs.androidx.espresso.core)
-}
\ No newline at end of file
diff --git a/feature/forgot-password/impl/consumer-rules.pro b/feature/forgot-password/impl/consumer-rules.pro
deleted file mode 100644
index e69de29b..00000000
diff --git a/feature/forgot-password/impl/src/main/AndroidManifest.xml b/feature/forgot-password/impl/src/main/AndroidManifest.xml
deleted file mode 100644
index a5918e68..00000000
--- a/feature/forgot-password/impl/src/main/AndroidManifest.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
-
-
\ No newline at end of file
diff --git a/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/data/dto/ForgotPasswordRequestDto.kt b/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/data/dto/ForgotPasswordRequestDto.kt
deleted file mode 100644
index 0517819a..00000000
--- a/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/data/dto/ForgotPasswordRequestDto.kt
+++ /dev/null
@@ -1,5 +0,0 @@
-package ru.yeahub.impl.data.dto
-
-data class ForgotPasswordRequestDto(
- val email: String
-)
\ No newline at end of file
diff --git a/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/data/dto/ForgotPasswordResponseDto.kt b/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/data/dto/ForgotPasswordResponseDto.kt
deleted file mode 100644
index cfc5132c..00000000
--- a/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/data/dto/ForgotPasswordResponseDto.kt
+++ /dev/null
@@ -1,6 +0,0 @@
-package ru.yeahub.impl.data.dto
-
-data class ForgotPasswordResponseDto(
- val ok: Boolean,
- val message: String
-)
\ No newline at end of file
diff --git a/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/data/mapper/ForgotPasswordMapper.kt b/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/data/mapper/ForgotPasswordMapper.kt
deleted file mode 100644
index ce20fabe..00000000
--- a/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/data/mapper/ForgotPasswordMapper.kt
+++ /dev/null
@@ -1,14 +0,0 @@
-package ru.yeahub.impl.data.mapper
-
-import ru.yeahub.impl.data.dto.ForgotPasswordResponseDto
-import ru.yeahub.impl.domain.ForgotPasswordResult
-
-class ForgotPasswordMapper {
- fun toDomain(responseDto: ForgotPasswordResponseDto): ForgotPasswordResult {
- return if (responseDto.ok) {
- ForgotPasswordResult.Success
- } else {
- ForgotPasswordResult.Error(responseDto.message)
- }
- }
-}
\ No newline at end of file
diff --git a/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/data/remote/AuthApi.kt b/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/data/remote/AuthApi.kt
deleted file mode 100644
index b673889e..00000000
--- a/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/data/remote/AuthApi.kt
+++ /dev/null
@@ -1,8 +0,0 @@
-package ru.yeahub.impl.data.remote
-
-import ru.yeahub.impl.data.dto.ForgotPasswordRequestDto
-import ru.yeahub.impl.data.dto.ForgotPasswordResponseDto
-
-interface AuthApi {
- suspend fun forgotPassword(request: ForgotPasswordRequestDto): ForgotPasswordResponseDto
-}
\ No newline at end of file
diff --git a/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/data/repository/ForgotPasswordRepositoryImpl.kt b/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/data/repository/ForgotPasswordRepositoryImpl.kt
deleted file mode 100644
index 118a9cae..00000000
--- a/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/data/repository/ForgotPasswordRepositoryImpl.kt
+++ /dev/null
@@ -1,48 +0,0 @@
-package ru.yeahub.impl.data.repository
-
-import kotlinx.coroutines.CancellationException
-import java.io.IOException
-import ru.yeahub.impl.data.dto.ForgotPasswordRequestDto
-import ru.yeahub.impl.data.mapper.ForgotPasswordMapper
-import ru.yeahub.impl.data.remote.AuthApi
-import ru.yeahub.impl.domain.ForgotPasswordRepository
-import ru.yeahub.impl.domain.ForgotPasswordResult
-import timber.log.Timber
-
-class ForgotPasswordRepositoryImpl(
- private val api: AuthApi,
- private val mapper: ForgotPasswordMapper
-) : ForgotPasswordRepository {
-
- @Suppress("TooGenericExceptionCaught")
- override suspend fun sendResetLink(email: String): ForgotPasswordResult {
- return try {
- val dto = api.forgotPassword(ForgotPasswordRequestDto(email))
- mapper.toDomain(dto)
- } catch (e: CancellationException) {
- throw e
- } catch (e: IOException) {
- Timber.e(
- "Network error sending reset link for email: $email," +
- "error: ${e.message}"
- )
- ForgotPasswordResult.Error(
- "Ошибка сети. Проверьте подключение к интернету."
- )
- } catch (e: IllegalArgumentException) {
- Timber.e(
- "Invalid data error sending reset link for email: $email, " +
- "error: ${e.message}"
- )
- ForgotPasswordResult.Error(
- "Некорректные данные. Проверьте введенный email."
- )
- } catch (e: Exception) {
- Timber.e(
- "Unexpected error sending reset link for email: $email," +
- "error: ${e.message}"
- )
- ForgotPasswordResult.Error("Неизвестная ошибка. Попробуйте позже.")
- }
- }
-}
\ No newline at end of file
diff --git a/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/di/ForgotPasswordFactory.kt b/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/di/ForgotPasswordFactory.kt
deleted file mode 100644
index f7202c10..00000000
--- a/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/di/ForgotPasswordFactory.kt
+++ /dev/null
@@ -1,23 +0,0 @@
-package ru.yeahub.impl.di
-
-import ru.yeahub.impl.data.mapper.ForgotPasswordMapper
-import ru.yeahub.impl.data.remote.AuthApi
-import ru.yeahub.impl.data.repository.ForgotPasswordRepositoryImpl
-import ru.yeahub.impl.domain.SendResetLinkUseCase
-import ru.yeahub.impl.presentation.ForgotPasswordViewModel
-import ru.yeahub.impl.presentation.mapper.EmailValidator
-import ru.yeahub.impl.presentation.mapper.ForgotPasswordScreenMapper
-
-object ForgotPasswordFactory {
-
- fun createViewModel(
- api: AuthApi,
- mapper: ForgotPasswordMapper,
- emailValidator: EmailValidator,
- ): ForgotPasswordViewModel {
- val forgotPasswordRepository = ForgotPasswordRepositoryImpl(api, mapper)
- val sendResetLinkUseCase = SendResetLinkUseCase(forgotPasswordRepository)
- val forgotPasswordScreenMapper = ForgotPasswordScreenMapper(emailValidator)
- return ForgotPasswordViewModel(forgotPasswordScreenMapper, sendResetLinkUseCase)
- }
-}
\ No newline at end of file
diff --git a/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/domain/ForgotPasswordRepository.kt b/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/domain/ForgotPasswordRepository.kt
deleted file mode 100644
index 07aef5e8..00000000
--- a/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/domain/ForgotPasswordRepository.kt
+++ /dev/null
@@ -1,6 +0,0 @@
-package ru.yeahub.impl.domain
-
-interface ForgotPasswordRepository {
-
- suspend fun sendResetLink(email: String): ForgotPasswordResult
-}
\ No newline at end of file
diff --git a/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/domain/ForgotPasswordResult.kt b/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/domain/ForgotPasswordResult.kt
deleted file mode 100644
index 4220d03e..00000000
--- a/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/domain/ForgotPasswordResult.kt
+++ /dev/null
@@ -1,6 +0,0 @@
-package ru.yeahub.impl.domain
-
-sealed interface ForgotPasswordResult {
- data object Success : ForgotPasswordResult
- data class Error(val message: String) : ForgotPasswordResult
-}
\ No newline at end of file
diff --git a/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/domain/SendResetLinkUseCase.kt b/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/domain/SendResetLinkUseCase.kt
deleted file mode 100644
index c141eb30..00000000
--- a/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/domain/SendResetLinkUseCase.kt
+++ /dev/null
@@ -1,15 +0,0 @@
-package ru.yeahub.impl.domain
-
-class SendResetLinkUseCase(
- private val repository: ForgotPasswordRepository
-) {
- suspend operator fun invoke(email: String): ForgotPasswordResult {
- val errorMessage = when {
- email.isBlank() -> "Введите email"
- !email.contains("@") -> "Некорректный email"
- else -> null
- }
- return errorMessage?.let { ForgotPasswordResult.Error(it) }
- ?: repository.sendResetLink(email.trim())
- }
-}
\ No newline at end of file
diff --git a/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/presentation/ForgotPasswordViewModel.kt b/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/presentation/ForgotPasswordViewModel.kt
deleted file mode 100644
index 346a31fc..00000000
--- a/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/presentation/ForgotPasswordViewModel.kt
+++ /dev/null
@@ -1,123 +0,0 @@
-package ru.yeahub.impl.presentation
-
-import androidx.lifecycle.ViewModel
-import androidx.lifecycle.viewModelScope
-import kotlinx.coroutines.flow.MutableSharedFlow
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.SharedFlow
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.asStateFlow
-import kotlinx.coroutines.flow.update
-import kotlinx.coroutines.launch
-import ru.yeahub.impl.domain.ForgotPasswordResult
-import ru.yeahub.impl.domain.SendResetLinkUseCase
-import ru.yeahub.impl.presentation.intents.ForgotPasswordCommand
-import ru.yeahub.impl.presentation.intents.ForgotPasswordEvent
-import ru.yeahub.impl.presentation.mapper.ForgotPasswordScreenMapper
-import ru.yeahub.impl.presentation.state.ForgotPasswordScreenState
-import ru.yeahub.impl.presentation.state.ForgotPasswordState
-
-class ForgotPasswordViewModel(
- private val forgotPasswordScreenMapper: ForgotPasswordScreenMapper,
- private val sendResetLinkUseCase: SendResetLinkUseCase
-) : ViewModel() {
-
- private val mutableState = MutableStateFlow(
- ForgotPasswordState(
- email = "",
- isLoading = false,
- error = null,
- emailValidationError = "",
- isSuccessDialogVisible = false,
- )
- )
-
- private val mutableUiState =
- MutableStateFlow(ForgotPasswordScreenState.Initial)
- val uiState: StateFlow = mutableUiState.asStateFlow()
-
- private val mutableCommands =
- MutableSharedFlow()
- val commands: SharedFlow = mutableCommands
-
- init {
- viewModelScope.launch {
- mutableState.collect { state ->
- mutableUiState.value = forgotPasswordScreenMapper.getScreenState(state)
- }
- }
- }
-
- fun handleEvents(event: ForgotPasswordEvent) {
- when (event) {
- is ForgotPasswordEvent.EmailChanged -> onEmailChanged(event.value)
- is ForgotPasswordEvent.SubmitClicked -> onSubmit()
- is ForgotPasswordEvent.BackClicked -> handleBackClick()
- }
- }
-
- private fun onEmailChanged(value: String) {
- mutableState.update { currentState ->
- val validationError = if (value.isNotBlank()) {
- forgotPasswordScreenMapper.validateEmail(value)
- } else {
- null
- }
- currentState.copy(
- email = value,
- emailValidationError = validationError,
- error = null
- )
- }
- }
-
- private fun onSubmit() {
- val currentState = mutableState.value
- val email = currentState.email.trim()
-
- if (!forgotPasswordScreenMapper.canSubmit(email)) {
- mutableState.update {
- it.copy(emailValidationError = forgotPasswordScreenMapper.validateEmail(email))
- }
- return
- }
-
- mutableState.update { it.copy(isLoading = true, error = null) }
-
- viewModelScope.launch {
- when (val result = sendResetLinkUseCase(email)) {
- is ForgotPasswordResult.Success -> {
- mutableState.update {
- it.copy(
- isLoading = false,
- isSuccessDialogVisible = true
- )
- }
- emitCommand(ForgotPasswordCommand.NavigateToCheckEmail)
- }
-
- is ForgotPasswordResult.Error -> {
- mutableState.update {
- it.copy(
- isLoading = false,
- error = result.message
- )
- }
- emitCommand(ForgotPasswordCommand.ShowSnackbar(result.message))
- }
- }
- }
- }
-
- private fun handleBackClick() {
- viewModelScope.launch {
- mutableCommands.emit(ForgotPasswordCommand.NavigateBack)
- }
- }
-
- private fun emitCommand(command: ForgotPasswordCommand) {
- viewModelScope.launch {
- mutableCommands.emit(command)
- }
- }
-}
\ No newline at end of file
diff --git a/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/presentation/intents/ForgotPasswordCommand.kt b/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/presentation/intents/ForgotPasswordCommand.kt
deleted file mode 100644
index 091724ec..00000000
--- a/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/presentation/intents/ForgotPasswordCommand.kt
+++ /dev/null
@@ -1,7 +0,0 @@
-package ru.yeahub.impl.presentation.intents
-
-sealed interface ForgotPasswordCommand {
- data class ShowSnackbar(val message: String) : ForgotPasswordCommand
- data object NavigateBack : ForgotPasswordCommand
- data object NavigateToCheckEmail : ForgotPasswordCommand
-}
\ No newline at end of file
diff --git a/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/presentation/intents/ForgotPasswordEvent.kt b/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/presentation/intents/ForgotPasswordEvent.kt
deleted file mode 100644
index 72324f87..00000000
--- a/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/presentation/intents/ForgotPasswordEvent.kt
+++ /dev/null
@@ -1,7 +0,0 @@
-package ru.yeahub.impl.presentation.intents
-
-sealed interface ForgotPasswordEvent {
- data class EmailChanged(val value: String) : ForgotPasswordEvent
- data object SubmitClicked : ForgotPasswordEvent
- data object BackClicked : ForgotPasswordEvent
-}
\ No newline at end of file
diff --git a/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/presentation/mapper/EmailValidator.kt b/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/presentation/mapper/EmailValidator.kt
deleted file mode 100644
index 19d9b3e7..00000000
--- a/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/presentation/mapper/EmailValidator.kt
+++ /dev/null
@@ -1,28 +0,0 @@
-package ru.yeahub.impl.presentation.mapper
-
-import android.util.Patterns
-
-sealed interface EmailValidationResult {
- data object Valid : EmailValidationResult
- data object Empty : EmailValidationResult
- data class Invalid(val errorMessage: String) : EmailValidationResult
-}
-
-class EmailValidator {
-
- fun validate(email: String): EmailValidationResult {
- val trimmedEmail = email.trim()
-
- return when {
- trimmedEmail.isBlank() -> EmailValidationResult.Empty
- !Patterns.EMAIL_ADDRESS.matcher(trimmedEmail).matches() -> {
- EmailValidationResult.Invalid("Введите корректный email")
- }
- else -> EmailValidationResult.Valid
- }
- }
-
- fun isValid(email: String): Boolean {
- return validate(email) is EmailValidationResult.Valid
- }
-}
\ No newline at end of file
diff --git a/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/presentation/mapper/ForgotPasswordScreenMapper.kt b/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/presentation/mapper/ForgotPasswordScreenMapper.kt
deleted file mode 100644
index 8dd1cbac..00000000
--- a/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/presentation/mapper/ForgotPasswordScreenMapper.kt
+++ /dev/null
@@ -1,41 +0,0 @@
-package ru.yeahub.impl.presentation.mapper
-
-import ru.yeahub.impl.presentation.state.ForgotPasswordScreenState
-import ru.yeahub.impl.presentation.state.ForgotPasswordState
-
-class ForgotPasswordScreenMapper(
- private val emailValidator: EmailValidator,
-) {
- fun getScreenState(state: ForgotPasswordState): ForgotPasswordScreenState {
- return when {
- state.error != null -> {
- ForgotPasswordScreenState.Error(message = state.error)
- }
-
- state.email.isBlank() && state.emailValidationError == null -> {
- ForgotPasswordScreenState.Initial
- }
-
- else -> {
- ForgotPasswordScreenState.Content(
- email = state.email,
- isLoading = state.isLoading,
- emailError = state.emailValidationError,
- isSent = state.isSuccessDialogVisible,
- )
- }
- }
- }
-
- fun validateEmail(email: String): String? {
- return when (val result = emailValidator.validate(email)) {
- is EmailValidationResult.Valid -> null
- is EmailValidationResult.Empty -> null
- is EmailValidationResult.Invalid -> result.errorMessage
- }
- }
-
- fun canSubmit(email: String): Boolean {
- return emailValidator.isValid(email)
- }
-}
diff --git a/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/presentation/state/ForgotPasswordScreenState.kt b/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/presentation/state/ForgotPasswordScreenState.kt
deleted file mode 100644
index 11735ce2..00000000
--- a/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/presentation/state/ForgotPasswordScreenState.kt
+++ /dev/null
@@ -1,14 +0,0 @@
-package ru.yeahub.impl.presentation.state
-
-sealed class ForgotPasswordScreenState {
- data object Initial : ForgotPasswordScreenState()
- data class Content(
- val email: String,
- val isLoading: Boolean,
- val emailError: String?,
- val isSent: Boolean,
- ) : ForgotPasswordScreenState() {
- val isEmailValid: Boolean = emailError == null && email.isNotBlank()
- }
- data class Error(val message: String?) : ForgotPasswordScreenState()
-}
\ No newline at end of file
diff --git a/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/presentation/state/ForgotPasswordState.kt b/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/presentation/state/ForgotPasswordState.kt
deleted file mode 100644
index 78486f01..00000000
--- a/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/presentation/state/ForgotPasswordState.kt
+++ /dev/null
@@ -1,9 +0,0 @@
-package ru.yeahub.impl.presentation.state
-
-data class ForgotPasswordState(
- val email: String,
- val isLoading: Boolean,
- val error: String?,
- val emailValidationError: String?,
- val isSuccessDialogVisible: Boolean,
-)
\ No newline at end of file
diff --git a/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/ui/ForgotPasswordModalWindow.kt b/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/ui/ForgotPasswordModalWindow.kt
deleted file mode 100644
index 4fbefe02..00000000
--- a/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/ui/ForgotPasswordModalWindow.kt
+++ /dev/null
@@ -1,173 +0,0 @@
-@file:OptIn(ExperimentalMaterial3Api::class)
-
-package ru.yeahub.impl.ui
-
-import androidx.compose.foundation.BorderStroke
-import androidx.compose.foundation.Image
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.IconButton
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Surface
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.clip
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.res.painterResource
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.style.TextAlign
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.window.Dialog
-import kotlinx.coroutines.delay
-import ru.yeahub.core_ui.example.staticPreview.StaticPreview
-import ru.yeahub.core_ui.theme.Theme
-import ru.yeahub.impl.R
-
-@Composable
-fun InstructionsSentDialog(
- onDismiss: () -> Unit,
- onResend: () -> Unit,
- modifier: Modifier = Modifier,
-) {
- Dialog(onDismissRequest = onDismiss) {
- InstructionsSentDialogContent(
- onDismiss = onDismiss,
- onResend = onResend,
- modifier = modifier
- )
- }
-}
-
-@Composable
-private fun InstructionsSentDialogContent(
- onDismiss: () -> Unit,
- onResend: () -> Unit,
- modifier: Modifier = Modifier,
-) {
- val color = Theme.colors.purple700
- val shape = RoundedCornerShape(16.dp)
-
- var secondsLeft by remember { mutableStateOf(60) }
-
- LaunchedEffect(Unit) {
- while (secondsLeft > 0) {
- delay(1_000)
- secondsLeft--
- }
- }
- Surface(
- modifier = modifier
- .fillMaxWidth()
- .padding(horizontal = 24.dp),
- shape = shape,
- color = Color.White,
- tonalElevation = 0.dp,
- shadowElevation = 0.dp,
- border = BorderStroke(1.dp, color),
- ) {
- Box(
- modifier = Modifier
- .clip(shape)
- .padding(16.dp),
- contentAlignment = Alignment.TopEnd
- ) {
- IconButton(
- onClick = onDismiss,
- modifier = Modifier
- .size(32.dp)
- ) {
- Image(
- painter = painterResource(id = R.drawable.close),
- contentDescription = stringResource(R.string.close),
- modifier = Modifier.size(24.dp)
- )
- }
-
- Column(
- modifier = Modifier
- .fillMaxWidth(),
- horizontalAlignment = Alignment.CenterHorizontally
- ) {
- Image(
- painter = painterResource(id = R.drawable.letter),
- contentDescription = stringResource(R.string.letter),
- modifier = Modifier
- .size(116.dp)
- .padding(vertical = 2.dp)
- )
-
- Text(
- text = stringResource(R.string.instructions_letter),
- style = Theme.typography.body5Accent,
- textAlign = TextAlign.Center
- )
-
- Spacer(Modifier.height(6.dp))
-
- Text(
- text = stringResource(R.string.instruction),
- style = Theme.typography.body2,
- color = Theme.colors.black900,
- textAlign = TextAlign.Center,
- lineHeight = MaterialTheme.typography.bodyMedium.lineHeight
- )
-
- Spacer(Modifier.height(14.dp))
-
- if (secondsLeft > 0) {
- Text(
- text = stringResource(R.string.resend, secondsLeft),
- color = Theme.colors.black300,
- style = Theme.typography.body2
- )
- } else {
- Text(
- text = stringResource(R.string.resend_button),
- color = color,
- style = Theme.typography.body2,
- modifier = Modifier
- .clip(RoundedCornerShape(10.dp))
- .clickable(onClick = onResend)
- .padding(horizontal = 12.dp, vertical = 10.dp)
- )
- }
-
- Spacer(Modifier.height(8.dp))
-
- Text(
- text = stringResource(R.string.ok),
- color = color,
- style = Theme.typography.body2,
- modifier = Modifier
- .clip(RoundedCornerShape(10.dp))
- .clickable(onClick = onDismiss)
- .padding(horizontal = 12.dp, vertical = 4.dp)
- )
- }
- }
- }
-}
-
-@StaticPreview
-@Composable
-fun InstructionsSentDialogPreview() {
- InstructionsSentDialogContent(
- onDismiss = {},
- onResend = {}
- )
-}
diff --git a/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/ui/ForgotPasswordRoute.kt b/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/ui/ForgotPasswordRoute.kt
deleted file mode 100644
index 7264ad8e..00000000
--- a/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/ui/ForgotPasswordRoute.kt
+++ /dev/null
@@ -1,33 +0,0 @@
-package ru.yeahub.impl.ui
-
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.collectAsState
-import androidx.compose.runtime.getValue
-import ru.yeahub.impl.presentation.ForgotPasswordViewModel
-import ru.yeahub.impl.presentation.intents.ForgotPasswordCommand
-
-@Composable
-fun ForgotPasswordRoute(
- viewModel: ForgotPasswordViewModel,
- onBack: () -> Unit,
- onCheckEmail: () -> Unit,
- showSnackbar: suspend (String) -> Unit
-) {
- val state by viewModel.uiState.collectAsState()
-
- LaunchedEffect(Unit) {
- viewModel.commands.collect { command ->
- when (command) {
- is ForgotPasswordCommand.NavigateBack -> onBack()
- is ForgotPasswordCommand.NavigateToCheckEmail -> onCheckEmail()
- is ForgotPasswordCommand.ShowSnackbar -> showSnackbar(command.message)
- }
- }
- }
-
- ForgotPasswordScreen(
- state = state,
- onEvent = viewModel::handleEvents
- )
-}
\ No newline at end of file
diff --git a/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/ui/ForgotPasswordScreen.kt b/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/ui/ForgotPasswordScreen.kt
deleted file mode 100644
index e8e55718..00000000
--- a/feature/forgot-password/impl/src/main/java/ru/yeahub/impl/ui/ForgotPasswordScreen.kt
+++ /dev/null
@@ -1,173 +0,0 @@
-@file:OptIn(ExperimentalMaterial3Api::class)
-
-package ru.yeahub.impl.ui
-
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.padding
-import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.tooling.preview.Preview
-import androidx.compose.ui.unit.dp
-import ru.yeahub.core_ui.component.PrimaryButton
-import ru.yeahub.core_ui.component.YeahubButtonColors
-import ru.yeahub.core_ui.component.textInput.DefaultTextField
-import ru.yeahub.core_ui.component.textInput.TextInputColorsDefaults
-import ru.yeahub.core_ui.theme.Theme
-import ru.yeahub.impl.R
-import ru.yeahub.impl.presentation.intents.ForgotPasswordEvent
-import ru.yeahub.impl.presentation.state.ForgotPasswordScreenState
-
-@Composable
-fun ForgotPasswordScreen(
- modifier: Modifier = Modifier,
- state: ForgotPasswordScreenState,
- onEvent: (ForgotPasswordEvent) -> Unit
-) {
- Column(
- modifier = modifier
- .fillMaxSize()
- .padding(horizontal = 16.dp)
- .padding(top = 32.dp)
- ) {
- Text(
- text = stringResource(R.string.title_forgot_password),
- style = Theme.typography.head5
- )
-
- Spacer(Modifier.height(6.dp))
-
- Text(
- text = stringResource(R.string.enter_email_instruction),
- style = Theme.typography.body2Accent,
- color = Theme.colors.black500
- )
-
- Spacer(Modifier.height(96.dp))
-
- Text(
- text = stringResource(R.string.email),
- style = Theme.typography.body3Accent
- )
-
- Spacer(Modifier.height(6.dp))
-
- val email = when (state) {
- is ForgotPasswordScreenState.Content -> state.email
- else -> ""
- }
- val isLoading = when (state) {
- is ForgotPasswordScreenState.Content -> state.isLoading
- else -> false
- }
- val emailError = when (state) {
- is ForgotPasswordScreenState.Content -> state.emailError
- else -> null
- }
- val isEmailValid = when (state) {
- is ForgotPasswordScreenState.Content -> state.isEmailValid
- else -> false
- }
-
- DefaultTextField(
- value = email,
- onValueChange = { onEvent(ForgotPasswordEvent.EmailChanged(it)) },
- label = stringResource(R.string.enter_email),
- isEnabled = !isLoading,
- isError = emailError != null,
- showLeadingIcon = false,
- onExpandedChange = { },
- modifier = Modifier
- .fillMaxWidth()
- .height(52.dp),
- colors = TextInputColorsDefaults.defaultColors()
- )
-
- if (emailError != null) {
- Spacer(Modifier.height(4.dp))
- Text(
- text = emailError,
- style = Theme.typography.body3,
- color = Theme.colors.red500
- )
- }
-
- Spacer(Modifier.height(20.dp))
-
- PrimaryButton(
- onClick = { onEvent(ForgotPasswordEvent.SubmitClicked) },
- enabled = isEmailValid && !isLoading,
- modifier = Modifier
- .fillMaxWidth()
- .height(48.dp),
- colors = YeahubButtonColors(
- contentColor = Theme.colors.white900,
- containerColor = Theme.colors.purple700,
- disabledContentColor = Theme.colors.black200,
- disabledContainerColor = Theme.colors.black50
- )
- ) {
- Text(
- text = stringResource(R.string.send_button),
- style = Theme.typography.body4
- )
- }
- }
-}
-
-@Preview(showBackground = true, name = "Initial State")
-@Composable
-fun ForgotPasswordScreenPreview_Initial() {
- ForgotPasswordScreen(
- state = ForgotPasswordScreenState.Initial,
- onEvent = {}
- )
-}
-
-@Preview(showBackground = true, name = "Valid Email")
-@Composable
-fun ForgotPasswordScreenPreview_Valid() {
- ForgotPasswordScreen(
- state = ForgotPasswordScreenState.Content(
- email = "user@example.com",
- isLoading = false,
- emailError = null,
- isSent = false
- ),
- onEvent = {}
- )
-}
-
-@Preview(showBackground = true, name = "Invalid Email")
-@Composable
-fun ForgotPasswordScreenPreview_Invalid() {
- ForgotPasswordScreen(
- state = ForgotPasswordScreenState.Content(
- email = "invalid-email",
- isLoading = false,
- emailError = "Введите корректный email",
- isSent = false
- ),
- onEvent = {}
- )
-}
-
-@Preview(showBackground = true, name = "Loading")
-@Composable
-fun ForgotPasswordScreenPreview_Loading() {
- ForgotPasswordScreen(
- state = ForgotPasswordScreenState.Content(
- email = "user@example.com",
- isLoading = true,
- emailError = null,
- isSent = false
- ),
- onEvent = {}
- )
-}
diff --git a/feature/forgot-password/impl/src/main/res/drawable/close.jpg b/feature/forgot-password/impl/src/main/res/drawable/close.jpg
deleted file mode 100644
index 4de6c081..00000000
Binary files a/feature/forgot-password/impl/src/main/res/drawable/close.jpg and /dev/null differ
diff --git a/feature/forgot-password/impl/src/main/res/drawable/letter.jpg b/feature/forgot-password/impl/src/main/res/drawable/letter.jpg
deleted file mode 100644
index 4b336363..00000000
Binary files a/feature/forgot-password/impl/src/main/res/drawable/letter.jpg and /dev/null differ
diff --git a/feature/forgot-password/impl/src/main/res/values/strings.xml b/feature/forgot-password/impl/src/main/res/values/strings.xml
deleted file mode 100644
index 6dfa844e..00000000
--- a/feature/forgot-password/impl/src/main/res/values/strings.xml
+++ /dev/null
@@ -1,16 +0,0 @@
-
-
- Close
- Letter
- Мы отправили письмо\nс инструкциями
- Если вы не получили письмо\nс инструкциями, проверьте, пожалуйста,\nпапку «Спам» или попробуйте отправить\nзапрос еще раз
- Отправить повторно через %1$d c
- Отправить повторно
- Хорошо
- Забыли пароль?
- Для восстановления пароля введите адрес\nэл.почты, на который вы регистрировались.\nМы отправим письмо для восстановления пароля
- Электронная почта
- Введите электронную почту
- Отправить
- Не удалось отправить письмо
-
\ No newline at end of file
diff --git a/feature/forgot-password/src/main/AndroidManifest.xml b/feature/forgot-password/src/main/AndroidManifest.xml
deleted file mode 100644
index a5918e68..00000000
--- a/feature/forgot-password/src/main/AndroidManifest.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
-
-
\ No newline at end of file
diff --git a/settings.gradle.kts b/settings.gradle.kts
index a8d55eb7..ecf16c5a 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -64,7 +64,4 @@ include(":feature:authentication:impl")
include(":feature:enter-and-registration")
include(":feature:enter-and-registration:api")
include(":feature:enter-and-registration:impl")
-//include(":feature:forgot-password")
-//include(":feature:forgot-password:api")
-//include(":feature:forgot-password:impl")