Authentication error management like a boos !
This commit is contained in:
parent
f48dfd6488
commit
537cf214a9
8 changed files with 518 additions and 414 deletions
|
|
@ -1,5 +1,6 @@
|
||||||
package com.pixelized.biblib.ui
|
package com.pixelized.biblib.ui
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.imePadding
|
||||||
import androidx.compose.foundation.layout.systemBarsPadding
|
import androidx.compose.foundation.layout.systemBarsPadding
|
||||||
import androidx.compose.material.*
|
import androidx.compose.material.*
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
|
@ -11,7 +12,6 @@ import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||||
import com.bumptech.glide.request.RequestOptions
|
import com.bumptech.glide.request.RequestOptions
|
||||||
import com.pixelized.biblib.ui.navigation.ScreenNavHost
|
import com.pixelized.biblib.ui.navigation.ScreenNavHost
|
||||||
import com.pixelized.biblib.ui.screen.launch.LauncherViewModel
|
import com.pixelized.biblib.ui.screen.launch.LauncherViewModel
|
||||||
import com.pixelized.biblib.utils.extention.bibLib
|
|
||||||
import com.skydoves.landscapist.glide.LocalGlideRequestOptions
|
import com.skydoves.landscapist.glide.LocalGlideRequestOptions
|
||||||
|
|
||||||
val LocalSnackHostState = staticCompositionLocalOf<SnackbarHostState> {
|
val LocalSnackHostState = staticCompositionLocalOf<SnackbarHostState> {
|
||||||
|
|
@ -33,7 +33,9 @@ fun MainContent(
|
||||||
scaffoldState = scaffoldState,
|
scaffoldState = scaffoldState,
|
||||||
snackbarHost = { snackHostState ->
|
snackbarHost = { snackHostState ->
|
||||||
SnackbarHost(
|
SnackbarHost(
|
||||||
modifier = Modifier.systemBarsPadding(),
|
modifier = Modifier
|
||||||
|
.systemBarsPadding()
|
||||||
|
.imePadding(),
|
||||||
hostState = snackHostState,
|
hostState = snackHostState,
|
||||||
) { snackBarData ->
|
) { snackBarData ->
|
||||||
Snackbar(
|
Snackbar(
|
||||||
|
|
|
||||||
|
|
@ -1,56 +1,65 @@
|
||||||
package com.pixelized.biblib.ui.screen.authentication
|
package com.pixelized.biblib.ui.screen.authentication
|
||||||
|
|
||||||
import android.content.Intent
|
import androidx.annotation.StringRes
|
||||||
import android.content.res.Configuration
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import android.net.Uri
|
import androidx.compose.animation.fadeIn
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.animation.fadeOut
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.text.KeyboardActions
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.layout.systemBarsPadding
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.material.CircularProgressIndicator
|
||||||
import androidx.compose.material.*
|
import androidx.compose.material.MaterialTheme
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.SnackbarResult
|
||||||
import androidx.compose.material.icons.sharp.Visibility
|
|
||||||
import androidx.compose.material.icons.sharp.VisibilityOff
|
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.autofill.AutofillNode
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.autofill.AutofillType
|
import androidx.compose.ui.platform.LocalUriHandler
|
||||||
import androidx.compose.ui.composed
|
|
||||||
import androidx.compose.ui.focus.onFocusChanged
|
|
||||||
import androidx.compose.ui.layout.boundsInWindow
|
|
||||||
import androidx.compose.ui.layout.onGloballyPositioned
|
|
||||||
import androidx.compose.ui.platform.LocalAutofill
|
|
||||||
import androidx.compose.ui.platform.LocalAutofillTree
|
|
||||||
import androidx.compose.ui.platform.LocalFocusManager
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
|
||||||
import androidx.compose.ui.text.buildAnnotatedString
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.text.input.ImeAction
|
|
||||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
|
||||||
import androidx.compose.ui.text.input.VisualTransformation
|
|
||||||
import androidx.compose.ui.text.withStyle
|
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.navigation.NavHostController
|
|
||||||
import com.pixelized.biblib.R
|
|
||||||
import com.pixelized.biblib.network.client.IBibLibClient
|
import com.pixelized.biblib.network.client.IBibLibClient
|
||||||
import com.pixelized.biblib.ui.composable.StateUio
|
import com.pixelized.biblib.ui.LocalSnackHostState
|
||||||
import com.pixelized.biblib.ui.composable.StateUioHandler
|
|
||||||
import com.pixelized.biblib.ui.composable.animation.AnimatedDelayer
|
|
||||||
import com.pixelized.biblib.ui.composable.animation.AnimatedOffset
|
|
||||||
import com.pixelized.biblib.ui.navigation.LocalScreenNavHostController
|
import com.pixelized.biblib.ui.navigation.LocalScreenNavHostController
|
||||||
import com.pixelized.biblib.ui.navigation.navigateToHome
|
import com.pixelized.biblib.ui.navigation.navigateToHome
|
||||||
import com.pixelized.biblib.ui.screen.authentication.viewModel.AuthenticationFormViewModel
|
import com.pixelized.biblib.ui.screen.authentication.viewModel.AuthenticationFormViewModel
|
||||||
import com.pixelized.biblib.ui.screen.authentication.viewModel.AuthenticationViewModel
|
import com.pixelized.biblib.ui.screen.authentication.viewModel.AuthenticationViewModel
|
||||||
import com.pixelized.biblib.ui.theme.BibLibTheme
|
import com.pixelized.biblib.ui.theme.color.ShadowPalette
|
||||||
import com.pixelized.biblib.ui.theme.color.GoogleColorPalette
|
|
||||||
import com.pixelized.biblib.utils.extention.bibLib
|
import com.pixelized.biblib.utils.extention.bibLib
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@Stable
|
||||||
|
@Immutable
|
||||||
|
sealed class AuthenticationUio {
|
||||||
|
@Stable
|
||||||
|
@Immutable
|
||||||
|
object Progress : AuthenticationUio()
|
||||||
|
|
||||||
|
@Stable
|
||||||
|
@Immutable
|
||||||
|
object Done : AuthenticationUio()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Stable
|
||||||
|
@Immutable
|
||||||
|
sealed class AuthenticationErrorUio(
|
||||||
|
@StringRes val message: Int,
|
||||||
|
@StringRes val action: Int,
|
||||||
|
) {
|
||||||
|
@Stable
|
||||||
|
@Immutable
|
||||||
|
class Login(
|
||||||
|
@StringRes message: Int,
|
||||||
|
@StringRes action: Int,
|
||||||
|
) : AuthenticationErrorUio(message, action)
|
||||||
|
|
||||||
|
@Stable
|
||||||
|
@Immutable
|
||||||
|
class GoogleLogin(
|
||||||
|
@StringRes message: Int,
|
||||||
|
@StringRes action: Int,
|
||||||
|
) : AuthenticationErrorUio(message, action)
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AuthenticationScreen(
|
fun AuthenticationScreen(
|
||||||
|
|
@ -58,6 +67,12 @@ fun AuthenticationScreen(
|
||||||
formViewModel: AuthenticationFormViewModel = hiltViewModel(),
|
formViewModel: AuthenticationFormViewModel = hiltViewModel(),
|
||||||
) {
|
) {
|
||||||
val navHostController = LocalScreenNavHostController.current
|
val navHostController = LocalScreenNavHostController.current
|
||||||
|
val snackBarHostState = LocalSnackHostState.current
|
||||||
|
val uriHandler = LocalUriHandler.current
|
||||||
|
val context = LocalContext.current
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
AuthenticationScreenContent(
|
AuthenticationScreenContent(
|
||||||
modifier = Modifier.systemBarsPadding(),
|
modifier = Modifier.systemBarsPadding(),
|
||||||
|
|
@ -83,345 +98,81 @@ fun AuthenticationScreen(
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
onRegister = {
|
onRegister = {
|
||||||
navHostController.navigateToRegister()
|
uriHandler.openUri(uri = IBibLibClient.REGISTER_URL)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
AuthenticationHandler(
|
AuthenticationHandler(
|
||||||
onDismissRequest = {
|
onFailure = {
|
||||||
if (it is StateUio.Failure) authenticationViewModel.dismissError()
|
scope.launch {
|
||||||
|
val result = snackBarHostState.showSnackbar(
|
||||||
|
message = context.getString(it.message),
|
||||||
|
actionLabel = context.getString(it.action),
|
||||||
|
)
|
||||||
|
if (result == SnackbarResult.ActionPerformed) {
|
||||||
|
when (it) {
|
||||||
|
is AuthenticationErrorUio.GoogleLogin -> {
|
||||||
|
authenticationViewModel.loginWithGoogle()
|
||||||
|
}
|
||||||
|
is AuthenticationErrorUio.Login -> {
|
||||||
|
authenticationViewModel.login(
|
||||||
|
login = formViewModel.form.login,
|
||||||
|
password = formViewModel.form.password,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onSuccess = {
|
onSuccess = {
|
||||||
navHostController.navigateToHome()
|
navHostController.navigateToHome()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
AuthenticationProgress()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun AuthenticationScreenContent(
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
login: String,
|
|
||||||
onLoginChange: (String) -> Unit,
|
|
||||||
password: String,
|
|
||||||
onPasswordChange: (String) -> Unit,
|
|
||||||
rememberPassword: Boolean,
|
|
||||||
onRememberPasswordChange: (Boolean) -> Unit,
|
|
||||||
onGoogleSignIn: () -> Unit,
|
|
||||||
onSignIn: () -> Unit,
|
|
||||||
onRegister: () -> Unit,
|
|
||||||
) {
|
|
||||||
val scrollState = rememberScrollState()
|
|
||||||
val focusManager = LocalFocusManager.current
|
|
||||||
|
|
||||||
AnimatedDelayer {
|
|
||||||
Column(
|
|
||||||
modifier = modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.verticalScroll(scrollState)
|
|
||||||
) {
|
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
|
||||||
|
|
||||||
AnimatedOffset(
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(all = MaterialTheme.bibLib.dimen.dp16)
|
|
||||||
.align(alignment = Alignment.CenterHorizontally),
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
style = MaterialTheme.typography.h4,
|
|
||||||
color = MaterialTheme.colors.onBackground,
|
|
||||||
text = stringResource(id = R.string.authentication_title),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
|
||||||
|
|
||||||
AnimatedOffset(
|
|
||||||
modifier = Modifier.padding(horizontal = MaterialTheme.bibLib.dimen.dp16),
|
|
||||||
) {
|
|
||||||
LoginField(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
value = login,
|
|
||||||
onValueChange = onLoginChange,
|
|
||||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(MaterialTheme.bibLib.dimen.dp8))
|
|
||||||
|
|
||||||
AnimatedOffset(
|
|
||||||
modifier = Modifier.padding(horizontal = MaterialTheme.bibLib.dimen.dp16),
|
|
||||||
) {
|
|
||||||
PasswordField(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
value = password,
|
|
||||||
onValueChange = onPasswordChange,
|
|
||||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
|
|
||||||
keyboardActions = KeyboardActions { focusManager.clearFocus() },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(MaterialTheme.bibLib.dimen.dp16))
|
|
||||||
|
|
||||||
AnimatedOffset(
|
|
||||||
modifier = Modifier.padding(horizontal = MaterialTheme.bibLib.dimen.dp16),
|
|
||||||
) {
|
|
||||||
CredentialRemember(
|
|
||||||
value = rememberPassword,
|
|
||||||
onValueChange = onRememberPasswordChange,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(MaterialTheme.bibLib.dimen.dp16))
|
|
||||||
|
|
||||||
AnimatedOffset(
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(horizontal = MaterialTheme.bibLib.dimen.dp16)
|
|
||||||
.align(Alignment.End),
|
|
||||||
) {
|
|
||||||
Row {
|
|
||||||
Button(
|
|
||||||
colors = ButtonDefaults.outlinedButtonColors(),
|
|
||||||
onClick = onRegister,
|
|
||||||
) {
|
|
||||||
Text(text = stringResource(id = R.string.action_register))
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.width(MaterialTheme.bibLib.dimen.dp8))
|
|
||||||
|
|
||||||
Button(
|
|
||||||
colors = ButtonDefaults.buttonColors(),
|
|
||||||
onClick = onSignIn,
|
|
||||||
) {
|
|
||||||
Text(text = stringResource(id = R.string.action_login))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.weight(2f))
|
|
||||||
|
|
||||||
AnimatedOffset {
|
|
||||||
Button(
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(all = MaterialTheme.bibLib.dimen.dp16)
|
|
||||||
.fillMaxWidth(),
|
|
||||||
colors = ButtonDefaults.outlinedButtonColors(),
|
|
||||||
onClick = onGoogleSignIn,
|
|
||||||
) {
|
|
||||||
Text(text = googleStringResource())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
//////////////////////////////////////
|
|
||||||
// region: Authentication Handlers
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AuthenticationHandler(
|
fun AuthenticationHandler(
|
||||||
viewModel: AuthenticationViewModel = hiltViewModel(),
|
viewModel: AuthenticationViewModel = hiltViewModel(),
|
||||||
onDismissRequest: (StateUio<Unit>) -> Unit = {},
|
onFailure: (AuthenticationErrorUio) -> Unit = { },
|
||||||
onSuccess: () -> Unit = { },
|
onSuccess: (AuthenticationUio.Done) -> Unit = { },
|
||||||
) {
|
) {
|
||||||
viewModel.PrepareLoginWithGoogle()
|
viewModel.PrepareLoginWithGoogle()
|
||||||
|
|
||||||
val state by viewModel.authenticationProcess
|
val state = viewModel.authenticationProcess
|
||||||
StateUioHandler(
|
LaunchedEffect(key1 = state.value) {
|
||||||
state = state,
|
when (val authentication = state.value) {
|
||||||
onDismissRequest = onDismissRequest,
|
is AuthenticationUio.Done -> onSuccess(authentication)
|
||||||
onSuccess = onSuccess,
|
else -> Unit
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
//////////////////////////////////////
|
|
||||||
// region: Content Helper Composable
|
|
||||||
|
|
||||||
@OptIn(ExperimentalComposeUiApi::class)
|
|
||||||
@Composable
|
|
||||||
private fun LoginField(
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
value: String,
|
|
||||||
onValueChange: (String) -> Unit,
|
|
||||||
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
|
||||||
keyboardActions: KeyboardActions = KeyboardActions(),
|
|
||||||
) {
|
|
||||||
TextField(
|
|
||||||
modifier = modifier.autofill(
|
|
||||||
autofillTypes = listOf(AutofillType.Username, AutofillType.EmailAddress),
|
|
||||||
onFill = onValueChange,
|
|
||||||
),
|
|
||||||
value = value,
|
|
||||||
onValueChange = onValueChange,
|
|
||||||
label = { Text(text = stringResource(id = R.string.authentication_login)) },
|
|
||||||
colors = TextFieldDefaults.outlinedTextFieldColors(),
|
|
||||||
maxLines = 1,
|
|
||||||
singleLine = true,
|
|
||||||
keyboardOptions = keyboardOptions,
|
|
||||||
keyboardActions = keyboardActions,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalComposeUiApi::class)
|
|
||||||
@Composable
|
|
||||||
private fun PasswordField(
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
value: String,
|
|
||||||
onValueChange: (String) -> Unit,
|
|
||||||
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
|
||||||
keyboardActions: KeyboardActions = KeyboardActions(),
|
|
||||||
) {
|
|
||||||
var passwordVisibility by remember { mutableStateOf(false) }
|
|
||||||
TextField(
|
|
||||||
modifier = modifier.autofill(
|
|
||||||
autofillTypes = listOf(AutofillType.Password),
|
|
||||||
onFill = onValueChange,
|
|
||||||
),
|
|
||||||
value = value,
|
|
||||||
onValueChange = onValueChange,
|
|
||||||
label = { Text(text = stringResource(id = R.string.authentication_password)) },
|
|
||||||
colors = TextFieldDefaults.outlinedTextFieldColors(),
|
|
||||||
maxLines = 1,
|
|
||||||
singleLine = true,
|
|
||||||
visualTransformation = if (passwordVisibility) VisualTransformation.None else PasswordVisualTransformation(),
|
|
||||||
trailingIcon = {
|
|
||||||
IconButton(onClick = { passwordVisibility = passwordVisibility.not() }) {
|
|
||||||
Icon(
|
|
||||||
imageVector = if (passwordVisibility) Icons.Sharp.VisibilityOff else Icons.Sharp.Visibility,
|
|
||||||
contentDescription = null,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
keyboardOptions = keyboardOptions,
|
|
||||||
keyboardActions = keyboardActions,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun CredentialRemember(
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
value: Boolean,
|
|
||||||
onValueChange: (Boolean) -> Unit,
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = modifier.clickable(
|
|
||||||
interactionSource = remember { MutableInteractionSource() },
|
|
||||||
indication = null,
|
|
||||||
onClick = { onValueChange(value.not()) }
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
Checkbox(
|
|
||||||
modifier = Modifier.align(Alignment.CenterVertically),
|
|
||||||
checked = value,
|
|
||||||
onCheckedChange = null
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.width(MaterialTheme.bibLib.dimen.dp8))
|
|
||||||
|
|
||||||
Text(
|
|
||||||
modifier = Modifier.align(Alignment.CenterVertically),
|
|
||||||
style = MaterialTheme.typography.caption,
|
|
||||||
color = MaterialTheme.colors.onBackground,
|
|
||||||
text = stringResource(id = R.string.authentication_credential_remember)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// endregion
|
|
||||||
//////////////////////////////////////
|
|
||||||
// region: Navigation Helper
|
|
||||||
|
|
||||||
private fun NavHostController.navigateToRegister() {
|
|
||||||
context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(IBibLibClient.REGISTER_URL)))
|
|
||||||
}
|
|
||||||
|
|
||||||
// endregion
|
|
||||||
//////////////////////////////////////
|
|
||||||
// region: AnnotatedString
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
@ReadOnlyComposable
|
|
||||||
fun googleStringResource(): AnnotatedString = buildAnnotatedString {
|
|
||||||
val default = LocalTextStyle.current.toSpanStyle()
|
|
||||||
withStyle(
|
|
||||||
style = default
|
|
||||||
) {
|
|
||||||
append(stringResource(id = R.string.action_google_sign_in))
|
|
||||||
append(" ")
|
|
||||||
}
|
|
||||||
withStyle(
|
|
||||||
style = default.copy(color = GoogleColorPalette.blue, fontWeight = FontWeight.ExtraBold),
|
|
||||||
) {
|
|
||||||
append("G")
|
|
||||||
}
|
|
||||||
withStyle(
|
|
||||||
style = default.copy(color = GoogleColorPalette.red, fontWeight = FontWeight.ExtraBold),
|
|
||||||
) {
|
|
||||||
append("o")
|
|
||||||
}
|
|
||||||
withStyle(
|
|
||||||
style = default.copy(color = GoogleColorPalette.yellow, fontWeight = FontWeight.ExtraBold),
|
|
||||||
) {
|
|
||||||
append("o")
|
|
||||||
}
|
|
||||||
withStyle(
|
|
||||||
style = default.copy(color = GoogleColorPalette.blue, fontWeight = FontWeight.ExtraBold),
|
|
||||||
) {
|
|
||||||
append("g")
|
|
||||||
}
|
|
||||||
withStyle(
|
|
||||||
style = default.copy(color = GoogleColorPalette.green, fontWeight = FontWeight.ExtraBold),
|
|
||||||
) {
|
|
||||||
append("l")
|
|
||||||
}
|
|
||||||
withStyle(
|
|
||||||
style = default.copy(color = GoogleColorPalette.red, fontWeight = FontWeight.ExtraBold),
|
|
||||||
) {
|
|
||||||
append("e")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// endregion
|
|
||||||
|
|
||||||
@OptIn(ExperimentalComposeUiApi::class)
|
|
||||||
fun Modifier.autofill(
|
|
||||||
autofillTypes: List<AutofillType>,
|
|
||||||
onFill: ((String) -> Unit),
|
|
||||||
) = composed {
|
|
||||||
val autofill = LocalAutofill.current
|
|
||||||
val autofillNode = AutofillNode(onFill = onFill, autofillTypes = autofillTypes)
|
|
||||||
LocalAutofillTree.current += autofillNode
|
|
||||||
|
|
||||||
this
|
|
||||||
.onGloballyPositioned {
|
|
||||||
autofillNode.boundingBox = it.boundsInWindow()
|
|
||||||
}
|
}
|
||||||
.onFocusChanged { focusState ->
|
}
|
||||||
autofill?.run {
|
|
||||||
if (focusState.isFocused) {
|
LaunchedEffect("AuthenticationHandlerError") {
|
||||||
requestAutofillForNode(autofillNode)
|
viewModel.authenticationError.collect {
|
||||||
} else {
|
onFailure(it)
|
||||||
cancelAutofillForNode(autofillNode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_NO)
|
fun AuthenticationProgress(
|
||||||
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
viewModel: AuthenticationViewModel = hiltViewModel(),
|
||||||
private fun AuthenticationScreenContentPreview() {
|
) {
|
||||||
BibLibTheme {
|
AnimatedVisibility(
|
||||||
AuthenticationScreenContent(
|
visible = viewModel.isLoading,
|
||||||
login = "",
|
enter = fadeIn(),
|
||||||
onLoginChange = { },
|
exit = fadeOut(),
|
||||||
password = "",
|
) {
|
||||||
onPasswordChange = { },
|
Box(
|
||||||
rememberPassword = true,
|
modifier = Modifier
|
||||||
onRememberPasswordChange = { },
|
.fillMaxSize()
|
||||||
onGoogleSignIn = { },
|
.background(color = ShadowPalette.scrim),
|
||||||
onSignIn = { },
|
contentAlignment = Alignment.Center,
|
||||||
onRegister = { },
|
) {
|
||||||
)
|
CircularProgressIndicator(
|
||||||
|
modifier = Modifier.size(MaterialTheme.bibLib.dimen.dialog.iconSize),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,308 @@
|
||||||
|
package com.pixelized.biblib.ui.screen.authentication
|
||||||
|
|
||||||
|
import android.content.res.Configuration
|
||||||
|
import androidx.compose.foundation.ScrollState
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.*
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.sharp.Visibility
|
||||||
|
import androidx.compose.material.icons.sharp.VisibilityOff
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.autofill.AutofillType
|
||||||
|
import androidx.compose.ui.platform.LocalFocusManager
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
|
import androidx.compose.ui.text.buildAnnotatedString
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
|
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||||
|
import androidx.compose.ui.text.input.VisualTransformation
|
||||||
|
import androidx.compose.ui.text.withStyle
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import com.pixelized.biblib.R
|
||||||
|
import com.pixelized.biblib.ui.composable.animation.AnimatedDelayer
|
||||||
|
import com.pixelized.biblib.ui.composable.animation.AnimatedOffset
|
||||||
|
import com.pixelized.biblib.ui.theme.BibLibTheme
|
||||||
|
import com.pixelized.biblib.ui.theme.color.GoogleColorPalette
|
||||||
|
import com.pixelized.biblib.utils.extention.autofill
|
||||||
|
import com.pixelized.biblib.utils.extention.bibLib
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AuthenticationScreenContent(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
scrollState: ScrollState = rememberScrollState(),
|
||||||
|
login: String,
|
||||||
|
onLoginChange: (String) -> Unit,
|
||||||
|
password: String,
|
||||||
|
onPasswordChange: (String) -> Unit,
|
||||||
|
rememberPassword: Boolean,
|
||||||
|
onRememberPasswordChange: (Boolean) -> Unit,
|
||||||
|
onGoogleSignIn: () -> Unit,
|
||||||
|
onSignIn: () -> Unit,
|
||||||
|
onRegister: () -> Unit,
|
||||||
|
) {
|
||||||
|
val focusManager = LocalFocusManager.current
|
||||||
|
|
||||||
|
AnimatedDelayer(targetState = "AuthenticationScreenContent") {
|
||||||
|
Column(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.verticalScroll(scrollState)
|
||||||
|
) {
|
||||||
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
|
||||||
|
AnimatedOffset(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(all = MaterialTheme.bibLib.dimen.dp16)
|
||||||
|
.align(alignment = Alignment.CenterHorizontally),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
style = MaterialTheme.typography.h4,
|
||||||
|
color = MaterialTheme.colors.onBackground,
|
||||||
|
text = stringResource(id = R.string.authentication_title),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
|
||||||
|
AnimatedOffset(
|
||||||
|
modifier = Modifier.padding(horizontal = MaterialTheme.bibLib.dimen.dp16),
|
||||||
|
) {
|
||||||
|
LoginField(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
value = login,
|
||||||
|
onValueChange = onLoginChange,
|
||||||
|
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(MaterialTheme.bibLib.dimen.dp8))
|
||||||
|
|
||||||
|
AnimatedOffset(
|
||||||
|
modifier = Modifier.padding(horizontal = MaterialTheme.bibLib.dimen.dp16),
|
||||||
|
) {
|
||||||
|
PasswordField(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
value = password,
|
||||||
|
onValueChange = onPasswordChange,
|
||||||
|
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
|
||||||
|
keyboardActions = KeyboardActions { focusManager.clearFocus() },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(MaterialTheme.bibLib.dimen.dp16))
|
||||||
|
|
||||||
|
AnimatedOffset(
|
||||||
|
modifier = Modifier.padding(horizontal = MaterialTheme.bibLib.dimen.dp16),
|
||||||
|
) {
|
||||||
|
CredentialRemember(
|
||||||
|
value = rememberPassword,
|
||||||
|
onValueChange = onRememberPasswordChange,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(MaterialTheme.bibLib.dimen.dp16))
|
||||||
|
|
||||||
|
AnimatedOffset(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = MaterialTheme.bibLib.dimen.dp16)
|
||||||
|
.align(Alignment.End),
|
||||||
|
) {
|
||||||
|
Row {
|
||||||
|
Button(
|
||||||
|
colors = ButtonDefaults.outlinedButtonColors(),
|
||||||
|
onClick = onRegister,
|
||||||
|
) {
|
||||||
|
Text(text = stringResource(id = R.string.action_register))
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.width(MaterialTheme.bibLib.dimen.dp8))
|
||||||
|
|
||||||
|
Button(
|
||||||
|
colors = ButtonDefaults.buttonColors(),
|
||||||
|
onClick = onSignIn,
|
||||||
|
) {
|
||||||
|
Text(text = stringResource(id = R.string.action_login))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.weight(2f))
|
||||||
|
|
||||||
|
AnimatedOffset {
|
||||||
|
Button(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(all = MaterialTheme.bibLib.dimen.dp16)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
colors = ButtonDefaults.outlinedButtonColors(),
|
||||||
|
onClick = onGoogleSignIn,
|
||||||
|
) {
|
||||||
|
Text(text = googleStringResource())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//////////////////////////////////////
|
||||||
|
// region: Content Helper Composable
|
||||||
|
|
||||||
|
@OptIn(ExperimentalComposeUiApi::class)
|
||||||
|
@Composable
|
||||||
|
private fun LoginField(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
value: String,
|
||||||
|
onValueChange: (String) -> Unit,
|
||||||
|
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
||||||
|
keyboardActions: KeyboardActions = KeyboardActions(),
|
||||||
|
) {
|
||||||
|
TextField(
|
||||||
|
modifier = modifier.autofill(
|
||||||
|
autofillTypes = listOf(AutofillType.Username, AutofillType.EmailAddress),
|
||||||
|
onFill = onValueChange,
|
||||||
|
),
|
||||||
|
value = value,
|
||||||
|
onValueChange = onValueChange,
|
||||||
|
label = { Text(text = stringResource(id = R.string.authentication_login)) },
|
||||||
|
colors = TextFieldDefaults.outlinedTextFieldColors(),
|
||||||
|
maxLines = 1,
|
||||||
|
singleLine = true,
|
||||||
|
keyboardOptions = keyboardOptions,
|
||||||
|
keyboardActions = keyboardActions,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalComposeUiApi::class)
|
||||||
|
@Composable
|
||||||
|
private fun PasswordField(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
value: String,
|
||||||
|
onValueChange: (String) -> Unit,
|
||||||
|
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
||||||
|
keyboardActions: KeyboardActions = KeyboardActions(),
|
||||||
|
) {
|
||||||
|
var passwordVisibility by remember { mutableStateOf(false) }
|
||||||
|
TextField(
|
||||||
|
modifier = modifier.autofill(
|
||||||
|
autofillTypes = listOf(AutofillType.Password),
|
||||||
|
onFill = onValueChange,
|
||||||
|
),
|
||||||
|
value = value,
|
||||||
|
onValueChange = onValueChange,
|
||||||
|
label = { Text(text = stringResource(id = R.string.authentication_password)) },
|
||||||
|
colors = TextFieldDefaults.outlinedTextFieldColors(),
|
||||||
|
maxLines = 1,
|
||||||
|
singleLine = true,
|
||||||
|
visualTransformation = if (passwordVisibility) VisualTransformation.None else PasswordVisualTransformation(),
|
||||||
|
trailingIcon = {
|
||||||
|
IconButton(onClick = { passwordVisibility = passwordVisibility.not() }) {
|
||||||
|
Icon(
|
||||||
|
imageVector = if (passwordVisibility) Icons.Sharp.VisibilityOff else Icons.Sharp.Visibility,
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
keyboardOptions = keyboardOptions,
|
||||||
|
keyboardActions = keyboardActions,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun CredentialRemember(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
value: Boolean,
|
||||||
|
onValueChange: (Boolean) -> Unit,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = modifier.clickable(
|
||||||
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
|
indication = null,
|
||||||
|
onClick = { onValueChange(value.not()) }
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Checkbox(
|
||||||
|
modifier = Modifier.align(Alignment.CenterVertically),
|
||||||
|
checked = value,
|
||||||
|
onCheckedChange = null
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.width(MaterialTheme.bibLib.dimen.dp8))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.align(Alignment.CenterVertically),
|
||||||
|
style = MaterialTheme.typography.caption,
|
||||||
|
color = MaterialTheme.colors.onBackground,
|
||||||
|
text = stringResource(id = R.string.authentication_credential_remember)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@ReadOnlyComposable
|
||||||
|
private fun googleStringResource(): AnnotatedString = buildAnnotatedString {
|
||||||
|
val default = LocalTextStyle.current.toSpanStyle()
|
||||||
|
withStyle(
|
||||||
|
style = default
|
||||||
|
) {
|
||||||
|
append(stringResource(id = R.string.action_google_sign_in))
|
||||||
|
append(" ")
|
||||||
|
}
|
||||||
|
withStyle(
|
||||||
|
style = default.copy(color = GoogleColorPalette.blue, fontWeight = FontWeight.ExtraBold),
|
||||||
|
) {
|
||||||
|
append("G")
|
||||||
|
}
|
||||||
|
withStyle(
|
||||||
|
style = default.copy(color = GoogleColorPalette.red, fontWeight = FontWeight.ExtraBold),
|
||||||
|
) {
|
||||||
|
append("o")
|
||||||
|
}
|
||||||
|
withStyle(
|
||||||
|
style = default.copy(color = GoogleColorPalette.yellow, fontWeight = FontWeight.ExtraBold),
|
||||||
|
) {
|
||||||
|
append("o")
|
||||||
|
}
|
||||||
|
withStyle(
|
||||||
|
style = default.copy(color = GoogleColorPalette.blue, fontWeight = FontWeight.ExtraBold),
|
||||||
|
) {
|
||||||
|
append("g")
|
||||||
|
}
|
||||||
|
withStyle(
|
||||||
|
style = default.copy(color = GoogleColorPalette.green, fontWeight = FontWeight.ExtraBold),
|
||||||
|
) {
|
||||||
|
append("l")
|
||||||
|
}
|
||||||
|
withStyle(
|
||||||
|
style = default.copy(color = GoogleColorPalette.red, fontWeight = FontWeight.ExtraBold),
|
||||||
|
) {
|
||||||
|
append("e")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_NO)
|
||||||
|
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
private fun AuthenticationScreenContentPreview() {
|
||||||
|
BibLibTheme {
|
||||||
|
AuthenticationScreenContent(
|
||||||
|
login = "",
|
||||||
|
onLoginChange = { },
|
||||||
|
password = "",
|
||||||
|
onPasswordChange = { },
|
||||||
|
rememberPassword = true,
|
||||||
|
onRememberPasswordChange = { },
|
||||||
|
onGoogleSignIn = { },
|
||||||
|
onSignIn = { },
|
||||||
|
onRegister = { },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,30 +1,30 @@
|
||||||
package com.pixelized.biblib.ui.screen.authentication.viewModel
|
package com.pixelized.biblib.ui.screen.authentication.viewModel
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.ActivityResultLauncher
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.runtime.State
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.google.android.gms.auth.api.signin.GoogleSignIn
|
import com.google.android.gms.auth.api.signin.GoogleSignIn
|
||||||
import com.google.android.gms.common.api.ApiException
|
import com.google.android.gms.common.api.ApiException
|
||||||
|
import com.pixelized.biblib.R
|
||||||
import com.pixelized.biblib.network.client.IBibLibClient
|
import com.pixelized.biblib.network.client.IBibLibClient
|
||||||
import com.pixelized.biblib.network.data.query.AuthLoginQuery
|
import com.pixelized.biblib.network.data.query.AuthLoginQuery
|
||||||
import com.pixelized.biblib.repository.apiCache.IAPICacheRepository
|
|
||||||
import com.pixelized.biblib.repository.book.IBookRepository
|
|
||||||
import com.pixelized.biblib.repository.book.updateBooks
|
|
||||||
import com.pixelized.biblib.repository.credential.ICredentialRepository
|
import com.pixelized.biblib.repository.credential.ICredentialRepository
|
||||||
import com.pixelized.biblib.repository.googleSignIn.IGoogleSingInRepository
|
import com.pixelized.biblib.repository.googleSignIn.IGoogleSingInRepository
|
||||||
import com.pixelized.biblib.ui.composable.StateUio
|
import com.pixelized.biblib.ui.screen.authentication.AuthenticationErrorUio
|
||||||
|
import com.pixelized.biblib.ui.screen.authentication.AuthenticationUio
|
||||||
import com.pixelized.biblib.utils.exception.MissingGoogleTokenException
|
import com.pixelized.biblib.utils.exception.MissingGoogleTokenException
|
||||||
import com.pixelized.biblib.utils.exception.MissingTokenException
|
import com.pixelized.biblib.utils.exception.MissingTokenException
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
|
@ -33,14 +33,19 @@ class AuthenticationViewModel @Inject constructor(
|
||||||
private val credentialRepository: ICredentialRepository,
|
private val credentialRepository: ICredentialRepository,
|
||||||
private val googleSignIn: IGoogleSingInRepository,
|
private val googleSignIn: IGoogleSingInRepository,
|
||||||
private val client: IBibLibClient,
|
private val client: IBibLibClient,
|
||||||
private val bookRepository: IBookRepository,
|
|
||||||
private val apiCache: IAPICacheRepository,
|
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
private var launcher: ActivityResultLauncher<Intent>? = null
|
private var launcher: ActivityResultLauncher<Intent>? = null
|
||||||
|
|
||||||
private var authenticationJob: Job? = null
|
private var authenticationJob: Job? = null
|
||||||
private val _authenticationProcess = mutableStateOf<StateUio<Unit>?>(null)
|
|
||||||
val authenticationProcess: State<StateUio<Unit>?> get() = _authenticationProcess
|
private val _authenticationError = MutableSharedFlow<AuthenticationErrorUio>()
|
||||||
|
val authenticationError: Flow<AuthenticationErrorUio> get() = _authenticationError
|
||||||
|
|
||||||
|
private val _authenticationProcess = mutableStateOf<AuthenticationUio?>(null)
|
||||||
|
val authenticationProcess: State<AuthenticationUio?> get() = _authenticationProcess
|
||||||
|
|
||||||
|
val isLoading: Boolean by derivedStateOf {
|
||||||
|
authenticationProcess.value is AuthenticationUio.Progress
|
||||||
|
}
|
||||||
|
|
||||||
//////////////////////////////////////
|
//////////////////////////////////////
|
||||||
// region: Login with BibLibClient
|
// region: Login with BibLibClient
|
||||||
|
|
@ -48,18 +53,23 @@ class AuthenticationViewModel @Inject constructor(
|
||||||
fun login(login: String, password: String) {
|
fun login(login: String, password: String) {
|
||||||
authenticationJob?.cancel()
|
authenticationJob?.cancel()
|
||||||
authenticationJob = viewModelScope.launch(Dispatchers.IO) {
|
authenticationJob = viewModelScope.launch(Dispatchers.IO) {
|
||||||
val query = AuthLoginQuery(username = login, password = password)
|
_authenticationProcess.value = AuthenticationUio.Progress
|
||||||
_authenticationProcess.value = StateUio.Progress()
|
|
||||||
try {
|
try {
|
||||||
|
val query = AuthLoginQuery(username = login, password = password)
|
||||||
val response = client.service.login(query)
|
val response = client.service.login(query)
|
||||||
val idToken = response.token ?: throw MissingTokenException()
|
val idToken = response.token ?: throw MissingTokenException()
|
||||||
client.token = idToken
|
client.token = idToken
|
||||||
credentialRepository.bearer = response.token
|
credentialRepository.bearer = response.token
|
||||||
updateBooks() // update book if needed
|
_authenticationProcess.value = AuthenticationUio.Done
|
||||||
_authenticationProcess.value = StateUio.Success(Unit)
|
|
||||||
} catch (exception: Exception) {
|
} catch (exception: Exception) {
|
||||||
Log.e("AuthenticationViewModel", exception.message, exception)
|
Log.e("AuthenticationViewModel", exception.message, exception)
|
||||||
_authenticationProcess.value = StateUio.Failure(exception)
|
_authenticationProcess.value = null
|
||||||
|
_authenticationError.emit(
|
||||||
|
AuthenticationErrorUio.Login(
|
||||||
|
message = R.string.error_authentication_message,
|
||||||
|
action = R.string.error_authentication_action,
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -73,47 +83,37 @@ class AuthenticationViewModel @Inject constructor(
|
||||||
launcher = rememberLauncherForActivityResult(
|
launcher = rememberLauncherForActivityResult(
|
||||||
ActivityResultContracts.StartActivityForResult()
|
ActivityResultContracts.StartActivityForResult()
|
||||||
) {
|
) {
|
||||||
authenticationJob?.cancel()
|
if (it.resultCode == Activity.RESULT_OK) {
|
||||||
authenticationJob = viewModelScope.launch(Dispatchers.IO) {
|
authenticationJob?.cancel()
|
||||||
try {
|
authenticationJob = viewModelScope.launch(Dispatchers.IO) {
|
||||||
val task = GoogleSignIn.getSignedInAccountFromIntent(it.data)
|
_authenticationProcess.value = AuthenticationUio.Progress
|
||||||
val account = task.getResult(ApiException::class.java)
|
try {
|
||||||
val googleToken = account?.idToken ?: throw MissingGoogleTokenException()
|
val task = GoogleSignIn.getSignedInAccountFromIntent(it.data)
|
||||||
val response = client.service.loginWithGoogle(token = googleToken)
|
val account = task.getResult(ApiException::class.java)
|
||||||
val token = response.token ?: throw MissingTokenException()
|
val googleToken = account?.idToken ?: throw MissingGoogleTokenException()
|
||||||
client.token = token
|
val response = client.service.loginWithGoogle(token = googleToken)
|
||||||
updateBooks() // update book if needed
|
val token = response.token ?: throw MissingTokenException()
|
||||||
_authenticationProcess.value = StateUio.Success(Unit)
|
client.token = token
|
||||||
} catch (exception: Exception) {
|
_authenticationProcess.value = AuthenticationUio.Done
|
||||||
Log.e("AuthenticationViewModel", exception.message, exception)
|
} catch (exception: Exception) {
|
||||||
_authenticationProcess.value = StateUio.Failure(exception)
|
Log.e("AuthenticationViewModel", exception.message, exception)
|
||||||
|
_authenticationProcess.value = null
|
||||||
|
_authenticationError.emit(
|
||||||
|
AuthenticationErrorUio.GoogleLogin(
|
||||||
|
message = R.string.error_authentication_message,
|
||||||
|
action = R.string.error_authentication_action,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loginWithGoogle() {
|
fun loginWithGoogle() {
|
||||||
_authenticationProcess.value = StateUio.Progress()
|
|
||||||
launcher?.launch(googleSignIn.client.signInIntent)
|
launcher?.launch(googleSignIn.client.signInIntent)
|
||||||
}
|
}
|
||||||
|
|
||||||
// endregion
|
// endregion
|
||||||
//////////////////////////////////////
|
//////////////////////////////////////
|
||||||
// region: Books update
|
|
||||||
|
|
||||||
private suspend fun updateBooks() = updateBooks(
|
|
||||||
client = client,
|
|
||||||
cache = apiCache,
|
|
||||||
repository = bookRepository,
|
|
||||||
)
|
|
||||||
|
|
||||||
// endregion
|
|
||||||
//////////////////////////////////////
|
|
||||||
// region: Dialog
|
|
||||||
|
|
||||||
fun dismissError() {
|
|
||||||
_authenticationProcess.value = null
|
|
||||||
}
|
|
||||||
|
|
||||||
// endregion
|
|
||||||
}
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
package com.pixelized.biblib.ui.screen.home.detail
|
package com.pixelized.biblib.ui.screen.home.detail
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import androidx.compose.animation.animateContentSize
|
import androidx.compose.animation.animateContentSize
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
package com.pixelized.biblib.utils.extention
|
||||||
|
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.autofill.AutofillNode
|
||||||
|
import androidx.compose.ui.autofill.AutofillType
|
||||||
|
import androidx.compose.ui.composed
|
||||||
|
import androidx.compose.ui.focus.onFocusChanged
|
||||||
|
import androidx.compose.ui.layout.boundsInWindow
|
||||||
|
import androidx.compose.ui.layout.onGloballyPositioned
|
||||||
|
import androidx.compose.ui.platform.LocalAutofill
|
||||||
|
import androidx.compose.ui.platform.LocalAutofillTree
|
||||||
|
|
||||||
|
@OptIn(ExperimentalComposeUiApi::class)
|
||||||
|
fun Modifier.autofill(
|
||||||
|
autofillTypes: List<AutofillType>,
|
||||||
|
onFill: ((String) -> Unit),
|
||||||
|
) = composed {
|
||||||
|
val autofill = LocalAutofill.current
|
||||||
|
val autofillTree = LocalAutofillTree.current
|
||||||
|
val autofillNode = remember {
|
||||||
|
AutofillNode(
|
||||||
|
onFill = onFill,
|
||||||
|
autofillTypes = autofillTypes
|
||||||
|
).also {
|
||||||
|
autofillTree += it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return@composed this
|
||||||
|
.onGloballyPositioned {
|
||||||
|
autofillNode.boundingBox = it.boundsInWindow()
|
||||||
|
}
|
||||||
|
.onFocusChanged { focusState ->
|
||||||
|
when (focusState.isFocused) {
|
||||||
|
true -> autofill?.requestAutofillForNode(autofillNode)
|
||||||
|
else -> autofill?.cancelAutofillForNode(autofillNode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -17,10 +17,9 @@
|
||||||
<!-- Error -->
|
<!-- Error -->
|
||||||
|
|
||||||
<string name="error_action_retry">Réessayer</string>
|
<string name="error_action_retry">Réessayer</string>
|
||||||
|
<string name="error_authentication_message">L\'authentification à BibLib à échouée.</string>
|
||||||
<string name="error_get_book_detail_message">La récupération des détails a échouée.</string>
|
<string name="error_get_book_detail_message">La récupération des détails a échouée.</string>
|
||||||
<string name="error_get_book_detail_action">@string/error_action_retry</string>
|
|
||||||
<string name="error_send_book_message">L\'envoi de l\'eBook a échoué.</string>
|
<string name="error_send_book_message">L\'envoi de l\'eBook a échoué.</string>
|
||||||
<string name="error_send_book_action">@string/error_action_retry</string>
|
|
||||||
|
|
||||||
<!-- Dialogs -->
|
<!-- Dialogs -->
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,10 +23,12 @@
|
||||||
<!-- Error -->
|
<!-- Error -->
|
||||||
|
|
||||||
<string name="error_action_retry">Retry</string>
|
<string name="error_action_retry">Retry</string>
|
||||||
|
<string name="error_authentication_message">Failed to connect to BibLib</string>
|
||||||
|
<string name="error_authentication_action" translatable="false">@string/error_action_retry</string>
|
||||||
<string name="error_get_book_detail_message">Failed to retrieve book details.</string>
|
<string name="error_get_book_detail_message">Failed to retrieve book details.</string>
|
||||||
<string name="error_get_book_detail_action">@string/error_action_retry</string>
|
<string name="error_get_book_detail_action" translatable="false">@string/error_action_retry</string>
|
||||||
<string name="error_send_book_message">Failed to send the book to your kindle.</string>
|
<string name="error_send_book_message">Failed to send the book to your kindle.</string>
|
||||||
<string name="error_send_book_action">@string/error_action_retry</string>
|
<string name="error_send_book_action" translatable="false">@string/error_action_retry</string>
|
||||||
|
|
||||||
<!-- Dialogs -->
|
<!-- Dialogs -->
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue