[SOLVED] One Tap Sign in – Activity Result with Jetpack Compose

Issue

This Content is from Stack Overflow. Question asked by ibbs

I am unable to get Google’s one tap sign in implementation to work. There is documentation along with firebase for backend auth don’t use compose so I’ve tried to make this work along with other stackoverflow answers.

Some things have been cut out from the code for simplicity reasons.

MainActivity.kt


// Tags
private val TAG_SIGNIN = "Google Sign In"
private val TAG_FIREBASE = "Firebase backend auth"
private val TAG_GOOGLE = "Google Sign In Results"

@AndroidEntryPoint
class MainActivity2 : ComponentActivity() {

    // Google Sign in
    private lateinit var oneTapClient: SignInClient
    private lateinit var signInRequest: BeginSignInRequest
    private lateinit var signUpRequest: BeginSignInRequest

    // Firebase
    private lateinit var auth: FirebaseAuth

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // Firebase Auth Instantiate
        auth = Firebase.auth

        // Google Onetap Sign in
        oneTapClient = Identity.getSignInClient(this)
        signInRequest = BeginSignInRequest.builder()
            .setGoogleIdTokenRequestOptions(
                BeginSignInRequest.GoogleIdTokenRequestOptions.builder()
                    .setSupported(true)
                    .setServerClientId(getString(R.string.firebase_client_id))
                    .setFilterByAuthorizedAccounts(true)
                    .build())
            .setAutoSelectEnabled(true)
            .build()
        signUpRequest = BeginSignInRequest.builder()
            .setGoogleIdTokenRequestOptions(
                BeginSignInRequest.GoogleIdTokenRequestOptions.builder()
                    .setSupported(true)
                    .setServerClientId(getString(R.string.firebase_client_id))
                    .setFilterByAuthorizedAccounts(false)
                    .build())
            .build()

        setContent {
         
            val authViewModel: LoginViewModel = viewModel()
            val authState = authViewModel.viewstate.collectAsState().value


            LoginScreen(
                activity,
                oneTapClient,
                signInRequest,
                signUpRequest,
                authViewModel,
                auth)
            }
        }
    }
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun LoginScreen(
    activity: Activity,
    oneTapClient: SignInClient,
    signInRequest: BeginSignInRequest,
    signUpRequest: BeginSignInRequest,
    loginViewModel: LoginViewModel,
    auth: FirebaseAuth,
) {

    val launcher = rememberLauncherForActivityResult(
        ActivityResultContracts.StartIntentSenderForResult()
    ) { result ->
        if (result.resultCode == RESULT_OK) {
            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.

                when {
                    idToken != null -> {
                        // Got an ID token from Google. Use it to authenticate
                        // with Firebase.
                        val firebaseCredential = getCredential(idToken, null)
                        auth.signInWithCredential(firebaseCredential)
                            .addOnCompleteListener(activity) { task ->
                                if (task.isSuccessful) {
                                    // Sign in success, update UI with the signed-in user's information
                                    Log.d(TAG_FIREBASE, "signInWithCredential:success")
                                    val user = auth.currentUser
                                } else {
                                    // If sign in fails, display a message to the user.
                                    Log.w(TAG_FIREBASE,
                                        "signInWithCredential:failure",
                                        task. Exception)
                                }
                            }
                    }
                    else -> {
                        // Shouldn't happen.
                        Log.d(TAG_GOOGLE, "No ID token!")
                    }
                }
                Log.d("LOG", idToken)
            } else {
                Log.d("LOG", "Null Token")
            }
        } else {
            Log.e("Response", "${result.resultCode}")
        }
    }

    Material3AppTheme() {
        Scaffold { padding ->
            padding
            Column(
                modifier = Modifier.fillMaxSize(),
                verticalArrangement = Arrangement.Center,
                horizontalAlignment = Alignment.CenterHorizontally
            ) {
                val scope = rememberCoroutineScope()
                Surface(
                    onClick = {
                        scope.launch {
                            loginViewModel.signIn(activity, oneTapClient, signInRequest, signUpRequest, launcher)
                        }
                    },
                    color = MaterialTheme.colorScheme.onPrimary,
                    shadowElevation = 0.dp,
                    shape = RoundedCornerShape(5.dp),
                    border = BorderStroke(width = 1.dp, color = MaterialTheme.colorScheme.primaryContainer),
                ) {
                    Row(
                        modifier = Modifier
                            .padding(
                                start = 12.dp,
                                end = 16.dp,
                            ),
                        verticalAlignment = Alignment.CenterVertically,
                        horizontalArrangement = Arrangement.Center,
                    ) {

                        Icon(
                            painter = painterResource(id = R.drawable.ic_google_logo),
                            contentDescription = "SignInButton",
                            tint = androidx.compose.ui.graphics.Color.Unspecified,
                        )

                        Text(text = "Sign in with Google")
                    }
                }
            }
        }
    }
}

LoginViewModel.kt


// TAGS
private val TAG_SIGNIN = "GoogleSignIn"
private val TAG_SIGNUP = "GoogleSignUp"

@HiltViewModel
class LoginViewModel @Inject constructor(
    private val repositoryImpl: LoginRepositoryImpl,
    private val dataStore: DataStore<Preferences>,
) : ViewModel() {


//      Google One tap Sign in
        oneTapClient.beginSignIn(signInRequest)
            .addOnSuccessListener(activity) { result ->
                try {
                    val intentSenderRequest = IntentSenderRequest.Builder(result.pendingIntent.intentSender).build()
                    launcher.launch(intentSenderRequest)
                    Log.d(TAG_SIGNIN, "Started One Tap Sign In")
                } catch (e: IntentSender.SendIntentException) {
                    Log.e(TAG_SIGNIN, "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.

                e.localizedMessage?.let { Log.d(TAG_SIGNIN, "$it") }

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

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

            }
    }

}

The button call triggers the launcher. However, the logs show that both the sign in request and signup request fall back are returning null. I am not sure what to do. I’ve tried multiple stackoverflow answers to see if they could be the solution, but none worked, e.g.
One Tap Sign in – Activity Result with Jetpack Compose.



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


This Question was asked in StackOverflow by Hassa and Answered by ianhanniballake It is licensed under the terms of CC BY-SA 2.5. - CC BY-SA 3.0. - CC BY-SA 4.0.

people found this article helpful. What about you?