forked from melod1n/fast-messenger
Update API version (#147)
* Bump VK Api version to 5.238 * Implemented new authorization flow (at the moment, without auto re-requesting token) * Add support for sticker pack preview attachments * Bump LongPoll to version 19 * Improved messages handling * Fixed coloring issues * Cache improvements * Archive screen with full functionality * Recomposition fixes * Markdown support for messages bubbles * Adjust app name font size based on screen width * Navigation related improvements * Add logout functionality
This commit is contained in:
@@ -59,23 +59,23 @@ fun NavGraphBuilder.authNavGraph(
|
||||
|
||||
validationScreen(
|
||||
onBack = {
|
||||
navController.navigateUp()
|
||||
navController.setValidationResult(null)
|
||||
navController.navigateUp()
|
||||
},
|
||||
onResult = { code ->
|
||||
navController.popBackStack()
|
||||
navController.setValidationResult(code)
|
||||
navController.popBackStack()
|
||||
}
|
||||
)
|
||||
|
||||
captchaScreen(
|
||||
onBack = {
|
||||
navController.navigateUp()
|
||||
navController.setCaptchaResult(null)
|
||||
navController.navigateUp()
|
||||
},
|
||||
onResult = { code ->
|
||||
navController.popBackStack()
|
||||
navController.setCaptchaResult(code)
|
||||
navController.popBackStack()
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
+1
-1
@@ -34,7 +34,7 @@ fun NavController.navigateToCaptcha(captchaImageUrl: String) {
|
||||
}
|
||||
|
||||
fun NavController.setCaptchaResult(code: String?) {
|
||||
this.currentBackStackEntry
|
||||
this.previousBackStackEntry
|
||||
?.savedStateHandle
|
||||
?.set("captcha_code", code)
|
||||
}
|
||||
|
||||
+7
-1
@@ -8,10 +8,13 @@ import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.ime
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.union
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
@@ -24,6 +27,7 @@ import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.ScaffoldDefaults
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextField
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -123,7 +127,9 @@ fun CaptchaScreen(
|
||||
|
||||
val focusManager = LocalFocusManager.current
|
||||
|
||||
Scaffold { padding ->
|
||||
Scaffold(
|
||||
contentWindowInsets = ScaffoldDefaults.contentWindowInsets.union(WindowInsets.ime)
|
||||
) { padding ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
|
||||
@@ -15,18 +15,21 @@ import dev.meloda.fast.common.LongPollController
|
||||
import dev.meloda.fast.common.VkConstants
|
||||
import dev.meloda.fast.common.extensions.listenValue
|
||||
import dev.meloda.fast.common.extensions.setValue
|
||||
import dev.meloda.fast.common.extensions.updateValue
|
||||
import dev.meloda.fast.common.model.LongPollState
|
||||
import dev.meloda.fast.data.State
|
||||
import dev.meloda.fast.data.UserConfig
|
||||
import dev.meloda.fast.data.api.auth.AuthRepository
|
||||
import dev.meloda.fast.data.db.AccountsRepository
|
||||
import dev.meloda.fast.data.processState
|
||||
import dev.meloda.fast.data.success
|
||||
import dev.meloda.fast.datastore.AppSettings
|
||||
import dev.meloda.fast.domain.LoadUserByIdUseCase
|
||||
import dev.meloda.fast.domain.OAuthUseCase
|
||||
import dev.meloda.fast.model.database.AccountEntity
|
||||
import dev.meloda.fast.network.OAuthErrorDomain
|
||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
@@ -39,13 +42,14 @@ interface LoginViewModel {
|
||||
val screenState: StateFlow<LoginScreenState>
|
||||
val loginDialog: StateFlow<LoginDialog?>
|
||||
|
||||
val validationCode: StateFlow<String?>
|
||||
val validationArguments: StateFlow<LoginValidationArguments?>
|
||||
val captchaCode: StateFlow<String?>
|
||||
val captchaArguments: StateFlow<CaptchaArguments?>
|
||||
val userBannedArguments: StateFlow<LoginUserBannedArguments?>
|
||||
val isNeedToOpenMain: StateFlow<Boolean>
|
||||
|
||||
val isNeedToClearCaptchaCode: StateFlow<Boolean>
|
||||
val isNeedToClearValidationCode: StateFlow<Boolean>
|
||||
|
||||
fun onDialogConfirmed(dialog: LoginDialog, bundle: Bundle)
|
||||
fun onDialogDismissed(dialog: LoginDialog)
|
||||
|
||||
@@ -63,14 +67,15 @@ interface LoginViewModel {
|
||||
fun onNavigatedToCaptcha()
|
||||
fun onNavigatedToValidation()
|
||||
|
||||
fun onValidationCodeReceived(code: String)
|
||||
fun onCaptchaCodeReceived(code: String)
|
||||
|
||||
fun onLogoLongClicked()
|
||||
fun onValidationCodeReceived(code: String?)
|
||||
fun onValidationCodeCleared()
|
||||
fun onCaptchaCodeReceived(code: String?)
|
||||
fun onCaptchaCodeCleared()
|
||||
}
|
||||
|
||||
class LoginViewModelImpl(
|
||||
private val oAuthUseCase: OAuthUseCase,
|
||||
private val authRepository: AuthRepository,
|
||||
private val loadUserByIdUseCase: LoadUserByIdUseCase,
|
||||
private val accountsRepository: AccountsRepository,
|
||||
private val loginValidator: LoginValidator,
|
||||
@@ -80,27 +85,41 @@ class LoginViewModelImpl(
|
||||
override val screenState = MutableStateFlow(LoginScreenState.EMPTY)
|
||||
override val loginDialog = MutableStateFlow<LoginDialog?>(null)
|
||||
|
||||
override val validationCode = MutableStateFlow<String?>(null)
|
||||
override val validationArguments = MutableStateFlow<LoginValidationArguments?>(null)
|
||||
override val captchaCode = MutableStateFlow<String?>(null)
|
||||
override val captchaArguments = MutableStateFlow<CaptchaArguments?>(null)
|
||||
override val userBannedArguments = MutableStateFlow<LoginUserBannedArguments?>(null)
|
||||
override val isNeedToOpenMain = MutableStateFlow(false)
|
||||
|
||||
override val isNeedToClearCaptchaCode = MutableStateFlow(false)
|
||||
override val isNeedToClearValidationCode = MutableStateFlow(false)
|
||||
|
||||
private val validationState: StateFlow<List<LoginValidationResult>> =
|
||||
screenState.map(loginValidator::validate)
|
||||
.stateIn(viewModelScope, SharingStarted.Eagerly, listOf(LoginValidationResult.Empty))
|
||||
|
||||
private val captchaSid = MutableStateFlow<String?>(null)
|
||||
private val captchaCode = MutableStateFlow<String?>(null)
|
||||
private val validationSid = MutableStateFlow<String?>(null)
|
||||
private val validationCode = MutableStateFlow<String?>(null)
|
||||
|
||||
init {
|
||||
captchaCode.listenValue(viewModelScope) {
|
||||
if (it != null) {
|
||||
login()
|
||||
}
|
||||
}
|
||||
validationCode.listenValue(viewModelScope) {
|
||||
if (it != null) {
|
||||
login()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDialogConfirmed(dialog: LoginDialog, bundle: Bundle) {
|
||||
onDialogDismissed(dialog)
|
||||
|
||||
when (dialog) {
|
||||
is LoginDialog.Error -> Unit
|
||||
|
||||
LoginDialog.FastAuth -> {
|
||||
val token = bundle.getString("token")?.trim() ?: return
|
||||
fastAuth(token)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,68 +180,20 @@ class LoginViewModelImpl(
|
||||
validationArguments.update { null }
|
||||
}
|
||||
|
||||
override fun onValidationCodeReceived(code: String) {
|
||||
override fun onValidationCodeReceived(code: String?) {
|
||||
validationCode.update { code }
|
||||
|
||||
login()
|
||||
}
|
||||
|
||||
override fun onCaptchaCodeReceived(code: String) {
|
||||
override fun onValidationCodeCleared() {
|
||||
isNeedToClearValidationCode.update { false }
|
||||
}
|
||||
|
||||
override fun onCaptchaCodeReceived(code: String?) {
|
||||
captchaCode.update { code }
|
||||
|
||||
login()
|
||||
}
|
||||
|
||||
override fun onLogoLongClicked() {
|
||||
loginDialog.setValue { LoginDialog.FastAuth }
|
||||
}
|
||||
|
||||
private fun fastAuth(token: String) {
|
||||
var currentAccount = AccountEntity(
|
||||
userId = -1,
|
||||
accessToken = token,
|
||||
fastToken = null,
|
||||
trustedHash = null
|
||||
).also { account ->
|
||||
UserConfig.currentUserId = account.userId
|
||||
UserConfig.userId = account.userId
|
||||
UserConfig.accessToken = account.accessToken
|
||||
UserConfig.fastToken = account.fastToken
|
||||
UserConfig.trustedHash = account.trustedHash
|
||||
}
|
||||
|
||||
loadUserByIdUseCase(
|
||||
userId = null,
|
||||
fields = VkConstants.USER_FIELDS,
|
||||
nomCase = null
|
||||
).listenValue(viewModelScope) { state ->
|
||||
state.processState(
|
||||
error = {
|
||||
UserConfig.currentUserId = -1
|
||||
UserConfig.userId = -1
|
||||
UserConfig.accessToken = ""
|
||||
|
||||
loginDialog.setValue { LoginDialog.Error() }
|
||||
},
|
||||
success = { response ->
|
||||
val actualUserId = requireNotNull(response).id
|
||||
|
||||
currentAccount = currentAccount.copy(userId = actualUserId)
|
||||
|
||||
UserConfig.userId = actualUserId
|
||||
UserConfig.currentUserId = actualUserId
|
||||
|
||||
startLongPoll()
|
||||
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
accountsRepository.storeAccounts(listOf(currentAccount))
|
||||
delay(350)
|
||||
isNeedToOpenMain.update { true }
|
||||
}
|
||||
}
|
||||
)
|
||||
screenState.setValue { old -> old.copy(isLoading = state.isLoading()) }
|
||||
}
|
||||
override fun onCaptchaCodeCleared() {
|
||||
isNeedToClearCaptchaCode.update { false }
|
||||
}
|
||||
|
||||
private fun login(forceSms: Boolean = false) {
|
||||
@@ -239,77 +210,120 @@ class LoginViewModelImpl(
|
||||
processValidation()
|
||||
if (!validationState.value.contains(LoginValidationResult.Valid)) return
|
||||
|
||||
oAuthUseCase.auth(
|
||||
screenState.updateValue { copy(isLoading = false) }
|
||||
|
||||
val currentValidationSid = validationSid.value
|
||||
val currentValidationCode = validationCode.value?.takeIf { currentValidationSid != null }
|
||||
val currentCaptchaSid = captchaSid.value
|
||||
val currentCaptchaCode = captchaCode.value?.takeIf { currentCaptchaSid != null }
|
||||
|
||||
oAuthUseCase.getSilentToken(
|
||||
login = currentState.login,
|
||||
password = currentState.password,
|
||||
forceSms = forceSms,
|
||||
validationCode = validationCode.value,
|
||||
captchaSid = captchaArguments.value?.captchaSid,
|
||||
captchaKey = captchaCode.value
|
||||
validationCode = currentValidationCode,
|
||||
captchaSid = currentCaptchaSid,
|
||||
captchaKey = currentCaptchaCode
|
||||
).listenValue(viewModelScope) { state ->
|
||||
state.processState(
|
||||
error = { error ->
|
||||
Log.d("LoginViewModelImpl", "login: error: $error")
|
||||
|
||||
validationCode.update { null }
|
||||
captchaCode.update { null }
|
||||
screenState.updateValue { copy(isLoading = false) }
|
||||
captchaSid.setValue { null }
|
||||
|
||||
parseError(error)
|
||||
},
|
||||
success = { response ->
|
||||
val userId = response.userId
|
||||
val accessToken = response.accessToken
|
||||
val exceptionHandler =
|
||||
CoroutineExceptionHandler { _, _ ->
|
||||
screenState.updateValue { copy(isLoading = false) }
|
||||
loginDialog.setValue { LoginDialog.Error() }
|
||||
}
|
||||
|
||||
if (userId == null || accessToken == null) {
|
||||
loginDialog.setValue { LoginDialog.Error() }
|
||||
return@processState
|
||||
viewModelScope.launch(Dispatchers.IO + exceptionHandler) {
|
||||
val (anonymToken) = authRepository.getAnonymToken(
|
||||
VkConstants.MESSENGER_APP_ID.toString(),
|
||||
VkConstants.MESSENGER_APP_SECRET
|
||||
).success()
|
||||
|
||||
val exchangeSilentTokenResponse = authRepository.exchangeSilentToken(
|
||||
anonymToken = anonymToken,
|
||||
silentToken = response.silentToken,
|
||||
silentUuid = response.silentTokenUuid
|
||||
).success()
|
||||
|
||||
|
||||
val getExchangeTokenResponse =
|
||||
authRepository.getExchangeToken(exchangeSilentTokenResponse.accessToken)
|
||||
.success()
|
||||
|
||||
val exchangeToken =
|
||||
getExchangeTokenResponse.usersTokens.firstOrNull {
|
||||
it.userId == exchangeSilentTokenResponse.userId
|
||||
}
|
||||
|
||||
if (exchangeToken == null) {
|
||||
screenState.updateValue { copy(isLoading = false) }
|
||||
loginDialog.setValue { LoginDialog.Error() }
|
||||
return@launch
|
||||
}
|
||||
|
||||
val userId = exchangeSilentTokenResponse.userId
|
||||
val accessToken = exchangeSilentTokenResponse.accessToken
|
||||
|
||||
// TODO: 30-Mar-25, Danil Nikolaev: get fast's app token
|
||||
|
||||
val currentAccount = AccountEntity(
|
||||
userId = userId,
|
||||
accessToken = accessToken,
|
||||
fastToken = null,
|
||||
trustedHash = response.trustedHash,
|
||||
exchangeToken = exchangeToken.commonToken
|
||||
).also { account ->
|
||||
UserConfig.currentUserId = account.userId
|
||||
UserConfig.userId = account.userId
|
||||
UserConfig.accessToken = account.accessToken
|
||||
UserConfig.fastToken = account.fastToken
|
||||
UserConfig.trustedHash = account.trustedHash
|
||||
UserConfig.exchangeToken = account.exchangeToken
|
||||
}
|
||||
|
||||
accountsRepository.storeAccounts(listOf(currentAccount))
|
||||
|
||||
startLongPoll()
|
||||
|
||||
captchaSid.update { null }
|
||||
validationSid.update { null }
|
||||
|
||||
loadUserByIdUseCase(
|
||||
userId = userId,
|
||||
fields = VkConstants.USER_FIELDS,
|
||||
nomCase = null
|
||||
).listenValue(viewModelScope) { state ->
|
||||
state.processState(
|
||||
any = {
|
||||
screenState.updateValue { copy(isLoading = false) }
|
||||
},
|
||||
error = ::parseError,
|
||||
success = { user ->
|
||||
if (user == null) {
|
||||
loginDialog.update { LoginDialog.Error() }
|
||||
} else {
|
||||
screenState.updateValue { copy(login = "", password = "") }
|
||||
isNeedToOpenMain.update { true }
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
loadUserByIdUseCase(
|
||||
userId = userId,
|
||||
fields = VkConstants.USER_FIELDS,
|
||||
nomCase = null
|
||||
)
|
||||
|
||||
val currentAccount = AccountEntity(
|
||||
userId = userId,
|
||||
accessToken = accessToken,
|
||||
fastToken = null,
|
||||
trustedHash = response.validationHash
|
||||
).also { account ->
|
||||
UserConfig.currentUserId = account.userId
|
||||
UserConfig.userId = account.userId
|
||||
UserConfig.accessToken = account.accessToken
|
||||
UserConfig.fastToken = account.fastToken
|
||||
UserConfig.trustedHash = account.trustedHash
|
||||
}
|
||||
|
||||
startLongPoll()
|
||||
|
||||
accountsRepository.storeAccounts(listOf(currentAccount))
|
||||
|
||||
captchaArguments.update { null }
|
||||
captchaCode.update { null }
|
||||
|
||||
validationArguments.update { null }
|
||||
validationCode.update { null }
|
||||
|
||||
screenState.setValue { old ->
|
||||
old.copy(
|
||||
login = "",
|
||||
password = "",
|
||||
)
|
||||
}
|
||||
|
||||
isNeedToOpenMain.update { true }
|
||||
}
|
||||
)
|
||||
screenState.emit(screenState.value.copy(isLoading = state.isLoading()))
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseError(stateError: State.Error): Boolean {
|
||||
return when (stateError) {
|
||||
private fun parseError(stateError: State.Error) {
|
||||
when (stateError) {
|
||||
is State.Error.OAuthError -> {
|
||||
when (val error = stateError.error) {
|
||||
is OAuthErrorDomain.ValidationRequiredError -> {
|
||||
@@ -321,6 +335,7 @@ class LoginViewModelImpl(
|
||||
canResendSms = error.validationResend == "sms"
|
||||
)
|
||||
validationArguments.update { arguments }
|
||||
validationSid.update { error.validationSid }
|
||||
}
|
||||
|
||||
is OAuthErrorDomain.CaptchaRequiredError -> {
|
||||
@@ -329,6 +344,7 @@ class LoginViewModelImpl(
|
||||
captchaImageUrl = error.captchaImageUrl
|
||||
)
|
||||
captchaArguments.update { arguments }
|
||||
captchaSid.update { error.captchaSid }
|
||||
}
|
||||
|
||||
OAuthErrorDomain.InvalidCredentialsError -> {
|
||||
@@ -348,12 +364,16 @@ class LoginViewModelImpl(
|
||||
}
|
||||
|
||||
OAuthErrorDomain.WrongValidationCode -> {
|
||||
isNeedToClearValidationCode.update { true }
|
||||
validationCode.update { null }
|
||||
loginDialog.setValue {
|
||||
LoginDialog.Error(errorText = "Wrong validation code.")
|
||||
}
|
||||
}
|
||||
|
||||
OAuthErrorDomain.WrongValidationCodeFormat -> {
|
||||
isNeedToClearValidationCode.update { true }
|
||||
validationCode.update { null }
|
||||
loginDialog.setValue {
|
||||
LoginDialog.Error(errorText = "Wrong validation code format.")
|
||||
}
|
||||
@@ -369,18 +389,9 @@ class LoginViewModelImpl(
|
||||
loginDialog.setValue { LoginDialog.Error() }
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
is State.Error.TestError -> {
|
||||
loginDialog.setValue {
|
||||
LoginDialog.Error(errorText = stateError.message)
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
else -> false
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,8 +5,6 @@ import androidx.compose.runtime.Immutable
|
||||
@Immutable
|
||||
sealed class LoginDialog {
|
||||
|
||||
data object FastAuth : LoginDialog()
|
||||
|
||||
data class Error(
|
||||
val errorText: String? = null,
|
||||
val errorTextResId: Int? = null
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
package dev.meloda.fast.auth.login.model
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
|
||||
@Immutable
|
||||
sealed class LoginError {
|
||||
data object Unknown : LoginError()
|
||||
data object WrongCredentials : LoginError()
|
||||
data object TooManyTries : LoginError()
|
||||
data object WrongValidationCode : LoginError()
|
||||
data object WrongValidationCodeFormat : LoginError()
|
||||
data class SimpleError(val message: String): LoginError()
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
package dev.meloda.fast.auth.login.navigation
|
||||
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation.NavBackStackEntry
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
@@ -27,6 +30,23 @@ fun NavGraphBuilder.loginScreen(
|
||||
val viewModel: LoginViewModel =
|
||||
backStackEntry.sharedViewModel<LoginViewModelImpl>(navController = navController)
|
||||
|
||||
val clearValidationCode by viewModel.isNeedToClearValidationCode.collectAsStateWithLifecycle()
|
||||
val clearCaptchaCode by viewModel.isNeedToClearCaptchaCode.collectAsStateWithLifecycle()
|
||||
|
||||
LaunchedEffect(clearValidationCode) {
|
||||
if (clearValidationCode) {
|
||||
backStackEntry.savedStateHandle["validation_code"] = null
|
||||
viewModel.onValidationCodeCleared()
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(clearCaptchaCode) {
|
||||
if (clearCaptchaCode) {
|
||||
backStackEntry.savedStateHandle["captcha_code"] = null
|
||||
viewModel.onCaptchaCodeCleared()
|
||||
}
|
||||
}
|
||||
|
||||
val validationCode = backStackEntry.getValidationResult()
|
||||
val captchaCode = backStackEntry.getCaptchaResult()
|
||||
|
||||
|
||||
+20
-99
@@ -4,7 +4,6 @@ import android.os.Bundle
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.layout.Box
|
||||
@@ -35,7 +34,6 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.autofill.ContentType
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
@@ -56,16 +54,17 @@ import dev.meloda.fast.auth.login.LoginViewModel
|
||||
import dev.meloda.fast.auth.login.LoginViewModelImpl
|
||||
import dev.meloda.fast.auth.login.model.CaptchaArguments
|
||||
import dev.meloda.fast.auth.login.model.LoginDialog
|
||||
import dev.meloda.fast.auth.login.model.LoginError
|
||||
import dev.meloda.fast.auth.login.model.LoginScreenState
|
||||
import dev.meloda.fast.auth.login.model.LoginUserBannedArguments
|
||||
import dev.meloda.fast.auth.login.model.LoginValidationArguments
|
||||
import dev.meloda.fast.datastore.UserSettings
|
||||
import dev.meloda.fast.ui.components.MaterialDialog
|
||||
import dev.meloda.fast.ui.components.TextFieldErrorText
|
||||
import dev.meloda.fast.ui.theme.LocalSizeConfig
|
||||
import dev.meloda.fast.ui.util.handleEnterKey
|
||||
import dev.meloda.fast.ui.util.handleTabKey
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
import org.koin.compose.koinInject
|
||||
import dev.meloda.fast.ui.R as UiR
|
||||
|
||||
@Composable
|
||||
@@ -90,41 +89,36 @@ fun LoginRoute(
|
||||
onBack = viewModel::onBackPressed
|
||||
)
|
||||
|
||||
LaunchedEffect(
|
||||
isNeedToOpenMain,
|
||||
userBannedArguments,
|
||||
captchaArguments,
|
||||
validationArguments,
|
||||
validationCode,
|
||||
captchaCode
|
||||
) {
|
||||
LaunchedEffect(isNeedToOpenMain) {
|
||||
if (isNeedToOpenMain) {
|
||||
viewModel.onNavigatedToMain()
|
||||
onNavigateToMain()
|
||||
}
|
||||
|
||||
}
|
||||
LaunchedEffect(userBannedArguments) {
|
||||
userBannedArguments?.let { arguments ->
|
||||
viewModel.onNavigatedToUserBanned()
|
||||
onNavigateToUserBanned(arguments)
|
||||
}
|
||||
|
||||
}
|
||||
LaunchedEffect(captchaArguments) {
|
||||
captchaArguments?.let { arguments ->
|
||||
viewModel.onNavigatedToCaptcha()
|
||||
onNavigateToCaptcha(arguments)
|
||||
}
|
||||
|
||||
}
|
||||
LaunchedEffect(validationArguments) {
|
||||
validationArguments?.let { arguments ->
|
||||
viewModel.onNavigatedToValidation()
|
||||
onNavigateToValidation(arguments)
|
||||
}
|
||||
|
||||
if (validationCode != null) {
|
||||
viewModel.onValidationCodeReceived(validationCode)
|
||||
}
|
||||
|
||||
if (captchaCode != null) {
|
||||
viewModel.onCaptchaCodeReceived(captchaCode)
|
||||
}
|
||||
}
|
||||
LaunchedEffect(validationCode) {
|
||||
viewModel.onValidationCodeReceived(validationCode)
|
||||
}
|
||||
LaunchedEffect(captchaCode) {
|
||||
viewModel.onCaptchaCodeReceived(captchaCode)
|
||||
}
|
||||
|
||||
LoginScreen(
|
||||
@@ -134,8 +128,7 @@ fun LoginRoute(
|
||||
onPasswordFieldEnterKeyClicked = viewModel::onSignInButtonClicked,
|
||||
onPasswordVisibilityButtonClicked = viewModel::onPasswordVisibilityButtonClicked,
|
||||
onPasswordFieldGoAction = viewModel::onSignInButtonClicked,
|
||||
onSignInButtonClicked = viewModel::onSignInButtonClicked,
|
||||
onLogoLongClicked = viewModel::onLogoLongClicked
|
||||
onSignInButtonClicked = viewModel::onSignInButtonClicked
|
||||
)
|
||||
|
||||
HandleDialogs(
|
||||
@@ -153,8 +146,7 @@ fun LoginScreen(
|
||||
onPasswordFieldEnterKeyClicked: () -> Unit = {},
|
||||
onPasswordVisibilityButtonClicked: () -> Unit = {},
|
||||
onPasswordFieldGoAction: () -> Unit = {},
|
||||
onSignInButtonClicked: () -> Unit = {},
|
||||
onLogoLongClicked: () -> Unit = {}
|
||||
onSignInButtonClicked: () -> Unit = {}
|
||||
) {
|
||||
val size = LocalSizeConfig.current
|
||||
val focusManager = LocalFocusManager.current
|
||||
@@ -181,7 +173,7 @@ fun LoginScreen(
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut()
|
||||
) {
|
||||
Logo(onLogoLongClicked = onLogoLongClicked)
|
||||
Logo()
|
||||
}
|
||||
|
||||
AnimatedVisibility(
|
||||
@@ -371,79 +363,8 @@ fun HandleDialogs(
|
||||
onDismissRequest = { onDismissed(loginDialog) },
|
||||
title = stringResource(UiR.string.title_error),
|
||||
text = loginDialog.errorTextResId?.let { stringResource(it) }
|
||||
?: loginDialog.errorText.orEmpty(),
|
||||
confirmText = stringResource(id = UiR.string.ok)
|
||||
)
|
||||
}
|
||||
|
||||
LoginDialog.FastAuth -> {
|
||||
SignInAlert(
|
||||
onDismissRequest = { onDismissed(loginDialog) },
|
||||
onConfirmClick = { onConfirmed(loginDialog, bundleOf("token" to it)) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun HandleError(
|
||||
onDismiss: () -> Unit,
|
||||
error: LoginError?,
|
||||
) {
|
||||
when (error) {
|
||||
null -> Unit
|
||||
|
||||
LoginError.Unknown -> {
|
||||
MaterialDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = "Error",
|
||||
text = "Unknown error",
|
||||
confirmText = stringResource(id = UiR.string.ok)
|
||||
)
|
||||
}
|
||||
|
||||
LoginError.WrongCredentials -> {
|
||||
MaterialDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = "Error",
|
||||
text = "Wrong login or password.",
|
||||
confirmText = stringResource(id = UiR.string.ok)
|
||||
)
|
||||
}
|
||||
|
||||
LoginError.TooManyTries -> {
|
||||
MaterialDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = "Error",
|
||||
text = "Too many tries. Try in another hour or later.",
|
||||
confirmText = stringResource(id = UiR.string.ok)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
LoginError.WrongValidationCode -> {
|
||||
MaterialDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = "Error",
|
||||
text = "Wrong validation code.",
|
||||
confirmText = stringResource(id = UiR.string.ok)
|
||||
)
|
||||
}
|
||||
|
||||
LoginError.WrongValidationCodeFormat -> {
|
||||
MaterialDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = "Error",
|
||||
text = "Wrong validation code format.",
|
||||
confirmText = stringResource(id = UiR.string.ok)
|
||||
)
|
||||
}
|
||||
|
||||
is LoginError.SimpleError -> {
|
||||
MaterialDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = "Error",
|
||||
text = error.message,
|
||||
?: loginDialog.errorText
|
||||
?: stringResource(UiR.string.unknown_error_occurred),
|
||||
confirmText = stringResource(id = UiR.string.ok)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package dev.meloda.fast.auth.login.presentation
|
||||
|
||||
import android.os.Build
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
import androidx.compose.animation.core.animateIntAsState
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
@@ -24,21 +25,22 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import dev.meloda.fast.datastore.UserSettings
|
||||
import dev.meloda.fast.ui.R
|
||||
import dev.meloda.fast.ui.theme.LocalSizeConfig
|
||||
import org.koin.compose.koinInject
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun Logo(
|
||||
modifier: Modifier = Modifier,
|
||||
onLogoLongClicked: () -> Unit = {}
|
||||
) {
|
||||
fun Logo(modifier: Modifier = Modifier) {
|
||||
val size = LocalSizeConfig.current
|
||||
|
||||
val iconWidth by animateDpAsState(if (size.isWidthSmall) 110.dp else 134.dp)
|
||||
val appNameFontSize by animateIntAsState(if (size.isWidthSmall) 40 else 40)
|
||||
val appNameFontSize by animateIntAsState(if (size.isWidthSmall) 32 else 40)
|
||||
val bottomAdditionalPadding by animateDpAsState(if (size.isHeightSmall) 10.dp else 30.dp)
|
||||
|
||||
val userSettings: UserSettings = koinInject()
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
@@ -61,8 +63,14 @@ fun Logo(
|
||||
.combinedClickable(
|
||||
interactionSource = null,
|
||||
indication = null,
|
||||
onLongClick = onLogoLongClicked,
|
||||
onClick = {}
|
||||
onLongClick = null,
|
||||
onClick = {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
userSettings.onEnableDynamicColorsChanged(
|
||||
!userSettings.enableDynamicColors.value
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
package dev.meloda.fast.auth.login.presentation
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import dev.meloda.fast.auth.BuildConfig
|
||||
import dev.meloda.fast.ui.components.ActionInvokeDismiss
|
||||
import dev.meloda.fast.ui.components.MaterialDialog
|
||||
|
||||
import dev.meloda.fast.ui.R as UiR
|
||||
|
||||
@Composable
|
||||
fun SignInAlert(
|
||||
onDismissRequest: () -> Unit = {},
|
||||
onConfirmClick: (token: String) -> Unit = {}
|
||||
) {
|
||||
var tokenText by rememberSaveable {
|
||||
mutableStateOf(BuildConfig.debugToken)
|
||||
}
|
||||
|
||||
val maxWidthModifier = Modifier.fillMaxWidth()
|
||||
|
||||
MaterialDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
title = "Fast authorization",
|
||||
confirmText = stringResource(id = UiR.string.action_authorize),
|
||||
confirmAction = { onConfirmClick(tokenText) },
|
||||
cancelText = stringResource(id = UiR.string.cancel),
|
||||
actionInvokeDismiss = ActionInvokeDismiss.Always
|
||||
) {
|
||||
Column(modifier = maxWidthModifier) {
|
||||
OutlinedTextField(
|
||||
modifier = maxWidthModifier.padding(horizontal = 16.dp),
|
||||
value = tokenText,
|
||||
onValueChange = { tokenText = it },
|
||||
placeholder = { Text(text = "Access token") },
|
||||
label = { Text(text = "Access token") }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -38,7 +38,7 @@ fun NavController.navigateToValidation(arguments: ValidationArguments) {
|
||||
}
|
||||
|
||||
fun NavController.setValidationResult(code: String?) {
|
||||
this.currentBackStackEntry
|
||||
this.previousBackStackEntry
|
||||
?.savedStateHandle
|
||||
?.set("validation_code", code)
|
||||
}
|
||||
|
||||
+12
-2
@@ -7,10 +7,13 @@ import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.ime
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.union
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
@@ -23,6 +26,7 @@ import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.ScaffoldDefaults
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextField
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -35,10 +39,13 @@ import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.autofill.ContentType
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.contentType
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
@@ -146,7 +153,9 @@ fun ValidationScreen(
|
||||
|
||||
var code by remember { mutableStateOf(TextFieldValue(screenState.code.orEmpty())) }
|
||||
|
||||
Scaffold { padding ->
|
||||
Scaffold(
|
||||
contentWindowInsets = ScaffoldDefaults.contentWindowInsets.union(WindowInsets.ime)
|
||||
) { padding ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
@@ -210,7 +219,8 @@ fun ValidationScreen(
|
||||
placeholder = { Text(text = "Code") },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(10.dp)),
|
||||
.clip(RoundedCornerShape(10.dp))
|
||||
.semantics { contentType = ContentType.SmsOtpCode },
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
painter = painterResource(id = UiR.drawable.round_qr_code_24),
|
||||
|
||||
Reference in New Issue
Block a user