invertase / react-native-firebase

🔥 A well-tested feature-rich modular Firebase implementation for React Native. Supports both iOS & Android platforms for all Firebase services.
https://rnfirebase.io
Other
11.69k stars 2.21k forks source link

🔥 🐛 AdMob - Interstitial closes immediately on iOS #5093

Closed sasweb closed 3 years ago

sasweb commented 3 years ago

Issue

I am moving my production app (bare RN 0.63 app) to firebase. So I decided to move from expo admob to firebase admob as well. Unfortuantely, iOS interstitials are neither working on real iOS devices (testflight) nor on simulator with TestIds. No errors are thrown. The interstitial opens for 10ms and immediately closes again. This behavior can be see when logging the received events.

On Android everything works as expected. Looks to me like I am missing something but I double checked the docs like four times. All ids are correct as they worked before with expo admob. Also I am not quite sure if the firebase.json is used at all. Is this even supposed to work in bare RN projects? I couldn't find any other information regarding this.

I have moved the actual interstitial logic into a hook:

const useInterstitialAds = (showAds: boolean) => {
  const [loaded, setLoaded] = useState(false)
  const [isLoading, setIsLoading] = useState(false)

  // Keep in useState and recreate to reload interstitial properly
  const [interstitial, setInterstitial] = useState(
    InterstitialAd.createForAdRequest(adUnitId, {
      requestNonPersonalizedAdsOnly: true
    })
  )

  useEffect(() => {
    admob().setRequestConfiguration({
      maxAdContentRating: MaxAdContentRating.PG,
      tagForChildDirectedTreatment: true,
      tagForUnderAgeOfConsent: true
    })

    const unsubscribe = 
      interstitial.onAdEvent((type, error) => {
          // logs open and close events right after each other
          console.log(`Interstitial event received: ${type}`)
          if (error) log(error) // no errors are thrown
          if (type === AdEventType.LOADED) {
            setIsLoading(false)
            setLoaded(true)
          }
        })

    return () => {
      if (unsubscribe) unsubscribe()
    }
  }, [interstitial])

  useEffect(() => {
    if (someCondition) {
      setIsLoading(false)
      setLoaded(false)
      setInterstitial(
        InterstitialAd.createForAdRequest(adUnitId, {
          requestNonPersonalizedAdsOnly:true
        })
      )
    }

    if (someOtherCondition) {
      setIsLoading(true)
      interstitial.load()
    }
  }, [someDeps])

  const show = async () => {
    if (loaded) {
      interstitial.show()
    }
  }

  return { loaded, show }
}

Ids:

import { Platform } from 'react-native'
import { TestIds } from '@react-native-firebase/admob'

const AD_MOB_INTERSTITIAL_ANDROID = 'ca-app-pub-138738323662xxxx/2911yyyyyy'
const AD_MOB_INTERSTITIAL_IOS = 'ca-app-pub-138738323662xxxx/3078yyyyyy'
const AD_ID = Platform.OS === 'android' ? AD_MOB_INTERSTITIAL_ANDROID : AD_MOB_INTERSTITIAL_IOS

export const adUnitId = __DEV__ ? TestIds.INTERSTITIAL : AD_ID

I did update the GoogleService-Info.plist in iOS folder. It contains admob related keys as well:

<key>IS_ADS_ENABLED</key>
<true></true>
<key>ADMOB_APP_ID</key>
<string>ca-app-pub-138738323662xxxx~2129yyyyyy</string>

Any help is highly appreciated.


Project Files

Javascript

Click To Expand

#### `package.json`: ```json { "name": "app", "version": "1.0.0", "scripts": { "android": "npx react-native run-android", "devices": "xcrun simctl list devices", "ios": "npx react-native run-ios --simulator=\"iPhone 11\"", "ipad": "npx react-native run-ios --simulator=\"iPad Air (4th generation)\"", "iphone8": "npx react-native run-ios --simulator=\"iPhone 8\"", "iphone11": "npx react-native run-ios --simulator=\"iPhone 11\"", "lint": "eslint .", "start": "npx react-native start --reset-cache", "test": "jest", "test:i18n": "jest __tests__/i18n-test.ts", "test:types": "node_modules/typescript/bin/tsc -p ./tsconfig.json", "translate": "gulp" }, "dependencies": { "@invertase/react-native-apple-authentication": "^1.0.0", "@react-native-async-storage/async-storage": "^1.14.1", "@react-native-community/netinfo": "^5.9.7", "@react-native-firebase/admob": "^11.1.0", "@react-native-firebase/app": "^11.1.0", "@react-native-firebase/auth": "^11.1.0", "@react-native-firebase/firestore": "^11.1.0", "@react-native-google-signin/google-signin": "^6.0.0", "@sentry/react-native": "^1.9.0", "@types/i18n-js": "^3.0.3", "@types/lodash": "^4.14.167", "base-64": "^0.1.0", "crypto-random-string": "^3.3.0", "date-fns": "^2.16.1", "expo-document-picker": "^8.4.1", "expo-keep-awake": "^8.3.0", "i18n-js": "^3.8.0", "lodash": "^4.17.20", "lodash.chunk": "^4.2.0", "lodash.omit": "^4.5.0", "nanoid": "^3.1.20", "react": "16.13.1", "react-native": "0.63.3", "react-native-animatable": "^1.3.3", "react-native-device-info": "^7.0.2", "react-native-document-picker": "^3.3.0", "react-native-gesture-handler": "^1.8.0", "react-native-get-random-values": "^1.6.0", "react-native-iap": "^5.2.3", "react-native-image-picker": "^2.3.4", "react-native-image-resizer": "^1.3.0", "react-native-indicators": "^0.17.0", "react-native-localize": "^1.4.2", "react-native-modal": "^11.5.6", "react-native-safe-area-context": "^3.1.8", "react-native-screens": "^2.11.0", "react-native-share": "^4.0.2", "react-native-sound": "^0.11.0", "react-native-splash-screen": "^3.2.0", "react-native-svg": "^12.1.0", "react-native-unimodules": "^0.11.0", "react-navigation": "^4.0.10", "react-navigation-stack": "^1.10.3", "react-query": "^3.12.0", "slugify": "^1.4.5", "victory-native": "^34.3.0" }, "devDependencies": { "@babel/core": "^7.12.3", "@babel/plugin-transform-runtime": "^7.12.1", "@babel/runtime": "^7.12.1", "@react-native-community/eslint-config": "^1.1.0", "@testing-library/react-native": "^7.1.0", "@types/jest": "26.0.15", "@types/lodash.chunk": "^4.2.6", "@types/react": "16.9.53", "@types/react-native": "0.63.25", "@types/react-test-renderer": "16.9.3", "babel-jest": "^26.6.2", "babel-plugin-module-resolver": "^4.0.0", "babel-plugin-transform-remove-console": "^6.9.4", "eslint": "6.7.2", "eslint-plugin-react-hooks": "^4.2.0", "gulp": "^4.0.2", "gulp-json-transform": "^0.4.7", "gulp-prettier": "^3.0.0", "jest": "^26.6.2", "metro-react-native-babel-preset": "^0.59.0", "react-native-mock-render": "^0.1.9", "react-test-renderer": "17.0.1", "typescript": "^4.0.5" }, "jest": { "preset": "react-native", "moduleFileExtensions": [ "ts", "tsx", "js", "jsx", "json", "node" ], "transformIgnorePatterns": [ "node_modules/(?!(jest-)?react-native|react-clone-referenced-element|@react-native-community|expo(nent)?|@expo(nent)?/.*|react-navigation|@react-navigation/.*|@unimodules/.*|unimodules|sentry-expo|@sentry|native-base|@react-native-firebase/auth|@react-native-firebase/app|@react-native-firebase/firestore|@react-native-firebase/admob|react-native-sound)" ], "setupFilesAfterEnv": [ "/__mocks__/globalMock.js", "/__mocks__/react-native-localize.js" ] } } ``` #### `firebase.json` for react-native-firebase v6: ```json { "react-native": { "admob_android_app_id": "ca-app-pub-138738323662xxxx~3143yyyyyy", "admob_ios_app_id": "ca-app-pub-138738323662xxxx~2129yyyyyy" } } ```

iOS

Click To Expand

#### `ios/Podfile`: - [ ] I'm not using Pods - [x] I'm using Pods and my Podfile looks like: ```ruby require_relative '../node_modules/react-native/scripts/react_native_pods' require_relative '../node_modules/@react-native-community/cli-platform-ios/native_modules' require_relative '../node_modules/react-native-unimodules/cocoapods.rb' platform :ios, '10.0' target 'MyApp' do use_unimodules! config = use_native_modules! use_react_native!(:path => config["reactNativePath"]) target 'MyAppTests' 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!({ 'Flipper-Folly' => '2.3.0' }) post_install do |installer| flipper_post_install(installer) end end target 'MyApp-tvOS' do # Pods for MyApp-tvOS target 'MyApp-tvOSTests' do inherit! :search_paths # Pods for testing end end ``` #### `AppDelegate.m`: ```objc /** * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #import "AppDelegate.h" #import #import #import #import "RNSplashScreen.h" // RN unimodules (expo) #import #import #import #ifdef FB_SONARKIT_ENABLED #import #import #import #import #import #import 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 @interface AppDelegate () @property (nonatomic, strong) UMModuleRegistryAdapter *moduleRegistryAdapter; @end @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { if ([FIRApp defaultApp] == nil) { [FIRApp configure]; } #ifdef FB_SONARKIT_ENABLED InitializeFlipper(application); #endif self.moduleRegistryAdapter = [[UMModuleRegistryAdapter alloc] initWithModuleRegistryProvider:[[UMModuleRegistryProvider alloc] init]]; RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:launchOptions]; RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge moduleName:@"MyApp" 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]; [RNSplashScreen show]; return YES; } - (NSArray> *)extraModulesForBridge:(RCTBridge *)bridge { NSArray> *extraModules = [_moduleRegistryAdapter extraModulesForBridge:bridge]; // If you'd like to export some custom RCTBridgeModules that are not Expo modules, add them here! return extraModules; } - (NSURL *)sourceURLForBridge:(RCTBridge *)bridge { #if DEBUG return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil]; #else return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; #endif } @end ```


Android

Not relevant


Environment

Click To Expand

**`react-native info` output:** ``` System: OS: macOS 10.15.7 CPU: (8) x64 Intel(R) Core(TM) i5-8257U CPU @ 1.40GHz Memory: 235.31 MB / 8.00 GB Shell: 3.2.57 - /bin/bash Binaries: Node: 14.12.0 - ~/.nvm/versions/node/v14.12.0/bin/node Yarn: 1.17.3 - /usr/local/bin/yarn npm: 6.14.8 - ~/.nvm/versions/node/v14.12.0/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.0, DriverKit 19.0, macOS 10.15, tvOS 14.0, watchOS 7.0 Android SDK: API Levels: 26, 28, 29 Build Tools: 28.0.3, 29.0.2, 30.0.0, 30.0.2 System Images: android-28 | Intel x86 Atom_64, android-28 | Google APIs Intel x86 Atom, android-29 | Intel x86 Atom, android-29 | Intel x86 Atom_64, android-29 | Google APIs Intel x86 Atom, android-29 | Google APIs Intel x86 Atom_64, android-29 | Google Play Intel x86 Atom, android-29 | Google Play Intel x86 Atom_64 Android NDK: Not Found IDEs: Android Studio: 4.0 AI-193.6911.18.40.6626763 Xcode: 12.0.1/12A7300 - /usr/bin/xcodebuild Languages: Java: 1.8.0_222 - /usr/bin/javac 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.3 => 0.63.3 react-native-macos: Not Found npmGlobalPackages: *react-native*: Not Found ``` - **Platform that you're experiencing the issue on**: - [x] 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:** - `e.g. 11.1.0` - **`Firebase` module(s) you're using that has the issue:** - `@react-native-firebase/admob` - **Are you using `TypeScript`?** - `YES 4.0.5`

mikehardy commented 3 years ago

Hi there! Apologies for the delay - you are on pretty up to date versions for sure, that is likely not the issue. Is there a way you could condense this to a single App.js reproduction? I have a test reproduction script that generates a working app and puts the test admob ids in there so with that I should be able to see what you're seeing: https://github.com/mikehardy/rnfbdemo/blob/master/make-demo.sh

sasweb commented 3 years ago

Thank you for the response.

I have followed the instructions in the demo readme and added a basic admob interstitial setup. Basically it's just an Ads.js which adds the already described feature.

Ads.js

import React, {useEffect, useState} from 'react';
import {Pressable, Text, View} from 'react-native';
import admob, {
  InterstitialAd,
  MaxAdContentRating,
  TestIds,
} from '@react-native-firebase/admob';

const Ads = () => {
  const [loaded, setLoaded] = useState(false);
  const [isLoading, setIsLoading] = useState(false);

  const [interstitial, setInterstitial] = useState(
    InterstitialAd.createForAdRequest(TestIds.INTERSTITIAL, {
      requestNonPersonalizedAdsOnly: true,
    }),
  );

  useEffect(() => {
    admob().setRequestConfiguration({
      maxAdContentRating: MaxAdContentRating.PG,
      tagForChildDirectedTreatment: true,
      tagForUnderAgeOfConsent: true,
    });
  }, []);

  useEffect(() => {
    const unsubscribe = interstitial.onAdEvent((type, error) => {
      console.log('listener fired', type);

      setIsLoading(false);

      if (error) console.log(error);

      if (type === 'loaded') {
        setLoaded(true);
      }
    });

    return () => {
      if (unsubscribe) unsubscribe();
    };
  }, [interstitial]);

  const load = () => {
    setIsLoading(true);
    setInterstitial(
      InterstitialAd.createForAdRequest(TestIds.INTERSTITIAL, {
        requestNonPersonalizedAdsOnly: true,
      }),
    );
    interstitial.load();
  };

  const show = async () => {
    interstitial.show();
  };

  return isLoading ? (
    <Text>Loading...</Text>
  ) : (
    <View>
      <Pressable onPress={load}>
        <Text>(Re-)Load Interstitial</Text>
      </Pressable>
      {loaded && (
        <Pressable onPress={show}>
          <Text>Show Interstitial</Text>
        </Pressable>
      )}
    </View>
  );
};

export default Ads;

With this setup the interstitial is stuck in the loading process as soon as load is executed. The interstitial never gets loaded. The event listener doesn't fire with any event at all.

See all the code in the demo repo: https://github.com/pawelsas/rnfb

mikehardy commented 3 years ago

@pawelsas thank you for that! I'll give reproduction a go on my end

sasweb commented 3 years ago

Hey @mikehardy! Any updates here? I still haven't found a solution - neither in my project nor in the demo :/

mikehardy commented 3 years ago

I think this will actually resolve to the general statement "InterstitialAd is deprecated and should not be used" / #5127 (workaround by pinning admob version in build.gradle maybe?) - still planning on looking at this and if the build.gradle workaround works I'll issue a patch release

sasweb commented 3 years ago

I guess the build.gradle workaround won't work here since it's an iOS only issue.

mikehardy commented 3 years ago

I realized that after I posted it - sorry - I have cloned your repo and it's in the process of building, hopefully I have a local crash stack trace shortly but right now I cannot actually get it to load an interstitial at all, I never receive any events in your reproduction code after requesting the ad load :thinking:

I'm experimenting with different admob units to see if that affects it but so far I do not have a working reproduction yet

mikehardy commented 3 years ago

Okay, so I just clobbered your javascript with the stuff from our docs, and it works. Coding error somehow?

App.js contents - https://rnfirebase.io/admob/displaying-ads#interstitial-ads

import React, {useEffect, useState} from 'react';
import {Button} from 'react-native';
import {
  InterstitialAd,
  AdEventType,
  TestIds,
} from '@react-native-firebase/admob';

const adUnitId = __DEV__
  ? TestIds.INTERSTITIAL
  : 'ca-app-pub-xxxxxxxxxxxxx/yyyyyyyyyyyyyy';

const interstitial = InterstitialAd.createForAdRequest(adUnitId, {
  requestNonPersonalizedAdsOnly: true,
  keywords: ['fashion', 'clothing'],
});

export default function App() {
  const [loaded, setLoaded] = useState(false);

  useEffect(() => {
    const eventListener = interstitial.onAdEvent(type => {
      if (type === AdEventType.LOADED) {
        setLoaded(true);
      }
    });

    // Start loading the interstitial straight away
    interstitial.load();

    // Unsubscribe from events on unmount
    return () => {
      eventListener();
    };
  }, []);

  // No advert ready to show yet
  if (!loaded) {
    return null;
  }

  return (
    <Button
      title="Show Interstitial"
      onPress={() => {
        interstitial.show();
      }}
    />
  );
}
sasweb commented 3 years ago

@mikehardy you're right, apparently it's not valid (and needed) to have the interstitial instance created with createForAdRequest inside useState. That solved the issue with the demo. However, my actual code is still not working and I am experiencing still the same error where the interstitial is opened and immediately closed.

After some further research I realised that this is happening because my interstitial logic is inside a global store. When I move all code (loading and showing interstitial) to one file, it works on iOS as well. My setup looks like the following (simplified).

Context definition somewhere in the code:

const AdContext = React.createContext({ ... });

On root level:

// Some code for load and show interstitial

<AdContext.Provider value={{
  loadInterstitial,
  showInterstitial
}}>
  <ScreenA />
  <ScreenB />
</AdContext.Provider>
...

ScreenA is supposed to preload the interstitial:

const { loadInterstitial } = useContext(AdContext)
...
const someEvent = () => {
  loadInterstitial()
}

ScreenB is supposed to show the preloaded interstitial:

const { showInterstitial } = useContext(AdContext)
...
const someEvent = () => {
  showInterstitial()
}

What I don't understand:

Thanks.

mikehardy commented 3 years ago

Those are great follow-ons - and I'm glad it is...sort of working for you now

You have unfortunately (for me since I like just knowing all the things, and you since I can't answer) run completely out my depth on this one. I don't use admob myself so I just haven't gone deep on it

Happy to re-open in case someone else knows, but this might be better on stackoverflow or similar

RayosElDev commented 3 years ago

Hello, Thenk it is a good way to request interstitital? Create a const every time with events like that:

  showInterstitialAd = () => {
        // Create a new instance
        const interstitialAd = InterstitialAd.createForAdRequest(TestIds.INTERSTITIAL);

        // Add event handlers
        interstitialAd.onAdEvent((type, error) => {
            ToastAndroid.show('New event: '+ type + " err "+ error, ToastAndroid.SHORT);
            if (type === AdEventType.LOADED) {
                interstitialAd.show();
            }
        });

        // Load a new advert
        interstitialAd.load();
    }
mikehardy commented 3 years ago

With apologies Admob no longer present in v12 as it was removed in upstream SDKs so there are no APIs to wrap any longer - please stay with v11.5 until migration to another project (either from this same code in a new repo - planned, or a different project)