@@ -119,6 +119,13 @@ public final class AuthService {
119119 }
120120
121121 @ObservationIgnored @AppStorage ( " email-link " ) public var emailLink : String ?
122+ // Needed because provider data sign-in doesn't distinguish between email link and password
123+ // sign-in needed for reauthentication
124+ @ObservationIgnored @AppStorage ( " is-email-link " ) private var isEmailLinkSignIn : Bool = false
125+ // Storage for email link reauthentication (separate from sign-in)
126+ @ObservationIgnored @AppStorage ( " email-link-reauth " ) private var emailLinkReauth : String ?
127+ @ObservationIgnored @AppStorage ( " is-reauthenticating " ) private var isReauthenticating : Bool =
128+ false
122129
123130 private var currentMFAResolver : MultiFactorResolver ?
124131 private var listenerManager : AuthListenerManager ?
@@ -176,6 +183,11 @@ public final class AuthService {
176183 try await auth. signOut ( )
177184 // Cannot wait for auth listener to change, feedback needs to be immediate
178185 currentUser = nil
186+ // Clear email link sign-in flag
187+ isEmailLinkSignIn = false
188+ // Clear email link reauth state
189+ emailLinkReauth = nil
190+ isReauthenticating = false
179191 updateAuthenticationState ( )
180192 }
181193
@@ -380,20 +392,44 @@ public extension AuthService {
380392// MARK: - Email Link Sign In
381393
382394public extension AuthService {
383- func sendEmailSignInLink( email: String ) async throws {
395+ /// Send email link for sign-in or reauthentication
396+ /// - Parameters:
397+ /// - email: Email address to send link to
398+ /// - isReauth: Whether this is for reauthentication (default: false)
399+ func sendEmailSignInLink( email: String , isReauth: Bool = false ) async throws {
384400 let actionCodeSettings = try updateActionCodeSettings ( )
385401 try await auth. sendSignInLink (
386402 toEmail: email,
387403 actionCodeSettings: actionCodeSettings
388404 )
405+
406+ // Store email based on context
407+ if isReauth {
408+ emailLinkReauth = email
409+ isReauthenticating = true
410+ }
389411 }
390412
391413 func handleSignInLink( url url: URL ) async throws {
392414 do {
393- guard let email = emailLink else {
394- throw AuthServiceError
395- . invalidEmailLink ( " email address is missing from app storage. Is this the same device? " )
415+ // Check which flow we're in based on the flag
416+ let email : String
417+ let isReauth = isReauthenticating
418+
419+ if isReauth {
420+ guard let reauthEmail = emailLinkReauth else {
421+ throw AuthServiceError
422+ . invalidEmailLink ( " Email address is missing for reauthentication " )
423+ }
424+ email = reauthEmail
425+ } else {
426+ guard let signInEmail = emailLink else {
427+ throw AuthServiceError
428+ . invalidEmailLink ( " email address is missing from app storage. Is this the same device? " )
429+ }
430+ email = signInEmail
396431 }
432+
397433 let urlString = url. absoluteString
398434
399435 guard let originalLink = CommonUtils . getQueryParamValue ( from: urlString, paramName: " link " )
@@ -407,39 +443,62 @@ public extension AuthService {
407443 . invalidEmailLink ( " Failed to decode Link URL " )
408444 }
409445
410- guard let continueUrl = CommonUtils . getQueryParamValue ( from: link, paramName: " continueUrl " )
411- else {
412- throw AuthServiceError
413- . invalidEmailLink ( " `continueUrl` parameter is missing from the email link URL " )
414- }
415-
416446 if auth. isSignIn ( withEmailLink: link) {
417- let anonymousUserID = CommonUtils . getQueryParamValue (
418- from: continueUrl,
419- paramName: " ui_auid "
420- )
421- if shouldHandleAnonymousUpgrade, anonymousUserID == currentUser? . uid {
422- let credential = EmailAuthProvider . credential ( withEmail: email, link: link)
423- try await handleAutoUpgradeAnonymousUser ( credentials: credential)
447+ let credential = EmailAuthProvider . credential ( withEmail: email, link: link)
448+
449+ if isReauth {
450+ // Reauthentication flow
451+ try await reauthenticate ( with: credential)
452+ // Clean up reauth state
453+ emailLinkReauth = nil
454+ isReauthenticating = false
424455 } else {
425- let result = try await auth. signIn ( withEmail: email, link: link)
456+ // Sign-in flow
457+ guard let continueUrl = CommonUtils . getQueryParamValue (
458+ from: link,
459+ paramName: " continueUrl "
460+ )
461+ else {
462+ throw AuthServiceError
463+ . invalidEmailLink ( " `continueUrl` parameter is missing from the email link URL " )
464+ }
465+
466+ let anonymousUserID = CommonUtils . getQueryParamValue (
467+ from: continueUrl,
468+ paramName: " ui_auid "
469+ )
470+ if shouldHandleAnonymousUpgrade, anonymousUserID == currentUser? . uid {
471+ try await handleAutoUpgradeAnonymousUser ( credentials: credential)
472+ } else {
473+ let result = try await auth. signIn ( withEmail: email, link: link)
474+ }
475+ updateAuthenticationState ( )
476+ // Track that user signed in with email link
477+ isEmailLinkSignIn = true
478+ emailLink = nil
426479 }
427- updateAuthenticationState ( )
428- emailLink = nil
429480 }
430481 } catch {
431- // Reconstruct credential for conflict handling
482+ // Determine which email to use for error handling
483+ let email = isReauthenticating ? emailLinkReauth : emailLink
432484 let link = url. absoluteString
433- guard let email = emailLink else {
485+
486+ guard let email = email else {
434487 throw AuthServiceError
435- . invalidEmailLink ( " email address is missing from app storage. Is this the same device? " )
488+ . invalidEmailLink ( " email address is missing from app storage " )
436489 }
437490 let credential = EmailAuthProvider . credential ( withEmail: email, link: link)
438491
439- // Possible conflicts from auth.signIn(withEmail:link:):
440- // - accountExistsWithDifferentCredential: account exists with different provider
441- // - credentialAlreadyInUse: credential is already linked to another account
442- try handleErrorWithConflictCheck ( error: error, credential: credential)
492+ // Only handle conflicts for sign-in flow, not reauth
493+ if !isReauthenticating {
494+ // Possible conflicts from auth.signIn(withEmail:link:):
495+ // - accountExistsWithDifferentCredential: account exists with different provider
496+ // - credentialAlreadyInUse: credential is already linked to another account
497+ try handleErrorWithConflictCheck ( error: error, credential: credential)
498+ } else {
499+ // For reauth, just rethrow
500+ throw error
501+ }
443502 }
444503 }
445504}
@@ -883,8 +942,14 @@ private extension AuthService {
883942 guard let email = currentUser? . email else {
884943 throw AuthServiceError . noCurrentUser
885944 }
886- let context = EmailReauthContext ( email: email)
887- throw AuthServiceError . emailReauthenticationRequired ( context: context)
945+ // Check if user signed in with email link or password
946+ if isEmailLinkSignIn {
947+ let context = EmailLinkReauthContext ( email: email)
948+ throw AuthServiceError . emailLinkReauthenticationRequired ( context: context)
949+ } else {
950+ let context = EmailReauthContext ( email: email)
951+ throw AuthServiceError . emailReauthenticationRequired ( context: context)
952+ }
888953 case PhoneAuthProviderID:
889954 guard let phoneNumber = currentUser? . phoneNumber else {
890955 throw AuthServiceError . noCurrentUser
0 commit comments