Skip to content

Commit 751fcc9

Browse files
committed
feat: Email provider integration
1 parent b1d69e7 commit 751fcc9

File tree

3 files changed

+899
-171
lines changed

3 files changed

+899
-171
lines changed

auth/src/main/java/com/firebase/ui/auth/compose/configuration/auth_provider/AuthProvider.kt

Lines changed: 131 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import android.util.Log
2020
import androidx.compose.ui.graphics.Color
2121
import androidx.datastore.preferences.core.stringPreferencesKey
2222
import com.firebase.ui.auth.R
23+
import com.firebase.ui.auth.compose.AuthException
2324
import com.firebase.ui.auth.compose.configuration.AuthUIConfiguration
2425
import com.firebase.ui.auth.compose.configuration.AuthUIConfigurationDsl
2526
import com.firebase.ui.auth.compose.configuration.PasswordRule
@@ -54,11 +55,11 @@ class AuthProvidersBuilder {
5455
/**
5556
* Enum class to represent all possible providers.
5657
*/
57-
internal enum class Provider(val id: String) {
58-
GOOGLE(GoogleAuthProvider.PROVIDER_ID),
59-
FACEBOOK(FacebookAuthProvider.PROVIDER_ID),
60-
TWITTER(TwitterAuthProvider.PROVIDER_ID),
61-
GITHUB(GithubAuthProvider.PROVIDER_ID),
58+
internal enum class Provider(val id: String, val isSocialProvider: Boolean = false) {
59+
GOOGLE(GoogleAuthProvider.PROVIDER_ID, isSocialProvider = true),
60+
FACEBOOK(FacebookAuthProvider.PROVIDER_ID, isSocialProvider = true),
61+
TWITTER(TwitterAuthProvider.PROVIDER_ID, isSocialProvider = true),
62+
GITHUB(GithubAuthProvider.PROVIDER_ID, isSocialProvider = true),
6263
EMAIL(EmailAuthProvider.PROVIDER_ID),
6364
PHONE(PhoneAuthProvider.PROVIDER_ID),
6465
ANONYMOUS("anonymous"),
@@ -221,6 +222,131 @@ abstract class AuthProvider(open val providerId: String) {
221222
}
222223
}
223224

225+
/**
226+
* Handles cross-device email link validation.
227+
*
228+
* This method validates email links that are opened on a different device
229+
* from where they were sent. It performs security checks and throws appropriate
230+
* exceptions if the link cannot be used.
231+
*
232+
* @param auth FirebaseAuth instance for validation
233+
* @param sessionIdFromLink Session ID extracted from the email link
234+
* @param anonymousUserIdFromLink Anonymous user ID from the link (if present)
235+
* @param isEmailLinkForceSameDeviceEnabled Whether same-device is enforced
236+
* @param oobCode The action code from the email link
237+
* @param providerIdFromLink Provider ID from the link (for linking flows)
238+
*
239+
* @throws com.firebase.ui.auth.compose.AuthException.InvalidEmailLinkException if session ID is missing
240+
* @throws com.firebase.ui.auth.compose.AuthException.EmailLinkWrongDeviceException if same-device is required
241+
* @throws com.firebase.ui.auth.compose.AuthException.EmailLinkCrossDeviceLinkingException if provider linking is attempted
242+
* @throws com.firebase.ui.auth.compose.AuthException.EmailLinkPromptForEmailException if email input is required
243+
*/
244+
internal suspend fun handleCrossDeviceEmailLink(
245+
auth: FirebaseAuth,
246+
sessionIdFromLink: String?,
247+
anonymousUserIdFromLink: String?,
248+
isEmailLinkForceSameDeviceEnabled: Boolean,
249+
oobCode: String,
250+
providerIdFromLink: String?
251+
) {
252+
// Session ID must always be present in the link
253+
if (sessionIdFromLink.isNullOrEmpty()) {
254+
throw AuthException.InvalidEmailLinkException()
255+
}
256+
257+
// These scenarios require same-device flow
258+
if (isEmailLinkForceSameDeviceEnabled || !anonymousUserIdFromLink.isNullOrEmpty()) {
259+
throw AuthException.EmailLinkWrongDeviceException()
260+
}
261+
262+
// Validate the action code
263+
auth.checkActionCode(oobCode).await()
264+
265+
// If there's a provider ID, this is a linking flow which can't be done cross-device
266+
if (!providerIdFromLink.isNullOrEmpty()) {
267+
throw AuthException.EmailLinkCrossDeviceLinkingException()
268+
}
269+
270+
// Link is valid but we need the user to provide their email
271+
throw AuthException.EmailLinkPromptForEmailException()
272+
}
273+
274+
/**
275+
* Handles email link sign-in with social credential linking.
276+
*
277+
* This method signs in the user with an email link credential and then links
278+
* a stored social provider credential (e.g., Google, Facebook). It handles both
279+
* anonymous upgrade flows (with safe link) and normal linking flows.
280+
*
281+
* @param context Android context for creating scratch auth instance
282+
* @param config Auth configuration
283+
* @param auth FirebaseAuth instance
284+
* @param emailLinkCredential The email link credential to sign in with
285+
* @param storedCredentialForLink The social credential to link after sign-in
286+
* @param updateAuthState Callback to update auth state
287+
*
288+
* @return AuthResult from the linking operation
289+
*/
290+
internal suspend fun handleEmailLinkWithSocialLinking(
291+
context: Context,
292+
config: AuthUIConfiguration,
293+
auth: FirebaseAuth,
294+
emailLinkCredential: com.google.firebase.auth.AuthCredential,
295+
storedCredentialForLink: com.google.firebase.auth.AuthCredential,
296+
updateAuthState: (com.firebase.ui.auth.compose.AuthState) -> Unit
297+
): com.google.firebase.auth.AuthResult {
298+
return if (canUpgradeAnonymous(config, auth)) {
299+
// Anonymous upgrade: Use safe link pattern with scratch auth
300+
val appExplicitlyForValidation = com.google.firebase.FirebaseApp.initializeApp(
301+
context,
302+
auth.app.options,
303+
"FUIAuthScratchApp_${System.currentTimeMillis()}"
304+
)
305+
val authExplicitlyForValidation = FirebaseAuth
306+
.getInstance(appExplicitlyForValidation)
307+
308+
// Safe link: Validate that both credentials can be linked
309+
val emailResult = authExplicitlyForValidation
310+
.signInWithCredential(emailLinkCredential).await()
311+
312+
val linkResult = emailResult.user
313+
?.linkWithCredential(storedCredentialForLink)?.await()
314+
315+
// If safe link succeeds, emit merge conflict for UI to handle
316+
if (linkResult?.user != null) {
317+
updateAuthState(com.firebase.ui.auth.compose.AuthState.MergeConflict(storedCredentialForLink))
318+
}
319+
320+
// Return the link result (will be non-null if successful)
321+
linkResult!!
322+
} else {
323+
// Non-upgrade: Sign in with email link, then link social credential
324+
val emailLinkResult = auth.signInWithCredential(emailLinkCredential).await()
325+
326+
// Link the social credential
327+
val linkResult = emailLinkResult.user
328+
?.linkWithCredential(storedCredentialForLink)?.await()
329+
330+
// Merge profile from the linked social credential
331+
linkResult?.user?.let { user ->
332+
mergeProfile(auth, user.displayName, user.photoUrl)
333+
}
334+
335+
// Update to success state
336+
if (linkResult?.user != null) {
337+
updateAuthState(
338+
com.firebase.ui.auth.compose.AuthState.Success(
339+
result = linkResult,
340+
user = linkResult.user!!,
341+
isNewUser = linkResult.additionalUserInfo?.isNewUser ?: false
342+
)
343+
)
344+
}
345+
346+
linkResult!!
347+
}
348+
}
349+
224350
// For Send Email Link
225351
internal fun addSessionInfoToActionCodeSettings(
226352
sessionId: String,

0 commit comments

Comments
 (0)