איך מטמיעים התחברות באמצעות חשבון Google באפליקציית Android

1. לפני שמתחילים

איך מטמיעים התחברות באמצעות חשבון Google ב-Android באמצעות Credential Manager

דרישות מוקדמות

  • הבנה בסיסית של Kotlin לפיתוח ל-Android.
  • הבנה בסיסית של Jetpack Compose (מידע נוסף).

מה תלמדו

  • יוצרים פרויקט בענן ב-Google Cloud ולקוחות OAuth.
  • מטמיעים את תהליך הכניסה בגיליון התחתון.
  • מטמיעים את תהליך הכניסה המפורש באמצעות לחצן.

מה צריך

2. יצירת פרויקט ב-Android Studio

כדי להתחיל, יוצרים פרויקט חדש ב-Android Studio:

  1. פותחים את Android Studio ולוחצים על New Project (פרויקט חדש). מסך הפתיחה של Android Studio
  2. בוחרים באפשרות טלפון וטאבלט > פעילות ריקה ולוחצים על הבא. פרויקט Android Studio
  3. קובעים את הגדרות הפרויקט:
    • שם: בוחרים שם לפרויקט.
    • שם החבילה: אפשר להשתמש בשם ברירת המחדל או לבחור שם משלכם.
    • גרסת ה-SDK המינימלית: בוחרים את הגרסה היציבה העדכנית ביותר או > Android 14.

הגדרת פרויקט ב-Android Studio

  1. לוחצים על סיום וממתינים להשלמת הבנייה הראשונית של הפרויקט. פרויקט Android Studio נוצר

3. הגדרת הפרויקט ב-Google Cloud

יצירת פרויקט של Google Cloud

  1. נכנסים אל מסוף Google Cloud ובוחרים פרויקט או יוצרים פרויקט חדש. GCP create new project
  2. עוברים אל APIs & Services > OAuth consent screen (ממשקי API ושירותים > מסך הסכמה ל-OAuth). מסך הסכמה ל-OAuth ב-GCP
  3. לוחצים על שנתחיל? וממלאים את שדות החובה:
    • שם האפליקציה: משתמשים בשם של אפליקציית Android.
    • כתובת אימייל לתמיכה במשתמשים: בוחרים את חשבון Google.
    • קהל: בוחרים באפשרות חיצוני.
    • פרטים ליצירת קשר: מזינים את כתובת האימייל. פרטי אפליקציה ב-GCP
  4. קוראים את המדיניות של Google בנושא נתוני משתמשים בשירותי API ולוחצים על יצירה. יצירה ב-GCP

הגדרת לקוחות OAuth

כדי לקבל את מזהי הלקוח לאימות, צריך ליצור לקוח אינטרנט ולקוח Android במסוף Google Cloud.

  • לקוח Android: מאבטח את הבקשות על ידי אימות שם החבילה והחתימה מסוג SHA-1 של האפליקציה.
  • לקוח אינטרנט: פועל כלקוח הקצה של שירות הכניסה באמצעות חשבון Google.

יצירת לקוח OAuth 2.0 ל-Android

  1. בדף Clients, לוחצים על Create Client ובוחרים באפשרות Android בתור Application type. GCP Create Clients
  2. מזינים את שם החבילה של האפליקציה (שם זהה לשם שמופיע בשורה 1 של MainActivity.kt).
  3. יוצרים חתימת SHA-1. פותחים את הטרמינל של Android Studio ומריצים את הפקודה: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. מעתיקים את טביעת האצבע של SHA-1 מ<b>פלט הפקודה</b>, מדביקים אותה בשדה טביעת אצבע SHA-1 במסוף ולוחצים על יצירה. פרטי לקוח Android

יצירת לקוח OAuth 2.0 לאינטרנט

  1. לוחצים שוב על Create Client ובוחרים באפשרות Web Application בתור Application type.
  2. נותנים שם ללקוח האינטרנט, משאירים את השדות URL/Origins (כתובת אתר/מקורות) ריקים ולוחצים על Create (יצירה). פרטי לקוח האינטרנט
  3. מעתיקים את מזהה הלקוח שנוצר מתיבת הדו-שיח לאישור. תשתמשו בזה בקוד Kotlin. העתקת ה-Client ID

4. הגדרה של מכשיר Android וירטואלי

כדי לבדוק את האפליקציה, אפשר להשתמש במכשיר Android פיזי או במכשיר Android וירטואלי (AVD).

יצירה והרצה של ה-AVD

  1. ב-Android Studio, פותחים את Device Manager (ניהול מכשירים), לוחצים על Create Virtual Device (יצירת מכשיר וירטואלי) (או על הסמל +) ובוחרים באפשרות Medium Phone (טלפון בגודל בינוני).
  2. בוחרים את הגרסה היציבה העדכנית ביותר כקובץ אימג' של המערכת ולוחצים על סיום.
  3. מפעילים את האמולטור בלחיצה על הסמל הפעלה/הרצה לצד המכשיר. מכשיר שבו פועלת האפליקציה

כניסה לחשבון Google במכשיר

  1. באימולטור, פותחים את אפליקציית ההגדרות ועוברים אל Google.
  2. לוחצים על כניסה לחשבון Google ופועלים לפי ההוראות. בוצעה התחברות למכשיר

5. הוספת יחסי תלות

מוסיפים לפרויקט את הספריות שנדרשות לאימות ולשילוב של Google ID:

  1. עוברים אל File > Project Structure > Dependencies > app (קובץ > מבנה הפרויקט > תלויות > אפליקציה).
  2. לוחצים על + > Library Dependency, מחפשים את com.google.android.libraries.identity.googleid:googleid ובוחרים את הגרסה האחרונה (למשל, 1.1.1).
  3. לוחצים שוב על + > Library Dependency, מחפשים את play-services-auth ובוחרים את הספרייה עם Group ID‏ com.google.android.gms.
  4. לוחצים על אישור כדי להחיל את השינויים ולסנכרן את הפרויקט. Finished Dependencies

6. הטמעה של תהליך הגיליון התחתון

תהליך הגיליון התחתון

תהליך הגיליון התחתון מסתמך על Credential Manager API כדי לספק למשתמשים דרך יעילה להיכנס לאפליקציה באמצעות חשבונות Google שלהם ב-Android. התהליך הזה נועד להיות מהיר ונוח, במיוחד למשתמשים חוזרים, והוא אמור להיות מופעל כשהאפליקציה מופעלת.

יצירת בקשת הכניסה

  1. כדי להתחיל, פותחים את MainActivity.kt ומסירים את פונקציות ברירת המחדל Greeting() ו-GreetingPreview().
  2. מוסיפים את הצהרות הייבוא הבאות אחרי ההצהרות הקיימות, החל משורה 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. מוסיפים את פונקציית ה-Composable הזו מתחת למחלקה MainActivity בקובץ 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)
     }
    

פירוט הקוד

  • LaunchedEffect(Unit): מפעיל את תהליך הכניסה לחשבון באופן מיידי כשהרכיב Composable מוצג בפעם הראשונה.
  • GetGoogleIdOption.Builder(): הגדרת הבקשה לאסימון מזהה של Google.
    • setFilterByAuthorizedAccounts(true): קודם מנסים כניסה שקטה על ידי סינון חשבונות שהמשתמש כבר אישר לאפליקציה הזו, כדי לצמצם את החיכוך למשתמשים חוזרים.
    • setNonce(...): מעביר צופן חד-פעמי אקראי ומאובטח שנוצר לכל בקשה על ידי generateSecureRandomNonce() כדי למנוע מתקפות שידור חוזר.
  • signIn(request, context): מריץ את הבקשה. אם הפעולה נכשלת עם NoCredentialException (כלומר, לא קיים חשבון שאושר בעבר), התהליך חוזר ל-setFilterByAuthorizedAccounts(false) כדי לאפשר למשתמש לבחור מתוך כל חשבון Google שמחובר למכשיר.

שליחת בקשת הכניסה

אחרי שיוצרים את בקשת הכניסה, אפשר להשתמש ב-Credential Manager כדי להשלים את תהליך הכניסה. יוצרים פונקציה בשם signIn שמבצעת את הבקשה ומטפלת בחריגים נפוצים שעשויים להתרחש.

מוסיפים את הפונקציה הזו מתחת לפונקציה BottomSheet בקובץ 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
    }
}

פירוט הקוד

  • credentialManager.getCredential(...): קריאה ל-Credential Manager API כדי להציג את גיליון התחתון או את תיבת הדו-שיח של בורר חשבונות המערכת.
  • delay(250): מושהה לזמן קצר כדי למנוע מרוץ תהליכים כשגיליון התחתון מופעל מיד בהפעלת האפליקציה לפני ששירות Credential Manager סיים את האתחול.
  • טיפול בחריגים: תופס שגיאות נפוצות בפרטי הכניסה (למשל ביטול, פרטי כניסה חסרים או בעיות בניתוח האסימון) ומתעד אותן, ומספק למשתמש משוב באמצעות הודעות קצרות.

הפעלת התהליך של הגיליון התחתון

מעדכנים את המחלקה MainActivity כדי לקרוא ל-BottomSheet() בהפעלה. מחליפים את YOUR_CLIENT_ID_HERE במזהה הלקוח של אפליקציית האינטרנט:

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)
                }
            }
        }
    }
}

שומרים את הפרויקט (קובץ > שמירה) ומריצים את האפליקציה:

  1. לוחצים על לחצן ההפעלה:הפעלת הפרויקט
  2. כשהאפליקציה תופעל באמולטור, יופיע גיליון תחתון לכניסה. לוחצים על המשך כדי לבדוק את התהליך.גיליון תחתון
  3. אמורה להופיע הודעה קופצת שמאשרת שהכניסה לחשבון בוצעה בהצלחה.גיליון תחתון של הצלחה

7. הטמעה של תהליך עבודה של כפתור

קובץ GIF של זרימת לחצנים

תהליך הכפתור מספק למשתמשים אפשרות ברורה להיכנס לחשבון או להירשם. שימוש במיתוג סטנדרטי מבטיח חוויה עקבית. להשתמש בנכסים שאושרו מראש ושעומדים בדרישות של הנחיות המיתוג של 'כניסה באמצעות חשבון Google'.

הוספת סמל המותג

  1. אפשר להוריד את הנכסים שקשורים למותג כאן ולחלץ את קובץ ה-ZIP.
  2. מעתיקים את signin-assets/Android/png@2x/neutral/android_neutral_sq_SI@2x.png.
  3. ב-Android Studio, מדביקים את הקובץ בתיקייה res > drawable, משנים את השם שלו ל-siwg_button.png ולוחצים על OK. הוספת כפתור

קוד של זרימת כפתור

בתרשים הזרימה הזה נעשה שימוש חוזר בפונקציית העזר signIn, אבל במקום GetGoogleIdOption מועבר GetSignInWithGoogleOption. בניגוד לתהליך של Bottom Sheet, בתהליך של כפתור מפורש אין סינון מראש או הנחיה אוטומטית לשימוש בפרטי כניסה או במפתחות גישה שמורים. מדביקים את הפונקציה הקומפוזבילית הזו מתחת לפונקציה 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)
    )
}

פירוט הקוד

  • GetSignInWithGoogleOption: בניגוד לתהליך של Bottom Sheet, בתהליך של לחצן מפורש נעשה שימוש באפשרות הזו כדי להציג למשתמשים בקשה לבחור את חשבון Google שלהם בלי סינון אוטומטי.
  • coroutineScope.launch: מפעיל שגרת המשך (coroutine) כדי להריץ את פונקציית ההשעיה signIn באופן אסינכרוני כשלוחצים על הכפתור.
  • Image: מציג את רכיב ה-drawable של המותג ומצרף מאזין לקליקים כדי להפעיל את התהליך.siwg_button

הוספת הכפתור לפריסה של ממשק המשתמש

מעדכנים את הפריסה MainActivity כך שגם BottomSheet וגם ButtonUI יוצגו אנכית:

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)
                    }
                }
            }
        }
    }
}

בדיקת התהליך של הלחצן

  1. מפעילים את האפליקציה.
  2. כדי לסגור את הגיליון התחתון הראשוני, לוחצים מחוץ לאזור הגיליון.
  3. לוחצים על הלחצן כניסה באמצעות חשבון Google כדי לפתוח את תיבת הדו-שיח לכניסה, ובוחרים את החשבון. תיבת דו-שיח לכניסה
  4. מאמתים את התוצאה: בודקים את Logcat כדי לוודא שהשם או כתובת האימייל שלכם הודפסו.

8. סיכום

מעולה! הטמעת בהצלחה את הכניסה באמצעות חשבון Google באמצעות Credential Manager של Android.

מקורות מידע נוספים

הקוד המלא של MainActivity.kt

לעיון, הנה הקוד המלא של MainActivity.kt:

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
    }
}