Skip to content

Commit

Permalink
Update authentication with Google flows (#84)
Browse files Browse the repository at this point in the history
* Remove sign up with Google button from sign up screen

* Launch credential manager bottom sheet as soon as authentication screen starts and create different flows for sign in and sign up

* Add flow to show snackbar and add error if no accounts exist for either sign in and sign up

* Split sign in and sign up flows, as they call different methods and have different flows from the Firebase Auth perspective

* Launch snackbar with no accounts erros when NoCredentialException is thrown

* Create AuthenticationViewModel, make SignInViewModel and SignUpViewModel extend and use credential manager flow methods

* Add different flow for click on sign in with google button

* Resolve comments on PR

* Add comment to CredentialsExt and move LaunchEffect up on the authentication screens
  • Loading branch information
marinacoelho authored Oct 15, 2024
1 parent 8e0e8bc commit 1a91c88
Show file tree
Hide file tree
Showing 14 changed files with 224 additions and 101 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,10 @@ dependencies {
implementation("com.google.firebase:firebase-firestore")

//Authentication with Credential Manager
implementation("com.google.android.gms:play-services-auth:21.1.0")
implementation("com.google.android.gms:play-services-auth:21.2.0")
implementation("androidx.credentials:credentials:1.2.2")
implementation("androidx.credentials:credentials-play-services-auth:1.2.2")
implementation("com.google.android.libraries.identity.googleid:googleid:1.1.0")
implementation("com.google.android.libraries.identity.googleid:googleid:1.1.1")

testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.navigation.compose.NavHost
import androidx.navigation.NavGraphBuilder
Expand All @@ -17,19 +20,23 @@ import androidx.navigation.navArgument
import com.notes.app.screens.account_center.AccountCenterScreen
import com.notes.app.screens.note.NoteScreen
import com.notes.app.screens.notes_list.NotesListScreen
import com.notes.app.screens.sign_in.SignInScreen
import com.notes.app.screens.sign_up.SignUpScreen
import com.notes.app.screens.authentication.sign_in.SignInScreen
import com.notes.app.screens.authentication.sign_up.SignUpScreen
import com.notes.app.screens.splash.SplashScreen
import com.notes.app.ui.theme.NotesTheme
import kotlinx.coroutines.CoroutineScope

@Composable
@OptIn(ExperimentalMaterial3Api::class)
fun NotesApp() {
NotesTheme {
Surface(color = MaterialTheme.colorScheme.background) {
val appState = rememberAppState()
val snackbarHostState = remember { SnackbarHostState() }
val appState = rememberAppState(snackbarHostState)

Scaffold { innerPaddingModifier ->
Scaffold(
snackbarHost = { SnackbarHost(hostState = snackbarHostState) }
) { innerPaddingModifier ->
NavHost(
navController = appState.navController,
startDestination = SPLASH_SCREEN,
Expand All @@ -43,10 +50,16 @@ fun NotesApp() {
}

@Composable
fun rememberAppState(navController: NavHostController = rememberNavController()) =
remember(navController) {
NotesAppState(navController)
fun rememberAppState(
snackbarHostState: SnackbarHostState,
navController: NavHostController = rememberNavController(),
snackbarManager: SnackbarManager = SnackbarManager,
coroutineScope: CoroutineScope = rememberCoroutineScope()
): NotesAppState {
return remember(snackbarHostState, navController, snackbarManager, coroutineScope) {
NotesAppState(snackbarHostState, navController, snackbarManager, coroutineScope)
}
}

fun NavGraphBuilder.notesGraph(appState: NotesAppState) {
composable(NOTES_LIST_SCREEN) {
Expand All @@ -68,7 +81,10 @@ fun NavGraphBuilder.notesGraph(appState: NotesAppState) {
}

composable(SIGN_IN_SCREEN) {
SignInScreen(openAndPopUp = { route, popUp -> appState.navigateAndPopUp(route, popUp) })
SignInScreen(
onSignUpClicked = { route -> appState.navigate(route) },
openAndPopUp = { route, popUp -> appState.navigateAndPopUp(route, popUp) }
)
}

composable(SIGN_UP_SCREEN) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,27 @@ package com.notes.app

import androidx.compose.runtime.Stable
import androidx.navigation.NavHostController
import androidx.compose.material3.SnackbarHostState
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.launch

@Stable
class NotesAppState(val navController: NavHostController) {
class NotesAppState(
private val snackbarHostState: SnackbarHostState,
val navController: NavHostController,
private val snackbarManager: SnackbarManager,
coroutineScope: CoroutineScope
) {
init {
coroutineScope.launch {
snackbarManager.snackbarMessages.filterNotNull().collect { message ->
snackbarHostState.showSnackbar(message)
snackbarManager.clearSnackbarState()
}
}
}

fun popUp() {
navController.popBackStack()
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.notes.app

import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow

object SnackbarManager {
private val messages: MutableStateFlow<String?> = MutableStateFlow(null)
val snackbarMessages: StateFlow<String?>
get() = messages

fun showMessage(message: String) {
messages.value = message
}

fun clearSnackbarState() {
messages.value = null
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.notes.app.screens.account_center

import android.util.Log
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
Expand All @@ -11,7 +10,6 @@ import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.filled.ExitToApp
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
Expand All @@ -21,25 +19,13 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.credentials.Credential
import androidx.credentials.CredentialManager
import androidx.credentials.GetCredentialRequest
import androidx.credentials.exceptions.GetCredentialException
import com.google.android.libraries.identity.googleid.GetGoogleIdOption
import com.notes.app.ERROR_TAG
import com.notes.app.R
import com.notes.app.ui.theme.Purple40
import kotlinx.coroutines.launch

@Composable
@OptIn(ExperimentalMaterial3Api::class)
Expand Down Expand Up @@ -170,55 +156,3 @@ fun RemoveAccountCard(onRemoveAccountClick: () -> Unit) {
)
}
}

@Composable
fun AuthenticationButton(
buttonText: Int,
onGetCredentialResponse: (Credential) -> Unit
) {
val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()
val credentialManager = CredentialManager.create(context)

Button(
onClick = {
val googleIdOption = GetGoogleIdOption.Builder()
.setFilterByAuthorizedAccounts(false)
.setServerClientId(context.getString(R.string.default_web_client_id))
.build()

val request = GetCredentialRequest.Builder()
.addCredentialOption(googleIdOption)
.build()

coroutineScope.launch {
try {
val result = credentialManager.getCredential(
request = request,
context = context
)

onGetCredentialResponse(result.credential)
} catch (e: GetCredentialException) {
Log.d(ERROR_TAG, e.message.orEmpty())
}
}
},
colors = ButtonDefaults.buttonColors(containerColor = Purple40),
modifier = Modifier
.fillMaxWidth()
.padding(16.dp, 0.dp)
) {
Icon(
painter = painterResource(id = R.drawable.google_g),
modifier = Modifier.padding(horizontal = 16.dp),
contentDescription = "Google logo"
)

Text(
text = stringResource(buttonText),
fontSize = 16.sp,
modifier = Modifier.padding(0.dp, 6.dp)
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -94,13 +94,9 @@ fun AccountCenterScreen(
.padding(12.dp))

if (user.isAnonymous) {
AccountCenterCard(stringResource(R.string.sign_in), Icons.Filled.Face, Modifier.card()) {
AccountCenterCard(stringResource(R.string.authenticate), Icons.Filled.AccountCircle, Modifier.card()) {
viewModel.onSignInClick(restartApp)
}

AccountCenterCard(stringResource(R.string.sign_up), Icons.Filled.AccountCircle, Modifier.card()) {
viewModel.onSignUpClick(restartApp)
}
} else {
ExitAppCard { viewModel.onSignOutClick(restartApp) }
RemoveAccountCard { viewModel.onDeleteAccountClick(restartApp) }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package com.notes.app.screens.account_center

import com.notes.app.SIGN_IN_SCREEN
import com.notes.app.SIGN_UP_SCREEN
import com.notes.app.SPLASH_SCREEN
import com.notes.app.model.User
import com.notes.app.model.service.AccountService
Expand Down Expand Up @@ -35,8 +34,6 @@ class AccountCenterViewModel @Inject constructor(

fun onSignInClick(openScreen: (String) -> Unit) = openScreen(SIGN_IN_SCREEN)

fun onSignUpClick(openScreen: (String) -> Unit) = openScreen(SIGN_UP_SCREEN)

fun onSignOutClick(restartApp: (String) -> Unit) {
launchCatching {
accountService.signOut()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package com.notes.app.screens.authentication

import android.content.Context
import android.util.Log
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.credentials.Credential
import androidx.credentials.CredentialManager
import androidx.credentials.GetCredentialRequest
import androidx.credentials.exceptions.GetCredentialException
import androidx.credentials.exceptions.NoCredentialException
import com.google.android.libraries.identity.googleid.GetGoogleIdOption
import com.google.android.libraries.identity.googleid.GetSignInWithGoogleOption
import com.notes.app.ERROR_TAG
import com.notes.app.R
import com.notes.app.SnackbarManager
import com.notes.app.ui.theme.Purple40
import kotlinx.coroutines.launch

@Composable
fun AuthenticationButton(buttonText: Int, onRequestResult: (Credential) -> Unit) {
val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()

Button(
onClick = { coroutineScope.launch { launchCredManButtonUI(context, onRequestResult) } },
colors = ButtonDefaults.buttonColors(containerColor = Purple40),
modifier = Modifier
.fillMaxWidth()
.padding(16.dp, 0.dp)
) {
Icon(
painter = painterResource(id = R.drawable.google_g),
modifier = Modifier.padding(horizontal = 16.dp),
contentDescription = "Google logo"
)

Text(
text = stringResource(buttonText),
fontSize = 16.sp,
modifier = Modifier.padding(0.dp, 6.dp)
)
}
}

private suspend fun launchCredManButtonUI(
context: Context,
onRequestResult: (Credential) -> Unit
) {
try {
val signInWithGoogleOption = GetSignInWithGoogleOption
.Builder(serverClientId = context.getString(R.string.default_web_client_id))
.build()

val request = GetCredentialRequest.Builder()
.addCredentialOption(signInWithGoogleOption)
.build()

val result = CredentialManager.create(context).getCredential(
request = request,
context = context
)

onRequestResult(result.credential)
} catch (e: NoCredentialException) {
Log.d(ERROR_TAG, e.message.orEmpty())
SnackbarManager.showMessage(context.getString(R.string.no_accounts_error))
} catch (e: GetCredentialException) {
Log.d(ERROR_TAG, e.message.orEmpty())
}
}

suspend fun launchCredManBottomSheet(
context: Context,
hasFilter: Boolean = true,
onRequestResult: (Credential) -> Unit
) {
try {
val googleIdOption = GetGoogleIdOption.Builder()
.setFilterByAuthorizedAccounts(hasFilter)
.setServerClientId(context.getString(R.string.default_web_client_id))
.build()

val request = GetCredentialRequest.Builder()
.addCredentialOption(googleIdOption)
.build()

val result = CredentialManager.create(context).getCredential(
request = request,
context = context
)

onRequestResult(result.credential)
} catch (e: NoCredentialException) {
Log.d(ERROR_TAG, e.message.orEmpty())

//If the bottom sheet was launched with filter by authorized accounts, we launch it again
//without filter so the user can see all available accounts, not only the ones that have
//been previously authorized in this app
if (hasFilter) {
launchCredManBottomSheet(context, hasFilter = false, onRequestResult)
}
} catch (e: GetCredentialException) {
Log.d(ERROR_TAG, e.message.orEmpty())
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.notes.app.screens.sign_up
package com.notes.app.screens.authentication

import android.util.Patterns
import java.util.regex.Pattern
Expand Down
Loading

0 comments on commit 1a91c88

Please sign in to comment.