Skip to content

Verify before update email#14

Open
PaulGoldschmidt wants to merge 15 commits into
mainfrom
paul/verifyBeforeUpdateEmail
Open

Verify before update email#14
PaulGoldschmidt wants to merge 15 commits into
mainfrom
paul/verifyBeforeUpdateEmail

Conversation

@PaulGoldschmidt

@PaulGoldschmidt PaulGoldschmidt commented Jul 4, 2026

Copy link
Copy Markdown

Verify before update email

♻️ Current situation & Problem

With newer Firebase projects, Email Enumeration Protection is enabled by default, requiring Spezi firebase to call the function sendEmailVerification() instead of updateEmail(to:).
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.

⚙️ Release Notes

We need to let the user know that the email change is pending until the user opens the verification link. This is why this PR introduces a isEmailChangeNoticePresented view, in which the user is let known that we've sent a confirmation link to his address.

📚 Documentation

N/A

✅ Testing

Happy for your input here, @lukaskollmer!

Code of Conduct & Contributing Guidelines

By creating and submitting this pull request, you agree to follow our Code of Conduct and Contributing Guidelines:

Summary by CodeRabbit

  • New Features
    • Email address changes now use a verification flow: a confirmation link is sent before the update takes effect.
    • Added an in-app security alert and notice indicating the email will change after opening the link (sign-in may be required again).
    • Updated account views to show the pending user identifier while confirmation is pending.
  • Localization
    • Added new translated strings for the email verification and “pending identifier” messages (including English, German, Spanish, and Swedish).
  • Documentation
    • Documented the new pendingUserId account metadata.

@PaulGoldschmidt PaulGoldschmidt self-assigned this Jul 4, 2026
@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This PR changes email updates to use verification links, adds pending email-change state, and shows the pending value in account views and a new confirmation alert.

Changes

Pending Email Change Verification Flow

Layer / File(s) Summary
Pending user id display
Sources/SpeziAccount/AccountValue/Collections/AccountDetails.swift, Sources/SpeziAccount/AccountValue/Keys/PendingUserIdKey.swift, Sources/SpeziAccount/ViewModel/AccountDisplayModel.swift, Sources/SpeziAccount/Views/AccountOverview/AccountOverviewHeader.swift, Sources/SpeziAccount/Views/AccountOverview/NameOverview.swift, Sources/SpeziAccount/Resources/Localizable.xcstrings
Adds pendingUserId to account details, exposes it through the display model, and renders it in account overview UI with a localized pending-change message.
Email verification update flow
Sources/SpeziFirebaseAccount/Models/FirebaseAccountModel.swift, Sources/SpeziFirebaseAccount/FirebaseAccountService.swift
Stores pending email-change state, switches userId updates to sendEmailVerification(beforeUpdatingEmail:), and threads pending-email state through account detail refreshes.
Email change notice alert
Sources/SpeziFirebaseAccount/Views/FirebaseSecurityAlert.swift, Sources/SpeziFirebaseAccount/Resources/Localizable.xcstrings
Adds the email-change notice binding and alert presentation, plus localized title and message strings for the verification notice.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Suggested labels: enhancement
Suggested reviewers: PSchmiedmayer

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title matches the main change: email verification is now required before updating the account email.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch paul/verifyBeforeUpdateEmail

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@PSchmiedmayer PSchmiedmayer left a comment

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.

Thanks for looking into this @PaulGoldschmidt 🚀



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.

@coderabbitai coderabbitai Bot left a comment

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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Sources/SpeziFirebaseAccount/FirebaseAccountService.swift (1)

582-608: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Pending-change notice is skipped if a later step in the same update fails.

sendEmailVerification(beforeUpdatingEmail:) is a real, non-idempotent Firebase side effect (an email is actually sent). But presentEmailChangeNotice(...) is only called at Line 601, after the entire mapFirebaseAccountError block (Lines 582-599) completes successfully. If the email verification succeeds but a subsequent step in the same block — updatePassword(to:) or updateDisplayName — throws, the function exits before ever recording the pending state or calling supplyUserDetails. The user has already received a verification email, but the UI never shows the pending notice, and a retry could trigger another verification email to be sent.

Move the notice call immediately after the successful sendEmailVerification call, inside the mapFirebaseAccountError closure, so the pending state is tracked regardless of what happens afterward.

🔧 Proposed fix
         try await mapFirebaseAccountError {
             if modifications.modifiedDetails.contains(AccountKeys.userId) {
                 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)
+                // record the pending state immediately after the (non-idempotent) email was sent, so a later
+                // failure in this block (e.g., updatePassword) doesn't leave the UI unaware that a link was sent
+                firebaseModel.presentEmailChangeNotice(
+                    PendingEmailChange(accountId: currentUser.uid, emailAddress: modifications.modifiedDetails.userId)
+                )
             }

             if let password = modifications.modifiedDetails.password {
                 logger.debug("updatePassword(to:) for user.")
                 try await currentUser.updatePassword(to: password)
             }

             if let name = modifications.modifiedDetails.name {
                 try await updateDisplayName(of: currentUser, name)
             }
         }

-        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
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/SpeziFirebaseAccount/FirebaseAccountService.swift` around lines 582 -
608, The pending email-change notice is being recorded too late in
FirebaseAccountService.update, after the full mapFirebaseAccountError block
succeeds. Move the presentEmailChangeNotice(PendingEmailChange(...)) call into
the same branch as sendEmailVerification(beforeUpdatingEmail:) immediately after
that call, so the pending state is saved even if updatePassword(to:) or
updateDisplayName(of:_:) fails later. Keep the logic keyed off
modifications.modifiedDetails.contains(AccountKeys.userId) and use the existing
currentUser.uid and modifications.modifiedDetails.userId values.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@Sources/SpeziFirebaseAccount/FirebaseAccountService.swift`:
- Around line 582-608: The pending email-change notice is being recorded too
late in FirebaseAccountService.update, after the full mapFirebaseAccountError
block succeeds. Move the presentEmailChangeNotice(PendingEmailChange(...)) call
into the same branch as sendEmailVerification(beforeUpdatingEmail:) immediately
after that call, so the pending state is saved even if updatePassword(to:) or
updateDisplayName(of:_:) fails later. Keep the logic keyed off
modifications.modifiedDetails.contains(AccountKeys.userId) and use the existing
currentUser.uid and modifications.modifiedDetails.userId values.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: a854a420-b2ce-4c6b-a5bb-9cdb85c8d53b

📥 Commits

Reviewing files that changed from the base of the PR and between 8701403 and 603dc8a.

📒 Files selected for processing (9)
  • Sources/SpeziAccount/AccountValue/Collections/AccountDetails.swift
  • Sources/SpeziAccount/AccountValue/Keys/PendingUserIdKey.swift
  • Sources/SpeziAccount/Resources/Localizable.xcstrings
  • Sources/SpeziAccount/ViewModel/AccountDisplayModel.swift
  • Sources/SpeziAccount/Views/AccountOverview/AccountOverviewHeader.swift
  • Sources/SpeziAccount/Views/AccountOverview/NameOverview.swift
  • Sources/SpeziFirebaseAccount/FirebaseAccountService.swift
  • Sources/SpeziFirebaseAccount/Models/FirebaseAccountModel.swift
  • Sources/SpeziFirebaseAccount/Views/FirebaseSecurityAlert.swift
✅ Files skipped from review due to trivial changes (1)
  • Sources/SpeziAccount/AccountValue/Collections/AccountDetails.swift
🚧 Files skipped from review as they are similar to previous changes (1)
  • Sources/SpeziFirebaseAccount/Views/FirebaseSecurityAlert.swift

@PSchmiedmayer PSchmiedmayer left a comment

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.

Thanks @PaulGoldschmidt; I'll let @lukaskollmer give a review here and then happy to merge once all CI is passing 👍

@lukaskollmer

Copy link
Copy Markdown
Collaborator

i had a look at this, and while it works it does introduce the side effect of forcing an unconditional logout of the user. which ideally is something we'd like avoid, especially in MHC atm, since we don't yet sync the scheduler state with the cloud backend, meaning that the user changing their email (and being forced to log out and back in as part of the process) will reset their local study participation state, which is not good.

@PSchmiedmayer

Copy link
Copy Markdown
Contributor

Ok, an other argument that the state storage and restoration for Study/Scheduler is a high priority there

@lukaskollmer

Copy link
Copy Markdown
Collaborator

@PSchmiedmayer agreed

@lukaskollmer lukaskollmer mentioned this pull request Jul 13, 2026
1 task
lukaskollmer added a commit that referenced this pull request Jul 14, 2026
### ♻️ Current situation & Problem
@PSchmiedmayer had a stab at various package-wide improvements and fixes
in #17, such as addressing warnings, deprecations, etc. this work also
partly overlaps with some things addressed in #5 (which probably will
end up getting superseded by this PR).
this PR picks up these adjustments (which initially were developed
against #16), and carries them over into the current #20-based main
branch state of the codebase.

somewhat complete list of changes:
- most of what was covered by #17
- address compiler warnings where possible
- address deprecation warnings where possible
- note that this PR intentionally does **not** address the firebase
`updateEmail(to:)` deprecation, as there currently are some issues
around the behaviour of the new API. see also #14
- some testing fixes that were left out of #20
- now that the package's effective availability is iOS 18+, we can
remove a bunch of `if #available(iOS 18, *)` checks from all over the
codebase
- *todo*


### ⚙️ Release Notes
*todo*


### 📚 Documentation
*todo*


### ✅ Testing
*todo*


### Code of Conduct & Contributing Guidelines
By creating and submitting this pull request, you agree to follow our
[Code of
Conduct](https://github.com/SchmiedmayerLab/.github/blob/main/CODE_OF_CONDUCT.md)
and [Contributing
Guidelines](https://github.com/SchmiedmayerLab/.github/blob/main/CONTRIBUTING.md):
- [x] I agree to follow the [Code of
Conduct](https://github.com/SchmiedmayerLab/.github/blob/main/CODE_OF_CONDUCT.md)
and [Contributing
Guidelines](https://github.com/SchmiedmayerLab/.github/blob/main/CONTRIBUTING.md).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants