In the main login screen, users can select from different sign-in providers. Choosing one triggers the signInWithCredential function, which attempts to login and link the account if necessary.
However, Apple allows users to hide their email for privacy, preventing Firebase from recognizing the user and linking the account to other providers. Even using the JWT sub token for unique user identification doesn't solve the issue.
When the user logs out and re-enters using another provider, there's no way to retrieve the Apple user ID from the same database node, as Firebase assigns a new user ID. The original user ID gets stored in the database when a user first logs in.
/// Asynchronously signs in a user with a provided set of credentials.
/// - Parameters:
/// - credential: Authentication credentials to sign in the user.
/// - appleJWTToken: An optional JWT token for Apple sign-in (default is nil).
/// - Throws: An error of type `NSError` for any exceptions during the execution.
/// - Returns: No return value.
func signInWithCredential(credential: AuthCredential, appleJWTToken: String? = nil) async throws {
do {
// Attempt user authentication with provided credentials.
let authResult = try await Auth.auth().signIn(with: credential)
let currentUser = authResult.user
// Verify the provider of the authentication credentials.
if credential.provider == FirebaseProviders.Apple.rawValue, let appleJWTToken = appleJWTToken {
// If the provider is Apple, store the JWT token in the database.
try await storeJWTToken(appleJWTToken, forUser: currentUser.uid)
} else if let pendingSignInData = pendingSignInData {
// If a pending credential exists, link the account with it.
try await linkFirebaseAccount(with: pendingSignInData.credential)
} else {
// Handle logins from other providers, and link Apple account if needed.
let isAlreadyLinked = isLinkedWithApple(forUser: currentUser.uid)
let appleJWT = try await retrieveAppleJWT(forUser: currentUser.uid)
if let appleJWT = appleJWT, !isAlreadyLinked {
// If Apple JWT exists and the user isn't already linked with Apple, create Apple credentials and link the account.
let appleCredential = OAuthProvider.credential(withProviderID: FirebaseProviders.Apple.rawValue, idToken: appleJWT, rawNonce: nil)
try await linkFirebaseAccount(with: appleCredential)
}
// Link other provider accounts, such as Google with Facebook.
try await linkFirebaseAccount(with: credential)
}
} catch let error as NSError {
// Handle errors related to account conflict or sign-in failure.
if error.code == AuthErrorCode.accountExistsWithDifferentCredential.rawValue {
let email = error.userInfo[AuthErrorUserInfoEmailKey] as! String
let pendingCredential = error.userInfo[AuthErrorUserInfoUpdatedCredentialKey] as! AuthCredential
guard let providers = try? await Auth.auth().fetchSignInMethods(forEmail: email), let provider = providers.first else {
throw SignInError.providerNotFound
}
pendingSignInData = PendingSignInData(email: email, credential: pendingCredential, method: provider)
throw SignInError.signInConflict
} else {
throw SignInError.failedToSignIn(error)
}
}
}
fileprivate func linkFirebaseAccount(with credential: AuthCredential) async throws {
guard let currentUser = Auth.auth().currentUser else {
// Handle error.
throw SignInError.failedToLinkAccount
}
do {
try await currentUser.link(with: credential)
pendingSignInData = nil // Clear the pending data after successful linking.
} catch {
// Handle error.
throw SignInError.failedToLinkAccount
}
}