Skip to content

feat: allow user to update password #1245

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
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) {
try await user.updatePassword(to: newPassword)
}
}
}

@MainActor
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
_ performOperation: () async throws -> Void) async throws {
do {
try await performOperation(on: user, with: nil)
try await performOperation()
} catch let error as NSError where error.requiresReauthentication {
let token = try await reauthenticate()
try await performOperation(on: user, with: token)
try await performOperation()
} catch AuthServiceError.reauthenticationRequired {
let token = try await reauthenticate()
try await performOperation(on: user, with: token)
try await performOperation()
}
}
}

protocol DeleteUserOperation: AuthenticatedOperation {}

extension DeleteUserOperation {
func performOperation(on user: User, with _: AuthenticationToken? = nil) async throws {
try await user.delete()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ public enum AuthView {
case authPicker
case passwordRecovery
case emailLink
case updatePassword
}

@MainActor
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
//
// UpdatePassword.swift
// FirebaseUI
//
// Created by Russell Wheatley on 24/04/2025.
//

import SwiftUI

private enum FocusableField: Hashable {
case password
case confirmPassword
}

@MainActor
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 {
private var isShowingPasswordPrompt: Binding<Bool> {
Binding(
get: { authService.passwordPrompt.isPromptingPassword },
set: { authService.passwordPrompt.isPromptingPassword = $0 }
)
}

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
}
}, label: {
Text("Update password")
.padding(.vertical, 8)
.frame(maxWidth: .infinity)

})
.disabled(!isValid)
.padding([.top, .bottom], 8)
.frame(maxWidth: .infinity)
.buttonStyle(.borderedProminent)
}.sheet(isPresented: isShowingPasswordPrompt) {
PasswordPromptSheet(coordinator: authService.passwordPrompt)
}
}
}