Skip to content

Commit 30818ed

Browse files
Merge branch 'main' into string-localisation
2 parents 8908d95 + 027572b commit 30818ed

File tree

23 files changed

+374
-116
lines changed

23 files changed

+374
-116
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
@@ -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: 1 addition & 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" : {

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

@@ -47,3 +48,20 @@ public class CommonUtils {
4748
return hash.map { String(format: "%02x", $0) }.joined()
4849
}
4950
}
51+
52+
public extension FirebaseOptions {
53+
static func dummyConfigurationForPreview() {
54+
guard FirebaseApp.app() == nil else { return }
55+
56+
let options = FirebaseOptions(
57+
googleAppID: "1:123:ios:123abc456def7890",
58+
gcmSenderID: "dummy"
59+
)
60+
options.apiKey = "dummy"
61+
options.projectID = "dummy-project-id"
62+
options.bundleID = Bundle.main.bundleIdentifier ?? "com.example.dummy"
63+
options.clientID = "dummy-abc.apps.googleusercontent.com"
64+
65+
FirebaseApp.configure(options: options)
66+
}
67+
}

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 {
@@ -133,3 +134,9 @@ extension EmailAuthView: View {
133134
}
134135
}
135136
}
137+
138+
#Preview {
139+
FirebaseOptions.dummyConfigurationForPreview()
140+
return EmailAuthView()
141+
.environment(AuthService())
142+
}

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 {
@@ -72,3 +73,9 @@ extension EmailLinkView: View {
7273
})
7374
}
7475
}
76+
77+
#Preview {
78+
FirebaseOptions.dummyConfigurationForPreview()
79+
return EmailLinkView()
80+
.environment(AuthService())
81+
}

FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/PasswordPromptView.swift

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

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 {
@@ -80,3 +81,9 @@ extension PasswordRecoveryView: View {
8081
})
8182
}
8283
}
84+
85+
#Preview {
86+
FirebaseOptions.dummyConfigurationForPreview()
87+
return PasswordRecoveryView()
88+
.environment(AuthService())
89+
}
Lines changed: 36 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import FirebaseCore
12
import SwiftUI
23

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

1617
public var body: some View {
17-
VStack {
18-
Text(authService.string.signedInTitle)
18+
if authService.authView == .updatePassword {
19+
UpdatePasswordView()
20+
} else {
21+
VStack {
22+
Text(authService.string.signedInTitle)
1923
.font(.largeTitle)
2024
.fontWeight(.bold)
2125
.padding()
2226
Text(authService.string.accountSettingsEmailLabel)
2327
Text("\(authService.currentUser?.email ?? "Unknown")")
2428

25-
if authService.currentUser?.isEmailVerified == false {
26-
VerifyEmailView()
27-
}
28-
29-
Button(authService.string.signOutButtonLabel) {
30-
Task {
31-
do {
32-
try await authService.signOut()
33-
} catch {}
29+
if authService.currentUser?.isEmailVerified == false {
30+
VerifyEmailView()
3431
}
35-
}
36-
Divider()
37-
Button(authService.string.deleteAccountButtonLabel) {
38-
Task {
39-
do {
40-
try await authService.deleteUser()
41-
} catch {}
32+
Divider()
33+
Button("Update password") {
34+
authService.authView = .updatePassword
35+
}
36+
Divider()
37+
Button(authService.string.signOutButtonLabel) {
38+
Task {
39+
do {
40+
try await authService.signOut()
41+
} catch {}
42+
}
4243
}
44+
Divider()
45+
Button(authService.string.deleteAccountButtonLabel) {
46+
Task {
47+
do {
48+
try await authService.deleteUser()
49+
} catch {}
50+
}
51+
}
52+
Text(authService.errorMessage).foregroundColor(.red)
53+
}.sheet(isPresented: isShowingPasswordPrompt) {
54+
PasswordPromptSheet(coordinator: authService.passwordPrompt)
4355
}
44-
Text(authService.errorMessage).foregroundColor(.red)
45-
}.sheet(isPresented: isShowingPasswordPrompt) {
46-
PasswordPromptSheet(coordinator: authService.passwordPrompt)
4756
}
4857
}
4958
}
59+
60+
#Preview {
61+
FirebaseOptions.dummyConfigurationForPreview()
62+
return SignedInView()
63+
.environment(AuthService())
64+
}

0 commit comments

Comments
 (0)