Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
46e54a0
feat(auth): add content slots to email sign-in screens
demolaf Oct 21, 2026
b28b50a
fix(auth): address review of the email content slots
demolaf Oct 21, 2026
cf0ffbe
docs(auth): point email screen docs at the whole-screen pickerContent
demolaf Oct 22, 2026
4efe16c
feat(auth): add content slot to MFA enrolment screen
demolaf Oct 21, 2026
67cc632
fix(auth): tighten MFA enrolment steps and factor validation
demolaf Oct 21, 2026
bce0016
fix(auth): return expired MFA enrolment sessions to factor selection
demolaf Oct 21, 2026
bdc298d
test(auth): cover MFA enrolment action guards
demolaf Oct 21, 2026
513cd6d
feat(auth): add content slots to MFA resolution and management screens
demolaf Oct 21, 2026
6bd36dc
fix(auth): attach MFA screen modifiers once and tighten their state
demolaf Oct 21, 2026
d9f07ca
feat(auth): add signedInContent slot to AuthPickerView
demolaf Oct 21, 2026
33c13ae
Merge remote-tracking branch 'origin/auth/mfa-enrolment-content-slot'…
demolaf Oct 22, 2026
99f8f36
Merge remote-tracking branch 'origin/auth/mfa-content-slots' into sam…
demolaf Oct 22, 2026
136d366
Merge remote-tracking branch 'origin/auth/signed-in-content' into sam…
demolaf Oct 22, 2026
074f290
feat(sample): add full-customization example covering every auth screen
demolaf Oct 22, 2026
995a908
fix(sample): address fresh-eyes review of the full-customization example
demolaf Oct 22, 2026
c9b83ae
fix(sample): size the full-customization form to the Figma width
demolaf Oct 22, 2026
d507f15
fix(sample): draw the full-customization background as its own layer
demolaf Oct 22, 2026
cfd695e
fix(sample): even out the full-customization sign-in method spacing
demolaf Oct 22, 2026
165ab3a
fix(sample): show stock provider buttons in the more sign-in options …
demolaf Oct 22, 2026
973d593
fix(sample): move the sign-in / sign-up switch under the email form
demolaf Oct 22, 2026
2b376f0
fix(sample): even the spacing around the sign-in / sign-up switch
demolaf Oct 22, 2026
0c36923
feat(sample): preview the full-customization TOTP setup step
demolaf Oct 22, 2026
edd4d3b
fix(sample): inset full-customization screens 32 points
demolaf Oct 22, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -17,31 +17,36 @@ import FirebaseAuthUIComponents
import SwiftUI

/// An ``AuthPickerView`` with custom sheet content, returned by
/// `AuthPickerView`'s `pickerContent` and ``AuthPickerView/pickerDestination(_:)`` modifiers.
/// `AuthPickerView`'s `pickerContent`, ``AuthPickerView/pickerDestination(_:)`` and
/// ``AuthPickerView/signedInContent(_:)`` modifiers.
///
/// It owns the authentication sheet, its `NavigationStack`, error alerts, and MFA and
/// account-conflict handling; only the first screen and the pushed screens come from the slots.
/// You don't create it directly.
/// account-conflict handling; only the first screen, the signed-in screen and the pushed screens
/// come from the slots. You don't create it directly.
@MainActor
public struct AuthPickerContent<
Content: View,
PickerContent: View,
DestinationContent: View
DestinationContent: View,
SignedInContent: View
>: View {
init(content: @escaping () -> Content,
pickerContent: @escaping (AuthMethodPickerState) -> PickerContent,
destination: @escaping (AuthView) -> DestinationContent,
signedInContent: @escaping (SignedInContentState) -> SignedInContent,
showsPickerTitle: Bool = true) {
self.content = content
self.pickerContent = pickerContent
self.destination = destination
self.signedInContent = signedInContent
self.showsPickerTitle = showsPickerTitle
}

@Environment(AuthService.self) private var authService
private let content: () -> Content
private let pickerContent: (AuthMethodPickerState) -> PickerContent
private let destination: (AuthView) -> DestinationContent
private let signedInContent: (SignedInContentState) -> SignedInContent
/// `false` when the whole first screen is custom, so the library's large title doesn't sit
/// above the app's own heading. The close button stays so the sheet can still be dismissed.
private let showsPickerTitle: Bool
Expand Down Expand Up @@ -89,13 +94,13 @@ public struct AuthPickerContent<
}
}

/// The sheet's first screen. The slot only replaces the signed-out screen, so signed-in users
/// always reach ``SignedInView`` and the progress overlay always shows while signing in.
/// The sheet's first screen: the signed-in slot for signed-in users, otherwise the picker slot.
/// The progress overlay always shows while signing in, whichever slot is on screen.
@ViewBuilder
private var root: some View {
VStack {
if authService.authenticationState == .authenticated {
SignedInView()
SignedInScreen(content: signedInContent)
} else {
AuthMethodPickerStateReader { state in
pickerContent(state)
Expand All @@ -122,11 +127,12 @@ public struct AuthPickerContent<
/// the no-argument `pickerContent` on ``AuthPickerView``.
public func pickerContent<NewPickerContent: View>(
@ViewBuilder _ pickerContent: @escaping () -> NewPickerContent
) -> AuthPickerContent<Content, NewPickerContent, DestinationContent> {
AuthPickerContent<Content, NewPickerContent, DestinationContent>(
) -> AuthPickerContent<Content, NewPickerContent, DestinationContent, SignedInContent> {
AuthPickerContent<Content, NewPickerContent, DestinationContent, SignedInContent>(
content: content,
pickerContent: { _ in pickerContent() },
destination: destination,
signedInContent: signedInContent,
showsPickerTitle: true
)
}
Expand All @@ -135,11 +141,12 @@ public struct AuthPickerContent<
/// ``AuthMethodPickerState``. See the matching `pickerContent` on ``AuthPickerView``.
public func pickerContent<NewPickerContent: View>(
@ViewBuilder _ pickerContent: @escaping (AuthMethodPickerState) -> NewPickerContent
) -> AuthPickerContent<Content, NewPickerContent, DestinationContent> {
AuthPickerContent<Content, NewPickerContent, DestinationContent>(
) -> AuthPickerContent<Content, NewPickerContent, DestinationContent, SignedInContent> {
AuthPickerContent<Content, NewPickerContent, DestinationContent, SignedInContent>(
content: content,
pickerContent: pickerContent,
destination: destination,
signedInContent: signedInContent,
showsPickerTitle: false
)
}
Expand All @@ -148,11 +155,26 @@ public struct AuthPickerContent<
/// ``AuthPickerView/pickerDestination(_:)``.
public func pickerDestination<NewDestinationContent: View>(
@ViewBuilder _ destination: @escaping (AuthView) -> NewDestinationContent
) -> AuthPickerContent<Content, PickerContent, NewDestinationContent> {
AuthPickerContent<Content, PickerContent, NewDestinationContent>(
) -> AuthPickerContent<Content, PickerContent, NewDestinationContent, SignedInContent> {
AuthPickerContent<Content, PickerContent, NewDestinationContent, SignedInContent>(
content: content,
pickerContent: pickerContent,
destination: destination,
signedInContent: signedInContent,
showsPickerTitle: showsPickerTitle
)
}

/// Replaces the signed-in screen of the authentication sheet. See
/// ``AuthPickerView/signedInContent(_:)``.
public func signedInContent<NewSignedInContent: View>(
@ViewBuilder _ signedInContent: @escaping (SignedInContentState) -> NewSignedInContent
) -> AuthPickerContent<Content, PickerContent, DestinationContent, NewSignedInContent> {
AuthPickerContent<Content, PickerContent, DestinationContent, NewSignedInContent>(
content: content,
pickerContent: pickerContent,
destination: destination,
signedInContent: signedInContent,
showsPickerTitle: showsPickerTitle
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@ import SwiftUI
/// Wraps your app content and presents the authentication sheet whenever
/// `AuthService.isPresented` is `true`.
///
/// Use the `pickerContent` modifiers and ``pickerDestination(_:)`` to replace the sheet's first screen or
/// any pushed screen while the library keeps driving navigation, MFA and account conflicts.
/// Use the `pickerContent` modifiers, ``pickerDestination(_:)`` and ``signedInContent(_:)`` to
/// replace the sheet's first screen, any pushed screen or the signed-in screen while the library
/// keeps driving navigation, MFA and account conflicts.
///
/// Example:
/// ```swift
Expand All @@ -42,12 +43,14 @@ public struct AuthPickerView<Content: View>: View {
AuthPickerContent(
content: content,
pickerContent: { _ in DefaultAuthPickerContent() },
destination: { DefaultAuthPickerDestination(screen: $0) }
destination: { DefaultAuthPickerDestination(screen: $0) },
signedInContent: { DefaultSignedInContent(state: $0) }
)
}

/// Replaces the signed-out first screen of the authentication sheet. The library still shows
/// ``SignedInView`` to signed-in users and its progress overlay while signing in.
/// Replaces the signed-out first screen of the authentication sheet. Signed-in users still see
/// ``DefaultSignedInContent`` unless you replace it with ``signedInContent(_:)``, and the
/// progress overlay still shows while signing in.
///
/// ```swift
/// AuthPickerView { authenticatedApp }
Expand All @@ -58,11 +61,17 @@ public struct AuthPickerView<Content: View>: View {
/// ```
public func pickerContent<NewPickerContent: View>(
@ViewBuilder _ pickerContent: @escaping () -> NewPickerContent
) -> AuthPickerContent<Content, NewPickerContent, DefaultAuthPickerDestination> {
) -> AuthPickerContent<
Content,
NewPickerContent,
DefaultAuthPickerDestination,
DefaultSignedInContent
> {
AuthPickerContent(
content: content,
pickerContent: { _ in pickerContent() },
destination: { DefaultAuthPickerDestination(screen: $0) }
destination: { DefaultAuthPickerDestination(screen: $0) },
signedInContent: { DefaultSignedInContent(state: $0) }
)
}

Expand All @@ -71,9 +80,9 @@ public struct AuthPickerView<Content: View>: View {
/// account conflicts and errors are handled the same as the default buttons.
///
/// Your screen replaces the logo, email sign-in, terms and the large "Sign in" title (the close
/// button stays), so show your own heading, email entry (for example one built around
/// ``EmailAuthView``) and any legal text you need. To keep the stock screen and only lay out
/// the provider buttons, use ``DefaultAuthPickerContent/init(authMethodPicker:)``.
/// button stays), so show your own heading, email entry (for example ``EmailAuthScreen``) and
/// any legal text you need. To keep the stock screen and only lay out the provider buttons, use
/// ``DefaultAuthPickerContent/init(authMethodPicker:)``.
///
/// Example:
/// ```swift
Expand All @@ -84,11 +93,17 @@ public struct AuthPickerView<Content: View>: View {
/// ```
public func pickerContent<NewPickerContent: View>(
@ViewBuilder _ pickerContent: @escaping (AuthMethodPickerState) -> NewPickerContent
) -> AuthPickerContent<Content, NewPickerContent, DefaultAuthPickerDestination> {
) -> AuthPickerContent<
Content,
NewPickerContent,
DefaultAuthPickerDestination,
DefaultSignedInContent
> {
AuthPickerContent(
content: content,
pickerContent: pickerContent,
destination: { DefaultAuthPickerDestination(screen: $0) },
signedInContent: { DefaultSignedInContent(state: $0) },
showsPickerTitle: false
)
}
Expand All @@ -115,12 +130,45 @@ public struct AuthPickerView<Content: View>: View {
) -> AuthPickerContent<
Content,
DefaultAuthPickerContent<DefaultAuthMethodPicker>,
NewDestinationContent
NewDestinationContent,
DefaultSignedInContent
> {
AuthPickerContent(
content: content,
pickerContent: { _ in DefaultAuthPickerContent() },
destination: destination,
signedInContent: { DefaultSignedInContent(state: $0) }
)
}

/// Replaces the screen the authentication sheet shows to signed-in users. Your content receives
/// a ``SignedInContentState``: the library still signs out, deletes the account behind its
/// confirmation and reauthentication, sends the verification email and navigates. Wrap
/// ``DefaultSignedInContent`` to keep the stock markup.
///
/// ```swift
/// AuthPickerView { authenticatedApp }
/// .signedInContent { state in
/// MyAccountView(
/// email: state.user?.email,
/// onDeleteAccount: state.deleteAccount,
/// onSignOut: state.signOut
/// )
/// }
/// ```
public func signedInContent<NewSignedInContent: View>(
@ViewBuilder _ signedInContent: @escaping (SignedInContentState) -> NewSignedInContent
) -> AuthPickerContent<
Content,
DefaultAuthPickerContent<DefaultAuthMethodPicker>,
DefaultAuthPickerDestination,
NewSignedInContent
> {
AuthPickerContent(
content: content,
pickerContent: { _ in DefaultAuthPickerContent() },
destination: destination
destination: { DefaultAuthPickerDestination(screen: $0) },
signedInContent: signedInContent
)
}
}
Expand Down
Loading