Add logout.

This commit is contained in:
Thomas Andres Gomez 2022-07-05 08:38:19 +02:00
parent 6aca43bc5f
commit 1c9f856e65
11 changed files with 103 additions and 30 deletions

View file

@ -68,7 +68,7 @@ android {
} }
composeOptions { composeOptions {
kotlinCompilerExtensionVersion '1.2.0-rc01' kotlinCompilerExtensionVersion '1.2.0-rc02'
} }
packagingOptions { packagingOptions {
@ -88,7 +88,7 @@ dependencies {
implementation 'androidx.activity:activity-compose:1.4.0' implementation 'androidx.activity:activity-compose:1.4.0'
// Android Compose // Android Compose
implementation "androidx.compose.ui:ui:1.2.0-rc01" implementation "androidx.compose.ui:ui:1.2.0-rc02"
implementation "androidx.compose.material:material:1.1.1" implementation "androidx.compose.material:material:1.1.1"
implementation "androidx.compose.runtime:runtime-livedata:1.1.1" implementation "androidx.compose.runtime:runtime-livedata:1.1.1"
implementation "androidx.compose.ui:ui-tooling-preview:1.1.1" implementation "androidx.compose.ui:ui-tooling-preview:1.1.1"

View file

@ -4,14 +4,10 @@ import android.os.Bundle
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface import androidx.compose.material.Surface
import androidx.compose.ui.graphics.Color
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
import com.google.accompanist.systemuicontroller.SystemUiController
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import com.pixelized.biblib.ui.composable.SystemThemeColor import com.pixelized.biblib.ui.composable.SystemThemeColor
import com.pixelized.biblib.ui.navigation.screen.ScreenNavHost import com.pixelized.biblib.ui.navigation.screen.ScreenNavHost
import com.pixelized.biblib.ui.screen.launch.LauncherViewModel import com.pixelized.biblib.ui.screen.launch.LauncherViewModel
@ -33,7 +29,7 @@ class MainActivity : ComponentActivity() {
// splashscreen management // splashscreen management
installSplashScreen().apply { installSplashScreen().apply {
setKeepOnScreenCondition { setKeepOnScreenCondition {
launcherViewModel.isLoading launcherViewModel.isLoadingDone.not()
} }
} }
@ -43,7 +39,7 @@ class MainActivity : ComponentActivity() {
SystemThemeColor { SystemThemeColor {
Surface(color = MaterialTheme.colors.background) { Surface(color = MaterialTheme.colors.background) {
// Handle the main Navigation // Handle the main Navigation
if (launcherViewModel.isLoading.not()) { if (launcherViewModel.isLoadingDone) {
ScreenNavHost( ScreenNavHost(
startDestination = launcherViewModel.startDestination startDestination = launcherViewModel.startDestination
) )

View file

@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Person
import androidx.compose.material.icons.filled.Search import androidx.compose.material.icons.filled.Search
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@ -79,16 +80,24 @@ fun Search(
), ),
onValueChange = { search = it } onValueChange = { search = it }
) )
avatar?.let {
IconButton( IconButton(
modifier = Modifier.padding(end = horizontalPadding), modifier = Modifier.padding(end = horizontalPadding),
onClick = onAvatar, onClick = onAvatar,
) { ) {
if (avatar != null) {
GlideImage( GlideImage(
modifier = Modifier.clip(CircleShape).size(32.dp), modifier = Modifier.clip(CircleShape).size(32.dp),
previewPlaceholder = R.drawable.ic_google, previewPlaceholder = R.drawable.ic_google,
contentScale = ContentScale.Fit, contentScale = ContentScale.Fit,
imageModel = it, imageModel = avatar,
)
} else {
Icon(
modifier = Modifier.clip(CircleShape).size(32.dp),
tint = MaterialTheme.colors.onSurface,
imageVector = Icons.Default.Person,
contentDescription = null,
) )
} }
} }

View file

@ -45,6 +45,17 @@ fun ScreenNavHost(
} }
} }
fun NavHostController.navigateToAuthentication() {
navigate(Screen.Authentication.route) {
launchSingleTop = true
restoreState = true
popUpTo(0) {
saveState = true
inclusive = true
}
}
}
fun NavHostController.navigateToHome() { fun NavHostController.navigateToHome() {
navigate(Screen.Home.route) { navigate(Screen.Home.route) {
launchSingleTop = true launchSingleTop = true

View file

@ -5,13 +5,17 @@ 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.* import androidx.compose.runtime.Composable
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.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.credential.CredentialRepository_Factory
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.composable.StateUio
import com.pixelized.biblib.utils.exception.MissingGoogleTokenException import com.pixelized.biblib.utils.exception.MissingGoogleTokenException
@ -24,6 +28,7 @@ import javax.inject.Inject
@HiltViewModel @HiltViewModel
class AuthenticationViewModel @Inject constructor( class AuthenticationViewModel @Inject constructor(
private val credentialRepository: ICredentialRepository,
private val googleSignIn: IGoogleSingInRepository, private val googleSignIn: IGoogleSingInRepository,
private val client: IBibLibClient, private val client: IBibLibClient,
) : ViewModel() { ) : ViewModel() {
@ -45,6 +50,7 @@ class AuthenticationViewModel @Inject constructor(
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
_authenticationProcess.value = StateUio.Success(Unit) _authenticationProcess.value = StateUio.Success(Unit)
} catch (exception: Exception) { } catch (exception: Exception) {
Log.e("AuthenticationViewModel", exception.message, exception) Log.e("AuthenticationViewModel", exception.message, exception)
@ -68,7 +74,7 @@ class AuthenticationViewModel @Inject constructor(
val task = GoogleSignIn.getSignedInAccountFromIntent(it.data) val task = GoogleSignIn.getSignedInAccountFromIntent(it.data)
val account = task.getResult(ApiException::class.java) val account = task.getResult(ApiException::class.java)
val googleToken = account?.idToken ?: throw MissingGoogleTokenException() val googleToken = account?.idToken ?: throw MissingGoogleTokenException()
val response = client.service.loginWithGoogle(googleToken) val response = client.service.loginWithGoogle(token = googleToken)
val token = response.token ?: throw MissingTokenException() val token = response.token ?: throw MissingTokenException()
client.token = token client.token = token
_authenticationProcess.value = StateUio.Success(Unit) _authenticationProcess.value = StateUio.Success(Unit)

View file

@ -4,6 +4,7 @@ import android.content.res.Configuration.UI_MODE_NIGHT_NO
import android.content.res.Configuration.UI_MODE_NIGHT_YES import android.content.res.Configuration.UI_MODE_NIGHT_YES
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.compose.animation.animateContentSize import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
@ -12,6 +13,7 @@ import androidx.compose.foundation.verticalScroll
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Download import androidx.compose.material.icons.filled.Download
import androidx.compose.material.icons.filled.NavigateNext
import androidx.compose.material.icons.filled.Send import androidx.compose.material.icons.filled.Send
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@ -21,6 +23,7 @@ import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.text.HtmlCompat import androidx.core.text.HtmlCompat
@ -70,8 +73,13 @@ fun DetailScreen(
val user by derivedStateOf { profileViewModel.user } val user by derivedStateOf { profileViewModel.user }
if (user.isSuccessful()) { if (user.isSuccessful()) {
DetailScreenSendContent( DetailScreenSendContent(
modifier = Modifier.navigationBarsPadding(), modifier = Modifier
.navigationBarsPadding()
.padding(bottom = MaterialTheme.bibLib.dimen.dp16),
emails = (user as StateUio.Success<UserUio>).value.amazonEmails, emails = (user as StateUio.Success<UserUio>).value.amazonEmails,
onAction = {
}
) )
} }
} }
@ -273,6 +281,7 @@ private fun TitleLabel(
private fun DetailScreenSendContent( private fun DetailScreenSendContent(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
emails: List<String>, emails: List<String>,
onAction: (email: String) -> Unit = default<String>(),
) { ) {
Column( Column(
modifier = modifier.fillMaxWidth(), modifier = modifier.fillMaxWidth(),
@ -287,18 +296,32 @@ private fun DetailScreenSendContent(
) )
LazyColumn { LazyColumn {
items(items = emails) { items(items = emails) { email ->
Box( Row(
modifier = Modifier modifier = Modifier
.height(height = MaterialTheme.bibLib.dimen.dp48) .clickable(onClick = { onAction(email) })
.height(height = MaterialTheme.bibLib.dimen.dp52)
.fillMaxWidth(), .fillMaxWidth(),
contentAlignment = Alignment.CenterStart verticalAlignment = Alignment.CenterVertically,
) { ) {
Text( Text(
modifier = Modifier.padding(horizontal = MaterialTheme.bibLib.dimen.dp16), modifier = Modifier
.padding(
start = MaterialTheme.bibLib.dimen.dp16,
end = MaterialTheme.bibLib.dimen.dp8,
)
.weight(1f),
style = MaterialTheme.typography.body1, style = MaterialTheme.typography.body1,
color = MaterialTheme.colors.onSurface, color = MaterialTheme.colors.onSurface,
text = it maxLines = 1,
overflow = TextOverflow.Ellipsis,
text = email
)
Icon(
modifier = Modifier.padding(end = MaterialTheme.bibLib.dimen.dp16),
imageVector = Icons.Default.NavigateNext,
contentDescription = null
) )
} }
} }
@ -333,8 +356,8 @@ private fun DetailScreenSendPreview() {
BibLibTheme { BibLibTheme {
DetailScreenSendContent( DetailScreenSendContent(
emails = listOf( emails = listOf(
"R.Giskard.Reventlov.Kindle@gmailcom", "R.Giskard.Reventlov.Kindle@gmail.com",
"R.Daneel.Olivaw.Kindle@gmailcom", "R.Daneel.Olivaw.Kindle@gmail.com",
), ),
) )
} }

View file

@ -2,6 +2,7 @@ package com.pixelized.biblib.ui.screen.launch
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.pixelized.biblib.network.client.IBibLibClient import com.pixelized.biblib.network.client.IBibLibClient
@ -26,8 +27,8 @@ class LauncherViewModel @Inject constructor(
private val apiCache: IAPICacheRepository, private val apiCache: IAPICacheRepository,
) : ViewModel() { ) : ViewModel() {
private val _isLoading = mutableStateOf(true) var isLoadingDone by mutableStateOf(false)
val isLoading: Boolean by _isLoading private set
var startDestination: Screen = Screen.Authentication var startDestination: Screen = Screen.Authentication
private set private set
@ -36,14 +37,15 @@ class LauncherViewModel @Inject constructor(
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
// Try to Authenticate // Try to Authenticate
if (autoLoginWithGoogle() || autologinWithCredential()) { if (autoLoginWithGoogle() || autologinWithCredential()) {
startDestination = Screen.Home
// Update book // Update book
if (loadNewBooks()) { if (loadNewBooks()) {
loadAllBooks() loadAllBooks()
} }
// Change the start destination
startDestination = Screen.Home
} }
// Update loading state. // Update loading state.
_isLoading.value = false isLoadingDone = true
} }
} }
@ -73,12 +75,13 @@ class LauncherViewModel @Inject constructor(
private suspend fun autologinWithCredential(): Boolean { private suspend fun autologinWithCredential(): Boolean {
val login = credentialRepository.login val login = credentialRepository.login
val password = credentialRepository.password val password = credentialRepository.password
return if (login != null && password != null) { return if (login != null && password != null && credentialRepository.bearer != null) {
try { try {
val query = AuthLoginQuery(login, password) val query = AuthLoginQuery(login, password)
client.service.login(query).let { response -> client.service.login(query).let { response ->
if (response.token != null) { if (response.token != null) {
client.token = response.token client.token = response.token
credentialRepository.bearer = response.token
true true
} else { } else {
false false

View file

@ -16,6 +16,9 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
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.composable.StateUio
import com.pixelized.biblib.ui.navigation.screen.LocalScreenNavHostController
import com.pixelized.biblib.ui.navigation.screen.navigateToAuthentication
import com.pixelized.biblib.ui.navigation.screen.navigateToProfile
import com.pixelized.biblib.ui.theme.BibLibTheme import com.pixelized.biblib.ui.theme.BibLibTheme
import com.pixelized.biblib.utils.extention.bibLib import com.pixelized.biblib.utils.extention.bibLib
import com.pixelized.biblib.utils.extention.default import com.pixelized.biblib.utils.extention.default
@ -25,6 +28,8 @@ fun ProfileScreen(
viewModel: ProfileViewModel = hiltViewModel(), viewModel: ProfileViewModel = hiltViewModel(),
) { ) {
val context = LocalContext.current val context = LocalContext.current
val navigation = LocalScreenNavHostController.current
when (val user = viewModel.user) { when (val user = viewModel.user) {
is StateUio.Progress -> Unit is StateUio.Progress -> Unit
is StateUio.Success -> ProfileScreenContent( is StateUio.Success -> ProfileScreenContent(
@ -34,6 +39,10 @@ fun ProfileScreen(
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(IBibLibClient.EDIT_PROFILE)) val intent = Intent(Intent.ACTION_VIEW, Uri.parse(IBibLibClient.EDIT_PROFILE))
context.startActivity(intent) context.startActivity(intent)
}, },
onLogoutClick = {
viewModel.logout()
navigation.navigateToAuthentication()
}
) )
is StateUio.Failure -> Unit is StateUio.Failure -> Unit
} }

View file

@ -9,6 +9,8 @@ import androidx.lifecycle.viewModelScope
import com.pixelized.biblib.model.user.User import com.pixelized.biblib.model.user.User
import com.pixelized.biblib.network.client.IBibLibClient import com.pixelized.biblib.network.client.IBibLibClient
import com.pixelized.biblib.network.factory.UserFactory import com.pixelized.biblib.network.factory.UserFactory
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.composable.StateUio
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -17,6 +19,8 @@ import javax.inject.Inject
@HiltViewModel @HiltViewModel
class ProfileViewModel @Inject constructor( class ProfileViewModel @Inject constructor(
private val credentialRepository: ICredentialRepository,
private val googleSignIn: IGoogleSingInRepository,
private val client: IBibLibClient, private val client: IBibLibClient,
) : ViewModel() { ) : ViewModel() {
@ -41,6 +45,11 @@ class ProfileViewModel @Inject constructor(
} }
} }
fun logout() {
credentialRepository.bearer = null
googleSignIn.client.signOut()
}
private fun User.toUio() = UserUio( private fun User.toUio() = UserUio(
username = username, username = username,
firstname = firstname, firstname = firstname,

View file

@ -15,6 +15,7 @@ data class BibLibDimen(
val dp16: Dp = 16.dp, val dp16: Dp = 16.dp,
val dp32: Dp = 32.dp, val dp32: Dp = 32.dp,
val dp48: Dp = 48.dp, val dp48: Dp = 48.dp,
val dp52: Dp = 52.dp,
val dp64: Dp = 64.dp, val dp64: Dp = 64.dp,
val dialog: Dialog = Dialog(), val dialog: Dialog = Dialog(),
val thumbnail: BookThumbnail = BookThumbnail(), val thumbnail: BookThumbnail = BookThumbnail(),

View file

@ -16,4 +16,10 @@ fun Context.notImplemented() {
fun default(): () -> Unit { fun default(): () -> Unit {
val context = LocalContext.current val context = LocalContext.current
return { context.notImplemented() } return { context.notImplemented() }
}
@Composable
fun <T> default(): (T) -> Unit {
val context = LocalContext.current
return { context.notImplemented() }
} }