Skip to content
Open
Show file tree
Hide file tree
Changes from 5 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
19 changes: 16 additions & 3 deletions Sources/SpeziFirebaseAccount/FirebaseAccountService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -553,6 +553,12 @@ public final class FirebaseAccountService: AccountService { // swiftlint:disable
/// Otherwise, an alert will be presented to enter the password credential. Make sure that the ``securityAlert`` modifier is injected from the point your are calling
/// this method. This is automatically done with native SpeziAccount views.
///
/// - Note: Changing the userId (the account's email address) does not take effect immediately. Firebase sends a verification link to the
/// new email address and only applies the change once the user opens that link. Until then, the account details continue to report the
/// old email address. Once the change takes effect, Firebase revokes the user's tokens on all devices; the user will be signed out and
/// has to log in again with the new email address. An alert informing the user about the verification email is presented through the
/// ``securityAlert`` modifier.
///
/// - Throws: Throws an ``FirebaseAccountError`` if the operation fails. A ``FirebaseAccountError/notSignedIn`` is thrown if delete
/// is called when no user was logged in.
public func updateAccountDetails(_ modifications: AccountModifications) async throws {
Expand All @@ -574,9 +580,11 @@ public final class FirebaseAccountService: AccountService { // swiftlint:disable

try await mapFirebaseAccountError {
if modifications.modifiedDetails.contains(AccountKeys.userId) {
logger.debug("updateEmail(to:) for user.")
try await currentUser.updateEmail(to: modifications.modifiedDetails.userId)
try await currentUser.reload()
logger.debug("sendEmailVerification(beforeUpdatingEmail:) for user.")
// `updateEmail(to:)` is deprecated and fails when email enumeration protection is enabled (the default).
// This call only sends a verification link to the new address; the email is updated once the user opens it,
// at which point Firebase revokes the user's tokens and the user has to sign in again.
try await currentUser.sendEmailVerification(beforeUpdatingEmail: modifications.modifiedDetails.userId)
}

if let password = modifications.modifiedDetails.password {
Expand All @@ -597,6 +605,11 @@ public final class FirebaseAccountService: AccountService { // swiftlint:disable

// None of the above requests will trigger our state change listener, therefore, we just call it manually.
await supplyUserDetails(for: currentUser)

if modifications.modifiedDetails.contains(AccountKeys.userId) {
// the email change is pending until the user opens the verification link; make sure they know about it
firebaseModel.presentEmailChangeNotice(for: modifications.modifiedDetails.userId)
}
}

private func reauthenticateUser(user: User) async throws -> ReauthenticationOperation {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,17 @@ class FirebaseAccountModel {
var isPresentingReauthentication = false
var reauthenticationContext: ReauthenticationContext?

var isPresentingEmailChangeNotice = false
private(set) var pendingEmailAddress: String?

nonisolated init() {}


func presentEmailChangeNotice(for newEmail: String) {
pendingEmailAddress = newEmail

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably show something in the main UI where we show the email that this is still in process. Probably can't and shouldn't persist it across app launches, but at least while the user goes back in the UI for the same change, we can display this. If we would persist it across launches, then we need to pull that information from Firebase (which I don't think we can), so keeping it as local state might be a good middle ground.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agree with the persistence part here, I will try to find a good way to show this at least in the session lifecycle.

isPresentingEmailChangeNotice = true
}

func reauthenticateUser(userId: String) async -> ReauthenticationResult {
defer {
reauthenticationContext = nil
Expand Down
32 changes: 32 additions & 0 deletions Sources/SpeziFirebaseAccount/Resources/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -512,6 +512,38 @@
}
}
}
},
"Verify Your New Email Address" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Bestätige deine neue E-Mail Adresse"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Verify Your New Email Address"
}
}
}
},
"We sent a confirmation link to %@. Your email address will change once you open the link. You may need to sign in again." : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Wir haben einen Bestätigungslink an %@ gesendet. Deine E-Mail Adresse ändert sich, sobald du den Link öffnest. Danach musst du dich möglicherweise erneut anmelden."
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "We sent a confirmation link to %@. Your email address will change once you open the link. You may need to sign in again."
}
}
}
}
},
"version" : "1.0"
Expand Down
23 changes: 23 additions & 0 deletions Sources/SpeziFirebaseAccount/Views/FirebaseSecurityAlert.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ import SwiftUI
///
/// The alert will request the user's password to authorize security-sensitive operations like account deletion or change of
/// sensitive account details.
///
/// It additionally presents informational alerts related to security-sensitive operations, like the notice that a
/// verification link was sent to the new email address after the user requested to change their email address.
public struct FirebaseSecurityAlert: ViewModifier {
@Environment(FirebaseAccountModel.self)
private var firebaseModel: FirebaseAccountModel
Expand All @@ -39,6 +42,14 @@ public struct FirebaseSecurityAlert: ViewModifier {
firebaseModel.reauthenticationContext
}

@MainActor private var isEmailChangeNoticePresented: Binding<Bool> {
Binding {
firebaseModel.isPresentingEmailChangeNotice && isActive
} set: { newValue in
firebaseModel.isPresentingEmailChangeNotice = newValue
}
}

nonisolated init() {}


Expand Down Expand Up @@ -81,6 +92,18 @@ public struct FirebaseSecurityAlert: ViewModifier {
} message: { context in
Text("Please enter your password for \(context.userId).")
}
.alert(
Text("Verify Your New Email Address", bundle: .module),
isPresented: isEmailChangeNoticePresented,
presenting: firebaseModel.pendingEmailAddress
) { _ in
// the system provides a default OK button
} message: { email in
Text(
"We sent a confirmation link to \(email). Your email address will change once you open the link. You may need to sign in again.",
bundle: .module
)
}
}
}

Expand Down
Loading