ability to import/export auth data and some refactoring

This commit is contained in:
2025-07-09 17:29:51 +03:00
parent d2aaac68e2
commit 9e6b079bf6
21 changed files with 721 additions and 437 deletions
@@ -23,6 +23,7 @@ object AuthGraph
fun NavGraphBuilder.authNavGraph(
onNavigateToMain: () -> Unit,
onNavigateToSettings: () -> Unit,
navController: NavController
) {
navigation<AuthGraph>(startDestination = Login) {
@@ -54,6 +55,7 @@ fun NavGraphBuilder.authNavGraph(
)
)
},
onNavigateToSettings = onNavigateToSettings,
navController = navController
)
@@ -35,49 +35,13 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
interface LoginViewModel {
val screenState: StateFlow<LoginScreenState>
val loginDialog: StateFlow<LoginDialog?>
val validationArguments: StateFlow<LoginValidationArguments?>
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)
fun onBackPressed()
fun onPasswordVisibilityButtonClicked()
fun onLoginInputChanged(newLogin: String)
fun onPasswordInputChanged(newPassword: String)
fun onSignInButtonClicked()
fun onLogoClicked()
fun onNavigatedToMain()
fun onNavigatedToUserBanned()
fun onNavigatedToCaptcha()
fun onNavigatedToValidation()
fun onValidationCodeReceived(code: String?)
fun onValidationCodeCleared()
fun onCaptchaCodeReceived(code: String?)
fun onCaptchaCodeCleared()
}
class LoginViewModelImpl(
class LoginViewModel(
private val oAuthUseCase: OAuthUseCase,
private val authRepository: AuthRepository,
private val loadUserByIdUseCase: LoadUserByIdUseCase,
@@ -85,18 +49,30 @@ class LoginViewModelImpl(
private val loginValidator: LoginValidator,
private val longPollController: LongPollController,
private val userSettings: UserSettings
) : ViewModel(), LoginViewModel {
) : ViewModel() {
private val _screenState = MutableStateFlow(LoginScreenState.EMPTY)
val screenState = _screenState.asStateFlow()
override val screenState = MutableStateFlow(LoginScreenState.EMPTY)
override val loginDialog = MutableStateFlow<LoginDialog?>(null)
private val _loginDialog = MutableStateFlow<LoginDialog?>(null)
val loginDialog = _loginDialog.asStateFlow()
override val validationArguments = MutableStateFlow<LoginValidationArguments?>(null)
override val captchaArguments = MutableStateFlow<CaptchaArguments?>(null)
override val userBannedArguments = MutableStateFlow<LoginUserBannedArguments?>(null)
override val isNeedToOpenMain = MutableStateFlow(false)
private val _validationArguments = MutableStateFlow<LoginValidationArguments?>(null)
val validationArguments = _validationArguments.asStateFlow()
override val isNeedToClearCaptchaCode = MutableStateFlow(false)
override val isNeedToClearValidationCode = MutableStateFlow(false)
private val _captchaArguments = MutableStateFlow<CaptchaArguments?>(null)
val captchaArguments = _captchaArguments.asStateFlow()
private val _userBannedArguments = MutableStateFlow<LoginUserBannedArguments?>(null)
val userBannedArguments = _userBannedArguments.asStateFlow()
private val _isNeedToOpenMain = MutableStateFlow(false)
val isNeedToOpenMain = _isNeedToOpenMain.asStateFlow()
private val _isNeedToClearCaptchaCode = MutableStateFlow(false)
val isNeedToClearCaptchaCode = _isNeedToClearCaptchaCode.asStateFlow()
private val _isNeedToClearValidationCode = MutableStateFlow(false)
val isNeedToClearValidationCode = _isNeedToClearValidationCode.asStateFlow()
private val validationState: StateFlow<List<LoginValidationResult>> =
screenState.map(loginValidator::validate)
@@ -120,7 +96,7 @@ class LoginViewModelImpl(
}
}
override fun onDialogConfirmed(dialog: LoginDialog, bundle: Bundle) {
fun onDialogConfirmed(dialog: LoginDialog, bundle: Bundle) {
onDialogDismissed(dialog)
when (dialog) {
@@ -128,20 +104,24 @@ class LoginViewModelImpl(
}
}
override fun onDialogDismissed(dialog: LoginDialog) {
loginDialog.setValue { null }
fun onDialogDismissed(dialog: LoginDialog) {
when (dialog) {
is LoginDialog.Error -> Unit
}
_loginDialog.setValue { null }
}
override fun onBackPressed() {
screenState.setValue { old -> old.copy(showLogo = true) }
fun onBackPressed() {
_screenState.setValue { old -> old.copy(showLogo = true) }
}
override fun onPasswordVisibilityButtonClicked() {
screenState.setValue { old -> old.copy(passwordVisible = !old.passwordVisible) }
fun onPasswordVisibilityButtonClicked() {
_screenState.setValue { old -> old.copy(passwordVisible = !old.passwordVisible) }
}
override fun onLoginInputChanged(newLogin: String) {
screenState.setValue { old ->
fun onLoginInputChanged(newLogin: String) {
_screenState.setValue { old ->
old.copy(
login = newLogin.trim(),
loginError = false
@@ -149,8 +129,8 @@ class LoginViewModelImpl(
}
}
override fun onPasswordInputChanged(newPassword: String) {
screenState.setValue { old ->
fun onPasswordInputChanged(newPassword: String) {
_screenState.setValue { old ->
old.copy(
password = newPassword.trim(),
passwordError = false
@@ -158,18 +138,18 @@ class LoginViewModelImpl(
}
}
override fun onSignInButtonClicked() {
fun onSignInButtonClicked() {
if (screenState.value.isLoading) return
if (screenState.value.showLogo) {
screenState.setValue { old -> old.copy(showLogo = false) }
_screenState.setValue { old -> old.copy(showLogo = false) }
return
}
login()
}
override fun onLogoClicked() {
fun onLogoClicked() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
userSettings.onEnableDynamicColorsChanged(
!userSettings.enableDynamicColors.value
@@ -177,36 +157,36 @@ class LoginViewModelImpl(
}
}
override fun onNavigatedToMain() {
isNeedToOpenMain.update { false }
fun onNavigatedToMain() {
_isNeedToOpenMain.update { false }
}
override fun onNavigatedToUserBanned() {
userBannedArguments.update { null }
fun onNavigatedToUserBanned() {
_userBannedArguments.update { null }
}
override fun onNavigatedToCaptcha() {
captchaArguments.update { null }
fun onNavigatedToCaptcha() {
_captchaArguments.update { null }
}
override fun onNavigatedToValidation() {
validationArguments.update { null }
fun onNavigatedToValidation() {
_validationArguments.update { null }
}
override fun onValidationCodeReceived(code: String?) {
fun onValidationCodeReceived(code: String?) {
validationCode.update { code }
}
override fun onValidationCodeCleared() {
isNeedToClearValidationCode.update { false }
fun onValidationCodeCleared() {
_isNeedToClearValidationCode.update { false }
}
override fun onCaptchaCodeReceived(code: String?) {
fun onCaptchaCodeReceived(code: String?) {
captchaCode.update { code }
}
override fun onCaptchaCodeCleared() {
isNeedToClearCaptchaCode.update { false }
fun onCaptchaCodeCleared() {
_isNeedToClearCaptchaCode.update { false }
}
private fun login(forceSms: Boolean = false) {
@@ -223,7 +203,7 @@ class LoginViewModelImpl(
processValidation()
if (!validationState.value.contains(LoginValidationResult.Valid)) return
screenState.updateValue { copy(isLoading = true) }
_screenState.updateValue { copy(isLoading = true) }
val currentValidationSid = validationSid.value
val currentValidationCode = validationCode.value?.takeIf { currentValidationSid != null }
@@ -242,7 +222,7 @@ class LoginViewModelImpl(
error = { error ->
Log.d("LoginViewModelImpl", "login: error: $error")
screenState.updateValue { copy(isLoading = false) }
_screenState.updateValue { copy(isLoading = false) }
captchaSid.setValue { null }
parseError(error)
@@ -250,8 +230,8 @@ class LoginViewModelImpl(
success = { response ->
val exceptionHandler =
CoroutineExceptionHandler { _, _ ->
screenState.updateValue { copy(isLoading = false) }
loginDialog.setValue { LoginDialog.Error() }
_screenState.updateValue { copy(isLoading = false) }
_loginDialog.setValue { LoginDialog.Error() }
}
viewModelScope.launch(Dispatchers.IO + exceptionHandler) {
@@ -277,8 +257,8 @@ class LoginViewModelImpl(
}
if (exchangeToken == null) {
screenState.updateValue { copy(isLoading = false) }
loginDialog.setValue { LoginDialog.Error() }
_screenState.updateValue { copy(isLoading = false) }
_loginDialog.setValue { LoginDialog.Error() }
return@launch
}
@@ -316,15 +296,15 @@ class LoginViewModelImpl(
).listenValue(viewModelScope) { state ->
state.processState(
any = {
screenState.updateValue { copy(isLoading = false) }
_screenState.updateValue { copy(isLoading = false) }
},
error = ::parseError,
success = { user ->
if (user == null) {
loginDialog.update { LoginDialog.Error() }
_loginDialog.update { LoginDialog.Error() }
} else {
screenState.updateValue { copy(login = "", password = "") }
isNeedToOpenMain.update { true }
_screenState.updateValue { copy(login = "", password = "") }
_isNeedToOpenMain.update { true }
}
}
)
@@ -347,7 +327,7 @@ class LoginViewModelImpl(
validationType = error.validationType.value,
canResendSms = error.validationResend == "sms"
)
validationArguments.update { arguments }
_validationArguments.update { arguments }
validationSid.update { error.validationSid }
}
@@ -356,12 +336,12 @@ class LoginViewModelImpl(
captchaSid = error.captchaSid,
captchaImageUrl = error.captchaImageUrl
)
captchaArguments.update { arguments }
_captchaArguments.update { arguments }
captchaSid.update { error.captchaSid }
}
OAuthErrorDomain.InvalidCredentialsError -> {
loginDialog.setValue {
_loginDialog.setValue {
LoginDialog.Error(errorText = "Wrong login or password.")
}
}
@@ -373,33 +353,33 @@ class LoginViewModelImpl(
restoreUrl = error.restoreUrl,
accessToken = error.accessToken
)
userBannedArguments.update { arguments }
_userBannedArguments.update { arguments }
}
OAuthErrorDomain.WrongValidationCode -> {
isNeedToClearValidationCode.update { true }
_isNeedToClearValidationCode.update { true }
validationCode.update { null }
loginDialog.setValue {
_loginDialog.setValue {
LoginDialog.Error(errorText = "Wrong validation code.")
}
}
OAuthErrorDomain.WrongValidationCodeFormat -> {
isNeedToClearValidationCode.update { true }
_isNeedToClearValidationCode.update { true }
validationCode.update { null }
loginDialog.setValue {
_loginDialog.setValue {
LoginDialog.Error(errorText = "Wrong validation code format.")
}
}
OAuthErrorDomain.TooManyTriesError -> {
loginDialog.setValue {
_loginDialog.setValue {
LoginDialog.Error(errorText = "Too many tries. Try in another hour or later.")
}
}
OAuthErrorDomain.UnknownError -> {
loginDialog.setValue { LoginDialog.Error() }
_loginDialog.setValue { LoginDialog.Error() }
}
}
}
@@ -412,11 +392,11 @@ class LoginViewModelImpl(
validationState.value.forEach { result ->
when (result) {
LoginValidationResult.LoginEmpty -> {
screenState.setValue { old -> old.copy(loginError = true) }
_screenState.setValue { old -> old.copy(loginError = true) }
}
LoginValidationResult.PasswordEmpty -> {
screenState.setValue { old -> old.copy(passwordError = true) }
_screenState.setValue { old -> old.copy(passwordError = true) }
}
LoginValidationResult.Empty -> Unit
@@ -1,9 +1,9 @@
package dev.meloda.fast.auth.login.di
import dev.meloda.fast.auth.login.LoginViewModelImpl
import dev.meloda.fast.auth.login.LoginViewModel
import dev.meloda.fast.auth.login.validation.LoginValidator
import dev.meloda.fast.domain.OAuthUseCase
import dev.meloda.fast.domain.OAuthUseCaseImpl
import dev.meloda.fast.auth.login.validation.LoginValidator
import org.koin.core.module.dsl.singleOf
import org.koin.core.module.dsl.viewModelOf
import org.koin.dsl.bind
@@ -11,6 +11,6 @@ import org.koin.dsl.module
val loginModule = module {
singleOf(::LoginValidator)
viewModelOf(::LoginViewModelImpl) bind dev.meloda.fast.auth.login.LoginViewModel::class
viewModelOf(::LoginViewModel)
singleOf(::OAuthUseCaseImpl) bind OAuthUseCase::class
}
@@ -8,7 +8,6 @@ import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.composable
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.LoginUserBannedArguments
import dev.meloda.fast.auth.login.model.LoginValidationArguments
@@ -24,11 +23,12 @@ fun NavGraphBuilder.loginScreen(
onNavigateToValidation: (LoginValidationArguments) -> Unit,
onNavigateToMain: () -> Unit,
onNavigateToUserBanned: (LoginUserBannedArguments) -> Unit,
onNavigateToSettings: () -> Unit,
navController: NavController
) {
composable<Login> { backStackEntry ->
val viewModel: LoginViewModel =
backStackEntry.sharedViewModel<LoginViewModelImpl>(navController = navController)
backStackEntry.sharedViewModel<LoginViewModel>(navController = navController)
val clearValidationCode by viewModel.isNeedToClearValidationCode.collectAsStateWithLifecycle()
val clearCaptchaCode by viewModel.isNeedToClearCaptchaCode.collectAsStateWithLifecycle()
@@ -55,6 +55,7 @@ fun NavGraphBuilder.loginScreen(
onNavigateToMain = onNavigateToMain,
onNavigateToCaptcha = onNavigateToCaptcha,
onNavigateToValidation = onNavigateToValidation,
onNavigateToSettings = onNavigateToSettings,
validationCode = validationCode,
captchaCode = captchaCode,
viewModel = viewModel
@@ -39,6 +39,8 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.autofill.ContentType
import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.FocusRequester.Companion.FocusRequesterFactory.component1
import androidx.compose.ui.focus.FocusRequester.Companion.FocusRequesterFactory.component2
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager
@@ -56,7 +58,6 @@ import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
import androidx.lifecycle.compose.collectAsStateWithLifecycle
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.LoginScreenState
@@ -76,9 +77,10 @@ fun LoginRoute(
onNavigateToMain: () -> Unit,
onNavigateToCaptcha: (CaptchaArguments) -> Unit,
onNavigateToValidation: (LoginValidationArguments) -> Unit,
onNavigateToSettings: () -> Unit,
validationCode: String?,
captchaCode: String?,
viewModel: LoginViewModel = koinViewModel<LoginViewModelImpl>()
viewModel: LoginViewModel = koinViewModel()
) {
val screenState by viewModel.screenState.collectAsStateWithLifecycle()
val isNeedToOpenMain by viewModel.isNeedToOpenMain.collectAsStateWithLifecycle()
@@ -132,7 +134,8 @@ fun LoginRoute(
onPasswordVisibilityButtonClicked = viewModel::onPasswordVisibilityButtonClicked,
onPasswordFieldGoAction = viewModel::onSignInButtonClicked,
onSignInButtonClicked = viewModel::onSignInButtonClicked,
onLogoClicked = viewModel::onLogoClicked
onLogoClicked = viewModel::onLogoClicked,
onLogoLongClicked = onNavigateToSettings
)
HandleDialogs(
@@ -151,7 +154,8 @@ fun LoginScreen(
onPasswordVisibilityButtonClicked: () -> Unit = {},
onPasswordFieldGoAction: () -> Unit = {},
onSignInButtonClicked: () -> Unit = {},
onLogoClicked: () -> Unit = {}
onLogoClicked: () -> Unit = {},
onLogoLongClicked: () -> Unit = {}
) {
val context = LocalContext.current
val size = LocalSizeConfig.current
@@ -185,7 +189,10 @@ fun LoginScreen(
exit = fadeOut(),
label = "Logo visibility"
) {
Logo(onLogoClicked = onLogoClicked)
Logo(
onLogoClicked = onLogoClicked,
onLogoLongClicked = onLogoLongClicked
)
}
AnimatedVisibility(
@@ -30,7 +30,8 @@ import dev.meloda.fast.ui.theme.LocalSizeConfig
@Composable
fun Logo(
modifier: Modifier = Modifier,
onLogoClicked: () -> Unit = {}
onLogoClicked: () -> Unit = {},
onLogoLongClicked: () -> Unit = {}
) {
val size = LocalSizeConfig.current
@@ -60,7 +61,7 @@ fun Logo(
.combinedClickable(
interactionSource = null,
indication = null,
onLongClick = null,
onLongClick = onLogoLongClicked,
onClick = onLogoClicked
)
)