ดูวิธีใช้ฟีเจอร์ลงชื่อเข้าใช้ด้วย Google ในแอป Android

1. ก่อนเริ่มต้น

ดูวิธีใช้ฟีเจอร์ลงชื่อเข้าใช้ด้วย Google ใน Android โดยใช้ Credential Manager

ข้อกำหนดเบื้องต้น

  • มีความเข้าใจพื้นฐานเกี่ยวกับ Kotlin สำหรับการพัฒนาแอป Android
  • มีความเข้าใจพื้นฐานเกี่ยวกับ Jetpack Compose (ดูข้อมูลเพิ่มเติม)

สิ่งที่คุณจะได้เรียนรู้

  • สร้างโปรเจ็กต์ที่อยู่ในระบบคลาวด์ของ Google และไคลเอ็นต์ OAuth
  • ใช้โฟลว์การลงชื่อเข้าใช้ Bottom Sheet
  • ใช้โฟลว์การลงชื่อเข้าใช้ด้วยปุ่มที่ชัดเจน

สิ่งที่ต้องมี

2. สร้างโปรเจ็กต์ Android Studio

หากต้องการเริ่มต้นใช้งาน ให้สร้างโปรเจ็กต์ใหม่ใน Android Studio โดยทำดังนี้

  1. เปิด Android Studio แล้วคลิก New Project ยินดีต้อนรับสู่ Android Studio
  2. เลือก Phone and Tablet > Empty Activity แล้วคลิก Next โปรเจ็กต์ Android Studio
  3. กำหนดการตั้งค่าโปรเจ็กต์ดังนี้
    • ชื่อ: เลือกชื่อโปรเจ็กต์
    • Package name: ใช้ค่าเริ่มต้นหรือเลือกค่าของคุณเอง
    • Minimum SDK: เลือกเวอร์ชันเสถียรล่าสุดหรือ > Android 14

โปรเจ็กต์การตั้งค่า Android Studio

  1. คลิก Finish แล้วรอให้การบิลด์โปรเจ็กต์เริ่มต้นเสร็จสมบูรณ์ สร้างโปรเจ็กต์ Android Studio แล้ว

3. ตั้งค่าโปรเจ็กต์ Google Cloud

สร้างโปรเจ็กต์ Google Cloud

  1. ไปที่ คอนโซล Google Cloud แล้วเลือกหรือสร้างโปรเจ็กต์ GCP สร้างโปรเจ็กต์ใหม่
  2. ไปที่ APIs & Services > หน้าจอขอความยินยอม OAuth หน้าจอขอความยินยอม OAuth ของ GCP
  3. คลิก Get started แล้วกรอกข้อมูลในช่องที่ต้องกรอกดังนี้
    • ชื่อแอป: ใช้ชื่อแอป Android
    • User Support Email: เลือกบัญชี Google
    • Audience: เลือก External
    • ข้อมูลติดต่อ: ป้อนอีเมล ข้อมูลแอป GCP
  4. อ่านบริการ Google API: นโยบายข้อมูลผู้ใช้ แล้วคลิก Create GCP Create

ตั้งค่าไคลเอ็นต์ OAuth

คุณต้องสร้างทั้งเว็บไคลเอ็นต์ และไคลเอ็นต์ Android ในคอนโซล Google Cloud เพื่อรับรหัสไคลเอ็นต์สำหรับการตรวจสอบสิทธิ์

  • ไคลเอ็นต์ Android: รักษาความปลอดภัยของคำขอโดยการยืนยันชื่อแพ็กเกจและลายเซ็น SHA-1 ของแอป
  • เว็บไคลเอ็นต์: ทำหน้าที่เป็นไคลเอ็นต์แบ็กเอนด์สำหรับบริการลงชื่อเข้าใช้ด้วย Google

สร้างไคลเอ็นต์ OAuth 2.0 สำหรับ Android

  1. ในหน้าไคลเอ็นต์ ให้คลิก Create Client แล้วเลือก Android เป็นประเภทแอปพลิเคชัน สร้างไคลเอ็นต์ GCP
  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 จากเอาต์พุตจากคำสั่ง แล้ววางลงในช่องฟิงเกอร์ปรินต์ SHA-1 ในคอนโซล จากนั้นคลิก Create รายละเอียดไคลเอ็นต์ Android

สร้างไคลเอ็นต์ OAuth 2.0 สำหรับเว็บ

  1. คลิก Create Client อีกครั้ง แล้วเลือก Web Application เป็นประเภทแอปพลิเคชัน
  2. ตั้งชื่อเว็บไคลเอ็นต์ เว้นช่อง URL/Origins ว่างไว้ แล้วคลิก Create รายละเอียดไคลเอ็นต์บนเว็บ
  3. คัดลอกรหัสไคลเอ็นต์ ที่สร้างขึ้นจากกล่องโต้ตอบการยืนยัน คุณจะใช้รหัสนี้ในโค้ด Kotlin คัดลอกรหัสไคลเอ็นต์

4. ตั้งค่าอุปกรณ์เสมือน Android

หากต้องการทดสอบแอป คุณสามารถใช้อุปกรณ์ Android จริงหรืออุปกรณ์เสมือน Android (AVD)

สร้างและเรียกใช้ AVD

  1. ใน Android Studio ให้เปิดโปรแกรมจัดการอุปกรณ์ คลิก Create Virtual Device (หรือไอคอน +) แล้วเลือก Medium Phone
  2. เลือกเวอร์ชันเสถียรล่าสุดเป็นอิมเมจระบบ แล้วคลิก Finish
  3. เปิดใช้โปรแกรมจำลองโดยคลิกไอคอน Play/Run ข้างอุปกรณ์ อุปกรณ์ที่ใช้

ลงชื่อเข้าใช้บัญชี Google ในอุปกรณ์

  1. เปิดแอปการตั้งค่า ในโปรแกรมจำลอง แล้วไปที่ Google
  2. คลิก ลงชื่อเข้าใช้บัญชี Google แล้วทำตามข้อความที่ปรากฏ ลงชื่อเข้าใช้อุปกรณ์แล้ว

5. เพิ่มทรัพยากร Dependency

เพิ่มไลบรารีที่จำเป็นสำหรับการตรวจสอบสิทธิ์และการผสานรวม 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. คลิก OK เพื่อใช้การเปลี่ยนแปลงและซิงค์โปรเจ็กต์ ทรัพยากร Dependency ที่เสร็จสมบูรณ์

6. ใช้โฟลว์ Bottom Sheet

โฟลว์ของ Bottom Sheet

โฟลว์ Bottom Sheet ใช้ประโยชน์จาก 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 ID
    • setFilterByAuthorizedAccounts(true): พยายามลงชื่อเข้าใช้แบบเงียบก่อนโดยกรองบัญชีที่ผู้ใช้ได้ให้สิทธิ์แอปนี้ไว้แล้ว ซึ่งจะช่วยลดความยุ่งยากสำหรับผู้ใช้ที่กลับมาใช้แอปอีกครั้ง
    • setNonce(...): ส่ง nonce แบบสุ่มที่ปลอดภัยซึ่งสร้างขึ้นสำหรับแต่ละคำขอโดย generateSecureRandomNonce() เพื่อป้องกันการโจมตีแบบ Replay
  • 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 เพื่อแสดง Bottom Sheet หรือกล่องโต้ตอบตัวเลือกบัญชีของระบบ
  • delay(250): หยุดชั่วคราวเพื่อป้องกันสภาวะการแข่งขันเมื่อมีการเรียกใช้ Bottom Sheet ทันทีที่เริ่มต้นแอปก่อนที่บริการ Credential Manager จะเริ่มต้นเสร็จสมบูรณ์
  • การจัดการข้อยกเว้น: ดักจับและบันทึกข้อผิดพลาดทั่วไปเกี่ยวกับข้อมูลเข้าสู่ระบบ (เช่น การยกเลิก ข้อมูลเข้าสู่ระบบหายไป หรือปัญหาการแยกวิเคราะห์โทเค็น) และแสดงความคิดเห็นของผู้ใช้โดยใช้ Toast

เรียกใช้โฟลว์ Bottom Sheet

อัปเดตคลาส 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)
                }
            }
        }
    }
}

บันทึกโปรเจ็กต์ (File > Save) แล้วเรียกใช้แอปพลิเคชันโดยทำดังนี้

  1. กดปุ่มเรียกใช้ เรียกใช้โปรเจ็กต์
  2. เมื่อแอปเปิดตัวในโปรแกรมจำลอง Bottom Sheet การลงชื่อเข้าใช้ควรปรากฏขึ้น คลิก Continue เพื่อทดสอบโฟลว์Bottom Sheet
  3. การแจ้งเตือน Toast ควรปรากฏขึ้นเพื่อยืนยันว่าการลงชื่อเข้าใช้สำเร็จแล้วBottom Sheet ที่สำเร็จ

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 เดิมซ้ำ แต่ส่ง GetSignInWithGoogleOption แทน GetGoogleIdOption โฟลว์ปุ่มที่ชัดเจนจะไม่กรองล่วงหน้าหรือแจ้งให้ป้อนข้อมูลเข้าสู่ระบบหรือ Passkey ที่จัดเก็บไว้โดยอัตโนมัติ ซึ่งแตกต่างจากโฟลว์ 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: โฟลว์ปุ่มที่ชัดเจนจะใช้ตัวเลือกนี้เพื่อแจ้งให้ผู้ใช้เลือกบัญชี Google โดยไม่มีการกรองอัตโนมัติ ซึ่งแตกต่างจากโฟลว์ Bottom Sheet
  • coroutineScope.launch: เปิดใช้ Coroutine เพื่อเรียกใช้ฟังก์ชัน signIn แบบระงับการทำงานแบบไม่พร้อมกันเมื่อมีการคลิกปุ่ม
  • Image: แสดง Drawable siwg_button ที่มีแบรนด์และแนบฟังก์ชันรอคลิกเพื่อเรียกใช้โฟลว์

เพิ่มปุ่มลงในเลย์เอาต์ของ UI

อัปเดตเลย์เอาต์ 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. ปิด Bottom Sheet เริ่มต้นโดยคลิกนอกพื้นที่ชีต
  3. คลิกปุ่มลงชื่อเข้าใช้ด้วย Google เพื่อเปิดกล่องโต้ตอบการลงชื่อเข้าใช้ แล้วเลือกบัญชี กล่องโต้ตอบการลงชื่อเข้าใช้
  4. ตรวจสอบผลลัพธ์โดยดู Logcat เพื่อยืนยันการพิมพ์ชื่อผู้ใช้/อีเมล

8. บทสรุป

ยินดีด้วย คุณใช้ฟีเจอร์ลงชื่อเข้าใช้ด้วย Google โดยใช้ Android Credential Manager ได้สำเร็จแล้ว

แหล่งข้อมูลเพิ่มเติม

โค้ด 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
    }
}