Search code examples
androidkotlingoogle-signingoogle-one-tap

One Tap Sign in - Activity Result with Jetpack Compose


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

Solution

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