Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[🐛] "auth"/linkWithCredential, does not return credential-already-in-use #4911

Closed
2 of 10 tasks
cqwcent opened this issue Feb 15, 2021 · 34 comments · Fixed by #7793
Closed
2 of 10 tasks

[🐛] "auth"/linkWithCredential, does not return credential-already-in-use #4911

cqwcent opened this issue Feb 15, 2021 · 34 comments · Fixed by #7793
Labels
platform: android plugin: authentication Firebase Authentication type: bug New bug report Type: Stale Issue has become stale - automatically added by Stale bot Workflow: Needs Review Pending feedback or review from a maintainer.

Comments

@cqwcent
Copy link

cqwcent commented Feb 15, 2021

Issue

Application with email/password, and Facebook login.

On Android, create email/password account, name it ABC. And link it with Facebook account. Success. Expected.
On Android, create another email/password account, name it XYZ. And try to link it with the same Facebook account. Expecting an error "credential-already-in-use", but instead, "auth" proceed login process and retrieved ABC's information.

Wipe out ABC and XYZ info on firebase console.

On IOS, create email/password account, name it ABC. And link it with Facebook account. Success. Expected.
On IOS, create another email/password account, name it XYZ. And try to link it with the same Facebook account. Got "credential-already-in-use", as expected.

GIF, Android, not as expected.

android_link_social_error

Screen recording, IOS, expected.

mac_link_social_expected.mov

Code used to link:

const linkToFacebook = () => {
  .......
  // Create a Firebase credential with the AccessToken
  const facebookCredential = auth.FacebookAuthProvider.credential(
      data.accessToken
  );
  
  // link with this fb account
  // quesiton, if this fb account is linked to anther firebase account, the current implementation will change the user to that one.
  
  auth()
      .currentUser?.linkWithCredential(facebookCredential)
      .catch((error) => {
          dispatch(setLoginError(errorLoginMessage('Facebook')));
      });
}

Project Files

Javascript

Click To Expand

package.json:

"dependencies": {
    "@invertase/react-native-apple-authentication": "^2.1.0",
    "@react-native-community/async-storage": "^1.12.1",
    "@react-native-community/google-signin": "^5.0.0",
    "@react-native-community/masked-view": "^0.1.10",
    "@react-native-firebase/app": "^10.6.4",
    "@react-native-firebase/auth": "^10.6.4",
    "@react-navigation/bottom-tabs": "^5.11.7",
    "@react-navigation/native": "^5.9.2",
    "@react-navigation/stack": "^5.14.2",
    "deepmerge": "^4.2.2",
    "react": "16.13.1",
    "react-native": "0.63.4",
    "react-native-animatable": "^1.3.3",
    "react-native-fbsdk": "^3.0.0",
    "react-native-gesture-handler": "^1.9.0",
    "react-native-image-crop-picker": "^0.35.3",
    "react-native-onboarding-swiper": "^1.1.4",
    "react-native-paper": "^4.7.1",
    "react-native-reanimated": "^1.13.2",
    "react-native-safe-area-context": "^3.1.9",
    "react-native-screens": "^2.17.1",
    "react-native-step-indicator": "^1.0.3",
    "react-native-vector-icons": "^8.0.0",
    "react-redux": "^7.2.2",
    "react-router-redux": "^4.0.8",
    "reanimated-bottom-sheet": "^1.0.0-alpha.22",
    "redux": "^4.0.5",
    "redux-logger": "^3.0.6",
    "redux-observable": "^1.2.0",
    "redux-thunk": "^2.3.0",
    "rxjs": "^6.6.3",
    "validator": "^13.5.2"
  },```

#### `firebase.json` for react-native-firebase v6:

```json
# N/A

iOS

Click To Expand

ios/Podfile:

  • I'm not using Pods
  • I'm using Pods and my Podfile looks like:
require_relative '../node_modules/react-native/scripts/react_native_pods'
require_relative '../node_modules/@react-native-community/cli-platform-ios/native_modules'

platform :ios, '10.0'

target 'myapp_name' do
  config = use_native_modules!
  pod 'RNVectorIcons', :path => '../node_modules/react-native-vector-icons'
  use_react_native!(:path => config["reactNativePath"])

  pod 'GoogleSignIn', '~> 5.0.2'
  target 'myapp_nameTests' do
    inherit! :complete
    # Pods for testing
  end

  # Enables Flipper.
  #
  # Note that if you have use_frameworks! enabled, Flipper will not work and
  # you should disable these next few lines.
  # use_flipper!
  # post_install do |installer|
    # flipper_post_install(installer)
  # end
end

target 'myapp_name-tvOS' do
  # Pods for myapp_name-tvOS

  target 'myapp_name-tvOSTests' do
    inherit! :search_paths
    # Pods for testing
  end
end

AppDelegate.m:

#import "AppDelegate.h"
#import <Firebase.h>
#import <React/RCTBridge.h>
#import <React/RCTBundleURLProvider.h>
#import <React/RCTRootView.h>

#import <FBSDKCoreKit/FBSDKCoreKit.h>
#import <FBSDKLoginKit/FBSDKLoginKit.h>
#import <FBSDKShareKit/FBSDKShareKit.h>

#import <RNGoogleSignin/RNGoogleSignin.h>

#ifdef FB_SONARKIT_ENABLED
#import <FlipperKit/FlipperClient.h>
#import <FlipperKitLayoutPlugin/FlipperKitLayoutPlugin.h>
#import <FlipperKitUserDefaultsPlugin/FKUserDefaultsPlugin.h>
#import <FlipperKitNetworkPlugin/FlipperKitNetworkPlugin.h>
#import <SKIOSNetworkPlugin/SKIOSNetworkAdapter.h>
#import <FlipperKitReactPlugin/FlipperKitReactPlugin.h>

static void InitializeFlipper(UIApplication *application) {
  FlipperClient *client = [FlipperClient sharedClient];
  SKDescriptorMapper *layoutDescriptorMapper = [[SKDescriptorMapper alloc] initWithDefaults];
  [client addPlugin:[[FlipperKitLayoutPlugin alloc] initWithRootNode:application withDescriptorMapper:layoutDescriptorMapper]];
  [client addPlugin:[[FKUserDefaultsPlugin alloc] initWithSuiteName:nil]];
  [client addPlugin:[FlipperKitReactPlugin new]];
  [client addPlugin:[[FlipperKitNetworkPlugin alloc] initWithNetworkAdapter:[SKIOSNetworkAdapter new]]];
  [client start];
}
#endif

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
  if ([FIRApp defaultApp] == nil) {
    [FIRApp configure];
  }
#ifdef FB_SONARKIT_ENABLED
  InitializeFlipper(application);
#endif

  RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:launchOptions];
  RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge
                                                   moduleName:@"myapp_name"
                                            initialProperties:nil];

  rootView.backgroundColor = [[UIColor alloc] initWithRed:1.0f green:1.0f blue:1.0f alpha:1];

  self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
  UIViewController *rootViewController = [UIViewController new];
  rootViewController.view = rootView;
  self.window.rootViewController = rootViewController;
  [self.window makeKeyAndVisible];
  [[FBSDKApplicationDelegate sharedInstance] application:application
                             didFinishLaunchingWithOptions:launchOptions];
  return YES;
}

- (BOOL)application:(UIApplication *)application
            openURL:(NSURL *)url
            options:(nonnull NSDictionary<UIApplicationOpenURLOptionsKey, id> *)options
{
  [[FBSDKApplicationDelegate sharedInstance] application:application
                                                 openURL:url
                                                 options:options] || [RNGoogleSignin application:application openURL:url options:options];
  return YES;
}

- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge
{
#if DEBUG
  return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil];
#else
  return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
#endif
}

@end


Android

Click To Expand

Have you converted to AndroidX?

  • my application is an AndroidX application?
  • I am using android/gradle.settings jetifier=true for Android compatibility?
  • I am using the NPM package jetifier for react-native compatibility?

android/build.gradle:

// Top-level build file where you can add configuration options common to all sub-projects/modules.

buildscript {
    ext {
        buildToolsVersion = "29.0.2"
        minSdkVersion = 16
        compileSdkVersion = 29
        targetSdkVersion = 29
        googlePlayServicesAuthVersion = "16.0.1"
    }
    repositories {
        google()
        jcenter()
    }
    dependencies {
        classpath("com.android.tools.build:gradle:4.0.1")
        classpath("com.google.gms:google-services:4.3.5")
        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

allprojects {
    repositories {
        mavenLocal()
        mavenCentral()
        maven {
            // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
            url("$rootDir/../node_modules/react-native/android")
        }
        maven {
            // Android JSC is installed from npm
            url("$rootDir/../node_modules/jsc-android/dist")
        }

        google()
        jcenter()
        maven { url 'https://maven.google.com' }
        maven { url 'https://www.jitpack.io' }
    }
}

android/app/build.gradle:

apply plugin: "com.android.application"

import com.android.build.OutputFile

/**
 * The react.gradle file registers a task for each build variant (e.g. bundleDebugJsAndAssets
 * and bundleReleaseJsAndAssets).
 * These basically call `react-native bundle` with the correct arguments during the Android build
 * cycle. By default, bundleDebugJsAndAssets is skipped, as in debug/dev mode we prefer to load the
 * bundle directly from the development server. Below you can see all the possible configurations
 * and their defaults. If you decide to add a configuration block, make sure to add it before the
 * `apply from: "../../node_modules/react-native/react.gradle"` line.
 *
 * project.ext.react = [
 *   // the name of the generated asset file containing your JS bundle
 *   bundleAssetName: "index.android.bundle",
 *
 *   // the entry file for bundle generation. If none specified and
 *   // "index.android.js" exists, it will be used. Otherwise "index.js" is
 *   // default. Can be overridden with ENTRY_FILE environment variable.
 *   entryFile: "index.android.js",
 *
 *   // https://reactnative.dev/docs/performance#enable-the-ram-format
 *   bundleCommand: "ram-bundle",
 *
 *   // whether to bundle JS and assets in debug mode
 *   bundleInDebug: false,
 *
 *   // whether to bundle JS and assets in release mode
 *   bundleInRelease: true,
 *
 *   // whether to bundle JS and assets in another build variant (if configured).
 *   // See http://tools.android.com/tech-docs/new-build-system/user-guide#TOC-Build-Variants
 *   // The configuration property can be in the following formats
 *   //         'bundleIn${productFlavor}${buildType}'
 *   //         'bundleIn${buildType}'
 *   // bundleInFreeDebug: true,
 *   // bundleInPaidRelease: true,
 *   // bundleInBeta: true,
 *
 *   // whether to disable dev mode in custom build variants (by default only disabled in release)
 *   // for example: to disable dev mode in the staging build type (if configured)
 *   devDisabledInStaging: true,
 *   // The configuration property can be in the following formats
 *   //         'devDisabledIn${productFlavor}${buildType}'
 *   //         'devDisabledIn${buildType}'
 *
 *   // the root of your project, i.e. where "package.json" lives
 *   root: "../../",
 *
 *   // where to put the JS bundle asset in debug mode
 *   jsBundleDirDebug: "$buildDir/intermediates/assets/debug",
 *
 *   // where to put the JS bundle asset in release mode
 *   jsBundleDirRelease: "$buildDir/intermediates/assets/release",
 *
 *   // where to put drawable resources / React Native assets, e.g. the ones you use via
 *   // require('./image.png')), in debug mode
 *   resourcesDirDebug: "$buildDir/intermediates/res/merged/debug",
 *
 *   // where to put drawable resources / React Native assets, e.g. the ones you use via
 *   // require('./image.png')), in release mode
 *   resourcesDirRelease: "$buildDir/intermediates/res/merged/release",
 *
 *   // by default the gradle tasks are skipped if none of the JS files or assets change; this means
 *   // that we don't look at files in android/ or ios/ to determine whether the tasks are up to
 *   // date; if you have any other folders that you want to ignore for performance reasons (gradle
 *   // indexes the entire tree), add them here. Alternatively, if you have JS files in android/
 *   // for example, you might want to remove it from here.
 *   inputExcludes: ["android/**", "ios/**"],
 *
 *   // override which node gets called and with what additional arguments
 *   nodeExecutableAndArgs: ["node"],
 *
 *   // supply additional arguments to the packager
 *   extraPackagerArgs: []
 * ]
 */

project.ext.react = [
    enableHermes: false,  // clean and rebuild if changing
]

apply from: "../../node_modules/react-native/react.gradle"

/**
 * Set this to true to create two separate APKs instead of one:
 *   - An APK that only works on ARM devices
 *   - An APK that only works on x86 devices
 * The advantage is the size of the APK is reduced by about 4MB.
 * Upload all the APKs to the Play Store and people will download
 * the correct one based on the CPU architecture of their device.
 */
def enableSeparateBuildPerCPUArchitecture = false

/**
 * Run Proguard to shrink the Java bytecode in release builds.
 */
def enableProguardInReleaseBuilds = false

/**
 * The preferred build flavor of JavaScriptCore.
 *
 * For example, to use the international variant, you can use:
 * `def jscFlavor = 'org.webkit:android-jsc-intl:+'`
 *
 * The international variant includes ICU i18n library and necessary data
 * allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that
 * give correct results when using with locales other than en-US.  Note that
 * this variant is about 6MiB larger per architecture than default.
 */
def jscFlavor = 'org.webkit:android-jsc:+'

/**
 * Whether to enable the Hermes VM.
 *
 * This should be set on project.ext.react and mirrored here.  If it is not set
 * on project.ext.react, JavaScript will not be compiled to Hermes Bytecode
 * and the benefits of using Hermes will therefore be sharply reduced.
 */
def enableHermes = project.ext.react.get("enableHermes", false);

android {
    compileSdkVersion rootProject.ext.compileSdkVersion

    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }

    defaultConfig {
        applicationId "com.myapp_name"
        minSdkVersion rootProject.ext.minSdkVersion
        targetSdkVersion rootProject.ext.targetSdkVersion
        versionCode 1
        versionName "1.0"
        multiDexEnabled true
        vectorDrawables.useSupportLibrary = true
    }
    splits {
        abi {
            reset()
            enable enableSeparateBuildPerCPUArchitecture
            universalApk false  // If true, also generate a universal APK
            include "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
        }
    }
    signingConfigs {
        debug {
            storeFile file('debug.keystore')
            storePassword 'android'
            keyAlias 'androiddebugkey'
            keyPassword 'android'
        }
    }
    buildTypes {
        debug {
            signingConfig signingConfigs.debug
        }
        release {
            // Caution! In production, you need to generate your own keystore file.
            // see https://reactnative.dev/docs/signed-apk-android.
            signingConfig signingConfigs.debug
            minifyEnabled enableProguardInReleaseBuilds
            proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
        }
    }

    // applicationVariants are e.g. debug, release
    applicationVariants.all { variant ->
        variant.outputs.each { output ->
            // For each separate APK per architecture, set a unique version code as described here:
            // https://developer.android.com/studio/build/configure-apk-splits.html
            def versionCodes = ["armeabi-v7a": 1, "x86": 2, "arm64-v8a": 3, "x86_64": 4]
            def abi = output.getFilter(OutputFile.ABI)
            if (abi != null) {  // null for the universal-debug, universal-release variants
                output.versionCodeOverride =
                        versionCodes.get(abi) * 1048576 + defaultConfig.versionCode
            }

        }
    }
}

dependencies {
    implementation fileTree(dir: "libs", include: ["*.jar"])
    //noinspection GradleDynamicVersion
    implementation "com.facebook.react:react-native:+"  // From node_modules
    // Import the Firebase BoM
    implementation platform("com.google.firebase:firebase-bom:26.4.0")

    implementation 'com.facebook.android:facebook-android-sdk:[4,5)'

    implementation 'com.android.support:multidex:1.0.3'
    // Add the dependencies for the desired Firebase products
    // https://firebase.google.com/docs/android/setup#available-libraries
    implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.0.0"

    debugImplementation("com.facebook.flipper:flipper:${FLIPPER_VERSION}") {
      exclude group:'com.facebook.fbjni'
    }

    debugImplementation("com.facebook.flipper:flipper-network-plugin:${FLIPPER_VERSION}") {
        exclude group:'com.facebook.flipper'
        exclude group:'com.squareup.okhttp3', module:'okhttp'
    }

    debugImplementation("com.facebook.flipper:flipper-fresco-plugin:${FLIPPER_VERSION}") {
        exclude group:'com.facebook.flipper'
    }

    if (enableHermes) {
        def hermesPath = "../../node_modules/hermes-engine/android/";
        debugImplementation files(hermesPath + "hermes-debug.aar")
        releaseImplementation files(hermesPath + "hermes-release.aar")
    } else {
        implementation jscFlavor
    }
}

// Run this once to be able to run the application with BUCK
// puts all compile dependencies into folder libs for BUCK to use
task copyDownloadableDepsToLibs(type: Copy) {
    from configurations.compile
    into 'libs'
}

apply from: file("../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesAppBuildGradle(project)

project.ext.vectoricons = [
    iconFontNames: [ 'MaterialCommunityIcons.ttf', 'MaterialIcons.ttf' ]
]

apply from: "../../node_modules/react-native-vector-icons/fonts.gradle"

apply plugin: "com.google.gms.google-services" // <--- this should be the last line

android/settings.gradle:

rootProject.name = 'myapp_name'
apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings)
include ':app'

MainApplication.java:

package com.myapp_name;

import android.app.Application;
import android.content.Context;
import com.facebook.react.PackageList;
import com.facebook.react.ReactApplication;
import com.facebook.react.ReactInstanceManager;
import com.facebook.react.ReactNativeHost;
import com.facebook.react.ReactPackage;
import com.facebook.soloader.SoLoader;
import java.lang.reflect.InvocationTargetException;
import java.util.List;

public class MainApplication extends Application implements ReactApplication {

  private final ReactNativeHost mReactNativeHost =
      new ReactNativeHost(this) {
        @Override
        public boolean getUseDeveloperSupport() {
          return BuildConfig.DEBUG;
        }

        @Override
        protected List<ReactPackage> getPackages() {
          @SuppressWarnings("UnnecessaryLocalVariable")
          List<ReactPackage> packages = new PackageList(this).getPackages();
          // Packages that cannot be autolinked yet can be added manually here, for example:
          // packages.add(new MyReactNativePackage());
          return packages;
        }

        @Override
        protected String getJSMainModuleName() {
          return "index";
        }
      };

  @Override
  public ReactNativeHost getReactNativeHost() {
    return mReactNativeHost;
  }

  @Override
  public void onCreate() {
    super.onCreate();
    SoLoader.init(this, /* native exopackage */ false);
    initializeFlipper(this, getReactNativeHost().getReactInstanceManager());
  }

  /**
   * Loads Flipper in React Native templates. Call this in the onCreate method with something like
   * initializeFlipper(this, getReactNativeHost().getReactInstanceManager());
   *
   * @param context
   * @param reactInstanceManager
   */
  private static void initializeFlipper(
      Context context, ReactInstanceManager reactInstanceManager) {
    if (BuildConfig.DEBUG) {
      try {
        /*
         We use reflection here to pick up the class that initializes Flipper,
        since Flipper library is not available in release mode
        */
        Class<?> aClass = Class.forName("com.myapp_name.ReactNativeFlipper");
        aClass
            .getMethod("initializeFlipper", Context.class, ReactInstanceManager.class)
            .invoke(null, context, reactInstanceManager);
      } catch (ClassNotFoundException e) {
        e.printStackTrace();
      } catch (NoSuchMethodException e) {
        e.printStackTrace();
      } catch (IllegalAccessException e) {
        e.printStackTrace();
      } catch (InvocationTargetException e) {
        e.printStackTrace();
      }
    }
  }
}

AndroidManifest.xml:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
  package="com.myapp_name">

    <uses-permission android:name="android.permission.INTERNET" />

    <application
      android:name=".MainApplication"
      android:label="@string/app_name"
      android:icon="@mipmap/ic_launcher"
      android:roundIcon="@mipmap/ic_launcher_round"
      android:allowBackup="false"
      android:theme="@style/AppTheme">
      <meta-data android:name="com.facebook.sdk.ApplicationId" android:value="@string/facebook_app_id"/>
      <activity
        android:name=".MainActivity"
        android:label="@string/app_name"
        android:configChanges="keyboard|keyboardHidden|orientation|screenSize|uiMode"
        android:launchMode="singleTask"
        android:windowSoftInputMode="adjustResize">
        <intent-filter>
            <action android:name="android.intent.action.MAIN" />
            <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>
      </activity>
      <activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />
    </application>

</manifest>


Environment

Click To Expand

react-native info output:

Android test was performed on both Mac and Windows.

 System:
    OS: macOS 11.2
    CPU: (4) x64 Intel(R) Core(TM) i5-5350U CPU @ 1.80GHz
    Memory: 1.67 GB / 8.00 GB
    Shell: 3.2.57 - /bin/bash
  Binaries:
    Node: 15.8.0 - /usr/local/bin/node
    Yarn: 1.22.10 - /usr/local/bin/yarn
    npm: 7.5.0 - /usr/local/bin/npm
    Watchman: 4.9.0 - /usr/local/bin/watchman
  Managers:
    CocoaPods: 1.10.1 - /usr/local/bin/pod
  SDKs:
    iOS SDK:
      Platforms: iOS 14.4, DriverKit 20.2, macOS 11.1, tvOS 14.3, watchOS 7.2
    Android SDK: Not Found
  IDEs:
    Android Studio: Not Found
    Xcode: 12.4/12D4e - /usr/bin/xcodebuild
  Languages:
    Java: Not Found
    Python: 2.7.16 - /usr/bin/python
  npmPackages:
    @react-native-community/cli: Not Found
    react: 16.13.1 => 16.13.1 
    react-native: 0.63.4 => 0.63.4 
    react-native-macos: Not Found
  npmGlobalPackages:
    *react-native*: Not Found
info Fetching system and libraries information...
System:
    OS: Windows 10 10.0.19042
    CPU: (4) x64 Intel(R) Core(TM) i5-6200U CPU @ 2.30GHz    
    Memory: 708.10 MB / 7.85 GB
  Binaries:
    Node: 12.13.1 - C:\Program Files\nodejs\node.EXE
    Yarn: 1.21.1 - ~\AppData\Roaming\npm\yarn.CMD
    npm: 6.14.11 - C:\Program Files\nodejs\npm.CMD
    Watchman: Not Found
  SDKs:
    Android SDK:
      API Levels: 29, 30
      Build Tools: 28.0.3, 29.0.2, 30.0.3
      System Images: android-30 | Google APIs Intel x86 Atom  
      Android NDK: Not Found
    Windows SDK:
      AllowAllTrustedApps: Disabled
      Versions: 10.0.15063.0
  IDEs:
    Android Studio: Version  4.1.0.0 AI-201.8743.12.41.7042882
    Visual Studio: Not Found
  Languages:
    Java: Not Found
    Python: 3.9.0 - /c/cygwin64/bin/python
  npmPackages:
    @react-native-community/cli: Not Found
    react: 16.13.1 => 16.13.1
    react-native: 0.63.4 => 0.63.4
    react-native-windows: Not Found
  npmGlobalPackages:
    *react-native*: Not Found
  • Platform that you're experiencing the issue on:
    • iOS
    • Android
    • iOS but have not tested behavior on Android
    • Android but have not tested behavior on iOS
    • Both
  • react-native-firebase version you're using that has this issue:
    • "^10.6.4" (see package.json)
  • Firebase module(s) you're using that has the issue:
    • "^10.6.4" (see package.json)
  • Are you using TypeScript?
    • yes, "^3.8.3"


@cqwcent cqwcent added help: needs-triage Issue needs additional investigation/triaging. type: bug New bug report labels Feb 15, 2021
@devtud
Copy link

devtud commented Feb 22, 2021

I have the same issue on Android with @react-native-firebase==10.8.0 when I try to link anonymous user to a social account that already exists. Last version to correctly raise auth/credential-already-in-use is 10.1.1 (though I'm not 100% sure).

@mikehardy
Copy link
Collaborator

@devtud have you tried reverting the firebase-android-sdk version back to the one that was in use with the last version of react-native-firebase that worked for you? The gradle build logs will tell you which firebase-android-sdk is being used and rnfirebase.io has instructions on how to override for testing https://rnfirebase.io/#android

@devtud
Copy link

devtud commented Feb 22, 2021

@mikehardy I have overwritten bom 26.5.0 with 26.0.0 and it seems the auth/credential-already-in-use error is raised correctly. Thanks.

@mikehardy
Copy link
Collaborator

Interesting. Then it is one of these two releases:

https://firebase.google.com/support/release-notes/android#auth_v20-0-1 (firebase-android-sdk 26.1.0)
https://firebase.google.com/support/release-notes/android#auth_v20-0-2 (firebase-android-sdk 26.3.0)

@devtud can you confirm which one breaks? If we know for sure 26.0.0 works and 26.1.0 breaks or that 26.2.0 still works but 26.3.0 breaks then we can really limit our search area for the regression down

I really appreciate the testing, thank you

@devtud
Copy link

devtud commented Feb 22, 2021

@mikehardy , 26.1.0 breaks.

@mikehardy mikehardy added the Workflow: Needs Review Pending feedback or review from a maintainer. label Feb 22, 2021
@mikehardy
Copy link
Collaborator

Unfortunately if I understand correctly firebase-android-sdk has not open sourced the auth module, but they do still track their issues on github (for auth as well) https://github.com/firebase/firebase-android-sdk/issues/new?assignees=&labels=&template=bug.md

They typically want a reproduction but they offer quickstarts to make it relatively painless https://github.com/firebase/quickstart-android/tree/master/auth - I suppose the hard part will be integrating the facebook example but they have a pretty good guide for native as well https://developers.facebook.com/docs/facebook-login/android

While recognizing that's a pain, if the only thing changed here (in react-native-firebase) was using underlying android SDK 26.0.0 vs 26.1.0, it's got to be an upstream issue and it must be pursued there

@mikehardy mikehardy added blocked: firebase-support Pending feedback or review from google support or response on official sdk repo issue. platform: android plugin: authentication Firebase Authentication Type: Firebase and removed help: needs-triage Issue needs additional investigation/triaging. Workflow: Needs Review Pending feedback or review from a maintainer. labels Feb 22, 2021
@GautamKrishnan
Copy link

Hi @mikehardy I am facing this same issue on linkWithCredentials while trying to link the PhoneNo. on Android Devices. It works fine on IOS devices. I did debug and found on the Android side on the error is caught logged on the console however its not returned and instead the user linked with phone no. is returned.

Screenshot 2021-03-30 at 9 22 16 PM

the section of code giving the issue.

@mikehardy
Copy link
Collaborator

@GautamKrishnan I don't understand how that moves the issue forward in the context of my previous comment. Can you explain? Or are we still waiting for a native reproduction and information from firebase-android-sdk support?

@modmido
Copy link

modmido commented Mar 31, 2021

I'm having the same issue.

@mikehardy
Copy link
Collaborator

@modmido how did it go when you did a native reproduction and submitted it to firebase-android-sdk issue tracker?

@RohovDmytro
Copy link

Same here. Created an issue. If anyone one would be able to create a reproducable demo - he would be a hero.

Any thought what could be done right now to prevent data loss from anonymous user while linking accounts?

@GautamKrishnan
Copy link

@RohovDmytro Hi, I commented this portion in ReactNativeFirebaseAuthModule.java and returned the promiseRejectAuthException(promise, exception);

Screenshot 2021-04-12 at 2 46 41 PM

@Maddoc42
Copy link

Maddoc42 commented Apr 13, 2021

Solution from @GautamKrishnan seems to work fine for me. Trying to link with an existing account throws an error, linking with a new (email) account works fine.

Patch in case anyone needs it (yes, unclean indentations ^^):

diff --git a/node_modules/@react-native-firebase/auth/android/src/main/java/io/invertase/firebase/auth/ReactNativeFirebaseAuthModule.java b/node_modules/@react-native-firebase/auth/android/src/main/java/io/invertase/firebase/auth/ReactNativeFirebaseAuthModule.java
index c649123..37e00af 100644
--- a/node_modules/@react-native-firebase/auth/android/src/main/java/io/invertase/firebase/auth/ReactNativeFirebaseAuthModule.java
+++ b/node_modules/@react-native-firebase/auth/android/src/main/java/io/invertase/firebase/auth/ReactNativeFirebaseAuthModule.java
@@ -1311,6 +1311,8 @@ class ReactNativeFirebaseAuthModule extends ReactNativeFirebaseModule {
             } else {
               Exception exception = task.getException();
               Log.e(TAG, "link:onComplete:failure", exception);
+			  promiseRejectAuthException(promise, exception);
+			  /*
               if (exception instanceof FirebaseAuthUserCollisionException) {
                 FirebaseAuthUserCollisionException authUserCollisionException = (FirebaseAuthUserCollisionException) exception;
                 AuthCredential updatedCredential = authUserCollisionException.getUpdatedCredential();
@@ -1329,6 +1331,7 @@ class ReactNativeFirebaseAuthModule extends ReactNativeFirebaseModule {
               } else {
                 promiseRejectAuthException(promise, exception);
               }
+			*/
             }});
       } else {
         promiseNoUser(promise, true);

@mikehardy
Copy link
Collaborator

We've never done this before but I want to help - if there was some way to constrain this workaround in the code to only the affected firebase-android-sdks that are affected 26.1.0 up through but maybe not including(?) 27.0.0 maybe we could include the version check then the workaround? I'm not sure if firebase-android-sdk's auth module makes it's version available dynamically or not to make something like that work

@stale
Copy link

stale bot commented Jun 9, 2021

Hello 👋, to help manage issues we automatically close stale issues.
This issue has been automatically marked as stale because it has not had activity for quite some time. Has this issue been fixed, or does it still require the community's attention?

This issue will be closed in 15 days if no further activity occurs.
Thank you for your contributions.

@stale stale bot added the Type: Stale Issue has become stale - automatically added by Stale bot label Jun 9, 2021
@Shaninnik
Copy link

I am having the same issue, we have anonymouse users + phone auth and I am trying to linkWithCredential and if it returns "auth/credential-already-in-use" error I am doing signInWithCredential instead. Works like a charm on iOS but fails on Android. After going through code for both platforms I've noticed is that behaviour of linkWithCredential on iOS and Android is completely different. On iOS, as expected, it throws "auth/credential-already-in-use" error, returns updated auth credentials hash key in error.userInfo.authCredential and also saves updated non-serializable credentials to temp dictionary which is later re-used if signInWithCredential is called using same hash key returned in error.userInfo.authCredential

On the other hand Android version attempts to do all of this automatically, which @GautamKrishnan tried to avoid by just replacing it with promiseRejectAuthException(promise, exception);

In my case this was only half working, because now I was getting a "auth/credential-already-in-use" error as expected, but still no updated credentials that I can use to attempt a signInWithCredential to match iOS behaviour. @mikehardy I am not exactly sure why it was done differently on iOS and Android, but it seems like it is an issue with react-native-firebase and not with firebase-android-sdk. Here is my Patch that fixes it for me (basically just copied what iOS code does):

diff --git a/node_modules/@react-native-firebase/auth/android/src/main/java/io/invertase/firebase/auth/ReactNativeFirebaseAuthModule.java b/node_modules/@react-native-firebase/auth/android/src/main/java/io/invertase/firebase/auth/ReactNativeFirebaseAuthModule.java
index d30755e..f8aeccd 100644
--- a/node_modules/@react-native-firebase/auth/android/src/main/java/io/invertase/firebase/auth/ReactNativeFirebaseAuthModule.java
+++ b/node_modules/@react-native-firebase/auth/android/src/main/java/io/invertase/firebase/auth/ReactNativeFirebaseAuthModule.java
@@ -84,6 +84,7 @@ class ReactNativeFirebaseAuthModule extends ReactNativeFirebaseModule {
   private String mLastPhoneNumber;
   private PhoneAuthProvider.ForceResendingToken mForceResendingToken;
   private PhoneAuthCredential mCredential;
+  private HashMap<String, AuthCredential> credentials = new HashMap<>();
 
 
   ReactNativeFirebaseAuthModule(ReactApplicationContext reactContext) {
@@ -1312,21 +1313,24 @@ class ReactNativeFirebaseAuthModule extends ReactNativeFirebaseModule {
             } else {
               Exception exception = task.getException();
               Log.e(TAG, "link:onComplete:failure", exception);
+              //promiseRejectAuthException(promise, exception);
+
               if (exception instanceof FirebaseAuthUserCollisionException) {
                 FirebaseAuthUserCollisionException authUserCollisionException = (FirebaseAuthUserCollisionException) exception;
                 AuthCredential updatedCredential = authUserCollisionException.getUpdatedCredential();
-                try {
-                  firebaseAuth.signInWithCredential(updatedCredential).addOnCompleteListener(getExecutor(), result -> {
-                    if (result.isSuccessful()) {
-                      promiseWithAuthResult(result.getResult(), promise);
-                    } else {
-                      promiseRejectAuthException(promise, exception);
-                    }
-                  });
-                } catch (Exception e) {
-                  // we the attempt to log in after the collision failed, reject back to JS
-                  promiseRejectAuthException(promise, exception);
-                }
+                promiseRejectAuthException(promise, exception, updatedCredential);
+                // try {
+                //   firebaseAuth.signInWithCredential(updatedCredential).addOnCompleteListener(getExecutor(), result -> {
+                //     if (result.isSuccessful()) {
+                //       promiseWithAuthResult(result.getResult(), promise);
+                //     } else {
+                //       promiseRejectAuthException(promise, exception);
+                //     }
+                //   });
+                // } catch (Exception e) {
+                //   // we the attempt to log in after the collision failed, reject back to JS
+                //   promiseRejectAuthException(promise, exception);
+                // }
               } else {
                 promiseRejectAuthException(promise, exception);
               }
@@ -1412,31 +1416,35 @@ class ReactNativeFirebaseAuthModule extends ReactNativeFirebaseModule {
     String authToken,
     String authSecret
   ) {
-    switch (provider) {
-      case "facebook.com":
-        return FacebookAuthProvider.getCredential(authToken);
-      case "google.com":
-        return GoogleAuthProvider.getCredential(authToken, authSecret);
-      case "twitter.com":
-        return TwitterAuthProvider.getCredential(authToken, authSecret);
-      case "github.com":
-        return GithubAuthProvider.getCredential(authToken);
-      case "apple.com":
-        return OAuthProvider.newCredentialBuilder(provider).setIdTokenWithRawNonce(authToken, authSecret).build();
-      case "oauth":
-        return OAuthProvider.getCredential(provider, authToken, authSecret);
-      case "phone":
-        return getPhoneAuthCredential(authToken, authSecret);
-      case "password":
-        // authToken = email
-        // authSecret = password
-        return EmailAuthProvider.getCredential(authToken, authSecret);
-      case "emailLink":
-        // authToken = email
-        // authSecret = link
-        return EmailAuthProvider.getCredentialWithLink(authToken, authSecret);
-      default:
-        return null;
+    if (credentials.containsKey(authToken) && credentials.get(authToken) != null) {
+      return credentials.get(authToken);
+    } else {
+      switch (provider) {
+        case "facebook.com":
+          return FacebookAuthProvider.getCredential(authToken);
+        case "google.com":
+          return GoogleAuthProvider.getCredential(authToken, authSecret);
+        case "twitter.com":
+          return TwitterAuthProvider.getCredential(authToken, authSecret);
+        case "github.com":
+          return GithubAuthProvider.getCredential(authToken);
+        case "apple.com":
+          return OAuthProvider.newCredentialBuilder(provider).setIdTokenWithRawNonce(authToken, authSecret).build();
+        case "oauth":
+          return OAuthProvider.getCredential(provider, authToken, authSecret);
+        case "phone":
+          return getPhoneAuthCredential(authToken, authSecret);
+        case "password":
+          // authToken = email
+          // authSecret = password
+          return EmailAuthProvider.getCredential(authToken, authSecret);
+        case "emailLink":
+          // authToken = email
+          // authSecret = link
+          return EmailAuthProvider.getCredentialWithLink(authToken, authSecret);
+        default:
+          return null;
+      }
     }
   }
 
@@ -1792,6 +1800,30 @@ class ReactNativeFirebaseAuthModule extends ReactNativeFirebaseModule {
     rejectPromiseWithCodeAndMessage(promise, error.getString("code"), error.getString("message"));
   }
 
+  public void promiseRejectAuthException(
+    Promise promise,
+    Exception exception,
+    AuthCredential authCredential
+  ) {
+    WritableMap error = getJSError(exception);
+
+    String authHashCode = String.valueOf(authCredential.hashCode());
+
+    WritableMap authCredentialsMap = Arguments.createMap();
+    authCredentialsMap.putString("providerId",  authCredential.getProvider());
+    authCredentialsMap.putString("token",  authHashCode);
+    authCredentialsMap.putString("secret",  null);
+
+    // Temporarily store the non-serializable credential for later
+    credentials.put(authHashCode, authCredential);
+
+    WritableMap userInfoMap = Arguments.createMap();
+    userInfoMap.putString("code",  error.getString("code"));
+    userInfoMap.putString("message", error.getString("message"));
+    userInfoMap.putMap("authCredential", authCredentialsMap);
+    promise.reject(error.getString("code"), error.getString("message"), userInfoMap);
+  }
+
   /**
    * getJSError
    *
    ```

I can do a PR if needed, but I think this might break things for people who are using it just for Android.

@stale stale bot removed the Type: Stale Issue has become stale - automatically added by Stale bot label Jul 16, 2021
@mikehardy
Copy link
Collaborator

I think this was fixed with #5694 - the original PR that added the attempt in android to upgrade anonymous users was later disavowed as no longer working by the author so removing it seems non-breaking (it was already broken?) and now the platforms should be equivalent. v12.7.5 release here has it

@kristfal
Copy link

@mikehardy FYI the revert still doesn't push the updated credentials to the JS side. In order to properly fix this, you'll have to add the patch based on the work of @Shaninnik):

  1. Import types:
import com.google.firebase.auth.FirebaseAuthInvalidCredentialsException;
  1. Add a private hashmap:
private HashMap<String, AuthCredential> credentials = new HashMap<>();
  1. Add new credential storage for getter
private AuthCredential getCredentialForProvider(
      String provider, String authToken, String authSecret) {
    if (credentials.containsKey(authToken) && credentials.get(authToken) != null) {
      return credentials.get(authToken);
    } else {
      switch (provider) {
        case "facebook.com":
          return FacebookAuthProvider.getCredential(authToken);
        case "google.com":
          return GoogleAuthProvider.getCredential(authToken, authSecret);
        case "twitter.com":
          return TwitterAuthProvider.getCredential(authToken, authSecret);
        case "github.com":
          return GithubAuthProvider.getCredential(authToken);
        case "apple.com":
          return OAuthProvider.newCredentialBuilder(provider)
              .setIdTokenWithRawNonce(authToken, authSecret)
              .build();
        case "oauth":
          return OAuthProvider.getCredential(provider, authToken, authSecret);
        case "phone":
          return getPhoneAuthCredential(authToken, authSecret);
        case "password":
          // authToken = email
          // authSecret = password
          return EmailAuthProvider.getCredential(authToken, authSecret);
        case "emailLink":
          // authToken = email
          // authSecret = link
          return EmailAuthProvider.getCredentialWithLink(authToken, authSecret);
        default:
          return null;
      }
    }
  }
  1. Handle link exceptions with new reject handler:
  /**
   * promiseRejectLinkAuthException
   *
   * @param promise
   * @param exception
   * @param authCredential
   */
  private void promiseRejectLinkAuthException(Promise promise, Exception exception, AuthCredential authCredential) {
    WritableMap error = getJSError(exception);
    String authHashCode = String.valueOf(authCredential.hashCode());

    WritableMap authCredentialsMap = Arguments.createMap();
    authCredentialsMap.putString("providerId", authCredential.getProvider());
    authCredentialsMap.putString("token", authHashCode);
    authCredentialsMap.putString("secret", null);

    // Temporarily store the non-serializable credential for later
    credentials.put(authHashCode, authCredential);

    WritableMap userInfoMap = Arguments.createMap();
    userInfoMap.putString("code", error.getString("code"));
    userInfoMap.putString("message", error.getString("message"));
    userInfoMap.putMap("authCredential", authCredentialsMap);

    promise.reject(error.getString("code"), error.getString("message"), userInfoMap);
  }
@ReactMethod
  private void linkWithCredential(
      String appName, String provider, String authToken, String authSecret, final Promise promise) {
    FirebaseApp firebaseApp = FirebaseApp.getInstance(appName);
    FirebaseAuth firebaseAuth = FirebaseAuth.getInstance(firebaseApp);

    AuthCredential credential = getCredentialForProvider(provider, authToken, authSecret);

    if (credential == null) {
      rejectPromiseWithCodeAndMessage(
          promise,
          "invalid-credential",
          "The supplied auth credential is malformed, has expired or is not currently supported.");
    } else {
      FirebaseUser user = firebaseAuth.getCurrentUser();
      Log.d(TAG, "link");

      if (user != null) {
        user.linkWithCredential(credential)
            .addOnCompleteListener(
                getExecutor(),
                task -> {
                  if (task.isSuccessful()) {
                    Log.d(TAG, "link:onComplete:success");
                    promiseWithAuthResult(task.getResult(), promise);
                  } else {
                    Exception exception = task.getException();

                    if (exception instanceof FirebaseAuthUserCollisionException) {
                      FirebaseAuthUserCollisionException authUserCollisionException = (FirebaseAuthUserCollisionException) exception;
                      AuthCredential updatedCredential = authUserCollisionException.getUpdatedCredential();
                      Log.e(TAG, "link:onComplete:collisionFailure", exception);
                      promiseRejectLinkAuthException(promise, exception, updatedCredential);
                    } else {
                      Log.e(TAG, "link:onComplete:failure", exception);
                      promiseRejectAuthException(promise, exception);
                    }
                  }
                });
      } else {
        promiseNoUser(promise, true);
      }
    }
  }

From the JS-side, you can now do the following:

try {
        // try linking credential
        await auth().currentUser!.linkWithCredential(credential);
        await auth().currentUser!.reload();

        // Handle success
      } catch (err) {
        const error = err as FirebaseAuthTypes.NativeFirebaseAuthError;

        // If authCredentials are available, we can log in on the 
        // account that we attempted to link with
        if (error.userInfo.authCredential) {
          await auth().signInWithCredential(error.userInfo.authCredential);
          await auth().currentUser!.reload();

          // Handle success, but now logged into the linked account
        }
        
        // Handle error

I got a got diff, but its mired with style changes, so hope you're able to follow.

@mikehardy
Copy link
Collaborator

Is there a way for you to use patch-package or similar? the text of the new is nowhere near as powerful as a diff I can examine + apply :-), one goes in the "oh no, needs triage" pile, one goes in the "review and likely merge" pile, and review+merge pile gets processed satisfyingly quickly, triage pile is slooow

@kristfal
Copy link

@mikehardy as said, my patch-package will get really messy due to stying changes. This approach now mirrors iOS perfectly. The same JS code can be used on both platforms to handle account colissions.

Here it is:

--- a/node_modules/@react-native-firebase/auth/android/src/main/java/io/invertase/firebase/auth/ReactNativeFirebaseAuthModule.java
+++ b/node_modules/@react-native-firebase/auth/android/src/main/java/io/invertase/firebase/auth/ReactNativeFirebaseAuthModule.java
@@ -42,6 +42,7 @@ import com.google.firebase.auth.FacebookAuthProvider;
 import com.google.firebase.auth.FirebaseAuth;
 import com.google.firebase.auth.FirebaseAuthException;
 import com.google.firebase.auth.FirebaseAuthInvalidCredentialsException;
+import com.google.firebase.auth.FirebaseAuthUserCollisionException;
 import com.google.firebase.auth.FirebaseAuthProvider;
 import com.google.firebase.auth.FirebaseAuthSettings;
 import com.google.firebase.auth.FirebaseUser;
@@ -70,11 +71,12 @@ import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 import javax.annotation.Nullable;
 
-@SuppressWarnings({"ThrowableResultOfMethodCallIgnored", "JavaDoc"})
+@SuppressWarnings({ "ThrowableResultOfMethodCallIgnored", "JavaDoc" })
 class ReactNativeFirebaseAuthModule extends ReactNativeFirebaseModule {
   private static final String TAG = "Auth";
   private static HashMap<String, FirebaseAuth.AuthStateListener> mAuthListeners = new HashMap<>();
   private static HashMap<String, FirebaseAuth.IdTokenListener> mIdTokenListeners = new HashMap<>();
+  private HashMap<String, AuthCredential> credentials = new HashMap<>();
   private String mVerificationId;
   private String mLastPhoneNumber;
   private PhoneAuthProvider.ForceResendingToken mForceResendingToken;
@@ -102,8 +104,7 @@ class ReactNativeFirebaseAuthModule extends ReactNativeFirebaseModule {
       String appName = (String) pair.getKey();
       FirebaseApp firebaseApp = FirebaseApp.getInstance(appName);
       FirebaseAuth firebaseAuth = FirebaseAuth.getInstance(firebaseApp);
-      FirebaseAuth.AuthStateListener mAuthListener =
-          (FirebaseAuth.AuthStateListener) pair.getValue();
+      FirebaseAuth.AuthStateListener mAuthListener = (FirebaseAuth.AuthStateListener) pair.getValue();
       firebaseAuth.removeAuthStateListener(mAuthListener);
       authListenerIterator.remove();
     }
@@ -130,24 +131,21 @@ class ReactNativeFirebaseAuthModule extends ReactNativeFirebaseModule {
     FirebaseAuth firebaseAuth = FirebaseAuth.getInstance(firebaseApp);
     FirebaseAuth.AuthStateListener mAuthListener = mAuthListeners.get(appName);
     if (mAuthListener == null) {
-      FirebaseAuth.AuthStateListener newAuthListener =
-          firebaseAuth1 -> {
-            FirebaseUser user = firebaseAuth1.getCurrentUser();
-            WritableMap eventBody = Arguments.createMap();
-            ReactNativeFirebaseEventEmitter emitter =
-                ReactNativeFirebaseEventEmitter.getSharedInstance();
-            if (user != null) {
-              eventBody.putString("appName", appName); // for js side distribution
-              eventBody.putMap("user", firebaseUserToMap(user));
-            } else {
-              eventBody.putString("appName", appName); // for js side distribution
-            }
-            Log.d(TAG, "addAuthStateListener:eventBody " + eventBody.toString());
-
-            ReactNativeFirebaseEvent event =
-                new ReactNativeFirebaseEvent("auth_state_changed", eventBody, appName);
-            emitter.sendEvent(event);
-          };
+      FirebaseAuth.AuthStateListener newAuthListener = firebaseAuth1 -> {
+        FirebaseUser user = firebaseAuth1.getCurrentUser();
+        WritableMap eventBody = Arguments.createMap();
+        ReactNativeFirebaseEventEmitter emitter = ReactNativeFirebaseEventEmitter.getSharedInstance();
+        if (user != null) {
+          eventBody.putString("appName", appName); // for js side distribution
+          eventBody.putMap("user", firebaseUserToMap(user));
+        } else {
+          eventBody.putString("appName", appName); // for js side distribution
+        }
+        Log.d(TAG, "addAuthStateListener:eventBody " + eventBody.toString());
+
+        ReactNativeFirebaseEvent event = new ReactNativeFirebaseEvent("auth_state_changed", eventBody, appName);
+        emitter.sendEvent(event);
+      };
 
       firebaseAuth.addAuthStateListener(newAuthListener);
       mAuthListeners.put(appName, newAuthListener);
@@ -179,25 +177,22 @@ class ReactNativeFirebaseAuthModule extends ReactNativeFirebaseModule {
     FirebaseAuth firebaseAuth = FirebaseAuth.getInstance(firebaseApp);
 
     if (!mIdTokenListeners.containsKey(appName)) {
-      FirebaseAuth.IdTokenListener newIdTokenListener =
-          firebaseAuth1 -> {
-            FirebaseUser user = firebaseAuth1.getCurrentUser();
-            ReactNativeFirebaseEventEmitter emitter =
-                ReactNativeFirebaseEventEmitter.getSharedInstance();
-            WritableMap eventBody = Arguments.createMap();
-            if (user != null) {
-              eventBody.putBoolean("authenticated", true);
-              eventBody.putString("appName", appName);
-              eventBody.putMap("user", firebaseUserToMap(user));
-            } else {
-              eventBody.putString("appName", appName);
-              eventBody.putBoolean("authenticated", false);
-            }
-
-            ReactNativeFirebaseEvent event =
-                new ReactNativeFirebaseEvent("auth_id_token_changed", eventBody, appName);
-            emitter.sendEvent(event);
-          };
+      FirebaseAuth.IdTokenListener newIdTokenListener = firebaseAuth1 -> {
+        FirebaseUser user = firebaseAuth1.getCurrentUser();
+        ReactNativeFirebaseEventEmitter emitter = ReactNativeFirebaseEventEmitter.getSharedInstance();
+        WritableMap eventBody = Arguments.createMap();
+        if (user != null) {
+          eventBody.putBoolean("authenticated", true);
+          eventBody.putString("appName", appName);
+          eventBody.putMap("user", firebaseUserToMap(user));
+        } else {
+          eventBody.putString("appName", appName);
+          eventBody.putBoolean("authenticated", false);
+        }
+
+        ReactNativeFirebaseEvent event = new ReactNativeFirebaseEvent("auth_id_token_changed", eventBody, appName);
+        emitter.sendEvent(event);
+      };
 
       firebaseAuth.addIdTokenListener(newIdTokenListener);
       mIdTokenListeners.put(appName, newIdTokenListener);
@@ -221,10 +216,13 @@ class ReactNativeFirebaseAuthModule extends ReactNativeFirebaseModule {
   }
 
   /**
-   * The phone number and SMS code here must have been configured in the Firebase Console
+   * The phone number and SMS code here must have been configured in the Firebase
+   * Console
    * (Authentication > Sign In Method > Phone).
    *
-   * <p>Calling this method a second time will overwrite the previously passed parameters. Only one
+   * <p>
+   * Calling this method a second time will overwrite the previously passed
+   * parameters. Only one
    * number can be configured at a given time.
    *
    * @param appName
@@ -245,13 +243,14 @@ class ReactNativeFirebaseAuthModule extends ReactNativeFirebaseModule {
 
   /**
    * Disable app verification for the running of tests
+   * 
    * @param appName
    * @param disabled
    * @param promise
    */
   @ReactMethod
   public void setAppVerificationDisabledForTesting(
-    String appName, boolean disabled, Promise promise) {
+      String appName, boolean disabled, Promise promise) {
     Log.d(TAG, "setAppVerificationDisabledForTesting");
     FirebaseApp firebaseApp = FirebaseApp.getInstance(appName);
     FirebaseAuth firebaseAuth = FirebaseAuth.getInstance(firebaseApp);
@@ -413,17 +412,16 @@ class ReactNativeFirebaseAuthModule extends ReactNativeFirebaseModule {
     FirebaseApp firebaseApp = FirebaseApp.getInstance(appName);
     FirebaseAuth firebaseAuth = FirebaseAuth.getInstance(firebaseApp);
 
-    OnCompleteListener<Void> listener =
-        task -> {
-          if (task.isSuccessful()) {
-            Log.d(TAG, "sendPasswordResetEmail:onComplete:success");
-            promiseNoUser(promise, false);
-          } else {
-            Exception exception = task.getException();
-            Log.e(TAG, "sendPasswordResetEmail:onComplete:failure", exception);
-            promiseRejectAuthException(promise, exception);
-          }
-        };
+    OnCompleteListener<Void> listener = task -> {
+      if (task.isSuccessful()) {
+        Log.d(TAG, "sendPasswordResetEmail:onComplete:success");
+        promiseNoUser(promise, false);
+      } else {
+        Exception exception = task.getException();
+        Log.e(TAG, "sendPasswordResetEmail:onComplete:failure", exception);
+        promiseRejectAuthException(promise, exception);
+      }
+    };
 
     if (actionCodeSettings == null) {
       firebaseAuth.sendPasswordResetEmail(email).addOnCompleteListener(getExecutor(), listener);
@@ -448,17 +446,16 @@ class ReactNativeFirebaseAuthModule extends ReactNativeFirebaseModule {
     FirebaseApp firebaseApp = FirebaseApp.getInstance(appName);
     FirebaseAuth firebaseAuth = FirebaseAuth.getInstance(firebaseApp);
 
-    OnCompleteListener<Void> listener =
-        task -> {
-          if (task.isSuccessful()) {
-            Log.d(TAG, "sendSignInLinkToEmail:onComplete:success");
-            promiseNoUser(promise, false);
-          } else {
-            Exception exception = task.getException();
-            Log.e(TAG, "sendSignInLinkToEmail:onComplete:failure", exception);
-            promiseRejectAuthException(promise, exception);
-          }
-        };
+    OnCompleteListener<Void> listener = task -> {
+      if (task.isSuccessful()) {
+        Log.d(TAG, "sendSignInLinkToEmail:onComplete:success");
+        promiseNoUser(promise, false);
+      } else {
+        Exception exception = task.getException();
+        Log.e(TAG, "sendSignInLinkToEmail:onComplete:failure", exception);
+        promiseRejectAuthException(promise, exception);
+      }
+    };
 
     ActionCodeSettings settings = buildActionCodeSettings(actionCodeSettings);
     firebaseAuth
@@ -466,9 +463,11 @@ class ReactNativeFirebaseAuthModule extends ReactNativeFirebaseModule {
         .addOnCompleteListener(getExecutor(), listener);
   }
 
-  /* ----------------------
-   *  .currentUser methods
-   * ---------------------- */
+  /*
+   * ----------------------
+   * .currentUser methods
+   * ----------------------
+   */
 
   /**
    * delete
@@ -553,17 +552,16 @@ class ReactNativeFirebaseAuthModule extends ReactNativeFirebaseModule {
       promiseNoUser(promise, false);
       Log.e(TAG, "sendEmailVerification:failure:noCurrentUser");
     } else {
-      OnCompleteListener<Void> listener =
-          task -> {
-            if (task.isSuccessful()) {
-              Log.d(TAG, "sendEmailVerification:onComplete:success");
-              promiseWithUser(firebaseAuth.getCurrentUser(), promise);
-            } else {
-              Exception exception = task.getException();
-              Log.e(TAG, "sendEmailVerification:onComplete:failure", exception);
-              promiseRejectAuthException(promise, exception);
-            }
-          };
+      OnCompleteListener<Void> listener = task -> {
+        if (task.isSuccessful()) {
+          Log.d(TAG, "sendEmailVerification:onComplete:success");
+          promiseWithUser(firebaseAuth.getCurrentUser(), promise);
+        } else {
+          Exception exception = task.getException();
+          Log.e(TAG, "sendEmailVerification:onComplete:failure", exception);
+          promiseRejectAuthException(promise, exception);
+        }
+      };
 
       if (actionCodeSettings == null) {
         user.sendEmailVerification().addOnCompleteListener(getExecutor(), listener);
@@ -592,17 +590,16 @@ class ReactNativeFirebaseAuthModule extends ReactNativeFirebaseModule {
       promiseNoUser(promise, false);
       Log.e(TAG, "verifyBeforeUpdateEmail:failure:noCurrentUser");
     } else {
-      OnCompleteListener<Void> listener =
-          task -> {
-            if (task.isSuccessful()) {
-              Log.d(TAG, "verifyBeforeUpdateEmail:onComplete:success");
-              promiseWithUser(firebaseAuth.getCurrentUser(), promise);
-            } else {
-              Exception exception = task.getException();
-              Log.e(TAG, "verifyBeforeUpdateEmail:onComplete:failure", exception);
-              promiseRejectAuthException(promise, exception);
-            }
-          };
+      OnCompleteListener<Void> listener = task -> {
+        if (task.isSuccessful()) {
+          Log.d(TAG, "verifyBeforeUpdateEmail:onComplete:success");
+          promiseWithUser(firebaseAuth.getCurrentUser(), promise);
+        } else {
+          Exception exception = task.getException();
+          Log.e(TAG, "verifyBeforeUpdateEmail:onComplete:failure", exception);
+          promiseRejectAuthException(promise, exception);
+        }
+      };
 
       if (actionCodeSettings == null) {
         user.verifyBeforeUpdateEmail(email).addOnCompleteListener(getExecutor(), listener);
@@ -835,85 +832,85 @@ class ReactNativeFirebaseAuthModule extends ReactNativeFirebaseModule {
     // Reset the verification Id
     mVerificationId = null;
 
-    PhoneAuthProvider.OnVerificationStateChangedCallbacks callbacks =
-        new PhoneAuthProvider.OnVerificationStateChangedCallbacks() {
-          private boolean promiseResolved = false;
-
-          @Override
-          public void onVerificationCompleted(final PhoneAuthCredential phoneAuthCredential) {
-            // User has been automatically verified, log them in
-            firebaseAuth
-                .signInWithCredential(phoneAuthCredential)
-                .addOnCompleteListener(
-                    getExecutor(),
-                    task -> {
-                      if (task.isSuccessful()) {
-                        // onAuthStateChanged will pick up the user change
-                        Log.d(
-                            TAG,
-                            "signInWithPhoneNumber:autoVerified:signInWithCredential:onComplete:success");
-                        // To ensure that there is no hanging promise, we resolve it with a null
-                        // verificationId
-                        // as calling ConfirmationResult.confirm(code) is invalid in this case
-                        // anyway
-                        if (!promiseResolved) {
-                          WritableMap verificationMap = Arguments.createMap();
-
-                          Parcel parcel = Parcel.obtain();
-                          phoneAuthCredential.writeToParcel(parcel, 0);
-                          parcel.setDataPosition(16); // verificationId
-                          String verificationId = parcel.readString();
-                          mVerificationId = verificationId;
-                          parcel.recycle();
-
-                          verificationMap.putString("verificationId", verificationId);
-                          promise.resolve(verificationMap);
-                        }
-                      } else {
-                        // With phone auth, the credential will only every be rejected if the user
-                        // account linked to the phone number has been disabled
-                        Exception exception = task.getException();
-                        Log.e(
-                            TAG,
-                            "signInWithPhoneNumber:autoVerified:signInWithCredential:onComplete:failure",
-                            exception);
-                        // In the scenario where an SMS code has been sent, we have no way to report
-                        // back to the front-end that as the promise has already been used
-                        if (!promiseResolved) {
-                          promiseRejectAuthException(promise, exception);
-                        }
-                      }
-                    });
-          }
-
-          @Override
-          public void onVerificationFailed(FirebaseException e) {
-            // This callback is invoked in an invalid request for verification is made,
-            // e.g. phone number format is incorrect, or the SMS quota for the project
-            // has been exceeded
-            Log.d(TAG, "signInWithPhoneNumber:verification:failed");
-            promiseRejectAuthException(promise, e);
-          }
-
-          @Override
-          public void onCodeSent(
-              String verificationId, PhoneAuthProvider.ForceResendingToken forceResendingToken) {
-            // TODO: This isn't being saved anywhere if the activity gets restarted when going to
-            // the SMS app
-            mVerificationId = verificationId;
-            mForceResendingToken = forceResendingToken;
-            WritableMap verificationMap = Arguments.createMap();
-            verificationMap.putString("verificationId", verificationId);
-            promise.resolve(verificationMap);
-            promiseResolved = true;
-          }
-
-          @Override
-          public void onCodeAutoRetrievalTimeOut(String verificationId) {
-            super.onCodeAutoRetrievalTimeOut(verificationId);
-            // Purposefully not doing anything with this at the moment
-          }
-        };
+    PhoneAuthProvider.OnVerificationStateChangedCallbacks callbacks = new PhoneAuthProvider.OnVerificationStateChangedCallbacks() {
+      private boolean promiseResolved = false;
+
+      @Override
+      public void onVerificationCompleted(final PhoneAuthCredential phoneAuthCredential) {
+        // User has been automatically verified, log them in
+        firebaseAuth
+            .signInWithCredential(phoneAuthCredential)
+            .addOnCompleteListener(
+                getExecutor(),
+                task -> {
+                  if (task.isSuccessful()) {
+                    // onAuthStateChanged will pick up the user change
+                    Log.d(
+                        TAG,
+                        "signInWithPhoneNumber:autoVerified:signInWithCredential:onComplete:success");
+                    // To ensure that there is no hanging promise, we resolve it with a null
+                    // verificationId
+                    // as calling ConfirmationResult.confirm(code) is invalid in this case
+                    // anyway
+                    if (!promiseResolved) {
+                      WritableMap verificationMap = Arguments.createMap();
+
+                      Parcel parcel = Parcel.obtain();
+                      phoneAuthCredential.writeToParcel(parcel, 0);
+                      parcel.setDataPosition(16); // verificationId
+                      String verificationId = parcel.readString();
+                      mVerificationId = verificationId;
+                      parcel.recycle();
+
+                      verificationMap.putString("verificationId", verificationId);
+                      promise.resolve(verificationMap);
+                    }
+                  } else {
+                    // With phone auth, the credential will only every be rejected if the user
+                    // account linked to the phone number has been disabled
+                    Exception exception = task.getException();
+                    Log.e(
+                        TAG,
+                        "signInWithPhoneNumber:autoVerified:signInWithCredential:onComplete:failure",
+                        exception);
+                    // In the scenario where an SMS code has been sent, we have no way to report
+                    // back to the front-end that as the promise has already been used
+                    if (!promiseResolved) {
+                      promiseRejectAuthException(promise, exception);
+                    }
+                  }
+                });
+      }
+
+      @Override
+      public void onVerificationFailed(FirebaseException e) {
+        // This callback is invoked in an invalid request for verification is made,
+        // e.g. phone number format is incorrect, or the SMS quota for the project
+        // has been exceeded
+        Log.d(TAG, "signInWithPhoneNumber:verification:failed");
+        promiseRejectAuthException(promise, e);
+      }
+
+      @Override
+      public void onCodeSent(
+          String verificationId, PhoneAuthProvider.ForceResendingToken forceResendingToken) {
+        // TODO: This isn't being saved anywhere if the activity gets restarted when
+        // going to
+        // the SMS app
+        mVerificationId = verificationId;
+        mForceResendingToken = forceResendingToken;
+        WritableMap verificationMap = Arguments.createMap();
+        verificationMap.putString("verificationId", verificationId);
+        promise.resolve(verificationMap);
+        promiseResolved = true;
+      }
+
+      @Override
+      public void onCodeAutoRetrievalTimeOut(String verificationId) {
+        super.onCodeAutoRetrievalTimeOut(verificationId);
+        // Purposefully not doing anything with this at the moment
+      }
+    };
 
     if (activity != null) {
       if (forceResend && mForceResendingToken != null) {
@@ -990,78 +987,78 @@ class ReactNativeFirebaseAuthModule extends ReactNativeFirebaseModule {
     // Reset the credential
     mCredential = null;
 
-    PhoneAuthProvider.OnVerificationStateChangedCallbacks callbacks =
-        new PhoneAuthProvider.OnVerificationStateChangedCallbacks() {
-
-          @Override
-          public void onVerificationCompleted(final PhoneAuthCredential phoneAuthCredential) {
-            // Cache the credential to protect against null verificationId
-            mCredential = phoneAuthCredential;
-
-            Log.d(TAG, "verifyPhoneNumber:verification:onVerificationCompleted");
-            WritableMap state = Arguments.createMap();
-
-            Parcel parcel = Parcel.obtain();
-            phoneAuthCredential.writeToParcel(parcel, 0);
-
-            // verificationId
-            parcel.setDataPosition(16);
-            String verificationId = parcel.readString();
-
-            // sms Code
-            parcel.setDataPosition(parcel.dataPosition() + 8);
-            String code = parcel.readString();
-
-            state.putString("code", code);
-            state.putString("verificationId", verificationId);
-            parcel.recycle();
-            sendPhoneStateEvent(appName, requestKey, "onVerificationComplete", state);
-          }
-
-          @Override
-          public void onVerificationFailed(FirebaseException e) {
-            // This callback is invoked in an invalid request for verification is made,
-            // e.g. phone number format is incorrect, or the SMS quota for the project
-            // has been exceeded
-            Log.d(TAG, "verifyPhoneNumber:verification:onVerificationFailed");
-            WritableMap state = Arguments.createMap();
-            state.putMap("error", getJSError(e));
-            sendPhoneStateEvent(appName, requestKey, "onVerificationFailed", state);
-          }
-
-          @Override
-          public void onCodeSent(
-              String verificationId, PhoneAuthProvider.ForceResendingToken forceResendingToken) {
-            Log.d(TAG, "verifyPhoneNumber:verification:onCodeSent");
-            mForceResendingToken = forceResendingToken;
-            WritableMap state = Arguments.createMap();
-            state.putString("verificationId", verificationId);
-
-            // todo forceResendingToken  - it's actually just an empty class ... no actual token >.>
-            // Parcel parcel = Parcel.obtain();
-            // forceResendingToken.writeToParcel(parcel, 0);
-            //
-            // // verificationId
-            // parcel.setDataPosition(0);
-            // int int1 = parcel.readInt();
-            // String token = parcel.readString();
-            //
-            // state.putString("refreshToken", token);
-            // parcel.recycle();
-
-            state.putString("verificationId", verificationId);
-            sendPhoneStateEvent(appName, requestKey, "onCodeSent", state);
-          }
-
-          @Override
-          public void onCodeAutoRetrievalTimeOut(String verificationId) {
-            super.onCodeAutoRetrievalTimeOut(verificationId);
-            Log.d(TAG, "verifyPhoneNumber:verification:onCodeAutoRetrievalTimeOut");
-            WritableMap state = Arguments.createMap();
-            state.putString("verificationId", verificationId);
-            sendPhoneStateEvent(appName, requestKey, "onCodeAutoRetrievalTimeout", state);
-          }
-        };
+    PhoneAuthProvider.OnVerificationStateChangedCallbacks callbacks = new PhoneAuthProvider.OnVerificationStateChangedCallbacks() {
+
+      @Override
+      public void onVerificationCompleted(final PhoneAuthCredential phoneAuthCredential) {
+        // Cache the credential to protect against null verificationId
+        mCredential = phoneAuthCredential;
+
+        Log.d(TAG, "verifyPhoneNumber:verification:onVerificationCompleted");
+        WritableMap state = Arguments.createMap();
+
+        Parcel parcel = Parcel.obtain();
+        phoneAuthCredential.writeToParcel(parcel, 0);
+
+        // verificationId
+        parcel.setDataPosition(16);
+        String verificationId = parcel.readString();
+
+        // sms Code
+        parcel.setDataPosition(parcel.dataPosition() + 8);
+        String code = parcel.readString();
+
+        state.putString("code", code);
+        state.putString("verificationId", verificationId);
+        parcel.recycle();
+        sendPhoneStateEvent(appName, requestKey, "onVerificationComplete", state);
+      }
+
+      @Override
+      public void onVerificationFailed(FirebaseException e) {
+        // This callback is invoked in an invalid request for verification is made,
+        // e.g. phone number format is incorrect, or the SMS quota for the project
+        // has been exceeded
+        Log.d(TAG, "verifyPhoneNumber:verification:onVerificationFailed");
+        WritableMap state = Arguments.createMap();
+        state.putMap("error", getJSError(e));
+        sendPhoneStateEvent(appName, requestKey, "onVerificationFailed", state);
+      }
+
+      @Override
+      public void onCodeSent(
+          String verificationId, PhoneAuthProvider.ForceResendingToken forceResendingToken) {
+        Log.d(TAG, "verifyPhoneNumber:verification:onCodeSent");
+        mForceResendingToken = forceResendingToken;
+        WritableMap state = Arguments.createMap();
+        state.putString("verificationId", verificationId);
+
+        // todo forceResendingToken - it's actually just an empty class ... no actual
+        // token >.>
+        // Parcel parcel = Parcel.obtain();
+        // forceResendingToken.writeToParcel(parcel, 0);
+        //
+        // // verificationId
+        // parcel.setDataPosition(0);
+        // int int1 = parcel.readInt();
+        // String token = parcel.readString();
+        //
+        // state.putString("refreshToken", token);
+        // parcel.recycle();
+
+        state.putString("verificationId", verificationId);
+        sendPhoneStateEvent(appName, requestKey, "onCodeSent", state);
+      }
+
+      @Override
+      public void onCodeAutoRetrievalTimeOut(String verificationId) {
+        super.onCodeAutoRetrievalTimeOut(verificationId);
+        Log.d(TAG, "verifyPhoneNumber:verification:onCodeAutoRetrievalTimeOut");
+        WritableMap state = Arguments.createMap();
+        state.putString("verificationId", verificationId);
+        sendPhoneStateEvent(appName, requestKey, "onCodeAutoRetrievalTimeout", state);
+      }
+    };
 
     if (activity != null) {
       if (forceResend && mForceResendingToken != null) {
@@ -1229,8 +1226,16 @@ class ReactNativeFirebaseAuthModule extends ReactNativeFirebaseModule {
                     promiseWithAuthResult(task.getResult(), promise);
                   } else {
                     Exception exception = task.getException();
-                    Log.e(TAG, "link:onComplete:failure", exception);
-                    promiseRejectAuthException(promise, exception);
+
+                    if (exception instanceof FirebaseAuthUserCollisionException) {
+                      FirebaseAuthUserCollisionException authUserCollisionException = (FirebaseAuthUserCollisionException) exception;
+                      AuthCredential updatedCredential = authUserCollisionException.getUpdatedCredential();
+                      Log.e(TAG, "link:onComplete:collisionFailure", exception);
+                      promiseRejectLinkAuthException(promise, exception, updatedCredential);
+                    } else {
+                      Log.e(TAG, "link:onComplete:failure", exception);
+                      promiseRejectAuthException(promise, exception);
+                    }
                   }
                 });
       } else {
@@ -1305,40 +1310,46 @@ class ReactNativeFirebaseAuthModule extends ReactNativeFirebaseModule {
   /** Returns an instance of AuthCredential for the specified provider */
   private AuthCredential getCredentialForProvider(
       String provider, String authToken, String authSecret) {
-    switch (provider) {
-      case "facebook.com":
-        return FacebookAuthProvider.getCredential(authToken);
-      case "google.com":
-        return GoogleAuthProvider.getCredential(authToken, authSecret);
-      case "twitter.com":
-        return TwitterAuthProvider.getCredential(authToken, authSecret);
-      case "github.com":
-        return GithubAuthProvider.getCredential(authToken);
-      case "apple.com":
-        return OAuthProvider.newCredentialBuilder(provider)
-            .setIdTokenWithRawNonce(authToken, authSecret)
-            .build();
-      case "oauth":
-        return OAuthProvider.getCredential(provider, authToken, authSecret);
-      case "phone":
-        return getPhoneAuthCredential(authToken, authSecret);
-      case "password":
-        // authToken = email
-        // authSecret = password
-        return EmailAuthProvider.getCredential(authToken, authSecret);
-      case "emailLink":
-        // authToken = email
-        // authSecret = link
-        return EmailAuthProvider.getCredentialWithLink(authToken, authSecret);
-      default:
-        return null;
+    if (credentials.containsKey(authToken) && credentials.get(authToken) != null) {
+      return credentials.get(authToken);
+    } else {
+      switch (provider) {
+        case "facebook.com":
+          return FacebookAuthProvider.getCredential(authToken);
+        case "google.com":
+          return GoogleAuthProvider.getCredential(authToken, authSecret);
+        case "twitter.com":
+          return TwitterAuthProvider.getCredential(authToken, authSecret);
+        case "github.com":
+          return GithubAuthProvider.getCredential(authToken);
+        case "apple.com":
+          return OAuthProvider.newCredentialBuilder(provider)
+              .setIdTokenWithRawNonce(authToken, authSecret)
+              .build();
+        case "oauth":
+          return OAuthProvider.getCredential(provider, authToken, authSecret);
+        case "phone":
+          return getPhoneAuthCredential(authToken, authSecret);
+        case "password":
+          // authToken = email
+          // authSecret = password
+          return EmailAuthProvider.getCredential(authToken, authSecret);
+        case "emailLink":
+          // authToken = email
+          // authSecret = link
+          return EmailAuthProvider.getCredentialWithLink(authToken, authSecret);
+        default:
+          return null;
+      }
     }
   }
 
   /** Returns an instance of PhoneAuthCredential, potentially cached */
   private PhoneAuthCredential getPhoneAuthCredential(String authToken, String authSecret) {
-    // If the phone number is auto-verified quickly, then the verificationId can be null
-    // We cached the credential as part of the verifyPhoneNumber request to be re-used here
+    // If the phone number is auto-verified quickly, then the verificationId can be
+    // null
+    // We cached the credential as part of the verifyPhoneNumber request to be
+    // re-used here
     // if possible
     if (authToken == null && mCredential != null) {
       PhoneAuthCredential credential = mCredential;
@@ -1472,8 +1483,7 @@ class ReactNativeFirebaseAuthModule extends ReactNativeFirebaseModule {
             task -> {
               if (task.isSuccessful()) {
                 Log.d(TAG, "fetchProvidersForEmail:onComplete:success");
-                List<String> providers =
-                    Objects.requireNonNull(task.getResult()).getSignInMethods();
+                List<String> providers = Objects.requireNonNull(task.getResult()).getSignInMethods();
                 WritableArray array = Arguments.createArray();
 
                 if (providers != null) {
@@ -1566,9 +1576,11 @@ class ReactNativeFirebaseAuthModule extends ReactNativeFirebaseModule {
     firebaseAuth.useEmulator(host, port);
   }
 
-  /* ------------------
+  /*
+   * ------------------
    * INTERNAL HELPERS
-   * ---------------- */
+   * ----------------
+   */
 
   /**
    * Resolves or rejects an auth method promise without a user (user was missing)
@@ -1652,6 +1664,33 @@ class ReactNativeFirebaseAuthModule extends ReactNativeFirebaseModule {
     rejectPromiseWithCodeAndMessage(promise, error.getString("code"), error.getString("message"));
   }
 
+  /**
+   * promiseRejectLinkAuthException
+   *
+   * @param promise
+   * @param exception
+   * @param authCredential
+   */
+  private void promiseRejectLinkAuthException(Promise promise, Exception exception, AuthCredential authCredential) {
+    WritableMap error = getJSError(exception);
+    String authHashCode = String.valueOf(authCredential.hashCode());
+
+    WritableMap authCredentialsMap = Arguments.createMap();
+    authCredentialsMap.putString("providerId", authCredential.getProvider());
+    authCredentialsMap.putString("token", authHashCode);
+    authCredentialsMap.putString("secret", null);
+
+    // Temporarily store the non-serializable credential for later
+    credentials.put(authHashCode, authCredential);
+
+    WritableMap userInfoMap = Arguments.createMap();
+    userInfoMap.putString("code", error.getString("code"));
+    userInfoMap.putString("message", error.getString("message"));
+    userInfoMap.putMap("authCredential", authCredentialsMap);
+
+    promise.reject(error.getString("code"), error.getString("message"), userInfoMap);
+  }
+
   /**
    * getJSError
    *
@@ -1689,18 +1728,15 @@ class ReactNativeFirebaseAuthModule extends ReactNativeFirebaseModule {
             message = "The password is invalid or the user does not have a password.";
             break;
           case "USER_MISMATCH":
-            message =
-                "The supplied credentials do not correspond to the previously signed in user.";
+            message = "The supplied credentials do not correspond to the previously signed in user.";
             break;
           case "REQUIRES_RECENT_LOGIN":
-            message =
-                "This operation is sensitive and requires recent authentication. Log in again"
-                    + " before retrying this request.";
+            message = "This operation is sensitive and requires recent authentication. Log in again"
+                + " before retrying this request.";
             break;
           case "ACCOUNT_EXISTS_WITH_DIFFERENT_CREDENTIAL":
-            message =
-                "An account already exists with the same email address but different sign-in"
-                    + " credentials. Sign in using a provider associated with this email address.";
+            message = "An account already exists with the same email address but different sign-in"
+                + " credentials. Sign in using a provider associated with this email address.";
             break;
           case "EMAIL_ALREADY_IN_USE":
             message = "The email address is already in use by another account.";
@@ -1715,9 +1751,8 @@ class ReactNativeFirebaseAuthModule extends ReactNativeFirebaseModule {
             message = "The user\'s credential is no longer valid. The user must sign in again.";
             break;
           case "USER_NOT_FOUND":
-            message =
-                "There is no user record corresponding to this identifier. The user may have been"
-                    + " deleted.";
+            message = "There is no user record corresponding to this identifier. The user may have been"
+                + " deleted.";
             break;
           case "INVALID_USER_TOKEN":
             message = "The user\'s credential is no longer valid. The user must sign in again.";
@@ -1756,7 +1791,8 @@ class ReactNativeFirebaseAuthModule extends ReactNativeFirebaseModule {
   }
 
   /**
-   * Converts a List of UserInfo instances into the correct format to match the web sdk
+   * Converts a List of UserInfo instances into the correct format to match the
+   * web sdk
    *
    * @param providerData List<UserInfo> user.getProviderData()
    * @return WritableArray array
@@ -1782,8 +1818,10 @@ class ReactNativeFirebaseAuthModule extends ReactNativeFirebaseModule {
         }
 
         final String phoneNumber = userInfo.getPhoneNumber();
-        // The Android SDK is missing the phone number property for the phone provider when the
-        // user first signs up using their phone number.  Use the phone number from the user
+        // The Android SDK is missing the phone number property for the phone provider
+        // when the
+        // user first signs up using their phone number. Use the phone number from the
+        // user
         // object instead
         if (PhoneAuthProvider.PROVIDER_ID.equals(userInfo.getProviderId())
             && (userInfo.getPhoneNumber() == null || "".equals(userInfo.getPhoneNumber()))) {
@@ -1794,7 +1832,8 @@ class ReactNativeFirebaseAuthModule extends ReactNativeFirebaseModule {
           userInfoMap.putNull("phoneNumber");
         }
 
-        // The Android SDK is missing the email property for the email provider, so we use UID
+        // The Android SDK is missing the email property for the email provider, so we
+        // use UID
         // instead
         if (EmailAuthProvider.PROVIDER_ID.equals(userInfo.getProviderId())
             && (userInfo.getEmail() == null || "".equals(userInfo.getEmail()))) {
@@ -1896,15 +1935,12 @@ class ReactNativeFirebaseAuthModule extends ReactNativeFirebaseModule {
 
     if (actionCodeSettings.hasKey("android")) {
       ReadableMap android = actionCodeSettings.getMap("android");
-      boolean installApp =
-          Objects.requireNonNull(android).hasKey("installApp") && android.getBoolean("installApp");
-      String minimumVersion =
-          android.hasKey("minimumVersion") ? android.getString("minimumVersion") : null;
+      boolean installApp = Objects.requireNonNull(android).hasKey("installApp") && android.getBoolean("installApp");
+      String minimumVersion = android.hasKey("minimumVersion") ? android.getString("minimumVersion") : null;
       String packageName = android.getString("packageName");
 
-      builder =
-          builder.setAndroidPackageName(
-              Objects.requireNonNull(packageName), installApp, minimumVersion);
+      builder = builder.setAndroidPackageName(
+          Objects.requireNonNull(packageName), installApp, minimumVersion);
     }
 
     if (actionCodeSettings.hasKey("iOS")) {
@@ -1929,8 +1965,7 @@ class ReactNativeFirebaseAuthModule extends ReactNativeFirebaseModule {
     eventBody.putString("requestKey", requestKey);
     eventBody.putString("type", type);
     eventBody.putMap("state", state);
-    ReactNativeFirebaseEvent event =
-        new ReactNativeFirebaseEvent("phone_auth_state_changed", eventBody, appName);
+    ReactNativeFirebaseEvent event = new ReactNativeFirebaseEvent("phone_auth_state_changed", eventBody, appName);
     emitter.sendEvent(event);
   }
 
@@ -1954,7 +1989,7 @@ class ReactNativeFirebaseAuthModule extends ReactNativeFirebaseModule {
       FirebaseAuth firebaseAuth = FirebaseAuth.getInstance(instance);
       FirebaseUser user = firebaseAuth.getCurrentUser();
 
-      //noinspection ConstantConditions
+      // noinspection ConstantConditions
       appLanguage.put(appName, firebaseAuth.getLanguageCode());
 
       if (user != null) {

@mikehardy
Copy link
Collaborator

Quite alright - I can still apply it then use tools to ignore spacing etc :-), thanks for posting this

@mikehardy mikehardy reopened this Mar 23, 2022
@mikehardy mikehardy added Workflow: Needs Review Pending feedback or review from a maintainer. and removed blocked: firebase-support Pending feedback or review from google support or response on official sdk repo issue. labels Mar 23, 2022
@Shaninnik
Copy link

@mikehardy There is also my patch that I've posted earlier here, been running it ever since in production with 0 issues. The one that @kristfal posted appears to be more complete, unfortunately I can't review right now

@stale
Copy link

stale bot commented Apr 28, 2022

Hello 👋, to help manage issues we automatically close stale issues.
This issue has been automatically marked as stale because it has not had activity for quite some time. Has this issue been fixed, or does it still require the community's attention?

This issue will be closed in 15 days if no further activity occurs.
Thank you for your contributions.

@stale stale bot added the Type: Stale Issue has become stale - automatically added by Stale bot label Apr 28, 2022
@mikehardy mikehardy removed the Type: Stale Issue has become stale - automatically added by Stale bot label May 10, 2022
@github-actions
Copy link

github-actions bot commented Dec 5, 2022

Hello 👋, to help manage issues we automatically close stale issues.

This issue has been automatically marked as stale because it has not had activity for quite some time.Has this issue been fixed, or does it still require attention?

This issue will be closed in 15 days if no further activity occurs.

Thank you for your contributions.

@github-actions github-actions bot added the Type: Stale Issue has become stale - automatically added by Stale bot label Dec 5, 2022
@Shaninnik
Copy link

Shaninnik commented Dec 5, 2022

@mikehardy I have just checked the source code for the latest version of auth and I think it is still an issue. (cant check because I cant use use_frameworks) Yes it was somewhat made better by removing an attempt to upgrade anonymous users but it still will not forward updated credentials authCredentials in promise reject to JS side on Android which is done in iOS:

      rejectPromiseWithUserInfo:reject
                       userInfo:(NSMutableDictionary *)@{
                         @"code" : [jsError valueForKey:@"code"],
                         @"message" : [jsError valueForKey:@"message"],
                         @"nativeErrorMessage" : [jsError valueForKey:@"nativeErrorMessage"],
                         @"authCredential" : [jsError valueForKey:@"authCredential"],
                         @"resolver" : [jsError valueForKey:@"resolver"]
                       }];

and

    @"code" : code,
    @"message" : message,
    @"nativeErrorMessage" : nativeErrorMessage,
    @"authCredential" : authCredentialDict != nil ? (id)authCredentialDict : [NSNull null],
    @"resolver" : resolverDict != nil ? (id)resolverDict : [NSNull null]
  };

@github-actions github-actions bot removed the Type: Stale Issue has become stale - automatically added by Stale bot label Dec 5, 2022
@github-actions
Copy link

github-actions bot commented Jan 2, 2023

Hello 👋, to help manage issues we automatically close stale issues.

This issue has been automatically marked as stale because it has not had activity for quite some time.Has this issue been fixed, or does it still require attention?

This issue will be closed in 15 days if no further activity occurs.

Thank you for your contributions.

@github-actions github-actions bot added the Type: Stale Issue has become stale - automatically added by Stale bot label Jan 2, 2023
@github-actions github-actions bot closed this as not planned Won't fix, can't repro, duplicate, stale Jan 17, 2023
@faljabi
Copy link

faljabi commented Jul 6, 2023

is this resolved? I'm still not getting the credentials on the error object for Android?

@Shaninnik
Copy link

This issue is still not resolved. You still can't upgrade anonymous users using phone auth on Android. Still same issue: iOS is passing authCredential back after calling linkWithCredential while Android does not. @faljabi you can use patch-package for latest react native firebase version 18.1.0:

diff --git a/node_modules/@react-native-firebase/auth/android/src/main/java/io/invertase/firebase/auth/ReactNativeFirebaseAuthModule.java b/node_modules/@react-native-firebase/auth/android/src/main/java/io/invertase/firebase/auth/ReactNativeFirebaseAuthModule.java
index b6406f1..cc2f6e1 100644
--- a/node_modules/@react-native-firebase/auth/android/src/main/java/io/invertase/firebase/auth/ReactNativeFirebaseAuthModule.java
+++ b/node_modules/@react-native-firebase/auth/android/src/main/java/io/invertase/firebase/auth/ReactNativeFirebaseAuthModule.java
@@ -43,6 +43,7 @@ import com.google.firebase.auth.FacebookAuthProvider;
 import com.google.firebase.auth.FirebaseAuth;
 import com.google.firebase.auth.FirebaseAuthException;
 import com.google.firebase.auth.FirebaseAuthInvalidCredentialsException;
+import com.google.firebase.auth.FirebaseAuthUserCollisionException;
 import com.google.firebase.auth.FirebaseAuthMultiFactorException;
 import com.google.firebase.auth.FirebaseAuthProvider;
 import com.google.firebase.auth.FirebaseAuthSettings;
@@ -103,6 +104,10 @@ class ReactNativeFirebaseAuthModule extends ReactNativeFirebaseModule {
   private final HashMap<String, MultiFactorResolver> mCachedResolvers = new HashMap<>();
   private final HashMap<String, MultiFactorSession> mMultiFactorSessions = new HashMap<>();
 
+  // patching anonumous phone auth linkWithCredentials
+  // https://github.com/invertase/react-native-firebase/issues/4911
+  private HashMap<String, AuthCredential> credentials = new HashMap<>();
+
   ReactNativeFirebaseAuthModule(ReactApplicationContext reactContext) {
     super(reactContext, TAG);
   }
@@ -1501,8 +1506,15 @@ class ReactNativeFirebaseAuthModule extends ReactNativeFirebaseModule {
                     promiseWithAuthResult(task.getResult(), promise);
                   } else {
                     Exception exception = task.getException();
-                    Log.e(TAG, "link:onComplete:failure", exception);
-                    promiseRejectAuthException(promise, exception);
+                    if (exception instanceof FirebaseAuthUserCollisionException) {
+                      FirebaseAuthUserCollisionException authUserCollisionException = (FirebaseAuthUserCollisionException) exception;
+                      AuthCredential updatedCredential = authUserCollisionException.getUpdatedCredential();
+                      Log.e(TAG, "link:onComplete:collisionFailure", exception);
+                      promiseRejectLinkAuthException(promise, exception, updatedCredential);
+                    } else {
+                      Log.e(TAG, "link:onComplete:failure", exception);
+                      promiseRejectAuthException(promise, exception);
+                    }
                   }
                 });
       } else {
@@ -1580,34 +1592,37 @@ class ReactNativeFirebaseAuthModule extends ReactNativeFirebaseModule {
     if (provider.startsWith("oidc.")) {
       return OAuthProvider.newCredentialBuilder(provider).setIdToken(authToken).build();
     }
-
-    switch (provider) {
-      case "facebook.com":
-        return FacebookAuthProvider.getCredential(authToken);
-      case "google.com":
-        return GoogleAuthProvider.getCredential(authToken, authSecret);
-      case "twitter.com":
-        return TwitterAuthProvider.getCredential(authToken, authSecret);
-      case "github.com":
-        return GithubAuthProvider.getCredential(authToken);
-      case "apple.com":
-        return OAuthProvider.newCredentialBuilder(provider)
-            .setIdTokenWithRawNonce(authToken, authSecret)
-            .build();
-      case "oauth":
-        return OAuthProvider.getCredential(provider, authToken, authSecret);
-      case "phone":
-        return getPhoneAuthCredential(authToken, authSecret);
-      case "password":
-        // authToken = email
-        // authSecret = password
-        return EmailAuthProvider.getCredential(authToken, authSecret);
-      case "emailLink":
-        // authToken = email
-        // authSecret = link
-        return EmailAuthProvider.getCredentialWithLink(authToken, authSecret);
-      default:
-        return null;
+    if (credentials.containsKey(authToken) && credentials.get(authToken) != null) {
+      return credentials.get(authToken);
+    } else {
+      switch (provider) {
+        case "facebook.com":
+          return FacebookAuthProvider.getCredential(authToken);
+        case "google.com":
+          return GoogleAuthProvider.getCredential(authToken, authSecret);
+        case "twitter.com":
+          return TwitterAuthProvider.getCredential(authToken, authSecret);
+        case "github.com":
+          return GithubAuthProvider.getCredential(authToken);
+        case "apple.com":
+          return OAuthProvider.newCredentialBuilder(provider)
+              .setIdTokenWithRawNonce(authToken, authSecret)
+              .build();
+        case "oauth":
+          return OAuthProvider.getCredential(provider, authToken, authSecret);
+        case "phone":
+          return getPhoneAuthCredential(authToken, authSecret);
+        case "password":
+          // authToken = email
+          // authSecret = password
+          return EmailAuthProvider.getCredential(authToken, authSecret);
+        case "emailLink":
+          // authToken = email
+          // authSecret = link
+          return EmailAuthProvider.getCredentialWithLink(authToken, authSecret);
+        default:
+          return null;
+      }
     }
   }
 
@@ -1939,6 +1954,33 @@ class ReactNativeFirebaseAuthModule extends ReactNativeFirebaseModule {
         promise, error.getString("code"), error.getString("message"), resolverAsMap);
   }
 
+  /**
+   * promiseRejectLinkAuthException
+   *
+   * @param promise
+   * @param exception
+   * @param authCredential
+   */
+  private void promiseRejectLinkAuthException(Promise promise, Exception exception, AuthCredential authCredential) {
+    WritableMap error = getJSError(exception);
+    String authHashCode = String.valueOf(authCredential.hashCode());
+
+    WritableMap authCredentialsMap = Arguments.createMap();
+    authCredentialsMap.putString("providerId", authCredential.getProvider());
+    authCredentialsMap.putString("token", authHashCode);
+    authCredentialsMap.putString("secret", null);
+
+    // Temporarily store the non-serializable credential for later
+    credentials.put(authHashCode, authCredential);
+
+    WritableMap userInfoMap = Arguments.createMap();
+    userInfoMap.putString("code", error.getString("code"));
+    userInfoMap.putString("message", error.getString("message"));
+    userInfoMap.putMap("authCredential", authCredentialsMap);
+
+    promise.reject(error.getString("code"), error.getString("message"), userInfoMap);
+  }
+
   /**
    * getJSError
    *

@joaqo
Copy link
Contributor

joaqo commented Apr 10, 2024

This is still an issue in April 2024. Shouldn't this just be a simple merge of the above patch @mikehardy ?

@mikehardy
Copy link
Collaborator

@joaqo reasonable PRs are merged if you would like to propose one - I haven't looked at this closely in a while but I try to merge PRs as a high priority with any time I have if you propose one

@joaqo
Copy link
Contributor

joaqo commented May 14, 2024

Tested @Shaninnik's patch and it works perfectly. Created this PR so other people can have it in the future as well. Added credit to @Shaninnik in the commit message, thanks for the great patch! @mikehardy I think its ready to merge.

@mikehardy
Copy link
Collaborator

Sorry this closed stale, thanks very much to @joaqo for carrying through the excellent detective + fixing work from @Shaninnik @kristfal and all the others collaborating here. This should now close for real when #7793 merges and releases

@mikehardy mikehardy reopened this May 14, 2024
@Shaninnik
Copy link

@joaqo thank you for your time creating this PR!

@joaqo
Copy link
Contributor

joaqo commented May 16, 2024

Awesome, thank you for the quick merge! 👏

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
platform: android plugin: authentication Firebase Authentication type: bug New bug report Type: Stale Issue has become stale - automatically added by Stale bot Workflow: Needs Review Pending feedback or review from a maintainer.
Projects
None yet