Skip to content

Commit 7511d9c

Browse files
refactor: authenticate() methods for different providers
1 parent 1306c7b commit 7511d9c

8 files changed

Lines changed: 191 additions & 82 deletions

File tree

FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/AuthServiceError.swift

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,32 @@
1515
import FirebaseAuth
1616
import SwiftUI
1717

18+
/// Context information for reauthentication
19+
public struct ReauthContext: Equatable {
20+
public let providerId: String
21+
public let providerName: String
22+
public let phoneNumber: String?
23+
public let email: String?
24+
25+
public init(providerId: String, providerName: String, phoneNumber: String?, email: String?) {
26+
self.providerId = providerId
27+
self.providerName = providerName
28+
self.phoneNumber = phoneNumber
29+
self.email = email
30+
}
31+
32+
public var displayMessage: String {
33+
switch providerId {
34+
case EmailAuthProviderID:
35+
return "Please enter your password to continue"
36+
case PhoneAuthProviderID:
37+
return "Please verify your phone number to continue"
38+
default:
39+
return "Please sign in with \(providerName) to continue"
40+
}
41+
}
42+
}
43+
1844
/// Describes the specific type of account conflict that occurred
1945
public enum AccountConflictType: Equatable {
2046
/// Account exists with a different provider (e.g., user signed up with Google, trying to use
@@ -72,8 +98,17 @@ public enum AuthServiceError: LocalizedError {
7298
case invalidEmailLink(String)
7399
case clientIdNotFound(String)
74100
case notConfiguredActionCodeSettings(String)
75-
case reauthenticationRequired(String)
76-
case phoneReauthenticationRequired(phoneNumber: String)
101+
102+
/// Simple reauthentication required (Google, Apple, Facebook, Twitter, etc.)
103+
/// Can be passed directly to `reauthenticate(context:)` method
104+
case simpleReauthenticationRequired(context: ReauthContext)
105+
106+
/// Email reauthentication required - user must handle password prompt externally
107+
case emailReauthenticationRequired(context: ReauthContext)
108+
109+
/// Phone reauthentication required - user must handle SMS verification flow externally
110+
case phoneReauthenticationRequired(context: ReauthContext)
111+
77112
case invalidCredentials(String)
78113
case signInFailed(underlying: Error)
79114
case accountConflict(AccountConflictContext)
@@ -93,10 +128,12 @@ public enum AuthServiceError: LocalizedError {
93128
return description
94129
case let .notConfiguredActionCodeSettings(description):
95130
return description
96-
case let .reauthenticationRequired(description):
97-
return description
98-
case let .phoneReauthenticationRequired(phoneNumber):
99-
return "Phone reauthentication required for \(phoneNumber)"
131+
case let .simpleReauthenticationRequired(context):
132+
return "Please sign in again with \(context.providerName) to continue"
133+
case let .emailReauthenticationRequired(context):
134+
return "Please enter your password to continue"
135+
case let .phoneReauthenticationRequired(context):
136+
return "Please verify your phone number to continue"
100137
case let .invalidCredentials(description):
101138
return description
102139
// Use when failed to sign-in with Firebase

FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthService.swift

Lines changed: 70 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -706,43 +706,57 @@ public extension AuthService {
706706
currentUser = auth.currentUser
707707
}
708708

709-
/// Reauthenticates the current user with their sign-in provider
710-
/// - Throws: `AuthServiceError.phoneReauthenticationRequired` for phone auth users
711-
/// - Throws: `AuthServiceError.providerNotFound` if provider is not configured
712-
func reauthenticate() async throws {
709+
/// Reauthenticates with a simple provider (Google, Apple, Facebook, Twitter, etc.)
710+
/// - Parameter context: The reauth context from `simpleReauthenticationRequired` error
711+
/// - Throws: Error if reauthentication fails or provider is not found
712+
/// - Note: This only works for providers that can automatically obtain credentials.
713+
/// For email/phone, handle the flow externally and use `reauthenticate(with:)`
714+
public func reauthenticate(context: ReauthContext) async throws {
713715
guard let user = currentUser else {
714716
throw AuthServiceError.noCurrentUser
715717
}
716-
717-
// Get the provider from the token instead of stored credential
718+
719+
// Find the provider and get credential
720+
guard let matchingProvider = providers.first(where: { $0.id == context.providerId }),
721+
let credentialProvider = matchingProvider.provider as? CredentialAuthProviderSwift else {
722+
throw AuthServiceError.providerNotFound("No provider found for \(context.providerId)")
723+
}
724+
725+
let credential = try await credentialProvider.createAuthCredential()
726+
try await user.reauthenticate(with: credential)
727+
currentUser = auth.currentUser
728+
}
729+
730+
/// Reauthenticates with a pre-obtained credential
731+
/// Use this when you've handled getting the credential yourself (email/phone)
732+
/// - Parameter credential: The authentication credential to use for reauthentication
733+
/// - Throws: Error if reauthentication fails
734+
public func reauthenticate(with credential: AuthCredential) async throws {
735+
guard let user = currentUser else {
736+
throw AuthServiceError.noCurrentUser
737+
}
738+
try await user.reauthenticate(with: credential)
739+
currentUser = auth.currentUser
740+
}
741+
742+
/// Internal helper to create reauth context and throw appropriate error
743+
/// - Throws: Appropriate `AuthServiceError` based on the provider type
744+
private func requireReauthentication() async throws -> Never {
718745
let providerId = try await getCurrentSignInProvider()
719-
720-
if providerId == EmailAuthProviderID {
721-
guard let email = user.email else {
722-
throw AuthServiceError.invalidCredentials("User does not have an email address")
723-
}
724-
725-
guard let emailProvider = emailProvider else {
726-
throw AuthServiceError.providerNotFound(
727-
"Email provider not configured. Call withEmailSignIn() first."
728-
)
729-
}
730-
731-
let credential = try await emailProvider.createReauthCredential(email: email)
732-
try await user.reauthenticate(with: credential)
733-
} else if providerId == PhoneAuthProviderID {
734-
guard let phoneNumber = user.phoneNumber else {
735-
throw AuthServiceError.invalidCredentials("User does not have a phone number")
736-
}
737-
738-
// Throw error with context for phone reauthentication
739-
throw AuthServiceError.phoneReauthenticationRequired(phoneNumber: phoneNumber)
740-
} else if let matchingProvider = providers.first(where: { $0.id == providerId }),
741-
let credentialProvider = matchingProvider.provider as? CredentialAuthProviderSwift {
742-
let credential = try await credentialProvider.createAuthCredential()
743-
try await user.reauthenticate(with: credential)
744-
} else {
745-
throw AuthServiceError.providerNotFound("No provider found for \(providerId)")
746+
let context = ReauthContext(
747+
providerId: providerId,
748+
providerName: getProviderDisplayName(providerId),
749+
phoneNumber: currentUser?.phoneNumber,
750+
email: currentUser?.email
751+
)
752+
753+
switch providerId {
754+
case EmailAuthProviderID:
755+
throw AuthServiceError.emailReauthenticationRequired(context: context)
756+
case PhoneAuthProviderID:
757+
throw AuthServiceError.phoneReauthenticationRequired(context: context)
758+
default:
759+
throw AuthServiceError.simpleReauthenticationRequired(context: context)
746760
}
747761
}
748762

@@ -769,13 +783,35 @@ public extension AuthService {
769783
?? user.providerData.first?.providerID
770784

771785
guard let providerId = providerId else {
772-
throw AuthServiceError.reauthenticationRequired(
786+
throw AuthServiceError.invalidCredentials(
773787
"Unable to determine sign-in provider for reauthentication"
774788
)
775789
}
776790

777791
return providerId
778792
}
793+
794+
/// Get a user-friendly display name for a provider ID
795+
/// - Parameter providerId: The provider ID from Firebase Auth
796+
/// - Returns: A user-friendly name for the provider
797+
private func getProviderDisplayName(_ providerId: String) -> String {
798+
switch providerId {
799+
case EmailAuthProviderID:
800+
return "Email"
801+
case PhoneAuthProviderID:
802+
return "Phone"
803+
case "google.com":
804+
return "Google"
805+
case "apple.com":
806+
return "Apple"
807+
case "facebook.com":
808+
return "Facebook"
809+
case "twitter.com":
810+
return "Twitter"
811+
default:
812+
return providerId
813+
}
814+
}
779815

780816
func unenrollMFA(_ factorUid: String) async throws -> [MultiFactorInfo] {
781817
guard let user = auth.currentUser else {

FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/ReauthenticationCoordinator.swift

Lines changed: 0 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -15,32 +15,6 @@
1515
import FirebaseAuth
1616
import Observation
1717

18-
/// Context information for reauthentication UI
19-
public struct ReauthContext {
20-
public let providerId: String
21-
public let providerName: String
22-
public let phoneNumber: String?
23-
public let email: String?
24-
25-
public init(providerId: String, providerName: String, phoneNumber: String?, email: String?) {
26-
self.providerId = providerId
27-
self.providerName = providerName
28-
self.phoneNumber = phoneNumber
29-
self.email = email
30-
}
31-
32-
public var displayMessage: String {
33-
switch providerId {
34-
case EmailAuthProviderID:
35-
return "Please enter your password to continue"
36-
case PhoneAuthProviderID:
37-
return "Please verify your phone number to continue"
38-
default:
39-
return "Please sign in with \(providerName) to continue"
40-
}
41-
}
42-
}
43-
4418
/// Coordinator for handling reauthentication flows
4519
@MainActor
4620
@Observable

FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/ReauthenticationHelpers.swift

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,23 +15,27 @@
1515
import FirebaseAuth
1616

1717
/// Execute an operation that may require reauthentication
18+
/// Automatically handles simple providers (Google, Apple, etc.)
19+
/// For email/phone, the coordinator must be provided to handle the UI flow
1820
/// - Parameters:
1921
/// - authService: The auth service managing authentication
20-
/// - coordinator: The coordinator managing reauthentication UI
22+
/// - coordinator: The coordinator managing reauthentication UI (for email/phone)
2123
/// - operation: The operation to execute
2224
/// - Throws: Rethrows errors from the operation or reauthentication process
2325
@MainActor
24-
public func withReauthenticationIfNeeded(authService: AuthService,
25-
coordinator: ReauthenticationCoordinator,
26-
operation: @escaping () async throws
27-
-> Void) async throws {
26+
public func withReauthenticationIfNeeded(
27+
authService: AuthService,
28+
coordinator: ReauthenticationCoordinator,
29+
operation: @escaping () async throws -> Void
30+
) async throws {
2831
do {
2932
try await operation()
3033
} catch let error as NSError {
3134
// Check if reauthentication is needed
3235
if error.domain == AuthErrorDomain,
3336
error.code == AuthErrorCode.requiresRecentLogin.rawValue ||
3437
error.code == AuthErrorCode.userTokenExpired.rawValue {
38+
3539
// Determine the provider context
3640
let providerId = try await authService.getCurrentSignInProvider()
3741
let context = ReauthContext(
@@ -40,10 +44,18 @@ public func withReauthenticationIfNeeded(authService: AuthService,
4044
phoneNumber: authService.currentUser?.phoneNumber,
4145
email: authService.currentUser?.email
4246
)
43-
44-
// Request reauthentication from user with context
45-
try await coordinator.requestReauth(context: context)
46-
47+
48+
// Handle based on provider type
49+
switch providerId {
50+
case PhoneAuthProviderID, EmailAuthProviderID:
51+
// For email/phone, use coordinator to handle UI flow
52+
try await coordinator.requestReauth(context: context)
53+
54+
default:
55+
// For simple providers (Google, Apple, etc.), AuthService can handle it directly
56+
try await authService.reauthenticate(context: context)
57+
}
58+
4759
// Retry the operation after successful reauth
4860
try await operation()
4961
} else {

0 commit comments

Comments
 (0)