Android uygulamanızda Google ile oturum açma özelliğini nasıl uygulayacağınızı öğrenme

1. Başlamadan önce

Kimlik Bilgisi Yöneticisi'ni kullanarak Android'de Google ile oturum açma özelliğini nasıl uygulayacağınızı öğrenin.

Ön koşullar

Neler öğreneceksiniz?

  • Google Cloud projesi ve OAuth istemcileri oluşturun.
  • Alt sayfa oturum açma akışını uygulayın.
  • Açık düğme ile oturum açma akışını uygulayın.

İhtiyacınız olanlar

2. Android Studio projesi oluşturma

Başlamak için Android Studio'da yeni bir proje oluşturun:

  1. Android Studio'yu açın ve New Project'i (Yeni Proje) tıklayın. Android Studio'ya Hoş Geldiniz
  2. Telefon ve Tablet > Boş Etkinlik'i seçin, ardından Sonraki'yi tıklayın. Android Studio Projesi
  3. Proje ayarlarını yapılandırın:
    • Ad: Bir proje adı seçin.
    • Paket adı: Varsayılanı kullanın veya kendi adınızı seçin.
    • Minimum SDK: En son kararlı sürümü veya > Android 14'ü seçin.

Android Studio'da Proje Oluşturma

  1. Bitir'i tıklayın ve ilk proje oluşturma işleminin tamamlanmasını bekleyin. Android Studio Projesi Oluşturuldu

3. Google Cloud projenizi oluşturma

Google Cloud projesi oluşturma

  1. Google Cloud Console'a gidip bir proje seçin veya oluşturun. GCP'de yeni proje oluşturma
  2. API'ler ve Hizmetler > OAuth kullanıcı rızası ekranı'na gidin. GCP OAuth kullanıcı rızası ekranı
  3. Başlayın'ı tıklayın ve zorunlu alanları doldurun:
    • Uygulama adı: Android uygulamanızın adını kullanın.
    • Kullanıcı Destek Ekibi E-posta Adresi: Google Hesabınızı seçin.
    • Kitle: Harici'yi seçin.
    • İletişim bilgileri: E-posta adresinizi girin. GCP Uygulama Bilgileri
  4. Google API Hizmetleri: Kullanıcı Verileri Politikası'nı inceleyin ve Oluştur'u tıklayın. GCP Create

OAuth istemcilerini ayarlama

Kimlik doğrulama için istemci kimliklerini almak üzere Google Cloud Console'da hem Web istemcisi hem de Android istemcisi oluşturmanız gerekir.

  • Android istemcisi: Uygulamanızın paket adını ve SHA-1 imzasını doğrulayarak istekleri güvenli hale getirir.
  • Web istemcisi: Google ile Oturum Açma hizmeti için arka uç istemcisi olarak çalışır.

Android OAuth 2.0 istemcisi oluşturma

  1. İstemciler sayfasında İstemci oluştur'u tıklayın ve Uygulama türü olarak Android'i seçin. GCP'de istemci oluşturma
  2. Uygulamanızın paket adını girin (MainActivity.kt dosyanızın 1. satırıyla eşleşmelidir).
  3. SHA-1 imzanızı oluşturun. Android Studio terminalini açın ve şu komutu çalıştırın: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. Komut çıkışından SHA-1 parmak izini kopyalayın, konsoldaki SHA-1 parmak izi alanına yapıştırın ve Oluştur'u tıklayın. Android İstemcisi Ayrıntıları

Web OAuth 2.0 istemcisi oluşturma

  1. Create Client'ı (İstemci Oluştur) tekrar tıklayın ve Application type (Uygulama türü) olarak Web Application'ı (Web Uygulaması) seçin.
  2. Web istemcinize ad verin, URL/Kaynak alanlarını boş bırakın ve Oluştur'u tıklayın. Web İstemcisi Ayrıntıları
  3. Onay iletişim kutusunda oluşturulan istemci kimliğini kopyalayın. Bunu Kotlin kodunuzda kullanacaksınız. İstemci kimliğini kopyalama

4. Android sanal cihazı kurma

Uygulamayı test etmek için fiziksel bir Android cihaz veya Android Sanal Cihaz (AVD) kullanabilirsiniz.

AVD oluşturma ve çalıştırma

  1. Android Studio'da Cihaz Yönetim Paneli'ı açın, Sanal Cihaz Oluştur'u (veya + simgesini) tıklayın ve Orta Boy Telefon'u seçin.
  2. En son kararlı sürümü sistem görüntüsü olarak seçin ve Bitir'i tıklayın.
  3. Cihazın yanındaki Play/Run (Oynat/Çalıştır) simgesini tıklayarak emülatörü başlatın. Çalışan Cihaz

Cihazda bir Google Hesabı'nda oturum açın.

  1. Emülatörde Ayarlar uygulamasını açıp Google'a gidin.
  2. Google Hesabınızda oturum açın'ı tıklayın ve talimatları uygulayın. Cihazda oturum açıldı

5. Bağımlılık ekleme

Kimlik doğrulama ve Google Kimliği entegrasyonu için gereken kitaplıkları projenize ekleyin:

  1. File > Project Structure > Dependencies > app'e (Dosya > Proje Yapısı > Bağımlılıklar > uygulama) gidin.
  2. + > Library Dependency'yi (Kitaplık Bağımlılığı) tıklayın, com.google.android.libraries.identity.googleid:googleid ifadesini arayın ve en son sürümü (ör. 1.1.1) seçin.
  3. Tekrar + > Library Dependency'yi tıklayın, play-services-auth ifadesini arayın ve Grup Kimliği com.google.android.gms olan kitaplığı seçin.
  4. Değişiklikleri uygulamak ve projenizi senkronize etmek için Tamam'ı tıklayın. Tamamlanan Bağımlılıklar

6. Alt sayfa akışını uygulama

Alt Sayfa Akışı

Alt sayfa akışı, kullanıcıların Android'de Google Hesaplarını kullanarak uygulamanızda oturum açmalarını kolaylaştırmak için Credential Manager API'den yararlanır. Özellikle geri gelen kullanıcılar için hız ve kolaylık düşünülerek tasarlanan bu akış, uygulama başlatıldığında tetiklenmelidir.

Oturum açma isteğini oluşturma

  1. Başlamak için MainActivity.kt uygulamasını açın ve varsayılan Greeting() ile GreetingPreview() işlevlerini kaldırın.
  2. 3. satırdan başlayarak mevcut içe aktarma ifadelerinin ardından aşağıdaki içe aktarma ifadelerini ekleyin:
    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.kt dosyanızdaki MainActivity sınıfının altına şu Composable işlevini ekleyin:
     @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)
     }
    

Kod Analizi

  • LaunchedEffect(Unit): Composable ilk kez görüntülendiğinde oturum açma akışını hemen tetikler.
  • GetGoogleIdOption.Builder(): Google kimlik jetonu isteğini yapılandırır.
    • setFilterByAuthorizedAccounts(true): Kullanıcının bu uygulama için daha önce yetkilendirdiği hesapları filtreleyerek önce sessiz oturum açma işlemi denenir. Böylece geri gelen kullanıcılar için sorun en aza indirilir.
    • setNonce(...): Tekrar oynatma saldırılarını önlemek için generateSecureRandomNonce() tarafından her istek için oluşturulan güvenli bir tek seferlik rastgele sayıyı iletir.
  • signIn(request, context): İsteği yürütür. NoCredentialException ile başarısız olursa (daha önce yetkilendirilmiş bir hesap olmadığı anlamına gelir) akış, kullanıcının cihazda oturum açılmış herhangi bir Google Hesabı'nı seçmesine izin vermek için setFilterByAuthorizedAccounts(false)'ye geri döner.

Oturum açma isteğinde bulunma

Oturum açma isteği oluşturulduktan sonra oturum açma işlemini tamamlamak için Kimlik Bilgisi Yöneticisi'ni kullanabilirsiniz. İsteği yürüten ve oluşabilecek yaygın istisnaları işleyen signIn adlı bir işlev oluşturun.

MainActivity.kt dosyanızdaki BottomSheet işlevinin altına şu işlevi ekleyin:

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

Kod Analizi

  • credentialManager.getCredential(...): Sistem hesabı seçici alt sayfasını veya iletişim kutusunu göstermek için Credential Manager API'yi çağırır.
  • delay(250): Kimlik bilgisi yöneticisi hizmetinin başlatılması tamamlanmadan önce uygulama başlatıldığında alt sayfa hemen tetiklenirse yarışma koşulunu önlemek için kısa bir süre duraklatılır.
  • İstisna İşleme: Yaygın kimlik bilgisi hatalarını (ör. iptal, eksik kimlik bilgileri veya jeton ayrıştırma sorunları) yakalayıp günlüğe kaydeder ve pop-up mesajlar aracılığıyla kullanıcı geri bildirimi sağlar.

Alt sayfa akışını tetikleme

Başlangıçta BottomSheet() işlevini çağırmak için MainActivity sınıfınızı güncelleyin. YOUR_CLIENT_ID_HERE öğesini web uygulaması istemci kimliğinizle değiştirin:

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

Projenizi kaydedin (Dosya > Kaydet) ve uygulamayı çalıştırın:

  1. Çalıştır düğmesine basın:Projeyi Çalıştır
  2. Uygulama emülatörde başlatıldığında oturum açma alt sayfası görünmelidir. Akışı test etmek için Devam'ı tıklayın.alt sayfa
  3. Oturum açma işleminin başarılı olduğunu onaylayan bir kısa ileti bildirimi gösterilir.Alt Sayfa Başarısı

7. Düğme akışını uygulama

Düğme akışı GIF'i

Düğme akışı, kullanıcıların oturum açması veya kaydolması için açık bir seçenek sunar. Standart marka öğeleri kullanmak tutarlı bir deneyim sağlar. Google ile Oturum Açma Markalama Kuralları'na uygun, önceden onaylanmış öğeler kullanın.

Marka simgesini ekleme

  1. Marka öğelerini buradan indirip ZIP dosyasını çıkarın.
  2. Kopyala'yı signin-assets/Android/png@2x/neutral/android_neutral_sq_SI@2x.png tıklayın.
  3. Android Studio'da dosyayı res > drawable klasörünüze yapıştırın, siwg_button.png olarak yeniden adlandırın ve Tamam'ı tıklayın. Düğme Ekleme

Düğme akışı kodu

Bu akışta aynı signIn yardımcı işlevi yeniden kullanılır ancak GetGoogleIdOption yerine GetSignInWithGoogleOption iletilir. Alt sayfa akışının aksine, açık düğme akışında önceden filtreleme yapılmaz veya kayıtlı kimlik bilgileri ya da geçiş anahtarları için otomatik olarak istem gösterilmez. Bu Composable işlevini BottomSheet işlevinin altına yapıştırın:

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

Kod Analizi

  • GetSignInWithGoogleOption: Açık düğme akışı, alt sayfa akışının aksine, kullanıcıları Google Hesaplarını seçmeye yönlendirmek için bu seçeneği otomatik filtreleme olmadan kullanır.
  • coroutineScope.launch: Düğme tıklandığında askıya alma signIn işlevini eşzamansız olarak yürütmek için bir eş yordam başlatır.
  • Image: Markalı siwg_button çizilebilir öğesini gösterir ve akışı tetiklemek için bir tıklama dinleyicisi ekler.

Düğmeyi kullanıcı arayüzü yerleşimine ekleme

MainActivity düzeninizi, hem otomatik BottomSheet hem de açık ButtonUI öğelerini dikey olarak hizalanmış şekilde gösterecek şekilde güncelleyin:

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

Düğme akışını test etme

  1. Uygulamayı çalıştırın.
  2. Sayfa alanının dışını tıklayarak ilk alt sayfayı kapatın.
  3. Oturum açma iletişim kutusunu başlatmak için Google ile oturum aç düğmesini tıklayın ve hesabınızı seçin. Oturum açma iletişim kutusu
  4. Sonucu doğrulayın: Kullanıcı adınızın/e-posta adresinizin çıktısını onaylamak için Logcat'i kontrol edin.

8. Sonuç

Tebrikler! Android Kimlik Bilgisi Yöneticisi'ni kullanarak Google ile oturum açma özelliğini başarıyla uyguladınız.

Ek kaynaklar

MainActivity.kt kodunun tamamı

Referans için MainActivity.kt dosyasının tam kodu aşağıda verilmiştir:

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