Blog Infos
Author
Published
Topics
, , , , ,
Published
Google Sign-In Kotlin Multiplatform

 

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!

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 linkPro 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

This article was previously published on proanadroiddev.com

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

Jobs

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
It’s one of the common UX across apps to provide swipe to dismiss so…
READ MORE
blog
Hi, today I come to you with a quick tip on how to update…
READ MORE
blog
Automation is a key point of Software Testing once it make possible to reproduce…
READ MORE
blog
Drag and Drop reordering in Recyclerview can be achieved with ItemTouchHelper (checkout implementation reference).…
READ MORE

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.

Menu