From 44594a1644217e36a692dd20dfe8c12c51b5a749 Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Thu, 24 Apr 2025 15:39:02 +0100 Subject: [PATCH 1/3] feat: allow user to update password --- .../Services/AccountService+Email.swift | 25 ++++++- .../Sources/Services/AccountService.swift | 18 ++--- .../Sources/Services/AuthService.swift | 19 +++++ .../Sources/Views/SignedInView.swift | 52 +++++++------ .../Sources/Views/UpdatePassword.swift | 75 +++++++++++++++++++ 5 files changed, 153 insertions(+), 36 deletions(-) create mode 100644 FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/UpdatePassword.swift diff --git a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AccountService+Email.swift b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AccountService+Email.swift index d5a91ab2c3..d8972059a2 100644 --- a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AccountService+Email.swift +++ b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AccountService+Email.swift @@ -28,13 +28,36 @@ extension EmailPasswordOperationReauthentication { } } -class EmailPasswordDeleteUserOperation: DeleteUserOperation, +class EmailPasswordDeleteUserOperation: AuthenticatedOperation, EmailPasswordOperationReauthentication { let passwordPrompt: PasswordPromptCoordinator init(passwordPrompt: PasswordPromptCoordinator) { self.passwordPrompt = passwordPrompt } + + func callAsFunction(on user: User) async throws { + try await callAsFunction(on: user) { _ in + try await user.delete() + } + } +} + +class EmailPasswordUpdatePasswordOperation: AuthenticatedOperation, + EmailPasswordOperationReauthentication { + let passwordPrompt: PasswordPromptCoordinator + let newPassword: String + + init(passwordPrompt: PasswordPromptCoordinator, newPassword: String) { + self.passwordPrompt = passwordPrompt + self.newPassword = newPassword + } + + func callAsFunction(on user: User) async throws { + try await callAsFunction(on: user) { _ in + try await user.updatePassword(to: newPassword) + } + } } @MainActor diff --git a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AccountService.swift b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AccountService.swift index 5e48484248..bdc7468a91 100644 --- a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AccountService.swift +++ b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AccountService.swift @@ -19,27 +19,19 @@ enum AuthenticationToken { protocol AuthenticatedOperation { func callAsFunction(on user: User) async throws func reauthenticate() async throws -> AuthenticationToken - func performOperation(on user: User, with token: AuthenticationToken?) async throws } extension AuthenticatedOperation { - func callAsFunction(on user: User) async throws { + func callAsFunction(on _: User, + _ perform: (AuthenticationToken?) async throws -> Void) async throws { do { - try await performOperation(on: user, with: nil) + try await perform(nil) } catch let error as NSError where error.requiresReauthentication { let token = try await reauthenticate() - try await performOperation(on: user, with: token) + try await perform(token) } catch AuthServiceError.reauthenticationRequired { let token = try await reauthenticate() - try await performOperation(on: user, with: token) + try await perform(token) } } } - -protocol DeleteUserOperation: AuthenticatedOperation {} - -extension DeleteUserOperation { - func performOperation(on user: User, with _: AuthenticationToken? = nil) async throws { - try await user.delete() - } -} diff --git a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthService.swift b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthService.swift index c5ecdc1643..bf7de1bdf1 100644 --- a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthService.swift +++ b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthService.swift @@ -29,6 +29,7 @@ public enum AuthView { case authPicker case passwordRecovery case emailLink + case updatePassword } @MainActor @@ -217,6 +218,24 @@ public extension AuthService { throw error } } + + func updatePassword(to password: String) async throws { + do { + if let user = auth.currentUser { + let operation = EmailPasswordUpdatePasswordOperation( + passwordPrompt: passwordPrompt, + newPassword: password + ) + try await operation(on: user) + } + + } catch { + errorMessage = string.localizedErrorMessage( + for: error + ) + throw error + } + } } // MARK: - Email/Password Sign In diff --git a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/SignedInView.swift b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/SignedInView.swift index 6b27315eb1..bd6f48a28a 100644 --- a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/SignedInView.swift +++ b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/SignedInView.swift @@ -14,32 +14,40 @@ extension SignedInView: View { } public var body: some View { - VStack { - Text("Signed in") - Text("User: \(authService.currentUser?.email ?? "Unknown")") + if authService.authView == .updatePassword { + UpdatePasswordView() + } else { + VStack { + Text("Signed in") + Text("User: \(authService.currentUser?.email ?? "Unknown")") - if authService.currentUser?.isEmailVerified == false { - VerifyEmailView() - } - - Button("Sign out") { - Task { - do { - try await authService.signOut() - } catch {} + if authService.currentUser?.isEmailVerified == false { + VerifyEmailView() } - } - Divider() - Button("Delete account") { - Task { - do { - try await authService.deleteUser() - } catch {} + Divider() + Button("Update password") { + authService.authView = .updatePassword + } + Divider() + Button("Sign out") { + Task { + do { + try await authService.signOut() + } catch {} + } + } + Divider() + Button("Delete account") { + Task { + do { + try await authService.deleteUser() + } catch {} + } } + Text(authService.errorMessage).foregroundColor(.red) + }.sheet(isPresented: isShowingPasswordPrompt) { + PasswordPromptSheet(coordinator: authService.passwordPrompt) } - Text(authService.errorMessage).foregroundColor(.red) - }.sheet(isPresented: isShowingPasswordPrompt) { - PasswordPromptSheet(coordinator: authService.passwordPrompt) } } } diff --git a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/UpdatePassword.swift b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/UpdatePassword.swift new file mode 100644 index 0000000000..02336bc5b2 --- /dev/null +++ b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/UpdatePassword.swift @@ -0,0 +1,75 @@ +// +// UpdatePassword.swift +// FirebaseUI +// +// Created by Russell Wheatley on 24/04/2025. +// + +import SwiftUI + +private enum FocusableField: Hashable { + case password + case confirmPassword +} + +public struct UpdatePasswordView { + @Environment(AuthService.self) private var authService + @State private var password = "" + @State private var confirmPassword = "" + + @FocusState private var focus: FocusableField? + private var isValid: Bool { + !password.isEmpty && password == confirmPassword + } +} + +extension UpdatePasswordView: View { + public var body: some View { + VStack { + LabeledContent { + SecureField("Password", text: $password) + .focused($focus, equals: .password) + .submitLabel(.go) + } label: { + Image(systemName: "lock") + } + .padding(.vertical, 6) + .background(Divider(), alignment: .bottom) + .padding(.bottom, 8) + Divider() + + LabeledContent { + SecureField("Confirm password", text: $confirmPassword) + .focused($focus, equals: .confirmPassword) + .submitLabel(.go) + } label: { + Image(systemName: "lock") + } + .padding(.vertical, 6) + .background(Divider(), alignment: .bottom) + .padding(.bottom, 8) + Divider() + Button(action: { + Task { + try await authService.updatePassword(to: confirmPassword) + authService.authView = .authPicker + } + }) { + if authService.authenticationState != .authenticating { + Text("Update password") + .padding(.vertical, 8) + .frame(maxWidth: .infinity) + } else { + ProgressView() + .progressViewStyle(CircularProgressViewStyle(tint: .white)) + .padding(.vertical, 8) + .frame(maxWidth: .infinity) + } + } + .disabled(!isValid) + .padding([.top, .bottom], 8) + .frame(maxWidth: .infinity) + .buttonStyle(.borderedProminent) + } + } +} From 227d8e1f6e58cabf21ce5f1dde0b18365dfc3ce7 Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Fri, 25 Apr 2025 17:19:00 +0100 Subject: [PATCH 2/3] chore: allow password prompt for changing password --- .../Sources/Services/AccountService+Email.swift | 4 ++-- .../Sources/Services/AccountService.swift | 8 ++++---- .../Sources/Views/UpdatePassword.swift | 10 ++++++++++ 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AccountService+Email.swift b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AccountService+Email.swift index d8972059a2..19ce50654c 100644 --- a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AccountService+Email.swift +++ b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AccountService+Email.swift @@ -37,7 +37,7 @@ class EmailPasswordDeleteUserOperation: AuthenticatedOperation, } func callAsFunction(on user: User) async throws { - try await callAsFunction(on: user) { _ in + try await callAsFunction(on: user) { try await user.delete() } } @@ -54,7 +54,7 @@ class EmailPasswordUpdatePasswordOperation: AuthenticatedOperation, } func callAsFunction(on user: User) async throws { - try await callAsFunction(on: user) { _ in + try await callAsFunction(on: user) { try await user.updatePassword(to: newPassword) } } diff --git a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AccountService.swift b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AccountService.swift index bdc7468a91..ad0c34b900 100644 --- a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AccountService.swift +++ b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AccountService.swift @@ -23,15 +23,15 @@ protocol AuthenticatedOperation { extension AuthenticatedOperation { func callAsFunction(on _: User, - _ perform: (AuthenticationToken?) async throws -> Void) async throws { + _ performOperation: () async throws -> Void) async throws { do { - try await perform(nil) + try await performOperation() } catch let error as NSError where error.requiresReauthentication { let token = try await reauthenticate() - try await perform(token) + try await performOperation() } catch AuthServiceError.reauthenticationRequired { let token = try await reauthenticate() - try await perform(token) + try await performOperation() } } } diff --git a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/UpdatePassword.swift b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/UpdatePassword.swift index 02336bc5b2..41994574b0 100644 --- a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/UpdatePassword.swift +++ b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/UpdatePassword.swift @@ -12,6 +12,7 @@ private enum FocusableField: Hashable { case confirmPassword } +@MainActor public struct UpdatePasswordView { @Environment(AuthService.self) private var authService @State private var password = "" @@ -24,6 +25,13 @@ public struct UpdatePasswordView { } extension UpdatePasswordView: View { + private var isShowingPasswordPrompt: Binding { + Binding( + get: { authService.passwordPrompt.isPromptingPassword }, + set: { authService.passwordPrompt.isPromptingPassword = $0 } + ) + } + public var body: some View { VStack { LabeledContent { @@ -70,6 +78,8 @@ extension UpdatePasswordView: View { .padding([.top, .bottom], 8) .frame(maxWidth: .infinity) .buttonStyle(.borderedProminent) + }.sheet(isPresented: isShowingPasswordPrompt) { + PasswordPromptSheet(coordinator: authService.passwordPrompt) } } } From 842caa666f3cc726288e5d3700acd1c88ee16126 Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Fri, 25 Apr 2025 17:33:04 +0100 Subject: [PATCH 3/3] chore: rm obsolete code --- .../Sources/Views/UpdatePassword.swift | 21 ++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/UpdatePassword.swift b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/UpdatePassword.swift index 41994574b0..cb7925197e 100644 --- a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/UpdatePassword.swift +++ b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/UpdatePassword.swift @@ -44,6 +44,7 @@ extension UpdatePasswordView: View { .padding(.vertical, 6) .background(Divider(), alignment: .bottom) .padding(.bottom, 8) + Divider() LabeledContent { @@ -56,24 +57,20 @@ extension UpdatePasswordView: View { .padding(.vertical, 6) .background(Divider(), alignment: .bottom) .padding(.bottom, 8) + Divider() + Button(action: { Task { try await authService.updatePassword(to: confirmPassword) authService.authView = .authPicker } - }) { - if authService.authenticationState != .authenticating { - Text("Update password") - .padding(.vertical, 8) - .frame(maxWidth: .infinity) - } else { - ProgressView() - .progressViewStyle(CircularProgressViewStyle(tint: .white)) - .padding(.vertical, 8) - .frame(maxWidth: .infinity) - } - } + }, label: { + Text("Update password") + .padding(.vertical, 8) + .frame(maxWidth: .infinity) + + }) .disabled(!isValid) .padding([.top, .bottom], 8) .frame(maxWidth: .infinity)