Skip to content

feat(auth): add signedInContent slot to AuthPickerView - #1417

Draft
demolaf wants to merge 1 commit into
auth/phone-content-slotsfrom
auth/signed-in-content
Draft

demolaf wants to merge 1 commit into
auth/phone-content-slotsfrom
auth/signed-in-content

Conversation

@demolaf

@demolaf demolaf commented Oct 21, 2026 •

Copy link
Copy Markdown
Member

Adds .signedInContent { state in ... } to AuthPickerView, so apps can replace the screen the authentication sheet shows to signed-in users while the library keeps signing out, deleting and reauthenticating. Based on #1412.

Changes

  • The content receives a SignedInContentState: closures for sign out, delete account, verify email and navigation, plus user, isEmailVerified, isMFAEnabled, isLoading and a localized errorMessage.
  • Delete still asks for confirmation and reauthenticates when Firebase requires it, under custom content too. Swiping away a reauth sheet now counts as a cancel instead of leaving the operation hanging.
  • DefaultSignedInContent(state:) holds the stock markup and is the default; SignedInView keeps its type, renders it unchanged and gains a public init.
  • The slot carries through pickerContent(_:) and pickerDestination(_:) in either order. AuthPickerContent gains a generic parameter, but has not shipped.

API Usage

AuthPickerView { authenticatedApp }
  .signedInContent { state in
    MyAccountView(
      email: state.user?.email,
      onDeleteAccount: state.deleteAccount,
      onSignOut: state.signOut
    )
  }

Preview


Maintainer note: Fixes internal CPRN-522

@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 a customizable signed-in screen slot (signedInContent) to AuthPickerView and AuthPickerContent, allowing developers to replace the default signed-in screen while retaining core authentication logic. Feedback on these changes highlights a potential SwiftUI sheet presentation conflict during account deletion, which can be resolved by utilizing the onDismiss callback. Additionally, the reviewer suggests correcting the isEmailVerified logic and documentation to prevent displaying the email verification button to users without an email address, and avoiding reporting errors when a user cancels the reauthentication flow.

Comment on lines +105 to +116
.sheet(isPresented: $showDeleteConfirmation) {
DeleteAccountConfirmationSheet(
onConfirm: {
showDeleteConfirmation = false
deleteAccount()
},
onCancel: {
showDeleteConfirmation = false
}
)
.presentationDetents([.medium])
}

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.

high

In SwiftUI, presenting a new sheet (like the reauthentication sheet) immediately after dismissing another sheet (the delete confirmation sheet) can fail because the dismissal animation of the first sheet is still in progress. To prevent this presentation conflict, we should trigger deleteAccount() only after the confirmation sheet has completely finished dismissing, using its onDismiss callback.

Note: Please also add @State private var pendingDelete = false to the properties of SignedInScreen.

    .sheet(isPresented: $showDeleteConfirmation, onDismiss: {
      if pendingDelete {
        pendingDelete = false
        deleteAccount()
      }
    }) {
      DeleteAccountConfirmationSheet(
        onConfirm: {
          pendingDelete = true
          showDeleteConfirmation = false
        },
        onCancel: {
          showDeleteConfirmation = false
        }
      )
      .presentationDetents([.medium])
    }

Comment on lines +41 to +42
/// Whether the user's email is verified; `true` when there is no user.
public let isEmailVerified: Bool

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 the documentation for isEmailVerified to reflect that it is also true when the user has no email address (e.g., phone or anonymous authentication), which helps prevent showing email verification UI to these users.

Suggested change
/// Whether the user's email is verified; `true` when there is no user.
public let isEmailVerified: Bool
/// Whether the user's email is verified; `true` when there is no user or no email address.
public let isEmailVerified: Bool

verifyEmail: verifyEmail,
goToMFAManagement: { authService.navigator.push(.mfaManagement) },
user: authService.currentUser,
isEmailVerified: authService.currentUser?.isEmailVerified != false,

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

For users signed in via phone or anonymous authentication, email is nil and isEmailVerified is false. This causes the "Verify Email" button to be incorrectly displayed in the default signed-in content (and potentially in custom signed-in views). We should treat isEmailVerified as true (or not applicable) if the user does not have an email address.

Suggested change
isEmailVerified: authService.currentUser?.isEmailVerified != false,
isEmailVerified: authService.currentUser?.email == nil || authService.currentUser?.isEmailVerified != false,

Comment on lines +179 to +184
if case AuthServiceError.signInCancelled = error {
// The user dismissed reauthentication; the sheet still reports it, as before.
} else {
errorMessage = authService.string.localizedErrorMessage(for: error)
}
reportError?(error)

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

When the user cancels or dismisses the reauthentication flow, AuthServiceError.signInCancelled is thrown. Currently, this error is still passed to reportError?(error), which triggers an unnecessary error alert to the user. We should avoid reporting the error when the user intentionally cancels the operation.

Suggested change
if case AuthServiceError.signInCancelled = error {
// The user dismissed reauthentication; the sheet still reports it, as before.
} else {
errorMessage = authService.string.localizedErrorMessage(for: error)
}
reportError?(error)
if case AuthServiceError.signInCancelled = error {
// The user dismissed reauthentication; do not show an error or report it.
} else {
errorMessage = authService.string.localizedErrorMessage(for: error)
reportError?(error)
}

@demolaf
demolaf force-pushed the auth/phone-content-slots branch from cc01c83 to ca1c05e Compare October 22, 2026 11:45
@demolaf
demolaf force-pushed the auth/signed-in-content branch 2 times, most recently from 8092009 to 79bd4e1 Compare October 22, 2026 13:34
@demolaf
demolaf force-pushed the auth/phone-content-slots branch from ca1c05e to 5336ee4 Compare October 22, 2026 13:34
@demolaf
demolaf force-pushed the auth/signed-in-content branch from 79bd4e1 to a2ab5af Compare October 22, 2026 13:48
@demolaf
demolaf force-pushed the auth/phone-content-slots branch 2 times, most recently from 75d9824 to ba7ecda Compare October 22, 2026 14:09
Add signedInContent(_:) to AuthPickerView and AuthPickerContent, so apps can replace the screen the authentication sheet shows to signed-in users. The content receives a SignedInContentState with the sign-out, delete-account, verify-email and navigation actions; an internal SignedInScreen keeps running them, including the delete confirmation, reauthentication and alerts. DefaultSignedInContent holds the stock markup and is the slot's default, and SignedInView renders it unchanged and gains a public init.

The slot carries through pickerContent(_:) and pickerDestination(_:) in either order, and the progress overlay stays outside it. Swiping away a reauthentication sheet now counts as a cancel, so the operation waiting on it finishes instead of hanging.
@demolaf
demolaf force-pushed the auth/signed-in-content branch from a2ab5af to d9f07ca Compare October 22, 2026 14:10
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