diff --git a/.idea/gradle.xml b/.idea/gradle.xml index d6d139c0..7a53b8e1 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -21,6 +21,9 @@ diff --git a/feature/authentication/api/.gitignore b/feature/authentication/api/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/feature/authentication/api/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/feature/authentication/api/build.gradle.kts b/feature/authentication/api/build.gradle.kts new file mode 100644 index 00000000..156a49db --- /dev/null +++ b/feature/authentication/api/build.gradle.kts @@ -0,0 +1,40 @@ +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) +} + +android { + namespace = "ru.yeahub.authentication.api" + compileSdk = 35 + + defaultConfig { + minSdk = 24 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + kotlinOptions { + jvmTarget = "11" + } +} + +dependencies { + implementation(libs.androidx.core.ktx) + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.junit) + androidTestImplementation(libs.androidx.espresso.core) +} \ No newline at end of file diff --git a/feature/authentication/api/consumer-rules.pro b/feature/authentication/api/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/feature/authentication/api/proguard-rules.pro b/feature/authentication/api/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/feature/authentication/api/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/feature/authentication/api/src/main/AndroidManifest.xml b/feature/authentication/api/src/main/AndroidManifest.xml new file mode 100644 index 00000000..a5918e68 --- /dev/null +++ b/feature/authentication/api/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/feature/authentication/impl/.gitignore b/feature/authentication/impl/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/feature/authentication/impl/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/feature/authentication/impl/build.gradle.kts b/feature/authentication/impl/build.gradle.kts new file mode 100644 index 00000000..2759ccdb --- /dev/null +++ b/feature/authentication/impl/build.gradle.kts @@ -0,0 +1,71 @@ +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.compose) +} + +android { + namespace = "ru.yeahub.authentication.impl" + compileSdk = 35 + + defaultConfig { + minSdk = 24 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + buildFeatures { + compose = true + } + testOptions { + unitTests.all { + it.useJUnitPlatform() + it.jvmArgs("-XX:+EnableDynamicAgentLoading") + } + } + kotlinOptions { + jvmTarget = "11" + } +} + +dependencies { + implementation(libs.androidx.compose.material.icons.extended) + implementation(project(":core:ui")) + implementation(project(":core:utils")) + implementation(project(":core:network-api")) + implementation(project(":core:navigation-api")) + implementation(libs.timber) + implementation(libs.androidx.core.ktx) + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.androidx.lifecycle.viewmodel.ktx) + implementation(libs.androidx.ui) + implementation(libs.androidx.ui.graphics) + implementation(libs.androidx.material3) + implementation(libs.androidx.runtime.android) + implementation(libs.androidx.ui.tooling.preview) + implementation(libs.koin.core) + implementation(libs.koin.android) + implementation(libs.koin.compose) + implementation(libs.compose.shimmer) + implementation(libs.androidx.navigation.compose) + debugImplementation(libs.androidx.ui.tooling) + debugImplementation(libs.androidx.ui.test.manifest) + testImplementation(libs.junit.jupiter) + testImplementation(platform(libs.junit.bom)) + testRuntimeOnly(libs.junit.platform.launcher) + testImplementation(libs.mockk) +} \ No newline at end of file diff --git a/feature/authentication/impl/consumer-rules.pro b/feature/authentication/impl/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/feature/authentication/impl/proguard-rules.pro b/feature/authentication/impl/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/feature/authentication/impl/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/feature/authentication/impl/src/main/AndroidManifest.xml b/feature/authentication/impl/src/main/AndroidManifest.xml new file mode 100644 index 00000000..a5918e68 --- /dev/null +++ b/feature/authentication/impl/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/registration/presentation/PasswordValidation.kt b/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/registration/presentation/PasswordValidation.kt new file mode 100644 index 00000000..2109da95 --- /dev/null +++ b/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/registration/presentation/PasswordValidation.kt @@ -0,0 +1,10 @@ +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 new file mode 100644 index 00000000..ad3652b9 --- /dev/null +++ b/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/registration/presentation/RegistrationScreen.kt @@ -0,0 +1,320 @@ +package ru.yeahub.authentication.impl.registration.presentation + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.ClickableText +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Visibility +import androidx.compose.material.icons.filled.VisibilityOff +import androidx.compose.material3.Checkbox +import androidx.compose.material3.CheckboxDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.OutlinedTextFieldDefaults +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.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 ru.yeahub.authentication.impl.R +import ru.yeahub.core_ui.component.PrimaryButton +import ru.yeahub.core_ui.theme.Theme + +@Composable +fun RegistrationScreen( + state: RegistrationUiState, + onAction: (RegistrationAction) -> Unit, + onOpenPdPolicy: () -> Unit, + onOpenOffer: () -> Unit +) { + val linkColor = MaterialTheme.colorScheme.primary + + Scaffold { paddings -> + Column( + modifier = Modifier + .padding(paddings) + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(horizontal = 16.dp, vertical = 24.dp), + verticalArrangement = Arrangement.spacedBy(14.dp) + ) { + Text( + text = stringResource(R.string.registration_title), + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.SemiBold + ) + + FormTextField( + title = stringResource(R.string.nickname_title), + placeholder = stringResource(R.string.nickname_placeholder), + value = state.nickname, + onValueChange = { onAction(RegistrationAction.NicknameChanged(it)) }, + keyboardType = KeyboardType.Ascii + ) + + FormTextField( + title = stringResource(R.string.email_title), + placeholder = stringResource(R.string.email_placeholder), + value = state.email, + onValueChange = { onAction(RegistrationAction.EmailChanged(it)) }, + keyboardType = KeyboardType.Email + ) + + FormPasswordField( + title = stringResource(R.string.password_title), + placeholder = stringResource(R.string.password_placeholder), + value = state.password, + isVisible = state.isPasswordVisible, + onValueChange = { onAction(RegistrationAction.PasswordChanged(it)) }, + onToggleVisibility = { onAction(RegistrationAction.TogglePasswordVisible) } + ) + + FormPasswordField( + title = stringResource(R.string.confirm_password_title), + placeholder = stringResource(R.string.password_placeholder), + value = state.confirmPassword, + isVisible = state.isConfirmPasswordVisible, + onValueChange = { onAction(RegistrationAction.ConfirmPasswordChanged(it)) }, + onToggleVisibility = { onAction(RegistrationAction.ToggleConfirmPasswordVisible) } + ) + + ConsentRow( + checked = state.isPdAccepted, + onCheckedChange = { onAction(RegistrationAction.PdAcceptedChanged(it)) }, + text = pdConsentText(linkColor), + onLinkClicked = { tag -> + if (tag == "pd") onOpenPdPolicy() + } + ) + + ConsentRow( + checked = state.isOfferAccepted, + onCheckedChange = { onAction(RegistrationAction.OfferAcceptedChanged(it)) }, + text = offerConsentText(linkColor), + onLinkClicked = { tag -> + if (tag == "offer") onOpenOffer() + } + ) + + ConsentRow( + checked = state.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 = state.isSubmitEnabled, + 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)) + pushStringAnnotation(tag = "pd", annotation = "pd") + pushStyle(SpanStyle(color = linkColor)) + append(stringResource(R.string.pd_consent_link)) + pop() + pop() + append(stringResource(R.string.pd_consent_suffix)) +} + +@Composable +private fun offerConsentText(linkColor: Color): AnnotatedString = buildAnnotatedString { + append(stringResource(R.string.offer_consent_prefix)) + pushStringAnnotation(tag = "offer", annotation = "offer") + pushStyle(SpanStyle(color = linkColor)) + append(stringResource(R.string.offer_consent_link)) + pop() + pop() +} + +@Composable +private fun ConsentRow( + checked: Boolean, + onCheckedChange: (Boolean) -> Unit, + text: AnnotatedString, + 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 + ) + ) + Spacer(Modifier.width(8.dp)) + ClickableText( + text = text, + style = MaterialTheme.typography.bodySmall.copy( + color = MaterialTheme.colorScheme.onSurface, + textDecoration = null + ), + onClick = { offset -> + text.getStringAnnotations(start = offset, end = offset) + .firstOrNull() + ?.let { onLinkClicked(it.tag) } + ?: onCheckedChange(!checked) + } + ) + } +} + +@Preview(showBackground = true) +@Composable +fun RegistrationScreenPreview() { + MaterialTheme { + RegistrationScreen( + state = RegistrationUiState( + nickname = "admin", + email = "admin@mail.ru", + password = "1234", + confirmPassword = "1234", + isPasswordVisible = true, + isConfirmPasswordVisible = true, + isPdAccepted = true, + isOfferAccepted = true, + isMailingAccepted = false, + isSubmitEnabled = true + ), + onAction = {}, + onOpenPdPolicy = {}, + onOpenOffer = {} + ) + } +} \ No newline at end of file diff --git a/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/registration/presentation/RegistrationUiState.kt b/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/registration/presentation/RegistrationUiState.kt new file mode 100644 index 00000000..15e96754 --- /dev/null +++ b/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/registration/presentation/RegistrationUiState.kt @@ -0,0 +1,27 @@ +package ru.yeahub.authentication.impl.registration.presentation + +data class RegistrationUiState( + val nickname: String, + val email: String, + val password: String, + val confirmPassword: String, + val isPdAccepted: Boolean, + val isOfferAccepted: Boolean, + val isMailingAccepted: Boolean, + val isPasswordVisible: Boolean, + val isConfirmPasswordVisible: Boolean, + val isSubmitEnabled: Boolean +) + +sealed class RegistrationAction { + data class NicknameChanged(val value: String) : RegistrationAction() + data class EmailChanged(val value: String) : RegistrationAction() + data class PasswordChanged(val value: String) : RegistrationAction() + data class ConfirmPasswordChanged(val value: String) : RegistrationAction() + data class PdAcceptedChanged(val value: Boolean) : RegistrationAction() + data class OfferAcceptedChanged(val value: Boolean) : RegistrationAction() + data class MailingAcceptedChanged(val value: Boolean) : RegistrationAction() + data object TogglePasswordVisible : RegistrationAction() + data object ToggleConfirmPasswordVisible : RegistrationAction() + data object SubmitClicked : RegistrationAction() +} \ No newline at end of file diff --git a/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/registration/presentation/RegistrationViewModel.kt b/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/registration/presentation/RegistrationViewModel.kt new file mode 100644 index 00000000..0af91624 --- /dev/null +++ b/feature/authentication/impl/src/main/java/ru/yeahub/authentication/impl/registration/presentation/RegistrationViewModel.kt @@ -0,0 +1,80 @@ +package ru.yeahub.authentication.impl.registration.presentation + +import android.util.Patterns +import androidx.lifecycle.ViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update + +class RegistrationViewModel : ViewModel() { + + private val _state = MutableStateFlow( + RegistrationUiState( + nickname = "", + email = "", + password = "", + confirmPassword = "", + isPdAccepted = false, + isOfferAccepted = false, + isMailingAccepted = false, + isPasswordVisible = false, + isConfirmPasswordVisible = false, + isSubmitEnabled = false + ) + ) + val state: StateFlow = _state + + fun onAction(action: RegistrationAction) { + when (action) { + is RegistrationAction.ConfirmPasswordChanged -> { + _state.update { it.copy(confirmPassword = action.value).revalidate() } + } + + is RegistrationAction.EmailChanged -> { + _state.update { it.copy(email = action.value).revalidate() } + } + + is RegistrationAction.MailingAcceptedChanged -> { + _state.update { it.copy(isMailingAccepted = action.value).revalidate() } + } + + is RegistrationAction.NicknameChanged -> { + _state.update { it.copy(nickname = action.value).revalidate() } + } + + is RegistrationAction.OfferAcceptedChanged -> { + _state.update { it.copy(isOfferAccepted = action.value).revalidate() } + } + + is RegistrationAction.PasswordChanged -> { + _state.update { it.copy(password = action.value).revalidate() } + } + + is RegistrationAction.PdAcceptedChanged -> { + _state.update { it.copy(isPdAccepted = action.value).revalidate() } + } + + RegistrationAction.SubmitClicked -> {} + + RegistrationAction.ToggleConfirmPasswordVisible -> { + _state.update { it.copy(isConfirmPasswordVisible = !it.isConfirmPasswordVisible) } + } + + RegistrationAction.TogglePasswordVisible -> { + _state.update { it.copy(isPasswordVisible = !it.isPasswordVisible) } + } + } + } + + private fun RegistrationUiState.revalidate(): RegistrationUiState { + val nicknameOk = nickname.trim().isNotEmpty() + val emailOk = Patterns.EMAIL_ADDRESS.matcher(email.trim()).matches() + val passOk = isPasswordValid(password) + val confirmOk = password == confirmPassword && confirmPassword.isNotEmpty() + val requiredConsentsOk = isPdAccepted && isOfferAccepted + + return copy( + isSubmitEnabled = nicknameOk && emailOk && passOk && confirmOk && requiredConsentsOk + ) + } +} \ No newline at end of file diff --git a/feature/authentication/impl/src/main/res/values/strings.xml b/feature/authentication/impl/src/main/res/values/strings.xml new file mode 100644 index 00000000..85249f30 --- /dev/null +++ b/feature/authentication/impl/src/main/res/values/strings.xml @@ -0,0 +1,18 @@ + + + Регистрация + Никнейм + Введите никнейм + Электронная почта + Введите электронную почту + Пароль + Введите пароль + Подтвердить пароль + Зарегистрироваться + Даю согласие на получение рекламных и информационных рассылок + "Даю " + Согласие на обработку персональных данных + , в соответствии с Политикой в отношении ПД + "Подтверждаю что ознакомился(-ась) с " + Договором-офертой + \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 00f8973c..25098835 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -33,6 +33,7 @@ runtime = "1.9.0" immutableCollections = "0.4.0" [libraries] +androidx-compose-material-icons-extended = { module = "androidx.compose.material:material-icons-extended" } androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } androidx-lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "lifecycleRuntimeCompose" } diff --git a/settings.gradle.kts b/settings.gradle.kts index 616cc767..261ce0c2 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -57,4 +57,5 @@ include(":feature:public-collections:api") include(":feature:questions-or-collections") include(":feature:questions-or-collections:api") include(":feature:questions-or-collections:impl") - +include(":feature:authentication:api") +include(":feature:authentication:impl")