Integrating Google Sign-In into Kotlin Multiplatform

Read on:

Medium

In this blog post I will share with you how to implement Google Sign-In in Kotlin Multiplatform. As we go step by step, we will start simple, and we will end with an awesome Google Sign-In for both Android and iOS platform!

Google Sign-In Kotlin Multiplatform

Before we dive into the coding adventure, let’s understand that Google Sign In is a mix of UI and data layers. To illustrate, consider signing out; you can do it from the data layer or repository (but not necessarily), but signing in is strictly a UI task. Why? Well, we display Google Accounts for one-tap sign-ins, making it a UI-centric task. So our code needs to be easy to use, manageable, and adaptable to both UI and data layers. If you want to jump to the code immediately, here is the pull request showing how I implemented it in the FindTravelNow app. However, I strongly recommend reading it to understand the logic behind the implementation.

First Step — Creating core class and functions

Firstly, you need to set up OAuth 2.0 in Google Cloud Platform Console. For steps you can follow this link. Pro Easy Tip: If you use Firebase and enable Google Sign-In authentication in Firebase it will automatically generate OAuth client IDs for each platform, and one will be Web Client ID which will be needed for identifying signed-in users in backend server.

Common (commonMain sourceSet)

We will create one data class for holding authenticated Google User properties, and the most important one will be idToken field which will be used to authenticate user in backend side.

data class GoogleUser(
val idToken: String,
val displayName: String = "",
val profilePicUrl: String? = null,
)

And another data class for holding required credential parameters.

data class GoogleAuthCredentials(val serverId: String) //Web client ID

Then we will create two interfaces, one will contain UI layer functionalities (a.k.a Sign-In ), and another will be related to Data layer.

interface GoogleAuthUiProvider {

/**
* Opens Sign In with Google UI,
* @return returns GoogleUser
*/
suspend fun signIn(): GoogleUser?
}

To ensure that the GoogleAuthUIProvider implementation is accessible only on the UI side and to leverage Compose Multiplatform for the UI, we will use the Composable annotation for returning GoogleAuthUIProvider in the second interface.

interface GoogleAuthProvider {
@Composable
fun getUiProvider(): GoogleAuthUiProvider

suspend fun signOut()
}

Next step is to create platform specific implementation of these interfaces.

Android (androidMain sourceSet)

In build.gradle.kts, we need to add the following Google Sign-In dependencies for the androidMain dependencies.

#Google Sign In
implementation("androidx.credentials:credentials:1.3.0-alpha01")
implementation("androidx.credentials:credentials-play-services-auth:1.3.0-alpha01")
implementation("com.google.android.libraries.identity.googleid:googleid:1.1.0")

GoogleAuthUiProvider implementation ->

internal class GoogleAuthUiProviderImpl(
private val activityContext: Context,
private val credentialManager: CredentialManager,
private val credentials: GoogleAuthCredentials,
) :
GoogleAuthUiProvider {
override suspend fun signIn(): GoogleUser? {
return try {
val credential = credentialManager.getCredential(
context = activityContext,
request = getCredentialRequest()
).credential
getGoogleUserFromCredential(credential)
} catch (e: GetCredentialException) {
AppLogger.e("GoogleAuthUiProvider error: ${e.message}")
null
} catch (e: NullPointerException) {
null
}
}

private fun getGoogleUserFromCredential(credential: Credential): GoogleUser? {
return when {
credential is CustomCredential && credential.type == GoogleIdTokenCredential.TYPE_GOOGLE_ID_TOKEN_CREDENTIAL -> {
try {
val googleIdTokenCredential =
GoogleIdTokenCredential.createFrom(credential.data)
GoogleUser(
idToken = googleIdTokenCredential.idToken,
displayName = googleIdTokenCredential.displayName ?: "",
profilePicUrl = googleIdTokenCredential.profilePictureUri?.toString()
)
} catch (e: GoogleIdTokenParsingException) {
AppLogger.e("GoogleAuthUiProvider Received an invalid google id token response: ${e.message}")
null
}
}

else -> null
}
}

private fun getCredentialRequest(): GetCredentialRequest {
return GetCredentialRequest.Builder()
.addCredentialOption(getGoogleIdOption(serverClientId = credentials.serverId))
.build()
}

private fun getGoogleIdOption(serverClientId: String): GetGoogleIdOption {
return GetGoogleIdOption.Builder()
.setFilterByAuthorizedAccounts(false)
.setAutoSelectEnabled(true)
.setServerClientId(serverClientId)
.build()
}
}

GoogleAuthProvider Implementation ->

internal class GoogleAuthProviderImpl(
private val credentials: GoogleAuthCredentials,
private val credentialManager: CredentialManager,
) : GoogleAuthProvider {

@Composable
override fun getUiProvider(): GoogleAuthUiProvider {
val activityContext = LocalContext.current
return GoogleAuthUiProviderImpl(
activityContext = activityContext,
credentialManager = credentialManager,
credentials = credentials
)
}

override suspend fun signOut() {
credentialManager.clearCredentialState(ClearCredentialStateRequest())
}
}

iOS (iosMain sourceSet)

In iOS, you also need to add Google Sign-In dependencies. If you use CocoaPods, you can add them as shown below, or you can simply add the library using Swift Package Manager from Xcode.

pod("GoogleSignIn")

And add client and server IDs to the Info.plist file.

<key>GIDServerClientID</key>
<string>YOUR_SERVER_CLIENT_ID</string>

<key>GIDClientID</key>
<string>YOUR_IOS_CLIENT_ID</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>YOUR_DOT_REVERSED_IOS_CLIENT_ID</string>
</array>
</dict>
</array>

GoogleAuthUiProvider implementation ->

internal class GoogleAuthUiProviderImpl : GoogleAuthUiProvider {
@OptIn(ExperimentalForeignApi::class)
override suspend fun signIn(): GoogleUser? = suspendCoroutine { continutation ->

val rootViewController =
UIApplication.sharedApplication.keyWindow?.rootViewController

if (rootViewController == null) continutation.resume(null)
else {
GIDSignIn.sharedInstance
.signInWithPresentingViewController(rootViewController) { gidSignInResult, nsError ->
nsError?.let { println("Error While signing: $nsError") }
val idToken = gidSignInResult?.user?.idToken?.tokenString
val profile = gidSignInResult?.user?.profile
if (idToken != null) {
val googleUser = GoogleUser(
idToken = idToken,
displayName = profile?.name ?: "",
profilePicUrl = profile?.imageURLWithDimension(320u)?.absoluteString
)
continutation.resume(googleUser)
} else continutation.resume(null)
}

}
}

}

GoogleAuthProvider implementation ->

internal class GoogleAuthProviderImpl :
GoogleAuthProvider {

@Composable
override fun getUiProvider(): GoogleAuthUiProvider = GoogleAuthUiProviderImpl()

@OptIn(ExperimentalForeignApi::class)
override suspend fun signOut() {
GIDSignIn.sharedInstance.signOut()
}


}

You only need the code below to implement application delegate function calls on the Swift side.

class AppDelegate: NSObject, UIApplicationDelegate {

func application(
_ app: UIApplication,
open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]
) -> Bool {
var handled: Bool

handled = GIDSignIn.sharedInstance.handle(url)
if handled {
return true
}

// Handle other custom URL types.

// If not handled by this app, return false.
return false
}


}

@main
struct iOSApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var delegate

var body: some Scene {
WindowGroup {
ContentView().onOpenURL(perform: { url in
GIDSignIn.sharedInstance.handle(url)
})
}
}
}

Finally, we bring all implementation classes together using either just ‘expect/actual’ or you can use it with the Koin DI framework, as I did in FindTravelNow. I’ve written more detailed blog post about the usage of Koin in Kotlin Multiplatform that you can check out: https://medium.com/proandroiddev/achieving-platform-specific-implementations-with-koin-in-kmm-5cb029ba4f3b.

Second Step — Creating GoogleButtonUiContainer.

Up to this point, our code is actually ready to use. However, we can add a little touch to make our lives easier, allowing us to use this button container across any project, handling all the heavy lifting for us. For each project, we can customize the button however we want.

interface GoogleButtonUiContainerScope {
fun onClick()
}

@Composable
fun GoogleButtonUiContainer(
modifier: Modifier = Modifier,
onGoogleSignInResult: (GoogleUser?) -> Unit,
content: @Composable GoogleButtonUiContainerScope.() -> Unit,
) {
val googleAuthProvider = koinInject<GoogleAuthProvider>()
val googleAuthUiProvider = googleAuthProvider.getUiProvider()
val coroutineScope = rememberCoroutineScope()
val uiContainerScope = remember {
object : GoogleButtonUiContainerScope {
override fun onClick() {
coroutineScope.launch {
val googleUser = googleAuthUiProvider.signIn()
onGoogleSignInResult(googleUser)
}
}
}
}
Surface(
modifier = modifier,
content = { uiContainerScope.content() }
)

}

Then we simply delegate our button or view click to this container’s click, which will perform Google One Tap Sign-In and notify our screen about the result (our GoogleUser object). Using the ID token, we can send it to our backend server to check the authentication of the user. Finally, this is how simple it is to use it in your views.

GoogleButtonUiContainer(onGoogleSignInResult = { googleUser ->
val idToken=googleUser.idToken // Send this idToken to your backend to verify
signedInUser=googleUser
}) {
Button(
onClick = { this.onClick() }
) {
Text("Sign-In with Google")
}
}

You can check out full source code in this PR changes: https://github.com/mirzemehdi/FindTravelNow-KMM/pull/7


Integrating Google Sign-In into Kotlin Multiplatform was originally published in ProAndroidDev on Medium, where people are continuing the conversation by highlighting and responding to this story.

[Read on Medium]