Skip to content

fix(auth): forget email-link reauthentication when it is cancelled or no user is signed in - #1431

Open
demolaf wants to merge 2 commits into
fix/email-link-reauth-cold-launchfrom
fix/stale-email-link-reauth
Open

demolaf wants to merge 2 commits into
fix/email-link-reauth-cold-launchfrom
fix/stale-email-link-reauth

Conversation

@demolaf

@demolaf demolaf commented Nov 5, 2026 •

Copy link
Copy Markdown
Member

Clears stored email-link reauthentication state when it is abandoned, so later sign-in links are handled as sign-in again. Stacked on #1430.

Changes

  • Cancelling or closing EmailLinkReauthView forgets the stored reauth email and flag, including when the close lands while the email is still sending. Before, the email-link screen skipped sign-in links and handleSignInLink(url:) routed them to reauthentication.
  • Swiping the email-link reauth sheet away now cancels the coordinator, so the caller waiting on reauthentication is not left hanging. A request made while the sheet closes is left alone.
  • Reauth state left with no signed-in (non-anonymous) user is ignored, not cleared, and the link is treated as sign-in. The check reads auth.currentUser, which is set before currentUser after a cold launch.
  • Integration tests cover cancelling and a sign-in link opened with leftover reauth state.

Preview

Recorded on an erased simulator in the e2e example app (--test-view-enabled), which signs out through Auth on relaunch, so the session ends without signOut(). Delete Account was patched locally to require a recent login, since the Auth emulator never does. Steps: sign in with an email link, Delete Account, cancel reauthentication, relaunch, then open a sign-in link for another user.

Before (#1430, 518defa19e)

Expected: when a user wants to sign in with an email link after a cancelled reauthentication, opening the link should sign them in.

pr1431-before.mp4

After (#1431, 98cc6ed8e0)

pr1431-after.mp4

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request introduces state cleanup for email-link reauthentication when the process is cancelled, dismissed, or when there is no longer a signed-in user. It adds cancellation handlers in AuthService and the corresponding SwiftUI views, along with integration tests to verify the new behavior. The feedback suggests renaming the helper method hasPendingEmailLinkReauth() to checkAndCleanPendingEmailLinkReauth() (and updating its callsites) to better reflect its side-effects, adhering to the Command-Query Separation principle and Swift API Design Guidelines.

Comment on lines +757 to +764
private func hasPendingEmailLinkReauth() -> Bool {
guard isReauthenticating else { return false }
guard let user = auth.currentUser, !user.isAnonymous else {
cancelEmailLinkReauthentication()
return false
}
return true
}

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.

medium

The method hasPendingEmailLinkReauth() violates the Command-Query Separation (CQS) principle and the Swift API Design Guidelines. It is named as a Boolean assertion (has...), which implies it is a pure query with no side-effects. However, it contains a side-effect that modifies state by calling cancelEmailLinkReauthentication() when there is no signed-in user.

To adhere to the Swift API Design Guidelines, methods with side-effects should be named using imperative verbs. Consider renaming this method to checkAndCleanPendingEmailLinkReauth() to explicitly signal that it performs cleanup, or separate the query and command logic.

Suggested change
private func hasPendingEmailLinkReauth() -> Bool {
guard isReauthenticating else { return false }
guard let user = auth.currentUser, !user.isAnonymous else {
cancelEmailLinkReauthentication()
return false
}
return true
}
private func checkAndCleanPendingEmailLinkReauth() -> Bool {
guard isReauthenticating else { return false }
guard let user = auth.currentUser, !user.isAnonymous else {
cancelEmailLinkReauthentication()
return false
}
return true
}
References
  1. Swift API Design Guidelines state that Boolean methods and properties should read as assertions about the receiver when the call has no side-effects. Methods with side-effects should be named using imperative verbs. (link)

func handleSignInLink(url: URL) async throws {
guard url != emailLinkInFlight else { return }
if isReauthenticating {
if hasPendingEmailLinkReauth() {

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.

medium

Update this callsite to use the renamed checkAndCleanPendingEmailLinkReauth() method to align with the Command-Query Separation principle.

Suggested change
if hasPendingEmailLinkReauth() {
if checkAndCleanPendingEmailLinkReauth() {

/// that shows its own screen through `withEmailLinkSignIn(onTap:)` handles links there.
internal func presentEmailLinkScreen(for url: URL) {
guard emailLinkSignInEnabled, emailLinkUsesLibraryScreen, !isReauthenticating,
guard emailLinkSignInEnabled, emailLinkUsesLibraryScreen, !hasPendingEmailLinkReauth(),

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.

medium

Update this callsite to use the renamed checkAndCleanPendingEmailLinkReauth() method to align with the Command-Query Separation principle.

Suggested change
guard emailLinkSignInEnabled, emailLinkUsesLibraryScreen, !hasPendingEmailLinkReauth(),
guard emailLinkSignInEnabled, emailLinkUsesLibraryScreen, !checkAndCleanPendingEmailLinkReauth(),

/// other links, or when that view is showing to handle the link itself.
internal func finishEmailLinkReauthentication(with url: URL) async throws {
guard isReauthenticating, !isShowingEmailLinkReauth, currentUser != nil else { return }
guard !isShowingEmailLinkReauth, hasPendingEmailLinkReauth() else { return }

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.

medium

Update this callsite to use the renamed checkAndCleanPendingEmailLinkReauth() method to align with the Command-Query Separation principle.

Suggested change
guard !isShowingEmailLinkReauth, hasPendingEmailLinkReauth() else { return }
guard !isShowingEmailLinkReauth, checkAndCleanPendingEmailLinkReauth() else { return }

@demolaf
demolaf force-pushed the fix/stale-email-link-reauth branch from dd53050 to c9bbb50 Compare November 9, 2026 15:49
@demolaf
demolaf added this pull request to stack #1433 November 9, 2026 15:50
@demolaf
demolaf force-pushed the fix/stale-email-link-reauth branch from c9bbb50 to ce97892 Compare November 10, 2026 14:30
… no user is signed in

Cancelling or dismissing EmailLinkReauthView left the stored reauthentication state set, so later sign-in links were skipped by the email-link screen and routed to reauthentication. Clear that state when the view closes, cancel the coordinator when the sheet is swiped away, and drop leftover state when no user is signed in.
Forget the reauthentication a send stores after the view has closed, only cancel the coordinator on swipe-dismiss when no new request has started, and ignore leftover reauthentication without a signed-in user instead of clearing it, so a user that can't be read yet doesn't lose a pending one.
@demolaf
demolaf force-pushed the fix/stale-email-link-reauth branch from d9d1cd9 to 98cc6ed Compare November 10, 2026 15:56
@demolaf
demolaf marked this pull request as ready for review November 10, 2026 15:56
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.

1 participant