Firebase Google & Phone Auth in Jetpack Compose

Firebase Google & Phone Auth in Jetpack Compose

In this article, we will explore how to implement Firebase authentication using Google and Phone Auth in Jetpack Compose and provide step-by-step guidance for developers to create a robust and secure authentication system for their android applications.

Pre-requisites

These are some basic setup tasks that you have to do by yourself and do not require any prior coding experience. If you are using Firebase for the first time, then refer to these docs.

  1. Setup Firebase.

  2. Link your project with Firebase

  3. Enable Google & Phone authentication

  4. Add following dependencies in app build.gradle:

implementation "com.google.firebase:firebase-auth-ktx:21.1.0"

5. Add permissions in AndroidManifest:

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

Output:

I had another screen for Phone number and OTP TextField. You can design according to your needs, here we will just focus on implementing the functionality of successfully signing in the user and also handling the edge cases/exceptions.

Implementation:

Google Sign In:

When we click on the Sign-In button, we need to launch an Intent using GoogleSignInClient and for getting a Client instance, we can use GoogleSignIn.getClient(…).

So, this method takes 2 params, context and GSO object, that we will create in OnClick itself.

val token = stringResource(R.string.default_web_client_id)//Will be generated after Firebase Integration
Button(
    onClick = {
        val gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
            .requestIdToken(token)
            .requestEmail()
            .build()//Creating GSO object
        //val context = LocalContext.current <- Outside of This button composition
        val googleSignInClient = GoogleSignIn.getClient(context, gso)//Get client obj
        launcher.launch(googleSignInClient.signInIntent)//Launch the intent using a launcher that we will create now.
    },
    content = {
        //Your Button Design
    }
)

Now, create a launcher.. that will act as a onActivityResult method that we used to override.

val launcher = rememberLauncherForActivityResult(
    contract = ActivityResultContracts.StartActivityForResult()
) { result ->//Here, we get the Intent result
    val task = GoogleSignIn.getSignedInAccountFromIntent(result.data)
    try {
        val account = task.getResult(ApiException::class.java)!!
        val credential = GoogleAuthProvider.getCredential(account.idToken, null)
        Firebase.auth.signInWithCredential(credential)
            .addOnCompleteListener {
                if (it.isSuccessful) {
                    //Here, do whatever you want to do after successful auth
                }else{
                    //Here, handle failure
                }
            }

    } catch (e: ApiException) {
        Log.e("TAG", "Google sign in failed", e)
    }
}

Phone Login:

For Login using a Phone number, we need two TextField. One for getting the user’s Phone number and another for entering the OTP.

Note: It is normal if the user is re-directing for Robot Verification before receiving an OTP.

val phoneNumber = rememberSaveable {
    mutableStateOf("")
}

val otp = rememberSaveable {
    mutableStateOf("")
}

val verificationID = rememberSaveable {
    mutableStateOf("")
}//Need this to get credentials

val codeSent = rememberSaveable {
    mutableStateOf(false)
}//Optional- Added just to make consistent UI

val loading = rememberSaveable {
    mutableStateOf(false)
}//Optional

val context = LocalContext.current

val mAuth: FirebaseAuth = Firebase.auth

We also need Verification callback methods..

lateinit var callbacks: PhoneAuthProvider.OnVerificationStateChangedCallbacks

Now, I will show my UI design, which is totally optional in your case.

Column {
    TextField(//import androidx.compose.material3.TextField
        enabled = !codeSent.value && !loading.value,
        value = phoneNumber.value,
        keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
        onValueChange = { if (it.length <= 10) phoneNumber.value = it },
        placeholder = { Text(text = "Enter your phone number") },
        modifier = Modifier
            .padding(16.dp)
            .fillMaxWidth(),
        supportingText = {
            Text(
                text = "${phoneNumber.value.length} / 10",
                modifier = Modifier.fillMaxWidth(),
                textAlign = TextAlign.End,
            )
        }
    )

    Spacer(modifier = Modifier.height(10.dp))

    AnimatedVisibility(
        visible = !codeSent.value,
        exit = scaleOut(
            targetScale = 0.5f,
            animationSpec = tween(durationMillis = 500, delayMillis = 100)
        ),
        enter = scaleIn(
            initialScale = 0.5f,
            animationSpec = tween(durationMillis = 500, delayMillis = 100)
        )
    ) {
        Button(
            enabled = !loading.value && !codeSent.value,
            onClick = {
                if (TextUtils.isEmpty(phoneNumber.value) || phoneNumber.value.length < 10) {
                    Toast.makeText(
                        context,
                        "Enter a valid phone number",
                        Toast.LENGTH_SHORT
                    )
                        .show()
                } else {
                    loading.value = true
                    val number = "+91${phoneNumber.value}"
                    sendVerificationCode(number, mAuth, context as Activity, callbacks)//This is the main method to send the code after verification
                }
            },
            modifier = Modifier
                .fillMaxWidth()
                .padding(16.dp)
        ) {
            Text(text = "Generate OTP", modifier = Modifier.padding(8.dp))
        }
    }
}

if (loading.value) {
    LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
}

Here’s the method for sending verification code:

private fun sendVerificationCode(
    number: String,
    auth: FirebaseAuth,
    activity: Activity,
    callbacks: PhoneAuthProvider.OnVerificationStateChangedCallbacks
) {
    val options = PhoneAuthOptions.newBuilder(auth)
        .setPhoneNumber(number)
        .setTimeout(60L, TimeUnit.SECONDS)
        .setActivity(activity)
        .setCallbacks(callbacks)
        .build()
    PhoneAuthProvider.verifyPhoneNumber(options)
}

Now, we will use callback methods to get verificationId and set other things..

callbacks = object : PhoneAuthProvider.OnVerificationStateChangedCallbacks() {
    override fun onVerificationCompleted(p0: PhoneAuthCredential) {
        Toast.makeText(context, "Verification successful..", Toast.LENGTH_SHORT).show()
        loading.value = false
    }

    override fun onVerificationFailed(p0: FirebaseException) {
        Toast.makeText(context, "Verification failed.. ${p0.message}", Toast.LENGTH_LONG)
            .show()
        loading.value = false
    }

    override fun onCodeSent(
        verificationId: String,
        p1: PhoneAuthProvider.ForceResendingToken
    ) {
        super.onCodeSent(verificationId, p1)
        verificationID.value = verificationId
        codeSent.value = true
        loading.value = false
    }
}

After receiving the OTP, now a new TextField and button will also popup…(add them in the same column with others).

AnimatedVisibility(
    visible = codeSent.value,
    // Add same animation here
) {
    Column {
        TextField(
            enabled = !loading.value,
            value = otp.value,
            keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
            onValueChange = { if (it.length <= 6) otp.value = it },
            placeholder = { Text(text = "Enter your otp") },
            modifier = Modifier
                .padding(16.dp)
                .fillMaxWidth(),
            supportingText = {
                Text(
                    text = "${otp.value.length} / 6",
                    modifier = Modifier.fillMaxWidth(),
                    textAlign = TextAlign.End,
                )
            }
        )

        Spacer(modifier = Modifier.height(10.dp))

        Button(
            enabled = !loading.value,
            onClick = {
                if (TextUtils.isEmpty(otp.value) || otp.value.length < 6) {
                    Toast.makeText(
                        context,
                        "Please enter a valid OTP",
                        Toast.LENGTH_SHORT
                    )
                        .show()
                } else {
                    loading.value = true
                    //This is the main part where we verify the OTP
                    val credential: PhoneAuthCredential =
                        PhoneAuthProvider.getCredential(
                            verificationID.value, otp.value
                        )//Get credential object
                    mAuth.signInWithCredential(credential)
                        .addOnCompleteListener(context as Activity) { task ->
                            if (task.isSuccessful) {
                                //Code after auth is successful
                            } else {
                                loading.value = false
                                if (task.exception is FirebaseAuthInvalidCredentialsException) {
                                    Toast.makeText(
                                        context,
                                        "Verification failed.." + (task.exception as FirebaseAuthInvalidCredentialsException).message,
                                        Toast.LENGTH_LONG
                                    ).show()
                                    if ((task.exception as FirebaseAuthInvalidCredentialsException).message?.contains(
                                            "expired"
                                        ) == true
                                    ) {//If code is expired then get a chance to resend the code
                                        codeSent.value = false
                                        otp.value = ""
                                    }
                                }
                            }
                        }
                }
            },
            modifier = Modifier
                .fillMaxWidth()
                .padding(16.dp)
        ) {
            Text(text = "Verify OTP", modifier = Modifier.padding(8.dp))
        }
    }
}

Conclusion:

It is actually way simpler to implement Firebase Auth in Jetpack compose than it looks in some docs.

I hope you found this helpful. If yes, then do FOLLOW ‘Sagar Malhotra’ for more Android-related content.

#androidWithSagar #android #androiddevelopment #development #compose #kotlin