Skip to content

Commit aa7b6aa

Browse files
Merge branch 'main' into swift-ui-feedback
2 parents d06bc07 + 920673c commit aa7b6aa

File tree

14 files changed

+246
-70
lines changed

14 files changed

+246
-70
lines changed

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

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,36 @@ extension EmailPasswordOperationReauthentication {
2828
}
2929
}
3030

31-
class EmailPasswordDeleteUserOperation: DeleteUserOperation,
31+
class EmailPasswordDeleteUserOperation: AuthenticatedOperation,
3232
EmailPasswordOperationReauthentication {
3333
let passwordPrompt: PasswordPromptCoordinator
3434

3535
init(passwordPrompt: PasswordPromptCoordinator) {
3636
self.passwordPrompt = passwordPrompt
3737
}
38+
39+
func callAsFunction(on user: User) async throws {
40+
try await callAsFunction(on: user) {
41+
try await user.delete()
42+
}
43+
}
44+
}
45+
46+
class EmailPasswordUpdatePasswordOperation: AuthenticatedOperation,
47+
EmailPasswordOperationReauthentication {
48+
let passwordPrompt: PasswordPromptCoordinator
49+
let newPassword: String
50+
51+
init(passwordPrompt: PasswordPromptCoordinator, newPassword: String) {
52+
self.passwordPrompt = passwordPrompt
53+
self.newPassword = newPassword
54+
}
55+
56+
func callAsFunction(on user: User) async throws {
57+
try await callAsFunction(on: user) {
58+
try await user.updatePassword(to: newPassword)
59+
}
60+
}
3861
}
3962

4063
@MainActor

FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AccountService.swift

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -19,27 +19,19 @@ enum AuthenticationToken {
1919
protocol AuthenticatedOperation {
2020
func callAsFunction(on user: User) async throws
2121
func reauthenticate() async throws -> AuthenticationToken
22-
func performOperation(on user: User, with token: AuthenticationToken?) async throws
2322
}
2423

2524
extension AuthenticatedOperation {
26-
func callAsFunction(on user: User) async throws {
25+
func callAsFunction(on _: User,
26+
_ performOperation: () async throws -> Void) async throws {
2727
do {
28-
try await performOperation(on: user, with: nil)
28+
try await performOperation()
2929
} catch let error as NSError where error.requiresReauthentication {
3030
let token = try await reauthenticate()
31-
try await performOperation(on: user, with: token)
31+
try await performOperation()
3232
} catch AuthServiceError.reauthenticationRequired {
3333
let token = try await reauthenticate()
34-
try await performOperation(on: user, with: token)
34+
try await performOperation()
3535
}
3636
}
3737
}
38-
39-
protocol DeleteUserOperation: AuthenticatedOperation {}
40-
41-
extension DeleteUserOperation {
42-
func performOperation(on user: User, with _: AuthenticationToken? = nil) async throws {
43-
try await user.delete()
44-
}
45-
}

FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthService.swift

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
@preconcurrency import FirebaseAuth
22
import SwiftUI
33

4+
public protocol ExternalAuthProvider {
5+
associatedtype ButtonType: View
6+
@MainActor var authButton: ButtonType { get }
7+
}
8+
49
public protocol GoogleProviderAuthUIProtocol {
5-
func handleUrl(_ url: URL) -> Bool
610
@MainActor func signInWithGoogle(clientID: String) async throws -> AuthCredential
711
}
812

@@ -29,6 +33,7 @@ public enum AuthView {
2933
case authPicker
3034
case passwordRecovery
3135
case emailLink
36+
case updatePassword
3237
}
3338

3439
@MainActor
@@ -84,12 +89,14 @@ public final class AuthService {
8489
public var errorMessage = ""
8590
public let passwordPrompt: PasswordPromptCoordinator = .init()
8691

92+
public var googleProvider: (any GoogleProviderAuthUIProtocol)?
93+
public var facebookProvider: (any FacebookProviderAuthUIProtocol)?
94+
public var phoneAuthProvider: (any PhoneAuthProviderAuthUIProtocol)?
95+
8796
private var listenerManager: AuthListenerManager?
88-
private let googleProvider: GoogleProviderAuthUIProtocol?
89-
private let facebookProvider: FacebookProviderAuthUIProtocol?
90-
private let phoneAuthProvider: PhoneAuthProviderAuthUIProtocol?
97+
private var signedInCredential: AuthCredential?
9198

92-
private var safeGoogleProvider: GoogleProviderAuthUIProtocol {
99+
private var safeGoogleProvider: any GoogleProviderAuthUIProtocol {
93100
get throws {
94101
guard let provider = googleProvider else {
95102
throw AuthServiceError
@@ -217,6 +224,24 @@ public extension AuthService {
217224
throw error
218225
}
219226
}
227+
228+
func updatePassword(to password: String) async throws {
229+
do {
230+
if let user = auth.currentUser {
231+
let operation = EmailPasswordUpdatePasswordOperation(
232+
passwordPrompt: passwordPrompt,
233+
newPassword: password
234+
)
235+
try await operation(on: user)
236+
}
237+
238+
} catch {
239+
errorMessage = string.localizedErrorMessage(
240+
for: error
241+
)
242+
throw error
243+
}
244+
}
220245
}
221246

222247
// MARK: - Email/Password Sign In

FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Strings/Localizable.xcstrings

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@
162162
},
163163
"AuthPickerTitle" : {
164164
"comment" : "Title for auth picker screen.",
165+
"extractionState" : "stale",
165166
"localizations" : {
166167
"en" : {
167168
"stringUnit" : {
@@ -404,6 +405,9 @@
404405
}
405406
}
406407
}
408+
},
409+
"Enter Password" : {
410+
407411
},
408412
"EnterYourEmail" : {
409413
"comment" : "Title for email entry screen, email text field placeholder. Use short/abbreviated translation for 'email' which is less than 15 chars.",
@@ -932,6 +936,9 @@
932936
}
933937
}
934938
}
939+
},
940+
"Submit" : {
941+
935942
},
936943
"TermsOfService" : {
937944
"comment" : "Text linked to a web page with the Terms of Service content.",

FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/SignedInView.swift

Lines changed: 30 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -15,32 +15,40 @@ extension SignedInView: View {
1515
}
1616

1717
public var body: some View {
18-
VStack {
19-
Text("Signed in")
20-
Text("User: \(authService.currentUser?.email ?? "Unknown")")
18+
if authService.authView == .updatePassword {
19+
UpdatePasswordView()
20+
} else {
21+
VStack {
22+
Text("Signed in")
23+
Text("User: \(authService.currentUser?.email ?? "Unknown")")
2124

22-
if authService.currentUser?.isEmailVerified == false {
23-
VerifyEmailView()
24-
}
25-
26-
Button("Sign out") {
27-
Task {
28-
do {
29-
try await authService.signOut()
30-
} catch {}
25+
if authService.currentUser?.isEmailVerified == false {
26+
VerifyEmailView()
3127
}
32-
}
33-
Divider()
34-
Button("Delete account") {
35-
Task {
36-
do {
37-
try await authService.deleteUser()
38-
} catch {}
28+
Divider()
29+
Button("Update password") {
30+
authService.authView = .updatePassword
31+
}
32+
Divider()
33+
Button("Sign out") {
34+
Task {
35+
do {
36+
try await authService.signOut()
37+
} catch {}
38+
}
39+
}
40+
Divider()
41+
Button("Delete account") {
42+
Task {
43+
do {
44+
try await authService.deleteUser()
45+
} catch {}
46+
}
3947
}
48+
Text(authService.errorMessage).foregroundColor(.red)
49+
}.sheet(isPresented: isShowingPasswordPrompt) {
50+
PasswordPromptSheet(coordinator: authService.passwordPrompt)
4051
}
41-
Text(authService.errorMessage).foregroundColor(.red)
42-
}.sheet(isPresented: isShowingPasswordPrompt) {
43-
PasswordPromptSheet(coordinator: authService.passwordPrompt)
4452
}
4553
}
4654
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
//
2+
// UpdatePassword.swift
3+
// FirebaseUI
4+
//
5+
// Created by Russell Wheatley on 24/04/2025.
6+
//
7+
8+
import SwiftUI
9+
10+
private enum FocusableField: Hashable {
11+
case password
12+
case confirmPassword
13+
}
14+
15+
@MainActor
16+
public struct UpdatePasswordView {
17+
@Environment(AuthService.self) private var authService
18+
@State private var password = ""
19+
@State private var confirmPassword = ""
20+
21+
@FocusState private var focus: FocusableField?
22+
private var isValid: Bool {
23+
!password.isEmpty && password == confirmPassword
24+
}
25+
}
26+
27+
extension UpdatePasswordView: View {
28+
private var isShowingPasswordPrompt: Binding<Bool> {
29+
Binding(
30+
get: { authService.passwordPrompt.isPromptingPassword },
31+
set: { authService.passwordPrompt.isPromptingPassword = $0 }
32+
)
33+
}
34+
35+
public var body: some View {
36+
VStack {
37+
LabeledContent {
38+
SecureField("Password", text: $password)
39+
.focused($focus, equals: .password)
40+
.submitLabel(.go)
41+
} label: {
42+
Image(systemName: "lock")
43+
}
44+
.padding(.vertical, 6)
45+
.background(Divider(), alignment: .bottom)
46+
.padding(.bottom, 8)
47+
48+
Divider()
49+
50+
LabeledContent {
51+
SecureField("Confirm password", text: $confirmPassword)
52+
.focused($focus, equals: .confirmPassword)
53+
.submitLabel(.go)
54+
} label: {
55+
Image(systemName: "lock")
56+
}
57+
.padding(.vertical, 6)
58+
.background(Divider(), alignment: .bottom)
59+
.padding(.bottom, 8)
60+
61+
Divider()
62+
63+
Button(action: {
64+
Task {
65+
try await authService.updatePassword(to: confirmPassword)
66+
authService.authView = .authPicker
67+
}
68+
}, label: {
69+
Text("Update password")
70+
.padding(.vertical, 8)
71+
.frame(maxWidth: .infinity)
72+
73+
})
74+
.disabled(!isValid)
75+
.padding([.top, .bottom], 8)
76+
.frame(maxWidth: .infinity)
77+
.buttonStyle(.borderedProminent)
78+
}.sheet(isPresented: isShowingPasswordPrompt) {
79+
PasswordPromptSheet(coordinator: authService.passwordPrompt)
80+
}
81+
}
82+
}

FirebaseSwiftUI/FirebaseFacebookSwiftUI/Sources/Services/FacebookProviderAuthUI.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import FacebookCore
33
import FacebookLogin
44
import FirebaseAuth
55
import FirebaseAuthSwiftUI
6+
import SwiftUI
67

78
let kFacebookEmailScope = "email"
89
let kFacebookProfileScope = "public_profile"
@@ -34,6 +35,10 @@ public class FacebookProviderAuthUI: FacebookProviderAuthUIProtocol {
3435
shaNonce = CommonUtils.sha256Hash(of: rawNonce)
3536
}
3637

38+
@MainActor public var authButton: SignInWithFacebookButton {
39+
return SignInWithFacebookButton()
40+
}
41+
3742
@MainActor public func signInWithFacebook(isLimitedLogin: Bool) async throws -> AuthCredential {
3843
let trackingStatus = ATTrackingManager.trackingAuthorizationStatus
3944
let tracking: LoginTracking = trackingStatus != .authorized ? .limited :
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
//
2+
// AuthService+Google.swift
3+
// FirebaseUI
4+
//
5+
// Created by Morgan Chen on 4/16/25.
6+
//
7+
8+
import FirebaseAuthSwiftUI
9+
10+
public extension AuthService {
11+
@discardableResult
12+
func withGoogleSignIn() -> AuthService {
13+
let clientID = auth.app?.options.clientID ?? ""
14+
googleProvider = GoogleProviderAuthUI(clientID: clientID)
15+
return self
16+
}
17+
}

FirebaseSwiftUI/FirebaseGoogleSwiftUI/Sources/Services/GoogleProviderAuthUI.swift

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
@preconcurrency import FirebaseAuth
22
import FirebaseAuthSwiftUI
3+
import FirebaseCore
34
import GoogleSignIn
5+
import GoogleSignInSwift
6+
import SwiftUI
47

58
let kGoogleUserInfoEmailScope = "https://www.googleapis.com/auth/userinfo.email"
69
let kGoogleUserInfoProfileScope = "https://www.googleapis.com/auth/userinfo.profile"
@@ -16,12 +19,18 @@ public class GoogleProviderAuthUI: @preconcurrency GoogleProviderAuthUIProtocol
1619
let scopes: [String]
1720
let shortName = "Google"
1821
let providerId = "google.com"
19-
public init(scopes: [String]? = nil) {
22+
let clientID: String
23+
public init(scopes: [String]? = nil, clientID: String = FirebaseApp.app()!.options.clientID!) {
2024
self.scopes = scopes ?? kDefaultScopes
25+
self.clientID = clientID
2126
}
2227

23-
public func handleUrl(_ url: URL) -> Bool {
24-
return GIDSignIn.sharedInstance.handle(url)
28+
@MainActor public var authButton: GoogleSignInButton {
29+
return GoogleSignInButton {
30+
Task {
31+
try await self.signInWithGoogle(clientID: self.clientID)
32+
}
33+
}
2534
}
2635

2736
@MainActor public func signInWithGoogle(clientID: String) async throws -> AuthCredential {

FirebaseSwiftUI/FirebasePhoneAuthSwiftUI/Sources/Services/PhoneAuthProviderAuthUI.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
@preconcurrency import FirebaseAuth
22
import FirebaseAuthSwiftUI
3+
import SwiftUI
34

45
public typealias VerificationID = String
56

67
public class PhoneAuthProviderAuthUI: @preconcurrency PhoneAuthProviderAuthUIProtocol {
8+
public var authButton: Button<Text> {
9+
// TODO: implement me
10+
return Button("Phone", action: {})
11+
}
12+
713
public init() {}
814

915
@MainActor public func verifyPhoneNumber(phoneNumber: String) async throws -> VerificationID {

0 commit comments

Comments
 (0)