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
 | 
			
		||||
 | 
			
		||||
import androidx.compose.foundation.layout.imePadding
 | 
			
		||||
import androidx.compose.foundation.layout.systemBarsPadding
 | 
			
		||||
import androidx.compose.material.*
 | 
			
		||||
import androidx.compose.runtime.Composable
 | 
			
		||||
| 
						 | 
				
			
			@ -11,7 +12,6 @@ import com.bumptech.glide.load.engine.DiskCacheStrategy
 | 
			
		|||
import com.bumptech.glide.request.RequestOptions
 | 
			
		||||
import com.pixelized.biblib.ui.navigation.ScreenNavHost
 | 
			
		||||
import com.pixelized.biblib.ui.screen.launch.LauncherViewModel
 | 
			
		||||
import com.pixelized.biblib.utils.extention.bibLib
 | 
			
		||||
import com.skydoves.landscapist.glide.LocalGlideRequestOptions
 | 
			
		||||
 | 
			
		||||
val LocalSnackHostState = staticCompositionLocalOf<SnackbarHostState> {
 | 
			
		||||
| 
						 | 
				
			
			@ -33,7 +33,9 @@ fun MainContent(
 | 
			
		|||
            scaffoldState = scaffoldState,
 | 
			
		||||
            snackbarHost = { snackHostState ->
 | 
			
		||||
                SnackbarHost(
 | 
			
		||||
                    modifier = Modifier.systemBarsPadding(),
 | 
			
		||||
                    modifier = Modifier
 | 
			
		||||
                        .systemBarsPadding()
 | 
			
		||||
                        .imePadding(),
 | 
			
		||||
                    hostState = snackHostState,
 | 
			
		||||
                ) { snackBarData ->
 | 
			
		||||
                    Snackbar(
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,56 +1,65 @@
 | 
			
		|||
package com.pixelized.biblib.ui.screen.authentication
 | 
			
		||||
 | 
			
		||||
import android.content.Intent
 | 
			
		||||
import android.content.res.Configuration
 | 
			
		||||
import android.net.Uri
 | 
			
		||||
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.annotation.StringRes
 | 
			
		||||
import androidx.compose.animation.AnimatedVisibility
 | 
			
		||||
import androidx.compose.animation.fadeIn
 | 
			
		||||
import androidx.compose.animation.fadeOut
 | 
			
		||||
import androidx.compose.foundation.background
 | 
			
		||||
import androidx.compose.foundation.layout.Box
 | 
			
		||||
import androidx.compose.foundation.layout.fillMaxSize
 | 
			
		||||
import androidx.compose.foundation.layout.size
 | 
			
		||||
import androidx.compose.foundation.layout.systemBarsPadding
 | 
			
		||||
import androidx.compose.material.CircularProgressIndicator
 | 
			
		||||
import androidx.compose.material.MaterialTheme
 | 
			
		||||
import androidx.compose.material.SnackbarResult
 | 
			
		||||
import androidx.compose.runtime.*
 | 
			
		||||
import androidx.compose.ui.Alignment
 | 
			
		||||
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
 | 
			
		||||
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.compose.ui.platform.LocalContext
 | 
			
		||||
import androidx.compose.ui.platform.LocalUriHandler
 | 
			
		||||
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.ui.composable.StateUio
 | 
			
		||||
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.LocalSnackHostState
 | 
			
		||||
import com.pixelized.biblib.ui.navigation.LocalScreenNavHostController
 | 
			
		||||
import com.pixelized.biblib.ui.navigation.navigateToHome
 | 
			
		||||
import com.pixelized.biblib.ui.screen.authentication.viewModel.AuthenticationFormViewModel
 | 
			
		||||
import com.pixelized.biblib.ui.screen.authentication.viewModel.AuthenticationViewModel
 | 
			
		||||
import com.pixelized.biblib.ui.theme.BibLibTheme
 | 
			
		||||
import com.pixelized.biblib.ui.theme.color.GoogleColorPalette
 | 
			
		||||
import com.pixelized.biblib.ui.theme.color.ShadowPalette
 | 
			
		||||
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
 | 
			
		||||
fun AuthenticationScreen(
 | 
			
		||||
| 
						 | 
				
			
			@ -58,6 +67,12 @@ fun AuthenticationScreen(
 | 
			
		|||
    formViewModel: AuthenticationFormViewModel = hiltViewModel(),
 | 
			
		||||
) {
 | 
			
		||||
    val navHostController = LocalScreenNavHostController.current
 | 
			
		||||
    val snackBarHostState = LocalSnackHostState.current
 | 
			
		||||
    val uriHandler = LocalUriHandler.current
 | 
			
		||||
    val context = LocalContext.current
 | 
			
		||||
    val scope = rememberCoroutineScope()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    AuthenticationScreenContent(
 | 
			
		||||
        modifier = Modifier.systemBarsPadding(),
 | 
			
		||||
| 
						 | 
				
			
			@ -83,345 +98,81 @@ fun AuthenticationScreen(
 | 
			
		|||
            )
 | 
			
		||||
        },
 | 
			
		||||
        onRegister = {
 | 
			
		||||
            navHostController.navigateToRegister()
 | 
			
		||||
            uriHandler.openUri(uri = IBibLibClient.REGISTER_URL)
 | 
			
		||||
        },
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    AuthenticationHandler(
 | 
			
		||||
        onDismissRequest = {
 | 
			
		||||
            if (it is StateUio.Failure) authenticationViewModel.dismissError()
 | 
			
		||||
        onFailure = {
 | 
			
		||||
            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 = {
 | 
			
		||||
            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
 | 
			
		||||
fun AuthenticationHandler(
 | 
			
		||||
    viewModel: AuthenticationViewModel = hiltViewModel(),
 | 
			
		||||
    onDismissRequest: (StateUio<Unit>) -> Unit = {},
 | 
			
		||||
    onSuccess: () -> Unit = { },
 | 
			
		||||
    onFailure: (AuthenticationErrorUio) -> Unit = { },
 | 
			
		||||
    onSuccess: (AuthenticationUio.Done) -> Unit = { },
 | 
			
		||||
) {
 | 
			
		||||
    viewModel.PrepareLoginWithGoogle()
 | 
			
		||||
 | 
			
		||||
    val state by viewModel.authenticationProcess
 | 
			
		||||
    StateUioHandler(
 | 
			
		||||
        state = state,
 | 
			
		||||
        onDismissRequest = onDismissRequest,
 | 
			
		||||
        onSuccess = onSuccess,
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
//////////////////////////////////////
 | 
			
		||||
// 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()
 | 
			
		||||
    val state = viewModel.authenticationProcess
 | 
			
		||||
    LaunchedEffect(key1 = state.value) {
 | 
			
		||||
        when (val authentication = state.value) {
 | 
			
		||||
            is AuthenticationUio.Done -> onSuccess(authentication)
 | 
			
		||||
            else -> Unit
 | 
			
		||||
        }
 | 
			
		||||
        .onFocusChanged { focusState ->
 | 
			
		||||
            autofill?.run {
 | 
			
		||||
                if (focusState.isFocused) {
 | 
			
		||||
                    requestAutofillForNode(autofillNode)
 | 
			
		||||
                } else {
 | 
			
		||||
                    cancelAutofillForNode(autofillNode)
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    LaunchedEffect("AuthenticationHandlerError") {
 | 
			
		||||
        viewModel.authenticationError.collect {
 | 
			
		||||
            onFailure(it)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@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 = { },
 | 
			
		||||
        )
 | 
			
		||||
fun AuthenticationProgress(
 | 
			
		||||
    viewModel: AuthenticationViewModel = hiltViewModel(),
 | 
			
		||||
) {
 | 
			
		||||
    AnimatedVisibility(
 | 
			
		||||
        visible = viewModel.isLoading,
 | 
			
		||||
        enter = fadeIn(),
 | 
			
		||||
        exit = fadeOut(),
 | 
			
		||||
    ) {
 | 
			
		||||
        Box(
 | 
			
		||||
            modifier = Modifier
 | 
			
		||||
                .fillMaxSize()
 | 
			
		||||
                .background(color = ShadowPalette.scrim),
 | 
			
		||||
            contentAlignment = Alignment.Center,
 | 
			
		||||
        ) {
 | 
			
		||||
            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
 | 
			
		||||
 | 
			
		||||
import android.app.Activity
 | 
			
		||||
import android.content.Intent
 | 
			
		||||
import android.util.Log
 | 
			
		||||
import androidx.activity.compose.rememberLauncherForActivityResult
 | 
			
		||||
import androidx.activity.result.ActivityResultLauncher
 | 
			
		||||
import androidx.activity.result.contract.ActivityResultContracts
 | 
			
		||||
import androidx.compose.runtime.Composable
 | 
			
		||||
import androidx.compose.runtime.State
 | 
			
		||||
import androidx.compose.runtime.mutableStateOf
 | 
			
		||||
import androidx.compose.runtime.*
 | 
			
		||||
import androidx.lifecycle.ViewModel
 | 
			
		||||
import androidx.lifecycle.viewModelScope
 | 
			
		||||
import com.google.android.gms.auth.api.signin.GoogleSignIn
 | 
			
		||||
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.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.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.MissingTokenException
 | 
			
		||||
import dagger.hilt.android.lifecycle.HiltViewModel
 | 
			
		||||
import kotlinx.coroutines.Dispatchers
 | 
			
		||||
import kotlinx.coroutines.Job
 | 
			
		||||
import kotlinx.coroutines.flow.Flow
 | 
			
		||||
import kotlinx.coroutines.flow.MutableSharedFlow
 | 
			
		||||
import kotlinx.coroutines.launch
 | 
			
		||||
import javax.inject.Inject
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -33,14 +33,19 @@ class AuthenticationViewModel @Inject constructor(
 | 
			
		|||
    private val credentialRepository: ICredentialRepository,
 | 
			
		||||
    private val googleSignIn: IGoogleSingInRepository,
 | 
			
		||||
    private val client: IBibLibClient,
 | 
			
		||||
    private val bookRepository: IBookRepository,
 | 
			
		||||
    private val apiCache: IAPICacheRepository,
 | 
			
		||||
) : ViewModel() {
 | 
			
		||||
    private var launcher: ActivityResultLauncher<Intent>? = 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
 | 
			
		||||
| 
						 | 
				
			
			@ -48,18 +53,23 @@ class AuthenticationViewModel @Inject constructor(
 | 
			
		|||
    fun login(login: String, password: String) {
 | 
			
		||||
        authenticationJob?.cancel()
 | 
			
		||||
        authenticationJob = viewModelScope.launch(Dispatchers.IO) {
 | 
			
		||||
            val query = AuthLoginQuery(username = login, password = password)
 | 
			
		||||
            _authenticationProcess.value = StateUio.Progress()
 | 
			
		||||
            _authenticationProcess.value = AuthenticationUio.Progress
 | 
			
		||||
            try {
 | 
			
		||||
                val query = AuthLoginQuery(username = login, password = password)
 | 
			
		||||
                val response = client.service.login(query)
 | 
			
		||||
                val idToken = response.token ?: throw MissingTokenException()
 | 
			
		||||
                client.token = idToken
 | 
			
		||||
                credentialRepository.bearer = response.token
 | 
			
		||||
                updateBooks() // update book if needed
 | 
			
		||||
                _authenticationProcess.value = StateUio.Success(Unit)
 | 
			
		||||
                _authenticationProcess.value = AuthenticationUio.Done
 | 
			
		||||
            } catch (exception: 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(
 | 
			
		||||
            ActivityResultContracts.StartActivityForResult()
 | 
			
		||||
        ) {
 | 
			
		||||
            authenticationJob?.cancel()
 | 
			
		||||
            authenticationJob = viewModelScope.launch(Dispatchers.IO) {
 | 
			
		||||
                try {
 | 
			
		||||
                    val task = GoogleSignIn.getSignedInAccountFromIntent(it.data)
 | 
			
		||||
                    val account = task.getResult(ApiException::class.java)
 | 
			
		||||
                    val googleToken = account?.idToken ?: throw MissingGoogleTokenException()
 | 
			
		||||
                    val response = client.service.loginWithGoogle(token = googleToken)
 | 
			
		||||
                    val token = response.token ?: throw MissingTokenException()
 | 
			
		||||
                    client.token = token
 | 
			
		||||
                    updateBooks() // update book if needed
 | 
			
		||||
                    _authenticationProcess.value = StateUio.Success(Unit)
 | 
			
		||||
                } catch (exception: Exception) {
 | 
			
		||||
                    Log.e("AuthenticationViewModel", exception.message, exception)
 | 
			
		||||
                    _authenticationProcess.value = StateUio.Failure(exception)
 | 
			
		||||
            if (it.resultCode == Activity.RESULT_OK) {
 | 
			
		||||
                authenticationJob?.cancel()
 | 
			
		||||
                authenticationJob = viewModelScope.launch(Dispatchers.IO) {
 | 
			
		||||
                    _authenticationProcess.value = AuthenticationUio.Progress
 | 
			
		||||
                    try {
 | 
			
		||||
                        val task = GoogleSignIn.getSignedInAccountFromIntent(it.data)
 | 
			
		||||
                        val account = task.getResult(ApiException::class.java)
 | 
			
		||||
                        val googleToken = account?.idToken ?: throw MissingGoogleTokenException()
 | 
			
		||||
                        val response = client.service.loginWithGoogle(token = googleToken)
 | 
			
		||||
                        val token = response.token ?: throw MissingTokenException()
 | 
			
		||||
                        client.token = token
 | 
			
		||||
                        _authenticationProcess.value = AuthenticationUio.Done
 | 
			
		||||
                    } catch (exception: 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() {
 | 
			
		||||
        _authenticationProcess.value = StateUio.Progress()
 | 
			
		||||
        launcher?.launch(googleSignIn.client.signInIntent)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // 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
 | 
			
		||||
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import androidx.compose.animation.animateContentSize
 | 
			
		||||
import androidx.compose.foundation.layout.*
 | 
			
		||||
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 -->
 | 
			
		||||
 | 
			
		||||
    <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_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_action">@string/error_action_retry</string>
 | 
			
		||||
 | 
			
		||||
    <!--  Dialogs -->
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -23,10 +23,12 @@
 | 
			
		|||
    <!-- Error -->
 | 
			
		||||
 | 
			
		||||
    <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_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_action">@string/error_action_retry</string>
 | 
			
		||||
    <string name="error_send_book_action" translatable="false">@string/error_action_retry</string>
 | 
			
		||||
 | 
			
		||||
    <!--  Dialogs -->
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue