Implémenter Se connecter avec Google dans votre application Android

1. Avant de commencer

Découvrez comment implémenter Se connecter avec Google sur Android à l'aide du Gestionnaire d'identifiants.

Prérequis

  • Connaissances de base de Kotlin pour le développement Android.
  • Connaissances de base de Jetpack Compose (en savoir plus).

Points abordés

  • Créer un projet Google Cloud et des clients OAuth.
  • Implémenter le flux de connexion Bottom Sheet.
  • Implémenter le flux de connexion explicite par bouton.

Ce dont vous avez besoin

2. Créer un projet Android Studio

Pour commencer, créez un projet dans Android Studio :

  1. Ouvrez Android Studio, puis cliquez sur New Project (Nouveau projet). Bienvenue dans Android Studio
  2. Sélectionnez Phone and Tablet > Empty Activity (Téléphone et tablette > Activité vide), puis cliquez sur Next (Suivant). Projet Android Studio
  3. Configurez les paramètres du projet :
    • Name (Nom) : choisissez un nom de projet.
    • Package name (Nom du package) : utilisez le nom par défaut ou choisissez le vôtre.
    • Minimum SDK (SDK minimal) : sélectionnez la dernière version stable ou > Android 14.

Projet de configuration Android Studio

  1. Cliquez sur Finish (Terminer) et attendez la fin de la compilation initiale du projet. Projet Android Studio créé

3. Configurer votre projet Google Cloud

Créer un projet Google Cloud

  1. Accédez à la console Google Cloud, puis sélectionnez ou créez un projet. Créer un projet GCP
  2. Accédez à API et services > Écran de consentement OAuth. Écran de consentement OAuth GCP
  3. Cliquez sur Get started (Commencer), puis remplissez les champs obligatoires :
    • App Name (Nom de l'application) : utilisez le nom de votre application Android.
    • User Support Email (Adresse e-mail d'assistance utilisateur) : sélectionnez votre compte Google.
    • Audience : sélectionnez External (Externe).
    • Contact Info (Informations de contact) : saisissez votre adresse e-mail. Infos sur l'application GCP
  4. Consultez le Règlement sur les données utilisateur dans les services d'API Google, puis cliquez sur Create (Créer). Créer un compte GCP

Configurer des clients OAuth

Vous devez créer un client Web et un client Android dans la console Google Cloud pour obtenir les ID client pour l'authentification.

  • Client Android : sécurise les requêtes en validant le nom du package et la signature SHA-1 de votre application.
  • Client Web : agit en tant que client de backend pour le service de connexion Google.

Créer un client OAuth 2.0 Android

  1. Sur la page Clients, cliquez sur Create Client (Créer un client), puis sélectionnez Android comme Application type (Type d'application). Créer des clients GCP
  2. Saisissez le nom du package de votre application (correspondant à la ligne 1 de votre fichier MainActivity.kt).
  3. Générez votre signature SHA-1. Ouvrez le terminal Android Studio et exécutez la commande suivante :macOS/Linux :
    keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android
    
    Windows:
    keytool -list -v -keystore "C:\Users\USERNAME\.android\debug.keystore" -alias androiddebugkey -storepass android -keypass android
    
  4. Copiez l'empreinte SHA-1 à partir du résultat de la commande, collez-la dans le champ SHA-1 fingerprint (Empreinte SHA-1) de la console, puis cliquez sur Create (Créer). Informations sur le client Android

Créer un client OAuth 2.0 Web

  1. Cliquez à nouveau sur Create Client (Créer un client), puis sélectionnez Web Application (Application Web) comme Application type (Type d'application).
  2. Nommez votre client Web, laissez les champs URL/Origines vides, puis cliquez sur Create. Détails du client Web
  3. Copiez l'ID client généré à partir de la boîte de dialogue de confirmation. Vous l'utiliserez dans votre code Kotlin. Copier l'ID client

4. Configurer un appareil virtuel Android

Pour tester l'application, vous pouvez utiliser un appareil Android physique ou un appareil virtuel Android (AVD).

Créer et exécuter l'AVD

  1. Dans Android Studio, ouvrez le Gestionnaire d'appareils, cliquez sur Create Virtual Device (Créer un appareil virtuel) (ou sur l'icône +), puis sélectionnez Medium Phone (Téléphone moyen).
  2. Sélectionnez la dernière version stable comme image système, puis cliquez sur Finish (Terminer).
  3. Lancez l'émulateur en cliquant sur l'icône Lecture/Exécuter à côté de l'appareil. Appareil en cours d'exécution

Se connecter à un compte Google sur l'appareil

  1. Sur l'émulateur, ouvrez l'application Settings (Paramètres), puis accédez à Google.
  2. Cliquez sur Sign in to your Google Account (Se connecter à votre compte Google), puis suivez les instructions. Appareil connecté

5. Ajouter des dépendances

Ajoutez les bibliothèques requises pour l'authentification et l'intégration de l'ID Google à votre projet :

  1. Accédez à File > Project Structure > Dependencies > app (Fichier > Structure du projet > Dépendances > app).
  2. Cliquez sur + > Library Dependency (Dépendance de la bibliothèque), recherchez com.google.android.libraries.identity.googleid:googleid, puis sélectionnez la dernière version (par exemple, 1.1.1).
  3. Cliquez à nouveau sur + > Library Dependency (Dépendance de la bibliothèque), recherchez play-services-auth, puis sélectionnez la bibliothèque avec l'ID de groupe com.google.android.gms.
  4. Cliquez sur OK pour appliquer les modifications et synchroniser votre projet. Dépendances terminées

6. Implémenter le flux Bottom Sheet

Flux de la bottom sheet

Le flux Bottom Sheet exploite l'API Credential Manager pour permettre aux utilisateurs de se connecter à votre application à l'aide de leur compte Google sur Android de manière simplifiée. Conçu pour être rapide et pratique, en particulier pour les utilisateurs réguliers, ce flux doit être déclenché au lancement de l'application.

Créer la requête de connexion

  1. Pour commencer, ouvrez MainActivity.kt, puis supprimez les fonctions par défaut Greeting() et GreetingPreview().
  2. Ajoutez les instructions d'importation suivantes après celles existantes à partir de la ligne 3 :
    import android.content.Context
    import android.os.Build
    import android.util.Log
    import android.widget.Toast
    import androidx.annotation.RequiresApi
    import androidx.compose.foundation.Image
    import androidx.compose.foundation.clickable
    import androidx.compose.foundation.layout.Arrangement
    import androidx.compose.foundation.layout.Column
    import androidx.compose.material3.MaterialTheme
    import androidx.compose.material3.Surface
    import androidx.compose.runtime.Composable
    import androidx.compose.runtime.LaunchedEffect
    import androidx.compose.runtime.rememberCoroutineScope
    import androidx.compose.ui.Alignment
    import androidx.compose.ui.Modifier
    import androidx.compose.ui.platform.LocalContext
    import androidx.compose.ui.res.painterResource
    import androidx.credentials.CredentialManager
    import androidx.credentials.CustomCredential
    import androidx.credentials.GetCredentialRequest
    import androidx.credentials.exceptions.GetCredentialCancellationException
    import androidx.credentials.exceptions.GetCredentialCustomException
    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.google.android.libraries.identity.googleid.GoogleIdTokenCredential
    import com.google.android.libraries.identity.googleid.GoogleIdTokenParsingException
    import java.security.SecureRandom
    import java.util.Base64
    import kotlinx.coroutines.delay
    import kotlinx.coroutines.launch
    
    const val TAG = "MainActivity"
    
  3. Ajoutez cette fonction composable sous la classe MainActivity dans votre fichier MainActivity.kt :
     @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
     @Composable
     fun BottomSheet(webClientId: String) {
         val context = LocalContext.current
    
         // LaunchedEffect is used to run a suspend function when the composable is first launched.
         LaunchedEffect(Unit) {
             // Create a Google ID option with filtering by authorized accounts enabled.
             val googleIdOption: GetGoogleIdOption = GetGoogleIdOption.Builder()
                 .setFilterByAuthorizedAccounts(true)
                 .setServerClientId(webClientId)
                 .setNonce(generateSecureRandomNonce())
                 .build()
    
             // Create a credential request with the Google ID option.
             val request: GetCredentialRequest = GetCredentialRequest.Builder()
                 .addCredentialOption(googleIdOption)
                 .build()
    
             // Attempt to sign in with the created request using an authorized account
             val e = signIn(request, context)
             // If the sign-in fails with NoCredentialException,  there are no authorized accounts.
             // In this case, we attempt to sign in again with filtering disabled.
             if (e is NoCredentialException) {
                 val googleIdOptionFalse: GetGoogleIdOption = GetGoogleIdOption.Builder()
                     .setFilterByAuthorizedAccounts(false)
                     .setServerClientId(webClientId)
                     .setNonce(generateSecureRandomNonce())
                     .build()
    
                 val requestFalse: GetCredentialRequest = GetCredentialRequest.Builder()
                     .addCredentialOption(googleIdOptionFalse)
                     .build()
    
                 //We will build out this function in a moment
                 signIn(requestFalse, context)
             }
         }
     }
    
     //This function is used to generate a secure nonce to pass in with our request
     fun generateSecureRandomNonce(byteLength: Int = 32): String {
         val randomBytes = ByteArray(byteLength)
         SecureRandom.getInstanceStrong().nextBytes(randomBytes)
         return Base64.getUrlEncoder().withoutPadding().encodeToString(randomBytes)
     }
    

Analyse du code

  • LaunchedEffect(Unit): déclenche le flux de connexion immédiatement lorsque le composable s'affiche pour la première fois.
  • GetGoogleIdOption.Builder(): configure la requête de jeton d'ID Google.
    • setFilterByAuthorizedAccounts(true) : tente d'abord une connexion silencieuse en filtrant les comptes que l'utilisateur a déjà autorisés pour cette application, ce qui réduit les frictions pour les utilisateurs réguliers.
    • setNonce(...) : transmet un nonce aléatoire sécurisé généré pour chaque requête par generateSecureRandomNonce() afin d'empêcher les attaques par rejeu.
  • signIn(request, context): exécute la requête. Si l'exécution échoue avec une exception NoCredentialException (ce qui signifie qu'aucun compte précédemment autorisé n'existe), le flux revient à setFilterByAuthorizedAccounts(false) pour permettre à l'utilisateur de sélectionner n'importe quel compte Google connecté sur l'appareil.

Envoyer la requête de connexion

Une fois la requête de connexion créée, vous pouvez utiliser le Gestionnaire d'identifiants pour terminer le processus de connexion. Créez une fonction appelée signIn qui exécute la requête et gère les exceptions courantes qui peuvent se produire.

Ajoutez cette fonction sous la fonction BottomSheet dans votre fichier MainActivity.kt :

@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
suspend fun signIn(request: GetCredentialRequest, context: Context): Exception? {
    val credentialManager = CredentialManager.create(context)
    val failureMessage = "Sign in failed!"
    //using delay() here helps prevent NoCredentialException when the BottomSheet Flow is triggered
    //on the initial running of our app
    delay(250)
    return try {
        // The getCredential is called to request a credential from Credential Manager.
        val result = credentialManager.getCredential(
            request = request,
            context = context,
        )
        Log.i(TAG, result.toString())

        val credential = result.credential
        if (credential is CustomCredential &&
            credential.type == GoogleIdTokenCredential.TYPE_GOOGLE_ID_TOKEN_CREDENTIAL) {
            val googleIdTokenCredential = GoogleIdTokenCredential.createFrom(credential.data)
            Log.i(TAG, "Signed in as: ${googleIdTokenCredential.id}")
        }

        Toast.makeText(context, "Sign in successful!", Toast.LENGTH_SHORT).show()
        Log.i(TAG, "(☞゚ヮ゚)☞  Sign in Successful!  ☜(゚ヮ゚☜)")
        null
    } catch (e: GoogleIdTokenParsingException) {
        Toast.makeText(context, failureMessage, Toast.LENGTH_SHORT).show()
        Log.e(TAG, failureMessage + ": Issue with parsing received GoogleIdToken", e)
        e
    } catch (e: NoCredentialException) {
        Toast.makeText(context, failureMessage, Toast.LENGTH_SHORT).show()
        Log.e(TAG, failureMessage + ": No credentials found", e)
        e
    } catch (e: GetCredentialCancellationException) {
        Toast.makeText(context, "Sign-in cancelled", Toast.LENGTH_SHORT).show()
        Log.e(TAG, failureMessage + ": Sign-in was cancelled", e)
        e
    } catch (e: GetCredentialCustomException) {
        Toast.makeText(context, failureMessage, Toast.LENGTH_SHORT).show()
        Log.e(TAG, failureMessage + ": Issue with custom credential request", e)
        e
    } catch (e: GetCredentialException) {
        Toast.makeText(context, failureMessage, Toast.LENGTH_SHORT).show()
        Log.e(TAG, failureMessage + ": Failure getting credentials", e)
        e
    }
}

Analyse du code

  • credentialManager.getCredential(...): appelle l'API Credential Manager pour afficher la bottom sheet ou la boîte de dialogue du sélecteur de compte système.
  • delay(250): met en pause brièvement pour éviter une condition de concurrence lorsque la bottom sheet est déclenchée immédiatement au démarrage de l'application avant que le service Credential Manager n'ait terminé l'initialisation.
  • Gestion des exceptions : capture et enregistre les erreurs d'identifiants courantes (par exemple, annulation, identifiants manquants ou problèmes d'analyse de jeton) et fournit des commentaires aux utilisateurs à l'aide de toasts.

Déclencher le flux Bottom Sheet

Modifiez votre classe MainActivity pour appeler BottomSheet() au démarrage. Remplacez YOUR_CLIENT_ID_HERE par l'ID client de votre application Web :

class MainActivity : ComponentActivity() {
    @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        //replace with your own web client ID from Google Cloud Console
        val webClientId = "YOUR_CLIENT_ID_HERE"

        setContent {
            //ExampleTheme - this is derived from the name of the project not any added library
            //e.g. if this project was named "Testing" it would be generated as TestingTheme
            ExampleTheme {
                Surface(
                    modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background,
                ) {
                    //This will trigger on launch
                    BottomSheet(webClientId)
                }
            }
        }
    }
}

Enregistrez votre projet (File > Save), puis exécutez l'application :

  1. Appuyez sur le bouton d'exécution : Exécuter le projet
  2. Lorsque l'application se lance sur l'émulateur, la bottom sheet de connexion doit s'afficher. Cliquez sur Continue (Continuer) pour tester le flux.Bottom sheet
  3. Une notification toast doit s'afficher pour confirmer que la connexion a réussi.Bottom sheet de réussite

7. Implémenter le flux par bouton

GIF du parcours du bouton

Le flux par bouton offre aux utilisateurs une option explicite pour se connecter ou s'inscrire. L'utilisation d'une image de marque standard garantit une expérience cohérente. Utilisez des composants pré-approuvés qui respectent les Consignes relatives à la marque Se connecter avec Google.

Ajouter l'icône de la marque

  1. Téléchargez les composants de la marque ici, puis extrayez le fichier ZIP.
  2. Copiez signin-assets/Android/png@2x/neutral/android_neutral_sq_SI@2x.png.
  3. Dans Android Studio, collez le fichier dans votre dossier res > drawable, renommez-le siwg_button.png, puis cliquez sur OK. Ajouter un bouton

Code du flux par bouton

Ce flux réutilise la même fonction d'assistance signIn, mais transmet une GetSignInWithGoogleOption au lieu de GetGoogleIdOption. Contrairement au flux Bottom Sheet, le flux par bouton explicite ne préfiltre pas ni n'invite automatiquement à utiliser des identifiants ou des clés d'accès stockés. Collez cette fonction composable sous la fonction BottomSheet :

@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
@Composable
fun ButtonUI(webClientId: String) {
    val context = LocalContext.current
    val coroutineScope = rememberCoroutineScope()

    val onClick: () -> Unit = {
        val signInWithGoogleOption: GetSignInWithGoogleOption = GetSignInWithGoogleOption
            .Builder(serverClientId = webClientId)
            .setNonce(generateSecureRandomNonce())
            .build()

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

        coroutineScope.launch {
            signIn(request, context)
        }
    }
    Image(
        painter = painterResource(id = R.drawable.siwg_button),
        contentDescription = "",
        modifier = Modifier
            .fillMaxSize()
            .clickable(enabled = true, onClick = onClick)
    )
}

Analyse du code

  • GetSignInWithGoogleOption: contrairement au flux Bottom Sheet, le flux par bouton explicite utilise cette option pour inviter les utilisateurs à sélectionner leur compte Google sans filtrage automatique.
  • coroutineScope.launch: lance une coroutine pour exécuter la fonction signIn suspendue de manière asynchrone lorsque l'utilisateur clique sur le bouton.
  • Image: affiche le drawable siwg_button de la marque et associe un écouteur de clics pour déclencher le flux.

Ajouter le bouton à la mise en page d'interface utilisateur

Modifiez votre mise en page MainActivity pour afficher à la fois le BottomSheet automatique et le ButtonUI explicite alignés verticalement :

class MainActivity : ComponentActivity() {
    @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        //replace with your own web client ID from Google Cloud Console
        val webClientId = "YOUR_CLIENT_ID_HERE"

        setContent {
            //ExampleTheme - this is derived from the name of the project not any added library
            //e.g. if this project was named "Testing" it would be generated as TestingTheme
            ExampleTheme {
                Surface(
                    modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background,
                ) {
                    Column(
                        verticalArrangement = Arrangement.Center,
                        horizontalAlignment = Alignment.CenterHorizontally

                    ) {
                        //This will trigger on launch
                        BottomSheet(webClientId)

                        //This requires the user to press the button
                        ButtonUI(webClientId)
                    }
                }
            }
        }
    }
}

Tester le flux par bouton

  1. Exécutez l'application.
  2. Fermez la bottom sheet initiale en cliquant en dehors de la zone de la feuille.
  3. Cliquez sur le bouton Se connecter avec Google pour lancer la boîte de dialogue de connexion, puis sélectionnez votre compte. Boîte de dialogue de connexion
  4. Vérifiez le résultat : consultez Logcat pour confirmer l'impression de votre nom d'utilisateur/adresse e-mail.

8. Conclusion

Félicitations ! Vous avez implémenté Se connecter avec Google à l'aide du Gestionnaire d'identifiants Android.

Ressources supplémentaires

Code complet de MainActivity.kt

Voici le code complet de MainActivity.kt à titre de référence :

package com.example.example

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.example.example.ui.theme.ExampleTheme
import android.content.ContentValues.TAG
import android.content.Context
import android.util.Log
import android.widget.Toast
import androidx.credentials.exceptions.GetCredentialException
import androidx.compose.foundation.clickable
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.credentials.CredentialManager
import androidx.credentials.exceptions.GetCredentialCancellationException
import androidx.credentials.exceptions.GetCredentialCustomException
import androidx.credentials.exceptions.NoCredentialException
import androidx.credentials.GetCredentialRequest
import com.google.android.libraries.identity.googleid.GetGoogleIdOption
import com.google.android.libraries.identity.googleid.GetSignInWithGoogleOption
import com.google.android.libraries.identity.googleid.GoogleIdTokenParsingException
import java.security.SecureRandom
import java.util.Base64
import kotlinx.coroutines.CoroutineScope
import androidx.compose.runtime.LaunchedEffect
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch

class MainActivity : ComponentActivity() {
    @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        //replace with your own web client ID from Google Cloud Console
        val webClientId = "YOUR_CLIENT_ID_HERE"

        setContent {
            //ExampleTheme - this is derived from the name of the project not any added library
            //e.g. if this project was named "Testing" it would be generated as TestingTheme
            ExampleTheme {
                Surface(
                    modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background,
                ) {
                    Column(
                        verticalArrangement = Arrangement.Center,
                        horizontalAlignment = Alignment.CenterHorizontally

                    ) {
                        //This will trigger on launch
                        BottomSheet(webClientId)

                        //This requires the user to press the button
                        ButtonUI(webClientId)
                    }
                }
            }
        }
    }
}

@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
@Composable
fun BottomSheet(webClientId: String) {
    val context = LocalContext.current

    // LaunchedEffect is used to run a suspend function when the composable is first launched.
    LaunchedEffect(Unit) {
        // Create a Google ID option with filtering by authorized accounts enabled.
        val googleIdOption: GetGoogleIdOption = GetGoogleIdOption.Builder()
            .setFilterByAuthorizedAccounts(true)
            .setServerClientId(webClientId)
            .setNonce(generateSecureRandomNonce())
            .build()

        // Create a credential request with the Google ID option.
        val request: GetCredentialRequest = GetCredentialRequest.Builder()
            .addCredentialOption(googleIdOption)
            .build()

        // Attempt to sign in with the created request using an authorized account
        val e = signIn(request, context)
        // If the sign-in fails with NoCredentialException,  there are no authorized accounts.
        // In this case, we attempt to sign in again with filtering disabled.
        if (e is NoCredentialException) {
            val googleIdOptionFalse: GetGoogleIdOption = GetGoogleIdOption.Builder()
                .setFilterByAuthorizedAccounts(false)
                .setServerClientId(webClientId)
                .setNonce(generateSecureRandomNonce())
                .build()

            val requestFalse: GetCredentialRequest = GetCredentialRequest.Builder()
                .addCredentialOption(googleIdOptionFalse)
                .build()

            signIn(requestFalse, context)
        }
    }
}

@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
@Composable
fun ButtonUI(webClientId: String) {
    val context = LocalContext.current
    val coroutineScope = rememberCoroutineScope()

    val onClick: () -> Unit = {
        val signInWithGoogleOption: GetSignInWithGoogleOption = GetSignInWithGoogleOption
            .Builder(serverClientId = webClientId)
            .setNonce(generateSecureRandomNonce())
            .build()

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

        coroutineScope.launch {
            signIn(request, context)
        }
    }
    Image(
        painter = painterResource(id = R.drawable.siwg_button),
        contentDescription = "",
        modifier = Modifier
            .fillMaxSize()
            .clickable(onClick = onClick)
    )
}

fun generateSecureRandomNonce(byteLength: Int = 32): String {
    val randomBytes = ByteArray(byteLength)
    SecureRandom.getInstanceStrong().nextBytes(randomBytes)
    return Base64.getUrlEncoder().withoutPadding().encodeToString(randomBytes)
}

@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
suspend fun signIn(request: GetCredentialRequest, context: Context): Exception? {
    val credentialManager = CredentialManager.create(context)
    val failureMessage = "Sign in failed!"
    //using delay() here helps prevent NoCredentialException when the BottomSheet Flow is triggered
    //on the initial running of our app
    delay(250)
    return try {
        // The getCredential is called to request a credential from Credential Manager.
        val result = credentialManager.getCredential(
            request = request,
            context = context,
        )
        Log.i(TAG, result.toString())

        val credential = result.credential
        if (credential is CustomCredential &&
            credential.type == GoogleIdTokenCredential.TYPE_GOOGLE_ID_TOKEN_CREDENTIAL) {
            val googleIdTokenCredential = GoogleIdTokenCredential.createFrom(credential.data)
            Log.i(TAG, "Signed in as: ${googleIdTokenCredential.id}")
        }

        Toast.makeText(context, "Sign in successful!", Toast.LENGTH_SHORT).show()
        Log.i(TAG, "(☞゚ヮ゚)☞  Sign in Successful!  ☜(゚ヮ゚☜)")
        null
    } catch (e: GoogleIdTokenParsingException) {
        Toast.makeText(context, failureMessage, Toast.LENGTH_SHORT).show()
        Log.e(TAG, failureMessage + ": Issue with parsing received GoogleIdToken", e)
        e
    } catch (e: NoCredentialException) {
        Toast.makeText(context, failureMessage, Toast.LENGTH_SHORT).show()
        Log.e(TAG, failureMessage + ": No credentials found", e)
        e
    } catch (e: GetCredentialCancellationException) {
        Toast.makeText(context, "Sign-in cancelled", Toast.LENGTH_SHORT).show()
        Log.e(TAG, failureMessage + ": Sign-in was cancelled", e)
        e
    } catch (e: GetCredentialCustomException) {
        Toast.makeText(context, failureMessage, Toast.LENGTH_SHORT).show()
        Log.e(TAG, failureMessage + ": Issue with custom credential request", e)
        e
    } catch (e: GetCredentialException) {
        Toast.makeText(context, failureMessage, Toast.LENGTH_SHORT).show()
        Log.e(TAG, failureMessage + ": Failure getting credentials", e)
        e
    }
}