@@ -20,6 +20,7 @@ import android.util.Log
2020import androidx.compose.ui.graphics.Color
2121import androidx.datastore.preferences.core.stringPreferencesKey
2222import com.firebase.ui.auth.R
23+ import com.firebase.ui.auth.compose.AuthException
2324import com.firebase.ui.auth.compose.configuration.AuthUIConfiguration
2425import com.firebase.ui.auth.compose.configuration.AuthUIConfigurationDsl
2526import 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