Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
d8097c9
feat(expo): Implement Google Sign-In support for Android and iOS
chriscanin Nov 11, 2025
83eea32
test(expo): Enhance mocks for expo-modules-core in test setup
chriscanin Nov 12, 2025
d2c85f0
chore(package): remove local dependency on @clerk/clerk-expo
chriscanin Nov 12, 2025
39f3a64
feat(expo): Refactor Google Sign-In to use web-based OAuth flow for A…
chriscanin Nov 21, 2025
07ab559
feat: add Clerk Google Sign-In module for Expo
chriscanin Nov 23, 2025
1808484
feat(expo): Add iOS support for Clerk Google Sign-In and enhance conf…
chriscanin Nov 24, 2025
45e8a4f
feat(expo): Implement iOS URL scheme configuration for Google Sign-In…
chriscanin Nov 25, 2025
27182a3
feat(google-signin): Add GoogleSignInActivityUnavailableException and…
chriscanin Nov 26, 2025
ddce471
feat(tests): Refactor Google Sign-In tests to use Clerk's One Tap Sig…
chriscanin Nov 26, 2025
3d79d5b
feat(google-signin): Add native Google Sign-In support for iOS and An…
chriscanin Nov 26, 2025
20069aa
fix(build.gradle): centralize dependency versions for easier updates
chriscanin Dec 5, 2025
4f01a6e
refactor(types): Update imports from '@clerk/types' to '@clerk/shared…
chriscanin Dec 8, 2025
b02e6fb
fix(tests): update nonce value in useSignInWithGoogle test to use UUI…
chriscanin Dec 8, 2025
8898517
fix(deps): correct package name for @clerk/expo and add native Google…
chriscanin Dec 9, 2025
b33d407
refactor(imports): update imports from '@clerk/clerk-react' to '@cler…
chriscanin Dec 10, 2025
cf0b9fa
feat(legacy): Add legacy module support and update nonce generation t…
chriscanin Dec 10, 2025
3443932
Resolves:
chriscanin Dec 10, 2025
ecb9aa0
refactor(types): Simplify type exports for Google authentication hook…
chriscanin Dec 10, 2025
d819e55
refactor(auth): streamline Google Sign-In logic by consolidating auth…
chriscanin Dec 10, 2025
7d1a476
refactor(expo): restructure plugin to match standard RN package pattern
chriscanin Dec 15, 2025
55d9cba
Merge branch 'main' into chris/mobile-289-expo-google-universal-sign-in
chriscanin Dec 15, 2025
b7737f2
fix(tests): Update import statement in useSignInWithGoogle test to ma…
chriscanin Dec 15, 2025
542a664
chore(dependencies): Update @expo/config-plugins, @expo/config-types,…
chriscanin Dec 15, 2025
62478be
Merge branch 'main' into chris/mobile-289-expo-google-universal-sign-in
chriscanin Dec 15, 2025
57ca39d
Merge branch 'main' into chris/mobile-289-expo-google-universal-sign-in
chriscanin Dec 16, 2025
32c626b
Merge branch 'main' into chris/mobile-289-expo-google-universal-sign-in
chriscanin Dec 16, 2025
c932cdb
chore(version): Bump @clerk/clerk-js version from 2.19.23 to 5.114.0
chriscanin Dec 16, 2025
888e0b8
chore(version): Downgrade @clerk/expo version from 2.19.26 to 2.19.10
chriscanin Dec 16, 2025
18864ba
Merge branch 'main' into chris/mobile-289-expo-google-universal-sign-in
chriscanin Dec 16, 2025
e0e99c1
fix(plugin): update package import path for @clerk/expo
chriscanin Dec 16, 2025
31f233d
refactor(google-one-tap): update module description and usage guidance
chriscanin Dec 16, 2025
29a7c9f
feat(google-one-tap): add new error code for Android activity unavail…
chriscanin Dec 16, 2025
8ab520f
refactor(google-one-tap): simplify Google Sign-In hooks for Android a…
chriscanin Dec 16, 2025
37484d3
chore(package): remove redundant package.json entries
chriscanin Dec 16, 2025
a62253e
chore(package): clean up package.json by removing unnecessary entries
chriscanin Dec 17, 2025
9bdc051
chore(package): update peerDependencies in package.json for expo
chriscanin Dec 17, 2025
85129a3
feat(google-one-tap): enhance Google client ID retrieval in useSignIn…
chriscanin Dec 17, 2025
40ad13b
fix(google-one-tap): enhance error handling in Google Sign-In
chriscanin Dec 17, 2025
db69b54
Merge branch 'main' into chris/mobile-289-expo-google-universal-sign-in
chriscanin Dec 17, 2025
2d71e61
chore(pnpm): update expo version in pnpm-lock.yaml
chriscanin Dec 17, 2025
5b20334
refactor(expo): update app.plugin.js path and clean up package.json
chriscanin Dec 17, 2025
3408d62
Merge remote-tracking branch 'origin/main' into chris/mobile-289-expo…
chriscanin Dec 17, 2025
a2eb579
Merge branch 'main' into chris/mobile-289-expo-google-universal-sign-in
chriscanin Dec 17, 2025
b0e2ffc
Merge branch 'main' into chris/mobile-289-expo-google-universal-sign-in
chriscanin Dec 17, 2025
c401182
Merge branch 'main' into chris/mobile-289-expo-google-universal-sign-in
chriscanin Dec 17, 2025
568f85a
refactor(expo): update import statement for package.json in withClerk…
chriscanin Dec 17, 2025
3514955
refactor(expo): remove unused SignUpUnsafeMetadata type from Google s…
chriscanin Dec 18, 2025
3cd75c2
Merge branch 'main' into chris/mobile-289-expo-google-universal-sign-in
chriscanin Dec 18, 2025
7f0ba45
chore(expo): add app.plugin.js entry to package.json exports
chriscanin Dec 18, 2025
b192d31
Merge branch 'main' into chris/mobile-289-expo-google-universal-sign-in
chriscanin Dec 18, 2025
a0f6f2c
resolve merge conflicts.
chriscanin Jan 5, 2026
2bb67e7
Merge branch 'main' into chris/mobile-289-expo-google-universal-sign-in
chriscanin Jan 5, 2026
ec7f753
test(expo): enhance useSignInWithGoogle tests with isClerkAPIResponse…
chriscanin Jan 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/brave-clouds-swim.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@clerk/expo': minor
---

Add native Google Sign-In support for iOS and Android using built-in native modules.
1 change: 1 addition & 0 deletions packages/expo/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
android/.gradle
64 changes: 64 additions & 0 deletions packages/expo/android/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'

group = 'com.clerk.expo'
version = '1.0.0'

// Dependency versions - centralized for easier updates
// See: https://docs.gradle.org/current/userguide/version_catalogs.html for app-level version catalogs
ext {
credentialsVersion = "1.3.0"
googleIdVersion = "1.1.1"
kotlinxCoroutinesVersion = "1.7.3"
}

def safeExtGet(prop, fallback) {
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
}

android {
namespace "expo.modules.clerk.googlesignin"

compileSdk safeExtGet("compileSdkVersion", 36)

defaultConfig {
minSdk safeExtGet("minSdkVersion", 24)
targetSdk safeExtGet("targetSdkVersion", 36)
versionCode 1
versionName "1.0.0"
}

buildTypes {
release {
minifyEnabled false
}
}

compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}

kotlinOptions {
jvmTarget = "17"
}

sourceSets {
main {
java.srcDirs = ['src/main/java']
}
}
}

dependencies {
// Expo modules core
implementation project(':expo-modules-core')

// Credential Manager for Google Sign-In with nonce support
implementation "androidx.credentials:credentials:$credentialsVersion"
implementation "androidx.credentials:credentials-play-services-auth:$credentialsVersion"
implementation "com.google.android.libraries.identity.googleid:googleid:$googleIdVersion"

// Coroutines for async operations
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlinxCoroutinesVersion"
}
2 changes: 2 additions & 0 deletions packages/expo/android/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,264 @@
package expo.modules.clerk.googlesignin

import android.content.Context
import androidx.credentials.ClearCredentialStateRequest
import androidx.credentials.CredentialManager
import androidx.credentials.CustomCredential
import androidx.credentials.GetCredentialRequest
import androidx.credentials.GetCredentialResponse
import androidx.credentials.exceptions.GetCredentialCancellationException
import androidx.credentials.exceptions.GetCredentialException
import androidx.credentials.exceptions.NoCredentialException
import com.google.android.libraries.identity.googleid.GetGoogleIdOption
import com.google.android.libraries.identity.googleid.GetSignInWithGoogleOption
import com.google.android.libraries.identity.googleid.GoogleIdTokenCredential
import com.google.android.libraries.identity.googleid.GoogleIdTokenParsingException
import expo.modules.kotlin.Promise
import expo.modules.kotlin.exception.CodedException
import expo.modules.kotlin.modules.Module
import expo.modules.kotlin.modules.ModuleDefinition
import expo.modules.kotlin.records.Field
import expo.modules.kotlin.records.Record
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch

// Configuration parameters
class ConfigureParams : Record {
@Field
val webClientId: String = ""

@Field
val hostedDomain: String? = null

@Field
val autoSelectEnabled: Boolean? = null
}

// Sign-in parameters
class SignInParams : Record {
@Field
val nonce: String? = null

@Field
val filterByAuthorizedAccounts: Boolean? = null
}

// Create account parameters
class CreateAccountParams : Record {
@Field
val nonce: String? = null
}

// Explicit sign-in parameters
class ExplicitSignInParams : Record {
@Field
val nonce: String? = null
}

// Custom exceptions
class GoogleSignInCancelledException : CodedException("SIGN_IN_CANCELLED", "User cancelled the sign-in flow", null)
class GoogleSignInNoCredentialException : CodedException("NO_SAVED_CREDENTIAL_FOUND", "No saved credential found", null)
class GoogleSignInException(message: String) : CodedException("GOOGLE_SIGN_IN_ERROR", message, null)
class GoogleSignInNotConfiguredException : CodedException("NOT_CONFIGURED", "Google Sign-In is not configured. Call configure() first.", null)
class GoogleSignInActivityUnavailableException : CodedException("E_ACTIVITY_UNAVAILABLE", "Activity is not available", null)

class ClerkGoogleSignInModule : Module() {
private var webClientId: String? = null
private var hostedDomain: String? = null
private var autoSelectEnabled: Boolean = false
private val mainScope = CoroutineScope(Dispatchers.Main)

private val context: Context
get() = requireNotNull(appContext.reactContext)

private val credentialManager: CredentialManager
get() = CredentialManager.create(context)

override fun definition() = ModuleDefinition {
Name("ClerkGoogleSignIn")

// Configure the module
Function("configure") { params: ConfigureParams ->
webClientId = params.webClientId
hostedDomain = params.hostedDomain
autoSelectEnabled = params.autoSelectEnabled ?: false
}

// Sign in - attempts automatic sign-in with saved credentials
AsyncFunction("signIn") { params: SignInParams?, promise: Promise ->
val clientId = webClientId ?: run {
promise.reject(GoogleSignInNotConfiguredException())
return@AsyncFunction
}

val activity = appContext.currentActivity ?: run {
promise.reject(GoogleSignInActivityUnavailableException())
return@AsyncFunction
}

mainScope.launch {
try {
val googleIdOption = GetGoogleIdOption.Builder()
.setFilterByAuthorizedAccounts(params?.filterByAuthorizedAccounts ?: true)
.setServerClientId(clientId)
.setAutoSelectEnabled(autoSelectEnabled)
.apply {
params?.nonce?.let { setNonce(it) }
}
.build()

val request = GetCredentialRequest.Builder()
.addCredentialOption(googleIdOption)
.build()

val result = credentialManager.getCredential(
request = request,
context = activity
)

handleSignInResult(result, promise)
} catch (e: GetCredentialCancellationException) {
promise.reject(GoogleSignInCancelledException())
} catch (e: NoCredentialException) {
promise.reject(GoogleSignInNoCredentialException())
} catch (e: GetCredentialException) {
promise.reject(GoogleSignInException(e.message ?: "Unknown error"))
} catch (e: Exception) {
promise.reject(GoogleSignInException(e.message ?: "Unknown error"))
}
}
}

// Create account - shows account creation UI
AsyncFunction("createAccount") { params: CreateAccountParams?, promise: Promise ->
val clientId = webClientId ?: run {
promise.reject(GoogleSignInNotConfiguredException())
return@AsyncFunction
}

val activity = appContext.currentActivity ?: run {
promise.reject(GoogleSignInActivityUnavailableException())
return@AsyncFunction
}

mainScope.launch {
try {
val googleIdOption = GetGoogleIdOption.Builder()
.setFilterByAuthorizedAccounts(false) // Show all accounts for creation
.setServerClientId(clientId)
.apply {
params?.nonce?.let { setNonce(it) }
}
.build()

val request = GetCredentialRequest.Builder()
.addCredentialOption(googleIdOption)
.build()

val result = credentialManager.getCredential(
request = request,
context = activity
)

handleSignInResult(result, promise)
} catch (e: GetCredentialCancellationException) {
promise.reject(GoogleSignInCancelledException())
} catch (e: NoCredentialException) {
promise.reject(GoogleSignInNoCredentialException())
} catch (e: GetCredentialException) {
promise.reject(GoogleSignInException(e.message ?: "Unknown error"))
} catch (e: Exception) {
promise.reject(GoogleSignInException(e.message ?: "Unknown error"))
}
}
}

// Explicit sign-in - uses Sign In With Google button flow
AsyncFunction("presentExplicitSignIn") { params: ExplicitSignInParams?, promise: Promise ->
val clientId = webClientId ?: run {
promise.reject(GoogleSignInNotConfiguredException())
return@AsyncFunction
}

val activity = appContext.currentActivity ?: run {
promise.reject(GoogleSignInActivityUnavailableException())
return@AsyncFunction
}

mainScope.launch {
try {
val signInWithGoogleOption = GetSignInWithGoogleOption.Builder(clientId)
.apply {
params?.nonce?.let { setNonce(it) }
hostedDomain?.let { setHostedDomainFilter(it) }
}
.build()

val request = GetCredentialRequest.Builder()
.addCredentialOption(signInWithGoogleOption)
.build()

val result = credentialManager.getCredential(
request = request,
context = activity
)

handleSignInResult(result, promise)
} catch (e: GetCredentialCancellationException) {
promise.reject(GoogleSignInCancelledException())
} catch (e: GetCredentialException) {
promise.reject(GoogleSignInException(e.message ?: "Unknown error"))
} catch (e: Exception) {
promise.reject(GoogleSignInException(e.message ?: "Unknown error"))
}
}
}

// Sign out - clears credential state
AsyncFunction("signOut") { promise: Promise ->
mainScope.launch {
try {
credentialManager.clearCredentialState(ClearCredentialStateRequest())
promise.resolve(null)
} catch (e: Exception) {
promise.reject(GoogleSignInException(e.message ?: "Failed to sign out"))
}
}
}
}

private fun handleSignInResult(result: GetCredentialResponse, promise: Promise) {
when (val credential = result.credential) {
is CustomCredential -> {
if (credential.type == GoogleIdTokenCredential.TYPE_GOOGLE_ID_TOKEN_CREDENTIAL) {
try {
val googleIdTokenCredential = GoogleIdTokenCredential.createFrom(credential.data)

promise.resolve(mapOf(
"type" to "success",
"data" to mapOf(
"idToken" to googleIdTokenCredential.idToken,
"user" to mapOf(
"id" to googleIdTokenCredential.id,
"email" to googleIdTokenCredential.id,
"name" to googleIdTokenCredential.displayName,
"givenName" to googleIdTokenCredential.givenName,
"familyName" to googleIdTokenCredential.familyName,
"photo" to googleIdTokenCredential.profilePictureUri?.toString()
)
)
))
} catch (e: GoogleIdTokenParsingException) {
promise.reject(GoogleSignInException("Failed to parse Google ID token: ${e.message}"))
}
} else {
promise.reject(GoogleSignInException("Unexpected credential type: ${credential.type}"))
}
}
else -> {
promise.reject(GoogleSignInException("Unexpected credential type"))
}
}
}
}
1 change: 1 addition & 0 deletions packages/expo/app.plugin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = require('./dist/plugin/withClerkExpo');
9 changes: 9 additions & 0 deletions packages/expo/expo-module.config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"platforms": ["android", "ios"],
"android": {
"modules": ["expo.modules.clerk.googlesignin.ClerkGoogleSignInModule"]
},
"ios": {
"modules": ["ClerkGoogleSignInModule"]
}
}
22 changes: 22 additions & 0 deletions packages/expo/ios/ClerkGoogleSignIn.podspec
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
require 'json'

package = JSON.parse(File.read(File.join(__dir__, '..', 'package.json')))

Pod::Spec.new do |s|
s.name = 'ClerkGoogleSignIn'
s.version = package['version']
s.summary = 'Native Google Sign-In module for Clerk Expo'
s.description = 'Native Google Sign-In functionality using Google Sign-In SDK with nonce support for Clerk authentication'
s.license = package['license']
s.author = package['author']
s.homepage = package['homepage']
s.platforms = { :ios => '13.4' }
s.swift_version = '5.4'
s.source = { :git => 'https://github.com/clerk/javascript.git' }
s.static_framework = true

s.dependency 'ExpoModulesCore'
s.dependency 'GoogleSignIn', '~> 9.0'

s.source_files = '*.swift'
end
Loading
Loading