Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,8 @@ android/generated

# React Native Nitro Modules
nitrogen/

# env files
.env
.env.local
!.env.example
6 changes: 3 additions & 3 deletions android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ android {

dependencies {
implementation "com.facebook.react:react-android"
implementation "androidx.credentials:credentials:1.7.0-alpha01"
implementation "androidx.credentials:credentials-play-services-auth:17.0-alpha01"
implementation "com.google.android.libraries.identity.googleid:googleid:1.2.0"
implementation "androidx.credentials:credentials:1.5.0"
implementation "androidx.credentials:credentials-play-services-auth:1.5.0"
implementation "com.google.android.libraries.identity.googleid:googleid:1.1.1"
}
Original file line number Diff line number Diff line change
@@ -1,34 +1,185 @@
package com.thoughtbot.reactnativesocialauth

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.facebook.react.bridge.Arguments
import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReadableMap
import com.google.android.libraries.identity.googleid.GetGoogleIdOption
import com.google.android.libraries.identity.googleid.GoogleIdTokenCredential
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch

class GoogleSignInModule(reactContext: ReactApplicationContext) :
NativeGoogleSignInSpec(reactContext) {

private val credentialManager: CredentialManager =
CredentialManager.create(reactContext)

private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())

private var webClientId: String? = null
private var nonce: String? = null
private var autoSelect: Boolean = false
private var currentUser: GoogleIdTokenCredential? = null

override fun configure(config: ReadableMap) {
// TODO: Phase 3 — Store configuration for Credential Manager
webClientId = config.getString("webClientId")
nonce = if (config.hasKey("nonce")) config.getString("nonce") else null
autoSelect = if (config.hasKey("autoSelect")) config.getBoolean("autoSelect") else false
}

override fun signIn(promise: Promise) {
promise.reject("ERR_NOT_IMPLEMENTED", "Google Sign-In is not yet implemented on Android")
val clientId = webClientId
if (clientId == null) {
promise.reject("ERR_NOT_CONFIGURED", "GoogleSignIn.configure() must be called before signIn()")
return
}

val activity = currentActivity
if (activity == null) {
promise.reject("ERR_NO_ACTIVITY", "No current activity available")
return
}

scope.launch {
try {
val result = getCredentialWithAutoSignIn(clientId, activity)
handleSignInResult(result, promise)
} catch (e: GetCredentialCancellationException) {
promise.reject("SIGN_IN_CANCELLED", "User cancelled the sign-in flow", e)
} catch (e: NoCredentialException) {
promise.reject("NO_CREDENTIALS", "No credentials available on this device", e)
} catch (e: GetCredentialException) {
promise.reject("SIGN_IN_FAILED", e.message, e)
}
}
}

private suspend fun getCredentialWithAutoSignIn(
clientId: String,
activity: android.app.Activity
): GetCredentialResponse {
val autoSignInOption = GetGoogleIdOption.Builder()
.setFilterByAuthorizedAccounts(true)
.setServerClientId(clientId)
.setAutoSelectEnabled(true)
.apply { nonce?.let { setNonce(it) } }
.build()

val autoRequest = GetCredentialRequest.Builder()
.addCredentialOption(autoSignInOption)
.build()

return try {
credentialManager.getCredential(activity, autoRequest)
} catch (e: NoCredentialException) {
val fallbackOption = GetGoogleIdOption.Builder()
.setFilterByAuthorizedAccounts(false)
.setServerClientId(clientId)
.setAutoSelectEnabled(false)
.apply { nonce?.let { setNonce(it) } }
.build()

val fallbackRequest = GetCredentialRequest.Builder()
.addCredentialOption(fallbackOption)
.build()

credentialManager.getCredential(activity, fallbackRequest)
}
}

private fun handleSignInResult(result: GetCredentialResponse, promise: Promise) {
val credential = result.credential

if (credential !is CustomCredential ||
credential.type != GoogleIdTokenCredential.TYPE_GOOGLE_ID_TOKEN_CREDENTIAL) {
promise.reject("SIGN_IN_FAILED", "Unexpected credential type: ${credential.type}")
return
}

val googleIdTokenCredential = GoogleIdTokenCredential.createFrom(credential.data)
currentUser = googleIdTokenCredential

val userMap = Arguments.createMap().apply {
putString("id", googleIdTokenCredential.id)
putString("email", googleIdTokenCredential.id)
putString("displayName", googleIdTokenCredential.displayName)
putString("givenName", googleIdTokenCredential.givenName)
putString("familyName", googleIdTokenCredential.familyName)
putString("photoUrl", googleIdTokenCredential.profilePictureUri?.toString())
}

val resultMap = Arguments.createMap().apply {
putString("idToken", googleIdTokenCredential.idToken)
putNull("accessToken")
putNull("serverAuthCode")
putMap("user", userMap)
}

promise.resolve(resultMap)
}

override fun signOut(promise: Promise) {
promise.reject("ERR_NOT_IMPLEMENTED", "Google Sign-In is not yet implemented on Android")
scope.launch {
try {
credentialManager.clearCredentialState(ClearCredentialStateRequest())
currentUser = null
promise.resolve(null)
} catch (e: Exception) {
promise.reject("SIGN_OUT_FAILED", e.message, e)
}
}
}

override fun getCurrentUser(promise: Promise) {
promise.reject("ERR_NOT_IMPLEMENTED", "Google Sign-In is not yet implemented on Android")
val user = currentUser
if (user == null) {
promise.resolve(null)
return
}

val userMap = Arguments.createMap().apply {
putString("id", user.id)
putString("email", user.id)
putString("displayName", user.displayName)
putString("givenName", user.givenName)
putString("familyName", user.familyName)
putString("photoUrl", user.profilePictureUri?.toString())
}

promise.resolve(userMap)
}

override fun revokeAccess(promise: Promise) {
promise.reject("ERR_NOT_IMPLEMENTED", "Google Sign-In is not yet implemented on Android")
scope.launch {
try {
credentialManager.clearCredentialState(ClearCredentialStateRequest())
currentUser = null
promise.resolve(null)
} catch (e: Exception) {
promise.reject("REVOKE_FAILED", e.message, e)
}
}
}

override fun isSignedIn(): Boolean {
return false
return currentUser != null
}

override fun invalidate() {
scope.cancel()
super.invalidate()
}

companion object {
Expand Down
3 changes: 3 additions & 0 deletions example/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Google OAuth Web Client ID from Google Cloud Console
# https://console.cloud.google.com/apis/credentials
EXPO_PUBLIC_GOOGLE_WEB_CLIENT_ID=your-web-client-id-here.apps.googleusercontent.com
1 change: 1 addition & 0 deletions example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"react": "19.2.0",
"react-dom": "19.2.0",
"react-native": "0.83.4",
"react-native-svg": "^15.15.5",
"react-native-web": "~0.21.0"
},
"private": true,
Expand Down
Loading
Loading