From 18774bbb408d39ff227af2a2c6b1d26653c17a46 Mon Sep 17 00:00:00 2001 From: Elliot Hesp Date: Thu, 25 May 2017 17:00:53 +0100 Subject: [PATCH 01/14] [admob][android] WIP Basic view --- android/build.gradle | 1 + .../io/invertase/firebase/RNFirebasePackage.java | 6 +++++- index.js | 2 ++ lib/modules/admob/Banner.js | 11 +++++++++++ lib/modules/admob/index.js | 1 + tests/src/main.js | 13 ++++++++----- 6 files changed, 28 insertions(+), 6 deletions(-) create mode 100644 lib/modules/admob/Banner.js create mode 100644 lib/modules/admob/index.js diff --git a/android/build.gradle b/android/build.gradle index 623cd2aa..bc7f78c5 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -59,4 +59,5 @@ dependencies { compile "com.google.firebase:firebase-crash:$firebaseVersion" compile "com.google.firebase:firebase-config:$firebaseVersion" compile "com.google.firebase:firebase-perf:$firebaseVersion" + compile "com.google.firebase:firebase-ads:$firebaseVersion" } diff --git a/android/src/main/java/io/invertase/firebase/RNFirebasePackage.java b/android/src/main/java/io/invertase/firebase/RNFirebasePackage.java index fb5ac6e1..90cf8887 100644 --- a/android/src/main/java/io/invertase/firebase/RNFirebasePackage.java +++ b/android/src/main/java/io/invertase/firebase/RNFirebasePackage.java @@ -9,10 +9,12 @@ import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.uimanager.UIManagerModule; import com.facebook.react.uimanager.ViewManager; +import java.util.Arrays; import java.util.List; import java.util.ArrayList; import java.util.Collections; +import io.invertase.firebase.admob.RNFirebaseAdMob; import io.invertase.firebase.auth.RNFirebaseAuth; import io.invertase.firebase.config.RNFirebaseRemoteConfig; import io.invertase.firebase.storage.RNFirebaseStorage; @@ -66,6 +68,8 @@ public class RNFirebasePackage implements ReactPackage { */ @Override public List createViewManagers(ReactApplicationContext reactContext) { - return Collections.emptyList(); + return Arrays.asList( + new RNFirebaseAdMob() + ); } } diff --git a/index.js b/index.js index b9f9de3c..ffcd3583 100644 --- a/index.js +++ b/index.js @@ -1,3 +1,5 @@ import Firebase from './lib/firebase'; +export const AdMob = require('./lib/modules/admob'); + export default Firebase; diff --git a/lib/modules/admob/Banner.js b/lib/modules/admob/Banner.js new file mode 100644 index 00000000..99aea05d --- /dev/null +++ b/lib/modules/admob/Banner.js @@ -0,0 +1,11 @@ +import { PropTypes } from 'react'; +import { requireNativeComponent, View } from 'react-native'; + +const Banner = { + name: 'Banner', + propTypes: { + src: PropTypes.string, + }, +}; + +module.exports = requireNativeComponent('RNFirebaseAdMobBanner', Banner); diff --git a/lib/modules/admob/index.js b/lib/modules/admob/index.js new file mode 100644 index 00000000..b9591bce --- /dev/null +++ b/lib/modules/admob/index.js @@ -0,0 +1 @@ +export const Banner = require('./Banner'); diff --git a/tests/src/main.js b/tests/src/main.js index 35086ae6..9d4a06ee 100644 --- a/tests/src/main.js +++ b/tests/src/main.js @@ -1,6 +1,8 @@ import React, { Component } from 'react'; import { Provider } from 'react-redux'; +import { Banner } from './../firebase/modules/admob'; + import CoreContainer from './containers/CoreContainer'; import setupStore from './store/setup'; import { setupSuites } from './tests/index'; @@ -50,11 +52,12 @@ function bootstrap() { return null; } - return ( - - - - ); + return ; + // return ( + // + // + // + // ); } } From b77ed93d2d8c236165a9615777701387bf7ba518 Mon Sep 17 00:00:00 2001 From: Elliot Hesp Date: Fri, 26 May 2017 15:14:19 +0100 Subject: [PATCH 02/14] [android] Add internet permission as default Mainifest requirement --- android/src/main/AndroidManifest.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml index 45a2feb8..6ed6b311 100644 --- a/android/src/main/AndroidManifest.xml +++ b/android/src/main/AndroidManifest.xml @@ -1,3 +1,5 @@ + + From b1e9561633129d8c7e149c4098d1cd54364ff017 Mon Sep 17 00:00:00 2001 From: Elliot Hesp Date: Fri, 26 May 2017 15:18:09 +0100 Subject: [PATCH 03/14] [admob][android] Add custom Banner component wrapper --- lib/modules/admob/Banner.js | 63 ++++++++++++++++++++++++++++++++----- 1 file changed, 55 insertions(+), 8 deletions(-) diff --git a/lib/modules/admob/Banner.js b/lib/modules/admob/Banner.js index 99aea05d..9ac725ad 100644 --- a/lib/modules/admob/Banner.js +++ b/lib/modules/admob/Banner.js @@ -1,11 +1,58 @@ -import { PropTypes } from 'react'; +import React, { PropTypes } from 'react'; import { requireNativeComponent, View } from 'react-native'; -const Banner = { - name: 'Banner', - propTypes: { - src: PropTypes.string, - }, -}; +class Banner extends React.Component { -module.exports = requireNativeComponent('RNFirebaseAdMobBanner', Banner); + static propTypes = { + ...View.propTypes, + size: PropTypes.string, + unitId: PropTypes.string, + onAdLoaded: PropTypes.func, + }; + + static defaultProps = { + size: 'SMART_BANNER', + unitId: 'ca-app-pub-3940256099942544/6300978111', // Testing + }; + + constructor() { + super(); + this.state = { + width: 0, + height: 0, + }; + } + + onBannerEvent = ({ nativeEvent }) => { + if (this.props[nativeEvent.type]) { + if (nativeEvent.type === 'onAdFailedToLoad') { + const error = new Error(nativeEvent.payload.message); + error.code = nativeEvent.payload.code; + this.props[nativeEvent.type](error); + } else { + this.props[nativeEvent.type](nativeEvent.payload || {}); + } + } + + if (nativeEvent.type === 'onSizeChange') this.updateSize(nativeEvent.payload); + }; + + updateSize = ({ width, height }) => { + this.setState({ width, height }); + }; + + render() { + return ( + + ); + } + +} + +const RNFirebaseAdMobBanner = requireNativeComponent('RNFirebaseAdMobBanner', Banner); + +export default Banner; From dd1262ca86a8eb64cde32d4de2fff982c8790f15 Mon Sep 17 00:00:00 2001 From: Elliot Hesp Date: Fri, 26 May 2017 15:18:57 +0100 Subject: [PATCH 04/14] [admob][android] Export interstitial JS module --- lib/modules/admob/AdRequest.js | 22 ++++++++++++++++++++++ lib/modules/admob/Interstitial.js | 29 +++++++++++++++++++++++++++++ lib/modules/admob/index.js | 21 ++++++++++++++++++++- 3 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 lib/modules/admob/AdRequest.js create mode 100644 lib/modules/admob/Interstitial.js diff --git a/lib/modules/admob/AdRequest.js b/lib/modules/admob/AdRequest.js new file mode 100644 index 00000000..217db457 --- /dev/null +++ b/lib/modules/admob/AdRequest.js @@ -0,0 +1,22 @@ +export default class AdRequest { + + constructor() { + this._props = { + keywords: [], + }; + } + + build() { + return this._props; + } + + addTestDevice() { + this._props.testDevice = true; + return this; + } + + addKeyword(word: string) { + this._props.keywords.push(word); + return this; + } +} diff --git a/lib/modules/admob/Interstitial.js b/lib/modules/admob/Interstitial.js new file mode 100644 index 00000000..84f03853 --- /dev/null +++ b/lib/modules/admob/Interstitial.js @@ -0,0 +1,29 @@ +import { NativeModules } from 'react-native'; + +const FirebaseAdMob = NativeModules.RNFirebaseAdmob; + +export default class Interstitial { + + constructor(admob: Object, eventListener: Object, adunit: string) { + this.admob = admob; + this.adUnit = adunit; + + eventListener.addListener('interstitial_event', this._onInterstitialEvent.bind(this)); + } + + _onInterstitialEvent(event) { + + } + + loadAd(request: AdRequest) { + return FirebaseAdMob.interstitialLoadAd(this.adUnit, request); + } + + isLoaded() { + return true; + } + + show() { + return FirebaseAdMob.interstitialShowAd(this.adUnit); + } +} diff --git a/lib/modules/admob/index.js b/lib/modules/admob/index.js index b9591bce..cb9f81cd 100644 --- a/lib/modules/admob/index.js +++ b/lib/modules/admob/index.js @@ -1 +1,20 @@ -export const Banner = require('./Banner'); +import Interstitial from './Interstitial'; +import AdRequest from './AdRequest'; +import Banner from './Banner'; +import Base from './../base'; + +const FirebaseAdMob = NativeModules.RNFirebaseAdMob; +const FirebaseAdMobEvt = new NativeEventEmitter(FirebaseAdMob); + +export default class Admob extends Base { + + interstitial(adUnit: string) { + return new Interstitial(this, FirebaseAdMobEvt, adUnit); + } + +} + +export const statics = { + Banner, + AdRequest, +}; From 0e47c57317c63cb361c36f06eead1435d4292ab6 Mon Sep 17 00:00:00 2001 From: Elliot Hesp Date: Fri, 26 May 2017 15:19:48 +0100 Subject: [PATCH 05/14] [admob][android] Add admob module to base firebase object --- .../firebase/admob/RNFirebaseAdMob.java | 97 ++++++ .../firebase/admob/RNFirebaseAdMobBanner.java | 303 ++++++++++++++++++ .../admob/RNFirebaseAdmobInterstitial.java | 47 +++ lib/firebase.js | 4 + 4 files changed, 451 insertions(+) create mode 100644 android/src/main/java/io/invertase/firebase/admob/RNFirebaseAdMob.java create mode 100644 android/src/main/java/io/invertase/firebase/admob/RNFirebaseAdMobBanner.java create mode 100644 android/src/main/java/io/invertase/firebase/admob/RNFirebaseAdmobInterstitial.java diff --git a/android/src/main/java/io/invertase/firebase/admob/RNFirebaseAdMob.java b/android/src/main/java/io/invertase/firebase/admob/RNFirebaseAdMob.java new file mode 100644 index 00000000..899dc017 --- /dev/null +++ b/android/src/main/java/io/invertase/firebase/admob/RNFirebaseAdMob.java @@ -0,0 +1,97 @@ +package io.invertase.firebase.admob; + + +import android.app.Activity; +import android.util.Log; + +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.Promise; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContextBaseJavaModule; +import com.facebook.react.bridge.ReactMethod; +import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.bridge.WritableArray; +import com.google.android.gms.ads.AdRequest; +import com.google.firebase.database.ServerValue; +import com.google.firebase.perf.FirebasePerformance; +import com.google.firebase.perf.metrics.Trace; +import com.google.firebase.remoteconfig.FirebaseRemoteConfig; + +import io.invertase.firebase.Utils; +import io.invertase.firebase.admob.RNFirebaseAdmobInterstitial; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public class RNFirebaseAdMob extends ReactContextBaseJavaModule { + + private static final String TAG = "RNFirebaseAdmob"; + + public ReactApplicationContext getContext() { + return getReactApplicationContext(); + } + + public Activity getActivity() { + return getCurrentActivity(); + } + + private ReactApplicationContext context; + private HashMap interstitials = new HashMap<>(); + + public RNFirebaseAdMob(ReactApplicationContext reactContext) { + super(reactContext); + context = reactContext; + Log.d(TAG, "New instance"); + } + + @Override + public String getName() { + return TAG; + } + + @ReactMethod + public void interstitialLoadAd(String adUnit, ReadableMap request) { + RNFirebaseAdmobInterstitial interstitial = getOrCreateInterstitial(adUnit); + AdRequest.Builder requestBuilder = new AdRequest.Builder(); + + if (request.hasKey("testDevice")) { + requestBuilder.addTestDevice(AdRequest.DEVICE_ID_EMULATOR); + } + + ReadableArray keywords = request.getArray("keywords"); + List keywordsList = Utils.recursivelyDeconstructReadableArray(keywords); + + for (Object word : keywordsList) { + requestBuilder.addKeyword((String) word); + } + + interstitial.loadAd(requestBuilder.build()); + } + + @ReactMethod + public void interstitialShowAd(String adUnit) { + RNFirebaseAdmobInterstitial interstitial = getOrCreateInterstitial(adUnit); + interstitial.show(); + } + + private RNFirebaseAdmobInterstitial getOrCreateInterstitial(String adUnit) { + if (interstitials.containsKey(adUnit)) { + return interstitials.get(adUnit); + } + RNFirebaseAdmobInterstitial interstitial = new RNFirebaseAdmobInterstitial(adUnit, this); + interstitials.put(adUnit, interstitial); + return interstitial; + } + + + + @Override + public Map getConstants() { + final Map constants = new HashMap<>(); + constants.put("DEVICE_ID_EMULATOR", AdRequest.DEVICE_ID_EMULATOR); + return constants; + } +} diff --git a/android/src/main/java/io/invertase/firebase/admob/RNFirebaseAdMobBanner.java b/android/src/main/java/io/invertase/firebase/admob/RNFirebaseAdMobBanner.java new file mode 100644 index 00000000..274fd62f --- /dev/null +++ b/android/src/main/java/io/invertase/firebase/admob/RNFirebaseAdMobBanner.java @@ -0,0 +1,303 @@ +package io.invertase.firebase.admob; + +import android.support.annotation.Nullable; +import android.util.Log; +import android.view.View; + +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.common.MapBuilder; +import com.facebook.react.uimanager.PixelUtil; +import com.facebook.react.uimanager.SimpleViewManager; +import com.facebook.react.uimanager.ThemedReactContext; +import com.facebook.react.uimanager.annotations.ReactProp; +import com.facebook.react.uimanager.events.RCTEventEmitter; +import com.facebook.react.views.view.ReactViewGroup; +import com.google.android.gms.ads.AdListener; +import com.google.android.gms.ads.AdRequest; +import com.google.android.gms.ads.AdSize; +import com.google.android.gms.ads.AdView; +import com.google.android.gms.ads.MobileAds; + +import java.util.Map; + + +public class RNFirebaseAdMobBanner extends SimpleViewManager implements View.OnLayoutChangeListener { + + public static final String REACT_CLASS = "RNFirebaseAdMobBanner"; + public static final String BANNER_EVENT = "bannerEvent"; + + public enum Events { + EVENT_AD_SIZE_CHANGE("onSizeChange"), + EVENT_AD_LOADED("onAdLoaded"), + EVENT_AD_FAILED_TO_LOAD("onAdFailedToLoad"), + EVENT_AD_OPENED("onAdOpened"), + EVENT_AD_CLOSED("onAdClosed"), + EVENT_AD_LEFT_APPLICATION("onAdLeftApplication"); + + private final String event; + + Events(final String name) { + event = name; + } + + @Override + public String toString() { + return event; + } + } + + private ThemedReactContext context; + private ReactViewGroup viewGroup; + private RCTEventEmitter emitter; + private String size; + + @Override + public String getName() { + return REACT_CLASS; + } + + /** + * Create & return view instance + * @param themedReactContext + * @return + */ + @Override + public ReactViewGroup createViewInstance(ThemedReactContext themedReactContext) { + context = themedReactContext; + viewGroup = new ReactViewGroup(themedReactContext); + emitter = themedReactContext.getJSModule(RCTEventEmitter.class); + + attachAdViewToViewGroup(); + + return viewGroup; + } + + /** + * Declare custom events + * @return + */ + @Override + public Map getExportedCustomDirectEventTypeConstants() { + MapBuilder.Builder builder = MapBuilder.builder(); + builder.put(BANNER_EVENT, MapBuilder.of("registrationName", BANNER_EVENT)); + return builder.build(); + } + + /** + * If the React View changes, reset the Ad size + * @param view + * @param left + * @param top + * @param right + * @param bottom + * @param oldLeft + * @param oldTop + * @param oldRight + * @param oldBottom + */ + @Override + public void onLayoutChange(View view, final int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) { + Log.d("Ads", "onLayoutChange"); + // If the view has changed at all, recalculate what banner we need + if (left != oldLeft || right != oldRight || top != oldTop || bottom != oldBottom) { + setSize(viewGroup, null); + } + } + + /** + * Handle unitId prop + * @param view + * @param unitId + */ + @ReactProp(name = "unitId") + public void setUnitId(final ReactViewGroup view, final String unitId) { + Log.d("Ads", "Prop unitId something " + unitId); + + AdView adViewView = (AdView) view.getChildAt(0); + adViewView.setAdUnitId(unitId); + requestAd(); + } + + /** + * Handle size prop + * @param view + * @param value + */ + @ReactProp(name = "size") + public void setSize(final ReactViewGroup view, final @Nullable String value) { + if (value != null) { + size = value; + } + + AdSize adSize = propToAdSize(size.toUpperCase()); + Log.d("Ads", "Prop size something " + adSize.toString()); + + AdView adViewView = (AdView) view.getChildAt(0); + adViewView.setAdSize(adSize); + + // Send the width & height back to the JS + int width; + int height; + WritableMap payload = Arguments.createMap(); + + if (adSize == AdSize.SMART_BANNER) { + width = (int) PixelUtil.toDIPFromPixel(adSize.getWidthInPixels(context)); + height = (int) PixelUtil.toDIPFromPixel(adSize.getHeightInPixels(context)); + } else { + width = adSize.getWidth(); + height = adSize.getHeight(); + } + + payload.putDouble("width", width); + payload.putDouble("height", height); + + sendEvent(Events.EVENT_AD_SIZE_CHANGE.toString(), payload); + requestAd(); + } + + + /** + * Creates a new instance of the AdView and attaches it to the + * current ReactViewGroup + */ + void attachAdViewToViewGroup() { + removeAdFromViewGroup(); + + final AdView adView = new AdView(context); + viewGroup.addView(adView); + setAdListener(); + } + + /** + * Removes the AdView from the ViewGroup + */ + void removeAdFromViewGroup() { + AdView adView = (AdView) viewGroup.getChildAt(0); + viewGroup.removeAllViews(); + + if (adView != null) { + adView.destroy(); + } + } + + /** + * Loads a new ad into a viewGroup + */ + void requestAd() { + AdView adView = (AdView) viewGroup.getChildAt(0); + + if (adView.getAdSize() == null || adView.getAdUnitId() == null) { + return; + } + + AdRequest.Builder adRequestBuilder = new AdRequest.Builder(); + adRequestBuilder.addTestDevice(AdRequest.DEVICE_ID_EMULATOR); + AdRequest adRequest = adRequestBuilder.build(); + adView.loadAd(adRequest); + } + + /** + * Listen to Ad events + */ + void setAdListener() { + final AdView adView = (AdView) viewGroup.getChildAt(0); + + adView.setAdListener(new AdListener() { + @Override + public void onAdLoaded() { + int left = adView.getLeft(); + int top = adView.getTop(); + + int width = adView.getAdSize().getWidthInPixels(context); + int height = adView.getAdSize().getHeightInPixels(context); + + adView.measure(width, height); + adView.layout(left, top, left + width, top + height); + + sendEvent(Events.EVENT_AD_LOADED.toString(), null); + } + + @Override + public void onAdFailedToLoad(int errorCode) { + WritableMap payload = Arguments.createMap(); + + // TODO Error code converter + switch (errorCode) { + case AdRequest.ERROR_CODE_INTERNAL_ERROR: + payload.putString("code", "admob/error-code-internal-error"); + payload.putString("message", "Something happened internally; for instance, an invalid response was received from the ad server."); + break; + case AdRequest.ERROR_CODE_INVALID_REQUEST: + payload.putString("code", "admob/error-code-invalid-request"); + payload.putString("message", "The ad request was invalid; for instance, the ad unit ID was incorrect."); + break; + case AdRequest.ERROR_CODE_NETWORK_ERROR: + payload.putString("code", "admob/error-code-network-error"); + payload.putString("message", "The ad request was unsuccessful due to network connectivity."); + break; + case AdRequest.ERROR_CODE_NO_FILL: + payload.putString("code", "admob/error-code-no-fill"); + payload.putString("message", "The ad request was successful, but no ad was returned due to lack of ad inventory."); + break; + } + + sendEvent(Events.EVENT_AD_FAILED_TO_LOAD.toString(), payload); + } + + @Override + public void onAdOpened() { + sendEvent(Events.EVENT_AD_OPENED.toString(), null); + } + + @Override + public void onAdClosed() { + sendEvent(Events.EVENT_AD_CLOSED.toString(), null); + } + + @Override + public void onAdLeftApplication() { + sendEvent(Events.EVENT_AD_LEFT_APPLICATION.toString(), null); + } + }); + } + + /** + * Sends an event back to the JS component to handle + * @param type + * @param payload + */ + void sendEvent(String type, final @Nullable WritableMap payload) { + WritableMap event = Arguments.createMap(); + event.putString("type", type); + + if (payload != null) { + event.putMap("payload", payload); + } + + emitter.receiveEvent(viewGroup.getId(), BANNER_EVENT, event); + } + + /** + * Map the size prop to the AdSize + * @param prop + * @return + */ + AdSize propToAdSize(String prop) { + switch (prop) { + default: + case "BANNER": + return AdSize.BANNER; + case "LARGE_BANNER": + return AdSize.LARGE_BANNER; + case "MEDIUM_RECTANGLE": + return AdSize.MEDIUM_RECTANGLE; + case "FULL_BANNER": + return AdSize.FULL_BANNER; + case "LEADERBOARD": + return AdSize.LEADERBOARD; + case "SMART_BANNER": + return AdSize.SMART_BANNER; + } + } +} diff --git a/android/src/main/java/io/invertase/firebase/admob/RNFirebaseAdmobInterstitial.java b/android/src/main/java/io/invertase/firebase/admob/RNFirebaseAdmobInterstitial.java new file mode 100644 index 00000000..c343392d --- /dev/null +++ b/android/src/main/java/io/invertase/firebase/admob/RNFirebaseAdmobInterstitial.java @@ -0,0 +1,47 @@ +package io.invertase.firebase.admob; + + +import android.app.Activity; +import android.util.Log; + +import com.facebook.react.bridge.ReactApplicationContext; +import com.google.android.gms.ads.AdRequest; +import com.google.android.gms.ads.InterstitialAd; + +class RNFirebaseAdmobInterstitial { + + private InterstitialAd interstitialAd; + private RNFirebaseAdMob adMob; + + RNFirebaseAdmobInterstitial(String adUnit, RNFirebaseAdMob adMobInstance) { + adMob = adMobInstance; + interstitialAd = new InterstitialAd(adMob.getContext()); + interstitialAd.setAdUnitId(adUnit); + } + + void loadAd(final AdRequest adRequest) { + Activity activity = adMob.getActivity(); + if (activity != null) { + activity.runOnUiThread(new Runnable() { + @Override + public void run() { + interstitialAd.loadAd(adRequest); + } + }); + } + } + + void show() { + Activity activity = adMob.getActivity(); + if (activity != null) { + activity.runOnUiThread(new Runnable() { + @Override + public void run() { + if (interstitialAd.isLoaded()) { + interstitialAd.show(); + } + } + }); + } + } +} diff --git a/lib/firebase.js b/lib/firebase.js index 84263a64..45db790f 100644 --- a/lib/firebase.js +++ b/lib/firebase.js @@ -16,6 +16,7 @@ import Analytics from './modules/analytics'; import Crash from './modules/crash'; import RemoteConfig from './modules/config'; import Performance from './modules/perf'; +import AdMob, { statics as AdMobStatics } from './modules/admob'; const instances: Object = { default: null }; const FirebaseModule = NativeModules.RNFirebase; @@ -36,6 +37,7 @@ export default class Firebase { _config: ?Object; _crash: ?Object; _perf: ?Object; + _admob: ?Object; auth: Function; crash: Function; @@ -45,6 +47,7 @@ export default class Firebase { messaging: Function; config: Function; perf: Function; + admob: Function; eventHandlers: Object; debug: boolean; @@ -90,6 +93,7 @@ export default class Firebase { this.crash = this._staticsOrInstance('crash', {}, Crash); this.config = this._staticsOrInstance('config', {}, RemoteConfig); this.perf = this._staticsOrInstance('perf', {}, Performance); + this.admob = this._staticsOrInstance('admob', AdMobStatics, AdMob); // init auth to start listeners this.auth(); From ce6cd4fc9e3c5e8bf1d1ff71359e19699b989db4 Mon Sep 17 00:00:00 2001 From: Elliot Hesp Date: Fri, 26 May 2017 15:20:14 +0100 Subject: [PATCH 06/14] [admob] Start of documentation --- docs/modules/admob.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 docs/modules/admob.md diff --git a/docs/modules/admob.md b/docs/modules/admob.md new file mode 100644 index 00000000..fec3e753 --- /dev/null +++ b/docs/modules/admob.md @@ -0,0 +1,13 @@ +# AdMob + +The admob allows you to display adverts in your app, using your account from [AdMob by Google](https://www.google.co.uk/admob/). + +RNFirebase allows you to display Banners, Interstitials, Native Ads & Rewarded Videos. + +## Banner + +## Interstitial + +## Native + +## Rewarded Video From b53c2089117327761ebc3cae8b261692e10decaf Mon Sep 17 00:00:00 2001 From: Elliot Hesp Date: Fri, 26 May 2017 15:21:00 +0100 Subject: [PATCH 07/14] [admob][android] Add RNFirebaseAdmob to modules list --- .../main/java/io/invertase/firebase/RNFirebasePackage.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/android/src/main/java/io/invertase/firebase/RNFirebasePackage.java b/android/src/main/java/io/invertase/firebase/RNFirebasePackage.java index 90cf8887..8f91ecfc 100644 --- a/android/src/main/java/io/invertase/firebase/RNFirebasePackage.java +++ b/android/src/main/java/io/invertase/firebase/RNFirebasePackage.java @@ -14,7 +14,7 @@ import java.util.List; import java.util.ArrayList; import java.util.Collections; -import io.invertase.firebase.admob.RNFirebaseAdMob; +import io.invertase.firebase.admob.RNFirebaseAdMobBanner; import io.invertase.firebase.auth.RNFirebaseAuth; import io.invertase.firebase.config.RNFirebaseRemoteConfig; import io.invertase.firebase.storage.RNFirebaseStorage; @@ -23,6 +23,7 @@ import io.invertase.firebase.analytics.RNFirebaseAnalytics; import io.invertase.firebase.crash.RNFirebaseCrash; import io.invertase.firebase.messaging.RNFirebaseMessaging; import io.invertase.firebase.perf.RNFirebasePerformance; +import io.invertase.firebase.admob.RNFirebaseAdMob; @SuppressWarnings("unused") public class RNFirebasePackage implements ReactPackage { @@ -47,6 +48,7 @@ public class RNFirebasePackage implements ReactPackage { modules.add(new RNFirebaseCrash(reactContext)); modules.add(new RNFirebaseRemoteConfig(reactContext)); modules.add(new RNFirebasePerformance(reactContext)); + modules.add(new RNFirebaseAdMob(reactContext)); return modules; } @@ -69,7 +71,7 @@ public class RNFirebasePackage implements ReactPackage { @Override public List createViewManagers(ReactApplicationContext reactContext) { return Arrays.asList( - new RNFirebaseAdMob() + new RNFirebaseAdMobBanner() ); } } From cf18f85720847789e7439896dbecc1841d623ff1 Mon Sep 17 00:00:00 2001 From: Elliot Hesp Date: Fri, 26 May 2017 17:51:24 +0100 Subject: [PATCH 08/14] [admob][android] Handle testing prop on banner component --- .../firebase/admob/RNFirebaseAdMobBanner.java | 57 +++++++------------ 1 file changed, 22 insertions(+), 35 deletions(-) diff --git a/android/src/main/java/io/invertase/firebase/admob/RNFirebaseAdMobBanner.java b/android/src/main/java/io/invertase/firebase/admob/RNFirebaseAdMobBanner.java index 274fd62f..e60bdd17 100644 --- a/android/src/main/java/io/invertase/firebase/admob/RNFirebaseAdMobBanner.java +++ b/android/src/main/java/io/invertase/firebase/admob/RNFirebaseAdMobBanner.java @@ -1,7 +1,6 @@ package io.invertase.firebase.admob; import android.support.annotation.Nullable; -import android.util.Log; import android.view.View; import com.facebook.react.bridge.Arguments; @@ -17,11 +16,9 @@ import com.google.android.gms.ads.AdListener; import com.google.android.gms.ads.AdRequest; import com.google.android.gms.ads.AdSize; import com.google.android.gms.ads.AdView; -import com.google.android.gms.ads.MobileAds; import java.util.Map; - public class RNFirebaseAdMobBanner extends SimpleViewManager implements View.OnLayoutChangeListener { public static final String REACT_CLASS = "RNFirebaseAdMobBanner"; @@ -51,6 +48,7 @@ public class RNFirebaseAdMobBanner extends SimpleViewManager imp private ReactViewGroup viewGroup; private RCTEventEmitter emitter; private String size; + private Boolean testing = false; @Override public String getName() { @@ -69,7 +67,6 @@ public class RNFirebaseAdMobBanner extends SimpleViewManager imp emitter = themedReactContext.getJSModule(RCTEventEmitter.class); attachAdViewToViewGroup(); - return viewGroup; } @@ -98,7 +95,6 @@ public class RNFirebaseAdMobBanner extends SimpleViewManager imp */ @Override public void onLayoutChange(View view, final int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) { - Log.d("Ads", "onLayoutChange"); // If the view has changed at all, recalculate what banner we need if (left != oldLeft || right != oldRight || top != oldTop || bottom != oldBottom) { setSize(viewGroup, null); @@ -108,14 +104,23 @@ public class RNFirebaseAdMobBanner extends SimpleViewManager imp /** * Handle unitId prop * @param view - * @param unitId + * @param value */ @ReactProp(name = "unitId") - public void setUnitId(final ReactViewGroup view, final String unitId) { - Log.d("Ads", "Prop unitId something " + unitId); - + public void setUnitId(final ReactViewGroup view, final String value) { AdView adViewView = (AdView) view.getChildAt(0); - adViewView.setAdUnitId(unitId); + adViewView.setAdUnitId(value); + requestAd(); + } + + /** + * Handle testing prop + * @param view + * @param value + */ + @ReactProp(name = "testing") + public void setUnitId(final ReactViewGroup view, final Boolean value) { + testing = value; requestAd(); } @@ -131,8 +136,6 @@ public class RNFirebaseAdMobBanner extends SimpleViewManager imp } AdSize adSize = propToAdSize(size.toUpperCase()); - Log.d("Ads", "Prop size something " + adSize.toString()); - AdView adViewView = (AdView) view.getChildAt(0); adViewView.setAdSize(adSize); @@ -192,7 +195,12 @@ public class RNFirebaseAdMobBanner extends SimpleViewManager imp } AdRequest.Builder adRequestBuilder = new AdRequest.Builder(); - adRequestBuilder.addTestDevice(AdRequest.DEVICE_ID_EMULATOR); + + // If the prop testing is set, assign the emulators device ID + if (testing) { + adRequestBuilder.addTestDevice(AdRequest.DEVICE_ID_EMULATOR); + } + AdRequest adRequest = adRequestBuilder.build(); adView.loadAd(adRequest); } @@ -220,28 +228,7 @@ public class RNFirebaseAdMobBanner extends SimpleViewManager imp @Override public void onAdFailedToLoad(int errorCode) { - WritableMap payload = Arguments.createMap(); - - // TODO Error code converter - switch (errorCode) { - case AdRequest.ERROR_CODE_INTERNAL_ERROR: - payload.putString("code", "admob/error-code-internal-error"); - payload.putString("message", "Something happened internally; for instance, an invalid response was received from the ad server."); - break; - case AdRequest.ERROR_CODE_INVALID_REQUEST: - payload.putString("code", "admob/error-code-invalid-request"); - payload.putString("message", "The ad request was invalid; for instance, the ad unit ID was incorrect."); - break; - case AdRequest.ERROR_CODE_NETWORK_ERROR: - payload.putString("code", "admob/error-code-network-error"); - payload.putString("message", "The ad request was unsuccessful due to network connectivity."); - break; - case AdRequest.ERROR_CODE_NO_FILL: - payload.putString("code", "admob/error-code-no-fill"); - payload.putString("message", "The ad request was successful, but no ad was returned due to lack of ad inventory."); - break; - } - + WritableMap payload = RNFirebaseAdMobUtils.errorCodeToMap(errorCode); sendEvent(Events.EVENT_AD_FAILED_TO_LOAD.toString(), payload); } From 5605610ba642f1d205c10db32d83c0c76e3e0ec3 Mon Sep 17 00:00:00 2001 From: Elliot Hesp Date: Fri, 26 May 2017 17:53:12 +0100 Subject: [PATCH 09/14] [admob][android] Handle all AdListener events --- .../admob/RNFirebaseAdmobInterstitial.java | 65 ++++++++++++++++++- 1 file changed, 64 insertions(+), 1 deletion(-) diff --git a/android/src/main/java/io/invertase/firebase/admob/RNFirebaseAdmobInterstitial.java b/android/src/main/java/io/invertase/firebase/admob/RNFirebaseAdmobInterstitial.java index c343392d..25b532a8 100644 --- a/android/src/main/java/io/invertase/firebase/admob/RNFirebaseAdmobInterstitial.java +++ b/android/src/main/java/io/invertase/firebase/admob/RNFirebaseAdmobInterstitial.java @@ -2,23 +2,66 @@ package io.invertase.firebase.admob; import android.app.Activity; +import android.support.annotation.Nullable; import android.util.Log; +import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.WritableMap; +import com.google.android.gms.ads.AdListener; import com.google.android.gms.ads.AdRequest; import com.google.android.gms.ads.InterstitialAd; +import io.invertase.firebase.Utils; + class RNFirebaseAdmobInterstitial { private InterstitialAd interstitialAd; private RNFirebaseAdMob adMob; + private AdListener adListener; + private String adUnit; - RNFirebaseAdmobInterstitial(String adUnit, RNFirebaseAdMob adMobInstance) { + RNFirebaseAdmobInterstitial(final String adUnitString, final RNFirebaseAdMob adMobInstance) { + adUnit = adUnitString; adMob = adMobInstance; interstitialAd = new InterstitialAd(adMob.getContext()); interstitialAd.setAdUnitId(adUnit); + + adListener = new AdListener() { + @Override + public void onAdLoaded() { + sendEvent("onAdLoaded", null); + } + + @Override + public void onAdOpened() { + sendEvent("onAdOpened", null); + } + + @Override + public void onAdLeftApplication() { + sendEvent("onAdLeftApplication", null); + } + + @Override + public void onAdClosed() { + sendEvent("onAdClosed", null); + } + + @Override + public void onAdFailedToLoad(int errorCode) { + WritableMap payload = RNFirebaseAdMobUtils.errorCodeToMap(errorCode); + sendEvent("onAdFailedToLoad", payload); + } + }; + + interstitialAd.setAdListener(adListener); } + /** + * Load an Ad with a AdRequest instance + * @param adRequest + */ void loadAd(final AdRequest adRequest) { Activity activity = adMob.getActivity(); if (activity != null) { @@ -31,6 +74,9 @@ class RNFirebaseAdmobInterstitial { } } + /** + * Show the loaded interstitial, if it's loaded + */ void show() { Activity activity = adMob.getActivity(); if (activity != null) { @@ -44,4 +90,21 @@ class RNFirebaseAdmobInterstitial { }); } } + + /** + * Send a native event over the bridge with a type and optional payload + * @param type + * @param payload + */ + void sendEvent(String type, final @Nullable WritableMap payload) { + WritableMap map = Arguments.createMap(); + map.putString("type", type); + map.putString("adunit", adUnit); + + if (payload != null) { + map.putMap("payload", payload); + } + + Utils.sendEvent(adMob.getContext(), "interstitial_event", map); + } } From d75405dbab16c37be9091ba8d655a43bdbd3f06f Mon Sep 17 00:00:00 2001 From: Elliot Hesp Date: Fri, 26 May 2017 17:54:09 +0100 Subject: [PATCH 10/14] [admob][android] Interstitial; Handle JS events from native --- lib/modules/admob/Interstitial.js | 63 ++++++++++++++++++++++++++++--- 1 file changed, 58 insertions(+), 5 deletions(-) diff --git a/lib/modules/admob/Interstitial.js b/lib/modules/admob/Interstitial.js index 84f03853..c8b20116 100644 --- a/lib/modules/admob/Interstitial.js +++ b/lib/modules/admob/Interstitial.js @@ -1,29 +1,82 @@ import { NativeModules } from 'react-native'; +import { statics } from './'; +import { nativeToJSError } from '../../utils'; const FirebaseAdMob = NativeModules.RNFirebaseAdmob; export default class Interstitial { - constructor(admob: Object, eventListener: Object, adunit: string) { + constructor(admob: Object, adunit: string) { this.admob = admob; this.adUnit = adunit; - - eventListener.addListener('interstitial_event', this._onInterstitialEvent.bind(this)); + this.loaded = false; + this.admob.on(`interstitial_${adunit}`, this._onInterstitialEvent.bind(this)); } + /** + * Handle a JS emit event + * @param event + * @private + */ _onInterstitialEvent(event) { + const eventType = `interstitial:${this.adUnit}:${event.type}`; + let emitData = Object.assign({}, event); + + switch (event.type) { + case 'onAdLoaded': + this.loaded = true; + break; + case 'onAdFailedToLoad': + emitData = nativeToJSError(event.payload.code, event.payload.message); + emitData.type = event.type; + break; + default: + } + + this.admob.emit(eventType, emitData); + this.admob.emit(`interstitial:${this.adUnit}:*`, emitData); } + /** + * Load an ad with an instance of AdRequest + * @param request + * @returns {*} + */ loadAd(request: AdRequest) { return FirebaseAdMob.interstitialLoadAd(this.adUnit, request); } + /** + * Return a local instance of isLoaded + * @returns {boolean} + */ isLoaded() { - return true; + return this.loaded; } + /** + * Show the advert - will only show if loaded + * @returns {*} + */ show() { - return FirebaseAdMob.interstitialShowAd(this.adUnit); + if (this.loaded) { + FirebaseAdMob.interstitialShowAd(this.adUnit); + } + } + + /** + * Listen to an Ad event + * @param eventType + * @param listenerCb + * @returns {null} + */ + on(eventType, listenerCb) { + if (!statics.EventTypes[eventType]) { + console.warn(`Invalid event type provided, must be one of: ${Object.keys(statics.EventTypes).join(', ')}`); + return null; + } + + return this.admob.on(`interstitial:${this.adUnit}:${eventType}`, listenerCb); } } From d0e1ae3decdf5f0a35e0cbf2b5ef4cc16ae4ff7c Mon Sep 17 00:00:00 2001 From: Elliot Hesp Date: Fri, 26 May 2017 17:54:34 +0100 Subject: [PATCH 11/14] [admob][android] Cleanup --- .../firebase/admob/RNFirebaseAdMob.java | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/android/src/main/java/io/invertase/firebase/admob/RNFirebaseAdMob.java b/android/src/main/java/io/invertase/firebase/admob/RNFirebaseAdMob.java index 899dc017..a954632c 100644 --- a/android/src/main/java/io/invertase/firebase/admob/RNFirebaseAdMob.java +++ b/android/src/main/java/io/invertase/firebase/admob/RNFirebaseAdMob.java @@ -4,46 +4,35 @@ package io.invertase.firebase.admob; import android.app.Activity; import android.util.Log; -import com.facebook.react.bridge.Arguments; -import com.facebook.react.bridge.Promise; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactMethod; import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.ReadableMap; -import com.facebook.react.bridge.WritableArray; import com.google.android.gms.ads.AdRequest; -import com.google.firebase.database.ServerValue; -import com.google.firebase.perf.FirebasePerformance; -import com.google.firebase.perf.metrics.Trace; -import com.google.firebase.remoteconfig.FirebaseRemoteConfig; - import io.invertase.firebase.Utils; -import io.invertase.firebase.admob.RNFirebaseAdmobInterstitial; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Set; + public class RNFirebaseAdMob extends ReactContextBaseJavaModule { private static final String TAG = "RNFirebaseAdmob"; - public ReactApplicationContext getContext() { + ReactApplicationContext getContext() { return getReactApplicationContext(); } - public Activity getActivity() { + Activity getActivity() { return getCurrentActivity(); } - private ReactApplicationContext context; private HashMap interstitials = new HashMap<>(); public RNFirebaseAdMob(ReactApplicationContext reactContext) { super(reactContext); - context = reactContext; Log.d(TAG, "New instance"); } From 1b8a2826acec4d2b11d7d88f6ae1d9953202622d Mon Sep 17 00:00:00 2001 From: Elliot Hesp Date: Fri, 26 May 2017 17:55:22 +0100 Subject: [PATCH 12/14] [admob][android] Implement shared event emitter @Salakar --- lib/modules/admob/Banner.js | 32 ++++++++++++-- lib/modules/admob/index.js | 33 ++++++++++++-- lib/modules/base.js | 88 ++++++++++++++++--------------------- 3 files changed, 97 insertions(+), 56 deletions(-) diff --git a/lib/modules/admob/Banner.js b/lib/modules/admob/Banner.js index 9ac725ad..70d93ded 100644 --- a/lib/modules/admob/Banner.js +++ b/lib/modules/admob/Banner.js @@ -1,18 +1,29 @@ import React, { PropTypes } from 'react'; import { requireNativeComponent, View } from 'react-native'; +import { statics } from './'; +import { nativeToJSError } from '../../utils'; class Banner extends React.Component { static propTypes = { ...View.propTypes, + // TODO ehesp: cant init this outside of the component; statics isn't defined + ...(() => { + const eventProps = {}; + Object.keys(statics.EventTypes).forEach((key) => { + eventProps[key] = PropTypes.func; + }); + return eventProps; + }), size: PropTypes.string, unitId: PropTypes.string, - onAdLoaded: PropTypes.func, + testing: PropTypes.bool, }; static defaultProps = { size: 'SMART_BANNER', unitId: 'ca-app-pub-3940256099942544/6300978111', // Testing + testing: true, }; constructor() { @@ -23,12 +34,16 @@ class Banner extends React.Component { }; } + /** + * Handle a single banner event and pass to + * any props watching it + * @param nativeEvent + */ onBannerEvent = ({ nativeEvent }) => { if (this.props[nativeEvent.type]) { if (nativeEvent.type === 'onAdFailedToLoad') { - const error = new Error(nativeEvent.payload.message); - error.code = nativeEvent.payload.code; - this.props[nativeEvent.type](error); + const { code, message } = nativeEvent.payload; + this.props[nativeEvent.type](nativeToJSError(code, message)); } else { this.props[nativeEvent.type](nativeEvent.payload || {}); } @@ -37,10 +52,19 @@ class Banner extends React.Component { if (nativeEvent.type === 'onSizeChange') this.updateSize(nativeEvent.payload); }; + /** + * Handle a native onSizeChange event + * @param width + * @param height + */ updateSize = ({ width, height }) => { this.setState({ width, height }); }; + /** + * Render the native component + * @returns {XML} + */ render() { return ( Date: Fri, 26 May 2017 17:56:04 +0100 Subject: [PATCH 13/14] [admob][android] Add util to convert code + message to JS Error --- lib/utils/index.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/utils/index.js b/lib/utils/index.js index bb957a69..0777975d 100644 --- a/lib/utils/index.js +++ b/lib/utils/index.js @@ -327,3 +327,9 @@ export function generatePushID(serverTimeOffset?: number = 0): string { return id; } + +export function nativeToJSError(code: string, message: string) { + const error = new Error(message); + error.code = code; + return error; +} From 608c1d8d6bd05307f99dbb91f406351f83c5dd69 Mon Sep 17 00:00:00 2001 From: Elliot Hesp Date: Fri, 26 May 2017 22:19:20 +0100 Subject: [PATCH 14/14] [admob][android] Start implementation of Rewarded Video --- .../firebase/admob/RNFirebaseAdMob.java | 74 +++++++---- .../firebase/admob/RNFirebaseAdMobUtils.java | 39 ++++++ .../admob/RNFirebaseRewardedVideo.java | 121 ++++++++++++++++++ docs/modules/admob.md | 64 ++++++++- lib/modules/admob/index.js | 9 ++ 5 files changed, 278 insertions(+), 29 deletions(-) create mode 100644 android/src/main/java/io/invertase/firebase/admob/RNFirebaseAdMobUtils.java create mode 100644 android/src/main/java/io/invertase/firebase/admob/RNFirebaseRewardedVideo.java diff --git a/android/src/main/java/io/invertase/firebase/admob/RNFirebaseAdMob.java b/android/src/main/java/io/invertase/firebase/admob/RNFirebaseAdMob.java index a954632c..9c7f1651 100644 --- a/android/src/main/java/io/invertase/firebase/admob/RNFirebaseAdMob.java +++ b/android/src/main/java/io/invertase/firebase/admob/RNFirebaseAdMob.java @@ -30,6 +30,7 @@ public class RNFirebaseAdMob extends ReactContextBaseJavaModule { } private HashMap interstitials = new HashMap<>(); + private HashMap rewardedVideos = new HashMap<>(); public RNFirebaseAdMob(ReactApplicationContext reactContext) { super(reactContext); @@ -44,6 +45,53 @@ public class RNFirebaseAdMob extends ReactContextBaseJavaModule { @ReactMethod public void interstitialLoadAd(String adUnit, ReadableMap request) { RNFirebaseAdmobInterstitial interstitial = getOrCreateInterstitial(adUnit); + interstitial.loadAd(buildRequest(request).build()); + } + + @ReactMethod + public void interstitialShowAd(String adUnit) { + RNFirebaseAdmobInterstitial interstitial = getOrCreateInterstitial(adUnit); + interstitial.show(); + } + + @ReactMethod + public void rewardedVideoLoadAd(String adUnit, ReadableMap request) { + RNFirebaseRewardedVideo rewardedVideo = getOrCreateRewardedVideo(adUnit); + rewardedVideo.loadAd(buildRequest(request).build()); + } + + @ReactMethod + public void rewardedVideoShowAd(String adUnit) { + RNFirebaseRewardedVideo rewardedVideo = getOrCreateRewardedVideo(adUnit); + rewardedVideo.show(); + } + + private RNFirebaseAdmobInterstitial getOrCreateInterstitial(String adUnit) { + if (interstitials.containsKey(adUnit)) { + return interstitials.get(adUnit); + } + RNFirebaseAdmobInterstitial interstitial = new RNFirebaseAdmobInterstitial(adUnit, this); + interstitials.put(adUnit, interstitial); + return interstitial; + } + + private RNFirebaseRewardedVideo getOrCreateRewardedVideo(String adUnit) { + if (rewardedVideos.containsKey(adUnit)) { + return rewardedVideos.get(adUnit); + } + RNFirebaseRewardedVideo rewardedVideo = new RNFirebaseRewardedVideo(adUnit, this); + rewardedVideos.put(adUnit, rewardedVideo); + return rewardedVideo; + } + + @Override + public Map getConstants() { + final Map constants = new HashMap<>(); + constants.put("DEVICE_ID_EMULATOR", AdRequest.DEVICE_ID_EMULATOR); + return constants; + } + + AdRequest.Builder buildRequest(ReadableMap request) { AdRequest.Builder requestBuilder = new AdRequest.Builder(); if (request.hasKey("testDevice")) { @@ -57,30 +105,6 @@ public class RNFirebaseAdMob extends ReactContextBaseJavaModule { requestBuilder.addKeyword((String) word); } - interstitial.loadAd(requestBuilder.build()); - } - - @ReactMethod - public void interstitialShowAd(String adUnit) { - RNFirebaseAdmobInterstitial interstitial = getOrCreateInterstitial(adUnit); - interstitial.show(); - } - - private RNFirebaseAdmobInterstitial getOrCreateInterstitial(String adUnit) { - if (interstitials.containsKey(adUnit)) { - return interstitials.get(adUnit); - } - RNFirebaseAdmobInterstitial interstitial = new RNFirebaseAdmobInterstitial(adUnit, this); - interstitials.put(adUnit, interstitial); - return interstitial; - } - - - - @Override - public Map getConstants() { - final Map constants = new HashMap<>(); - constants.put("DEVICE_ID_EMULATOR", AdRequest.DEVICE_ID_EMULATOR); - return constants; + return requestBuilder; } } diff --git a/android/src/main/java/io/invertase/firebase/admob/RNFirebaseAdMobUtils.java b/android/src/main/java/io/invertase/firebase/admob/RNFirebaseAdMobUtils.java new file mode 100644 index 00000000..7ce6310c --- /dev/null +++ b/android/src/main/java/io/invertase/firebase/admob/RNFirebaseAdMobUtils.java @@ -0,0 +1,39 @@ +package io.invertase.firebase.admob; + +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.WritableMap; +import com.google.android.gms.ads.AdRequest; + +class RNFirebaseAdMobUtils { + + /** + * Convert common AdMob errors into a standard format + * @param errorCode + * @return + */ + static WritableMap errorCodeToMap(int errorCode) { + WritableMap map = Arguments.createMap(); + + switch (errorCode) { + case AdRequest.ERROR_CODE_INTERNAL_ERROR: + map.putString("code", "admob/error-code-internal-error"); + map.putString("message", "Something happened internally; for instance, an invalid response was received from the ad server."); + break; + case AdRequest.ERROR_CODE_INVALID_REQUEST: + map.putString("code", "admob/error-code-invalid-request"); + map.putString("message", "The ad request was invalid; for instance, the ad unit ID was incorrect."); + break; + case AdRequest.ERROR_CODE_NETWORK_ERROR: + map.putString("code", "admob/error-code-network-error"); + map.putString("message", "The ad request was unsuccessful due to network connectivity."); + break; + case AdRequest.ERROR_CODE_NO_FILL: + map.putString("code", "admob/error-code-no-fill"); + map.putString("message", "The ad request was successful, but no ad was returned due to lack of ad inventory."); + break; + } + + return map; + } + +} diff --git a/android/src/main/java/io/invertase/firebase/admob/RNFirebaseRewardedVideo.java b/android/src/main/java/io/invertase/firebase/admob/RNFirebaseRewardedVideo.java new file mode 100644 index 00000000..136d8d7b --- /dev/null +++ b/android/src/main/java/io/invertase/firebase/admob/RNFirebaseRewardedVideo.java @@ -0,0 +1,121 @@ +package io.invertase.firebase.admob; + + +import android.app.Activity; +import android.support.annotation.Nullable; + +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.WritableMap; +import com.google.android.gms.ads.AdListener; +import com.google.android.gms.ads.AdRequest; +import com.google.android.gms.ads.InterstitialAd; +import com.google.android.gms.ads.MobileAds; +import com.google.android.gms.ads.reward.RewardItem; +import com.google.android.gms.ads.reward.RewardedVideoAd; +import com.google.android.gms.ads.reward.RewardedVideoAdListener; + +import io.invertase.firebase.Utils; + +public class RNFirebaseRewardedVideo implements RewardedVideoAdListener { + + private RewardedVideoAd mAd; + private String adUnit; + private RNFirebaseAdMob adMob; + private RewardedVideoAd rewardedVideo; + + RNFirebaseRewardedVideo(final String adUnitString, final RNFirebaseAdMob adMobInstance) { + adUnit = adUnitString; + adMob = adMobInstance; + + rewardedVideo = MobileAds.getRewardedVideoAdInstance(adMob.getContext()); + rewardedVideo.setRewardedVideoAdListener(this); + } + + /** + * Load an Ad with a AdRequest instance + * @param adRequest + */ + void loadAd(final AdRequest adRequest) { + Activity activity = adMob.getActivity(); + if (activity != null) { + activity.runOnUiThread(new Runnable() { + @Override + public void run() { + rewardedVideo.loadAd(adUnit, adRequest); + } + }); + } + } + + /** + * Show the loaded interstitial, if it's loaded + */ + void show() { + Activity activity = adMob.getActivity(); + if (activity != null) { + activity.runOnUiThread(new Runnable() { + @Override + public void run() { + if (rewardedVideo.isLoaded()) { + rewardedVideo.show(); + } + } + }); + } + } + + @Override + public void onRewarded(RewardItem reward) { + sendEvent("onRewarded", null); + } + + @Override + public void onRewardedVideoAdLeftApplication() { + sendEvent("onRewardedVideoAdLeftApplication", null); + } + + @Override + public void onRewardedVideoAdClosed() { + sendEvent("onRewardedVideoAdClosed", null); + } + + @Override + public void onRewardedVideoAdFailedToLoad(int errorCode) { + WritableMap payload = RNFirebaseAdMobUtils.errorCodeToMap(errorCode); + sendEvent("onRewardedVideoAdFailedToLoad", payload); + } + + @Override + public void onRewardedVideoAdLoaded() { + sendEvent("onRewardedVideoAdLoaded", null); + } + + @Override + public void onRewardedVideoAdOpened() { + sendEvent("onRewardedVideoAdOpened", null); + } + + @Override + public void onRewardedVideoStarted() { + sendEvent("onRewardedVideoStarted", null); + } + + // TODO onResume etc??? https://developers.google.com/admob/android/rewarded-video + + /** + * Send a native event over the bridge with a type and optional payload + * @param type + * @param payload + */ + void sendEvent(String type, final @Nullable WritableMap payload) { + WritableMap map = Arguments.createMap(); + map.putString("type", type); + map.putString("adunit", adUnit); + + if (payload != null) { + map.putMap("payload", payload); + } + + Utils.sendEvent(adMob.getContext(), "rewarded_video_event", map); + } +} diff --git a/docs/modules/admob.md b/docs/modules/admob.md index fec3e753..3c50dfc5 100644 --- a/docs/modules/admob.md +++ b/docs/modules/admob.md @@ -4,10 +4,66 @@ The admob allows you to display adverts in your app, using your account from [Ad RNFirebase allows you to display Banners, Interstitials, Native Ads & Rewarded Videos. -## Banner +### Banner -## Interstitial +AdMob Banners in RNFirebase are exported as a usable React component, allowing you to integrate it easily into your existing app very easily. -## Native +```js +const Banner = firebase.admob.Banner; +... +render() { + return ( + + ); +} + +``` + +### Interstitial + +An interstitial is a full screen advert which creates a new activity on top of React. As they need to be controlled, +allowing the developer to choose when to display them they're not available as a component. Instead they're controlled via +method calls. + +To request an interstitial from AdMob, the `loadAd` method must be called with an instance of `AdRequest` (see below for full API): + +```js +const advert = firebase.admob().interstitial('ca-app-pub-3940256099942544/1033173712'); + +const AdRequest = firebase.admob.AdRequest; +const request = new AdRequest(); +request.addKeyword('foo').addKeyword('bar'); + +// Load the advert with our AdRequest +advert.loadAd(request.build()); + +// Simulate the interstitial being shown "sometime" later during the apps lifecycle +setTimeout(() => { + if (advert.isLoaded()) { + advert.show(); + } else { + // Unable to show interstitial - not loaded yet. + } +}, 1000); + +``` + +### Native + +### Rewarded Video + +## Statics + +### Banner +> Accessed via `firebase.admob.Banner`. + +Exports a React component with the following PropTypes: + + +### AdRequest +> Accessed via `firebase.admob.AdRequest`. + +Used to build a request object to pass into AdMob requests. Exposes the following chainable methods: -## Rewarded Video diff --git a/lib/modules/admob/index.js b/lib/modules/admob/index.js index a3224527..2020eea1 100644 --- a/lib/modules/admob/index.js +++ b/lib/modules/admob/index.js @@ -44,4 +44,13 @@ export const statics = { onAdClosed: 'onAdClosed', onAdFailedToLoad: 'onAdFailedToLoad', }, + RewardedEventTypes: { + onRewarded: 'onRewarded', + onRewardedVideoAdLeftApplication: 'onRewardedVideoAdLeftApplication', + onRewardedVideoAdClosed: 'onRewardedVideoAdClosed', + onRewardedVideoAdFailedToLoad: 'onRewardedVideoAdFailedToLoad', + onRewardedVideoAdLoaded: 'onRewardedVideoAdLoaded', + onRewardedVideoAdOpened: 'onRewardedVideoAdOpened', + onRewardedVideoStarted: 'onRewardedVideoStarted', + }, };