From 59d84963a94617fa6840cc6e0a3e1445ca670e66 Mon Sep 17 00:00:00 2001 From: Thomas Andres Gomez Date: Sat, 8 May 2021 21:07:17 +0200 Subject: [PATCH] Add loading and error dialogs for authentication. --- .idea/misc.xml | 12 +++ .../biblib/network/client/BibLibClient.kt | 2 +- .../biblib/network/client/IBibLibClient.kt | 2 +- .../ui/composable/items/WaitingComposable.kt | 100 ------------------ .../ui/composable/items/dialog/ErrorCard.kt | 82 ++++++++++++++ .../ui/composable/items/dialog/LoadingCard.kt | 68 ++++++++++++ .../items/dialog/OverlayComposable.kt | 43 ++++++++ .../screen/LoginScreenComposable.kt | 66 ++++++++++-- .../authentication/AuthenticationViewModel.kt | 36 +++++-- .../authentication/IAuthentication.kt | 10 +- app/src/main/res/values/strings.xml | 2 + 11 files changed, 296 insertions(+), 127 deletions(-) delete mode 100644 app/src/main/java/com/pixelized/biblib/ui/composable/items/WaitingComposable.kt create mode 100644 app/src/main/java/com/pixelized/biblib/ui/composable/items/dialog/ErrorCard.kt create mode 100644 app/src/main/java/com/pixelized/biblib/ui/composable/items/dialog/LoadingCard.kt create mode 100644 app/src/main/java/com/pixelized/biblib/ui/composable/items/dialog/OverlayComposable.kt diff --git a/.idea/misc.xml b/.idea/misc.xml index 5a9794c..a0d7d47 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -107,6 +107,18 @@ + + + + + + + + + + + + diff --git a/app/src/main/java/com/pixelized/biblib/network/client/BibLibClient.kt b/app/src/main/java/com/pixelized/biblib/network/client/BibLibClient.kt index 679a9c8..2add48a 100644 --- a/app/src/main/java/com/pixelized/biblib/network/client/BibLibClient.kt +++ b/app/src/main/java/com/pixelized/biblib/network/client/BibLibClient.kt @@ -22,7 +22,7 @@ class BibLibClient : IBibLibClient { /////////////////////////////////// // region BibLib webservice Auth - override fun updateBearerToken(token: String?) { + override fun updateBearerToken(token: String) { interceptor.token = token } diff --git a/app/src/main/java/com/pixelized/biblib/network/client/IBibLibClient.kt b/app/src/main/java/com/pixelized/biblib/network/client/IBibLibClient.kt index 7fdc9fa..d5028cb 100644 --- a/app/src/main/java/com/pixelized/biblib/network/client/IBibLibClient.kt +++ b/app/src/main/java/com/pixelized/biblib/network/client/IBibLibClient.kt @@ -4,7 +4,7 @@ interface IBibLibClient { val service: IBibLibWebServiceAPI - fun updateBearerToken(token: String?) + fun updateBearerToken(token: String) companion object { const val BASE_URL = "https://bib.bibulle.fr" diff --git a/app/src/main/java/com/pixelized/biblib/ui/composable/items/WaitingComposable.kt b/app/src/main/java/com/pixelized/biblib/ui/composable/items/WaitingComposable.kt deleted file mode 100644 index 237c5ff..0000000 --- a/app/src/main/java/com/pixelized/biblib/ui/composable/items/WaitingComposable.kt +++ /dev/null @@ -1,100 +0,0 @@ -package com.pixelized.biblib.ui.composable.items - -import androidx.compose.animation.Crossfade -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.material.Card -import androidx.compose.material.CircularProgressIndicator -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.pixelized.biblib.R -import com.pixelized.biblib.ui.theme.BibLibTheme - - -@Preview -@Composable -fun WaitingComposableLightPreview() { - BibLibTheme(darkTheme = false) { - WaitingComposable( - visible = true, - message = stringResource(id = R.string.loading) - ) - } -} - -@Preview -@Composable -fun WaitingComposableDarkPreview() { - BibLibTheme(darkTheme = true) { - WaitingComposable( - visible = true, - message = stringResource(id = R.string.loading) - ) - } -} - -@Composable -fun WaitingComposable( - visible: Boolean, - modifier: Modifier = Modifier, - message: String? = null -) { - Crossfade( - modifier = modifier, - targetState = visible - ) { - if (it) { - Box( - modifier = Modifier - .fillMaxWidth() - .fillMaxHeight() - .clickable { } - ) { - Box( - modifier = Modifier - .fillMaxWidth() - .fillMaxHeight() - .alpha(.25f) - .background(Color.Black) - ) - Card( - elevation = 8.dp, - modifier = Modifier.align(Alignment.Center) - ) { - Column( - modifier = Modifier - .width(200.dp) - .padding(16.dp) - ) { - CircularProgressIndicator( - modifier = Modifier - .align(Alignment.CenterHorizontally) - .padding(16.dp) - ) - if (message?.isNotEmpty() == true) { - val typography = MaterialTheme.typography - Text( - modifier = Modifier.align(Alignment.CenterHorizontally), - style = typography.body1, - textAlign = TextAlign.Center, - text = message - ) - } - } - } - } - } else { - Box {} - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/biblib/ui/composable/items/dialog/ErrorCard.kt b/app/src/main/java/com/pixelized/biblib/ui/composable/items/dialog/ErrorCard.kt new file mode 100644 index 0000000..5d2ce6f --- /dev/null +++ b/app/src/main/java/com/pixelized/biblib/ui/composable/items/dialog/ErrorCard.kt @@ -0,0 +1,82 @@ +package com.pixelized.biblib.ui.composable.items.dialog + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material.Card +import androidx.compose.material.Icon +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.sharp.ErrorOutline +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.pixelized.biblib.R +import com.pixelized.biblib.ui.theme.BibLibTheme +import com.pixelized.biblib.utils.exception.NoBearerException + + +@Composable +fun ErrorCard( + modifier: Modifier = Modifier, + message: String = stringResource(id = R.string.error_generic), + exception: Exception? = null, +) { + Card(elevation = 8.dp, modifier = modifier) { + Column( + modifier = Modifier + .width(200.dp) + .padding(16.dp) + ) { + Icon( + modifier = Modifier + .width(72.dp) + .height(72.dp) + .align(Alignment.CenterHorizontally) + .padding(16.dp), + tint = MaterialTheme.colors.error, + imageVector = Icons.Sharp.ErrorOutline, + contentDescription = "error" + ) + val typography = MaterialTheme.typography + Text( + modifier = Modifier.align(Alignment.CenterHorizontally), + style = typography.body1, + textAlign = TextAlign.Center, + text = message + ) + if (exception != null) { + Text( + modifier = Modifier + .padding(top = 8.dp) + .align(Alignment.CenterHorizontally), + style = typography.caption, + textAlign = TextAlign.Center, + text = exception::class.java.simpleName + ) + } + } + } +} + +@Preview +@Composable +private fun ErrorCardLightPreview() { + BibLibTheme(darkTheme = false) { + ErrorCard(exception = NoBearerException()) + } +} + +@Preview +@Composable +private fun ErrorCardDarkPreview() { + BibLibTheme(darkTheme = true) { + ErrorCard(exception = NoBearerException()) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/biblib/ui/composable/items/dialog/LoadingCard.kt b/app/src/main/java/com/pixelized/biblib/ui/composable/items/dialog/LoadingCard.kt new file mode 100644 index 0000000..3f4bbd0 --- /dev/null +++ b/app/src/main/java/com/pixelized/biblib/ui/composable/items/dialog/LoadingCard.kt @@ -0,0 +1,68 @@ +package com.pixelized.biblib.ui.composable.items.dialog + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material.Card +import androidx.compose.material.CircularProgressIndicator +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.pixelized.biblib.R +import com.pixelized.biblib.ui.theme.BibLibTheme + + +@Composable +fun LoadingCard( + modifier: Modifier = Modifier, + message: String? = null +) { + Card(elevation = 8.dp, modifier = modifier) { + Column( + modifier = Modifier + .width(200.dp) + .padding(16.dp) + ) { + CircularProgressIndicator( + modifier = Modifier + .align(Alignment.CenterHorizontally) + .padding(16.dp) + ) + if (message?.isNotEmpty() == true) { + val typography = MaterialTheme.typography + Text( + modifier = Modifier.align(Alignment.CenterHorizontally), + style = typography.body1, + textAlign = TextAlign.Center, + text = message + ) + } + } + } +} + +@Preview +@Composable +private fun LoadingCardLightPreview() { + BibLibTheme(darkTheme = false) { + LoadingCard( + message = stringResource(id = R.string.loading) + ) + } +} + +@Preview +@Composable +private fun LoadingCardDarkPreview() { + BibLibTheme(darkTheme = true) { + LoadingCard( + message = stringResource(id = R.string.loading) + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/biblib/ui/composable/items/dialog/OverlayComposable.kt b/app/src/main/java/com/pixelized/biblib/ui/composable/items/dialog/OverlayComposable.kt new file mode 100644 index 0000000..7df10d4 --- /dev/null +++ b/app/src/main/java/com/pixelized/biblib/ui/composable/items/dialog/OverlayComposable.kt @@ -0,0 +1,43 @@ +package com.pixelized.biblib.ui.composable.items.dialog + +import androidx.compose.animation.Crossfade +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.graphics.Color + +@Composable +fun CrossFadeOverlay( + visible: Boolean, + content: @Composable BoxScope.() -> Unit +) { + Crossfade(targetState = visible) { + if (it) { + Box( + modifier = Modifier + .fillMaxWidth() + .fillMaxHeight() + .clickable { } + ) { + // Transparent background. + Box( + modifier = Modifier + .fillMaxWidth() + .fillMaxHeight() + .alpha(.25f) + .background(Color.Black) + ) + // Overlay content. + content() + } + } else { + Box {} + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/biblib/ui/composable/screen/LoginScreenComposable.kt b/app/src/main/java/com/pixelized/biblib/ui/composable/screen/LoginScreenComposable.kt index 1ae21f8..4d3fa27 100644 --- a/app/src/main/java/com/pixelized/biblib/ui/composable/screen/LoginScreenComposable.kt +++ b/app/src/main/java/com/pixelized/biblib/ui/composable/screen/LoginScreenComposable.kt @@ -30,12 +30,15 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import com.pixelized.biblib.R -import com.pixelized.biblib.ui.composable.items.WaitingComposable +import com.pixelized.biblib.ui.composable.items.dialog.CrossFadeOverlay +import com.pixelized.biblib.ui.composable.items.dialog.ErrorCard +import com.pixelized.biblib.ui.composable.items.dialog.LoadingCard import com.pixelized.biblib.ui.theme.BibLibTheme import com.pixelized.biblib.ui.viewmodel.authentication.AuthenticationViewModel import com.pixelized.biblib.ui.viewmodel.authentication.IAuthentication import com.pixelized.biblib.ui.viewmodel.navigation.INavigation import com.pixelized.biblib.ui.viewmodel.navigation.NavigationViewModel +import com.pixelized.biblib.utils.exception.NoBearerException @Composable @@ -49,14 +52,15 @@ fun LoginScreenComposable( .fillMaxHeight() ) { authentication.PrepareLoginWithGoogle() - LoginScreenContentComposable(navigation, authentication) - LoginScreenWaitingComposable(authentication) + LoginScreenNavigationComposable(navigation, authentication) + + LoginScreenContentComposable(authentication) + LoginScreenDialogComposable(authentication) } } @Composable private fun LoginScreenContentComposable( - navigation: INavigation, authentication: IAuthentication, ) { val typography = MaterialTheme.typography @@ -142,12 +146,40 @@ private fun LoginScreenContentComposable( } @Composable -private fun LoginScreenWaitingComposable(authentication: IAuthentication) { +private fun LoginScreenDialogComposable( + authentication: IAuthentication +) { val state = authentication.state.observeAsState() - WaitingComposable( - visible = state.value is IAuthentication.State.Loading, - message = stringResource(id = R.string.loading) - ) + CrossFadeOverlay( + visible = (state.value is IAuthentication.State.Initial).not() + ) { + when (val currentState = state.value) { + is IAuthentication.State.Error -> ErrorCard( + modifier = Modifier + .align(Alignment.Center) + .clickable { authentication.clearState() }, + message = stringResource(id = R.string.error_generic), + exception = currentState.exception + ) + is IAuthentication.State.Connect, + is IAuthentication.State.Loading -> LoadingCard( + modifier = Modifier.align(Alignment.Center), + message = stringResource(id = R.string.loading) + ) + else -> Box {} + } + } +} + +@Composable +private fun LoginScreenNavigationComposable( + navigation: INavigation, + authentication: IAuthentication +) { + val state = authentication.state.observeAsState() + if (state.value == IAuthentication.State.Connect) { + navigation.navigateTo(INavigation.Screen.MainScreen) + } } @Composable @@ -237,12 +269,24 @@ fun LoginScreenComposablePreview() { } } -@Preview +@Preview(name = "Loading") @Composable fun LoginScreenComposableWaitingPreview() { BibLibTheme { val navigationViewModel = INavigation.Mock() - val authenticationViewModel = IAuthentication.Mock(true) + val state = IAuthentication.State.Loading + val authenticationViewModel = IAuthentication.Mock(state) + LoginScreenComposable(navigationViewModel, authenticationViewModel) + } +} + +@Preview(name = "Error") +@Composable +fun LoginScreenComposableErrorPreview() { + BibLibTheme { + val navigationViewModel = INavigation.Mock() + val state = IAuthentication.State.Error(NoBearerException()) + val authenticationViewModel = IAuthentication.Mock(state) LoginScreenComposable(navigationViewModel, authenticationViewModel) } } \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/biblib/ui/viewmodel/authentication/AuthenticationViewModel.kt b/app/src/main/java/com/pixelized/biblib/ui/viewmodel/authentication/AuthenticationViewModel.kt index 09c9ec3..6a1a0f7 100644 --- a/app/src/main/java/com/pixelized/biblib/ui/viewmodel/authentication/AuthenticationViewModel.kt +++ b/app/src/main/java/com/pixelized/biblib/ui/viewmodel/authentication/AuthenticationViewModel.kt @@ -1,7 +1,6 @@ package com.pixelized.biblib.ui.viewmodel.authentication import android.content.Intent -import android.util.Log import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts @@ -14,12 +13,12 @@ import com.google.android.gms.auth.api.signin.GoogleSignIn import com.google.android.gms.auth.api.signin.GoogleSignInAccount import com.google.android.gms.common.api.ApiException import com.pixelized.biblib.data.network.query.AuthLoginQuery -import com.pixelized.biblib.utils.injection.inject import com.pixelized.biblib.network.client.IBibLibClient import com.pixelized.biblib.repository.credential.ICredentialRepository import com.pixelized.biblib.repository.googlesignin.IGoogleSingInRepository import com.pixelized.biblib.ui.viewmodel.authentication.IAuthentication.State import com.pixelized.biblib.utils.exception.MissingTokenException +import com.pixelized.biblib.utils.injection.inject import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -61,10 +60,13 @@ class AuthenticationViewModel : ViewModel(), IAuthentication { } override fun updateRememberCredential(rememberCredential: Boolean) { - _rememberCredential.postValue(rememberCredential) viewModelScope.launch { + _rememberCredential.postValue(rememberCredential) credentialRepository.rememberCredential = rememberCredential - if (rememberCredential.not()) { + if (rememberCredential) { + credentialRepository.login = login.value + credentialRepository.password = password.value + } else { credentialRepository.login = null credentialRepository.password = null } @@ -75,24 +77,37 @@ class AuthenticationViewModel : ViewModel(), IAuthentication { viewModelScope.launch { _state.postValue(State.Loading) delay(3000) - _state.postValue(State.Initial) + _state.postValue(State.Error(MissingTokenException())) } } + override fun clearState() { + _state.postValue(State.Initial) + } + override fun login() { viewModelScope.launch(Dispatchers.IO) { + // TODO : validation ! if (rememberCredential.value == true) { credentialRepository.login = login.value credentialRepository.password = password.value + } else { + credentialRepository.login = null + credentialRepository.password = null } - // TODO : validation ! val query = AuthLoginQuery( username = login.value, password = password.value ) - // TODO : Repository (token management & co) - val response = client.service.login(query) - Log.e("pouet", response.toString()) + _state.postValue(State.Loading) + try { + val response = client.service.login(query) + val idToken = response.token ?: throw MissingTokenException() + client.updateBearerToken(idToken) + _state.postValue(State.Connect) + } catch (exception: Exception) { + _state.postValue(State.Error(exception)) + } } } @@ -105,7 +120,8 @@ class AuthenticationViewModel : ViewModel(), IAuthentication { val task = GoogleSignIn.getSignedInAccountFromIntent(it.data) val account: GoogleSignInAccount? = task.getResult(ApiException::class.java) val idToken = account?.idToken ?: throw MissingTokenException() - _state.postValue(State.Connect(idToken)) + client.updateBearerToken(idToken) + _state.postValue(State.Connect) } catch (exception: Exception) { _state.postValue(State.Error(exception)) } diff --git a/app/src/main/java/com/pixelized/biblib/ui/viewmodel/authentication/IAuthentication.kt b/app/src/main/java/com/pixelized/biblib/ui/viewmodel/authentication/IAuthentication.kt index 402e506..85dbd90 100644 --- a/app/src/main/java/com/pixelized/biblib/ui/viewmodel/authentication/IAuthentication.kt +++ b/app/src/main/java/com/pixelized/biblib/ui/viewmodel/authentication/IAuthentication.kt @@ -13,6 +13,7 @@ interface IAuthentication { fun updateLoginField(login: String) fun updatePasswordField(password: String) fun updateRememberCredential(rememberCredential: Boolean) + fun clearState() fun register() fun login() @@ -24,13 +25,12 @@ interface IAuthentication { sealed class State { object Initial : State() object Loading : State() - data class Connect(val token: String) : State() + object Connect : State() data class Error(val exception: Exception) : State() } - class Mock(waiting: Boolean = false) : IAuthentication { - override val state: LiveData = - MutableLiveData(if (waiting) State.Loading else State.Initial) + class Mock(state: State = State.Initial) : IAuthentication { + override val state: LiveData = MutableLiveData(state) override val login: LiveData = MutableLiveData("") override val password: LiveData = MutableLiveData("") override val rememberCredential: LiveData = MutableLiveData(true) @@ -38,6 +38,8 @@ interface IAuthentication { override fun updateLoginField(login: String) = Unit override fun updatePasswordField(password: String) = Unit override fun updateRememberCredential(rememberCredential: Boolean) = Unit + override fun clearState() = Unit + override fun register() = Unit override fun login() = Unit diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 447fae5..c92c441 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -7,6 +7,8 @@ Entering the Imperial Library of Trantor. + Oops! + Sign in to BibLib Login