Skip to content

Commit 649a723

Browse files
Merge branch 'main' into swiftui-tests
2 parents c93af1f + 0a5962f commit 649a723

File tree

11 files changed

+175
-87
lines changed

11 files changed

+175
-87
lines changed

FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/AuthServiceError.swift

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ public struct AccountMergeConflictContext: LocalizedError {
1717
public enum AuthServiceError: LocalizedError {
1818
case noCurrentUser
1919
case invalidEmailLink(String)
20-
case notConfiguredProvider(String)
2120
case clientIdNotFound(String)
2221
case notConfiguredActionCodeSettings(String)
2322
case reauthenticationRequired(String)
@@ -31,8 +30,6 @@ public enum AuthServiceError: LocalizedError {
3130
return "No user is currently signed in."
3231
case let .invalidEmailLink(description):
3332
return description
34-
case let .notConfiguredProvider(description):
35-
return description
3633
case let .clientIdNotFound(description):
3734
return description
3835
case let .notConfiguredActionCodeSettings(description):

FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AccountService+Email.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ protocol EmailPasswordOperationReauthentication {
66
}
77

88
extension EmailPasswordOperationReauthentication {
9-
func reauthenticate() async throws -> AuthenticationToken {
9+
// TODO: - @MainActor because User is non-sendable. Might change this once User is sendable in firebase-ios-sdk
10+
@MainActor func reauthenticate() async throws -> AuthenticationToken {
1011
guard let user = Auth.auth().currentUser else {
1112
throw AuthServiceError.reauthenticationRequired("No user currently signed-in")
1213
}
@@ -28,6 +29,7 @@ extension EmailPasswordOperationReauthentication {
2829
}
2930
}
3031

32+
@MainActor
3133
class EmailPasswordDeleteUserOperation: AuthenticatedOperation,
3234
EmailPasswordOperationReauthentication {
3335
let passwordPrompt: PasswordPromptCoordinator

FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AccountService.swift

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,18 @@ extension NSError {
1111
}
1212
}
1313

14-
enum AuthenticationToken {
14+
public enum AuthenticationToken {
1515
case apple(ASAuthorizationAppleIDCredential, String)
1616
case firebase(String)
1717
}
1818

19-
protocol AuthenticatedOperation {
19+
@MainActor
20+
public protocol AuthenticatedOperation {
2021
func callAsFunction(on user: User) async throws
2122
func reauthenticate() async throws -> AuthenticationToken
2223
}
2324

24-
extension AuthenticatedOperation {
25+
public extension AuthenticatedOperation {
2526
func callAsFunction(on _: User,
2627
_ performOperation: () async throws -> Void) async throws {
2728
do {

FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthService.swift

Lines changed: 47 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ public protocol GoogleProviderAuthUIProtocol: ExternalAuthProvider {
1212

1313
public protocol FacebookProviderAuthUIProtocol: ExternalAuthProvider {
1414
@MainActor func signInWithFacebook(isLimitedLogin: Bool) async throws -> AuthCredential
15+
@MainActor func deleteUser(user: User) async throws
1516
}
1617

1718
public protocol PhoneAuthProviderAuthUIProtocol: ExternalAuthProvider {
@@ -82,17 +83,30 @@ public final class AuthService {
8283
public var authenticationFlow: AuthenticationFlow = .login
8384
public var errorMessage = ""
8485
public let passwordPrompt: PasswordPromptCoordinator = .init()
85-
86-
public var googleProvider: (any GoogleProviderAuthUIProtocol)?
87-
public var facebookProvider: (any FacebookProviderAuthUIProtocol)?
88-
public var phoneAuthProvider: (any PhoneAuthProviderAuthUIProtocol)?
86+
private var unsafeGoogleProvider: (any GoogleProviderAuthUIProtocol)?
87+
private var unsafeFacebookProvider: (any FacebookProviderAuthUIProtocol)?
88+
private var unsafePhoneAuthProvider: (any PhoneAuthProviderAuthUIProtocol)?
8989

9090
private var listenerManager: AuthListenerManager?
9191
public var signedInCredential: AuthCredential?
9292

93+
var emailSignInEnabled = false
94+
9395
private var providers: [ExternalAuthProvider] = []
9496
public func register(provider: ExternalAuthProvider) {
95-
providers.append(provider)
97+
switch provider {
98+
case let google as GoogleProviderAuthUIProtocol:
99+
unsafeGoogleProvider = google
100+
providers.append(provider)
101+
case let facebook as FacebookProviderAuthUIProtocol:
102+
unsafeFacebookProvider = facebook
103+
providers.append(provider)
104+
case let phone as PhoneAuthProviderAuthUIProtocol:
105+
unsafePhoneAuthProvider = phone
106+
providers.append(provider)
107+
default:
108+
break
109+
}
96110
}
97111

98112
public func renderButtons(spacing: CGFloat = 16) -> AnyView {
@@ -105,31 +119,28 @@ public final class AuthService {
105119
)
106120
}
107121

108-
private var safeGoogleProvider: any GoogleProviderAuthUIProtocol {
122+
private var googleProvider: any GoogleProviderAuthUIProtocol {
109123
get throws {
110-
guard let provider = googleProvider else {
111-
throw AuthServiceError
112-
.notConfiguredProvider("`GoogleProviderSwift` has not been configured")
124+
guard let provider = unsafeGoogleProvider else {
125+
fatalError("`GoogleProviderAuthUI` has not been configured")
113126
}
114127
return provider
115128
}
116129
}
117130

118-
private var safeFacebookProvider: any FacebookProviderAuthUIProtocol {
131+
private var facebookProvider: any FacebookProviderAuthUIProtocol {
119132
get throws {
120-
guard let provider = facebookProvider else {
121-
throw AuthServiceError
122-
.notConfiguredProvider("`FacebookProviderSwift` has not been configured")
133+
guard let provider = unsafeFacebookProvider else {
134+
fatalError("`FacebookProviderAuthUI` has not been configured")
123135
}
124136
return provider
125137
}
126138
}
127139

128-
private var safePhoneAuthProvider: any PhoneAuthProviderAuthUIProtocol {
140+
private var phoneAuthProvider: any PhoneAuthProviderAuthUIProtocol {
129141
get throws {
130-
guard let provider = phoneAuthProvider else {
131-
throw AuthServiceError
132-
.notConfiguredProvider("`PhoneAuthProviderSwift` has not been configured")
142+
guard let provider = unsafePhoneAuthProvider else {
143+
fatalError("`PhoneAuthProviderAuthUI` has not been configured")
133144
}
134145
return provider
135146
}
@@ -215,7 +226,8 @@ public final class AuthService {
215226
if shouldHandleAnonymousUpgrade {
216227
try await handleAutoUpgradeAnonymousUser(credentials: credentials)
217228
} else {
218-
try await auth.signIn(with: credentials)
229+
let result = try await auth.signIn(with: credentials)
230+
signedInCredential = result.credential
219231
}
220232
updateAuthenticationState()
221233
} catch {
@@ -247,9 +259,13 @@ public final class AuthService {
247259
public extension AuthService {
248260
func deleteUser() async throws {
249261
do {
250-
if let user = auth.currentUser {
251-
let operation = EmailPasswordDeleteUserOperation(passwordPrompt: passwordPrompt)
252-
try await operation(on: user)
262+
if let user = auth.currentUser, let providerId = signedInCredential?.provider {
263+
if providerId == "password" {
264+
let operation = EmailPasswordDeleteUserOperation(passwordPrompt: passwordPrompt)
265+
try await operation(on: user)
266+
} else if providerId == "facebook.com" {
267+
try await facebookProvider.deleteUser(user: user)
268+
}
253269
}
254270

255271
} catch {
@@ -282,6 +298,11 @@ public extension AuthService {
282298
// MARK: - Email/Password Sign In
283299

284300
public extension AuthService {
301+
func withEmailSignIn() -> AuthService {
302+
emailSignInEnabled = true
303+
return self
304+
}
305+
285306
func signIn(withEmail email: String, password: String) async throws {
286307
let credential = EmailAuthProvider.credential(withEmail: email, password: password)
287308
try await signIn(credentials: credential)
@@ -295,7 +316,8 @@ public extension AuthService {
295316
let credential = EmailAuthProvider.credential(withEmail: email, password: password)
296317
try await handleAutoUpgradeAnonymousUser(credentials: credential)
297318
} else {
298-
try await auth.createUser(withEmail: email, password: password)
319+
let result = try await auth.createUser(withEmail: email, password: password)
320+
signedInCredential = result.credential
299321
}
300322
updateAuthenticationState()
301323
} catch {
@@ -410,7 +432,7 @@ public extension AuthService {
410432
"OAuth client ID not found. Please make sure Google Sign-In is enabled in the Firebase console. You may have to download a new GoogleService-Info.plist file after enabling Google Sign-In."
411433
)
412434
}
413-
let credential = try await safeGoogleProvider.signInWithGoogle(clientID: clientID)
435+
let credential = try await googleProvider.signInWithGoogle(clientID: clientID)
414436

415437
try await signIn(credentials: credential)
416438
}
@@ -420,7 +442,7 @@ public extension AuthService {
420442

421443
public extension AuthService {
422444
func signInWithFacebook(limitedLogin: Bool = true) async throws {
423-
let credential = try await safeFacebookProvider
445+
let credential = try await facebookProvider
424446
.signInWithFacebook(isLimitedLogin: limitedLogin)
425447
try await signIn(credentials: credential)
426448
}
@@ -431,7 +453,7 @@ public extension AuthService {
431453
public extension AuthService {
432454
func verifyPhoneNumber(phoneNumber: String) async throws -> String {
433455
do {
434-
return try await safePhoneAuthProvider.verifyPhoneNumber(phoneNumber: phoneNumber)
456+
return try await phoneAuthProvider.verifyPhoneNumber(phoneNumber: phoneNumber)
435457
} catch {
436458
errorMessage = string.localizedErrorMessage(
437459
for: error
Lines changed: 33 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
1+
import FirebaseCore
12
import SwiftUI
23

34
@MainActor
4-
public struct AuthPickerView<Content: View> {
5+
public struct AuthPickerView {
56
@Environment(AuthService.self) private var authService
6-
let providerButtons: () -> Content
77

8-
public init(@ViewBuilder providerButtons: @escaping () -> Content) {
9-
self.providerButtons = providerButtons
10-
}
8+
public init() {}
119

1210
private func switchFlow() {
1311
authService.authenticationFlow = authService
@@ -29,34 +27,43 @@ extension AuthPickerView: View {
2927
} else if authService.authView == .emailLink {
3028
EmailLinkView()
3129
} else {
32-
Text(authService.authenticationFlow == .login ? authService.string
33-
.emailLoginFlowLabel : authService.string.emailSignUpFlowLabel)
34-
VStack { Divider() }
35-
36-
EmailAuthView()
30+
if authService.emailSignInEnabled {
31+
Text(authService.authenticationFlow == .login ? authService.string
32+
.emailLoginFlowLabel : authService.string.emailSignUpFlowLabel)
33+
Divider()
34+
EmailAuthView()
35+
}
3736
VStack {
3837
authService.renderButtons()
3938
}.padding(.horizontal)
40-
41-
VStack { Divider() }
42-
HStack {
43-
Text(authService
44-
.authenticationFlow == .login ? authService.string.dontHaveAnAccountYetLabel :
45-
authService.string.alreadyHaveAnAccountLabel)
46-
Button(action: {
47-
withAnimation {
48-
switchFlow()
49-
}
50-
}) {
51-
Text(authService.authenticationFlow == .signUp ? authService.string
52-
.emailLoginFlowLabel : authService.string.emailSignUpFlowLabel)
53-
.fontWeight(.semibold)
54-
.foregroundColor(.blue)
55-
}.accessibilityIdentifier("switch-auth-flow")
39+
if authService.emailSignInEnabled {
40+
Divider()
41+
HStack {
42+
Text(authService
43+
.authenticationFlow == .login ? authService.string.dontHaveAnAccountYetLabel :
44+
authService.string.alreadyHaveAnAccountLabel)
45+
Button(action: {
46+
withAnimation {
47+
switchFlow()
48+
}
49+
}) {
50+
Text(authService.authenticationFlow == .signUp ? authService.string
51+
.emailLoginFlowLabel : authService.string.emailSignUpFlowLabel)
52+
.fontWeight(.semibold)
53+
.foregroundColor(.blue)
54+
}.accessibilityIdentifier("switch-auth-flow")
55+
}
5656
}
5757
PrivacyTOCsView(displayMode: .footer)
5858
Text(authService.errorMessage).foregroundColor(.red)
5959
}
6060
}
6161
}
6262
}
63+
64+
#Preview {
65+
FirebaseOptions.dummyConfigurationForPreview()
66+
let authService = AuthService()
67+
.withEmailSignIn()
68+
return AuthPickerView().environment(authService)
69+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
//
2+
// AccountService+Facebook.swift
3+
// FirebaseUI
4+
//
5+
// Created by Russell Wheatley on 14/05/2025.
6+
//
7+
8+
@preconcurrency import FirebaseAuth
9+
import FirebaseAuthSwiftUI
10+
import Observation
11+
12+
protocol FacebookOperationReauthentication {
13+
var facebookProvider: FacebookProviderAuthUI { get }
14+
}
15+
16+
extension FacebookOperationReauthentication {
17+
@MainActor func reauthenticate() async throws -> AuthenticationToken {
18+
guard let user = Auth.auth().currentUser else {
19+
throw AuthServiceError.reauthenticationRequired("No user currently signed-in")
20+
}
21+
22+
do {
23+
let credential = try await facebookProvider
24+
.signInWithFacebook(isLimitedLogin: facebookProvider.isLimitedLogin)
25+
try await user.reauthenticate(with: credential)
26+
27+
return .firebase("")
28+
} catch {
29+
throw AuthServiceError.signInFailed(underlying: error)
30+
}
31+
}
32+
}
33+
34+
@MainActor
35+
class FacebookDeleteUserOperation: AuthenticatedOperation,
36+
@preconcurrency FacebookOperationReauthentication {
37+
let facebookProvider: FacebookProviderAuthUI
38+
init(facebookProvider: FacebookProviderAuthUI) {
39+
self.facebookProvider = facebookProvider
40+
}
41+
42+
func callAsFunction(on user: User) async throws {
43+
try await callAsFunction(on: user) {
44+
try await user.delete()
45+
}
46+
}
47+
}

FirebaseSwiftUI/FirebaseFacebookSwiftUI/Sources/Services/AuthService+Facebook.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ import FirebaseAuthSwiftUI
1010
public extension AuthService {
1111
@discardableResult
1212
func withFacebookSignIn(scopes scopes: [String]? = nil) -> AuthService {
13-
facebookProvider = FacebookProviderAuthUI(scopes: scopes)
14-
register(provider: facebookProvider!)
13+
FacebookProviderAuthUI.configureProvider(scopes: scopes)
14+
register(provider: FacebookProviderAuthUI.shared)
1515
return self
1616
}
1717
}

0 commit comments

Comments
 (0)