Skip to content
Open
17 changes: 17 additions & 0 deletions FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Exports.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// FirebaseAuthUIComponents is not a library product, so re-export it to make
// AuthTextFieldStyle and AuthTypography reachable from `import FirebaseAuthSwiftUI`.
@_exported import FirebaseAuthUIComponents
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import FirebaseAuthUIComponents
import SwiftUI

/// Styling configuration for the primary call-to-action buttons used throughout the auth flow
/// (sign in, send code, update password, etc). All fields default to `nil`, in which case
/// today's exact appearance is unchanged — including `backgroundColor`, which otherwise already
/// follows the environment's `.tint()` via `.buttonStyle(.borderedProminent)`; setting it here
/// overrides that for CTA buttons specifically, without affecting `.tint()` elsewhere.
public struct AuthCTAButtonStyle: Sendable {
public var backgroundColor: Color?
public var contentColor: Color?
public var shape: ButtonBorderShape?
public var font: Font?

public init(backgroundColor: Color? = nil,
contentColor: Color? = nil,
shape: ButtonBorderShape? = nil,
font: Font? = nil) {
self.backgroundColor = backgroundColor
self.contentColor = contentColor
self.shape = shape
self.font = font
}

public static let `default` = AuthCTAButtonStyle()
}

private struct AuthCTAButtonStyleKey: EnvironmentKey {
static let defaultValue: AuthCTAButtonStyle = .default
}

public extension EnvironmentValues {
var authCTAButtonStyle: AuthCTAButtonStyle {
get { self[AuthCTAButtonStyleKey.self] }
set { self[AuthCTAButtonStyleKey.self] = newValue }
}
}

/// Applies `.buttonStyle(.borderedProminent)` plus the colors/shape/font from the environment's
/// ``AuthCTAButtonStyle``, in place of a bare `.buttonStyle(.borderedProminent)` call. When
/// `style.font` is left unset, the button falls back to ``AuthTypography`` (via `.authFont(_:)`)
/// so it stays consistent with the rest of the auth flow's typography by default — an explicit
/// `style.font` still overrides that, for buttons that should intentionally look different.
struct AuthCTAButtonModifier: ViewModifier {
@Environment(\.authCTAButtonStyle) private var style

func body(content: Content) -> some View {
Group {
if let contentColor = style.contentColor {
content.foregroundStyle(contentColor)
} else {
content
}
}
.buttonStyle(.borderedProminent)
.buttonBorderShape(style.shape ?? .automatic)
.tint(style.backgroundColor)
.modifier(CTAFontModifier(explicitFont: style.font))
}
Comment thread
demolaf marked this conversation as resolved.
}

private struct CTAFontModifier: ViewModifier {
let explicitFont: Font?

func body(content: Content) -> some View {
if let explicitFont {
content.font(explicitFont)
} else {
content.authFont(.body)
}
}
}

extension View {
/// Applies the auth flow's primary call-to-action button styling, reading colors/shape/font
/// from the environment's ``AuthCTAButtonStyle`` (set via `.authCTAButtonStyle(_:)`).
func authCTAButtonStyle() -> some View {
modifier(AuthCTAButtonModifier())
}
}

public extension View {
/// Sets the ``AuthCTAButtonStyle`` used by every CTA button in this view's subtree.
///
/// ```swift
/// AuthPickerView { ... }
/// .authCTAButtonStyle(
/// AuthCTAButtonStyle(backgroundColor: theme.colors.tint, contentColor: .white, shape: .capsule)
/// )
/// ```
func authCTAButtonStyle(_ style: AuthCTAButtonStyle) -> some View {
environment(\.authCTAButtonStyle, style)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ extension AuthPickerView: View {
.scaleEffect(1.25)
.tint(.white)
Text("Authenticating...")
.authFont(.body)
.foregroundStyle(.white)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ extension EmailAuthView: View {
authService.navigator.push(.passwordRecovery)
} label: {
Text(authService.string.passwordButtonLabel)
.authFont(.body, weight: .medium)
.frame(maxWidth: .infinity, alignment: .trailing)
}
.accessibilityIdentifier("password-recovery-button")
Expand Down Expand Up @@ -205,7 +206,7 @@ extension EmailAuthView: View {
.disabled(!isValid)
.padding([.top, .bottom], 8)
.frame(maxWidth: .infinity)
.buttonStyle(.borderedProminent)
.authCTAButtonStyle()
.accessibilityIdentifier("sign-in-button")
}
Button(action: {
Expand All @@ -222,14 +223,14 @@ extension EmailAuthView: View {
? authService.string.dontHaveAnAccountYetLabel
: authService.string.alreadyHaveAnAccountLabel
)
.authFont(.body)
.foregroundStyle(Color(.label))
Text(
authService.authenticationFlow == .signUp
? authService.string.emailLoginFlowLabel
: authService.string.emailSignUpFlowLabel
)
.fontWeight(.semibold)
.foregroundColor(.blue)
.authFont(.body, weight: .medium)
Comment thread
demolaf marked this conversation as resolved.
}
}
.accessibilityIdentifier("switch-auth-flow")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
// limitations under the License.

import FirebaseAuth
import FirebaseAuthUIComponents
import FirebaseCore
import SwiftUI

Expand Down Expand Up @@ -81,20 +82,20 @@ extension EmailLinkReauthView: View {
.padding(.top, 32)

Text("Check Your Email")
.font(.title)
.authFont(.title)
.fontWeight(.bold)

Text("We've sent a verification link to:")
.font(.body)
.authFont(.body)
.foregroundStyle(.secondary)

Text(email)
.font(.body)
.authFont(.body)
.fontWeight(.medium)
.padding(.horizontal)

Text("Tap the link in the email to complete reauthentication.")
.font(.body)
.authFont(.body)
.multilineTextAlignment(.center)
.foregroundStyle(.secondary)
.padding(.horizontal, 32)
Expand All @@ -110,6 +111,7 @@ extension EmailLinkReauthView: View {
.frame(height: 32)
} else {
Text("Resend Email")
.authFont(.body)
.frame(height: 32)
}
}
Expand All @@ -123,6 +125,7 @@ extension EmailLinkReauthView: View {
ProgressView()
.padding(.top, 32)
Text("Sending verification email...")
.authFont(.body)
.foregroundStyle(.secondary)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ extension EmailLinkView: View {
.padding(.vertical, 8)
.frame(maxWidth: .infinity)
}
.buttonStyle(.borderedProminent)
.authCTAButtonStyle()
.disabled(!CommonUtils.isValidEmail(email))
.padding([.top, .bottom], 8)
.frame(maxWidth: .infinity)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,19 +66,19 @@ extension EmailReauthView: View {
.foregroundColor(.blue)

Text(authService.string.confirmPasswordTitle)
.font(.title)
.authFont(.title)
.fontWeight(.bold)

Text(authService.string.forSecurityEnterPasswordMessage)
.font(.body)
.authFont(.body)
.foregroundColor(.secondary)
.multilineTextAlignment(.center)
}
.padding()

VStack(spacing: 20) {
Text(authService.string.emailPrefix(email: email))
.font(.caption)
.authFont(.caption)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.bottom, 8)

Expand Down Expand Up @@ -109,7 +109,7 @@ extension EmailReauthView: View {
.frame(maxWidth: .infinity)
}
}
.buttonStyle(.borderedProminent)
.authCTAButtonStyle()
.disabled(password.isEmpty || isLoading)
.accessibilityIdentifier("confirm-password-button")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ struct EnterPhoneNumberView: View {
var body: some View {
VStack(spacing: 16) {
Text(authService.string.enterPhoneNumberPlaceholder)
.font(.subheadline)
.authFont(.subheadline)
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
.frame(maxWidth: .infinity, alignment: .leading)
Expand Down Expand Up @@ -77,7 +77,7 @@ struct EnterPhoneNumberView: View {
.frame(maxWidth: .infinity)
}
}
.buttonStyle(.borderedProminent)
.authCTAButtonStyle()
.disabled(authService.authenticationState == .authenticating || phoneNumber.isEmpty)
.padding(.top, 8)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ struct EnterVerificationCodeView: View {
VStack(spacing: 16) {
VStack(spacing: 8) {
Text(authService.string.sentCodeMessage(phoneNumber: fullPhoneNumber))
.font(.subheadline)
.authFont(.subheadline)
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
.frame(maxWidth: .infinity, alignment: .leading)
Expand All @@ -42,7 +42,7 @@ struct EnterVerificationCodeView: View {
authService.navigator.pop()
} label: {
Text(authService.string.changeNumberButtonLabel)
.font(.caption)
.authFont(.caption)
.frame(maxWidth: .infinity, alignment: .leading)
}
}
Expand Down Expand Up @@ -87,7 +87,7 @@ struct EnterVerificationCodeView: View {
.frame(maxWidth: .infinity)
}
}
.buttonStyle(.borderedProminent)
.authCTAButtonStyle()
.disabled(authService.authenticationState == .authenticating || verificationCode.count != 6)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.

import FirebaseAuthUIComponents
import FirebaseCore
import SwiftUI

Expand All @@ -26,16 +27,17 @@ struct LegacySignInRecoveryView: View {
VStack(alignment: .leading, spacing: 24) {
VStack(alignment: .leading, spacing: 12) {
Text(authService.string.legacySignInRecoveryTitle)
.font(.title2.weight(.semibold))
.authFont(.title2, weight: .semibold)
Text(authService.string.legacySignInRecoveryMessage(email: recovery.email))
.authFont(.body)
.foregroundStyle(.secondary)
}

authService.renderLegacyRecoveryButtons()

if !recovery.unavailableProviders.isEmpty {
Text(authService.string.legacySignInRecoveryUnavailableMessage)
.font(.footnote)
.authFont(.footnote)
.foregroundStyle(.secondary)
}

Expand Down
Loading
Loading