5

I'm trying to integrate One Tap Sign in with Google into my app which I'm building with Jetpack Compose. I'm using startIntentSenderForResult to launch an intent, but now the problem is that I'm unable to receive activity result from my composable function. I'm using rememberLauncherForActivityResult to get the result from an intent but still not getting anywhere. Any solutions?

LoginScreen

@Composable
fun LoginScreen() {
    val activity = LocalContext.current as Activity

    val activityResult = remember { mutableStateOf<ActivityResult?>(null) }
    val launcher = rememberLauncherForActivityResult(
        ActivityResultContracts.StartActivityForResult()
    ) { result ->
        val oneTapClient = Identity.getSignInClient(activity)
        val credential = oneTapClient.getSignInCredentialFromIntent(result.data)
        val idToken = credential.googleIdToken
        if (idToken != null) {
            // Got an ID token from Google. Use it to authenticate
            // with your backend.
            Log.d("LOG", idToken)
        } else {
            Log.d("LOG", "Null Token")
        }

        Log.d("LOG", "ActivityResult")
        if (result.resultCode == Activity.RESULT_OK) {
            activityResult.value = result
        }
    }

    activityResult.value?.let { _ ->
        Log.d("LOG", "ActivityResultValue")
    }

    Column(
        modifier = Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        GoogleButton(
            onClick = {
                signIn(
                    activity = activity
                )
            }
        )
    }
}

fun signIn(
    activity: Activity
) {
    val oneTapClient = Identity.getSignInClient(activity)
    val signInRequest = BeginSignInRequest.builder()
        .setGoogleIdTokenRequestOptions(
            BeginSignInRequest.GoogleIdTokenRequestOptions.builder()
                .setSupported(true)
                // Your server's client ID, not your Android client ID.
                .setServerClientId(CLIENT_ID)
                // Only show accounts previously used to sign in.
                .setFilterByAuthorizedAccounts(true)
                .build()
        )
        // Automatically sign in when exactly one credential is retrieved.
        .setAutoSelectEnabled(true)
        .build()

    oneTapClient.beginSignIn(signInRequest)
        .addOnSuccessListener(activity) { result ->
            try {
                startIntentSenderForResult(
                    activity, result.pendingIntent.intentSender, ONE_TAP_REQ_CODE,
                    null, 0, 0, 0, null
                )
            } catch (e: IntentSender.SendIntentException) {
                Log.e("LOG", "Couldn't start One Tap UI: ${e.localizedMessage}")
            }
        }
        .addOnFailureListener(activity) { e ->
            // No saved credentials found. Launch the One Tap sign-up flow, or
            // do nothing and continue presenting the signed-out UI.
            Log.d("LOG", e.message.toString())
        }
}
Stefan
  • 2,829
  • 5
  • 20
  • 44
  • 1
    If you are using an `IntentSender`, then why aren't you using the [`StartIntentSenderForResult` contract](https://developer.android.com/reference/androidx/activity/result/contract/ActivityResultContracts.StartIntentSenderForResult) with your `rememberLauncherForActivityResult`? – ianhanniballake Jan 25 '22 at 15:42
  • @ianhanniballake Thanks! – Stefan Jan 25 '22 at 16:04
  • I think that this [resource](https://medium.com/firebase-developers/how-to-authenticate-to-firebase-using-google-one-tap-in-jetpack-compose-60b30e621d0d) might help. Here is the corresponding [repo](https://github.com/alexmamo/FirebaseSignInWithGoogle). – Alex Mamo Jul 23 '22 at 07:05

1 Answers1

9

You aren't actually calling launch on the launcher you create, so you would never get a result back there.

Instead of using the StartActivityForResult contract, you need to use the StartIntentSenderForResult contract - that's the one that takes an IntentSender like the one you get back from your beginSignIn method.

This means your code should look like:

@Composable
fun LoginScreen() {
    val context = LocalContext.current

    val launcher = rememberLauncherForActivityResult(
        ActivityResultContracts.StartIntentSenderForResult()
    ) { result ->
        if (result.resultCode != Activity.RESULT_OK) {
          // The user cancelled the login, was it due to an Exception?
          if (result.data?.action == StartIntentSenderForResult.ACTION_INTENT_SENDER_REQUEST) {
            val exception: Exception? = result.data?.getSerializableExtra(StartIntentSenderForResult.EXTRA_SEND_INTENT_EXCEPTION)
            Log.e("LOG", "Couldn't start One Tap UI: ${e?.localizedMessage}")
          }
          return@rememberLauncherForActivityResult
        }
        val oneTapClient = Identity.getSignInClient(context)
        val credential = oneTapClient.getSignInCredentialFromIntent(result.data)
        val idToken = credential.googleIdToken
        if (idToken != null) {
            // Got an ID token from Google. Use it to authenticate
            // with your backend.
            Log.d("LOG", idToken)
        } else {
            Log.d("LOG", "Null Token")
        }
    }

    Column(
        modifier = Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        // Create a scope that is automatically cancelled
        // if the user closes your app while async work is
        // happening
        val scope = rememberCoroutineScope()
        GoogleButton(
            onClick = {
                scope.launch {
                  signIn(
                    context = context,
                    launcher = launcher
                  )
                }
            }
        )
    }
}

suspend fun signIn(
    context: Context,
    launcher: ActivityResultLauncher<IntentSenderRequest>
) {
    val oneTapClient = Identity.getSignInClient(context)
    val signInRequest = BeginSignInRequest.builder()
        .setGoogleIdTokenRequestOptions(
            BeginSignInRequest.GoogleIdTokenRequestOptions.builder()
                .setSupported(true)
                // Your server's client ID, not your Android client ID.
                .setServerClientId(CLIENT_ID)
                // Only show accounts previously used to sign in.
                .setFilterByAuthorizedAccounts(true)
                .build()
        )
        // Automatically sign in when exactly one credential is retrieved.
        .setAutoSelectEnabled(true)
        .build()

    try {
        // Use await() from https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-play-services
        // Instead of listeners that aren't cleaned up automatically
        val result = oneTapClient.beginSignIn(signInRequest).await()

        // Now construct the IntentSenderRequest the launcher requires
        val intentSenderRequest = IntentSenderRequest.Builder(result.pendingIntent).build()
        launcher.launch(intentSenderRequest)
    } catch (e: Exception) {
        // No saved credentials found. Launch the One Tap sign-up flow, or
        // do nothing and continue presenting the signed-out UI.
        Log.d("LOG", e.message.toString())
    }
}
ianhanniballake
  • 191,609
  • 30
  • 470
  • 443
  • Note that 'return' is not allowed in the launcher scope. – ShadeToD Feb 10 '22 at 13:45
  • 1
    @ShadeToD - returns are certainly supported - you just need to fully qualify what you are returning from using `return@rememberLauncherForActivityResult`. I've corrected the code snippet. – ianhanniballake Feb 11 '22 at 04:01
  • 1
    thanks for clarifying that, do you know if its possible to handle this inside view model? Or we should always pass it to the composable ? – ShadeToD Feb 11 '22 at 08:10
  • 3
    This basically explains modern google sign in approach with all possible tricks and caveats, which google itself has failed to provide. Why does this have 0 upvotes? – Nek.12 Jul 19 '22 at 20:06