Skip to content

Commit 44594a1

Browse files
feat: allow user to update password
1 parent a29ab0a commit 44594a1

File tree

5 files changed

+153
-36
lines changed

5 files changed

+153
-36
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) { _ in
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) { _ in
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+
_ perform: (AuthenticationToken?) async throws -> Void) async throws {
2727
do {
28-
try await performOperation(on: user, with: nil)
28+
try await perform(nil)
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 perform(token)
3232
} catch AuthServiceError.reauthenticationRequired {
3333
let token = try await reauthenticate()
34-
try await performOperation(on: user, with: token)
34+
try await perform(token)
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: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ public enum AuthView {
2929
case authPicker
3030
case passwordRecovery
3131
case emailLink
32+
case updatePassword
3233
}
3334

3435
@MainActor
@@ -217,6 +218,24 @@ public extension AuthService {
217218
throw error
218219
}
219220
}
221+
222+
func updatePassword(to password: String) async throws {
223+
do {
224+
if let user = auth.currentUser {
225+
let operation = EmailPasswordUpdatePasswordOperation(
226+
passwordPrompt: passwordPrompt,
227+
newPassword: password
228+
)
229+
try await operation(on: user)
230+
}
231+
232+
} catch {
233+
errorMessage = string.localizedErrorMessage(
234+
for: error
235+
)
236+
throw error
237+
}
238+
}
220239
}
221240

222241
// MARK: - Email/Password Sign In

FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/SignedInView.swift

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

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

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 {}
24+
if authService.currentUser?.isEmailVerified == false {
25+
VerifyEmailView()
3026
}
31-
}
32-
Divider()
33-
Button("Delete account") {
34-
Task {
35-
do {
36-
try await authService.deleteUser()
37-
} catch {}
27+
Divider()
28+
Button("Update password") {
29+
authService.authView = .updatePassword
30+
}
31+
Divider()
32+
Button("Sign out") {
33+
Task {
34+
do {
35+
try await authService.signOut()
36+
} catch {}
37+
}
38+
}
39+
Divider()
40+
Button("Delete account") {
41+
Task {
42+
do {
43+
try await authService.deleteUser()
44+
} catch {}
45+
}
3846
}
47+
Text(authService.errorMessage).foregroundColor(.red)
48+
}.sheet(isPresented: isShowingPasswordPrompt) {
49+
PasswordPromptSheet(coordinator: authService.passwordPrompt)
3950
}
40-
Text(authService.errorMessage).foregroundColor(.red)
41-
}.sheet(isPresented: isShowingPasswordPrompt) {
42-
PasswordPromptSheet(coordinator: authService.passwordPrompt)
4351
}
4452
}
4553
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
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+
public struct UpdatePasswordView {
16+
@Environment(AuthService.self) private var authService
17+
@State private var password = ""
18+
@State private var confirmPassword = ""
19+
20+
@FocusState private var focus: FocusableField?
21+
private var isValid: Bool {
22+
!password.isEmpty && password == confirmPassword
23+
}
24+
}
25+
26+
extension UpdatePasswordView: View {
27+
public var body: some View {
28+
VStack {
29+
LabeledContent {
30+
SecureField("Password", text: $password)
31+
.focused($focus, equals: .password)
32+
.submitLabel(.go)
33+
} label: {
34+
Image(systemName: "lock")
35+
}
36+
.padding(.vertical, 6)
37+
.background(Divider(), alignment: .bottom)
38+
.padding(.bottom, 8)
39+
Divider()
40+
41+
LabeledContent {
42+
SecureField("Confirm password", text: $confirmPassword)
43+
.focused($focus, equals: .confirmPassword)
44+
.submitLabel(.go)
45+
} label: {
46+
Image(systemName: "lock")
47+
}
48+
.padding(.vertical, 6)
49+
.background(Divider(), alignment: .bottom)
50+
.padding(.bottom, 8)
51+
Divider()
52+
Button(action: {
53+
Task {
54+
try await authService.updatePassword(to: confirmPassword)
55+
authService.authView = .authPicker
56+
}
57+
}) {
58+
if authService.authenticationState != .authenticating {
59+
Text("Update password")
60+
.padding(.vertical, 8)
61+
.frame(maxWidth: .infinity)
62+
} else {
63+
ProgressView()
64+
.progressViewStyle(CircularProgressViewStyle(tint: .white))
65+
.padding(.vertical, 8)
66+
.frame(maxWidth: .infinity)
67+
}
68+
}
69+
.disabled(!isValid)
70+
.padding([.top, .bottom], 8)
71+
.frame(maxWidth: .infinity)
72+
.buttonStyle(.borderedProminent)
73+
}
74+
}
75+
}

0 commit comments

Comments
 (0)