@@ -20,6 +20,7 @@ import android.util.Log
20
20
import androidx.compose.ui.graphics.Color
21
21
import androidx.datastore.preferences.core.stringPreferencesKey
22
22
import com.firebase.ui.auth.R
23
+ import com.firebase.ui.auth.compose.AuthException
23
24
import com.firebase.ui.auth.compose.configuration.AuthUIConfiguration
24
25
import com.firebase.ui.auth.compose.configuration.AuthUIConfigurationDsl
25
26
import com.firebase.ui.auth.compose.configuration.PasswordRule
@@ -54,11 +55,11 @@ class AuthProvidersBuilder {
54
55
/* *
55
56
* Enum class to represent all possible providers.
56
57
*/
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 ),
62
63
EMAIL (EmailAuthProvider .PROVIDER_ID ),
63
64
PHONE (PhoneAuthProvider .PROVIDER_ID ),
64
65
ANONYMOUS (" anonymous" ),
@@ -221,6 +222,131 @@ abstract class AuthProvider(open val providerId: String) {
221
222
}
222
223
}
223
224
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
+
224
350
// For Send Email Link
225
351
internal fun addSessionInfoToActionCodeSettings (
226
352
sessionId : String ,
0 commit comments