diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index ce45158d..b85b1136 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -151,7 +151,7 @@ dependencies {
// Coil for Compose
implementation(libs.coil.compose)
- androidTestImplementation(libs.compose.ui.test.junit4)
+// androidTestImplementation(libs.compose.ui.test.junit4)
debugImplementation(libs.compose.ui.test.manifest)
debugImplementation(libs.compose.ui.tooling)
diff --git a/core/data/src/main/kotlin/com/meloda/app/fast/data/api/oauth/OAuthRepositoryImpl.kt b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/oauth/OAuthRepositoryImpl.kt
index eee89074..874f8ab3 100644
--- a/core/data/src/main/kotlin/com/meloda/app/fast/data/api/oauth/OAuthRepositoryImpl.kt
+++ b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/oauth/OAuthRepositoryImpl.kt
@@ -40,12 +40,15 @@ class OAuthRepositoryImpl(
requireNotNull(result.error)
}
- else -> throw IllegalStateException("Unknown result")
+ is ApiResult.Failure.ApiFailure -> TODO()
-// is ApiResult.Failure.ApiFailure -> TODO()
-// is ApiResult.Failure.HttpFailure -> TODO()
-// is ApiResult.Failure.NetworkFailure -> TODO()
-// is ApiResult.Failure.UnknownFailure -> TODO()
+ is ApiResult.Failure.NetworkFailure -> {
+ // TODO: 13/07/2024, Danil Nikolaev: implement showing network error
+ TODO()
+ }
+ is ApiResult.Failure.UnknownFailure -> TODO()
+
+ else -> throw IllegalStateException("Unknown result")
}
}
}
diff --git a/core/designsystem/src/main/res/values/strings.xml b/core/designsystem/src/main/res/values/strings.xml
index 92d3bd9b..8cf1c8a2 100644
--- a/core/designsystem/src/main/res/values/strings.xml
+++ b/core/designsystem/src/main/res/values/strings.xml
@@ -104,6 +104,7 @@
Playlist
Podcast
Narrative
+ Article
Uploading file
Uploading photo
diff --git a/core/model/build.gradle.kts b/core/model/build.gradle.kts
index 0cfeb77b..5d0c71c3 100644
--- a/core/model/build.gradle.kts
+++ b/core/model/build.gradle.kts
@@ -32,6 +32,9 @@ dependencies {
implementation(libs.moshi.kotlin)
ksp(libs.moshi.kotlin.codegen)
+ implementation(platform(libs.compose.bom))
+ implementation(libs.bundles.compose)
+
implementation(libs.room.ktx)
implementation(libs.room.runtime)
ksp(libs.room.compiler)
diff --git a/core/model/src/main/kotlin/com/meloda/app/fast/model/BaseError.kt b/core/model/src/main/kotlin/com/meloda/app/fast/model/BaseError.kt
index 382b7dea..1aa057b3 100644
--- a/core/model/src/main/kotlin/com/meloda/app/fast/model/BaseError.kt
+++ b/core/model/src/main/kotlin/com/meloda/app/fast/model/BaseError.kt
@@ -1,5 +1,9 @@
package com.meloda.app.fast.model
+import androidx.compose.runtime.Immutable
+
+@Immutable
sealed class BaseError {
+
data object SessionExpired : BaseError()
}
diff --git a/core/model/src/main/kotlin/com/meloda/app/fast/model/api/data/AttachmentType.kt b/core/model/src/main/kotlin/com/meloda/app/fast/model/api/data/AttachmentType.kt
index 5ffc19d7..00677feb 100644
--- a/core/model/src/main/kotlin/com/meloda/app/fast/model/api/data/AttachmentType.kt
+++ b/core/model/src/main/kotlin/com/meloda/app/fast/model/api/data/AttachmentType.kt
@@ -26,7 +26,8 @@ enum class AttachmentType(var value: String) {
ARTIST("artist"),
AUDIO_PLAYLIST("audio_playlist"),
PODCAST("podcast"),
- NARRATIVE("narrative");
+ NARRATIVE("narrative"),
+ ARTICLE("article");
fun isMultiple(): Boolean = this in listOf(PHOTO, VIDEO, AUDIO, FILE)
diff --git a/core/model/src/main/kotlin/com/meloda/app/fast/model/api/data/VkArticleData.kt b/core/model/src/main/kotlin/com/meloda/app/fast/model/api/data/VkArticleData.kt
new file mode 100644
index 00000000..c8e0d28e
--- /dev/null
+++ b/core/model/src/main/kotlin/com/meloda/app/fast/model/api/data/VkArticleData.kt
@@ -0,0 +1,15 @@
+package com.meloda.app.fast.model.api.data
+
+import com.meloda.app.fast.model.api.domain.VkArticleDomain
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+
+@JsonClass(generateAdapter = true)
+data class VkArticleData(
+ @Json(name = "id") val id: Int
+) : VkAttachmentData {
+
+ fun toDomain(): VkArticleDomain = VkArticleDomain(
+ id = id
+ )
+}
diff --git a/core/model/src/main/kotlin/com/meloda/app/fast/model/api/data/VkAttachmentItemData.kt b/core/model/src/main/kotlin/com/meloda/app/fast/model/api/data/VkAttachmentItemData.kt
index cee40fb0..db1570f6 100644
--- a/core/model/src/main/kotlin/com/meloda/app/fast/model/api/data/VkAttachmentItemData.kt
+++ b/core/model/src/main/kotlin/com/meloda/app/fast/model/api/data/VkAttachmentItemData.kt
@@ -31,7 +31,8 @@ data class VkAttachmentItemData(
@Json(name = "audios") val audios: List?,
@Json(name = "audio_playlist") val audioPlaylist: VkAudioPlaylistData?,
@Json(name = "podcast") val podcast: VkPodcastData?,
- @Json(name = "narrative") val narrative: VkNarrativeData?
+ @Json(name = "narrative") val narrative: VkNarrativeData?,
+ @Json(name = "article") val article: VkArticleData?
) {
fun toDomain(): VkAttachment = when (AttachmentType.parse(type)) {
AttachmentType.UNKNOWN -> VkUnknownAttachment
@@ -58,5 +59,6 @@ data class VkAttachmentItemData(
AttachmentType.AUDIO_PLAYLIST -> audioPlaylist?.toDomain()
AttachmentType.PODCAST -> podcast?.toDomain()
AttachmentType.NARRATIVE -> narrative?.toDomain()
+ AttachmentType.ARTICLE -> article?.toDomain()
} ?: VkUnknownAttachment
}
diff --git a/core/model/src/main/kotlin/com/meloda/app/fast/model/api/data/VkGroupData.kt b/core/model/src/main/kotlin/com/meloda/app/fast/model/api/data/VkGroupData.kt
index fabed87b..36014196 100644
--- a/core/model/src/main/kotlin/com/meloda/app/fast/model/api/data/VkGroupData.kt
+++ b/core/model/src/main/kotlin/com/meloda/app/fast/model/api/data/VkGroupData.kt
@@ -10,11 +10,11 @@ data class VkGroupData(
@Json(name = "id") val id: Int,
@Json(name = "name") val name: String,
@Json(name = "screen_name") val screenName: String,
- @Json(name = "is_closed") val isClosed: Int,
+ @Json(name = "is_closed") val isClosed: Int?,
@Json(name = "type") val type: String,
- @Json(name = "is_admin") val isAdmin: Int,
- @Json(name = "is_member") val isMember: Int,
- @Json(name = "is_advertiser") val isAdvertiser: Int,
+ @Json(name = "is_admin") val isAdmin: Int?,
+ @Json(name = "is_member") val isMember: Int?,
+ @Json(name = "is_advertiser") val isAdvertiser: Int?,
@Json(name = "photo_50") val photo50: String?,
@Json(name = "photo_100") val photo100: String?,
@Json(name = "photo_200") val photo200: String?,
diff --git a/core/model/src/main/kotlin/com/meloda/app/fast/model/api/domain/VkArticleDomain.kt b/core/model/src/main/kotlin/com/meloda/app/fast/model/api/domain/VkArticleDomain.kt
new file mode 100644
index 00000000..fead8b38
--- /dev/null
+++ b/core/model/src/main/kotlin/com/meloda/app/fast/model/api/domain/VkArticleDomain.kt
@@ -0,0 +1,10 @@
+package com.meloda.app.fast.model.api.domain
+
+import com.meloda.app.fast.model.api.data.AttachmentType
+
+data class VkArticleDomain(
+ val id: Int
+) : VkAttachment {
+
+ override val type: AttachmentType = AttachmentType.ARTICLE
+}
diff --git a/core/network/src/main/kotlin/com/meloda/app/fast/network/OAuthError.kt b/core/network/src/main/kotlin/com/meloda/app/fast/network/OAuthError.kt
index 52ca2a75..90fefb1c 100644
--- a/core/network/src/main/kotlin/com/meloda/app/fast/network/OAuthError.kt
+++ b/core/network/src/main/kotlin/com/meloda/app/fast/network/OAuthError.kt
@@ -75,7 +75,7 @@ data class InvalidCredentialsError(
)
@JsonClass(generateAdapter = true)
-data class WrongTwoFaCode(
+data class WrongTwoFaCodeError(
@Json(name = "error") override val error: String, // "invalid_request"
@Json(name = "error_description") override val errorDescription: String,
@Json(name = "error_type") override val errorType: String // "wrong_otp"
@@ -86,7 +86,7 @@ data class WrongTwoFaCode(
)
@JsonClass(generateAdapter = true)
-data class WrongTwoFaCodeFormat(
+data class WrongTwoFaCodeFormatError(
@Json(name = "error") override val error: String, // "invalid_request"
@Json(name = "error_description") override val errorDescription: String,
@Json(name = "error_type") override val errorType: String // "otp_format_is_incorrect"
@@ -96,6 +96,17 @@ data class WrongTwoFaCodeFormat(
errorType = errorType
)
+@JsonClass(generateAdapter = true)
+data class TooManyTriesError(
+ @Json(name = "error") override val error: String, // "9;Flood control"
+ @Json(name = "error_description") override val errorDescription: String,
+ @Json(name = "error_type") override val errorType: String // "password_bruteforce_attempt"
+) : OAuthError(
+ error = error,
+ errorDescription = errorDescription,
+ errorType = errorType
+)
+
fun OAuthError.toDomain(): OAuthErrorDomain? = when (this) {
is ValidationRequiredError -> {
OAuthErrorDomain.ValidationRequiredError(
@@ -129,13 +140,17 @@ fun OAuthError.toDomain(): OAuthErrorDomain? = when (this) {
OAuthErrorDomain.InvalidCredentialsError
}
- is WrongTwoFaCode -> {
+ is WrongTwoFaCodeError -> {
OAuthErrorDomain.WrongTwoFaCode
}
- is WrongTwoFaCodeFormat -> {
+ is WrongTwoFaCodeFormatError -> {
OAuthErrorDomain.WrongTwoFaCodeFormat
}
+ is TooManyTriesError -> {
+ OAuthErrorDomain.TooManyTriesError
+ }
+
else -> null
}
diff --git a/core/network/src/main/kotlin/com/meloda/app/fast/network/OAuthErrorDomain.kt b/core/network/src/main/kotlin/com/meloda/app/fast/network/OAuthErrorDomain.kt
index 7066e53d..b33039f0 100644
--- a/core/network/src/main/kotlin/com/meloda/app/fast/network/OAuthErrorDomain.kt
+++ b/core/network/src/main/kotlin/com/meloda/app/fast/network/OAuthErrorDomain.kt
@@ -27,5 +27,7 @@ sealed class OAuthErrorDomain {
data object InvalidCredentialsError : OAuthErrorDomain()
data object WrongTwoFaCode : OAuthErrorDomain()
data object WrongTwoFaCodeFormat : OAuthErrorDomain()
+ data object TooManyTriesError: OAuthErrorDomain()
+
data object UnknownError : OAuthErrorDomain()
}
diff --git a/core/network/src/main/kotlin/com/meloda/app/fast/network/OAuthResultCallFactory.kt b/core/network/src/main/kotlin/com/meloda/app/fast/network/OAuthResultCallFactory.kt
index 55d299cc..4ec71364 100644
--- a/core/network/src/main/kotlin/com/meloda/app/fast/network/OAuthResultCallFactory.kt
+++ b/core/network/src/main/kotlin/com/meloda/app/fast/network/OAuthResultCallFactory.kt
@@ -110,6 +110,11 @@ internal class ResultCall(
.fromJson(errorBodyString.orEmpty()) ?: return
val error: OAuthError? = when (baseError.error) {
+ "9;Flood control" -> {
+ moshi.adapter(TooManyTriesError::class.java)
+ .fromJson(errorBodyString.orEmpty())
+ }
+
"invalid_client" -> {
moshi.adapter(InvalidCredentialsError::class.java)
.fromJson(errorBodyString.orEmpty())
@@ -123,12 +128,12 @@ internal class ResultCall(
"invalid_request" -> {
when (val type = baseError.errorType) {
"wrong_otp" -> {
- moshi.adapter(WrongTwoFaCode::class.java)
+ moshi.adapter(WrongTwoFaCodeError::class.java)
.fromJson(errorBodyString.orEmpty())
}
"otp_format_is_incorrect" -> {
- moshi.adapter(WrongTwoFaCodeFormat::class.java)
+ moshi.adapter(WrongTwoFaCodeFormatError::class.java)
.fromJson(errorBodyString.orEmpty())
}
diff --git a/core/network/src/main/kotlin/com/meloda/app/fast/network/VkErrorCodes.kt b/core/network/src/main/kotlin/com/meloda/app/fast/network/VkErrorCodes.kt
index 9f597b68..c4faa244 100644
--- a/core/network/src/main/kotlin/com/meloda/app/fast/network/VkErrorCodes.kt
+++ b/core/network/src/main/kotlin/com/meloda/app/fast/network/VkErrorCodes.kt
@@ -51,10 +51,12 @@ object VkOAuthErrors {
const val NEED_CAPTCHA = "need_captcha"
const val INVALID_CLIENT = "invalid_client"
const val INVALID_REQUEST = "invalid_request"
+ const val FLOOD_CONTROL = "9;Flood control"
}
object VkErrorTypes {
const val WRONG_OTP_FORMAT = "otp_format_is_incorrect"
const val WRONG_OTP = "wrong_otp"
+ const val PASSWORD_BRUTEFORCE_ATTEMPT = "password_bruteforce_attempt"
}
diff --git a/feature/auth/captcha/src/main/kotlin/com/meloda/app/fast/auth/captcha/CaptchaViewModel.kt b/feature/auth/captcha/src/main/kotlin/com/meloda/app/fast/auth/captcha/CaptchaViewModel.kt
index ccbf20a0..3187699f 100644
--- a/feature/auth/captcha/src/main/kotlin/com/meloda/app/fast/auth/captcha/CaptchaViewModel.kt
+++ b/feature/auth/captcha/src/main/kotlin/com/meloda/app/fast/auth/captcha/CaptchaViewModel.kt
@@ -2,7 +2,6 @@ package com.meloda.app.fast.auth.captcha
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
-import com.meloda.app.fast.auth.captcha.model.CaptchaArguments
import com.meloda.app.fast.auth.captcha.model.CaptchaScreenState
import com.meloda.app.fast.auth.captcha.navigation.Captcha
import com.meloda.app.fast.auth.captcha.validation.CaptchaValidator
@@ -13,16 +12,14 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
interface CaptchaViewModel {
-
val screenState: StateFlow
+ val isNeedToOpenLogin: StateFlow
fun onCodeInputChanged(newCode: String)
fun onTextFieldDoneClicked()
fun onDoneButtonClicked()
- fun setArguments(arguments: CaptchaArguments)
-
fun onNavigatedToLogin()
}
@@ -32,6 +29,7 @@ class CaptchaViewModelImpl(
) : CaptchaViewModel, ViewModel() {
override val screenState = MutableStateFlow(CaptchaScreenState.EMPTY)
+ override val isNeedToOpenLogin = MutableStateFlow(false)
init {
val arguments = Captcha.from(savedStateHandle).arguments
@@ -57,20 +55,12 @@ class CaptchaViewModelImpl(
override fun onDoneButtonClicked() {
if (!processValidation()) return
- screenState.updateValue(screenState.value.copy(isNeedToOpenLogin = true))
- }
-
- override fun setArguments(arguments: CaptchaArguments) {
-// screenState.updateValue(
-// screenState.value.copy(
-// captchaSid = arguments.captchaSid,
-// captchaImage = arguments.captchaImage
-// )
-// )
+ isNeedToOpenLogin.update { true }
}
override fun onNavigatedToLogin() {
screenState.updateValue(CaptchaScreenState.EMPTY)
+ isNeedToOpenLogin.update { false }
}
private fun processValidation(): Boolean {
diff --git a/feature/auth/captcha/src/main/kotlin/com/meloda/app/fast/auth/captcha/model/CaptchaScreenState.kt b/feature/auth/captcha/src/main/kotlin/com/meloda/app/fast/auth/captcha/model/CaptchaScreenState.kt
index ed5a116d..b794364c 100644
--- a/feature/auth/captcha/src/main/kotlin/com/meloda/app/fast/auth/captcha/model/CaptchaScreenState.kt
+++ b/feature/auth/captcha/src/main/kotlin/com/meloda/app/fast/auth/captcha/model/CaptchaScreenState.kt
@@ -4,8 +4,7 @@ data class CaptchaScreenState(
val captchaSid: String,
val captchaImage: String,
val captchaCode: String,
- val codeError: Boolean,
- val isNeedToOpenLogin: Boolean
+ val codeError: Boolean
) {
companion object {
@@ -13,8 +12,7 @@ data class CaptchaScreenState(
captchaSid = "",
captchaImage = "",
captchaCode = "",
- codeError = false,
- isNeedToOpenLogin = false
+ codeError = false
)
}
}
diff --git a/feature/auth/captcha/src/main/kotlin/com/meloda/app/fast/auth/captcha/navigation/CaptchaRoute.kt b/feature/auth/captcha/src/main/kotlin/com/meloda/app/fast/auth/captcha/navigation/CaptchaRoute.kt
index 10eabb52..308d74cb 100644
--- a/feature/auth/captcha/src/main/kotlin/com/meloda/app/fast/auth/captcha/navigation/CaptchaRoute.kt
+++ b/feature/auth/captcha/src/main/kotlin/com/meloda/app/fast/auth/captcha/navigation/CaptchaRoute.kt
@@ -41,7 +41,7 @@ fun NavController.navigateToCaptcha(arguments: CaptchaArguments) {
this.navigate(Captcha(arguments))
}
-fun NavController.setCaptchaResult(code: String) {
+fun NavController.setCaptchaResult(code: String?) {
this.currentBackStackEntry
?.savedStateHandle
?.set("captchacode", code)
diff --git a/feature/auth/captcha/src/main/kotlin/com/meloda/app/fast/auth/captcha/presentation/CaptchaScreen.kt b/feature/auth/captcha/src/main/kotlin/com/meloda/app/fast/auth/captcha/presentation/CaptchaScreen.kt
index 37204ab7..35078b71 100644
--- a/feature/auth/captcha/src/main/kotlin/com/meloda/app/fast/auth/captcha/presentation/CaptchaScreen.kt
+++ b/feature/auth/captcha/src/main/kotlin/com/meloda/app/fast/auth/captcha/presentation/CaptchaScreen.kt
@@ -27,6 +27,7 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
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
@@ -61,6 +62,7 @@ fun CaptchaScreen(
viewModel: CaptchaViewModel = koinViewModel()
) {
val screenState by viewModel.screenState.collectAsStateWithLifecycle()
+ val isNeedToOpenLogin by viewModel.isNeedToOpenLogin.collectAsStateWithLifecycle()
var confirmedExit by rememberSaveable {
mutableStateOf(false)
@@ -70,8 +72,10 @@ fun CaptchaScreen(
mutableStateOf(false)
}
- if (confirmedExit) {
- onBack()
+ LaunchedEffect(confirmedExit) {
+ if (confirmedExit) {
+ onBack()
+ }
}
BackHandler(enabled = !confirmedExit) {
@@ -93,9 +97,11 @@ fun CaptchaScreen(
)
}
- if (screenState.isNeedToOpenLogin) {
- viewModel.onNavigatedToLogin()
- onResult(screenState.captchaCode)
+ LaunchedEffect(isNeedToOpenLogin) {
+ if (isNeedToOpenLogin) {
+ viewModel.onNavigatedToLogin()
+ onResult(screenState.captchaCode)
+ }
}
val focusManager = LocalFocusManager.current
diff --git a/feature/auth/login/build.gradle.kts b/feature/auth/login/build.gradle.kts
index e09a69b8..90be05ef 100644
--- a/feature/auth/login/build.gradle.kts
+++ b/feature/auth/login/build.gradle.kts
@@ -89,4 +89,6 @@ dependencies {
implementation(libs.eithernet)
implementation(libs.androidx.navigation.compose)
implementation(libs.kotlin.serialization)
+
+ implementation(libs.rebugger)
}
diff --git a/feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/LoginViewModel.kt b/feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/LoginViewModel.kt
index e69b975e..f270ad56 100644
--- a/feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/LoginViewModel.kt
+++ b/feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/LoginViewModel.kt
@@ -15,9 +15,11 @@ import com.meloda.app.fast.data.db.AccountsRepository
import com.meloda.app.fast.data.processState
import com.meloda.app.fast.model.database.AccountEntity
import com.meloda.app.fast.network.OAuthErrorDomain
-import com.meloda.fast.auth.login.model.CaptchaArguments
+import com.meloda.fast.auth.login.model.LoginCaptchaArguments
+import com.meloda.fast.auth.login.model.LoginError
import com.meloda.fast.auth.login.model.LoginScreenState
import com.meloda.fast.auth.login.model.LoginTwoFaArguments
+import com.meloda.fast.auth.login.model.LoginUserBannedArguments
import com.meloda.fast.auth.login.model.LoginValidationResult
import com.meloda.fast.auth.login.validation.LoginValidator
import kotlinx.coroutines.Dispatchers
@@ -27,10 +29,19 @@ import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
interface LoginViewModel {
val screenState: StateFlow
+ val loginError: StateFlow
+
+ val twoFaCode: StateFlow
+ val twoFaArguments: StateFlow
+ val captchaCode: StateFlow
+ val captchaArguments: StateFlow
+ val userBannedArguments: StateFlow
+ val isNeedToOpenMain: StateFlow
fun onPasswordVisibilityButtonClicked()
@@ -56,10 +67,18 @@ class LoginViewModelImpl(
private val oAuthUseCase: OAuthUseCase,
private val usersUseCase: UsersUseCase,
private val accountsRepository: AccountsRepository,
- private val loginValidator: LoginValidator,
+ private val loginValidator: LoginValidator
) : ViewModel(), LoginViewModel {
override val screenState = MutableStateFlow(LoginScreenState.EMPTY)
+ override val loginError = MutableStateFlow(null)
+
+ override val twoFaCode = MutableStateFlow(null)
+ override val twoFaArguments = MutableStateFlow(null)
+ override val captchaCode = MutableStateFlow(null)
+ override val captchaArguments = MutableStateFlow(null)
+ override val userBannedArguments = MutableStateFlow(null)
+ override val isNeedToOpenMain = MutableStateFlow(false)
private val validationState: StateFlow> =
screenState.map(loginValidator::validate)
@@ -86,37 +105,38 @@ class LoginViewModelImpl(
}
override fun onSignInButtonClicked() {
+ if (screenState.value.isLoading) return
login()
}
override fun onErrorDialogDismissed() {
- screenState.setValue { old -> old.copy(error = null) }
+ loginError.update { null }
}
override fun onNavigatedToMain() {
- screenState.setValue { old -> old.copy(isNeedToNavigateToMain = false) }
+ isNeedToOpenMain.update { false }
}
override fun onNavigatedToUserBanned() {
- screenState.setValue { old -> old.copy(userBannedArguments = null) }
+ userBannedArguments.update { null }
}
override fun onNavigatedToCaptcha() {
- screenState.setValue { old -> old.copy(captchaArguments = null) }
+ captchaArguments.update { null }
}
override fun onNavigatedToTwoFa() {
- screenState.setValue { old -> old.copy(twoFaArguments = null) }
+ twoFaArguments.update { null }
}
override fun onTwoFaCodeReceived(code: String) {
- screenState.setValue { old -> old.copy(validationCode = code) }
+ twoFaCode.update { code }
login()
}
override fun onCaptchaCodeReceived(code: String) {
- screenState.setValue { old -> old.copy(captchaCode = code) }
+ captchaCode.update { code }
login()
}
@@ -149,7 +169,8 @@ class LoginViewModelImpl(
accountsRepository.storeAccounts(listOf(currentAccount))
delay(350)
- screenState.setValue { old -> old.copy(isNeedToNavigateToMain = true) }
+
+ isNeedToOpenMain.update { true }
}
}
)
@@ -163,7 +184,10 @@ class LoginViewModelImpl(
Log.d(
"LoginViewModel",
- "auth: login: ${currentState.login}; password: ${currentState.password}; code: ${currentState.validationCode}"
+ "auth: login: ${currentState.login}; " +
+ "password: ${currentState.password}; " +
+ "2fa code: ${twoFaCode.value}; " +
+ "captcha code: ${captchaCode.value}"
)
processValidation()
@@ -173,14 +197,17 @@ class LoginViewModelImpl(
login = currentState.login,
password = currentState.password,
forceSms = forceSms,
- twoFaCode = currentState.validationCode,
- captchaSid = currentState.captchaArguments?.captchaSid,
- captchaKey = currentState.captchaCode
+ twoFaCode = twoFaCode.value,
+ captchaSid = captchaArguments.value?.captchaSid,
+ captchaKey = captchaCode.value
).listenValue { state ->
state.processState(
error = { error ->
Log.d("LoginViewModelImpl", "login: error: $error")
+ twoFaCode.update { null }
+ captchaCode.update { null }
+
parseError(error)
},
success = { response ->
@@ -213,20 +240,20 @@ class LoginViewModelImpl(
accountsRepository.storeAccounts(listOf(currentAccount))
+ captchaArguments.update { null }
+ captchaCode.update { null }
+
+ twoFaArguments.update { null }
+ twoFaCode.update { null }
+
screenState.setValue { old ->
old.copy(
- captchaArguments = null,
- captchaCode = null,
- validationSid = null,
- validationCode = null,
- twoFaArguments = null,
-
login = "",
password = "",
-
- isNeedToNavigateToMain = true
)
}
+
+ isNeedToOpenMain.update { true }
}
)
screenState.emit(screenState.value.copy(isLoading = state.isLoading()))
@@ -238,7 +265,7 @@ class LoginViewModelImpl(
is State.Error.OAuthError -> {
when (val error = stateError.error) {
is OAuthErrorDomain.ValidationRequiredError -> {
- val twoFaArguments = LoginTwoFaArguments(
+ val arguments = LoginTwoFaArguments(
validationSid = error.validationSid,
redirectUri = error.redirectUri,
phoneMask = error.phoneMask,
@@ -246,25 +273,49 @@ class LoginViewModelImpl(
canResendSms = error.validationResend == "sms",
wrongCodeError = null
)
- screenState.setValue { old -> old.copy(twoFaArguments = twoFaArguments) }
- true
+ twoFaArguments.update { arguments }
}
is OAuthErrorDomain.CaptchaRequiredError -> {
- val captchaArguments = CaptchaArguments(
+ val arguments = LoginCaptchaArguments(
captchaSid = error.captchaSid,
captchaImage = error.captchaImageUrl
)
- screenState.setValue { old -> old.copy(captchaArguments = captchaArguments) }
- true
+ captchaArguments.update { arguments }
}
- OAuthErrorDomain.InvalidCredentialsError -> TODO()
- is OAuthErrorDomain.UserBannedError -> TODO()
- OAuthErrorDomain.WrongTwoFaCode -> TODO()
- OAuthErrorDomain.WrongTwoFaCodeFormat -> TODO()
- OAuthErrorDomain.UnknownError -> TODO()
+ OAuthErrorDomain.InvalidCredentialsError -> {
+ loginError.update { LoginError.WrongCredentials }
+ }
+
+ is OAuthErrorDomain.UserBannedError -> {
+ val arguments = LoginUserBannedArguments(
+ name = error.memberName,
+ message = error.message,
+ restoreUrl = error.restoreUrl,
+ accessToken = error.accessToken
+ )
+ userBannedArguments.update { arguments }
+ }
+
+ OAuthErrorDomain.WrongTwoFaCode -> {
+ loginError.update { LoginError.WrongTwoFaCode }
+ }
+
+ OAuthErrorDomain.WrongTwoFaCodeFormat -> {
+ loginError.update { LoginError.WrongTwoFaCodeFormat }
+ }
+
+ OAuthErrorDomain.TooManyTriesError -> {
+ loginError.update { LoginError.TooManyTries }
+ }
+
+ OAuthErrorDomain.UnknownError -> {
+ loginError.update { LoginError.Unknown }
+ }
}
+
+ true
}
else -> false
diff --git a/feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/OAuthUseCaseImpl.kt b/feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/OAuthUseCaseImpl.kt
index b6f50e4f..c16baba5 100644
--- a/feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/OAuthUseCaseImpl.kt
+++ b/feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/OAuthUseCaseImpl.kt
@@ -44,6 +44,10 @@ class OAuthUseCaseImpl(
)
}
+ VkOAuthErrors.FLOOD_CONTROL -> {
+ State.Error.OAuthError(OAuthErrorDomain.TooManyTriesError)
+ }
+
VkOAuthErrors.NEED_VALIDATION -> {
if (response.banInfo != null) {
val info = requireNotNull(response.banInfo)
diff --git a/feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/model/CaptchaArguments.kt b/feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/model/LoginCaptchaArguments.kt
similarity index 87%
rename from feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/model/CaptchaArguments.kt
rename to feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/model/LoginCaptchaArguments.kt
index 583955dd..b8837e89 100644
--- a/feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/model/CaptchaArguments.kt
+++ b/feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/model/LoginCaptchaArguments.kt
@@ -6,7 +6,7 @@ import kotlinx.serialization.Serializable
@Serializable
@Parcelize
-data class CaptchaArguments(
+data class LoginCaptchaArguments(
val captchaSid: String,
val captchaImage: String
) : Parcelable
diff --git a/feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/model/LoginError.kt b/feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/model/LoginError.kt
index 114fa6db..6dbdcd96 100644
--- a/feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/model/LoginError.kt
+++ b/feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/model/LoginError.kt
@@ -1,6 +1,12 @@
package com.meloda.fast.auth.login.model
-sealed interface LoginError {
+import androidx.compose.runtime.Immutable
- data object WrongCredentials : LoginError
+@Immutable
+sealed class LoginError {
+ data object Unknown : LoginError()
+ data object WrongCredentials : LoginError()
+ data object TooManyTries : LoginError()
+ data object WrongTwoFaCode : LoginError()
+ data object WrongTwoFaCodeFormat : LoginError()
}
diff --git a/feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/model/LoginScreenState.kt b/feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/model/LoginScreenState.kt
index 8951ad4a..2cc6cc8d 100644
--- a/feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/model/LoginScreenState.kt
+++ b/feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/model/LoginScreenState.kt
@@ -2,43 +2,24 @@ package com.meloda.fast.auth.login.model
import androidx.compose.runtime.Immutable
-// TODO: 04/05/2024, Danil Nikolaev: simplify
@Immutable
data class LoginScreenState(
val login: String,
val password: String,
- val captchaCode: String?,
- val validationSid: String?,
- val validationCode: String?,
val isLoading: Boolean,
val loginError: Boolean,
val passwordError: Boolean,
val passwordVisible: Boolean,
- val copiedCode: String?,
- val isNeedToNavigateToMain: Boolean,
- val twoFaArguments: LoginTwoFaArguments?,
- val captchaArguments: CaptchaArguments?,
- val userBannedArguments: UserBannedArguments?,
- val error: LoginError?,
) {
companion object {
val EMPTY = LoginScreenState(
login = "",
password = "",
- captchaCode = null,
- validationSid = null,
- validationCode = null,
isLoading = false,
loginError = false,
passwordError = false,
- passwordVisible = false,
- copiedCode = null,
- isNeedToNavigateToMain = false,
- twoFaArguments = null,
- captchaArguments = null,
- userBannedArguments = null,
- error = null,
+ passwordVisible = false
)
}
}
diff --git a/feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/model/UserBannedArguments.kt b/feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/model/LoginUserBannedArguments.kt
similarity index 88%
rename from feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/model/UserBannedArguments.kt
rename to feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/model/LoginUserBannedArguments.kt
index 03dfb5e8..b8b986d8 100644
--- a/feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/model/UserBannedArguments.kt
+++ b/feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/model/LoginUserBannedArguments.kt
@@ -6,7 +6,7 @@ import kotlinx.serialization.Serializable
@Serializable
@Parcelize
-data class UserBannedArguments(
+data class LoginUserBannedArguments(
val name: String,
val message: String,
val restoreUrl: String,
diff --git a/feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/navigation/LoginRoute.kt b/feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/navigation/LoginRoute.kt
index d1e08003..ed346db6 100644
--- a/feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/navigation/LoginRoute.kt
+++ b/feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/navigation/LoginRoute.kt
@@ -1,5 +1,6 @@
package com.meloda.fast.auth.login.navigation
+import androidx.navigation.NavBackStackEntry
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.composable
@@ -7,9 +8,9 @@ import com.meloda.app.fast.common.extensions.navigation.sharedViewModel
import com.meloda.app.fast.model.BaseError
import com.meloda.fast.auth.login.LoginViewModel
import com.meloda.fast.auth.login.LoginViewModelImpl
-import com.meloda.fast.auth.login.model.CaptchaArguments
+import com.meloda.fast.auth.login.model.LoginCaptchaArguments
import com.meloda.fast.auth.login.model.LoginTwoFaArguments
-import com.meloda.fast.auth.login.model.UserBannedArguments
+import com.meloda.fast.auth.login.model.LoginUserBannedArguments
import com.meloda.fast.auth.login.presentation.LoginScreen
import com.meloda.fast.auth.login.presentation.LogoScreen
import kotlinx.serialization.Serializable
@@ -22,16 +23,19 @@ object Logo
fun NavGraphBuilder.loginRoute(
onError: (BaseError) -> Unit,
- onNavigateToCaptcha: (CaptchaArguments) -> Unit,
+ onNavigateToCaptcha: (LoginCaptchaArguments) -> Unit,
onNavigateToTwoFa: (LoginTwoFaArguments) -> Unit,
onNavigateToMain: () -> Unit,
- onNavigateToUserBanned: (UserBannedArguments) -> Unit,
+ onNavigateToUserBanned: (LoginUserBannedArguments) -> Unit,
onNavigateToCredentials: () -> Unit,
navController: NavController
) {
- composable {
+ composable { backStackEntry ->
val viewModel: LoginViewModel =
- it.sharedViewModel(navController = navController)
+ backStackEntry.sharedViewModel(navController = navController)
+
+ val twoFaCode = backStackEntry.getTwoFaResult()
+ val captchaCode = backStackEntry.getCaptchaResult()
LoginScreen(
onError = onError,
@@ -39,6 +43,8 @@ fun NavGraphBuilder.loginRoute(
onNavigateToMain = onNavigateToMain,
onNavigateToCaptcha = onNavigateToCaptcha,
onNavigateToTwoFa = onNavigateToTwoFa,
+ twoFaCode = twoFaCode,
+ captchaCode = captchaCode,
viewModel = viewModel
)
}
@@ -54,3 +60,11 @@ fun NavGraphBuilder.loginRoute(
fun NavController.navigateToLogin() {
this.navigate(route = Login)
}
+
+fun NavBackStackEntry.getTwoFaResult(): String? {
+ return savedStateHandle["twofacode"]
+}
+
+fun NavBackStackEntry.getCaptchaResult(): String? {
+ return savedStateHandle["captchacode"]
+}
diff --git a/feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/presentation/LoginScreen.kt b/feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/presentation/LoginScreen.kt
index 5108aeee..92c7189c 100644
--- a/feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/presentation/LoginScreen.kt
+++ b/feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/presentation/LoginScreen.kt
@@ -23,6 +23,7 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
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
@@ -57,10 +58,11 @@ import com.meloda.app.fast.designsystem.handleTabKey
import com.meloda.app.fast.model.BaseError
import com.meloda.fast.auth.login.LoginViewModel
import com.meloda.fast.auth.login.LoginViewModelImpl
-import com.meloda.fast.auth.login.model.CaptchaArguments
+import com.meloda.fast.auth.login.model.LoginCaptchaArguments
import com.meloda.fast.auth.login.model.LoginError
import com.meloda.fast.auth.login.model.LoginTwoFaArguments
-import com.meloda.fast.auth.login.model.UserBannedArguments
+import com.meloda.fast.auth.login.model.LoginUserBannedArguments
+import com.theapache64.rebugger.Rebugger
import org.koin.androidx.compose.koinViewModel
import com.meloda.app.fast.designsystem.R as UiR
@@ -68,49 +70,64 @@ import com.meloda.app.fast.designsystem.R as UiR
@Composable
fun LoginScreen(
onError: (BaseError) -> Unit,
- onNavigateToUserBanned: (UserBannedArguments) -> Unit,
+ onNavigateToUserBanned: (LoginUserBannedArguments) -> Unit,
onNavigateToMain: () -> Unit,
- onNavigateToCaptcha: (CaptchaArguments) -> Unit,
+ onNavigateToCaptcha: (LoginCaptchaArguments) -> Unit,
onNavigateToTwoFa: (LoginTwoFaArguments) -> Unit,
+ twoFaCode: String?,
+ captchaCode: String?,
viewModel: LoginViewModel = koinViewModel()
) {
val screenState by viewModel.screenState.collectAsStateWithLifecycle()
+ val isNeedToOpenMain by viewModel.isNeedToOpenMain.collectAsStateWithLifecycle()
+ val userBannedArguments by viewModel.userBannedArguments.collectAsStateWithLifecycle()
+ val captchaArguments by viewModel.captchaArguments.collectAsStateWithLifecycle()
+ val twoFaArguments by viewModel.twoFaArguments.collectAsStateWithLifecycle()
+ val loginError by viewModel.loginError.collectAsStateWithLifecycle()
- if (screenState.isNeedToNavigateToMain) {
- viewModel.onNavigatedToMain()
- onNavigateToMain()
+ LaunchedEffect(isNeedToOpenMain) {
+ if (isNeedToOpenMain) {
+ viewModel.onNavigatedToMain()
+ onNavigateToMain()
+ }
}
- screenState.userBannedArguments?.let { arguments ->
- viewModel.onNavigatedToUserBanned()
- onNavigateToUserBanned(arguments)
+ LaunchedEffect(userBannedArguments) {
+ userBannedArguments?.let { arguments ->
+ viewModel.onNavigatedToUserBanned()
+ onNavigateToUserBanned(arguments)
+ }
}
- screenState.captchaArguments?.let { arguments ->
- viewModel.onNavigatedToCaptcha()
- onNavigateToCaptcha(arguments)
+ LaunchedEffect(captchaArguments) {
+ captchaArguments?.let { arguments ->
+ viewModel.onNavigatedToCaptcha()
+ onNavigateToCaptcha(arguments)
+ }
}
- screenState.twoFaArguments?.let { arguments ->
- viewModel.onNavigatedToTwoFa()
- onNavigateToTwoFa(arguments)
+ LaunchedEffect(twoFaArguments) {
+ twoFaArguments?.let { arguments ->
+ viewModel.onNavigatedToTwoFa()
+ onNavigateToTwoFa(arguments)
+ }
+ }
+
+ LaunchedEffect(twoFaCode) {
+ if (twoFaCode != null) {
+ viewModel.onTwoFaCodeReceived(twoFaCode)
+ }
+ }
+
+ LaunchedEffect(captchaCode) {
+ if (captchaCode != null) {
+ viewModel.onCaptchaCodeReceived(captchaCode)
+ }
}
val focusManager = LocalFocusManager.current
val (loginFocusable, passwordFocusable) = FocusRequester.createRefs()
- // TODO: 29/06/2024, Danil Nikolaev: remove lambda
- val goButtonClickAction = {
- if (!screenState.isLoading) {
- focusManager.clearFocus()
- viewModel.onSignInButtonClicked()
- }
- }
- val loginFieldTabClick = {
- passwordFocusable.requestFocus()
- true
- }
-
var loginText by remember { mutableStateOf(TextFieldValue(screenState.login)) }
val showLoginError = screenState.loginError
@@ -165,8 +182,14 @@ fun LoginScreen(
.height(58.dp)
.fillMaxWidth()
.clip(RoundedCornerShape(10.dp))
- .handleEnterKey(loginFieldTabClick::invoke)
- .handleTabKey(loginFieldTabClick::invoke)
+ .handleEnterKey {
+ passwordFocusable.requestFocus()
+ true
+ }
+ .handleTabKey {
+ passwordFocusable.requestFocus()
+ true
+ }
.focusRequester(loginFocusable)
.connectNode(handler = autoFillEmailHandler)
.defaultFocusChangeAutoFill(handler = autoFillEmailHandler),
@@ -213,7 +236,8 @@ fun LoginScreen(
.fillMaxWidth()
.clip(RoundedCornerShape(10.dp))
.handleEnterKey {
- goButtonClickAction.invoke()
+ focusManager.clearFocus()
+ viewModel.onSignInButtonClicked()
true
}
.focusRequester(passwordFocusable)
@@ -261,7 +285,10 @@ fun LoginScreen(
keyboardType = KeyboardType.Password
),
keyboardActions = KeyboardActions(
- onGo = { goButtonClickAction.invoke() }
+ onGo = {
+ focusManager.clearFocus()
+ viewModel.onSignInButtonClicked()
+ }
),
isError = showPasswordError,
visualTransformation = if (screenState.passwordVisible) {
@@ -282,7 +309,10 @@ fun LoginScreen(
) {
FloatingActionButton(
- onClick = goButtonClickAction::invoke,
+ onClick = {
+ focusManager.clearFocus()
+ viewModel.onSignInButtonClicked()
+ },
containerColor = MaterialTheme.colorScheme.secondaryContainer,
modifier = Modifier.testTag("Sign in button")
) {
@@ -306,7 +336,31 @@ fun LoginScreen(
HandleError(
onDismiss = viewModel::onErrorDialogDismissed,
- error = screenState.error
+ error = loginError
+ )
+
+ Rebugger(
+ trackMap = mapOf(
+ "onError" to onError,
+ "onNavigateToUserBanned" to onNavigateToUserBanned,
+ "onNavigateToMain" to onNavigateToMain,
+ "onNavigateToCaptcha" to onNavigateToCaptcha,
+ "onNavigateToTwoFa" to onNavigateToTwoFa,
+ "viewModel" to viewModel,
+ "screenState" to screenState,
+ "isNeedToOpenMain" to isNeedToOpenMain,
+ "userBannedArguments" to userBannedArguments,
+ "captchaArguments" to captchaArguments,
+ "twoFaArguments" to twoFaArguments,
+ "loginError" to loginError,
+ "focusManager" to focusManager,
+ "loginText" to loginText,
+ "showLoginError" to showLoginError,
+ "autoFillEmailHandler" to autoFillEmailHandler,
+ "passwordText" to passwordText,
+ "showPasswordError" to showPasswordError,
+ "autoFillPasswordHandler" to autoFillPasswordHandler,
+ ),
)
}
@@ -316,15 +370,52 @@ fun HandleError(
error: LoginError?,
) {
when (error) {
- LoginError.WrongCredentials -> {
+ null -> Unit
+
+ LoginError.Unknown -> {
MaterialDialog(
onDismissAction = onDismiss,
title = UiText.Simple("Error"),
- text = UiText.Simple("Wrong login or password"),
+ text = UiText.Simple("Unknown error."),
confirmText = UiText.Resource(UiR.string.ok)
)
}
- null -> Unit
+ LoginError.WrongCredentials -> {
+ MaterialDialog(
+ onDismissAction = onDismiss,
+ title = UiText.Simple("Error"),
+ text = UiText.Simple("Wrong login or password."),
+ confirmText = UiText.Resource(UiR.string.ok)
+ )
+ }
+
+ LoginError.TooManyTries -> {
+ MaterialDialog(
+ onDismissAction = onDismiss,
+ title = UiText.Simple("Error"),
+ text = UiText.Simple("Too many tries. Try in another hour or later."),
+ confirmText = UiText.Resource(UiR.string.ok)
+ )
+ }
+
+
+ LoginError.WrongTwoFaCode -> {
+ MaterialDialog(
+ onDismissAction = onDismiss,
+ title = UiText.Simple("Error"),
+ text = UiText.Simple("Wrong validation code."),
+ confirmText = UiText.Resource(UiR.string.ok)
+ )
+ }
+
+ LoginError.WrongTwoFaCodeFormat -> {
+ MaterialDialog(
+ onDismissAction = onDismiss,
+ title = UiText.Simple("Error"),
+ text = UiText.Simple("Wrong validation code format."),
+ confirmText = UiText.Resource(UiR.string.ok)
+ )
+ }
}
}
diff --git a/feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/presentation/LogoScreen.kt b/feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/presentation/LogoScreen.kt
index 232fe5a0..fdbed2b1 100644
--- a/feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/presentation/LogoScreen.kt
+++ b/feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/presentation/LogoScreen.kt
@@ -19,6 +19,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
@@ -40,11 +41,13 @@ fun LogoScreen(
onShowCredentials: () -> Unit,
viewModel: LoginViewModel = koinViewModel()
) {
- val screenState by viewModel.screenState.collectAsStateWithLifecycle()
+ val isNeedToOpenMain by viewModel.isNeedToOpenMain.collectAsStateWithLifecycle()
- if (screenState.isNeedToNavigateToMain) {
- viewModel.onNavigatedToMain()
- onNavigateToMain()
+ LaunchedEffect(isNeedToOpenMain) {
+ if (isNeedToOpenMain) {
+ viewModel.onNavigatedToMain()
+ onNavigateToMain()
+ }
}
Scaffold { padding ->
diff --git a/feature/auth/src/main/kotlin/com/meloda/app/fast/auth/AuthGraph.kt b/feature/auth/src/main/kotlin/com/meloda/app/fast/auth/AuthGraph.kt
index d65b644a..d80bd51c 100644
--- a/feature/auth/src/main/kotlin/com/meloda/app/fast/auth/AuthGraph.kt
+++ b/feature/auth/src/main/kotlin/com/meloda/app/fast/auth/AuthGraph.kt
@@ -2,7 +2,6 @@ package com.meloda.app.fast.auth
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
-import androidx.navigation.NavHostController
import androidx.navigation.navigation
import com.meloda.app.fast.auth.captcha.model.CaptchaArguments
import com.meloda.app.fast.auth.captcha.navigation.captchaRoute
@@ -20,6 +19,7 @@ import com.meloda.fast.auth.login.navigation.Logo
import com.meloda.fast.auth.login.navigation.loginRoute
import com.meloda.fast.auth.login.navigation.navigateToLogin
import kotlinx.serialization.Serializable
+import java.net.URLEncoder
@Serializable
object AuthGraph
@@ -27,7 +27,7 @@ object AuthGraph
fun NavGraphBuilder.authNavGraph(
onError: (BaseError) -> Unit,
onNavigateToMain: () -> Unit,
- navController: NavHostController
+ navController: NavController
) {
navigation(
startDestination = Logo
@@ -46,7 +46,7 @@ fun NavGraphBuilder.authNavGraph(
navController.navigateToTwoFa(
TwoFaArguments(
validationSid = arguments.validationSid,
- redirectUri = arguments.redirectUri,
+ redirectUri = URLEncoder.encode(arguments.redirectUri, "utf-8"),
phoneMask = arguments.phoneMask,
validationType = arguments.validationType,
canResendSms = arguments.canResendSms,
@@ -70,7 +70,10 @@ fun NavGraphBuilder.authNavGraph(
)
twoFaRoute(
- onBack = navController::navigateUp,
+ onBack = {
+ navController.navigateUp()
+ navController.setTwoFaResult(null)
+ },
onResult = { code ->
navController.popBackStack()
navController.setTwoFaResult(code)
@@ -78,7 +81,10 @@ fun NavGraphBuilder.authNavGraph(
)
captchaRoute(
- onBack = navController::navigateUp,
+ onBack = {
+ navController.navigateUp()
+ navController.setCaptchaResult(null)
+ },
onResult = { code ->
navController.popBackStack()
navController.setCaptchaResult(code)
diff --git a/feature/auth/twofa/src/main/kotlin/com/meloda/app/fast/auth/twofa/TwoFaViewModel.kt b/feature/auth/twofa/src/main/kotlin/com/meloda/app/fast/auth/twofa/TwoFaViewModel.kt
index 3626c5aa..a808d190 100644
--- a/feature/auth/twofa/src/main/kotlin/com/meloda/app/fast/auth/twofa/TwoFaViewModel.kt
+++ b/feature/auth/twofa/src/main/kotlin/com/meloda/app/fast/auth/twofa/TwoFaViewModel.kt
@@ -1,10 +1,8 @@
package com.meloda.app.fast.auth.twofa
-import android.util.Log
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
-import com.meloda.app.fast.auth.twofa.model.TwoFaArguments
import com.meloda.app.fast.auth.twofa.model.TwoFaScreenState
import com.meloda.app.fast.auth.twofa.model.TwoFaValidationType
import com.meloda.app.fast.auth.twofa.navigation.TwoFa
@@ -20,6 +18,7 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
@@ -27,6 +26,8 @@ interface TwoFaViewModel {
val screenState: StateFlow
+ val isNeedToOpenLogin: StateFlow
+
fun onCodeInputChanged(newCode: String)
fun onBackButtonClicked()
@@ -36,8 +37,6 @@ interface TwoFaViewModel {
fun onDoneButtonClicked()
fun onNavigatedToLogin()
-
- fun setArguments(arguments: TwoFaArguments)
}
class TwoFaViewModelImpl(
@@ -48,6 +47,8 @@ class TwoFaViewModelImpl(
override val screenState = MutableStateFlow(TwoFaScreenState.EMPTY)
+ override val isNeedToOpenLogin = MutableStateFlow(false)
+
private var delayJob: Job? = null
init {
@@ -88,12 +89,8 @@ class TwoFaViewModelImpl(
}
override fun onCancelButtonClicked() {
- screenState.updateValue(
- screenState.value.copy(
- twoFaCode = null,
- isNeedToOpenLogin = true
- )
- )
+ screenState.setValue { old -> old.copy(twoFaCode = null) }
+ isNeedToOpenLogin.update { true }
}
override fun onRequestSmsButtonClicked() {
@@ -107,25 +104,12 @@ class TwoFaViewModelImpl(
override fun onDoneButtonClicked() {
if (!processValidation()) return
- screenState.updateValue(screenState.value.copy(isNeedToOpenLogin = true))
+ isNeedToOpenLogin.update { true }
}
override fun onNavigatedToLogin() {
screenState.updateValue(TwoFaScreenState.EMPTY)
- }
-
- override fun setArguments(arguments: TwoFaArguments) {
- Log.d("TwoFaViewModel", "TwoFaArguments: $arguments")
-
-// screenState.updateValue(
-// screenState.value.copy(
-// twoFaSid = arguments.validationSid,
-// canResendSms = arguments.canResendSms,
-// codeError = arguments.wrongCodeError,
-// twoFaText = getTwoFaText(TwoFaValidationType.parse(arguments.validationType)),
-// phoneMask = arguments.phoneMask
-// )
-// )
+ isNeedToOpenLogin.update { false }
}
private fun processValidation(): Boolean {
@@ -147,7 +131,9 @@ class TwoFaViewModelImpl(
authUseCase.sendSms(validationSid)
.listenValue { state ->
state.processState(
- error = { error -> },
+ error = { error ->
+
+ },
success = { response ->
val newValidationType = response.validationType
val newCanResendSms = response.validationResend == "sms"
diff --git a/feature/auth/twofa/src/main/kotlin/com/meloda/app/fast/auth/twofa/model/TwoFaScreenState.kt b/feature/auth/twofa/src/main/kotlin/com/meloda/app/fast/auth/twofa/model/TwoFaScreenState.kt
index 04456d85..fb90afdb 100644
--- a/feature/auth/twofa/src/main/kotlin/com/meloda/app/fast/auth/twofa/model/TwoFaScreenState.kt
+++ b/feature/auth/twofa/src/main/kotlin/com/meloda/app/fast/auth/twofa/model/TwoFaScreenState.kt
@@ -9,7 +9,6 @@ data class TwoFaScreenState(
val canResendSms: Boolean,
val codeError: String?,
val delayTime: Int,
- val isNeedToOpenLogin: Boolean,
val phoneMask: String
) {
@@ -21,7 +20,6 @@ data class TwoFaScreenState(
canResendSms = false,
codeError = null,
delayTime = 0,
- isNeedToOpenLogin = false,
phoneMask = ""
)
}
diff --git a/feature/auth/twofa/src/main/kotlin/com/meloda/app/fast/auth/twofa/model/TwoFaUiAction.kt b/feature/auth/twofa/src/main/kotlin/com/meloda/app/fast/auth/twofa/model/TwoFaUiAction.kt
deleted file mode 100644
index 6205be5e..00000000
--- a/feature/auth/twofa/src/main/kotlin/com/meloda/app/fast/auth/twofa/model/TwoFaUiAction.kt
+++ /dev/null
@@ -1,6 +0,0 @@
-package com.meloda.app.fast.auth.twofa.model
-
-sealed class TwoFaUiAction {
- data class CodeResult(val code: String) : TwoFaUiAction()
- data object BackClicked : TwoFaUiAction()
-}
diff --git a/feature/auth/twofa/src/main/kotlin/com/meloda/app/fast/auth/twofa/navigation/TwoFaRoute.kt b/feature/auth/twofa/src/main/kotlin/com/meloda/app/fast/auth/twofa/navigation/TwoFaRoute.kt
index 8ad0740e..8c4bfe9a 100644
--- a/feature/auth/twofa/src/main/kotlin/com/meloda/app/fast/auth/twofa/navigation/TwoFaRoute.kt
+++ b/feature/auth/twofa/src/main/kotlin/com/meloda/app/fast/auth/twofa/navigation/TwoFaRoute.kt
@@ -6,7 +6,6 @@ import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.composable
import androidx.navigation.toRoute
import com.meloda.app.fast.auth.twofa.model.TwoFaArguments
-import com.meloda.app.fast.auth.twofa.model.TwoFaUiAction
import com.meloda.app.fast.auth.twofa.presentation.TwoFaScreen
import com.meloda.app.fast.common.customNavType
import kotlinx.serialization.Serializable
@@ -28,12 +27,8 @@ fun NavGraphBuilder.twoFaRoute(
) {
composable(typeMap = TwoFa.typeMap) {
TwoFaScreen(
- onAction = { action ->
- when (action) {
- TwoFaUiAction.BackClicked -> onBack()
- is TwoFaUiAction.CodeResult -> onResult(action.code)
- }
- }
+ onBack = onBack,
+ onCodeResult = onResult
)
}
}
@@ -42,8 +37,10 @@ fun NavController.navigateToTwoFa(arguments: TwoFaArguments) {
this.navigate(TwoFa(arguments))
}
-fun NavController.setTwoFaResult(code: String) {
+fun NavController.setTwoFaResult(code: String?) {
this.currentBackStackEntry
?.savedStateHandle
?.set("twofacode", code)
}
+
+
diff --git a/feature/auth/twofa/src/main/kotlin/com/meloda/app/fast/auth/twofa/presentation/TwoFaScreen.kt b/feature/auth/twofa/src/main/kotlin/com/meloda/app/fast/auth/twofa/presentation/TwoFaScreen.kt
index 7723d441..35344f1d 100644
--- a/feature/auth/twofa/src/main/kotlin/com/meloda/app/fast/auth/twofa/presentation/TwoFaScreen.kt
+++ b/feature/auth/twofa/src/main/kotlin/com/meloda/app/fast/auth/twofa/presentation/TwoFaScreen.kt
@@ -26,6 +26,7 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
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
@@ -43,7 +44,6 @@ import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.meloda.app.fast.auth.twofa.TwoFaViewModel
import com.meloda.app.fast.auth.twofa.TwoFaViewModelImpl
-import com.meloda.app.fast.auth.twofa.model.TwoFaUiAction
import com.meloda.app.fast.common.UiText
import com.meloda.app.fast.designsystem.MaterialDialog
import com.meloda.app.fast.designsystem.TextFieldErrorText
@@ -51,17 +51,18 @@ import com.meloda.app.fast.designsystem.getString
import org.koin.androidx.compose.koinViewModel
import com.meloda.app.fast.designsystem.R as UiR
-private typealias OnAction = (TwoFaUiAction) -> Unit
-
@Composable
fun TwoFaScreen(
- onAction: OnAction,
+ onBack: () -> Unit,
+ onCodeResult: (code: String) -> Unit,
viewModel: TwoFaViewModel = koinViewModel(),
) {
val focusManager = LocalFocusManager.current
val screenState by viewModel.screenState.collectAsStateWithLifecycle()
+ val isNeedToOpenLogin by viewModel.isNeedToOpenLogin.collectAsStateWithLifecycle()
+
var confirmedExit by rememberSaveable {
mutableStateOf(false)
}
@@ -70,8 +71,10 @@ fun TwoFaScreen(
mutableStateOf(false)
}
- if (confirmedExit) {
- onAction(TwoFaUiAction.BackClicked)
+ LaunchedEffect(confirmedExit) {
+ if (confirmedExit) {
+ onBack()
+ }
}
BackHandler(enabled = !confirmedExit) {
@@ -93,17 +96,22 @@ fun TwoFaScreen(
)
}
- if (screenState.isNeedToOpenLogin) {
- viewModel.onNavigatedToLogin()
+ LaunchedEffect(isNeedToOpenLogin) {
+ if (isNeedToOpenLogin) {
+ viewModel.onNavigatedToLogin()
- val code = screenState.twoFaCode
- if (code == null) {
- onAction(TwoFaUiAction.BackClicked)
- } else {
- onAction(TwoFaUiAction.CodeResult(code = code))
+ val code = screenState.twoFaCode
+ if (code == null) {
+ onBack()
+ } else {
+ onCodeResult(code)
+ }
}
}
+ var code by remember { mutableStateOf(TextFieldValue(screenState.twoFaCode.orEmpty())) }
+ val codeError = screenState.codeError
+
Scaffold { padding ->
Column(
modifier = Modifier
@@ -113,7 +121,7 @@ fun TwoFaScreen(
verticalArrangement = Arrangement.SpaceBetween
) {
ExtendedFloatingActionButton(
- onClick = { onAction(TwoFaUiAction.BackClicked) },
+ onClick = onBack,
text = {
Text(
text = "Cancel",
@@ -155,9 +163,6 @@ fun TwoFaScreen(
}
Spacer(modifier = Modifier.height(10.dp))
- var code by remember { mutableStateOf(TextFieldValue(screenState.twoFaCode.orEmpty())) }
- val codeError = screenState.codeError
-
TextField(
value = code,
onValueChange = { newText ->
diff --git a/feature/conversations/src/main/kotlin/com/meloda/app/fast/conversations/ConversationsViewModel.kt b/feature/conversations/src/main/kotlin/com/meloda/app/fast/conversations/ConversationsViewModel.kt
index 4b3d923f..175d9615 100644
--- a/feature/conversations/src/main/kotlin/com/meloda/app/fast/conversations/ConversationsViewModel.kt
+++ b/feature/conversations/src/main/kotlin/com/meloda/app/fast/conversations/ConversationsViewModel.kt
@@ -222,7 +222,7 @@ class ConversationsViewModelImpl(
private fun loadConversations(
offset: Int = currentOffset.value
) {
- conversationsUseCase.getConversations(count = 30, offset = offset).listenValue { state ->
+ conversationsUseCase.getConversations(count = LOAD_COUNT, offset = offset).listenValue { state ->
state.processState(
error = { error ->
when (error) {
@@ -247,7 +247,7 @@ class ConversationsViewModelImpl(
}
},
success = { response ->
- val itemsCountSufficient = response.size == 30
+ val itemsCountSufficient = response.size == LOAD_COUNT
canPaginate.setValue { itemsCountSufficient }
val paginationExhausted = !itemsCountSufficient &&
@@ -621,5 +621,9 @@ class ConversationsViewModelImpl(
old.copy(conversations = uiConversations)
}
}
+
+ companion object {
+ const val LOAD_COUNT = 30
+ }
}
diff --git a/feature/conversations/src/main/kotlin/com/meloda/app/fast/conversations/presentation/ConversationItem.kt b/feature/conversations/src/main/kotlin/com/meloda/app/fast/conversations/presentation/ConversationItem.kt
index 23d28ef6..7407a97f 100644
--- a/feature/conversations/src/main/kotlin/com/meloda/app/fast/conversations/presentation/ConversationItem.kt
+++ b/feature/conversations/src/main/kotlin/com/meloda/app/fast/conversations/presentation/ConversationItem.kt
@@ -47,6 +47,7 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@@ -299,7 +300,35 @@ fun ConversationItem(
LocalContentAlpha(alpha = ContentAlpha.medium) {
Text(
modifier = Modifier.weight(1f),
- text = conversation.message,
+ text = kotlin.run {
+ val builder =
+ AnnotatedString.Builder(conversation.message.text)
+
+ conversation.message.spanStyles.map { spanStyleRange ->
+ val updatedSpanStyle =
+ if (spanStyleRange.item.color == Color.Red) {
+ spanStyleRange.item.copy(color = MaterialTheme.colorScheme.primary)
+ } else {
+ spanStyleRange.item
+ }
+
+ builder.addStyle(
+ style = updatedSpanStyle,
+ start = spanStyleRange.start,
+ end = spanStyleRange.end
+ )
+ }
+
+ conversation.message.paragraphStyles.forEach { style ->
+ builder.addStyle(
+ style = style.item,
+ start = style.start,
+ end = style.end
+ )
+ }
+
+ builder.toAnnotatedString()
+ },
minLines = 1,
maxLines = maxLines,
style = MaterialTheme.typography.bodyLarge,
diff --git a/feature/conversations/src/main/kotlin/com/meloda/app/fast/conversations/util/ConversationDomainMapper.kt b/feature/conversations/src/main/kotlin/com/meloda/app/fast/conversations/util/ConversationDomainMapper.kt
index cab80f36..a6d9d8f6 100644
--- a/feature/conversations/src/main/kotlin/com/meloda/app/fast/conversations/util/ConversationDomainMapper.kt
+++ b/feature/conversations/src/main/kotlin/com/meloda/app/fast/conversations/util/ConversationDomainMapper.kt
@@ -611,6 +611,7 @@ private fun getAttachmentIconByType(attachmentType: AttachmentType): UiImage? {
AttachmentType.AUDIO_PLAYLIST -> null
AttachmentType.PODCAST -> null
AttachmentType.NARRATIVE -> null
+ AttachmentType.ARTICLE -> null
}?.let(UiImage::Resource)
}
@@ -660,18 +661,14 @@ private fun getTextWithVisualizedMentions(
var currentIndex = 0
val replacements = mutableListOf>()
- // TODO: 25/04/2024, Danil Nikolaev: check why not working ([id279494346|@iworld2rist] да убери ты Елену Шлипс от меня)
val result = regex.replace(originalText) { matchResult ->
val idPrefix = matchResult.groups[1]?.value.orEmpty()
val startIndex = matchResult.range.first
val endIndex = matchResult.range.last
val id = matchResult.groups[2]?.value ?: ""
- val text = matchResult.groups[3]?.value ?: ""
- val replaced =
- text.substring(startIndex, endIndex + 1)
- .replace("[$idPrefix$id|$text]", text)
+ val replaced = matchResult.groups[3]?.value.orEmpty()
val indexRange =
(startIndex + currentIndex)..startIndex + currentIndex + replaced.length
@@ -757,6 +754,7 @@ private fun getAttachmentUiText(
AttachmentType.AUDIO_PLAYLIST -> UiR.string.message_attachments_audio_playlist
AttachmentType.PODCAST -> UiR.string.message_attachments_podcast
AttachmentType.NARRATIVE -> UiR.string.message_attachments_narrative
+ AttachmentType.ARTICLE -> UiR.string.message_attachments_article
}.let(UiText::Resource)
}
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 77b81e3a..882404c3 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -5,6 +5,7 @@ eithernet = "1.9.0"
haze = "0.7.3"
kotlin = "2.0.0"
ksp = "2.0.0-1.0.22"
+vkompose = "0.5.4-k2"
accompanist = "0.34.0"
coil = "2.6.0"
@@ -28,6 +29,7 @@ espressoCore = "3.6.1"
appcompat = "1.7.0"
androidx-navigation = "2.8.0-beta05"
serialization = "1.7.1"
+rebugger = "1.0.0-rc03"
[libraries]
accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanist" }
@@ -80,6 +82,8 @@ appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "a
androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "androidx-navigation" }
kotlin-serialization = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization" }
+rebugger = { module = "io.github.theapache64:rebugger-android", version.ref = "rebugger" }
+
[bundles]
compose = [
"compose-material3",
@@ -100,3 +104,4 @@ com-google-devtools-ksp = { id = "com.google.devtools.ksp", version.ref = "ksp"
org-jetbrains-kotlin-plugin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" }
android-library = { id = "com.android.library", version.ref = "agp" }
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
+com-vk-vkompose = { id = "com.vk.vkompose", version.ref = "vkompose" }