Search code examples
firebaseflutterfirebase-authenticationgoogle-authenticationfacebook-authentication

Flutter: have different authentication providers in Firebase


I have different methods in my app to log in:

  • Facebook
  • Google
  • Apple
  • Email

For the question I'll focus on the first 2 ones. When the user logs in with Facebook the providers look like this:

enter image description here

That's fine but if I log out and log in again, this time with a new Google account but using same email, the providers look like this:

enter image description here

Now, if I log out and log in again with Facebook I face the account-exists-with-different-credential error. Something for which I have the logic prepared and show its provider login method, but this user should have both provider available and he should be able to log in with both methods.

This is my code:

Future facebookSignIn(BuildContext context) async {
    final LoginResult result = await FacebookAuth.instance.login();

    if (result.status == LoginStatus.success) {
      final AccessToken accessToken = result.accessToken!;
      AuthCredential credential =
          FacebookAuthProvider.credential(accessToken.token);
      await _firebaseCredential(context, credential);
    }
  }

 Future googleSignIn(BuildContext context,
      [String? email, facebookCredential]) async {
    try {
      GoogleSignInAccount googleUser;
      dynamic popup = await _googleSignIn.signIn();

      // cancelled login
      if (popup == null) {
        return null;
      }

      googleUser = popup;

      GoogleSignInAuthentication googleAuth = await googleUser.authentication;
      final AuthCredential credential = GoogleAuthProvider.credential(
        accessToken: googleAuth.accessToken,
        idToken: googleAuth.idToken,
      );
      await _firebaseCredential(context, credential);
    } on FirebaseAuthException catch (e) {
      // await FirebaseCrashlytics.instance.recordError(
      //   e,
      //   StackTrace.fromString("/googleSignIn"),
      //   reason: e.message,
      // );
      // return null;
    }
  }

 _firebaseCredential(BuildContext context, credential) async {
    try {
      User user =
          (await FirebaseAuth.instance.signInWithCredential(credential)).user!;
      // Provider.of<MyRents>(context, listen: false).updateUI();
      await firebaseProfile.updateUserData(context, user);
    } on FirebaseAuthException catch (error) {
      // final error = e as FirebaseAuthException;
      if (error.code == 'account-exists-with-different-credential') {
        String email = error.email!;
        // AuthCredential pendingCredential = e.credential;

        List<String> signInMethods =
            await FirebaseAuth.instance.fetchSignInMethodsForEmail(email);

        // If the user has several sign-in methods,
        // the first method in the list will be the "recommended" method to use.
        if (signInMethods.first == 'google.com' ||
            signInMethods.first == 'facebook.com') {
          // TODO: fix facebook
          return await googleSignIn(context, email, credential);
        } else {
          ScaffoldMessenger.of(context)
              .showSnackBar(SnackBar(content: Text(error.message!)));
        }
      } else {
        ScaffoldMessenger.of(context)
            .showSnackBar(SnackBar(content: Text(error.message!)));
      }
    }
  }

Am I missing something?

flutter_facebook_auth: ^4.3.3
google_sign_in: ^5.2.1

Solution

  • Future googleSignIn(BuildContext context,
          [String? email, facebookCredential]) async {
        try {
          GoogleSignInAccount googleUser;
          dynamic popup = await _googleSignIn.signIn();
    
          // cancelled login
          if (popup == null) {
            return null;
          }
    
          googleUser = popup;
    
          GoogleSignInAuthentication googleAuth = await googleUser.authentication;
          final AuthCredential credential = GoogleAuthProvider.credential(
            accessToken: googleAuth.accessToken,
            idToken: googleAuth.idToken,
          );
          await _firebaseCredential(context, credential);
        } on FirebaseAuthException catch (e) {
          // await FirebaseCrashlytics.instance.recordError(
          //   e,
          //   StackTrace.fromString("/googleSignIn"),
          //   reason: e.message,
          // );
          // return null;
        }
      }
    
    Future facebookSignIn(BuildContext context) async {
        final LoginResult result = await FacebookAuth.instance.login();
    
        if (result.status == LoginStatus.success) {
          final AccessToken accessToken = result.accessToken!;
          AuthCredential credential =
              FacebookAuthProvider.credential(accessToken.token);
    
          await _firebaseCredential(context, credential);
        }
      }
    
    // other methods...
    
    _firebaseCredential(BuildContext context, credential) async {
        try {
          User user =
              (await FirebaseAuth.instance.signInWithCredential(credential)).user!;
          await firebaseProfile.updateUserData(context, user);
        } on FirebaseAuthException catch (error) {
          if (error.code == 'account-exists-with-different-credential') {
            String email = error.email!;
            List<String> signInMethods =
                await FirebaseAuth.instance.fetchSignInMethodsForEmail(email);
            // bool newUser = (signInMethods.length > 0) ? false : true;
    
            // If the user has several sign-in methods,
            // the first method in the list will be the "recommended" method to use.
            var user;
            switch (signInMethods.first) {
              case 'google.com':
                user = await googleSignIn(context, email, credential);
                break;
              case 'facebook.com':
                user = await facebookSignIn(context);
                break;
              case 'apple.com':
                user = await appleSignIn(context);
                break;
              case 'password':
                // since password is managed by user we force have email provider only
                ScaffoldMessenger.of(context).showSnackBar(SnackBar(
                    content: Text(translate('auth.signInMethods_password'))));
                break;
              // TODO: apple
            }
            await linkProvider(context, credential);
            return user;
          }
    
          return ScaffoldMessenger.of(context)
              .showSnackBar(SnackBar(content: Text(error.message!)));
        }
      }
    
      // just some extra error covering
      Future linkProvider(BuildContext context, credential) async {
        try {
          await FirebaseAuth.instance.currentUser?.linkWithCredential(credential);
        } on FirebaseAuthException catch (e) {
          switch (e.code) {
            case "provider-already-linked":
              ScaffoldMessenger.of(context).showSnackBar(SnackBar(
                  content: Text(translate('auth.provider_already_linked'))));
              break;
            case "invalid-credential":
              ScaffoldMessenger.of(context).showSnackBar(
                  SnackBar(content: Text(translate('auth.invalid_credential'))));
              break;
            case "credential-already-in-use":
              ScaffoldMessenger.of(context).showSnackBar(SnackBar(
                  content: Text(translate('auth.credential_already_in_use'))));
              break;
            default:
              ScaffoldMessenger.of(context).showSnackBar(
                  SnackBar(content: Text(translate('auth.something_happened'))));
          }
        }
      }
    

    If you do Google -> Facebook it will look like this:

    enter image description here

    Other way around only Google will be present if your Google email is a trusted email (gmail). More info about that: https://groups.google.com/g/firebase-talk/c/ms_NVQem_Cw/m/8g7BFk1IAAAJ