Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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 @@ -19,6 +19,7 @@ import SwiftUI
@MainActor
public struct SignInWithAppleButton {
@Environment(AuthService.self) private var authService
@Environment(\.signInWithMergeConflictHandler) private var signInHandler
let provider: AuthProviderSwift
public init(provider: AuthProviderSwift) {
self.provider = provider
Expand All @@ -29,7 +30,13 @@ extension SignInWithAppleButton: View {
public var body: some View {
Button(action: {
Task {
try? await authService.signIn(provider)
if let handler = signInHandler {
try? await handler(authService) {
try await authService.signIn(provider)
}
} else {
try? await authService.signIn(provider)
}
}
}) {
HStack {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,8 @@ public final class AuthService {
public func signOut() async throws {
do {
try await auth.signOut()
// Cannot wait for auth listener to change, feedback needs to be immediate
currentUser = nil
updateAuthenticationState()
} catch {
updateError(message: string.localizedErrorMessage(for: error))
Expand All @@ -202,7 +204,7 @@ public final class AuthService {
}
}

public func handleAutoUpgradeAnonymousUser(credentials: AuthCredential) async throws
private func handleAutoUpgradeAnonymousUser(credentials: AuthCredential) async throws
-> SignInOutcome {
if currentUser == nil {
throw AuthServiceError.noCurrentUser
Expand All @@ -213,11 +215,27 @@ public final class AuthService {
updateAuthenticationState()
return .signedIn(result)
} catch let error as NSError {
// Handle credentialAlreadyInUse error
if error.code == AuthErrorCode.credentialAlreadyInUse.rawValue {
// Extract the updated credential from the error
let updatedCredential = error.userInfo["FIRAuthUpdatedCredentialKey"] as? AuthCredential
?? credentials

let context = AccountMergeConflictContext(
credential: updatedCredential,
underlyingError: error,
message: "Unable to merge accounts. The credential is already associated with a different account.",
uid: currentUser?.uid
)
throw AuthServiceError.accountMergeConflict(context: context)
}

// Handle emailAlreadyInUse error
if error.code == AuthErrorCode.emailAlreadyInUse.rawValue {
let context = AccountMergeConflictContext(
credential: credentials,
underlyingError: error,
message: "Unable to merge accounts. Use the credential in the context to resolve the conflict.",
message: "Unable to merge accounts. This email is already associated with a different account.",
uid: currentUser?.uid
)
throw AuthServiceError.accountMergeConflict(context: context)
Expand Down
2 changes: 1 addition & 1 deletion FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Version.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,4 @@
public enum FirebaseAuthSwiftUIVersion {
// Use the release-swift.sh script to bump this version number, commit and push a new tag.
public static let version = "15.1.0"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,59 @@
import FirebaseCore
import SwiftUI

// MARK: - Merge Conflict Handling

/// Helper function to handle sign-in with automatic merge conflict resolution.
///
/// This function attempts to sign in with the provided action. If a merge conflict occurs
/// (when an anonymous user is being upgraded and the credential is already associated with
/// an existing account), it automatically signs out the anonymous user and signs in with
/// the existing account's credential.
///
/// - Parameters:
/// - authService: The AuthService instance to use for sign-in operations
/// - signInAction: An async closure that performs the sign-in operation
/// - Returns: The SignInOutcome from the successful sign-in
/// - Throws: Re-throws any errors except accountMergeConflict (which is handled internally)
@MainActor
public func signInWithMergeConflictHandling(authService: AuthService,
signInAction: () async throws
-> SignInOutcome) async throws -> SignInOutcome {
do {
return try await signInAction()
} catch let error as AuthServiceError {
if case let .accountMergeConflict(context) = error {
// The anonymous account conflicts with an existing account
// Sign out the anonymous user
try await authService.signOut()

// Sign in with the existing account's credential
// This works because shouldHandleAnonymousUpgrade is now false after sign out
return try await authService.signIn(credentials: context.credential)
}
throw error
}
}

// MARK: - Environment Key for Sign-In Handler

/// Environment key for a sign-in handler that includes merge conflict resolution
private struct SignInHandlerKey: EnvironmentKey {
static let defaultValue: (@MainActor (AuthService, () async throws -> SignInOutcome) async throws
-> SignInOutcome)? = nil
}

public extension EnvironmentValues {
/// A sign-in handler that automatically handles merge conflicts for anonymous user upgrades.
/// When set in the environment, views should use this handler to wrap their sign-in calls.
var signInWithMergeConflictHandler: (@MainActor (AuthService,
() async throws -> SignInOutcome) async throws
-> SignInOutcome)? {
get { self[SignInHandlerKey.self] }
set { self[SignInHandlerKey.self] = newValue }
}
}

@MainActor
public struct AuthPickerView {
@Environment(AuthService.self) private var authService
Expand Down Expand Up @@ -67,10 +120,13 @@ extension AuthPickerView: View {
.emailLoginFlowLabel : authService.string.emailSignUpFlowLabel)
Divider()
EmailAuthView()
.environment(\.signInWithMergeConflictHandler, signInWithMergeConflictHandling)
}
VStack {
authService.renderButtons()
}.padding(.horizontal)
}
.padding(.horizontal)
.environment(\.signInWithMergeConflictHandler, signInWithMergeConflictHandling)
if authService.emailSignInEnabled {
Divider()
HStack {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ private enum FocusableField: Hashable {
@MainActor
public struct EmailAuthView {
@Environment(AuthService.self) private var authService
@Environment(\.signInWithMergeConflictHandler) private var signInHandler

@State private var email = ""
@State private var password = ""
Expand All @@ -49,11 +50,23 @@ public struct EmailAuthView {
}

private func signInWithEmailPassword() async {
try? await authService.signIn(email: email, password: password)
if let handler = signInHandler {
try? await handler(authService) {
try await authService.signIn(email: email, password: password)
}
} else {
try? await authService.signIn(email: email, password: password)
}
}

private func createUserWithEmailPassword() async {
try? await authService.createUser(email: email, password: password)
if let handler = signInHandler {
try? await handler(authService) {
try await authService.createUser(email: email, password: password)
}
} else {
try? await authService.createUser(email: email, password: password)
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import SwiftUI
@MainActor
public struct SignInWithFacebookButton {
@Environment(AuthService.self) private var authService
@Environment(\.signInWithMergeConflictHandler) private var signInHandler
let facebookProvider: FacebookProviderSwift
@State private var showCanceledAlert = false
@State private var limitedLogin = true
Expand Down Expand Up @@ -67,7 +68,13 @@ extension SignInWithFacebookButton: View {
Button(action: {
Task {
facebookProvider.isLimitedLogin = limitedLogin
try? await authService.signIn(facebookProvider)
if let handler = signInHandler {
try? await handler(authService) {
try await authService.signIn(facebookProvider)
}
} else {
try? await authService.signIn(facebookProvider)
}
}
}) {
HStack {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import SwiftUI
@MainActor
public struct SignInWithGoogleButton {
@Environment(AuthService.self) private var authService
@Environment(\.signInWithMergeConflictHandler) private var signInHandler
let googleProvider: AuthProviderSwift

public init(googleProvider: AuthProviderSwift) {
Expand All @@ -43,7 +44,13 @@ extension SignInWithGoogleButton: View {
public var body: some View {
GoogleSignInButton(viewModel: customViewModel) {
Task {
try? await authService.signIn(googleProvider)
if let handler = signInHandler {
try? await handler(authService) {
try await authService.signIn(googleProvider)
}
} else {
try? await authService.signIn(googleProvider)
}
}
}
.accessibilityIdentifier("sign-in-with-google-button")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import SwiftUI
@MainActor
public struct GenericOAuthButton {
@Environment(AuthService.self) private var authService
@Environment(\.signInWithMergeConflictHandler) private var signInHandler
let provider: AuthProviderSwift
public init(provider: AuthProviderSwift) {
self.provider = provider
Expand All @@ -36,7 +37,13 @@ extension GenericOAuthButton: View {
return AnyView(
Button(action: {
Task {
try await authService.signIn(provider)
if let handler = signInHandler {
try? await handler(authService) {
try await authService.signIn(provider)
}
} else {
try? await authService.signIn(provider)
}
}
}) {
HStack {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import SwiftUI
@MainActor
public struct PhoneAuthButtonView {
@Environment(AuthService.self) private var authService
@Environment(\.signInWithMergeConflictHandler) private var signInHandler
let phoneProvider: PhoneAuthProviderSwift

public init(phoneProvider: PhoneAuthProviderSwift) {
Expand All @@ -30,7 +31,13 @@ extension PhoneAuthButtonView: View {
public var body: some View {
Button(action: {
Task {
try await authService.signIn(phoneProvider)
if let handler = signInHandler {
try? await handler(authService) {
try await authService.signIn(phoneProvider)
}
} else {
try? await authService.signIn(phoneProvider)
}
}
}) {
Label("Sign in with Phone", systemImage: "phone.fill")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import SwiftUI
@MainActor
public struct SignInWithTwitterButton {
@Environment(AuthService.self) private var authService
@Environment(\.signInWithMergeConflictHandler) private var signInHandler
let provider: AuthProviderSwift
public init(provider: AuthProviderSwift) {
self.provider = provider
Expand All @@ -29,7 +30,13 @@ extension SignInWithTwitterButton: View {
public var body: some View {
Button(action: {
Task {
try? await authService.signIn(provider)
if let handler = signInHandler {
try? await handler(authService) {
try await authService.signIn(provider)
}
} else {
try? await authService.signIn(provider)
}
}
}) {
HStack {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,15 @@ struct ContentView: View {
let authService: AuthService

init() {
Auth.auth().signInAnonymously()
let actionCodeSettings = ActionCodeSettings()
actionCodeSettings.handleCodeInApp = true
actionCodeSettings
.url = URL(string: "https://flutterfire-e2e-tests.firebaseapp.com")
actionCodeSettings.linkDomain = "flutterfire-e2e-tests.firebaseapp.com"
actionCodeSettings.setIOSBundleID(Bundle.main.bundleIdentifier!)
let configuration = AuthConfiguration(
shouldAutoUpgradeAnonymousUsers: true,
tosUrl: URL(string: "https://example.com/tos"),
privacyPolicyUrl: URL(string: "https://example.com/privacy"),
emailLinkSignInActionCodeSettings: actionCodeSettings,
Expand Down
Loading