react-native-firebase icon indicating copy to clipboard operation
react-native-firebase copied to clipboard

[🐛] Bug Report Title - Receiving multiple otp sms in iOS device for phone authentication

Open akhilsanker opened this issue 1 year ago • 7 comments

Issue

We are using @react-native-firebase/auth v11.5.0 in our react native (v0.66.5) app for phone authentication. But frequently in iOS devices, users are getting multiple otp sms at the same time.

B8505052-1A87-4DD2-9157-BCA5E8DB3B89


Project Files

Javascript

Click To Expand

package.json:

{
  "name": "",
  "version": "0.0.1",
  "private": true,
  "scripts": {
    "android": "react-native run-android",
    "ios": "react-native run-ios",
    "start": "react-native start",
    "test": "jest",
    "lint": "eslint ."
  },
  "dependencies": {
    "@appbaseio/reactivesearch-native": "0.12.0",
    "@codler/react-native-keyboard-aware-scroll-view": "2.0.0",
    "@ptomasroos/react-native-multi-slider": "2.2.2",
    "@react-native-camera-roll/camera-roll": "5.4.0",
    "@react-native-clipboard/clipboard": "1.9.0",
    "@react-native-community/art": "1.2.0",
    "@react-native-community/async-storage": "1.9.0",
    "@react-native-community/datetimepicker": "2.6.2",
    "@react-native-community/geolocation": "2.0.2",
    "@react-native-community/masked-view": "0.1.11",
    "@react-native-community/netinfo": "5.9.0",
    "@react-native-community/viewpager": "4.2.2",
    "@react-native-firebase/app": "11.5.0",
    "@react-native-firebase/auth": "11.5.0",
    "@react-native-firebase/firestore": "11.5.0",
    "@react-native-google-signin/google-signin": "9.0.2",
    "@react-native-picker/picker": "1.16.8",
    "@react-native-twitter-signin/twitter-signin": "1.2.0",
    "@react-navigation/bottom-tabs": "5.11.15",
    "@react-navigation/core": "5.16.1",
    "@react-navigation/drawer": "5.12.9",
    "@react-navigation/material-bottom-tabs": "5.3.19",
    "@react-navigation/material-top-tabs": "5.3.19",
    "@react-navigation/native": "5.9.8",
    "@react-navigation/stack": "5.14.9",
    "@segment/analytics-react-native": "1.5.3",
    "@segment/analytics-react-native-firebase": "1.5.0",
    "@segment/analytics-react-native-plugin-facebook-app-events": "0.2.2",
    "@types/i18n-js": "3.8.2",
    "@types/jest": "25.1.4",
    "@types/lodash": "4.14.176",
    "@types/react": "16.14.20",
    "@types/react-native": "0.66.5",
    "@types/react-native-datepicker": "1.7.1",
    "@types/react-native-snap-carousel": "3.8.5",
    "@types/react-native-text-input-mask": "0.7.6",
    "@types/react-redux": "7.1.20",
    "@types/react-test-renderer": "16.9.2",
    "@viro-community/react-viro": "^2.23.0",
    "appcenter": "4.4.3",
    "appcenter-analytics": "4.4.3",
    "appcenter-crashes": "4.4.3",
    "axios": "0.27.2",
    "axios-hooks": "3.1.0",
    "bodybuilder": "2.2.21",
    "credit-card-type": "9.1.0",
    "cross-fetch": "3.0.6",
    "i18n-js": "3.5.1",
    "isomorphic-fetch": "3.0.0",
    "lodash": "4.17.15",
    "moment": "2.26.0",
    "prop-types": "15.7.2",
    "react": "17.0.2",
    "react-countup": "4.4.0",
    "react-hook-form": "6.15.8",
    "react-native": "0.66.5",
    "react-native-animated-ellipsis": "2.0.0",
    "react-native-appsflyer": "6.10.2",
    "react-native-arkit": "^0.8.0",
    "react-native-autocomplete-dropdown": "2.0.7",
    "react-native-autocomplete-input": "4.1.0",
    "react-native-calendars": "1.1255.0",
    "react-native-code-push": "7.0.4",
    "react-native-countdown-circle-timer": "2.5.4",
    "react-native-date-picker": "4.1.1",
    "react-native-datepicker": "1.7.2",
    "react-native-device-info": "8.4.5",
    "react-native-dropdown-picker": "5.2.3",
    "react-native-elements": "2.3.2",
    "react-native-fast-image": "8.5.11",
    "react-native-fbsdk-next": "8.0.5",
    "react-native-geolocation-service": "5.0.0",
    "react-native-gesture-handler": "1.6.0",
    "react-native-gifted-chat": "2.1.0",
    "react-native-google-places-autocomplete": "2.4.1",
    "react-native-image-crop-picker": "0.39.0",
    "react-native-image-progress": "1.2.0",
    "react-native-image-resizer": "1.4.5",
    "react-native-input-scroll-view": "1.11.0",
    "react-native-klarna-inapp-sdk": "2.1.9",
    "react-native-linear-gradient": "2.5.6",
    "react-native-localize": "1.4.3",
    "react-native-maps": "0.27.1",
    "react-native-modal": "12.1.0",
    "react-native-normalize": "1.0.1",
    "react-native-onesignal": "4.5.0",
    "react-native-paper": "4.12.2",
    "react-native-permissions": "3.6.1",
    "react-native-pie-chart": "2.1.1",
    "react-native-pinchable": "0.2.1",
    "react-native-popup-menu": "0.15.11",
    "react-native-progress": "5.0.0",
    "react-native-reanimated": "1.7.0",
    "react-native-render-html": "5.1.1",
    "react-native-responsive-dimensions": "3.1.1",
    "react-native-restart": "0.0.17",
    "react-native-root-toast": "3.3.0",
    "react-native-safe-area-context": "0.7.3",
    "react-native-screens": "2.2.0",
    "react-native-share": "7.3.7",
    "react-native-skeleton-placeholder": "2.0.7",
    "react-native-snap-carousel": "4.0.0-beta.6",
    "react-native-stripe-checkout-webview": "0.0.12",
    "react-native-svg": "12.1.1",
    "react-native-svg-transformer": "1.0.0",
    "react-native-swipe-list-view": "3.2.9",
    "react-native-swiper": "1.6.0-rc.3",
    "react-native-tab-view": "2.14.4",
    "react-native-text-input-mask": "3.1.4",
    "react-native-tracking-transparency": "0.1.1",
    "react-native-typescript-transformer": "1.2.13",
    "react-native-ux-cam": "5.4.0",
    "react-native-vector-icons": "8.1.0",
    "react-native-webview": "11.22.7",
    "react-redux": "7.2.6",
    "recyclerlistview": "3.1.0-beta.6",
    "redux": "4.1.2",
    "redux-logger": "3.0.6",
    "redux-thunk": "2.4.0",
    "rn-fetch-blob": "0.12.0",
    "rn-swipe-button": "1.3.6",
    "typescript": "3.9.10"
  },
  "devDependencies": {
    "@babel/core": "7.12.9",
    "@babel/runtime": "7.12.5",
    "@react-native-community/eslint-config": "2.0.0",
    "@types/metro-config": "0.66.0",
    "@types/react-native-calendars": "1.1264.3",
    "@types/react-native-dotenv": "0.2.0",
    "@types/react-native-restart": "0.0.0",
    "@typescript-eslint/eslint-plugin": "2.24.0",
    "babel-jest": "26.6.3",
    "eslint": "7.32.0",
    "eslint-config-airbnb-typescript": "7.2.0",
    "eslint-config-prettier": "6.10.0",
    "eslint-detailed-reporter": "0.8.0",
    "eslint-plugin-import": "2.25.2",
    "eslint-plugin-jsx-a11y": "6.2.3",
    "eslint-plugin-prettier": "3.4.1",
    "eslint-plugin-promise": "4.2.1",
    "eslint-plugin-react": "7.26.1",
    "eslint-plugin-react-hooks": "2.5.0",
    "jest": "26.6.3",
    "metro-react-native-babel-preset": "0.66.2",
    "prettier": "1.19.1",
    "react-native-dotenv": "0.2.0",
    "react-test-renderer": "17.0.2"
  },
  "jest": {
    "preset": "react-native"
  }
}

firebase.json for react-native-firebase v6:

# N/A

iOS

Click To Expand

ios/Podfile:

  • [ ] I'm not using Pods
  • [x] 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, '12.1'

target '' do

  config = use_native_modules!

  use_react_native!(
    :path => config[:reactNativePath],
    # to enable hermes on iOS, change `false` to `true` and then install pods
    :hermes_enabled => false
  )

  permissions_path = '../node_modules/react-native-permissions/ios'

  pod 'React-RCTText', :path => '../node_modules/react-native/Libraries/Text', :modular_headers => true
  pod 'ReactNativeART', :path => '../node_modules/@react-native-community/art'
  pod 'Permission-PhotoLibrary', :path => "#{permissions_path}/PhotoLibrary"
  pod 'Permission-PhotoLibraryAddOnly', :path => "#{permissions_path}/PhotoLibraryAddOnly"
  pod 'boost', :podspec => '../node_modules/react-native/third-party-podspecs/boost.podspec'
  # target 'inckdTests' 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 the next line.
  use_flipper!({ "Flipper-DoubleConversion" => "1.1.7" })
  # post_install do |installer|
  #   react_native_post_install(installer)
  # end

  post_install do |installer|
    __apply_Xcode_12_5_M1_post_install_workaround(installer)
    installer.pods_project.targets.each do |target|
      target.build_configurations.each do |config|
        config.build_settings['APPLICATION_EXTENSION_API_ONLY'] = 'NO'
      end
    end
    installer.pods_project.targets.each do |target|
      target.build_configurations.each do |config|
        # Force CocoaPods targets to always build for x86_64
        config.build_settings['ARCHS[sdk=iphonesimulator*]'] = 'x86_64'
      end
    end
  end
end

target 'OneSignalNotificationServiceExtension' do
  pod 'OneSignalXCFramework', '>= 3.0', '< 4.0'
end

# post_install do |pi|
#   pi.pods_project.targets.each do |t|
#     t.build_configurations.each do |config|
#       config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '12.1'
#     end
#   end
# end

# post_install do |installer|
#   installer.pods_project.targets.each do |target|
#     target.build_configurations.each do |config|
#       config.build_settings['APPLICATION_EXTENSION_API_ONLY'] = 'NO'
#     end
#   end
# end

AppDelegate.m:

#import "AppDelegate.h"

#import <React/RCTBridge.h>
#import <React/RCTBundleURLProvider.h>
#import <React/RCTRootView.h>
#import <Firebase.h>
#import <CodePush/CodePush.h>
#import <FBSDKCoreKit/FBSDKCoreKit.h>
#import <TwitterKit/TWTRKit.h>
#import <React/RCTLinkingManager.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 (@available(iOS 14, *)) {
    UIDatePicker *picker = [UIDatePicker appearance];
    picker.preferredDatePickerStyle = UIDatePickerStyleWheels;
  }
  
  #ifdef FB_SONARKIT_ENABLED
    InitializeFlipper(application);
  #endif

  if ([FIRApp defaultApp] == nil) {
    [FIRApp configure];
  }
  RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:launchOptions];
  RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge
                                                   moduleName:@""
                                            initialProperties:nil];

  if (@available(iOS 13.0, *)) {
      rootView.backgroundColor = [UIColor systemBackgroundColor];
  } else {
      rootView.backgroundColor = [UIColor whiteColor];
  }

  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;
}

- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge
{
#if DEBUG
  return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil];
#else
  return [CodePush bundleURL];
#endif
}

- (void)applicationWillResignActive:(UIApplication *)application {
    if (self.taskIdentifier != UIBackgroundTaskInvalid) {
        [application endBackgroundTask:self.taskIdentifier];
        self.taskIdentifier = UIBackgroundTaskInvalid;
    }
    
    __weak typeof(self) weakSelf = self;
    self.taskIdentifier = [application beginBackgroundTaskWithName:nil expirationHandler:^{
        [application endBackgroundTask:weakSelf.taskIdentifier];
        weakSelf.taskIdentifier = UIBackgroundTaskInvalid;
    }];
}


- (BOOL)application:(UIApplication *)application
   openURL:(NSURL *)url
   options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options
{
  return [RCTLinkingManager application:application openURL:url options:options];
}

- (BOOL)application:(UIApplication *)application continueUserActivity:(nonnull NSUserActivity *)userActivity
 restorationHandler:(nonnull void (^)(NSArray<id<UIUserActivityRestoring>> * _Nullable))restorationHandler
{
 return [RCTLinkingManager application:application
                  continueUserActivity:userActivity
                    restorationHandler:restorationHandler];
}

@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:

// N/A

android/app/build.gradle:

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

buildscript {
    ext {
        buildToolsVersion = "30.0.2"
        minSdkVersion = 21
        compileSdkVersion = 33
        targetSdkVersion = 33
        ndkVersion = "21.4.7075529"
        googlePlayServicesVersion = "17.0.0"//added for react-native-geolocation-service modules
        androidMapsUtilsVersion="2.2.0"
        androidXCore = "1.6.0"
        kotlinVersion = "1.6.0"
        googlePlayServicesAuthVersion = "19.2.0" // <--- use version 19.2.0 or newer for social media login
    }
    repositories {
        google()
        mavenCentral()
    }
    dependencies {
        classpath("com.android.tools.build:gradle:4.2.2") // <--- use version 4.2.2 or newer for social media login
        classpath 'com.google.gms:google-services:4.3.10' // <--- use version 4.3.10 or newer for social media login
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

allprojects {
    repositories {
        mavenCentral()
        mavenLocal()
        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://www.jitpack.io' }
        maven { url 'https://maven.google.com' }
        maven {
            url 'https://x.klarnacdn.net/mobile-sdk/'
        }
    }
    configurations.all {
        resolutionStrategy {
            dependencySubstitution {
                substitute module("com.redmadrobot:input-mask-android:6.0.0") using module('com.github.RedMadRobot:input-mask-android:6.0.0')
            }
        }
    }
}

android/settings.gradle:

rootProject.name = ''
include ':react-native-vector-icons'
project(':react-native-vector-icons').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-vector-icons/android')
include ':react-native-linear-gradient'
project(':react-native-linear-gradient').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-linear-gradient/android')
include ':react-native-localize'
project(':react-native-localize').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-localize/android')
apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings)
include ':app', ':react-native-code-push'
project(':react-native-code-push').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-code-push/android/app')
include ':app'

MainApplication.java:

package xxx.xxxx.xxxx;

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.oblador.vectoricons.VectorIconsPackage;
import com.reactcommunity.rnlocalize.RNLocalizePackage;
import com.facebook.react.ReactNativeHost;
import com.facebook.react.ReactPackage;
import com.facebook.soloader.SoLoader;
import com.BV.LinearGradient.LinearGradientPackage;
import com.reactnative.ivpusic.imagepicker.PickerPackage;
import java.lang.reflect.InvocationTargetException;
import java.util.List;
import com.microsoft.codepush.react.CodePush;

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());
             packages.add(new LinearGradientPackage());
          return packages;
        }

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

        @Override
        protected String getJSBundleFile() {
            return CodePush.getJSBundleFile();
        }
      };

  @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
   */
  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.xxx.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="xxxx"
  android:exported="true" 
>

  <uses-permission android:name="android.permission.INTERNET" />
  <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
  <uses-permission android:name="android.permission.CAMERA"/>
  <uses-feature android:name="android.hardware.camera" android:required="false" />
  <uses-feature android:name="android.hardware.camera.front" android:required="false" />
  <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
  <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
  <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
  <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
  <uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
  <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
  <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <application
      android:usesCleartextTraffic="true"
      android:name=".MainApplication"
      android:label="@string/app_name"
      android:icon="@mipmap/ic_launcher"
      android:allowBackup="false"
      android:theme="@style/AppTheme"
      android:largeHeap="true"
      android:requestLegacyExternalStorage="true"
      android:networkSecurityConfig="@xml/network_security_config"
      >
      <activity
        android:name=".MainActivity" 
        android:label="@string/app_name"
        android:configChanges="keyboard|keyboardHidden|orientation|screenSize|uiMode"
        android:launchMode="singleTask"
        android:windowSoftInputMode="adjustResize"
        android:screenOrientation="portrait"
        android:exported="true"
      >
        <intent-filter
          android:exported="false"
        >
            <action android:name="android.intent.action.MAIN" />
            <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>
        <intent-filter
          android:autoVerify="true">
          <action android:name="android.intent.action.VIEW" />
          <category android:name="android.intent.category.DEFAULT" />
          <category android:name="android.intent.category.BROWSABLE" />
          <data android:scheme="https" />
          <data android:host="xxx" />
          <data android:pathPattern=".*" />
        </intent-filter>
      </activity>
      <activity 
        android:name="com.facebook.react.devsupport.DevSettingsActivity" 
        android:exported="true"
        />
     
      <meta-data
       android:name="com.google.android.geo.API_KEY"
       android:value="AIzaSyCkXEvPUDC09JqbSgBF-uW0gecKocNbSW8"/>
      <meta-data 
      android:name="com.onesignal.NotificationAccentColor.DEFAULT"
       android:value="#8C62DB"
       />
      <meta-data
      android:name="com.google.android.gms.wallet.api.enabled"
      android:value="true" />

      <meta-data android:name="com.facebook.sdk.ApplicationId" android:value="@string/facebook_app_id"/>
      <meta-data android:name="com.facebook.sdk.ClientToken" android:value="@string/facebook_client_token"/>

       <!-- You will also only need to add this uses-library tag -->
      <uses-library android:name="org.apache.http.legacy" android:required="false"/>
    </application>
</manifest>


Environment

Click To Expand

react-native info output:

System:
    OS: macOS 13.0.1
    CPU: (16) x64 Intel(R) Core(TM) i9-9980HK CPU @ 2.40GHz
    Memory: 74.70 MB / 32.00 GB
    Shell: 5.8.1 - /bin/zsh
  Binaries:
    Node: 18.3.0 - /usr/local/bin/node
    Yarn: Not Found
    npm: 8.11.0 - /usr/local/bin/npm
    Watchman: Not Found
  Managers:
    CocoaPods: 1.11.3 - /usr/local/bin/pod
  SDKs:
    iOS SDK:
      Platforms: DriverKit 22.2, iOS 16.2, macOS 13.1, tvOS 16.1, watchOS 9.1
    Android SDK: Not Found
  IDEs:
    Android Studio: 2021.2 AI-212.5712.43.2112.8609683
    Xcode: 14.2/14C18 - /usr/bin/xcodebuild
  Languages:
    Java: 11.0.12 - /usr/bin/javac
  npmPackages:
    @react-native-community/cli: Not Found
    react: 17.0.2 => 17.0.2 
    react-native: 0.66.5 => 0.66.5 
    react-native-macos: Not Found
  npmGlobalPackages:
    *react-native*: Not Found
  • Platform that you're experiencing the issue on:
    • [ ] iOS
    • [ ] Android
    • [x] 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:
    • @react-native-firebase/auth": "11.5.0"
  • Are you using TypeScript?
    • Y & "typescript": "3.9.10"

akhilsanker avatar Dec 20 '23 08:12 akhilsanker

We are using @react-native-firebase/auth v11.5.0 in our react native (v0.66.5) app for phone authentication.

This is software from years ago, when logging issues it needs to be on current versions to make sure we're not chasing ghosts with limited time

Please let me know if you reproduce on current version of react-native-firebase

Also note that if you do reproduce, we'll need a reproducible example that is minimal, just a simple index.js / App.js that demonstrates the problem https://stackoverflow.com/help/minimal-reproducible-example as it is most likely a project-specific error with (for example) multiple instances of hooks or code running when you thought they would run only once

mikehardy avatar Dec 20 '23 13:12 mikehardy

Hi @mikehardy , Thanks for the reply.

We are currently running our project in the above mentioned versions. There are lesser chance to upgrade the react native version. However, we will try upgrading the firebase auth version to latest version and will give an update about the issue

Thanks.

akhilsanker avatar Dec 20 '23 13:12 akhilsanker

I'm not sure react-native 0.66.5 will even compile on Xcode 15 FWIW. I believe you need at least react-native 0.68 for that. Believe it or not, upgrading from 0.66 to 0.73 is not that hard anymore, as the new architecture stuff that came in (requiring lots of native changes as you upgraded) were also refined quite a bit and mostly removed (also requiring a lot of native changes...) leaving the upgrade process to no longer require that many changes :-). Perhaps waiting so long has benefited you.

https://react-native-community.github.io/upgrade-helper/?from=0.66.5&to=0.73.1

Either way, you are asking for support, and you are doing so on outdated versions, and it is important to realize that there is a high risk that you are possibly mal-investing your time by doing so as it is always possible for bugs to be fixed already, meaning all time spent investigating problems on the old version were wasted - while the alternative - upgrading - even react-native - is something you know you will have to do eventually, so at least to me it always seems best to go ahead and get everything up to date, then try to reproduce, then ask for support

mikehardy avatar Dec 20 '23 14:12 mikehardy

hi @mikehardy ,

We upgraded react native and @react-native-firebase/auth, but getting the same in iOS. Receiving multiple otp at same time.

"@react-native-firebase/app": "18.7.3",
"@react-native-firebase/auth": "18.7.3",
"@react-native-firebase/firestore": "18.7.3",
"react-native": "0.71.14",

Updated to stable version of react native.

Code snippet

Phone input screen

import React, { useEffect, useRef, useState } from 'react';
import {
  Animated,
  Keyboard,
  ScrollView,
  StyleSheet,
  TouchableOpacity,
  View
} from 'react-native';
import { AccessToken, LoginManager } from 'react-native-fbsdk-next';
import { GoogleSignin } from '@react-native-google-signin/google-signin';
import { useDispatch, useSelector } from 'react-redux';
import auth, { FirebaseAuthTypes } from '@react-native-firebase/auth';
import OneSignal from 'react-native-onesignal';

import {
  useAxiosGet,
  useAxiosPost,
  useAxiosPut,
  useBackHandler,
  useBasicAnimation,
  useKeyboardHideShow
} from '@hook';

import { ASYNC_STORE, storeData } from '@storage';
import {
  Button,
  FontIcon,
  HeaderBarTitled,
  Modal,
  OverlayModalViewRevamp,
  ProgressView,
  RowBar,
  Text
} from '@components';
import { ChooseCountryModalView } from '@components/commonComponents';
import { I18n, isAndroid, isIPhoneX } from '@lib';
import { logOut, setPhone, setProfile } from '@store/modules/auth/actions';
import { ModalPropsType } from '@lib/types';
import { setSearchFilter } from '@store/modules/search/actions';
import { setSystemLanguage, setUserProcessAndRole, showNotification } from '@store/modules/system/actions';
import { theme } from '@styles';
import ErrorDisplayComponent from '@modules/registration/components/ErrorDisplayComponent';
import InputBox from '@modules/registration/components/InputBox';
import StepperCountText from '@modules/registration/components/StepperCountText';
import { SystemState } from '@store/modules/system/types';

import {
  LoginEmailBold,
  LoginFBIcon,
  LoginGoogleIcon
} from '@assets';
import { countrySelectionData, getAPI, icons, responsiveScale } from '@constants';
import { SignInKeys } from '@constants/constants';
import {
  AsyncBoolean,
  IdentifyTypes,
  NotificationType,
  SocialMediaLoginVariants,
  UserProcess,
  UserRole
} from '@constants/enums';

interface PhoneValidationScreenProps {
  navigation: Record<string, Function>;
}

const styles: Record<string, object> = StyleSheet.create({
  phoneCodeText: {
    letterSpacing: 0.3,
    lineHeight: responsiveScale(20)
  },
  phoneCodeWrapper: {
    alignItems: 'center',
    backgroundColor: theme.colors.background_light_grey,
    borderRadius: responsiveScale(4),
    height: responsiveScale(50),
    justifyContent: 'center',
    marginRight: responsiveScale(16),
    paddingHorizontal: responsiveScale(15)
  },
  scrollContainer: {
    backgroundColor: theme.colors.light,
    flexGrow: 1,
    justifyContent: 'space-between',
    paddingHorizontal: responsiveScale(16),
    paddingTop: responsiveScale(20)
  },
  textWrapper: {
    borderRadius: responsiveScale(4),
    borderWidth: responsiveScale(1),
    width: responsiveScale(248)
  },
  phoneNumberText: {
    fontFamily: `${theme.fontFamily.figtree}-Regular`,
    fontSize: responsiveScale(16),
    letterSpacing: 0.3
  },
  progressRail: {
    backgroundColor: theme.colors.background_light_grey,
    height: responsiveScale(4)
  },
  acceptButtonWrapper: {
    borderRadius: responsiveScale(4),
    borderWidth: 0,
    height: responsiveScale(48),
    marginVertical: responsiveScale(8),
    width: '100%'
  },
  viewDivider: {
    backgroundColor: theme.colors.border_primary,
    flex: 1,
    height: responsiveScale(1)
  },
  otherSignUpWrapper: {
    alignItems: 'center',
    borderColor: theme.colors.button_secondary_100,
    borderRadius: responsiveScale(4),
    borderWidth: responsiveScale(1),
    flexDirection: 'row',
    height: responsiveScale(40),
    justifyContent: 'center',
    marginBottom: responsiveScale(10),
    width: '100%'
  }
});

const PhoneValidationScreen: React.FC<PhoneValidationScreenProps> = ({ navigation }) => {
  const [country, setCountry] = useState<Record<string, any>>(countrySelectionData.DE);
  const [errorText, setErrorText] = useState('');
  const [firebaseToken, setFirebaseToken] = useState('');
  const [isKeyboardActive, setIsKeyboardActive] = useState(false);
  const [isSocialMediaLogin, setIsSocialMediaLogin] = useState(false);
  const [phoneNumber, setPhoneNumber] = useState<string>('');
  const [phoneNumberFormatted, setPhoneNumberFormatted] = useState<string>('');
  const [socialMediaAccessTypeSelected, setSocialMediaAccessTypeSelected] = useState<string>('');
  const { systemLanguage } = useSelector((state: Record<string, SystemState>) => state.system);

  // const { RNTwitterSignIn } = NativeModules;
  GoogleSignin.configure({ webClientId: SignInKeys.google.WEB_CLIENT_ID });
  // RNTwitterSignIn.init(
  //   SignInKeys.twitter.TWITTER_CONSUMER_KEY, SignInKeys.twitter.TWITTER_CONSUMER_SECRET
  // );

  const [, isKeyboardVisible] = useKeyboardHideShow();
  const countrySelectionModalRef = useRef<ModalPropsType>();
  const dispatch = useDispatch();
  const footerAnimation = useBasicAnimation({
    doAnimation: isKeyboardVisible && isAndroid(),
    useNativeDriver: true
  });
  const profileNotFoundModalRef = useRef<ModalPropsType>();

  const firebaseAuthStatus = async (): Promise<void> => {
    if (auth().currentUser) {
      await auth().signOut();
    }
  };

  const handleSuccess = (): void => {
    dispatch(setPhone(country.countryCode.toString(), phoneNumber));
    firebaseAuthStatus();
    navigation.navigate('PrimaryAuth', {
      screen: 'PhoneOtpVerificationScreen',
      params: {
        formattedPhnNumber: phoneNumberFormatted
      }
    });
  };

  const [{ loading: validatingNumber }, triggerValidatePhone] = useAxiosPost(
    getAPI.PHONE_VALIDATION,
    {
      onSuccess: () => handleSuccess(),
      onError: () => setErrorText(I18n.t('screen.login.phn_number.err'))
    }
  );

  const isDisabled = !(phoneNumber.length >= country.phoneNumberMinLength)
    || errorText.length > 0
    || validatingNumber;

  const clearSavedDetails = async (): Promise<void> => {
    LoginManager.logOut();
    dispatch(logOut());
    await GoogleSignin.revokeAccess();
    await GoogleSignin.signOut();
  };

  useEffect(() => {
    clearSavedDetails();
  }, []);

  const keyboardDidShow = (): void => {
    setIsKeyboardActive(true);
  };

  const keyboardDidHide = (): void => {
    setIsKeyboardActive(false);
  };

  useEffect(() => {
    Keyboard.addListener('keyboardDidShow', keyboardDidShow);
    Keyboard.addListener('keyboardDidHide', keyboardDidHide);

    // cleanup function
    return () => {
      Keyboard.addListener('keyboardDidShow', keyboardDidShow).remove();
      Keyboard.addListener('keyboardDidHide', keyboardDidHide).remove();
    };
  }, [isKeyboardActive]);

  const goBack = (): void => {
    if (navigation.canGoBack()) {
      navigation.goBack();
    }
  };

  useBackHandler(goBack);

  useEffect(() => {
    setPhoneNumber('');
    setPhoneNumberFormatted('');
    dispatch(setSearchFilter(null));
  }, [country]);

  const onPressStudioOwnerLogin = (): void => {
    setIsSocialMediaLogin(false);
    navigation.navigate('StudioOwnerEmailValidationScreen');
  };

  const onPressNext = (): void => {
    Keyboard.dismiss();
    setIsSocialMediaLogin(false);
    triggerValidatePhone({
      data: {
        phone_code: country.countryCode.toString(),
        phone_number: phoneNumber,
        process: UserProcess.LOGIN
      }
    });
  };

  useEffect(() => {
    if (phoneNumber.length >= country.phoneNumberLength) Keyboard.dismiss();
  }, [phoneNumber]);

  const onPressEmailLogin = (): void => {
    setIsSocialMediaLogin(false);
    navigation.navigate('EmailValidationScreen');
  };

  const handleAppLanguageSuccess = (res: Record<string, Record<string, string>>): void => {
    const { data } = res;
    I18n.locale = data.app_language;
    dispatch(setSystemLanguage(data.app_language));
    storeData(ASYNC_STORE.CURRENT_LANGUAGE, data.app_language);
  };

  const [, setAppLanguage] = useAxiosPut(
    getAPI.GET_APP_LANGUAGE,
    {
      onError: () => dispatch(showNotification(I18n.t('general.something_went_wrong'), NotificationType.WARNING)),
      onSuccess: (res: Record<string, Record<string, string>>) => handleAppLanguageSuccess(res)
    }
  );

  const setLanguage = async (): Promise<void> => {
    setAppLanguage({
      data: {
        app_language: systemLanguage
      }
    });
  };

  const handleLoginResponse = async (res: any): Promise<void> => {
    setLanguage();
    await storeData(
      ASYNC_STORE.LOGIN_DETAILS.ACCESS_TOKEN,
      res.data.login_details.access_token
    );
    await storeData(
      ASYNC_STORE.LOGIN_DETAILS.REFRESH_TOKEN,
      res.data.login_details.refresh_token
    );

    const { role } = res.data?.basic_details;

    dispatch(setUserProcessAndRole(UserProcess.LOGIN, role));
    dispatch(setProfile(res, IdentifyTypes.LOGIN));
    if (role === UserRole.CUSTOMER) {
      OneSignal.setEmail(res?.data?.basic_details?.email);
      OneSignal.sendTags({
        is_customer: AsyncBoolean.TRUE,
        first_name: res.data.basic_details.first_name
      });
    }
  };

  const [
    { loading: validatingSocialLogin },
    triggerSignInWithSocialMedia
  ] = useAxiosGet(`${getAPI.SOCIAL_MEDIA_ACCESS}?social_media=${socialMediaAccessTypeSelected}`, {
    headers: { Authorization: `FirebaseToken ${firebaseToken}` },
    onError: (error) => {
      if (error?.response?.status === 401) {
        profileNotFoundModalRef?.current?.showModal();
      } else {
        dispatch(showNotification(I18n.t('general.something_went_wrong'), NotificationType.WARNING));
      }
    },
    onSuccess: async (res: any) => handleLoginResponse(res)
  });

  navigation.setOptions({
    header: () => (
      <HeaderBarTitled
        color="light"
        containerStyle={{ paddingRight: responsiveScale(10) }}
        headerRightComponent={(
          <StepperCountText
            currentCount={1}
            totalTaskCount={2}
          />
        )}
        leftIconComponent={(
          <FontIcon name={icons.ArrowLeft} size={24} color={theme.colors.button_secondary_100} />
        )}
        leftIconPress={() => goBack()}
        title={I18n.t('general.login')}
        titleColor="text_primary"
        titleFontFamily="figtree"
        titleSize={18}
        titleWeight="semiBold"
      />
    )
  });

  useEffect(() => {
    if (firebaseToken !== '' && isSocialMediaLogin) {
      triggerSignInWithSocialMedia();
    }
  }, [firebaseToken]);

  useEffect(() => {
    const subscriber = auth().onAuthStateChanged((userData: FirebaseAuthTypes.User | null) => {
      if (userData) {
        userData.getIdToken().then((tokenValue: string) => {
          setFirebaseToken(tokenValue);
        });
      }
    });
    return subscriber; // unsubscribe on unmount
  }, []);

  return (
    <>
      <RowBar
        style={styles.progressRail}
        withBottomGap={false}
      >
        <View
          style={[styles.progressRail, {
            backgroundColor: theme.colors.icon_light,
            width: '50%'
          }]}
        />
      </RowBar>
      <ProgressView
        isLoading={validatingSocialLogin}
      />
      <ScrollView
        bounces={false}
        contentContainerStyle={styles.scrollContainer}
        keyboardShouldPersistTaps="handled"
      >
        <View>
          <Text
            color="text_secondary"
            fontFamily="figtree"
            size={16}
            style={{
              paddingBottom: responsiveScale(8)
            }}
          >
            {I18n.t('screen.phone_number_signup_heading')}
          </Text>
          <View>
            <RowBar
              style={{
                paddingVertical: responsiveScale(16)
              }}
              withBottomGap={false}
            >
              <TouchableOpacity
                activeOpacity={0.7}
                onPress={() => {
                  countrySelectionModalRef?.current?.showModal();
                  setErrorText('');
                }}
                style={styles.phoneCodeWrapper}
              >
                <Text
                  color="text_primary"
                  fontFamily="figtree"
                  size={16}
                  style={styles.phoneCodeText}
                >
                  {`${typeof country.countryGoogleId === 'string' && country.countryGoogleId.toLocaleUpperCase()} ${country.countryCode}`}
                </Text>
              </TouchableOpacity>
              <InputBox
                keyboardType="numeric"
                maskDetails={{
                  textFormat: country.textFormat.toString()
                }}
                maxLength={typeof country.phoneNumberLength === 'string'
                  ? +country.phoneNumberLength
                  : country.phoneNumberLength}
                onChangeValue={(phone: string, phoneFormatted: string) => {
                  setPhoneNumber(phone);
                  setPhoneNumberFormatted(phoneFormatted);
                }}
                onFocus={() => setErrorText('')}
                placeholderTextColor="text_placeholder"
                selectionColor={theme.colors.text_placeholder}
                textColor="text_primary"
                textStyle={styles.phoneNumberText}
                textWrapperStyle={{
                  ...styles.textWrapper,
                  borderColor: theme.colors[isKeyboardVisible ? 'button_secondary_100' : 'border_primary']
                }}
                useMaskedInput
                value={phoneNumberFormatted}
              />
            </RowBar>
            {errorText.length > 0
              && (
                <View
                  style={{
                    paddingBottom: responsiveScale(10)
                  }}
                >
                  <ErrorDisplayComponent
                    errorText={errorText}
                  />
                </View>
              )}
            <Button
              color={isDisabled ? 'background_light_grey' : 'button_secondary_100'}
              disabled={isDisabled}
              fontFamily="figtree"
              fontWeight="semiBold"
              loading={validatingNumber}
              onPress={onPressNext}
              style={styles.acceptButtonWrapper}
              subColor={isDisabled ? 'text_disabled' : 'light'}
              textSize={16}
              title={I18n.t('general.login')}
            />
          </View>
        </View>
        <Animated.View
          style={{
            transform: [
              {
                translateY: footerAnimation.interpolate({
                  inputRange: [0, 1],
                  outputRange: [0, responsiveScale(300)]
                })
              }
            ]
          }}
        >
          <RowBar
            style={{
              paddingVertical: responsiveScale(16)
            }}
            withBottomGap={false}
          >
            <View style={styles.viewDivider} />
            <Text
              color="button_secondary_50"
              fontFamily="figtree"
              style={{
                paddingHorizontal: responsiveScale(10)
              }}
              size={14}
            >
              {I18n.t('screen.other_signin_option')}
            </Text>
            <View style={styles.viewDivider} />
          </RowBar>
          <View
            style={{
              paddingBottom: responsiveScale(isIPhoneX() ? 10 : 6),
              paddingTop: responsiveScale(16)
            }}
          >
            <TouchableOpacity
              style={styles.otherSignUpWrapper}
              onPress={onPressStudioOwnerLogin}
            >
              <FontIcon
                name={icons.Studio}
                size={20}
                color={theme.colors.primary_400}
                style={{
                  position: 'absolute',
                  left: responsiveScale(16)
                }}
              />
              <Text
                color="button_secondary_100"
                fontFamily="figtree"
                size={14}
                weight="semiBold"
              >
                {I18n.t('screen.login.phn_number.sign_in_as_studio_owner')}
              </Text>
            </TouchableOpacity>
            <TouchableOpacity
              style={styles.otherSignUpWrapper}
              onPress={onPressEmailLogin}
            >
              <View
                style={{
                  position: 'absolute',
                  left: responsiveScale(8)
                }}
              >
                <LoginEmailBold />
              </View>
              <Text
                color="button_secondary_100"
                fontFamily="figtree"
                size={14}
                weight="semiBold"
              >
                {I18n.t('screen.login.phn_number.sign_with_email')}
              </Text>
            </TouchableOpacity>
          </View>
        </Animated.View>
      </ScrollView>
      <Modal
        backDropColor={theme.colors.shadow_primary}
        onBackButtonPress={() => countrySelectionModalRef?.current?.hideModal()}
        onBackdropPress={() => countrySelectionModalRef?.current?.hideModal()}
        ref={countrySelectionModalRef}
        screen={(
          <ChooseCountryModalView
            displayPhnCode
            onPressContinue={(value: Record<string, string | number>) => {
              setCountry(value);
              countrySelectionModalRef?.current?.hideModal();
            }}
            value={country.countryGoogleId.toLowerCase()}
          />
        )}
        withRef
      />
      <Modal
        backDropColor={theme.colors.shadow_primary}
        onBackButtonPress={() => profileNotFoundModalRef?.current?.hideModal()}
        onBackdropPress={() => profileNotFoundModalRef?.current?.hideModal()}
        ref={profileNotFoundModalRef}
        screen={(
          <OverlayModalViewRevamp
            cancel={I18n.t('button.ok')}
            cancelHandle={() => profileNotFoundModalRef?.current?.hideModal()}
            subtitle={I18n.t('screen.social_media_login.account_not_found_desc')}
            subTitleTextStyle={{
              paddingRight: responsiveScale(50)
            }}
            cancelButtonStyle={{
              height: responsiveScale(40),
              width: responsiveScale(90)
            }}
            title={I18n.t('screen.social_media_login.account_not_found')}
            titleColor="dark_black_light"
            titleFont="secondary"
            titleSize={18}
            titleWeight="bold"
          />
        )}
        style={{
          justifyContent: 'flex-end'
        }}
        withRef
      />
    </>
  );
};

export default PhoneValidationScreen;

Simulator Screen Shot - iPhone 11 Pro - 2024-01-12 at 09 53 53

Otp screen:

import React, { useEffect, useState } from 'react';
import {
  Keyboard,
  ScrollView,
  StyleSheet,
  TouchableOpacity,
  View
} from 'react-native';
import { useDispatch, useSelector } from 'react-redux';
import auth, { FirebaseAuthTypes } from '@react-native-firebase/auth';

import { useAxiosPost, useAxiosPut } from '@hook';

import { ArtistState } from '@store/modules/artist/types';
import { ASYNC_STORE, storeData } from '@storage';
import { AuthState } from '@store/modules/auth/types';
import { Button, FontIcon, HeaderBarTitled, RowBar, Text } from '@components';
import { I18n } from '@lib';
import { setPhoneOtp } from '@store/modules/registration/actions';
import { setProfile } from '@store/modules/auth/actions';
import { setSystemLanguage, setUserProcessAndRole, showNotification } from '@store/modules/system/actions';
import { SystemState } from '@store/modules/system/types';
import { theme } from '@styles';
import ErrorDisplayComponent from '@modules/registration/components/ErrorDisplayComponent';
import OtpInputPhone from '@modules/registration/components/OtpInputPhone';
import StepperCountText from '@modules/registration/components/StepperCountText';

import { getAPI, icons, REGEX, responsiveScale } from '@constants';
import { IdentifyTypes, NotificationType, SystemLanguage, UserProcess } from '@constants/enums';

const styles: Record<string, object> = StyleSheet.create({
  infoLineWrapper: {
    flexDirection: 'row',
    justifyContent: 'center',
    paddingBottom: responsiveScale(20)
  },
  infoText: {
    letterSpacing: 0.03,
    lineHeight: responsiveScale(16),
    paddingLeft: responsiveScale(2)
  },
  inputContainer: {
    flex: 1,
    marginVertical: responsiveScale(10)
  },
  scrollContainer: {
    backgroundColor: theme.colors.light,
    flexGrow: 1,
    justifyContent: 'space-between',
    paddingHorizontal: responsiveScale(16),
    paddingTop: responsiveScale(20)
  },
  acceptButtonWrapper: {
    borderRadius: responsiveScale(4),
    borderWidth: 0,
    height: responsiveScale(48),
    marginVertical: responsiveScale(10),
    width: '100%'
  },
  progressRail: {
    backgroundColor: theme.colors.background_light_grey,
    height: responsiveScale(4)
  }
});

const secondsToRetry = 15;

const PhoneOtpVerificationScreen: React.FC<{
  navigation: Record<string, Function>;
  route: {
    params: {
      formattedPhnNumber?: string;
    };
  };
}> = ({ navigation, route = { params: { formattedPhnNumber: '' } } }) => {
  const { basicDetails } = useSelector((state: Record<string, ArtistState>) => state.artist);
  const { systemLanguage } = useSelector((state: Record<string, SystemState>) => state.system);
  const authState = useSelector((state: Record<string, AuthState>) => state.auth);
  const dispatch = useDispatch();
  const isConnected = useSelector((state: Record<string, SystemState>) => state.system.isConnected);

  const [
    confirmation, setConfirmation
  ] = useState<FirebaseAuthTypes.ConfirmationResult | null>(null);
  const [enableButton, setEnableButton] = useState<boolean>(false);
  const [firebaseToken, setFirebaseToken] = useState('');
  const [isActive, setIsActive] = useState(false);
  const [isInvalidOtp, setIsInvalidOtp] = useState(false);
  const [isValidating, setIsValidating] = useState(false);
  const [otp, setOtp] = useState('');
  const [seconds, setSeconds] = useState(secondsToRetry);

  const { formattedPhnNumber } = route.params;
  const { phoneCountryCode, phoneNumber } = basicDetails;
  const phoneNum = `${phoneCountryCode}${phoneNumber}`;
  const formattedPhoneNum = `${phoneCountryCode} ${formattedPhnNumber && formattedPhnNumber.length > 0
    ? formattedPhnNumber
    : phoneNumber}`;
  const isButtonDisabled = confirmation === null || isValidating;
  const selectedLanguage = systemLanguage === 'DE' ? SystemLanguage.GERMAN : SystemLanguage.ENGLISH;

  const noInternet = (): void => {
    if (!isConnected) {
      navigation.navigate('InternetNotify');
    }
  };

  const handleAppLanguageSuccess = (res: Record<string, Record<string, string>>): void => {
    const { data } = res;
    I18n.locale = data.app_language;
    dispatch(setSystemLanguage(data.app_language));
    storeData(ASYNC_STORE.CURRENT_LANGUAGE, data.app_language);
  };

  const [, setAppLanguage] = useAxiosPut(
    getAPI.GET_APP_LANGUAGE,
    {
      onError: () => dispatch(showNotification(I18n.t('general.something_went_wrong'), NotificationType.WARNING)),
      onSuccess: (res: Record<string, Record<string, string>>) => handleAppLanguageSuccess(res)
    }
  );

  const setLanguage = async (): Promise<void> => {
    setAppLanguage({
      data: {
        app_language: systemLanguage
      }
    });
  };

  const [, triggerSignInWithPhoneNumber] = useAxiosPost(getAPI.USER_LOGIN, {
    headers: { Authorization: `FirebaseToken ${firebaseToken}`, 'Accept-Language': selectedLanguage },
    onError: () => {
    },
    onSuccess: async (res: any) => {
      setLanguage();
      await storeData(
        ASYNC_STORE.LOGIN_DETAILS.ACCESS_TOKEN,
        res.data.login_details.access_token
      );
      await storeData(
        ASYNC_STORE.LOGIN_DETAILS.REFRESH_TOKEN,
        res.data.login_details.refresh_token
      );

      const { role } = res.data?.basic_details;

      dispatch(setUserProcessAndRole(UserProcess.LOGIN, role));
      dispatch(setProfile(res, IdentifyTypes.LOGIN));
    }
  });

  useEffect(() => {
    if (firebaseToken !== '' || firebaseToken.length > 0) {
      const data = {
        phone_code: authState.phoneCountryCode,
        phone_no: authState.phoneNumber,
        process: UserProcess.LOGIN
      };
      triggerSignInWithPhoneNumber({ data });
    }
  }, [firebaseToken]);

  const triggerFirebaseSignIn = (): void => {
    noInternet();
    auth()
      .signInWithPhoneNumber(phoneNum, true)
      .then((confirm) => {
        setConfirmation(confirm);
      });
  };

  useEffect(() => {
    if (confirmation === null) {
      triggerFirebaseSignIn();
    }
  });

  const phoneNumberSignIn = (): void => {
    if (!isConnected) navigation.navigate('InternetNotify');

    auth().signInWithPhoneNumber(phoneNum, true).then((confirm) => {
      setConfirmation(confirm);
    });
  };

  useEffect(() => {
    phoneNumberSignIn();

    const unsubscribe = auth().onAuthStateChanged((user: FirebaseAuthTypes.User | null) => {
      if (user) {
        user.getIdToken().then((tokenValue: string) => {
          setFirebaseToken(tokenValue);
        });
      }
    });

    return unsubscribe;
  }, []);

  useEffect(() => {
    let interval: any;
    if (seconds <= 0) {
      setIsActive(true);
    }
    if (!isActive) {
      interval = setInterval(() => {
        setSeconds((second) => second - 1);
      }, 1000);
    } else if (isActive && seconds !== 0) {
      clearInterval(interval);
    }
    return () => clearInterval(interval);
  }, [isActive, seconds]);

  const onResend = (): void => {
    phoneNumberSignIn();
    if (isActive) {
      dispatch(setPhoneOtp([]));
      // navigation.goBack();
    }
  };

  const validateOtp = (): void => {
    Keyboard.dismiss();
    if (isConnected) {
      setIsValidating(true);
      if (confirmation) {
        if (REGEX.NUMBER.test(otp)) {
          confirmation.confirm(otp).catch(() => {
            setIsInvalidOtp(true);
            setIsValidating(false);
          });
        } else {
          setIsInvalidOtp(true);
          setIsValidating(false);
        }
      }
    } else {
      navigation.navigate('InternetNotify');
      setIsValidating(false);
    }
  };

  return (
    <>
      <HeaderBarTitled
        color="light"
        containerStyle={{ paddingRight: responsiveScale(10) }}
        headerRightComponent={(
          <StepperCountText
            currentCount={2}
            totalTaskCount={2}
          />
        )}
        leftIconComponent={(
          <FontIcon name={icons.ArrowLeft} size={24} color={theme.colors.button_secondary_100} />
        )}
        leftIconPress={() => {
          navigation.goBack();
          dispatch(setPhoneOtp([]));
        }}
        title={I18n.t('screen.otp.header')}
        titleColor="text_primary"
        titleFontFamily="figtree"
        titleSize={18}
        titleWeight="semiBold"
      />
      <RowBar
        style={styles.progressRail}
        withBottomGap={false}
      >
        <View
          style={[styles.progressRail, {
            backgroundColor: theme.colors.icon_light,
            width: '100%'
          }]}
        />
      </RowBar>
      <ScrollView
        bounces={false}
        contentContainerStyle={styles.scrollContainer}
        keyboardShouldPersistTaps="handled"
      >
        <View>
          <View
            style={{
              paddingBottom: responsiveScale(8)
            }}
          >
            <Text
              color="text_secondary"
              fontFamily="figtree"
              size={16}
              style={{ lineHeight: responsiveScale(24) }}
            >
              {I18n.t('screen.otp.phone_header_1')}
              <Text
                color="text_primary"
                fontFamily="figtree"
                size={16}
                style={{ lineHeight: responsiveScale(24) }}
                weight="semiBold"
              >
                {` ${formattedPhoneNum} `}
              </Text>
              <Text
                color="text_secondary"
                fontFamily="figtree"
                size={16}
                style={{ lineHeight: responsiveScale(24) }}
              >
                {I18n.t('screen.otp.phone_header_2')}
              </Text>
            </Text>
          </View>
          <View
            style={styles.inputContainer}
          >
            <OtpInputPhone
              isError={isInvalidOtp}
              onCodechange={(value: Array<string>) => setEnableButton(value.length === 6)}
              onCodeComplete={(code: string) => {
                setOtp(code);
                Keyboard.dismiss();
              }}
              onFocus={() => {
                if (isInvalidOtp) dispatch(setPhoneOtp([])); setIsInvalidOtp(false);
              }}
            />
          </View>
          {
            isInvalidOtp
              ? (
                <View
                  style={{
                    paddingBottom: responsiveScale(10)
                  }}
                >
                  <ErrorDisplayComponent
                    errorText={I18n.t('screen.otp_invalid_otp')}
                  />
                </View>
              ) : null
          }
          <Button
            color={(!enableButton || isButtonDisabled) && !isValidating ? 'background_light_grey' : 'button_secondary_100'}
            disabled={!enableButton || isButtonDisabled}
            fontFamily="figtree"
            fontWeight="semiBold"
            loading={isValidating}
            onPress={() => validateOtp()}
            style={styles.acceptButtonWrapper}
            textSize={16}
            subColor={!enableButton || isButtonDisabled ? 'text_disabled' : 'light'}
            title={I18n.t('screen.otp.verifyButton')}
          />
        </View>
        <View
          style={styles.infoLineWrapper}
        >
          <Text
            color="button_secondary_50"
            fontFamily="figtree"
            size={14}
            style={styles.infoText}
          >
            {I18n.t('screen.otp.didnot_receive')}
          </Text>
          <TouchableOpacity
            disabled={!isActive}
            onPress={() => onResend()}
          >
            <Text
              color="button_secondary_100"
              fontFamily="figtree"
              size={14}
              style={[styles.infoText, {
                opacity: isActive ? 1 : 0.5
              }]}
              weight="semiBold"
            >
              {` ${I18n.t('screen.otp.resend')}`}
            </Text>
          </TouchableOpacity>
        </View>
      </ScrollView>
    </>
  );
};

export default PhoneOtpVerificationScreen;

Simulator Screen Shot - iPhone 11 Pro - 2024-01-12 at 09 54 26

Is anything I'm missing here. Please note there is no issue in Android.

Thanks.

akhilsanker avatar Jan 12 '24 03:01 akhilsanker

@mikehardy , Any updates on this Thanks

akhilsanker avatar Jan 15 '24 04:01 akhilsanker

@mikehardy , Any updates on this.

Thanks.

akhilsanker avatar Jan 16 '24 04:01 akhilsanker

With apologies, this is very very far from a minimal reproduction.

My initial response is that you have two cases where you try to do a phone OTP:


  const triggerFirebaseSignIn = (): void => {
    noInternet();
    auth()
      .signInWithPhoneNumber(phoneNum, true)
      .then((confirm) => {
        setConfirmation(confirm);
      });
  };

  useEffect(() => {
    if (confirmation === null) {
      triggerFirebaseSignIn();
    }
  });

  const phoneNumberSignIn = (): void => {
    if (!isConnected) navigation.navigate('InternetNotify');

    auth().signInWithPhoneNumber(phoneNum, true).then((confirm) => {
      setConfirmation(confirm);
    });
  };

And they are triggered by state changes in some cases. They are likely both being called, perhaps in response to some unexpected state change.

This is a commonly used flow (Phone OTP) and no one else is reporting this, while you have pretty complicated code that is causing a problem. My working hypothesis is that the library is working fine but you have an error in your code.

If you were to boil it down to a simple page that did nothing but take a phone number, and call the phone verification API in response to a button press next to the phone number, I bet you would see one and only one OTP SMS every time --> https://stackoverflow.com/help/minimal-reproducible-example

If you can do that - create a single screen with nothing but a phone number input box that calls the react-native-firebase API - and you reproduce multiple SMS then we have an issue here

mikehardy avatar Jan 30 '24 17:01 mikehardy

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[bot] avatar Feb 27 '24 18:02 github-actions[bot]