@@ -7,6 +7,8 @@ import com.firebase.ui.auth.compose.AuthException
77import com.firebase.ui.auth.compose.AuthState
88import com.firebase.ui.auth.compose.FirebaseAuthUI
99import com.firebase.ui.auth.compose.configuration.AuthUIConfiguration
10+ import com.firebase.ui.auth.compose.configuration.auth_provider.AuthProvider.Companion.canUpgradeAnonymous
11+ import com.firebase.ui.auth.compose.configuration.auth_provider.AuthProvider.Companion.mergeProfile
1012import com.firebase.ui.auth.compose.util.EmailLinkPersistenceManager
1113import com.firebase.ui.auth.util.data.EmailLinkParser
1214import com.firebase.ui.auth.util.data.SessionUtils
@@ -122,7 +124,7 @@ internal suspend fun FirebaseAuthUI.createOrLinkUserWithEmailAndPassword(
122124 email : String ,
123125 password : String
124126): AuthResult ? {
125- val canUpgrade = AuthProvider . canUpgradeAnonymous(config, auth)
127+ val canUpgrade = canUpgradeAnonymous(config, auth)
126128 val pendingCredential =
127129 if (canUpgrade) EmailAuthProvider .getCredential(email, password) else null
128130
@@ -160,7 +162,7 @@ internal suspend fun FirebaseAuthUI.createOrLinkUserWithEmailAndPassword(
160162 }.also { authResult ->
161163 authResult?.user?.let {
162164 // Merge display name into profile (photoUri is always null for email/password)
163- AuthProvider . mergeProfile(auth, name, null )
165+ mergeProfile(auth, name, null )
164166 }
165167 }
166168 updateAuthState(AuthState .Idle )
@@ -221,7 +223,6 @@ internal suspend fun FirebaseAuthUI.createOrLinkUserWithEmailAndPassword(
221223 *
222224 * @throws AuthException.InvalidCredentialsException if email or password is incorrect
223225 * @throws AuthException.UserNotFoundException if the user doesn't exist
224- * @throws AuthException.UserDisabledException if the user account is disabled
225226 * @throws AuthException.AuthCancelledException if the operation is cancelled
226227 * @throws AuthException.NetworkException for network-related failures
227228 *
@@ -291,7 +292,7 @@ internal suspend fun FirebaseAuthUI.signInWithEmailAndPassword(
291292): AuthResult ? {
292293 try {
293294 updateAuthState(AuthState .Loading (" Signing in..." ))
294- return if (AuthProvider . canUpgradeAnonymous(config, auth)) {
295+ return if (canUpgradeAnonymous(config, auth)) {
295296 // Anonymous upgrade flow: validate credential in scratch auth
296297 val credentialToValidate = EmailAuthProvider .getCredential(email, password)
297298
@@ -338,7 +339,7 @@ internal suspend fun FirebaseAuthUI.signInWithEmailAndPassword(
338339 .also { linkResult ->
339340 // Merge profile from social provider
340341 linkResult?.user?.let { user ->
341- AuthProvider . mergeProfile(
342+ mergeProfile(
342343 auth,
343344 user.displayName,
344345 user.photoUrl
@@ -465,22 +466,22 @@ internal suspend fun FirebaseAuthUI.signInAndLinkWithCredential(
465466): AuthResult ? {
466467 try {
467468 updateAuthState(AuthState .Loading (" Signing in user..." ))
468- return if (AuthProvider . canUpgradeAnonymous(config, auth)) {
469+ return if (canUpgradeAnonymous(config, auth)) {
469470 auth.currentUser?.linkWithCredential(credential)?.await()
470471 } else {
471472 auth.signInWithCredential(credential).await()
472473 }.also { result ->
473474 // Merge profile information from the provider
474475 result?.user?.let {
475- AuthProvider . mergeProfile(auth, displayName, photoUrl)
476+ mergeProfile(auth, displayName, photoUrl)
476477 }
477478 updateAuthState(AuthState .Idle )
478479 }
479480 } catch (e: FirebaseAuthUserCollisionException ) {
480481 // Special handling for collision exceptions
481482 val authException = AuthException .from(e)
482483
483- if (AuthProvider . canUpgradeAnonymous(config, auth)) {
484+ if (canUpgradeAnonymous(config, auth)) {
484485 // Anonymous upgrade collision: emit merge conflict with updated credential
485486 val updatedCredential = e.updatedCredential
486487 if (updatedCredential != null ) {
@@ -664,7 +665,7 @@ internal suspend fun FirebaseAuthUI.sendSignInLinkToEmail(
664665 // NOTE: check for empty string instead of null to validate anonymous user ID matches
665666 // when sign in from email link
666667 val anonymousUserId =
667- if (AuthProvider . canUpgradeAnonymous(config, auth)) (auth.currentUser?.uid
668+ if (canUpgradeAnonymous(config, auth)) (auth.currentUser?.uid
668669 ? : " " ) else " "
669670
670671 // Generate sessionId
@@ -791,14 +792,26 @@ internal suspend fun FirebaseAuthUI.signInWithEmailLink(
791792
792793 if (isDifferentDevice) {
793794 // Handle cross-device flow
794- provider.handleCrossDeviceEmailLink(
795- auth = auth,
796- sessionIdFromLink = sessionIdFromLink,
797- anonymousUserIdFromLink = anonymousUserIdFromLink,
798- isEmailLinkForceSameDeviceEnabled = isEmailLinkForceSameDeviceEnabled,
799- oobCode = oobCode,
800- providerIdFromLink = providerIdFromLink
801- )
795+ // Session ID must always be present in the link
796+ if (sessionIdFromLink.isNullOrEmpty()) {
797+ throw AuthException .InvalidEmailLinkException ()
798+ }
799+
800+ // These scenarios require same-device flow
801+ if (isEmailLinkForceSameDeviceEnabled || ! anonymousUserIdFromLink.isNullOrEmpty()) {
802+ throw AuthException .EmailLinkWrongDeviceException ()
803+ }
804+
805+ // Validate the action code
806+ auth.checkActionCode(oobCode).await()
807+
808+ // If there's a provider ID, this is a linking flow which can't be done cross-device
809+ if (! providerIdFromLink.isNullOrEmpty()) {
810+ throw AuthException .EmailLinkCrossDeviceLinkingException ()
811+ }
812+
813+ // Link is valid but we need the user to provide their email
814+ throw AuthException .EmailLinkPromptForEmailException ()
802815 }
803816
804817 // Validate anonymous user ID matches (same-device flow)
@@ -819,14 +832,60 @@ internal suspend fun FirebaseAuthUI.signInWithEmailLink(
819832 ? : throw AuthException .UnknownException (" Sign in failed" )
820833 } else {
821834 // Linking Flow: Sign in with email link, then link the social credential
822- provider.handleEmailLinkWithSocialLinking(
823- context = context,
824- config = config,
825- auth = auth,
826- emailLinkCredential = emailLinkCredential,
827- storedCredentialForLink = storedCredentialForLink,
828- updateAuthState = ::updateAuthState
829- )
835+ if (canUpgradeAnonymous(config, auth)) {
836+ // Anonymous upgrade: Use safe link pattern with scratch auth
837+ val appExplicitlyForValidation = FirebaseApp .initializeApp(
838+ context,
839+ auth.app.options,
840+ " FUIAuthScratchApp_${System .currentTimeMillis()} "
841+ )
842+ val authExplicitlyForValidation = FirebaseAuth
843+ .getInstance(appExplicitlyForValidation)
844+
845+ // Safe link: Validate that both credentials can be linked
846+ val emailResult = authExplicitlyForValidation
847+ .signInWithCredential(emailLinkCredential).await()
848+
849+ val linkResult = emailResult.user
850+ ?.linkWithCredential(storedCredentialForLink)?.await()
851+
852+ // If safe link succeeds, emit merge conflict for UI to handle
853+ if (linkResult?.user != null ) {
854+ updateAuthState(
855+ AuthState .MergeConflict (
856+ storedCredentialForLink
857+ )
858+ )
859+ }
860+
861+ // Return the link result (will be non-null if successful)
862+ linkResult!!
863+ } else {
864+ // Non-upgrade: Sign in with email link, then link social credential
865+ val emailLinkResult = auth.signInWithCredential(emailLinkCredential).await()
866+
867+ // Link the social credential
868+ val linkResult = emailLinkResult.user
869+ ?.linkWithCredential(storedCredentialForLink)?.await()
870+
871+ // Merge profile from the linked social credential
872+ linkResult?.user?.let { user ->
873+ mergeProfile(auth, user.displayName, user.photoUrl)
874+ }
875+
876+ // Update to success state
877+ if (linkResult?.user != null ) {
878+ updateAuthState(
879+ AuthState .Success (
880+ result = linkResult,
881+ user = linkResult.user!! ,
882+ isNewUser = linkResult.additionalUserInfo?.isNewUser ? : false
883+ )
884+ )
885+ }
886+
887+ linkResult!!
888+ }
830889 }
831890
832891 // Clear DataStore after success
0 commit comments