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 @@ -34,21 +34,19 @@ public struct AuthConfiguration {
public let allowedSecondFactors: Set<SecondFactorType>
public let mfaIssuer: String

public init(
logo: ImageResource? = nil,
languageCode: String? = nil,
shouldHideCancelButton: Bool = false,
interactiveDismissEnabled: Bool = true,
shouldAutoUpgradeAnonymousUsers: Bool = false,
customStringsBundle: Bundle? = nil,
tosUrl: URL? = nil,
privacyPolicyUrl: URL? = nil,
emailLinkSignInActionCodeSettings: ActionCodeSettings? = nil,
verifyEmailActionCodeSettings: ActionCodeSettings? = nil,
mfaEnabled: Bool = false,
allowedSecondFactors: Set<SecondFactorType> = [.sms, .totp],
mfaIssuer: String = "Firebase Auth"
) {
public init(logo: ImageResource? = nil,
languageCode: String? = nil,
shouldHideCancelButton: Bool = false,
interactiveDismissEnabled: Bool = true,
shouldAutoUpgradeAnonymousUsers: Bool = false,
customStringsBundle: Bundle? = nil,
tosUrl: URL? = nil,
privacyPolicyUrl: URL? = nil,
emailLinkSignInActionCodeSettings: ActionCodeSettings? = nil,
verifyEmailActionCodeSettings: ActionCodeSettings? = nil,
mfaEnabled: Bool = false,
allowedSecondFactors: Set<SecondFactorType> = [.sms, .totp],
mfaIssuer: String = "Firebase Auth") {
self.logo = logo
self.shouldHideCancelButton = shouldHideCancelButton
self.interactiveDismissEnabled = interactiveDismissEnabled
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ public protocol AuthProviderUI {

public protocol PhoneAuthProviderSwift: AuthProviderSwift {
@MainActor func verifyPhoneNumber(phoneNumber: String) async throws -> String
@MainActor func createAuthCredential(verificationId: String, verificationCode: String) async throws -> AuthCredential
@MainActor func createAuthCredential(verificationId: String,
verificationCode: String) async throws -> AuthCredential
}

public enum AuthenticationState {
Expand Down Expand Up @@ -109,7 +110,10 @@ public final class AuthService {
public init(configuration: AuthConfiguration = AuthConfiguration(), auth: Auth = Auth.auth()) {
self.auth = auth
self.configuration = configuration
string = StringUtils(bundle: configuration.customStringsBundle ?? Bundle.module, languageCode: configuration.languageCode)
string = StringUtils(
bundle: configuration.customStringsBundle ?? Bundle.module,
languageCode: configuration.languageCode
)
listenerManager = AuthListenerManager(auth: auth, authEnvironment: self)
FirebaseApp.registerLibrary("firebase-ui-ios", withVersion: FirebaseAuthSwiftUIVersion.version)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ public class StringUtils {

public func localizedString(for key: String) -> String {
// If a specific language code is set, load strings from that language bundle
if let languageCode, let path = bundle.path(forResource: languageCode, ofType: "lproj"), let localizedBundle = Bundle(path: path) {
if let languageCode, let path = bundle.path(forResource: languageCode, ofType: "lproj"),
let localizedBundle = Bundle(path: path) {
return localizedBundle.localizedString(forKey: key, value: nil, table: "Localizable")
}

Expand Down Expand Up @@ -605,14 +606,18 @@ public class StringUtils {
/// found in:
/// - MFAEnrolmentView
public var authenticatorAppInstructionsMessage: String {
return localizedString(for: "Use an authenticator app like Google Authenticator or Authy to generate verification codes.")
return localizedString(
for: "Use an authenticator app like Google Authenticator or Authy to generate verification codes."
)
}

/// Set up two-factor authentication to add an extra layer of security to your account.
/// found in:
/// - MFAEnrolmentView
public var setUpTwoFactorAuthMessage: String {
return localizedString(for: "Set up two-factor authentication to add an extra layer of security to your account.")
return localizedString(
for: "Set up two-factor authentication to add an extra layer of security to your account."
)
}

/// We'll send a verification code to this number
Expand All @@ -633,7 +638,9 @@ public class StringUtils {
/// found in:
/// - MFAEnrolmentView
public var sendVerificationCodeEachSignInMessage: String {
return localizedString(for: "We'll send a verification code to your phone number each time you sign in.")
return localizedString(
for: "We'll send a verification code to your phone number each time you sign in."
)
}

/// Unable to generate QR Code
Expand Down Expand Up @@ -663,7 +670,9 @@ public class StringUtils {
/// - MFAEnrolmentView
/// - MFAResolutionView
public var mfaNotEnabledMessage: String {
return localizedString(for: "MFA is not enabled in the current configuration. Please contact your administrator.")
return localizedString(
for: "MFA is not enabled in the current configuration. Please contact your administrator."
)
}

/// No Authentication Methods Available
Expand All @@ -679,7 +688,9 @@ public class StringUtils {
/// - MFAEnrolmentView
/// - MFAResolutionView
public var noMFAMethodsConfiguredMessage: String {
return localizedString(for: "No MFA methods are configured as allowed. Please contact your administrator.")
return localizedString(
for: "No MFA methods are configured as allowed. Please contact your administrator."
)
}

/// Complete sign-in with your second factor
Expand Down Expand Up @@ -823,11 +834,14 @@ public class StringUtils {
return localizedString(for: "Delete Account?")
}

/// This action cannot be undone. All your data will be permanently deleted. You may need to reauthenticate to complete this action.
/// This action cannot be undone. All your data will be permanently deleted. You may need to
/// reauthenticate to complete this action.
/// found in:
/// - SignedInView
public var deleteAccountWarningMessage: String {
return localizedString(for: "This action cannot be undone. All your data will be permanently deleted. You may need to reauthenticate to complete this action.")
return localizedString(
for: "This action cannot be undone. All your data will be permanently deleted. You may need to reauthenticate to complete this action."
)
}

/// Invalid OAuth Provider error
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public struct AuthPickerView<Content: View> {
public init(@ViewBuilder content: @escaping () -> Content = { EmptyView() }) {
self.content = content
}

@Environment(AuthService.self) private var authService
private let content: () -> Content

Expand Down Expand Up @@ -61,7 +61,10 @@ extension AuthPickerView: View {
case AuthView.enterPhoneNumber:
EnterPhoneNumberView()
case let .enterVerificationCode(verificationID, fullPhoneNumber):
EnterVerificationCodeView(verificationID: verificationID, fullPhoneNumber: fullPhoneNumber)
EnterVerificationCodeView(
verificationID: verificationID,
fullPhoneNumber: fullPhoneNumber
)
}
}
}
Expand Down Expand Up @@ -123,7 +126,7 @@ extension AuthPickerView: View {
}
}
}

@ToolbarContentBuilder
var toolbar: some ToolbarContent {
ToolbarItem(placement: .topBarTrailing) {
Expand All @@ -137,7 +140,7 @@ extension AuthPickerView: View {
}
}
}

@ViewBuilder
var authPickerViewInternal: some View {
@Bindable var authService = authService
Expand Down Expand Up @@ -167,7 +170,7 @@ extension AuthPickerView: View {
okButtonLabel: authService.string.okButtonLabel
)
}

@ViewBuilder
var authMethodPicker: some View {
GeometryReader { proxy in
Expand All @@ -187,7 +190,7 @@ extension AuthPickerView: View {
}
}
}

@ViewBuilder
func otherSignInOptions(_ proxy: GeometryProxy) -> some View {
VStack {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,27 +32,27 @@ private enum FocusableField: Hashable {
@MainActor
public struct EmailAuthView {
@Environment(AuthService.self) private var authService

@State private var email = ""
@State private var password = ""
@State private var confirmPassword = ""

@FocusState private var focus: FocusableField?

public init() {}

private var isValid: Bool {
return if authService.authenticationFlow == .signIn {
!email.isEmpty && !password.isEmpty
} else {
!email.isEmpty && !password.isEmpty && password == confirmPassword
}
}

private func signInWithEmailPassword() async {
try? await authService.signIn(email: email, password: password)
}

private func createUserWithEmailPassword() async {
try? await authService.createUser(email: email, password: password)
}
Expand Down Expand Up @@ -101,7 +101,7 @@ extension EmailAuthView: View {
}
.accessibilityIdentifier("password-recovery-button")
}

if authService.authenticationFlow == .signUp {
AuthTextField(
text: $confirmPassword,
Expand All @@ -120,7 +120,7 @@ extension EmailAuthView: View {
.focused($focus, equals: .confirmPassword)
.accessibilityIdentifier("confirm-password-field")
}

Button(action: {
Task {
if authService.authenticationFlow == .signIn {
Expand All @@ -133,8 +133,8 @@ extension EmailAuthView: View {
if authService.authenticationState != .authenticating {
Text(
authService.authenticationFlow == .signIn
? authService.string.signInWithEmailButtonLabel
: authService.string.signUpWithEmailButtonLabel
? authService.string.signInWithEmailButtonLabel
: authService.string.signUpWithEmailButtonLabel
)
.padding(.vertical, 8)
.frame(maxWidth: .infinity)
Expand All @@ -154,22 +154,22 @@ extension EmailAuthView: View {
Button(action: {
withAnimation {
authService.authenticationFlow =
authService
.authenticationFlow == .signIn ? .signUp : .signIn
authService
.authenticationFlow == .signIn ? .signUp : .signIn
}
}) {
HStack(spacing: 4) {
Text(
authService
.authenticationFlow == .signIn
? authService.string.dontHaveAnAccountYetLabel
: authService.string.alreadyHaveAnAccountLabel
? authService.string.dontHaveAnAccountYetLabel
: authService.string.alreadyHaveAnAccountLabel
)
.foregroundStyle(Color(.label))
Text(
authService.authenticationFlow == .signUp
? authService.string.emailLoginFlowLabel
: authService.string.emailSignUpFlowLabel
? authService.string.emailLoginFlowLabel
: authService.string.emailSignUpFlowLabel
)
.fontWeight(.semibold)
.foregroundColor(.blue)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,7 @@ struct EnterPhoneNumberView: View {
verificationID: id,
fullPhoneNumber: fullPhoneNumber
))
} catch {
}
} catch {}
}
}) {
if authService.authenticationState == .authenticating {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,14 @@ struct EnterVerificationCodeView: View {
guard let provider = authService.currentPhoneProvider else {
fatalError("No phone provider found")
}
let credential = try await provider.createAuthCredential(verificationId: verificationID, verificationCode: verificationCode)
let credential = try await provider.createAuthCredential(
verificationId: verificationID,
verificationCode: verificationCode
)

_ = try await authService.signIn(credentials: credential)
authService.navigator.clear()
} catch {

}
} catch {}
}
}) {
if authService.authenticationState == .authenticating {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,10 @@ public class PhoneProviderSwift: PhoneAuthProviderSwift {
@MainActor public func createAuthCredential() async throws -> AuthCredential {
fatalError("Not implemented")
}

@MainActor public func createAuthCredential(verificationId: String, verificationCode: String) async throws -> AuthCredential {

@MainActor public func createAuthCredential(verificationId: String,
verificationCode: String) async throws
-> AuthCredential {
return PhoneAuthProvider.provider()
.credential(withVerificationID: verificationId, verificationCode: verificationCode)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ func createEmail() -> String {

/// Helper to create a test user in the emulator via REST API (avoids keychain issues)
@MainActor func createTestUser(email: String, password: String = "123456",
verifyEmail: Bool = false) async throws {
verifyEmail: Bool = false) async throws {
// Use Firebase Auth emulator REST API directly to avoid keychain access issues in UI tests
let signUpUrl =
"http://127.0.0.1:9099/identitytoolkit.googleapis.com/v1/accounts:signUp?key=fake-api-key"
Expand Down Expand Up @@ -78,9 +78,9 @@ func createEmail() -> String {

/// Verifies an email address in the emulator using the OOB code mechanism
@MainActor func verifyEmailInEmulator(email: String,
idToken: String,
projectID: String = "flutterfire-e2e-tests",
emulatorHost: String = "127.0.0.1:9099") async throws {
idToken: String,
projectID: String = "flutterfire-e2e-tests",
emulatorHost: String = "127.0.0.1:9099") async throws {
let base = "http://\(emulatorHost)"

// Step 1: Trigger email verification (creates OOB code in emulator)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,29 +34,29 @@ final class UpgradeAccountUITests: XCTestCase {
let email = createEmail()
let password = "123456"
try await createTestUser(email: email, password: password)

// Launch app with anonymous sign-in enabled
let app = createTestApp()
app.launchArguments.append("--anonymous-sign-in-enabled")
app.launch()

// Wait for sign-in screen to appear
let emailField = app.textFields["email-field"]
XCTAssertTrue(emailField.waitForExistence(timeout: 6), "Email field should exist")
emailField.tap()
emailField.typeText(email)

let passwordField = app.secureTextFields["password-field"]
XCTAssertTrue(passwordField.exists, "Password field should exist")
passwordField.tap()
passwordField.typeText(password)

let signInButton = app.buttons["sign-in-button"]
XCTAssertTrue(signInButton.exists, "Sign-In button should exist")
signInButton.tap()

let signedInText = app.staticTexts["signed-in-text"]

// Wait for authentication to complete and signed-in view to appear
XCTAssertTrue(
signedInText.waitForExistence(timeout: 30),
Expand Down
Loading