From 8d39534f93e575b0cb6e9bb88cf8629879c8b902 Mon Sep 17 00:00:00 2001 From: Paul Goldschmidt Date: Sat, 4 Jul 2026 15:49:23 +0200 Subject: [PATCH 01/15] add isEmailChangeNoticePresented and alert to Sources/SpeziFirebaseAccount/Views/FirebaseSecurityAlert.swift --- .../Views/FirebaseSecurityAlert.swift | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/Sources/SpeziFirebaseAccount/Views/FirebaseSecurityAlert.swift b/Sources/SpeziFirebaseAccount/Views/FirebaseSecurityAlert.swift index b37afa98b..e0f3b58d1 100644 --- a/Sources/SpeziFirebaseAccount/Views/FirebaseSecurityAlert.swift +++ b/Sources/SpeziFirebaseAccount/Views/FirebaseSecurityAlert.swift @@ -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 @@ -39,6 +42,14 @@ public struct FirebaseSecurityAlert: ViewModifier { firebaseModel.reauthenticationContext } + @MainActor private var isEmailChangeNoticePresented: Binding { + Binding { + firebaseModel.isPresentingEmailChangeNotice && isActive + } set: { newValue in + firebaseModel.isPresentingEmailChangeNotice = newValue + } + } + nonisolated init() {} @@ -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 + ) + } } } From 8532005137387ad706402b4a80843dde81001a3a Mon Sep 17 00:00:00 2001 From: Paul Goldschmidt Date: Sat, 4 Jul 2026 15:49:34 +0200 Subject: [PATCH 02/15] add presentEmailChangeNotice func --- .../Models/FirebaseAccountModel.swift | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Sources/SpeziFirebaseAccount/Models/FirebaseAccountModel.swift b/Sources/SpeziFirebaseAccount/Models/FirebaseAccountModel.swift index 422474055..f68ea1b85 100644 --- a/Sources/SpeziFirebaseAccount/Models/FirebaseAccountModel.swift +++ b/Sources/SpeziFirebaseAccount/Models/FirebaseAccountModel.swift @@ -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 + isPresentingEmailChangeNotice = true + } + func reauthenticateUser(userId: String) async -> ReauthenticationResult { defer { reauthenticationContext = nil From adc3c199d4ad9ca17b671a5c0e917ac618a9f22f Mon Sep 17 00:00:00 2001 From: Paul Goldschmidt Date: Sat, 4 Jul 2026 15:49:45 +0200 Subject: [PATCH 03/15] switch to sendEmailVerification --- .../FirebaseAccountService.swift | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/Sources/SpeziFirebaseAccount/FirebaseAccountService.swift b/Sources/SpeziFirebaseAccount/FirebaseAccountService.swift index 439c6b92b..b8cfef514 100644 --- a/Sources/SpeziFirebaseAccount/FirebaseAccountService.swift +++ b/Sources/SpeziFirebaseAccount/FirebaseAccountService.swift @@ -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 { @@ -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 { @@ -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 + await firebaseModel.presentEmailChangeNotice(for: modifications.modifiedDetails.userId) + } } private func reauthenticateUser(user: User) async throws -> ReauthenticationOperation { From 870140330cf7e3757435e10841e1e8bdbc3fbbae Mon Sep 17 00:00:00 2001 From: Paul Goldschmidt Date: Sat, 4 Jul 2026 15:49:58 +0200 Subject: [PATCH 04/15] add localizations for sendEmailVerification --- .../Resources/Localizable.xcstrings | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/Sources/SpeziFirebaseAccount/Resources/Localizable.xcstrings b/Sources/SpeziFirebaseAccount/Resources/Localizable.xcstrings index 8ce50e216..94d9d9d64 100644 --- a/Sources/SpeziFirebaseAccount/Resources/Localizable.xcstrings +++ b/Sources/SpeziFirebaseAccount/Resources/Localizable.xcstrings @@ -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" From 6a1027b89075718e8c5c1b600ee86fa77785973c Mon Sep 17 00:00:00 2001 From: Paul Goldschmidt Date: Sat, 4 Jul 2026 15:57:37 +0200 Subject: [PATCH 05/15] no await for you --- Sources/SpeziFirebaseAccount/FirebaseAccountService.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/SpeziFirebaseAccount/FirebaseAccountService.swift b/Sources/SpeziFirebaseAccount/FirebaseAccountService.swift index b8cfef514..56e93ff8b 100644 --- a/Sources/SpeziFirebaseAccount/FirebaseAccountService.swift +++ b/Sources/SpeziFirebaseAccount/FirebaseAccountService.swift @@ -608,7 +608,7 @@ public final class FirebaseAccountService: AccountService { // swiftlint:disable if modifications.modifiedDetails.contains(AccountKeys.userId) { // the email change is pending until the user opens the verification link; make sure they know about it - await firebaseModel.presentEmailChangeNotice(for: modifications.modifiedDetails.userId) + firebaseModel.presentEmailChangeNotice(for: modifications.modifiedDetails.userId) } } From abd14be126b455026eec7dad3b0e0c13dbf91cc7 Mon Sep 17 00:00:00 2001 From: Paul Goldschmidt Date: Sun, 5 Jul 2026 19:25:39 +0200 Subject: [PATCH 06/15] Create PendingUserIdKey.swift --- .../AccountValue/Keys/PendingUserIdKey.swift | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 Sources/SpeziAccount/AccountValue/Keys/PendingUserIdKey.swift diff --git a/Sources/SpeziAccount/AccountValue/Keys/PendingUserIdKey.swift b/Sources/SpeziAccount/AccountValue/Keys/PendingUserIdKey.swift new file mode 100644 index 000000000..129e27cec --- /dev/null +++ b/Sources/SpeziAccount/AccountValue/Keys/PendingUserIdKey.swift @@ -0,0 +1,34 @@ +// +// This source file is part of the Stanford Spezi open-source project +// +// SPDX-FileCopyrightText: 2026 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import SpeziFoundation + + +extension AccountDetails { + private struct PendingUserIdKey: KnowledgeSource { + typealias Anchor = AccountAnchor + typealias Value = String + } + + /// A new user identifier that was requested but is still pending confirmation. + /// + /// An account service can set this property to indicate that a change of the ``userId`` was requested but did not take effect yet + /// (e.g., the user still needs to open a verification link that was sent to their new email address). + /// Views like `AccountOverview` display this information alongside the current user identifier. + /// + /// - Note: This is transient, in-memory state supplied by the account service with the rest of the account details. + /// It is generally not persisted and, therefore, might not be available across application launches. + public var pendingUserId: String? { + get { + self[PendingUserIdKey.self] + } + set { + self[PendingUserIdKey.self] = newValue + } + } +} From ddd0b8dbc4dcc7fe9a8bf857438604b4d4da9ed7 Mon Sep 17 00:00:00 2001 From: Paul Goldschmidt Date: Sun, 5 Jul 2026 19:25:51 +0200 Subject: [PATCH 07/15] add pendingUserId to account details --- .../SpeziAccount/AccountValue/Collections/AccountDetails.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/SpeziAccount/AccountValue/Collections/AccountDetails.swift b/Sources/SpeziAccount/AccountValue/Collections/AccountDetails.swift index 54725e296..1d00a81c7 100644 --- a/Sources/SpeziAccount/AccountValue/Collections/AccountDetails.swift +++ b/Sources/SpeziAccount/AccountValue/Collections/AccountDetails.swift @@ -123,6 +123,7 @@ private struct CopyKeyVisitor: AccountKeyVisitor { /// - ``isNewUser`` /// - ``isIncomplete`` /// - ``isVerified`` +/// - ``pendingUserId`` /// - ``accountServiceConfiguration`` /// - ``userIdType`` /// From 5ef39ef93e707e2772fcd4b6923b8f417212f4c0 Mon Sep 17 00:00:00 2001 From: Paul Goldschmidt Date: Sun, 5 Jul 2026 19:26:04 +0200 Subject: [PATCH 08/15] add pendingUserId to account display model --- Sources/SpeziAccount/ViewModel/AccountDisplayModel.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Sources/SpeziAccount/ViewModel/AccountDisplayModel.swift b/Sources/SpeziAccount/ViewModel/AccountDisplayModel.swift index e610f6a5d..48f11c06d 100644 --- a/Sources/SpeziAccount/ViewModel/AccountDisplayModel.swift +++ b/Sources/SpeziAccount/ViewModel/AccountDisplayModel.swift @@ -28,6 +28,10 @@ struct AccountDisplayModel { } } + var pendingUserId: String? { + accountDetails.pendingUserId + } + var accountSubheadline: String? { if accountDetails.name != nil { if !accountDetails.contains(AccountKeys.userId) { From 3bcb673ece09ba20971eb1036549ae2a93271b87 Mon Sep 17 00:00:00 2001 From: Paul Goldschmidt Date: Sun, 5 Jul 2026 19:26:21 +0200 Subject: [PATCH 09/15] show in account overview header --- .../Views/AccountOverview/AccountOverviewHeader.swift | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Sources/SpeziAccount/Views/AccountOverview/AccountOverviewHeader.swift b/Sources/SpeziAccount/Views/AccountOverview/AccountOverviewHeader.swift index 615efbf5b..324c3b1d3 100644 --- a/Sources/SpeziAccount/Views/AccountOverview/AccountOverviewHeader.swift +++ b/Sources/SpeziAccount/Views/AccountOverview/AccountOverviewHeader.swift @@ -45,6 +45,13 @@ struct AccountOverviewHeader: View { .font(.subheadline) .foregroundColor(.secondary) } + + if let pendingUserId = model.pendingUserId { + Text("USER_ID_CHANGE_PENDING \(pendingUserId)", bundle: .module) + .font(.footnote) + .foregroundColor(.secondary) + .multilineTextAlignment(.center) + } } .accessibilityElement(children: .combine) .frame(maxWidth: .infinity, alignment: .center) From 90b60099bdd8b692260dfcefcd8e4f8d88e42e19 Mon Sep 17 00:00:00 2001 From: Paul Goldschmidt Date: Sun, 5 Jul 2026 19:26:43 +0200 Subject: [PATCH 10/15] show in footer of name overview view --- .../SpeziAccount/Views/AccountOverview/NameOverview.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Sources/SpeziAccount/Views/AccountOverview/NameOverview.swift b/Sources/SpeziAccount/Views/AccountOverview/NameOverview.swift index fbe3e4f5d..4f3c77bfd 100644 --- a/Sources/SpeziAccount/Views/AccountOverview/NameOverview.swift +++ b/Sources/SpeziAccount/Views/AccountOverview/NameOverview.swift @@ -51,6 +51,11 @@ struct NameOverview: View { let title = AccountKeys.name.category.categoryTitle { Text(title) } + } footer: { + if wrapper.accountKey == AccountKeys.userId, + let pendingUserId = accountDetails.pendingUserId { + Text("USER_ID_CHANGE_PENDING \(pendingUserId)", bundle: .module) + } } } } From ddcae788e2a2cadb1f3dacfa1cd53f8916e7135a Mon Sep 17 00:00:00 2001 From: Paul Goldschmidt Date: Sun, 5 Jul 2026 19:26:55 +0200 Subject: [PATCH 11/15] add clearPendingEmailChange --- .../Models/FirebaseAccountModel.swift | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/Sources/SpeziFirebaseAccount/Models/FirebaseAccountModel.swift b/Sources/SpeziFirebaseAccount/Models/FirebaseAccountModel.swift index f68ea1b85..85b1cd8a2 100644 --- a/Sources/SpeziFirebaseAccount/Models/FirebaseAccountModel.swift +++ b/Sources/SpeziFirebaseAccount/Models/FirebaseAccountModel.swift @@ -11,6 +11,15 @@ import Observation import SwiftUI +/// An email address change that was requested but is still pending verification through the confirmation link. +struct PendingEmailChange: Equatable, Sendable { + /// The Firebase user identifier (`uid`) of the account the change was requested for. + let accountId: String + /// The new email address that is pending verification. + let emailAddress: String +} + + @Observable @MainActor class FirebaseAccountModel { @@ -20,16 +29,23 @@ class FirebaseAccountModel { var reauthenticationContext: ReauthenticationContext? var isPresentingEmailChangeNotice = false - private(set) var pendingEmailAddress: String? + private(set) var pendingEmailChange: PendingEmailChange? nonisolated init() {} - func presentEmailChangeNotice(for newEmail: String) { - pendingEmailAddress = newEmail + func presentEmailChangeNotice(_ change: PendingEmailChange) { + pendingEmailChange = change isPresentingEmailChangeNotice = true } + func clearPendingEmailChange(for accountId: String) { + guard pendingEmailChange?.accountId == accountId else { + return + } + pendingEmailChange = nil + } + func reauthenticateUser(userId: String) async -> ReauthenticationResult { defer { reauthenticationContext = nil From 97ba96d1f1dcb67ff3e17a8a5d75f6649a6e5118 Mon Sep 17 00:00:00 2001 From: Paul Goldschmidt Date: Sun, 5 Jul 2026 19:27:08 +0200 Subject: [PATCH 12/15] adopt namechange --- .../SpeziFirebaseAccount/Views/FirebaseSecurityAlert.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Sources/SpeziFirebaseAccount/Views/FirebaseSecurityAlert.swift b/Sources/SpeziFirebaseAccount/Views/FirebaseSecurityAlert.swift index e0f3b58d1..d06b7cd14 100644 --- a/Sources/SpeziFirebaseAccount/Views/FirebaseSecurityAlert.swift +++ b/Sources/SpeziFirebaseAccount/Views/FirebaseSecurityAlert.swift @@ -95,10 +95,11 @@ public struct FirebaseSecurityAlert: ViewModifier { .alert( Text("Verify Your New Email Address", bundle: .module), isPresented: isEmailChangeNoticePresented, - presenting: firebaseModel.pendingEmailAddress + presenting: firebaseModel.pendingEmailChange ) { _ in // the system provides a default OK button - } message: { email in + } message: { change in + let email = change.emailAddress 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 From 89ede958ef80d9381df25d027b188d7ff5573edf Mon Sep 17 00:00:00 2001 From: Paul Goldschmidt Date: Sun, 5 Jul 2026 19:27:14 +0200 Subject: [PATCH 13/15] Update Localizable.xcstrings --- .../Resources/Localizable.xcstrings | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/Sources/SpeziAccount/Resources/Localizable.xcstrings b/Sources/SpeziAccount/Resources/Localizable.xcstrings index 8c6495455..67bd939c5 100644 --- a/Sources/SpeziAccount/Resources/Localizable.xcstrings +++ b/Sources/SpeziAccount/Resources/Localizable.xcstrings @@ -2615,6 +2615,34 @@ } } }, + "USER_ID_CHANGE_PENDING %@" : { + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Die Änderung zu %@ muss noch bestätigt werden." + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "The change to %@ is pending confirmation." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "El cambio a %@ está pendiente de confirmación." + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ändringen till %@ väntar på bekräftelse." + } + } + } + }, "USER_ID_EMAIL" : { "comment" : "The key for the localized string resource that represents the user id type \"email address\".", "isCommentAutoGenerated" : true, From 603dc8aa6ec4a9116a9409e7db278e4c2e2f823b Mon Sep 17 00:00:00 2001 From: Paul Goldschmidt Date: Sun, 5 Jul 2026 19:27:19 +0200 Subject: [PATCH 14/15] docs --- .../FirebaseAccountService.swift | 41 ++++++++++++++----- 1 file changed, 31 insertions(+), 10 deletions(-) diff --git a/Sources/SpeziFirebaseAccount/FirebaseAccountService.swift b/Sources/SpeziFirebaseAccount/FirebaseAccountService.swift index 56e93ff8b..dc7d014ce 100644 --- a/Sources/SpeziFirebaseAccount/FirebaseAccountService.swift +++ b/Sources/SpeziFirebaseAccount/FirebaseAccountService.swift @@ -555,9 +555,10 @@ public final class FirebaseAccountService: AccountService { // swiftlint:disable /// /// - 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. + /// old email address and expose the new one via the `AccountDetails/pendingUserId` property, which is displayed by views like + /// `AccountOverview`. The pending state is kept in memory only and is not persisted across app launches. 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. @@ -597,6 +598,14 @@ public final class FirebaseAccountService: AccountService { // swiftlint:disable } } + if modifications.modifiedDetails.contains(AccountKeys.userId) { + // the email change is pending until the user opens the verification link; present a notice and track the + // pending state so that views can display it alongside the current email address (see `withPendingEmailChange`) + firebaseModel.presentEmailChangeNotice( + PendingEmailChange(accountId: currentUser.uid, emailAddress: modifications.modifiedDetails.userId) + ) + } + var externalModifications = modifications externalModifications.removeModifications(for: Self.supportedAccountKeys) if !externalModifications.isEmpty { @@ -605,11 +614,6 @@ 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 { @@ -758,7 +762,7 @@ extension FirebaseAccountService { let details = buildUser(user, isNewUser: consideredNewUser, mergeWith: details) logger.debug("Update user details due to updates in the externally stored account details.") - account.supplyUserDetails(details) + account.supplyUserDetails(withPendingEmailChange(details, for: user)) } } @@ -1073,7 +1077,24 @@ extension FirebaseAccountService { let isNewUser = isNewUser ?? account.details?.isNewUser ?? false let details = await buildUserQueryingStorageProvider(user: user, isNewUser: isNewUser) logger.debug("Notifying SpeziAccount with updated user details.") - account.supplyUserDetails(details) + account.supplyUserDetails(withPendingEmailChange(details, for: user)) + } + + /// Attach a pending email address change to the account details, if one exists for the user. + /// + /// The pending state is kept in memory only. It is cleared once we observe that the change took effect + /// (the user's email address matches the previously requested one). + private func withPendingEmailChange(_ details: AccountDetails, for user: User) -> AccountDetails { + guard let change = firebaseModel.pendingEmailChange, change.accountId == user.uid else { + return details + } + guard change.emailAddress != user.email else { + firebaseModel.clearPendingEmailChange(for: user.uid) + return details + } + var details = details + details.pendingUserId = change.emailAddress + return details } func notifyUserRemoval() { From 7e6b2a6bd837029f9c5b6adfeedb7b3c62ec8177 Mon Sep 17 00:00:00 2001 From: Paul Goldschmidt Date: Sun, 5 Jul 2026 19:32:51 +0200 Subject: [PATCH 15/15] fix linter --- .../Views/AccountOverview/NameOverview.swift | 73 ++++++++++--------- 1 file changed, 39 insertions(+), 34 deletions(-) diff --git a/Sources/SpeziAccount/Views/AccountOverview/NameOverview.swift b/Sources/SpeziAccount/Views/AccountOverview/NameOverview.swift index 4f3c77bfd..e22ceadbb 100644 --- a/Sources/SpeziAccount/Views/AccountOverview/NameOverview.swift +++ b/Sources/SpeziAccount/Views/AccountOverview/NameOverview.swift @@ -24,39 +24,7 @@ struct NameOverview: View { .map { ForEachAccountKeyWrapper($0) } ForEach(forEachWrappers, id: \.id) { wrapper in - Section { - NavigationLink { - wrapper.accountKey.singleEditView(model: model, details: accountDetails) - .anyModifiers(account.securityRelatedModifiers.map { $0.anyViewModifier }) - } label: { - if let view = wrapper.accountKey.dataDisplayViewWithCurrentStoredValue(from: accountDetails) { - view - } else { - let name = wrapper.accountKey == AccountKeys.userId - ? accountDetails.userIdType.localizedStringResource - : wrapper.accountKey.name - - HStack { - Text(name) - .accessibilityHidden(true) - Spacer() - Text("VALUE_ADD \(name)", bundle: .module) - .foregroundColor(.secondary) - } - .accessibilityElement(children: .combine) - } - } - } header: { - if wrapper.accountKey == AccountKeys.name, - let title = AccountKeys.name.category.categoryTitle { - Text(title) - } - } footer: { - if wrapper.accountKey == AccountKeys.userId, - let pendingUserId = accountDetails.pendingUserId { - Text("USER_ID_CHANGE_PENDING \(pendingUserId)", bundle: .module) - } - } + section(for: wrapper.accountKey) } } .navigationTitle(model.accountIdentifierLabel(configuration: account.configuration, accountDetails)) @@ -67,11 +35,48 @@ struct NameOverview: View { .environment(\.accountViewType, .overview(mode: .display)) } - init(model: AccountOverviewFormViewModel, details accountDetails: AccountDetails) { self.model = model self.accountDetails = accountDetails } + + + @ViewBuilder + private func section(for accountKey: any AccountKey.Type) -> some View { + Section { + NavigationLink { + accountKey.singleEditView(model: model, details: accountDetails) + .anyModifiers(account.securityRelatedModifiers.map { $0.anyViewModifier }) + } label: { + if let view = accountKey.dataDisplayViewWithCurrentStoredValue(from: accountDetails) { + view + } else { + let name = accountKey == AccountKeys.userId + ? accountDetails.userIdType.localizedStringResource + : accountKey.name + + HStack { + Text(name) + .accessibilityHidden(true) + Spacer() + Text("VALUE_ADD \(name)", bundle: .module) + .foregroundColor(.secondary) + } + .accessibilityElement(children: .combine) + } + } + } header: { + if accountKey == AccountKeys.name, + let title = AccountKeys.name.category.categoryTitle { + Text(title) + } + } footer: { + if accountKey == AccountKeys.userId, + let pendingUserId = accountDetails.pendingUserId { + Text("USER_ID_CHANGE_PENDING \(pendingUserId)", bundle: .module) + } + } + } }