كيفية تنفيذ ميزة "تسجيل الدخول باستخدام حساب Google" في تطبيق Android

1- قبل البدء

تعرَّفوا على كيفية تنفيذ ميزة "تسجيل الدخول باستخدام حساب Google" على Android باستخدام "مدير بيانات الاعتماد".

المتطلبات الأساسية

أهداف الدورة التعليمية

  • إنشاء مشروع على Google Cloud وعملاء OAuth
  • تنفيذ مسار تسجيل الدخول في البطاقة السفلية
  • تنفيذ مسار تسجيل الدخول باستخدام الزر الصريح

ما تحتاج إليه

2. إنشاء مشروع في "استوديو Android"

للبدء، أنشئوا مشروعًا جديدًا في "استوديو Android":

  1. افتحوا "استوديو Android" وانقروا على مشروع جديد. رسالة الترحيب في "استوديو Android"
  2. اختاروا الهاتف والجهاز اللوحي > نشاط فارغ ، ثم انقروا على التالي. مشروع "استوديو Android"
  3. اضبطوا إعدادات المشروع:
    • الاسم: اختاروا اسمًا للمشروع.
    • اسم الحزمة: استخدِموا الاسم التلقائي أو اختاروا اسمًا خاصًا بكم.
    • الحد الأدنى من حزمة تطوير البرامج (SDK): اختاروا أحدث إصدار ثابت أو > Android 14.

مشروع إعداد "استوديو Android"

  1. انقروا على إنهاء وانتظروا إلى أن يكتمل الإصدار الأوّلي للمشروع. إنشاء مشروع "استوديو Android"

3. إعداد مشروعكم على Google Cloud

إنشاء مشروع على Google Cloud

  1. انتقِلوا إلى Google Cloud Console واختاروا مشروعًا أو أنشئوا مشروعًا. إنشاء مشروع جديد على Google Cloud Platform
  2. انتقِلوا إلى واجهات برمجة التطبيقات والخدمات > شاشة طلب موافقة OAuth. شاشة طلب الموافقة المتعلّقة ببروتوكول OAuth في Google Cloud Platform
  3. انقروا على البدء وأدخِلوا الحقول المطلوبة:
    • اسم التطبيق: استخدِموا اسم تطبيق Android.
    • البريد الإلكتروني لدعم المستخدمين: اختاروا حساب Google.
    • الجمهور: اختاروا خارجي.
    • معلومات التواصل: أدخِلوا عنوان بريدكم الإلكتروني. معلومات تطبيق GCP
  4. راجِعوا بنود خدمة Google APIs وسياسة بيانات المستخدمين الخاصة بخدمات واجهة برمجة التطبيقات من Google وانقروا على إنشاء. GCP Create

إعداد عملاء OAuth

عليكم إنشاء عميل ويب وعميل Android في Google Cloud Console للحصول على معرّفات العملاء من أجل المصادقة.

  • عميل Android: يؤمِّن الطلبات من خلال التحقّق من اسم حزمة تطبيقكم وتوقيع SHA-1.
  • عميل الويب: يعمل كعميل في الخلفية لخدمة "تسجيل الدخول باستخدام حساب Google".

إنشاء عميل OAuth 2.0 على Android

  1. في صفحة العملاء ، انقروا على إنشاء عميل واختاروا Android كـ نوع التطبيق. إنشاء عملاء Google Cloud Platform
  2. أدخِلوا اسم حزمة تطبيقكم (الذي يتطابق مع السطر 1 من MainActivity.kt).
  3. أنشئوا توقيع SHA-1. افتحوا نافذة المحطة الطرفية في "استوديو Android" وشغِّلوا: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 من ناتج الأمر، وألصِقوها في حقل بصمة الإصبع SHA-1 في وحدة التحكّم، ثم انقروا على إنشاء. تفاصيل عميل Android

إنشاء عميل OAuth 2.0 على الويب

  1. انقروا على إنشاء عميل مرة أخرى واختاروا تطبيق الويب كـ نوع التطبيق.
  2. أطلقوا اسمًا على عميل الويب، واتركوا حقلَي عنوان URL/المصادر فارغَين، ثم انقروا على إنشاء. تفاصيل برنامج تطبيقات الويب
  3. انسخوا معرّف العميل الذي تم إنشاؤه من مربّع حوار التأكيد. ستستخدمون هذا المعرّف في رمز Kotlin. نسخ معرّف العميل

4. إعداد جهاز Android افتراضي

لاختبار التطبيق، يمكنكم استخدام جهاز Android فعلي أو جهاز Android افتراضي.

إنشاء جهاز Android افتراضي وتشغيله

  1. في "استوديو Android"، افتحوا أداة إدارة الأجهزة، وانقروا على إنشاء جهاز افتراضي (أو رمز +)، واختاروا هاتف متوسط الحجم.
  2. اختاروا أحدث إصدار ثابت كصورة النظام وانقروا على إنهاء.
  3. شغِّلوا المحاكي من خلال النقر على رمز تشغيل/تنفيذ بجانب الجهاز. تشغيل الجهاز

تسجيل الدخول إلى حساب Google على الجهاز

  1. على المحاكي، افتحوا تطبيق الإعدادات وانتقِلوا إلى Google.
  2. انقروا على تسجيل الدخول إلى حساب Google واتّبِعوا التعليمات. تم تسجيل الدخول إلى الجهاز

5. إضافة التبعيات

أضيفوا المكتبات المطلوبة للمصادقة ودمج معرّف Google إلى مشروعكم:

  1. انتقِلوا إلى ملف > بنية المشروع > التبعيات > التطبيق.
  2. انقروا على + > تبعية المكتبة ، وابحثوا عن com.google.android.libraries.identity.googleid:googleid، واختاروا أحدث إصدار (مثل 1.1.1).
  3. انقروا على + > تبعية المكتبة مرة أخرى، وابحثوا عن play-services-auth، واختاروا المكتبة التي تحمل معرّف المجموعة com.google.android.gms.
  4. انقروا على حسنًا لتطبيق التغييرات ومزامنة مشروعكم. المهام التابعة المكتملة

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. أضيفوا هذه الدالة البرمجية المركّبة أسفل فئة 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): يؤدي إلى تفعيل مسار تسجيل الدخول فور عرض العنصر المركّب لأول مرة.
  • 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"، ألصِقوا الملف في مجلد res > drawable، وأعيدوا تسميته إلى siwg_button.png، ثم انقروا على حسنًا. إضافة زر

رمز مسار الزر

يعيد هذا المسار استخدام دالة المساعدة signIn نفسها، ولكنّه يمرِّر GetSignInWithGoogleOption بدلاً من GetGoogleIdOption. على عكس مسار البطاقة السفلية، لا يفلتر مسار الزر الصريح بيانات الاعتماد أو مفاتيح المرور المخزّنة أو يطلبها تلقائيًا. ألصِقوا هذه الدالة البرمجية المركّبة أسفل دالة 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: على عكس مسار البطاقة السفلية، يستخدم مسار الزر الصريح هذا الخيار لطلب اختيار حساب Google من المستخدمين بدون فلترة تلقائية.
  • coroutineScope.launch: يشغِّل كوروتين لتنفيذ دالة التعليق signIn بشكلٍ غير متزامن عند النقر على الزر.
  • Image: يعرض الرسم 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" باستخدام "مدير بيانات الاعتماد" على 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
    }
}