Skip to content

Commit 086cb4e

Browse files
Merge branch 'main' into auto-upgrade-anonymous
2 parents fbd0d4f + 027572b commit 086cb4e

File tree

23 files changed

+381
-117
lines changed

23 files changed

+381
-117
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: 38 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,20 @@
11
@preconcurrency import FirebaseAuth
22
import SwiftUI
33

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

9-
public protocol FacebookProviderProtocol {
13+
public protocol FacebookProviderAuthUIProtocol: ExternalAuthProvider {
1014
@MainActor func signInWithFacebook(isLimitedLogin: Bool) async throws -> AuthCredential
1115
}
1216

13-
public protocol PhoneAuthProviderProtocol {
17+
public protocol PhoneAuthProviderAuthUIProtocol: ExternalAuthProvider {
1418
@MainActor func verifyPhoneNumber(phoneNumber: String) async throws -> String
1519
}
1620

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

3439
@MainActor
@@ -61,9 +66,9 @@ private final class AuthListenerManager {
6166
@Observable
6267
public final class AuthService {
6368
public init(configuration: AuthConfiguration = AuthConfiguration(), auth: Auth = Auth.auth(),
64-
googleProvider: GoogleProviderProtocol? = nil,
65-
facebookProvider: FacebookProviderProtocol? = nil,
66-
phoneAuthProvider: PhoneAuthProviderProtocol? = nil) {
69+
googleProvider: (any GoogleProviderAuthUIProtocol)? = nil,
70+
facebookProvider: (any FacebookProviderAuthUIProtocol)? = nil,
71+
phoneAuthProvider: (any PhoneAuthProviderAuthUIProtocol)? = nil) {
6772
self.auth = auth
6873
self.configuration = configuration
6974
self.googleProvider = googleProvider
@@ -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: GoogleProviderProtocol?
89-
private let facebookProvider: FacebookProviderProtocol?
90-
private let phoneAuthProvider: PhoneAuthProviderProtocol?
97+
private var signedInCredential: AuthCredential?
9198

92-
private var safeGoogleProvider: GoogleProviderProtocol {
99+
private var safeGoogleProvider: any GoogleProviderAuthUIProtocol {
93100
get throws {
94101
guard let provider = googleProvider else {
95102
throw AuthServiceError
@@ -99,7 +106,7 @@ public final class AuthService {
99106
}
100107
}
101108

102-
private var safeFacebookProvider: FacebookProviderProtocol {
109+
private var safeFacebookProvider: any FacebookProviderAuthUIProtocol {
103110
get throws {
104111
guard let provider = facebookProvider else {
105112
throw AuthServiceError
@@ -109,7 +116,7 @@ public final class AuthService {
109116
}
110117
}
111118

112-
private var safePhoneAuthProvider: PhoneAuthProviderProtocol {
119+
private var safePhoneAuthProvider: any PhoneAuthProviderAuthUIProtocol {
113120
get throws {
114121
guard let provider = phoneAuthProvider else {
115122
throw AuthServiceError
@@ -239,6 +246,24 @@ public extension AuthService {
239246
throw error
240247
}
241248
}
249+
250+
func updatePassword(to password: String) async throws {
251+
do {
252+
if let user = auth.currentUser {
253+
let operation = EmailPasswordUpdatePasswordOperation(
254+
passwordPrompt: passwordPrompt,
255+
newPassword: password
256+
)
257+
try await operation(on: user)
258+
}
259+
260+
} catch {
261+
errorMessage = string.localizedErrorMessage(
262+
for: error
263+
)
264+
throw error
265+
}
266+
}
242267
}
243268

244269
// 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/Utils/CommonUtils.swift

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import CommonCrypto
2+
import FirebaseCore
23
import Foundation
34
import Security
45

@@ -55,3 +56,20 @@ public class CommonUtils {
5556
return urlComponents.queryItems?.first(where: { $0.name == paramName })?.value
5657
}
5758
}
59+
60+
public extension FirebaseOptions {
61+
static func dummyConfigurationForPreview() {
62+
guard FirebaseApp.app() == nil else { return }
63+
64+
let options = FirebaseOptions(
65+
googleAppID: "1:123:ios:123abc456def7890",
66+
gcmSenderID: "dummy"
67+
)
68+
options.apiKey = "dummy"
69+
options.projectID = "dummy-project-id"
70+
options.bundleID = Bundle.main.bundleIdentifier ?? "com.example.dummy"
71+
options.clientID = "dummy-abc.apps.googleusercontent.com"
72+
73+
FirebaseApp.configure(options: options)
74+
}
75+
}

FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/EmailAuthView.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
// Created by Russell Wheatley on 20/03/2025.
66
//
77
import FirebaseAuth
8+
import FirebaseCore
89
import SwiftUI
910

1011
private enum FocusableField: Hashable {
@@ -132,3 +133,9 @@ extension EmailAuthView: View {
132133
}
133134
}
134135
}
136+
137+
#Preview {
138+
FirebaseOptions.dummyConfigurationForPreview()
139+
return EmailAuthView()
140+
.environment(AuthService())
141+
}

FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/EmailLinkView.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import FirebaseAuth
2+
import FirebaseCore
23
import SwiftUI
34

45
public struct EmailLinkView {
@@ -74,3 +75,9 @@ extension EmailLinkView: View {
7475
})
7576
}
7677
}
78+
79+
#Preview {
80+
FirebaseOptions.dummyConfigurationForPreview()
81+
return EmailLinkView()
82+
.environment(AuthService())
83+
}

FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/PasswordPromptView.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,7 @@ extension PasswordPromptSheet: View {
2727
.padding()
2828
}
2929
}
30+
31+
#Preview {
32+
PasswordPromptSheet(coordinator: PasswordPromptCoordinator())
33+
}

FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/PasswordRecoveryView.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import FirebaseCore
12
import SwiftUI
23

34
public struct PasswordRecoveryView {
@@ -67,3 +68,9 @@ extension PasswordRecoveryView: View {
6768
})
6869
}
6970
}
71+
72+
#Preview {
73+
FirebaseOptions.dummyConfigurationForPreview()
74+
return PasswordRecoveryView()
75+
.environment(AuthService())
76+
}
Lines changed: 37 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import FirebaseCore
12
import SwiftUI
23

34
@MainActor
@@ -14,32 +15,46 @@ extension SignedInView: View {
1415
}
1516

1617
public var body: some View {
17-
VStack {
18-
Text("Signed in")
19-
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")")
2024

21-
if authService.currentUser?.isEmailVerified == false {
22-
VerifyEmailView()
23-
}
24-
25-
Button("Sign out") {
26-
Task {
27-
do {
28-
try await authService.signOut()
29-
} catch {}
25+
if authService.currentUser?.isEmailVerified == false {
26+
VerifyEmailView()
3027
}
31-
}
32-
Divider()
33-
Button("Delete account") {
34-
Task {
35-
do {
36-
try await authService.deleteUser()
37-
} 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+
}
3839
}
40+
Divider()
41+
Button("Delete account") {
42+
Task {
43+
do {
44+
try await authService.deleteUser()
45+
} catch {}
46+
}
47+
}
48+
Text(authService.errorMessage).foregroundColor(.red)
49+
}.sheet(isPresented: isShowingPasswordPrompt) {
50+
PasswordPromptSheet(coordinator: authService.passwordPrompt)
3951
}
40-
Text(authService.errorMessage).foregroundColor(.red)
41-
}.sheet(isPresented: isShowingPasswordPrompt) {
42-
PasswordPromptSheet(coordinator: authService.passwordPrompt)
4352
}
4453
}
4554
}
55+
56+
#Preview {
57+
FirebaseOptions.dummyConfigurationForPreview()
58+
return SignedInView()
59+
.environment(AuthService())
60+
}

0 commit comments

Comments
 (0)