Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -83,6 +83,45 @@ public final class AuthService {
public var authenticationFlow: AuthenticationFlow = .login
public var errorMessage = ""
public let passwordPrompt: PasswordPromptCoordinator = .init()

// MARK: - AuthPickerView Modal APIs

public var isShowingAuthModal = false

public enum AuthModalContentType {
case phoneAuth
}

public var currentModal: AuthModalContentType?

public var authModalViewBuilderRegistry: [AuthModalContentType: () -> AnyView] = [:]

public func registerModalView(for type: AuthModalContentType,
@ViewBuilder builder: @escaping () -> AnyView) {
authModalViewBuilderRegistry[type] = builder
}

public func viewForCurrentModal() -> AnyView? {
guard let type = currentModal,
let builder = authModalViewBuilderRegistry[type] else {
return nil
}
return builder()
}

public func presentModal(for type: AuthModalContentType) {
currentModal = type
isShowingAuthModal = true
}

public func dismissModal() {
isShowingAuthModal = false
}

// MARK: - End AuthPickerView Modal APIs

// MARK: - Provider APIs

private var unsafeGoogleProvider: (any GoogleProviderAuthUIProtocol)?
private var unsafeFacebookProvider: (any FacebookProviderAuthUIProtocol)?
private var unsafePhoneAuthProvider: (any PhoneAuthProviderAuthUIProtocol)?
Expand Down Expand Up @@ -146,6 +185,8 @@ public final class AuthService {
}
}

// MARK: - End Provider APIs

private func safeActionCodeSettings() throws -> ActionCodeSettings {
// email sign-in requires action code settings
guard let actionCodeSettings = configuration
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,7 @@ public class StringUtils {
/// found in:
/// - SignInWithFacebookButton
public var facebookLoginButtonLabel: String {
return localizedString(for: "Continue with Facebook")
return localizedString(for: "Sign in with Facebook")
}

/// Facebook provider
Expand Down Expand Up @@ -319,8 +319,8 @@ public class StringUtils {
/// Phone provider
/// found in:
/// - PhoneAuthButtonView
public var smsCodeSentLabel: String {
return localizedString(for: "SMS code sent")
public var smsCodeSendButtonLabel: String {
return localizedString(for: "Send SMS code")
}

/// Phone provider
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@ public struct AuthPickerView {
.authenticationFlow == .login ? .signUp : .login
}

private var isAuthModalPresented: Binding<Bool> {
Binding(
get: { authService.isShowingAuthModal },
set: { authService.isShowingAuthModal = $0 }
)
}

@ViewBuilder
private var authPickerTitleView: some View {
if authService.authView == .authPicker {
Expand Down Expand Up @@ -71,6 +78,33 @@ extension AuthPickerView: View {
EmptyView()
}
}
}.sheet(isPresented: isAuthModalPresented) {
VStack(spacing: 0) {
HStack {
Button(action: {
authService.dismissModal()
}) {
HStack(spacing: 4) {
Image(systemName: "chevron.left")
.font(.system(size: 17, weight: .medium))
Text(authService.string.backButtonLabel)
.font(.system(size: 17))
}
.foregroundColor(.blue)
}
Spacer()
}
.padding()
.background(Color(.systemBackground))

Divider()

if let view = authService.viewForCurrentModal() {
view
.frame(maxWidth: .infinity, maxHeight: .infinity)
.padding()
}
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ extension EmailAuthView: View {
}
}
.disabled(!isValid)
.padding([.top, .bottom], 8)
.padding([.top, .bottom, .horizontal], 8)
.frame(maxWidth: .infinity)
.buttonStyle(.borderedProminent)
Button(action: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ extension VerifyEmailView: View {
.padding(.vertical, 8)
.frame(maxWidth: .infinity)
}
.padding([.top, .bottom], 8)
.padding([.top, .bottom, .horizontal], 8)
.frame(maxWidth: .infinity)
.buttonStyle(.borderedProminent)
}.sheet(isPresented: $showModal) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,90 +5,25 @@ import SwiftUI
@MainActor
public struct PhoneAuthButtonView {
@Environment(AuthService.self) private var authService
@State private var errorMessage = ""
@State private var phoneNumber = ""
@State private var showVerificationCodeInput = false
@State private var verificationCode = ""
@State private var verificationID = ""

public init() {}
}

extension PhoneAuthButtonView: View {
public var body: some View {
if authService.authenticationState != .authenticating {
VStack {
LabeledContent {
TextField(authService.string.enterPhoneNumberLabel, text: $phoneNumber)
.textInputAutocapitalization(.never)
.disableAutocorrection(true)
.submitLabel(.next)
} label: {
Image(systemName: "at")
}.padding(.vertical, 6)
.background(Divider(), alignment: .bottom)
.padding(.bottom, 4)
Button(action: {
Task {
do {
let id = try await authService.verifyPhoneNumber(phoneNumber: phoneNumber)
verificationID = id
showVerificationCodeInput = true
} catch {
errorMessage = authService.string.localizedErrorMessage(
for: error
)
}
}
}) {
Text(authService.string.smsCodeSentLabel)
.padding(.vertical, 8)
.frame(maxWidth: .infinity)
}
.disabled(!PhoneUtils.isValidPhoneNumber(phoneNumber))
.padding([.top, .bottom], 8)
.frame(maxWidth: .infinity)
.buttonStyle(.borderedProminent)
Text(errorMessage).foregroundColor(.red)
}.sheet(isPresented: $showVerificationCodeInput) {
TextField(authService.string.phoneNumberVerificationCodeLabel, text: $verificationCode)
.keyboardType(.numberPad)
.padding()
.background(Color(.systemGray6))
.cornerRadius(8)
.padding(.horizontal)

Button(action: {
Task {
do {
try await authService.signInWithPhoneNumber(
verificationID: verificationID,
verificationCode: verificationCode
)
} catch {
errorMessage = authService.string.localizedErrorMessage(for: error)
}
showVerificationCodeInput = false
}
}) {
Text(authService.string.verifyPhoneNumberAndSignInLabel)
.foregroundColor(.white)
.padding()
.frame(maxWidth: .infinity)
.background(Color.green)
.cornerRadius(8)
.padding(.horizontal)
}
}.onOpenURL { url in
authService.auth.canHandle(url)
Button(action: {
authService.registerModalView(for: .phoneAuth) {
AnyView(PhoneAuthView().environment(authService))
}
} else {
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: .white))
.padding(.vertical, 8)
.frame(maxWidth: .infinity)
authService.presentModal(for: .phoneAuth)
}) {
Label("Sign in with Phone", systemImage: "phone.fill")
.foregroundColor(.white)
.padding()
.frame(maxWidth: .infinity, alignment: .leading)
.background(Color.green.opacity(0.8)) // Light green
.cornerRadius(8)
}
Text(errorMessage).foregroundColor(.red)
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
//
// PhoneAuthView.swift
// FirebaseUI
//
// Created by Russell Wheatley on 14/05/2025.
//

import FirebaseAuthSwiftUI
import FirebaseCore
import SwiftUI

@MainActor
public struct PhoneAuthView {
@Environment(AuthService.self) private var authService
@State private var errorMessage = ""
@State private var phoneNumber = ""
@State private var showVerificationCodeInput = false
@State private var verificationCode = ""
@State private var verificationID = ""

public init() {}
}

extension PhoneAuthView: View {
public var body: some View {
if authService.authenticationState != .authenticating {
VStack {
LabeledContent {
TextField(authService.string.enterPhoneNumberLabel, text: $phoneNumber)
.textInputAutocapitalization(.never)
.disableAutocorrection(true)
.submitLabel(.next)
} label: {
Image(systemName: "at")
}.padding(.vertical, 6)
.background(Divider(), alignment: .bottom)
.padding(.bottom, 4)
Button(action: {
Task {
do {
let id = try await authService.verifyPhoneNumber(phoneNumber: phoneNumber)
verificationID = id
showVerificationCodeInput = true
} catch {
errorMessage = authService.string.localizedErrorMessage(
for: error
)
}
}
}) {
Text(authService.string.smsCodeSendButtonLabel)
.padding(.vertical, 8)
.frame(maxWidth: .infinity)
}
.disabled(!PhoneUtils.isValidPhoneNumber(phoneNumber))
.padding([.top, .bottom], 8)
.frame(maxWidth: .infinity)
.buttonStyle(.borderedProminent)
Text(errorMessage).foregroundColor(.red)
}.sheet(isPresented: $showVerificationCodeInput) {
TextField(authService.string.phoneNumberVerificationCodeLabel, text: $verificationCode)
.keyboardType(.numberPad)
.padding()
.background(Color(.systemGray6))
.cornerRadius(8)
.padding(.horizontal)

Button(action: {
Task {
do {
try await authService.signInWithPhoneNumber(
verificationID: verificationID,
verificationCode: verificationCode
)
} catch {
errorMessage = authService.string.localizedErrorMessage(for: error)
}
showVerificationCodeInput = false
authService.dismissModal()
}
}) {
Text(authService.string.verifyPhoneNumberAndSignInLabel)
.foregroundColor(.white)
.padding()
.frame(maxWidth: .infinity)
.background(Color.green)
.cornerRadius(8)
.padding(.horizontal)
}
}.onOpenURL { url in
authService.auth.canHandle(url)
}
} else {
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: .white))
.padding(.vertical, 8)
.frame(maxWidth: .infinity)
}
}
}

#Preview {
FirebaseOptions.dummyConfigurationForPreview()
return PhoneAuthView()
.environment(AuthService())
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ struct ContentView: View {
configuration: configuration
)
.withGoogleSignIn()
.withFacebookSignIn()
.withPhoneSignIn()
.withFacebookSignIn()
.withEmailSignIn()
}

Expand Down