Repository navigation
Conversation
Add EmailAuthScreen, PasswordRecoveryScreen and EmailLinkScreen, which own the email flows' state and logic and hand their content a state struct. The stock markup moves to DefaultEmailAuthContent, DefaultPasswordRecoveryContent and DefaultEmailLinkContent, and the existing views render them through the screens.
Add goToEmailLink and hideSuccess to the state structs, keep the recovery screen when its success sheet is swiped away, handle an opened email link once even when custom content has several root views, move focus to confirm password on Go during sign-up, and align the recovery screen's isLoading with the other screens.
Add public MFAEnrolmentScreen, which owns the enrolment state, reauthentication and error reporting and passes its content an MFAEnrollmentContentState. DefaultMFAEnrolmentContent holds the stock markup, and MFAEnrolmentView now renders it through the screen.
Map finished enrolment sessions back to factor selection, require the selected factor to be allowed before starting, keep copy feedback from being cut short, clear focus between steps, and cover the step and isValid rules with unit tests.
Map sessions past expiresAt back to factor selection, only send codes from the SMS steps, disable Resend Code while loading, and note that custom content sets its own navigation title.
Move the startEnrollment, sendCode and enroll guards into Step helpers with unit tests, and replace the unused focus enum with a single phone number focus flag.
Add public MFAResolutionScreen and MFAManagementScreen, which own the screen state and actions and pass MFAResolutionContentState / MFAManagementContentState to consumer content. DefaultMFAResolutionContent and DefaultMFAManagementContent hold the stock markup, and MFAResolutionView and MFAManagementView keep their public signatures by rendering them through the screens. MFAManagementScreen keeps the reauthentication modifier outside the content, so the reauth sheet presents for custom content too.
Wrap each MFA screen's content in one container so the reauthentication sheets and lifecycle modifiers attach once, even when the content returns several views or none. Setting selectedHintIndex resets the challenge again on every tap, a code sent for a hint the user switched away from is dropped, cancelling reauthentication no longer sets errorMessage, and refresh clears it.
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.
… into sample/full-customization
…ple/full-customization
…ple/full-customization
Adds a Full customization example to the SwiftUI sample. It replaces the first screen with .pickerContent { state in }, every pushed screen through its content slot and the signed-in screen with .signedInContent, while the library keeps auth, navigation, MFA and reauthentication.
Doggo, Grid and Coffee Spark themes come from the iOS Landing frames of the Firebase auth flows Figma file and switch live from any screen. Screens the frames don't cover follow the FirebaseUI-Android demo's copy. The theming modifiers restyle the stock screens left in the flow.
There was a problem hiding this comment.
Code Review
This pull request refactors the authentication views in FirebaseAuthSwiftUI to support full UI customization by introducing customizable screen wrappers (such as EmailAuthScreen and MFAEnrolmentScreen) and extracting the stock UI into default content views. It also adds a comprehensive multi-theme customization example to the sample application. The review feedback highlights critical improvements, including reusing a static CIContext for QR code generation to prevent performance degradation, and replacing a force cast with safe optional casting to avoid potential runtime crashes.
| private func generateQRCode(from string: String) -> UIImage? { | ||
| let data = Data(string.utf8) | ||
|
|
||
| guard let filter = CIFilter(name: "CIQRCodeGenerator") else { return nil } | ||
| filter.setValue(data, forKey: "inputMessage") | ||
| filter.setValue("H", forKey: "inputCorrectionLevel") | ||
|
|
||
| guard let ciImage = filter.outputImage else { return nil } | ||
|
|
||
| // Scale up the QR code for better quality | ||
| let transform = CGAffineTransform(scaleX: 10, y: 10) | ||
| let scaledImage = ciImage.transformed(by: transform) | ||
|
|
||
| let context = CIContext() | ||
| guard let cgImage = context.createCGImage(scaledImage, from: scaledImage.extent) else { | ||
| return nil | ||
| } | ||
|
|
||
| return UIImage(cgImage: cgImage) | ||
| } |
There was a problem hiding this comment.
Creating a new CIContext on every invocation of generateQRCode(from:) is highly inefficient and can cause UI stuttering/performance degradation because CIContext is expensive to instantiate. It is recommended to reuse a single shared/static CIContext instance.
private static let qrContext = CIContext()
private func generateQRCode(from string: String) -> UIImage? {
let data = Data(string.utf8)
guard let filter = CIFilter(name: "CIQRCodeGenerator") else { return nil }
filter.setValue(data, forKey: "inputMessage")
filter.setValue("H", forKey: "inputCorrectionLevel")
guard let ciImage = filter.outputImage else { return nil }
// Scale up the QR code for better quality
let transform = CGAffineTransform(scaleX: 10, y: 10)
let scaledImage = ciImage.transformed(by: transform)
guard let cgImage = Self.qrContext.createCGImage(scaledImage, from: scaledImage.extent) else {
return nil
}
return UIImage(cgImage: cgImage)
}| if factor.factorID == PhoneMultiFactorID { | ||
| let phoneInfo = factor as! PhoneMultiFactorInfo | ||
| Text("SMS: \(phoneInfo.phoneNumber)") | ||
| .authFont(.caption) | ||
| .foregroundColor(.secondary) | ||
| } else { | ||
| Text("Authenticator App") | ||
| .authFont(.caption) | ||
| .foregroundColor(.secondary) | ||
| } |
There was a problem hiding this comment.
Force casting factor as! PhoneMultiFactorInfo can lead to runtime crashes if the factor is not of the expected type. It is safer and more idiomatic to use safe optional casting (as?) to prevent potential crashes.
if let phoneInfo = factor as? PhoneMultiFactorInfo {
Text("SMS: \(phoneInfo.phoneNumber)")
.authFont(.caption)
.foregroundColor(.secondary)
} else {
Text("Authenticator App")
.authFont(.caption)
.foregroundColor(.secondary)
}| private struct DemoQRCode: View { | ||
| let image: UIImage | ||
|
|
||
| init?(url: URL) { | ||
| let filter = CIFilter.qrCodeGenerator() | ||
| filter.message = Data(url.absoluteString.utf8) | ||
| guard let output = filter.outputImage?.transformed(by: CGAffineTransform(scaleX: 8, y: 8)), | ||
| let cgImage = CIContext().createCGImage(output, from: output.extent) else { | ||
| return nil | ||
| } | ||
| image = UIImage(cgImage: cgImage) | ||
| } |
There was a problem hiding this comment.
Instantiating a new CIContext inside the initializer of DemoQRCode is inefficient because SwiftUI views are frequently recreated. Reusing a single static CIContext avoids this overhead.
| private struct DemoQRCode: View { | |
| let image: UIImage | |
| init?(url: URL) { | |
| let filter = CIFilter.qrCodeGenerator() | |
| filter.message = Data(url.absoluteString.utf8) | |
| guard let output = filter.outputImage?.transformed(by: CGAffineTransform(scaleX: 8, y: 8)), | |
| let cgImage = CIContext().createCGImage(output, from: output.extent) else { | |
| return nil | |
| } | |
| image = UIImage(cgImage: cgImage) | |
| } | |
| private struct DemoQRCode: View { | |
| private static let qrContext = CIContext() | |
| let image: UIImage | |
| init?(url: URL) { | |
| let filter = CIFilter.qrCodeGenerator() | |
| filter.message = Data(url.absoluteString.utf8) | |
| guard let output = filter.outputImage?.transformed(by: CGAffineTransform(scaleX: 8, y: 8)), | |
| let cgImage = Self.qrContext.createCGImage(output, from: output.extent) else { | |
| return nil | |
| } | |
| image = UIImage(cgImage: cgImage) | |
| } |
6cbeed7 to
074f290
Compare
Starts sign-in from the other-providers sheet after it dismisses, creates the example's AuthService when it appears rather than when ContentView builds the link, anchors the mascot to the form's trailing edge, generates the TOTP QR code once per URL, disables Remove while an unenrolment runs, gives sheet providers an icon, and ships the spark background at 2x only.
1847902 to
995a908
Compare
The form and footer now keep the Figma frame's 332 and 334 point widths, centred, instead of fixed 49 and 48 point insets that only fit the 430 point frame and squeezed the form on narrower phones.
The background sat in the scroll view's .background, so it shrank with the scroll view and left white edges once the form had a maximum width. It is now a ZStack layer behind a full-width scroll view.
The remaining providers open from a pill styled like Google's instead of a text link, so the three buttons keep Figma's 12 point spacing. The flow switch gets a 44 point tap target, and text links get a taller tap area without changing the layout.
…sheet Providers the Figma frames don't draw now appear as the library's AuthProviderButton with each provider's own label and ProviderStyle, instead of improvised pills. The action still goes through AuthMethodPickerState.select once the sheet has closed.
The switch toggles the email form, so it now sits right below the form and above the other sign-in methods divider, rather than under the provider buttons where the Figma frames place it.
The switch text now sits 24 points from the form above and the divider below in every theme. The per-theme form-to-divider spacing it replaced is removed.
The enrolment steps move into DemoMFAEnrolmentContent so a preview can render the TOTP setup step from a stub MFAEnrollmentContentState. The Auth emulator only enrols SMS factors, so the step can't be reached against it.
Every demo screen now uses one content width: 32 points from the sheet's edges, capped at 480 points. The Figma frames' 332 point form left 49 points each side on a 430 point phone, which was too much.
Related to #1400.
Adds a full customization example to the SwiftUI sample: every auth screen drawn by the app, with three live-switchable themes from the "Firebase auth flows" Figma file.
Draft: based on #1412 with #1414 to #1417 merged in, so the diff includes them. It gets rebased onto main once #1402 to #1417 merge.
Changes
.pickerContent { state in }draws the Figma Landing;.pickerDestinationand.signedInContentreplace every other screen through its content slot.AuthServiceto enable email-link sign-in without changing other examples. Fonts ship with their OFL licenses.Preview
Maintainer note: Fixes internal CPRN-523