How to implement Google Sign-In with Compose Multiplatform

630 Views Asked by At

I'm begginer in Compose Multiplatform and I'm trying to implement Google Sing In (whose objective is to get an GoogleSignInAccountobject, to retrieve the serverAuthCode value) with Compose Multiplatform.

Here is my UI code :

@Composable
fun MainActivity(colorScheme: ColorScheme, viewModel: MainActivityViewModel) {

    val context = getContext()
    val launcherManager = getLauncherManager()


    Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {

        Row(
            verticalAlignment = Alignment.CenterVertically,
            horizontalArrangement = Arrangement.Center,
            modifier = Modifier
                .clickable(
                    onClick = {
                        viewModel.viewModelScope.launch {
                            launcherManager?.let {
                                AndroidGoogleSignInHandler(context, it.getActivityForResult {

                                }).signIn()
                            }
                        }

                    }
                )
                .border(width = 1.dp, color = Color.LightGray)
                .padding(
                    start = 12.dp,
                    end = 16.dp,
                    top = 12.dp,
                    bottom = 12.dp
                )
                .fillMaxWidth()
                .height(40.dp)

        ) {
            /*
            Icon(
                painter = painterResource(id = R.drawable.ic_google_logo),
                contentDescription = "Google Login",
                tint = Color.Unspecified
            )

             */
            Spacer(modifier = Modifier.width(8.dp))
            Text(
                text = "Sign in With Google"
            )

            Spacer(modifier = Modifier.width(16.dp))
        }
    }
}

The method getLauncherManager() is an expect method able to return (or not because of platform incompatibility) the function registerForActivityResultand looks like this with AndroidGoogleSignInHandler and ActivityResultLauncherManager :

@Composable
expect fun getLauncherManager(): ActivityResultLauncherManager?

expect class AndroidGoogleSignInHandler(activity: Any?, signInLauncher: Any?) : GoogleSignInHandler {
    override fun signIn()
    override fun signOut()
}

expect class ActivityResultLauncherManager(activity: Any?) {
    fun getActivityForResult(callback: (Boolean) -> Unit): Any?
}

You can see that I'm using "Any" eveywhere because iOS doesn't have ActivityResultLauncher<Intent> type for example

Here are implementation of actual functions inside androidMain :

actual interface GoogleSignInHandler {
    // Add other methods as needed
    actual fun signIn()
    actual fun signOut()
}

actual class AndroidGoogleSignInHandler actual constructor(val activity: Any?, val signInLauncher: Any?) : GoogleSignInHandler {
    private var googleSignInClient: GoogleSignInClient

    init {
        // Initialize the GoogleSignInClient
        val gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
            .requestEmail()
            .build()
        googleSignInClient = GoogleSignIn.getClient(activity as AppCompatActivity, gso)
    }

    actual override fun signIn() {
        if (activity is Context) {
            // Start the sign-in process
            val signInIntent = googleSignInClient.signInIntent
            (signInLauncher as ActivityResultLauncher<Intent>).launch(signInIntent)
        } else {
            // Handle invalid activity parameter
        }
    }

    actual override fun signOut() {
        // Sign out from Google account
        googleSignInClient.signOut()
            .addOnCompleteListener { /* Handle sign out completion */ }
    }
}

@Composable
actual fun getContext(): Any? {
    return LocalContext.current
}

actual class ActivityResultLauncherManager actual constructor(val activity: Any?) {
    actual fun getActivityForResult(callback: (Boolean) -> Unit): Any? {
        return (activity as? AppCompatActivity)!!.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
                val signInResult = GoogleSignIn.getSignedInAccountFromIntent(result.data)
                val isSuccess = signInResult.isSuccessful

                callback(isSuccess)
            }
    }
}

@Composable
actual fun getLauncherManager(): ActivityResultLauncherManager? {
    val context = LocalContext.current as? AppCompatActivity
    return remember {
        ActivityResultLauncherManager(context!!)
    }

You can see in my code that I have a callback callback: (Boolean) -> Unitthat had in parameter the success of the request (Boolean), but as I said in the top of the post, the goal will be to get the serverAuthCode value so I will replace "Boolean" with "String" if we find the solution ;)

So, in my UI, when I'm trying to call AndroidGoogleSignInHandler(...).signIn(), I'm getting this error :

java.lang.IllegalStateException: LifecycleOwner xxx.xxx.xxx.MainActivity@7dabaee is attempting to register while current state is RESUMED. LifecycleOwners must call register before they are STARTED.

So I think that I'm implementing registerForActivityResult too lately, but I'm don't know to deal with that because it's done by expect and actual keywords...

Is there a better way to implement GoogleSinIn with Compose Multiplatform ? I searched everywhere and I can't find a solution, except with KMM Jetpack Compose, so without Compose Multiplatform

One solution is to use a WebView, with a login page and this Google login button, then no problem ! But I don't want to deal with that if I can implement natively for both Android AND iOS. My code only deals with Android but if you have the solution for that...

1

There are 1 best solutions below

6
On

You're trying to convert your android code to be common.

But common code doesn't need to know how each platform does authentication, all it needs is a callback which should be called when button is pressed.

So you can move your platform implementation to a single composable. it can look something like this:

@Composable
actual fun rememberOnGoogleAuthAction(onTokenReceived: (String) -> Unit): () -> Unit {
    val launcher = rememberLauncherForActivityResult(
        ActivityResultContracts.StartActivityForResult(),
        onResult = { result ->
            val idToken = GoogleSignIn.getSignedInAccountFromIntent(result.data)
                .getResult(ApiException::class.java)
                .idToken!!
            onTokenReceived(idToken)
        }
    )
    val context = LocalContext.current
    val client = remember {
        GoogleSignIn.getClient(
            context,
            GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
                .requestEmail()
                .requestIdToken("clientId")
                .requestProfile()
                .build()
        )
    }
    val coroutineScope = rememberCoroutineScope()
    return remember {
        {
            coroutineScope
                .launch {
                    client.signOut().await()
                    launcher.launch(client.signInIntent)
                }
        }
    }
}

Then in your common code call it like this:

.clickable(
    onClick = rememberOnGoogleAuthAction(viewModel::onTokenReceived)
)