forked from melod1n/fast-messenger
support for articles; ui & ux & logic fixes for 2fa and captcha screens; fix mentions;
This commit is contained in:
@@ -151,7 +151,7 @@ dependencies {
|
|||||||
// Coil for Compose
|
// Coil for Compose
|
||||||
implementation(libs.coil.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.test.manifest)
|
||||||
debugImplementation(libs.compose.ui.tooling)
|
debugImplementation(libs.compose.ui.tooling)
|
||||||
|
|
||||||
|
|||||||
@@ -40,12 +40,15 @@ class OAuthRepositoryImpl(
|
|||||||
requireNotNull(result.error)
|
requireNotNull(result.error)
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> throw IllegalStateException("Unknown result")
|
is ApiResult.Failure.ApiFailure -> TODO()
|
||||||
|
|
||||||
// is ApiResult.Failure.ApiFailure -> TODO()
|
is ApiResult.Failure.NetworkFailure -> {
|
||||||
// is ApiResult.Failure.HttpFailure -> TODO()
|
// TODO: 13/07/2024, Danil Nikolaev: implement showing network error
|
||||||
// is ApiResult.Failure.NetworkFailure -> TODO()
|
TODO()
|
||||||
// is ApiResult.Failure.UnknownFailure -> TODO()
|
}
|
||||||
|
is ApiResult.Failure.UnknownFailure -> TODO()
|
||||||
|
|
||||||
|
else -> throw IllegalStateException("Unknown result")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -104,6 +104,7 @@
|
|||||||
<string name="message_attachments_audio_playlist">Playlist</string>
|
<string name="message_attachments_audio_playlist">Playlist</string>
|
||||||
<string name="message_attachments_podcast">Podcast</string>
|
<string name="message_attachments_podcast">Podcast</string>
|
||||||
<string name="message_attachments_narrative">Narrative</string>
|
<string name="message_attachments_narrative">Narrative</string>
|
||||||
|
<string name="message_attachments_article">Article</string>
|
||||||
|
|
||||||
<string name="chat_interaction_uploading_file">Uploading file</string>
|
<string name="chat_interaction_uploading_file">Uploading file</string>
|
||||||
<string name="chat_interaction_uploading_photo">Uploading photo</string>
|
<string name="chat_interaction_uploading_photo">Uploading photo</string>
|
||||||
|
|||||||
@@ -32,6 +32,9 @@ dependencies {
|
|||||||
implementation(libs.moshi.kotlin)
|
implementation(libs.moshi.kotlin)
|
||||||
ksp(libs.moshi.kotlin.codegen)
|
ksp(libs.moshi.kotlin.codegen)
|
||||||
|
|
||||||
|
implementation(platform(libs.compose.bom))
|
||||||
|
implementation(libs.bundles.compose)
|
||||||
|
|
||||||
implementation(libs.room.ktx)
|
implementation(libs.room.ktx)
|
||||||
implementation(libs.room.runtime)
|
implementation(libs.room.runtime)
|
||||||
ksp(libs.room.compiler)
|
ksp(libs.room.compiler)
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
package com.meloda.app.fast.model
|
package com.meloda.app.fast.model
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
|
||||||
|
@Immutable
|
||||||
sealed class BaseError {
|
sealed class BaseError {
|
||||||
|
|
||||||
data object SessionExpired : BaseError()
|
data object SessionExpired : BaseError()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,8 @@ enum class AttachmentType(var value: String) {
|
|||||||
ARTIST("artist"),
|
ARTIST("artist"),
|
||||||
AUDIO_PLAYLIST("audio_playlist"),
|
AUDIO_PLAYLIST("audio_playlist"),
|
||||||
PODCAST("podcast"),
|
PODCAST("podcast"),
|
||||||
NARRATIVE("narrative");
|
NARRATIVE("narrative"),
|
||||||
|
ARTICLE("article");
|
||||||
|
|
||||||
fun isMultiple(): Boolean = this in listOf(PHOTO, VIDEO, AUDIO, FILE)
|
fun isMultiple(): Boolean = this in listOf(PHOTO, VIDEO, AUDIO, FILE)
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
+3
-1
@@ -31,7 +31,8 @@ data class VkAttachmentItemData(
|
|||||||
@Json(name = "audios") val audios: List<VkAudioData>?,
|
@Json(name = "audios") val audios: List<VkAudioData>?,
|
||||||
@Json(name = "audio_playlist") val audioPlaylist: VkAudioPlaylistData?,
|
@Json(name = "audio_playlist") val audioPlaylist: VkAudioPlaylistData?,
|
||||||
@Json(name = "podcast") val podcast: VkPodcastData?,
|
@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)) {
|
fun toDomain(): VkAttachment = when (AttachmentType.parse(type)) {
|
||||||
AttachmentType.UNKNOWN -> VkUnknownAttachment
|
AttachmentType.UNKNOWN -> VkUnknownAttachment
|
||||||
@@ -58,5 +59,6 @@ data class VkAttachmentItemData(
|
|||||||
AttachmentType.AUDIO_PLAYLIST -> audioPlaylist?.toDomain()
|
AttachmentType.AUDIO_PLAYLIST -> audioPlaylist?.toDomain()
|
||||||
AttachmentType.PODCAST -> podcast?.toDomain()
|
AttachmentType.PODCAST -> podcast?.toDomain()
|
||||||
AttachmentType.NARRATIVE -> narrative?.toDomain()
|
AttachmentType.NARRATIVE -> narrative?.toDomain()
|
||||||
|
AttachmentType.ARTICLE -> article?.toDomain()
|
||||||
} ?: VkUnknownAttachment
|
} ?: VkUnknownAttachment
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,11 +10,11 @@ data class VkGroupData(
|
|||||||
@Json(name = "id") val id: Int,
|
@Json(name = "id") val id: Int,
|
||||||
@Json(name = "name") val name: String,
|
@Json(name = "name") val name: String,
|
||||||
@Json(name = "screen_name") val screenName: 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 = "type") val type: String,
|
||||||
@Json(name = "is_admin") val isAdmin: Int,
|
@Json(name = "is_admin") val isAdmin: Int?,
|
||||||
@Json(name = "is_member") val isMember: Int,
|
@Json(name = "is_member") val isMember: Int?,
|
||||||
@Json(name = "is_advertiser") val isAdvertiser: Int,
|
@Json(name = "is_advertiser") val isAdvertiser: Int?,
|
||||||
@Json(name = "photo_50") val photo50: String?,
|
@Json(name = "photo_50") val photo50: String?,
|
||||||
@Json(name = "photo_100") val photo100: String?,
|
@Json(name = "photo_100") val photo100: String?,
|
||||||
@Json(name = "photo_200") val photo200: String?,
|
@Json(name = "photo_200") val photo200: String?,
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -75,7 +75,7 @@ data class InvalidCredentialsError(
|
|||||||
)
|
)
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class WrongTwoFaCode(
|
data class WrongTwoFaCodeError(
|
||||||
@Json(name = "error") override val error: String, // "invalid_request"
|
@Json(name = "error") override val error: String, // "invalid_request"
|
||||||
@Json(name = "error_description") override val errorDescription: String,
|
@Json(name = "error_description") override val errorDescription: String,
|
||||||
@Json(name = "error_type") override val errorType: String // "wrong_otp"
|
@Json(name = "error_type") override val errorType: String // "wrong_otp"
|
||||||
@@ -86,7 +86,7 @@ data class WrongTwoFaCode(
|
|||||||
)
|
)
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class WrongTwoFaCodeFormat(
|
data class WrongTwoFaCodeFormatError(
|
||||||
@Json(name = "error") override val error: String, // "invalid_request"
|
@Json(name = "error") override val error: String, // "invalid_request"
|
||||||
@Json(name = "error_description") override val errorDescription: String,
|
@Json(name = "error_description") override val errorDescription: String,
|
||||||
@Json(name = "error_type") override val errorType: String // "otp_format_is_incorrect"
|
@Json(name = "error_type") override val errorType: String // "otp_format_is_incorrect"
|
||||||
@@ -96,6 +96,17 @@ data class WrongTwoFaCodeFormat(
|
|||||||
errorType = errorType
|
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) {
|
fun OAuthError.toDomain(): OAuthErrorDomain? = when (this) {
|
||||||
is ValidationRequiredError -> {
|
is ValidationRequiredError -> {
|
||||||
OAuthErrorDomain.ValidationRequiredError(
|
OAuthErrorDomain.ValidationRequiredError(
|
||||||
@@ -129,13 +140,17 @@ fun OAuthError.toDomain(): OAuthErrorDomain? = when (this) {
|
|||||||
OAuthErrorDomain.InvalidCredentialsError
|
OAuthErrorDomain.InvalidCredentialsError
|
||||||
}
|
}
|
||||||
|
|
||||||
is WrongTwoFaCode -> {
|
is WrongTwoFaCodeError -> {
|
||||||
OAuthErrorDomain.WrongTwoFaCode
|
OAuthErrorDomain.WrongTwoFaCode
|
||||||
}
|
}
|
||||||
|
|
||||||
is WrongTwoFaCodeFormat -> {
|
is WrongTwoFaCodeFormatError -> {
|
||||||
OAuthErrorDomain.WrongTwoFaCodeFormat
|
OAuthErrorDomain.WrongTwoFaCodeFormat
|
||||||
}
|
}
|
||||||
|
|
||||||
|
is TooManyTriesError -> {
|
||||||
|
OAuthErrorDomain.TooManyTriesError
|
||||||
|
}
|
||||||
|
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,5 +27,7 @@ sealed class OAuthErrorDomain {
|
|||||||
data object InvalidCredentialsError : OAuthErrorDomain()
|
data object InvalidCredentialsError : OAuthErrorDomain()
|
||||||
data object WrongTwoFaCode : OAuthErrorDomain()
|
data object WrongTwoFaCode : OAuthErrorDomain()
|
||||||
data object WrongTwoFaCodeFormat : OAuthErrorDomain()
|
data object WrongTwoFaCodeFormat : OAuthErrorDomain()
|
||||||
|
data object TooManyTriesError: OAuthErrorDomain()
|
||||||
|
|
||||||
data object UnknownError : OAuthErrorDomain()
|
data object UnknownError : OAuthErrorDomain()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -110,6 +110,11 @@ internal class ResultCall<R : Any, E : OAuthError>(
|
|||||||
.fromJson(errorBodyString.orEmpty()) ?: return
|
.fromJson(errorBodyString.orEmpty()) ?: return
|
||||||
|
|
||||||
val error: OAuthError? = when (baseError.error) {
|
val error: OAuthError? = when (baseError.error) {
|
||||||
|
"9;Flood control" -> {
|
||||||
|
moshi.adapter(TooManyTriesError::class.java)
|
||||||
|
.fromJson(errorBodyString.orEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
"invalid_client" -> {
|
"invalid_client" -> {
|
||||||
moshi.adapter(InvalidCredentialsError::class.java)
|
moshi.adapter(InvalidCredentialsError::class.java)
|
||||||
.fromJson(errorBodyString.orEmpty())
|
.fromJson(errorBodyString.orEmpty())
|
||||||
@@ -123,12 +128,12 @@ internal class ResultCall<R : Any, E : OAuthError>(
|
|||||||
"invalid_request" -> {
|
"invalid_request" -> {
|
||||||
when (val type = baseError.errorType) {
|
when (val type = baseError.errorType) {
|
||||||
"wrong_otp" -> {
|
"wrong_otp" -> {
|
||||||
moshi.adapter(WrongTwoFaCode::class.java)
|
moshi.adapter(WrongTwoFaCodeError::class.java)
|
||||||
.fromJson(errorBodyString.orEmpty())
|
.fromJson(errorBodyString.orEmpty())
|
||||||
}
|
}
|
||||||
|
|
||||||
"otp_format_is_incorrect" -> {
|
"otp_format_is_incorrect" -> {
|
||||||
moshi.adapter(WrongTwoFaCodeFormat::class.java)
|
moshi.adapter(WrongTwoFaCodeFormatError::class.java)
|
||||||
.fromJson(errorBodyString.orEmpty())
|
.fromJson(errorBodyString.orEmpty())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -51,10 +51,12 @@ object VkOAuthErrors {
|
|||||||
const val NEED_CAPTCHA = "need_captcha"
|
const val NEED_CAPTCHA = "need_captcha"
|
||||||
const val INVALID_CLIENT = "invalid_client"
|
const val INVALID_CLIENT = "invalid_client"
|
||||||
const val INVALID_REQUEST = "invalid_request"
|
const val INVALID_REQUEST = "invalid_request"
|
||||||
|
const val FLOOD_CONTROL = "9;Flood control"
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
object VkErrorTypes {
|
object VkErrorTypes {
|
||||||
const val WRONG_OTP_FORMAT = "otp_format_is_incorrect"
|
const val WRONG_OTP_FORMAT = "otp_format_is_incorrect"
|
||||||
const val WRONG_OTP = "wrong_otp"
|
const val WRONG_OTP = "wrong_otp"
|
||||||
|
const val PASSWORD_BRUTEFORCE_ATTEMPT = "password_bruteforce_attempt"
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-14
@@ -2,7 +2,6 @@ package com.meloda.app.fast.auth.captcha
|
|||||||
|
|
||||||
import androidx.lifecycle.SavedStateHandle
|
import androidx.lifecycle.SavedStateHandle
|
||||||
import androidx.lifecycle.ViewModel
|
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.model.CaptchaScreenState
|
||||||
import com.meloda.app.fast.auth.captcha.navigation.Captcha
|
import com.meloda.app.fast.auth.captcha.navigation.Captcha
|
||||||
import com.meloda.app.fast.auth.captcha.validation.CaptchaValidator
|
import com.meloda.app.fast.auth.captcha.validation.CaptchaValidator
|
||||||
@@ -13,16 +12,14 @@ import kotlinx.coroutines.flow.StateFlow
|
|||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
|
|
||||||
interface CaptchaViewModel {
|
interface CaptchaViewModel {
|
||||||
|
|
||||||
val screenState: StateFlow<CaptchaScreenState>
|
val screenState: StateFlow<CaptchaScreenState>
|
||||||
|
val isNeedToOpenLogin: StateFlow<Boolean>
|
||||||
|
|
||||||
fun onCodeInputChanged(newCode: String)
|
fun onCodeInputChanged(newCode: String)
|
||||||
|
|
||||||
fun onTextFieldDoneClicked()
|
fun onTextFieldDoneClicked()
|
||||||
fun onDoneButtonClicked()
|
fun onDoneButtonClicked()
|
||||||
|
|
||||||
fun setArguments(arguments: CaptchaArguments)
|
|
||||||
|
|
||||||
fun onNavigatedToLogin()
|
fun onNavigatedToLogin()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,6 +29,7 @@ class CaptchaViewModelImpl(
|
|||||||
) : CaptchaViewModel, ViewModel() {
|
) : CaptchaViewModel, ViewModel() {
|
||||||
|
|
||||||
override val screenState = MutableStateFlow(CaptchaScreenState.EMPTY)
|
override val screenState = MutableStateFlow(CaptchaScreenState.EMPTY)
|
||||||
|
override val isNeedToOpenLogin = MutableStateFlow(false)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
val arguments = Captcha.from(savedStateHandle).arguments
|
val arguments = Captcha.from(savedStateHandle).arguments
|
||||||
@@ -57,20 +55,12 @@ class CaptchaViewModelImpl(
|
|||||||
override fun onDoneButtonClicked() {
|
override fun onDoneButtonClicked() {
|
||||||
if (!processValidation()) return
|
if (!processValidation()) return
|
||||||
|
|
||||||
screenState.updateValue(screenState.value.copy(isNeedToOpenLogin = true))
|
isNeedToOpenLogin.update { true }
|
||||||
}
|
|
||||||
|
|
||||||
override fun setArguments(arguments: CaptchaArguments) {
|
|
||||||
// screenState.updateValue(
|
|
||||||
// screenState.value.copy(
|
|
||||||
// captchaSid = arguments.captchaSid,
|
|
||||||
// captchaImage = arguments.captchaImage
|
|
||||||
// )
|
|
||||||
// )
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNavigatedToLogin() {
|
override fun onNavigatedToLogin() {
|
||||||
screenState.updateValue(CaptchaScreenState.EMPTY)
|
screenState.updateValue(CaptchaScreenState.EMPTY)
|
||||||
|
isNeedToOpenLogin.update { false }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun processValidation(): Boolean {
|
private fun processValidation(): Boolean {
|
||||||
|
|||||||
+2
-4
@@ -4,8 +4,7 @@ data class CaptchaScreenState(
|
|||||||
val captchaSid: String,
|
val captchaSid: String,
|
||||||
val captchaImage: String,
|
val captchaImage: String,
|
||||||
val captchaCode: String,
|
val captchaCode: String,
|
||||||
val codeError: Boolean,
|
val codeError: Boolean
|
||||||
val isNeedToOpenLogin: Boolean
|
|
||||||
) {
|
) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -13,8 +12,7 @@ data class CaptchaScreenState(
|
|||||||
captchaSid = "",
|
captchaSid = "",
|
||||||
captchaImage = "",
|
captchaImage = "",
|
||||||
captchaCode = "",
|
captchaCode = "",
|
||||||
codeError = false,
|
codeError = false
|
||||||
isNeedToOpenLogin = false
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -41,7 +41,7 @@ fun NavController.navigateToCaptcha(arguments: CaptchaArguments) {
|
|||||||
this.navigate(Captcha(arguments))
|
this.navigate(Captcha(arguments))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun NavController.setCaptchaResult(code: String) {
|
fun NavController.setCaptchaResult(code: String?) {
|
||||||
this.currentBackStackEntry
|
this.currentBackStackEntry
|
||||||
?.savedStateHandle
|
?.savedStateHandle
|
||||||
?.set("captchacode", code)
|
?.set("captchacode", code)
|
||||||
|
|||||||
+11
-5
@@ -27,6 +27,7 @@ import androidx.compose.material3.Scaffold
|
|||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextField
|
import androidx.compose.material3.TextField
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
@@ -61,6 +62,7 @@ fun CaptchaScreen(
|
|||||||
viewModel: CaptchaViewModel = koinViewModel<CaptchaViewModelImpl>()
|
viewModel: CaptchaViewModel = koinViewModel<CaptchaViewModelImpl>()
|
||||||
) {
|
) {
|
||||||
val screenState by viewModel.screenState.collectAsStateWithLifecycle()
|
val screenState by viewModel.screenState.collectAsStateWithLifecycle()
|
||||||
|
val isNeedToOpenLogin by viewModel.isNeedToOpenLogin.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
var confirmedExit by rememberSaveable {
|
var confirmedExit by rememberSaveable {
|
||||||
mutableStateOf(false)
|
mutableStateOf(false)
|
||||||
@@ -70,8 +72,10 @@ fun CaptchaScreen(
|
|||||||
mutableStateOf(false)
|
mutableStateOf(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (confirmedExit) {
|
LaunchedEffect(confirmedExit) {
|
||||||
onBack()
|
if (confirmedExit) {
|
||||||
|
onBack()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
BackHandler(enabled = !confirmedExit) {
|
BackHandler(enabled = !confirmedExit) {
|
||||||
@@ -93,9 +97,11 @@ fun CaptchaScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (screenState.isNeedToOpenLogin) {
|
LaunchedEffect(isNeedToOpenLogin) {
|
||||||
viewModel.onNavigatedToLogin()
|
if (isNeedToOpenLogin) {
|
||||||
onResult(screenState.captchaCode)
|
viewModel.onNavigatedToLogin()
|
||||||
|
onResult(screenState.captchaCode)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val focusManager = LocalFocusManager.current
|
val focusManager = LocalFocusManager.current
|
||||||
|
|||||||
@@ -89,4 +89,6 @@ dependencies {
|
|||||||
implementation(libs.eithernet)
|
implementation(libs.eithernet)
|
||||||
implementation(libs.androidx.navigation.compose)
|
implementation(libs.androidx.navigation.compose)
|
||||||
implementation(libs.kotlin.serialization)
|
implementation(libs.kotlin.serialization)
|
||||||
|
|
||||||
|
implementation(libs.rebugger)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,9 +15,11 @@ import com.meloda.app.fast.data.db.AccountsRepository
|
|||||||
import com.meloda.app.fast.data.processState
|
import com.meloda.app.fast.data.processState
|
||||||
import com.meloda.app.fast.model.database.AccountEntity
|
import com.meloda.app.fast.model.database.AccountEntity
|
||||||
import com.meloda.app.fast.network.OAuthErrorDomain
|
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.LoginScreenState
|
||||||
import com.meloda.fast.auth.login.model.LoginTwoFaArguments
|
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.model.LoginValidationResult
|
||||||
import com.meloda.fast.auth.login.validation.LoginValidator
|
import com.meloda.fast.auth.login.validation.LoginValidator
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -27,10 +29,19 @@ import kotlinx.coroutines.flow.SharingStarted
|
|||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
interface LoginViewModel {
|
interface LoginViewModel {
|
||||||
val screenState: StateFlow<LoginScreenState>
|
val screenState: StateFlow<LoginScreenState>
|
||||||
|
val loginError: StateFlow<LoginError?>
|
||||||
|
|
||||||
|
val twoFaCode: StateFlow<String?>
|
||||||
|
val twoFaArguments: StateFlow<LoginTwoFaArguments?>
|
||||||
|
val captchaCode: StateFlow<String?>
|
||||||
|
val captchaArguments: StateFlow<LoginCaptchaArguments?>
|
||||||
|
val userBannedArguments: StateFlow<LoginUserBannedArguments?>
|
||||||
|
val isNeedToOpenMain: StateFlow<Boolean>
|
||||||
|
|
||||||
fun onPasswordVisibilityButtonClicked()
|
fun onPasswordVisibilityButtonClicked()
|
||||||
|
|
||||||
@@ -56,10 +67,18 @@ class LoginViewModelImpl(
|
|||||||
private val oAuthUseCase: OAuthUseCase,
|
private val oAuthUseCase: OAuthUseCase,
|
||||||
private val usersUseCase: UsersUseCase,
|
private val usersUseCase: UsersUseCase,
|
||||||
private val accountsRepository: AccountsRepository,
|
private val accountsRepository: AccountsRepository,
|
||||||
private val loginValidator: LoginValidator,
|
private val loginValidator: LoginValidator
|
||||||
) : ViewModel(), LoginViewModel {
|
) : ViewModel(), LoginViewModel {
|
||||||
|
|
||||||
override val screenState = MutableStateFlow(LoginScreenState.EMPTY)
|
override val screenState = MutableStateFlow(LoginScreenState.EMPTY)
|
||||||
|
override val loginError = MutableStateFlow<LoginError?>(null)
|
||||||
|
|
||||||
|
override val twoFaCode = MutableStateFlow<String?>(null)
|
||||||
|
override val twoFaArguments = MutableStateFlow<LoginTwoFaArguments?>(null)
|
||||||
|
override val captchaCode = MutableStateFlow<String?>(null)
|
||||||
|
override val captchaArguments = MutableStateFlow<LoginCaptchaArguments?>(null)
|
||||||
|
override val userBannedArguments = MutableStateFlow<LoginUserBannedArguments?>(null)
|
||||||
|
override val isNeedToOpenMain = MutableStateFlow(false)
|
||||||
|
|
||||||
private val validationState: StateFlow<List<LoginValidationResult>> =
|
private val validationState: StateFlow<List<LoginValidationResult>> =
|
||||||
screenState.map(loginValidator::validate)
|
screenState.map(loginValidator::validate)
|
||||||
@@ -86,37 +105,38 @@ class LoginViewModelImpl(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onSignInButtonClicked() {
|
override fun onSignInButtonClicked() {
|
||||||
|
if (screenState.value.isLoading) return
|
||||||
login()
|
login()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onErrorDialogDismissed() {
|
override fun onErrorDialogDismissed() {
|
||||||
screenState.setValue { old -> old.copy(error = null) }
|
loginError.update { null }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNavigatedToMain() {
|
override fun onNavigatedToMain() {
|
||||||
screenState.setValue { old -> old.copy(isNeedToNavigateToMain = false) }
|
isNeedToOpenMain.update { false }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNavigatedToUserBanned() {
|
override fun onNavigatedToUserBanned() {
|
||||||
screenState.setValue { old -> old.copy(userBannedArguments = null) }
|
userBannedArguments.update { null }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNavigatedToCaptcha() {
|
override fun onNavigatedToCaptcha() {
|
||||||
screenState.setValue { old -> old.copy(captchaArguments = null) }
|
captchaArguments.update { null }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNavigatedToTwoFa() {
|
override fun onNavigatedToTwoFa() {
|
||||||
screenState.setValue { old -> old.copy(twoFaArguments = null) }
|
twoFaArguments.update { null }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onTwoFaCodeReceived(code: String) {
|
override fun onTwoFaCodeReceived(code: String) {
|
||||||
screenState.setValue { old -> old.copy(validationCode = code) }
|
twoFaCode.update { code }
|
||||||
|
|
||||||
login()
|
login()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCaptchaCodeReceived(code: String) {
|
override fun onCaptchaCodeReceived(code: String) {
|
||||||
screenState.setValue { old -> old.copy(captchaCode = code) }
|
captchaCode.update { code }
|
||||||
|
|
||||||
login()
|
login()
|
||||||
}
|
}
|
||||||
@@ -149,7 +169,8 @@ class LoginViewModelImpl(
|
|||||||
accountsRepository.storeAccounts(listOf(currentAccount))
|
accountsRepository.storeAccounts(listOf(currentAccount))
|
||||||
|
|
||||||
delay(350)
|
delay(350)
|
||||||
screenState.setValue { old -> old.copy(isNeedToNavigateToMain = true) }
|
|
||||||
|
isNeedToOpenMain.update { true }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -163,7 +184,10 @@ class LoginViewModelImpl(
|
|||||||
|
|
||||||
Log.d(
|
Log.d(
|
||||||
"LoginViewModel",
|
"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()
|
processValidation()
|
||||||
@@ -173,14 +197,17 @@ class LoginViewModelImpl(
|
|||||||
login = currentState.login,
|
login = currentState.login,
|
||||||
password = currentState.password,
|
password = currentState.password,
|
||||||
forceSms = forceSms,
|
forceSms = forceSms,
|
||||||
twoFaCode = currentState.validationCode,
|
twoFaCode = twoFaCode.value,
|
||||||
captchaSid = currentState.captchaArguments?.captchaSid,
|
captchaSid = captchaArguments.value?.captchaSid,
|
||||||
captchaKey = currentState.captchaCode
|
captchaKey = captchaCode.value
|
||||||
).listenValue { state ->
|
).listenValue { state ->
|
||||||
state.processState(
|
state.processState(
|
||||||
error = { error ->
|
error = { error ->
|
||||||
Log.d("LoginViewModelImpl", "login: error: $error")
|
Log.d("LoginViewModelImpl", "login: error: $error")
|
||||||
|
|
||||||
|
twoFaCode.update { null }
|
||||||
|
captchaCode.update { null }
|
||||||
|
|
||||||
parseError(error)
|
parseError(error)
|
||||||
},
|
},
|
||||||
success = { response ->
|
success = { response ->
|
||||||
@@ -213,20 +240,20 @@ class LoginViewModelImpl(
|
|||||||
|
|
||||||
accountsRepository.storeAccounts(listOf(currentAccount))
|
accountsRepository.storeAccounts(listOf(currentAccount))
|
||||||
|
|
||||||
|
captchaArguments.update { null }
|
||||||
|
captchaCode.update { null }
|
||||||
|
|
||||||
|
twoFaArguments.update { null }
|
||||||
|
twoFaCode.update { null }
|
||||||
|
|
||||||
screenState.setValue { old ->
|
screenState.setValue { old ->
|
||||||
old.copy(
|
old.copy(
|
||||||
captchaArguments = null,
|
|
||||||
captchaCode = null,
|
|
||||||
validationSid = null,
|
|
||||||
validationCode = null,
|
|
||||||
twoFaArguments = null,
|
|
||||||
|
|
||||||
login = "",
|
login = "",
|
||||||
password = "",
|
password = "",
|
||||||
|
|
||||||
isNeedToNavigateToMain = true
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isNeedToOpenMain.update { true }
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
screenState.emit(screenState.value.copy(isLoading = state.isLoading()))
|
screenState.emit(screenState.value.copy(isLoading = state.isLoading()))
|
||||||
@@ -238,7 +265,7 @@ class LoginViewModelImpl(
|
|||||||
is State.Error.OAuthError -> {
|
is State.Error.OAuthError -> {
|
||||||
when (val error = stateError.error) {
|
when (val error = stateError.error) {
|
||||||
is OAuthErrorDomain.ValidationRequiredError -> {
|
is OAuthErrorDomain.ValidationRequiredError -> {
|
||||||
val twoFaArguments = LoginTwoFaArguments(
|
val arguments = LoginTwoFaArguments(
|
||||||
validationSid = error.validationSid,
|
validationSid = error.validationSid,
|
||||||
redirectUri = error.redirectUri,
|
redirectUri = error.redirectUri,
|
||||||
phoneMask = error.phoneMask,
|
phoneMask = error.phoneMask,
|
||||||
@@ -246,25 +273,49 @@ class LoginViewModelImpl(
|
|||||||
canResendSms = error.validationResend == "sms",
|
canResendSms = error.validationResend == "sms",
|
||||||
wrongCodeError = null
|
wrongCodeError = null
|
||||||
)
|
)
|
||||||
screenState.setValue { old -> old.copy(twoFaArguments = twoFaArguments) }
|
twoFaArguments.update { arguments }
|
||||||
true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
is OAuthErrorDomain.CaptchaRequiredError -> {
|
is OAuthErrorDomain.CaptchaRequiredError -> {
|
||||||
val captchaArguments = CaptchaArguments(
|
val arguments = LoginCaptchaArguments(
|
||||||
captchaSid = error.captchaSid,
|
captchaSid = error.captchaSid,
|
||||||
captchaImage = error.captchaImageUrl
|
captchaImage = error.captchaImageUrl
|
||||||
)
|
)
|
||||||
screenState.setValue { old -> old.copy(captchaArguments = captchaArguments) }
|
captchaArguments.update { arguments }
|
||||||
true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
OAuthErrorDomain.InvalidCredentialsError -> TODO()
|
OAuthErrorDomain.InvalidCredentialsError -> {
|
||||||
is OAuthErrorDomain.UserBannedError -> TODO()
|
loginError.update { LoginError.WrongCredentials }
|
||||||
OAuthErrorDomain.WrongTwoFaCode -> TODO()
|
}
|
||||||
OAuthErrorDomain.WrongTwoFaCodeFormat -> TODO()
|
|
||||||
OAuthErrorDomain.UnknownError -> TODO()
|
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
|
else -> false
|
||||||
|
|||||||
@@ -44,6 +44,10 @@ class OAuthUseCaseImpl(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
VkOAuthErrors.FLOOD_CONTROL -> {
|
||||||
|
State.Error.OAuthError(OAuthErrorDomain.TooManyTriesError)
|
||||||
|
}
|
||||||
|
|
||||||
VkOAuthErrors.NEED_VALIDATION -> {
|
VkOAuthErrors.NEED_VALIDATION -> {
|
||||||
if (response.banInfo != null) {
|
if (response.banInfo != null) {
|
||||||
val info = requireNotNull(response.banInfo)
|
val info = requireNotNull(response.banInfo)
|
||||||
|
|||||||
+1
-1
@@ -6,7 +6,7 @@ import kotlinx.serialization.Serializable
|
|||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class CaptchaArguments(
|
data class LoginCaptchaArguments(
|
||||||
val captchaSid: String,
|
val captchaSid: String,
|
||||||
val captchaImage: String
|
val captchaImage: String
|
||||||
) : Parcelable
|
) : Parcelable
|
||||||
@@ -1,6 +1,12 @@
|
|||||||
package com.meloda.fast.auth.login.model
|
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()
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-20
@@ -2,43 +2,24 @@ package com.meloda.fast.auth.login.model
|
|||||||
|
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
|
|
||||||
// TODO: 04/05/2024, Danil Nikolaev: simplify
|
|
||||||
@Immutable
|
@Immutable
|
||||||
data class LoginScreenState(
|
data class LoginScreenState(
|
||||||
val login: String,
|
val login: String,
|
||||||
val password: String,
|
val password: String,
|
||||||
val captchaCode: String?,
|
|
||||||
val validationSid: String?,
|
|
||||||
val validationCode: String?,
|
|
||||||
val isLoading: Boolean,
|
val isLoading: Boolean,
|
||||||
val loginError: Boolean,
|
val loginError: Boolean,
|
||||||
val passwordError: Boolean,
|
val passwordError: Boolean,
|
||||||
val passwordVisible: Boolean,
|
val passwordVisible: Boolean,
|
||||||
val copiedCode: String?,
|
|
||||||
val isNeedToNavigateToMain: Boolean,
|
|
||||||
val twoFaArguments: LoginTwoFaArguments?,
|
|
||||||
val captchaArguments: CaptchaArguments?,
|
|
||||||
val userBannedArguments: UserBannedArguments?,
|
|
||||||
val error: LoginError?,
|
|
||||||
) {
|
) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val EMPTY = LoginScreenState(
|
val EMPTY = LoginScreenState(
|
||||||
login = "",
|
login = "",
|
||||||
password = "",
|
password = "",
|
||||||
captchaCode = null,
|
|
||||||
validationSid = null,
|
|
||||||
validationCode = null,
|
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
loginError = false,
|
loginError = false,
|
||||||
passwordError = false,
|
passwordError = false,
|
||||||
passwordVisible = false,
|
passwordVisible = false
|
||||||
copiedCode = null,
|
|
||||||
isNeedToNavigateToMain = false,
|
|
||||||
twoFaArguments = null,
|
|
||||||
captchaArguments = null,
|
|
||||||
userBannedArguments = null,
|
|
||||||
error = null,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -6,7 +6,7 @@ import kotlinx.serialization.Serializable
|
|||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class UserBannedArguments(
|
data class LoginUserBannedArguments(
|
||||||
val name: String,
|
val name: String,
|
||||||
val message: String,
|
val message: String,
|
||||||
val restoreUrl: String,
|
val restoreUrl: String,
|
||||||
+20
-6
@@ -1,5 +1,6 @@
|
|||||||
package com.meloda.fast.auth.login.navigation
|
package com.meloda.fast.auth.login.navigation
|
||||||
|
|
||||||
|
import androidx.navigation.NavBackStackEntry
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import androidx.navigation.NavGraphBuilder
|
import androidx.navigation.NavGraphBuilder
|
||||||
import androidx.navigation.compose.composable
|
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.app.fast.model.BaseError
|
||||||
import com.meloda.fast.auth.login.LoginViewModel
|
import com.meloda.fast.auth.login.LoginViewModel
|
||||||
import com.meloda.fast.auth.login.LoginViewModelImpl
|
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.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.LoginScreen
|
||||||
import com.meloda.fast.auth.login.presentation.LogoScreen
|
import com.meloda.fast.auth.login.presentation.LogoScreen
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
@@ -22,16 +23,19 @@ object Logo
|
|||||||
|
|
||||||
fun NavGraphBuilder.loginRoute(
|
fun NavGraphBuilder.loginRoute(
|
||||||
onError: (BaseError) -> Unit,
|
onError: (BaseError) -> Unit,
|
||||||
onNavigateToCaptcha: (CaptchaArguments) -> Unit,
|
onNavigateToCaptcha: (LoginCaptchaArguments) -> Unit,
|
||||||
onNavigateToTwoFa: (LoginTwoFaArguments) -> Unit,
|
onNavigateToTwoFa: (LoginTwoFaArguments) -> Unit,
|
||||||
onNavigateToMain: () -> Unit,
|
onNavigateToMain: () -> Unit,
|
||||||
onNavigateToUserBanned: (UserBannedArguments) -> Unit,
|
onNavigateToUserBanned: (LoginUserBannedArguments) -> Unit,
|
||||||
onNavigateToCredentials: () -> Unit,
|
onNavigateToCredentials: () -> Unit,
|
||||||
navController: NavController
|
navController: NavController
|
||||||
) {
|
) {
|
||||||
composable<Login> {
|
composable<Login> { backStackEntry ->
|
||||||
val viewModel: LoginViewModel =
|
val viewModel: LoginViewModel =
|
||||||
it.sharedViewModel<LoginViewModelImpl>(navController = navController)
|
backStackEntry.sharedViewModel<LoginViewModelImpl>(navController = navController)
|
||||||
|
|
||||||
|
val twoFaCode = backStackEntry.getTwoFaResult()
|
||||||
|
val captchaCode = backStackEntry.getCaptchaResult()
|
||||||
|
|
||||||
LoginScreen(
|
LoginScreen(
|
||||||
onError = onError,
|
onError = onError,
|
||||||
@@ -39,6 +43,8 @@ fun NavGraphBuilder.loginRoute(
|
|||||||
onNavigateToMain = onNavigateToMain,
|
onNavigateToMain = onNavigateToMain,
|
||||||
onNavigateToCaptcha = onNavigateToCaptcha,
|
onNavigateToCaptcha = onNavigateToCaptcha,
|
||||||
onNavigateToTwoFa = onNavigateToTwoFa,
|
onNavigateToTwoFa = onNavigateToTwoFa,
|
||||||
|
twoFaCode = twoFaCode,
|
||||||
|
captchaCode = captchaCode,
|
||||||
viewModel = viewModel
|
viewModel = viewModel
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -54,3 +60,11 @@ fun NavGraphBuilder.loginRoute(
|
|||||||
fun NavController.navigateToLogin() {
|
fun NavController.navigateToLogin() {
|
||||||
this.navigate(route = Login)
|
this.navigate(route = Login)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun NavBackStackEntry.getTwoFaResult(): String? {
|
||||||
|
return savedStateHandle["twofacode"]
|
||||||
|
}
|
||||||
|
|
||||||
|
fun NavBackStackEntry.getCaptchaResult(): String? {
|
||||||
|
return savedStateHandle["captchacode"]
|
||||||
|
}
|
||||||
|
|||||||
+128
-37
@@ -23,6 +23,7 @@ import androidx.compose.material3.Scaffold
|
|||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextField
|
import androidx.compose.material3.TextField
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
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.app.fast.model.BaseError
|
||||||
import com.meloda.fast.auth.login.LoginViewModel
|
import com.meloda.fast.auth.login.LoginViewModel
|
||||||
import com.meloda.fast.auth.login.LoginViewModelImpl
|
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.LoginError
|
||||||
import com.meloda.fast.auth.login.model.LoginTwoFaArguments
|
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 org.koin.androidx.compose.koinViewModel
|
||||||
import com.meloda.app.fast.designsystem.R as UiR
|
import com.meloda.app.fast.designsystem.R as UiR
|
||||||
|
|
||||||
@@ -68,49 +70,64 @@ import com.meloda.app.fast.designsystem.R as UiR
|
|||||||
@Composable
|
@Composable
|
||||||
fun LoginScreen(
|
fun LoginScreen(
|
||||||
onError: (BaseError) -> Unit,
|
onError: (BaseError) -> Unit,
|
||||||
onNavigateToUserBanned: (UserBannedArguments) -> Unit,
|
onNavigateToUserBanned: (LoginUserBannedArguments) -> Unit,
|
||||||
onNavigateToMain: () -> Unit,
|
onNavigateToMain: () -> Unit,
|
||||||
onNavigateToCaptcha: (CaptchaArguments) -> Unit,
|
onNavigateToCaptcha: (LoginCaptchaArguments) -> Unit,
|
||||||
onNavigateToTwoFa: (LoginTwoFaArguments) -> Unit,
|
onNavigateToTwoFa: (LoginTwoFaArguments) -> Unit,
|
||||||
|
twoFaCode: String?,
|
||||||
|
captchaCode: String?,
|
||||||
viewModel: LoginViewModel = koinViewModel<LoginViewModelImpl>()
|
viewModel: LoginViewModel = koinViewModel<LoginViewModelImpl>()
|
||||||
) {
|
) {
|
||||||
val screenState by viewModel.screenState.collectAsStateWithLifecycle()
|
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) {
|
LaunchedEffect(isNeedToOpenMain) {
|
||||||
viewModel.onNavigatedToMain()
|
if (isNeedToOpenMain) {
|
||||||
onNavigateToMain()
|
viewModel.onNavigatedToMain()
|
||||||
|
onNavigateToMain()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
screenState.userBannedArguments?.let { arguments ->
|
LaunchedEffect(userBannedArguments) {
|
||||||
viewModel.onNavigatedToUserBanned()
|
userBannedArguments?.let { arguments ->
|
||||||
onNavigateToUserBanned(arguments)
|
viewModel.onNavigatedToUserBanned()
|
||||||
|
onNavigateToUserBanned(arguments)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
screenState.captchaArguments?.let { arguments ->
|
LaunchedEffect(captchaArguments) {
|
||||||
viewModel.onNavigatedToCaptcha()
|
captchaArguments?.let { arguments ->
|
||||||
onNavigateToCaptcha(arguments)
|
viewModel.onNavigatedToCaptcha()
|
||||||
|
onNavigateToCaptcha(arguments)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
screenState.twoFaArguments?.let { arguments ->
|
LaunchedEffect(twoFaArguments) {
|
||||||
viewModel.onNavigatedToTwoFa()
|
twoFaArguments?.let { arguments ->
|
||||||
onNavigateToTwoFa(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 focusManager = LocalFocusManager.current
|
||||||
val (loginFocusable, passwordFocusable) = FocusRequester.createRefs()
|
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)) }
|
var loginText by remember { mutableStateOf(TextFieldValue(screenState.login)) }
|
||||||
val showLoginError = screenState.loginError
|
val showLoginError = screenState.loginError
|
||||||
|
|
||||||
@@ -165,8 +182,14 @@ fun LoginScreen(
|
|||||||
.height(58.dp)
|
.height(58.dp)
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clip(RoundedCornerShape(10.dp))
|
.clip(RoundedCornerShape(10.dp))
|
||||||
.handleEnterKey(loginFieldTabClick::invoke)
|
.handleEnterKey {
|
||||||
.handleTabKey(loginFieldTabClick::invoke)
|
passwordFocusable.requestFocus()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
.handleTabKey {
|
||||||
|
passwordFocusable.requestFocus()
|
||||||
|
true
|
||||||
|
}
|
||||||
.focusRequester(loginFocusable)
|
.focusRequester(loginFocusable)
|
||||||
.connectNode(handler = autoFillEmailHandler)
|
.connectNode(handler = autoFillEmailHandler)
|
||||||
.defaultFocusChangeAutoFill(handler = autoFillEmailHandler),
|
.defaultFocusChangeAutoFill(handler = autoFillEmailHandler),
|
||||||
@@ -213,7 +236,8 @@ fun LoginScreen(
|
|||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clip(RoundedCornerShape(10.dp))
|
.clip(RoundedCornerShape(10.dp))
|
||||||
.handleEnterKey {
|
.handleEnterKey {
|
||||||
goButtonClickAction.invoke()
|
focusManager.clearFocus()
|
||||||
|
viewModel.onSignInButtonClicked()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
.focusRequester(passwordFocusable)
|
.focusRequester(passwordFocusable)
|
||||||
@@ -261,7 +285,10 @@ fun LoginScreen(
|
|||||||
keyboardType = KeyboardType.Password
|
keyboardType = KeyboardType.Password
|
||||||
),
|
),
|
||||||
keyboardActions = KeyboardActions(
|
keyboardActions = KeyboardActions(
|
||||||
onGo = { goButtonClickAction.invoke() }
|
onGo = {
|
||||||
|
focusManager.clearFocus()
|
||||||
|
viewModel.onSignInButtonClicked()
|
||||||
|
}
|
||||||
),
|
),
|
||||||
isError = showPasswordError,
|
isError = showPasswordError,
|
||||||
visualTransformation = if (screenState.passwordVisible) {
|
visualTransformation = if (screenState.passwordVisible) {
|
||||||
@@ -282,7 +309,10 @@ fun LoginScreen(
|
|||||||
) {
|
) {
|
||||||
|
|
||||||
FloatingActionButton(
|
FloatingActionButton(
|
||||||
onClick = goButtonClickAction::invoke,
|
onClick = {
|
||||||
|
focusManager.clearFocus()
|
||||||
|
viewModel.onSignInButtonClicked()
|
||||||
|
},
|
||||||
containerColor = MaterialTheme.colorScheme.secondaryContainer,
|
containerColor = MaterialTheme.colorScheme.secondaryContainer,
|
||||||
modifier = Modifier.testTag("Sign in button")
|
modifier = Modifier.testTag("Sign in button")
|
||||||
) {
|
) {
|
||||||
@@ -306,7 +336,31 @@ fun LoginScreen(
|
|||||||
|
|
||||||
HandleError(
|
HandleError(
|
||||||
onDismiss = viewModel::onErrorDialogDismissed,
|
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?,
|
error: LoginError?,
|
||||||
) {
|
) {
|
||||||
when (error) {
|
when (error) {
|
||||||
LoginError.WrongCredentials -> {
|
null -> Unit
|
||||||
|
|
||||||
|
LoginError.Unknown -> {
|
||||||
MaterialDialog(
|
MaterialDialog(
|
||||||
onDismissAction = onDismiss,
|
onDismissAction = onDismiss,
|
||||||
title = UiText.Simple("Error"),
|
title = UiText.Simple("Error"),
|
||||||
text = UiText.Simple("Wrong login or password"),
|
text = UiText.Simple("Unknown error."),
|
||||||
confirmText = UiText.Resource(UiR.string.ok)
|
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)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-4
@@ -19,6 +19,7 @@ import androidx.compose.material3.MaterialTheme
|
|||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
@@ -40,11 +41,13 @@ fun LogoScreen(
|
|||||||
onShowCredentials: () -> Unit,
|
onShowCredentials: () -> Unit,
|
||||||
viewModel: LoginViewModel = koinViewModel<LoginViewModelImpl>()
|
viewModel: LoginViewModel = koinViewModel<LoginViewModelImpl>()
|
||||||
) {
|
) {
|
||||||
val screenState by viewModel.screenState.collectAsStateWithLifecycle()
|
val isNeedToOpenMain by viewModel.isNeedToOpenMain.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
if (screenState.isNeedToNavigateToMain) {
|
LaunchedEffect(isNeedToOpenMain) {
|
||||||
viewModel.onNavigatedToMain()
|
if (isNeedToOpenMain) {
|
||||||
onNavigateToMain()
|
viewModel.onNavigatedToMain()
|
||||||
|
onNavigateToMain()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Scaffold { padding ->
|
Scaffold { padding ->
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package com.meloda.app.fast.auth
|
|||||||
|
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import androidx.navigation.NavGraphBuilder
|
import androidx.navigation.NavGraphBuilder
|
||||||
import androidx.navigation.NavHostController
|
|
||||||
import androidx.navigation.navigation
|
import androidx.navigation.navigation
|
||||||
import com.meloda.app.fast.auth.captcha.model.CaptchaArguments
|
import com.meloda.app.fast.auth.captcha.model.CaptchaArguments
|
||||||
import com.meloda.app.fast.auth.captcha.navigation.captchaRoute
|
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.loginRoute
|
||||||
import com.meloda.fast.auth.login.navigation.navigateToLogin
|
import com.meloda.fast.auth.login.navigation.navigateToLogin
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
import java.net.URLEncoder
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
object AuthGraph
|
object AuthGraph
|
||||||
@@ -27,7 +27,7 @@ object AuthGraph
|
|||||||
fun NavGraphBuilder.authNavGraph(
|
fun NavGraphBuilder.authNavGraph(
|
||||||
onError: (BaseError) -> Unit,
|
onError: (BaseError) -> Unit,
|
||||||
onNavigateToMain: () -> Unit,
|
onNavigateToMain: () -> Unit,
|
||||||
navController: NavHostController
|
navController: NavController
|
||||||
) {
|
) {
|
||||||
navigation<AuthGraph>(
|
navigation<AuthGraph>(
|
||||||
startDestination = Logo
|
startDestination = Logo
|
||||||
@@ -46,7 +46,7 @@ fun NavGraphBuilder.authNavGraph(
|
|||||||
navController.navigateToTwoFa(
|
navController.navigateToTwoFa(
|
||||||
TwoFaArguments(
|
TwoFaArguments(
|
||||||
validationSid = arguments.validationSid,
|
validationSid = arguments.validationSid,
|
||||||
redirectUri = arguments.redirectUri,
|
redirectUri = URLEncoder.encode(arguments.redirectUri, "utf-8"),
|
||||||
phoneMask = arguments.phoneMask,
|
phoneMask = arguments.phoneMask,
|
||||||
validationType = arguments.validationType,
|
validationType = arguments.validationType,
|
||||||
canResendSms = arguments.canResendSms,
|
canResendSms = arguments.canResendSms,
|
||||||
@@ -70,7 +70,10 @@ fun NavGraphBuilder.authNavGraph(
|
|||||||
)
|
)
|
||||||
|
|
||||||
twoFaRoute(
|
twoFaRoute(
|
||||||
onBack = navController::navigateUp,
|
onBack = {
|
||||||
|
navController.navigateUp()
|
||||||
|
navController.setTwoFaResult(null)
|
||||||
|
},
|
||||||
onResult = { code ->
|
onResult = { code ->
|
||||||
navController.popBackStack()
|
navController.popBackStack()
|
||||||
navController.setTwoFaResult(code)
|
navController.setTwoFaResult(code)
|
||||||
@@ -78,7 +81,10 @@ fun NavGraphBuilder.authNavGraph(
|
|||||||
)
|
)
|
||||||
|
|
||||||
captchaRoute(
|
captchaRoute(
|
||||||
onBack = navController::navigateUp,
|
onBack = {
|
||||||
|
navController.navigateUp()
|
||||||
|
navController.setCaptchaResult(null)
|
||||||
|
},
|
||||||
onResult = { code ->
|
onResult = { code ->
|
||||||
navController.popBackStack()
|
navController.popBackStack()
|
||||||
navController.setCaptchaResult(code)
|
navController.setCaptchaResult(code)
|
||||||
|
|||||||
+12
-26
@@ -1,10 +1,8 @@
|
|||||||
package com.meloda.app.fast.auth.twofa
|
package com.meloda.app.fast.auth.twofa
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.lifecycle.SavedStateHandle
|
import androidx.lifecycle.SavedStateHandle
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
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.TwoFaScreenState
|
||||||
import com.meloda.app.fast.auth.twofa.model.TwoFaValidationType
|
import com.meloda.app.fast.auth.twofa.model.TwoFaValidationType
|
||||||
import com.meloda.app.fast.auth.twofa.navigation.TwoFa
|
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.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
|
||||||
@@ -27,6 +26,8 @@ interface TwoFaViewModel {
|
|||||||
|
|
||||||
val screenState: StateFlow<TwoFaScreenState>
|
val screenState: StateFlow<TwoFaScreenState>
|
||||||
|
|
||||||
|
val isNeedToOpenLogin: StateFlow<Boolean>
|
||||||
|
|
||||||
fun onCodeInputChanged(newCode: String)
|
fun onCodeInputChanged(newCode: String)
|
||||||
|
|
||||||
fun onBackButtonClicked()
|
fun onBackButtonClicked()
|
||||||
@@ -36,8 +37,6 @@ interface TwoFaViewModel {
|
|||||||
fun onDoneButtonClicked()
|
fun onDoneButtonClicked()
|
||||||
|
|
||||||
fun onNavigatedToLogin()
|
fun onNavigatedToLogin()
|
||||||
|
|
||||||
fun setArguments(arguments: TwoFaArguments)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class TwoFaViewModelImpl(
|
class TwoFaViewModelImpl(
|
||||||
@@ -48,6 +47,8 @@ class TwoFaViewModelImpl(
|
|||||||
|
|
||||||
override val screenState = MutableStateFlow(TwoFaScreenState.EMPTY)
|
override val screenState = MutableStateFlow(TwoFaScreenState.EMPTY)
|
||||||
|
|
||||||
|
override val isNeedToOpenLogin = MutableStateFlow(false)
|
||||||
|
|
||||||
private var delayJob: Job? = null
|
private var delayJob: Job? = null
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@@ -88,12 +89,8 @@ class TwoFaViewModelImpl(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onCancelButtonClicked() {
|
override fun onCancelButtonClicked() {
|
||||||
screenState.updateValue(
|
screenState.setValue { old -> old.copy(twoFaCode = null) }
|
||||||
screenState.value.copy(
|
isNeedToOpenLogin.update { true }
|
||||||
twoFaCode = null,
|
|
||||||
isNeedToOpenLogin = true
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRequestSmsButtonClicked() {
|
override fun onRequestSmsButtonClicked() {
|
||||||
@@ -107,25 +104,12 @@ class TwoFaViewModelImpl(
|
|||||||
override fun onDoneButtonClicked() {
|
override fun onDoneButtonClicked() {
|
||||||
if (!processValidation()) return
|
if (!processValidation()) return
|
||||||
|
|
||||||
screenState.updateValue(screenState.value.copy(isNeedToOpenLogin = true))
|
isNeedToOpenLogin.update { true }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNavigatedToLogin() {
|
override fun onNavigatedToLogin() {
|
||||||
screenState.updateValue(TwoFaScreenState.EMPTY)
|
screenState.updateValue(TwoFaScreenState.EMPTY)
|
||||||
}
|
isNeedToOpenLogin.update { false }
|
||||||
|
|
||||||
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
|
|
||||||
// )
|
|
||||||
// )
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun processValidation(): Boolean {
|
private fun processValidation(): Boolean {
|
||||||
@@ -147,7 +131,9 @@ class TwoFaViewModelImpl(
|
|||||||
authUseCase.sendSms(validationSid)
|
authUseCase.sendSms(validationSid)
|
||||||
.listenValue { state ->
|
.listenValue { state ->
|
||||||
state.processState(
|
state.processState(
|
||||||
error = { error -> },
|
error = { error ->
|
||||||
|
|
||||||
|
},
|
||||||
success = { response ->
|
success = { response ->
|
||||||
val newValidationType = response.validationType
|
val newValidationType = response.validationType
|
||||||
val newCanResendSms = response.validationResend == "sms"
|
val newCanResendSms = response.validationResend == "sms"
|
||||||
|
|||||||
-2
@@ -9,7 +9,6 @@ data class TwoFaScreenState(
|
|||||||
val canResendSms: Boolean,
|
val canResendSms: Boolean,
|
||||||
val codeError: String?,
|
val codeError: String?,
|
||||||
val delayTime: Int,
|
val delayTime: Int,
|
||||||
val isNeedToOpenLogin: Boolean,
|
|
||||||
val phoneMask: String
|
val phoneMask: String
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@@ -21,7 +20,6 @@ data class TwoFaScreenState(
|
|||||||
canResendSms = false,
|
canResendSms = false,
|
||||||
codeError = null,
|
codeError = null,
|
||||||
delayTime = 0,
|
delayTime = 0,
|
||||||
isNeedToOpenLogin = false,
|
|
||||||
phoneMask = ""
|
phoneMask = ""
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
-6
@@ -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()
|
|
||||||
}
|
|
||||||
+5
-8
@@ -6,7 +6,6 @@ import androidx.navigation.NavGraphBuilder
|
|||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
import androidx.navigation.toRoute
|
import androidx.navigation.toRoute
|
||||||
import com.meloda.app.fast.auth.twofa.model.TwoFaArguments
|
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.auth.twofa.presentation.TwoFaScreen
|
||||||
import com.meloda.app.fast.common.customNavType
|
import com.meloda.app.fast.common.customNavType
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
@@ -28,12 +27,8 @@ fun NavGraphBuilder.twoFaRoute(
|
|||||||
) {
|
) {
|
||||||
composable<TwoFa>(typeMap = TwoFa.typeMap) {
|
composable<TwoFa>(typeMap = TwoFa.typeMap) {
|
||||||
TwoFaScreen(
|
TwoFaScreen(
|
||||||
onAction = { action ->
|
onBack = onBack,
|
||||||
when (action) {
|
onCodeResult = onResult
|
||||||
TwoFaUiAction.BackClicked -> onBack()
|
|
||||||
is TwoFaUiAction.CodeResult -> onResult(action.code)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -42,8 +37,10 @@ fun NavController.navigateToTwoFa(arguments: TwoFaArguments) {
|
|||||||
this.navigate(TwoFa(arguments))
|
this.navigate(TwoFa(arguments))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun NavController.setTwoFaResult(code: String) {
|
fun NavController.setTwoFaResult(code: String?) {
|
||||||
this.currentBackStackEntry
|
this.currentBackStackEntry
|
||||||
?.savedStateHandle
|
?.savedStateHandle
|
||||||
?.set("twofacode", code)
|
?.set("twofacode", code)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+22
-17
@@ -26,6 +26,7 @@ import androidx.compose.material3.Scaffold
|
|||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextField
|
import androidx.compose.material3.TextField
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
@@ -43,7 +44,6 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.meloda.app.fast.auth.twofa.TwoFaViewModel
|
import com.meloda.app.fast.auth.twofa.TwoFaViewModel
|
||||||
import com.meloda.app.fast.auth.twofa.TwoFaViewModelImpl
|
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.common.UiText
|
||||||
import com.meloda.app.fast.designsystem.MaterialDialog
|
import com.meloda.app.fast.designsystem.MaterialDialog
|
||||||
import com.meloda.app.fast.designsystem.TextFieldErrorText
|
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 org.koin.androidx.compose.koinViewModel
|
||||||
import com.meloda.app.fast.designsystem.R as UiR
|
import com.meloda.app.fast.designsystem.R as UiR
|
||||||
|
|
||||||
private typealias OnAction = (TwoFaUiAction) -> Unit
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TwoFaScreen(
|
fun TwoFaScreen(
|
||||||
onAction: OnAction,
|
onBack: () -> Unit,
|
||||||
|
onCodeResult: (code: String) -> Unit,
|
||||||
viewModel: TwoFaViewModel = koinViewModel<TwoFaViewModelImpl>(),
|
viewModel: TwoFaViewModel = koinViewModel<TwoFaViewModelImpl>(),
|
||||||
) {
|
) {
|
||||||
val focusManager = LocalFocusManager.current
|
val focusManager = LocalFocusManager.current
|
||||||
|
|
||||||
val screenState by viewModel.screenState.collectAsStateWithLifecycle()
|
val screenState by viewModel.screenState.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
|
val isNeedToOpenLogin by viewModel.isNeedToOpenLogin.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
var confirmedExit by rememberSaveable {
|
var confirmedExit by rememberSaveable {
|
||||||
mutableStateOf(false)
|
mutableStateOf(false)
|
||||||
}
|
}
|
||||||
@@ -70,8 +71,10 @@ fun TwoFaScreen(
|
|||||||
mutableStateOf(false)
|
mutableStateOf(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (confirmedExit) {
|
LaunchedEffect(confirmedExit) {
|
||||||
onAction(TwoFaUiAction.BackClicked)
|
if (confirmedExit) {
|
||||||
|
onBack()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
BackHandler(enabled = !confirmedExit) {
|
BackHandler(enabled = !confirmedExit) {
|
||||||
@@ -93,17 +96,22 @@ fun TwoFaScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (screenState.isNeedToOpenLogin) {
|
LaunchedEffect(isNeedToOpenLogin) {
|
||||||
viewModel.onNavigatedToLogin()
|
if (isNeedToOpenLogin) {
|
||||||
|
viewModel.onNavigatedToLogin()
|
||||||
|
|
||||||
val code = screenState.twoFaCode
|
val code = screenState.twoFaCode
|
||||||
if (code == null) {
|
if (code == null) {
|
||||||
onAction(TwoFaUiAction.BackClicked)
|
onBack()
|
||||||
} else {
|
} else {
|
||||||
onAction(TwoFaUiAction.CodeResult(code = code))
|
onCodeResult(code)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var code by remember { mutableStateOf(TextFieldValue(screenState.twoFaCode.orEmpty())) }
|
||||||
|
val codeError = screenState.codeError
|
||||||
|
|
||||||
Scaffold { padding ->
|
Scaffold { padding ->
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -113,7 +121,7 @@ fun TwoFaScreen(
|
|||||||
verticalArrangement = Arrangement.SpaceBetween
|
verticalArrangement = Arrangement.SpaceBetween
|
||||||
) {
|
) {
|
||||||
ExtendedFloatingActionButton(
|
ExtendedFloatingActionButton(
|
||||||
onClick = { onAction(TwoFaUiAction.BackClicked) },
|
onClick = onBack,
|
||||||
text = {
|
text = {
|
||||||
Text(
|
Text(
|
||||||
text = "Cancel",
|
text = "Cancel",
|
||||||
@@ -155,9 +163,6 @@ fun TwoFaScreen(
|
|||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.height(10.dp))
|
Spacer(modifier = Modifier.height(10.dp))
|
||||||
|
|
||||||
var code by remember { mutableStateOf(TextFieldValue(screenState.twoFaCode.orEmpty())) }
|
|
||||||
val codeError = screenState.codeError
|
|
||||||
|
|
||||||
TextField(
|
TextField(
|
||||||
value = code,
|
value = code,
|
||||||
onValueChange = { newText ->
|
onValueChange = { newText ->
|
||||||
|
|||||||
+6
-2
@@ -222,7 +222,7 @@ class ConversationsViewModelImpl(
|
|||||||
private fun loadConversations(
|
private fun loadConversations(
|
||||||
offset: Int = currentOffset.value
|
offset: Int = currentOffset.value
|
||||||
) {
|
) {
|
||||||
conversationsUseCase.getConversations(count = 30, offset = offset).listenValue { state ->
|
conversationsUseCase.getConversations(count = LOAD_COUNT, offset = offset).listenValue { state ->
|
||||||
state.processState(
|
state.processState(
|
||||||
error = { error ->
|
error = { error ->
|
||||||
when (error) {
|
when (error) {
|
||||||
@@ -247,7 +247,7 @@ class ConversationsViewModelImpl(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
success = { response ->
|
success = { response ->
|
||||||
val itemsCountSufficient = response.size == 30
|
val itemsCountSufficient = response.size == LOAD_COUNT
|
||||||
canPaginate.setValue { itemsCountSufficient }
|
canPaginate.setValue { itemsCountSufficient }
|
||||||
|
|
||||||
val paginationExhausted = !itemsCountSufficient &&
|
val paginationExhausted = !itemsCountSufficient &&
|
||||||
@@ -621,5 +621,9 @@ class ConversationsViewModelImpl(
|
|||||||
old.copy(conversations = uiConversations)
|
old.copy(conversations = uiConversations)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val LOAD_COUNT = 30
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+30
-1
@@ -47,6 +47,7 @@ import androidx.compose.ui.platform.LocalContext
|
|||||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||||
import androidx.compose.ui.res.colorResource
|
import androidx.compose.ui.res.colorResource
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
@@ -299,7 +300,35 @@ fun ConversationItem(
|
|||||||
LocalContentAlpha(alpha = ContentAlpha.medium) {
|
LocalContentAlpha(alpha = ContentAlpha.medium) {
|
||||||
Text(
|
Text(
|
||||||
modifier = Modifier.weight(1f),
|
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,
|
minLines = 1,
|
||||||
maxLines = maxLines,
|
maxLines = maxLines,
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
|||||||
+3
-5
@@ -611,6 +611,7 @@ private fun getAttachmentIconByType(attachmentType: AttachmentType): UiImage? {
|
|||||||
AttachmentType.AUDIO_PLAYLIST -> null
|
AttachmentType.AUDIO_PLAYLIST -> null
|
||||||
AttachmentType.PODCAST -> null
|
AttachmentType.PODCAST -> null
|
||||||
AttachmentType.NARRATIVE -> null
|
AttachmentType.NARRATIVE -> null
|
||||||
|
AttachmentType.ARTICLE -> null
|
||||||
}?.let(UiImage::Resource)
|
}?.let(UiImage::Resource)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -660,18 +661,14 @@ private fun getTextWithVisualizedMentions(
|
|||||||
var currentIndex = 0
|
var currentIndex = 0
|
||||||
val replacements = mutableListOf<Pair<IntRange, String>>()
|
val replacements = mutableListOf<Pair<IntRange, String>>()
|
||||||
|
|
||||||
// TODO: 25/04/2024, Danil Nikolaev: check why not working ([id279494346|@iworld2rist] да убери ты Елену Шлипс от меня)
|
|
||||||
val result = regex.replace(originalText) { matchResult ->
|
val result = regex.replace(originalText) { matchResult ->
|
||||||
val idPrefix = matchResult.groups[1]?.value.orEmpty()
|
val idPrefix = matchResult.groups[1]?.value.orEmpty()
|
||||||
val startIndex = matchResult.range.first
|
val startIndex = matchResult.range.first
|
||||||
val endIndex = matchResult.range.last
|
val endIndex = matchResult.range.last
|
||||||
|
|
||||||
val id = matchResult.groups[2]?.value ?: ""
|
val id = matchResult.groups[2]?.value ?: ""
|
||||||
val text = matchResult.groups[3]?.value ?: ""
|
|
||||||
|
|
||||||
val replaced =
|
val replaced = matchResult.groups[3]?.value.orEmpty()
|
||||||
text.substring(startIndex, endIndex + 1)
|
|
||||||
.replace("[$idPrefix$id|$text]", text)
|
|
||||||
|
|
||||||
val indexRange =
|
val indexRange =
|
||||||
(startIndex + currentIndex)..startIndex + currentIndex + replaced.length
|
(startIndex + currentIndex)..startIndex + currentIndex + replaced.length
|
||||||
@@ -757,6 +754,7 @@ private fun getAttachmentUiText(
|
|||||||
AttachmentType.AUDIO_PLAYLIST -> UiR.string.message_attachments_audio_playlist
|
AttachmentType.AUDIO_PLAYLIST -> UiR.string.message_attachments_audio_playlist
|
||||||
AttachmentType.PODCAST -> UiR.string.message_attachments_podcast
|
AttachmentType.PODCAST -> UiR.string.message_attachments_podcast
|
||||||
AttachmentType.NARRATIVE -> UiR.string.message_attachments_narrative
|
AttachmentType.NARRATIVE -> UiR.string.message_attachments_narrative
|
||||||
|
AttachmentType.ARTICLE -> UiR.string.message_attachments_article
|
||||||
}.let(UiText::Resource)
|
}.let(UiText::Resource)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ eithernet = "1.9.0"
|
|||||||
haze = "0.7.3"
|
haze = "0.7.3"
|
||||||
kotlin = "2.0.0"
|
kotlin = "2.0.0"
|
||||||
ksp = "2.0.0-1.0.22"
|
ksp = "2.0.0-1.0.22"
|
||||||
|
vkompose = "0.5.4-k2"
|
||||||
|
|
||||||
accompanist = "0.34.0"
|
accompanist = "0.34.0"
|
||||||
coil = "2.6.0"
|
coil = "2.6.0"
|
||||||
@@ -28,6 +29,7 @@ espressoCore = "3.6.1"
|
|||||||
appcompat = "1.7.0"
|
appcompat = "1.7.0"
|
||||||
androidx-navigation = "2.8.0-beta05"
|
androidx-navigation = "2.8.0-beta05"
|
||||||
serialization = "1.7.1"
|
serialization = "1.7.1"
|
||||||
|
rebugger = "1.0.0-rc03"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanist" }
|
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" }
|
androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "androidx-navigation" }
|
||||||
kotlin-serialization = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization" }
|
kotlin-serialization = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization" }
|
||||||
|
|
||||||
|
rebugger = { module = "io.github.theapache64:rebugger-android", version.ref = "rebugger" }
|
||||||
|
|
||||||
[bundles]
|
[bundles]
|
||||||
compose = [
|
compose = [
|
||||||
"compose-material3",
|
"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" }
|
org-jetbrains-kotlin-plugin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" }
|
||||||
android-library = { id = "com.android.library", version.ref = "agp" }
|
android-library = { id = "com.android.library", version.ref = "agp" }
|
||||||
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
|
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
|
||||||
|
com-vk-vkompose = { id = "com.vk.vkompose", version.ref = "vkompose" }
|
||||||
|
|||||||
Reference in New Issue
Block a user