Skip to content

Commit 6351838

Browse files
committed
Merge branch 'feat/P2' of github.com:demolaf/FirebaseUI-Android into feat/S3
2 parents ff17661 + 9c665ca commit 6351838

File tree

2 files changed

+84
-150
lines changed

2 files changed

+84
-150
lines changed

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

Lines changed: 0 additions & 125 deletions
Original file line numberDiff line numberDiff line change
@@ -232,131 +232,6 @@ abstract class AuthProvider(open val providerId: String) {
232232
}
233233
}
234234

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

auth/src/main/java/com/firebase/ui/auth/compose/configuration/auth_provider/EmailAuthProvider+FirebaseAuthUI.kt

Lines changed: 84 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import com.firebase.ui.auth.compose.AuthException
77
import com.firebase.ui.auth.compose.AuthState
88
import com.firebase.ui.auth.compose.FirebaseAuthUI
99
import 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
1012
import com.firebase.ui.auth.compose.util.EmailLinkPersistenceManager
1113
import com.firebase.ui.auth.util.data.EmailLinkParser
1214
import 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

Comments
 (0)