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/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 @@ + + diff --git a/android/src/main/java/io/invertase/firebase/RNFirebasePackage.java b/android/src/main/java/io/invertase/firebase/RNFirebasePackage.java index fb5ac6e1..8f91ecfc 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.RNFirebaseAdMobBanner; import io.invertase.firebase.auth.RNFirebaseAuth; import io.invertase.firebase.config.RNFirebaseRemoteConfig; import io.invertase.firebase.storage.RNFirebaseStorage; @@ -21,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 { @@ -45,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; } @@ -66,6 +70,8 @@ public class RNFirebasePackage implements ReactPackage { */ @Override public List createViewManagers(ReactApplicationContext reactContext) { - return Collections.emptyList(); + return Arrays.asList( + new RNFirebaseAdMobBanner() + ); } } 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..9c7f1651 --- /dev/null +++ b/android/src/main/java/io/invertase/firebase/admob/RNFirebaseAdMob.java @@ -0,0 +1,110 @@ +package io.invertase.firebase.admob; + + +import android.app.Activity; +import android.util.Log; + +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.google.android.gms.ads.AdRequest; +import io.invertase.firebase.Utils; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + + +public class RNFirebaseAdMob extends ReactContextBaseJavaModule { + + private static final String TAG = "RNFirebaseAdmob"; + + ReactApplicationContext getContext() { + return getReactApplicationContext(); + } + + Activity getActivity() { + return getCurrentActivity(); + } + + private HashMap interstitials = new HashMap<>(); + private HashMap rewardedVideos = new HashMap<>(); + + public RNFirebaseAdMob(ReactApplicationContext reactContext) { + super(reactContext); + Log.d(TAG, "New instance"); + } + + @Override + public String getName() { + return TAG; + } + + @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")) { + requestBuilder.addTestDevice(AdRequest.DEVICE_ID_EMULATOR); + } + + ReadableArray keywords = request.getArray("keywords"); + List keywordsList = Utils.recursivelyDeconstructReadableArray(keywords); + + for (Object word : keywordsList) { + requestBuilder.addKeyword((String) word); + } + + return requestBuilder; + } +} 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..e60bdd17 --- /dev/null +++ b/android/src/main/java/io/invertase/firebase/admob/RNFirebaseAdMobBanner.java @@ -0,0 +1,290 @@ +package io.invertase.firebase.admob; + +import android.support.annotation.Nullable; +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 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; + private Boolean testing = false; + + @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) { + // 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 value + */ + @ReactProp(name = "unitId") + public void setUnitId(final ReactViewGroup view, final String value) { + AdView adViewView = (AdView) view.getChildAt(0); + 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(); + } + + /** + * 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()); + 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(); + + // 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); + } + + /** + * 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 = RNFirebaseAdMobUtils.errorCodeToMap(errorCode); + 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/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/RNFirebaseAdmobInterstitial.java b/android/src/main/java/io/invertase/firebase/admob/RNFirebaseAdmobInterstitial.java new file mode 100644 index 00000000..25b532a8 --- /dev/null +++ b/android/src/main/java/io/invertase/firebase/admob/RNFirebaseAdmobInterstitial.java @@ -0,0 +1,110 @@ +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(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) { + activity.runOnUiThread(new Runnable() { + @Override + public void run() { + interstitialAd.loadAd(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 (interstitialAd.isLoaded()) { + interstitialAd.show(); + } + } + }); + } + } + + /** + * 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); + } +} 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 new file mode 100644 index 00000000..3c50dfc5 --- /dev/null +++ b/docs/modules/admob.md @@ -0,0 +1,69 @@ +# 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 + +AdMob Banners in RNFirebase are exported as a usable React component, allowing you to integrate it easily into your existing app very easily. + +```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: + 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/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(); 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/Banner.js b/lib/modules/admob/Banner.js new file mode 100644 index 00000000..70d93ded --- /dev/null +++ b/lib/modules/admob/Banner.js @@ -0,0 +1,82 @@ +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, + testing: PropTypes.bool, + }; + + static defaultProps = { + size: 'SMART_BANNER', + unitId: 'ca-app-pub-3940256099942544/6300978111', // Testing + testing: true, + }; + + constructor() { + super(); + this.state = { + width: 0, + height: 0, + }; + } + + /** + * 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 { code, message } = nativeEvent.payload; + this.props[nativeEvent.type](nativeToJSError(code, message)); + } else { + this.props[nativeEvent.type](nativeEvent.payload || {}); + } + } + + 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 ( + + ); + } + +} + +const RNFirebaseAdMobBanner = requireNativeComponent('RNFirebaseAdMobBanner', Banner); + +export default Banner; diff --git a/lib/modules/admob/Interstitial.js b/lib/modules/admob/Interstitial.js new file mode 100644 index 00000000..c8b20116 --- /dev/null +++ b/lib/modules/admob/Interstitial.js @@ -0,0 +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, adunit: string) { + this.admob = admob; + this.adUnit = adunit; + 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 this.loaded; + } + + /** + * Show the advert - will only show if loaded + * @returns {*} + */ + show() { + 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); + } +} diff --git a/lib/modules/admob/index.js b/lib/modules/admob/index.js new file mode 100644 index 00000000..2020eea1 --- /dev/null +++ b/lib/modules/admob/index.js @@ -0,0 +1,56 @@ +import { NativeModules, NativeEventEmitter } from 'react-native'; +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 { + + constructor() { + super(); + FirebaseAdMobEvt.addListener('interstitial_event', this._onInterstitialEvent.bind(this)); + } + + _onInterstitialEvent(event) { + const { adunit } = event; + const jsEventType = `interstitial_${adunit}`; + + if (!this.hasListeners(jsEventType)) { + // TODO + } + + this.emit(jsEventType, event); + } + + interstitial(adUnit: string) { + return new Interstitial(this, adUnit); + } + + static get statics() { + return statics; + } +} + +export const statics = { + Banner, + AdRequest, + EventTypes: { + onAdLoaded: 'onAdLoaded', + onAdOpened: 'onAdOpened', + onAdLeftApplication: 'onAdLeftApplication', + onAdClosed: 'onAdClosed', + onAdFailedToLoad: 'onAdFailedToLoad', + }, + RewardedEventTypes: { + onRewarded: 'onRewarded', + onRewardedVideoAdLeftApplication: 'onRewardedVideoAdLeftApplication', + onRewardedVideoAdClosed: 'onRewardedVideoAdClosed', + onRewardedVideoAdFailedToLoad: 'onRewardedVideoAdFailedToLoad', + onRewardedVideoAdLoaded: 'onRewardedVideoAdLoaded', + onRewardedVideoAdOpened: 'onRewardedVideoAdOpened', + onRewardedVideoStarted: 'onRewardedVideoStarted', + }, +}; diff --git a/lib/modules/base.js b/lib/modules/base.js index 8a44b896..e69c278b 100644 --- a/lib/modules/base.js +++ b/lib/modules/base.js @@ -1,25 +1,14 @@ /** * @flow */ -import { NativeModules, NativeEventEmitter } from 'react-native'; +import EventEmitter from 'EventEmitter'; import Log from '../utils/log'; -import EventEmitter from './../utils/eventEmitter'; - -const FirebaseModule = NativeModules.RNFirebase; -const FirebaseModuleEvt = new NativeEventEmitter(FirebaseModule); const logs = {}; +const SharedEventEmitter = new EventEmitter(); -type FirebaseOptions = {}; - -export class Base extends EventEmitter { - constructor(firebase: Object, options: FirebaseOptions = {}) { - super(); - this.firebase = firebase; - this.eventHandlers = {}; - this.options = Object.assign({}, firebase.options, options); - } +export class Base { /** * Return a namespaced instance of Log @@ -27,52 +16,53 @@ export class Base extends EventEmitter { */ get log(): Log { if (logs[this.namespace]) return logs[this.namespace]; - return logs[this.namespace] = new Log(this.namespace, this.firebase._debug); + + // todo grab log level from global config provider (still todo); + return logs[this.namespace] = new Log(this.namespace, '*'); } - /** - * app instance - **/ - get app(): Object { - return this.firebase.app; - } - - - /** - * Add a native module event subscription - * @param name - * @param handler - * @param nativeModule - * @returns {*} - * @private + /* + * Proxy functions to shared event emitter instance + * https://github.com/facebook/react-native/blob/master/Libraries/EventEmitter/EventEmitter.js */ - _on(name, handler, nativeModule) { - let _nativeModule = nativeModule; - if (!_nativeModule) { - _nativeModule = FirebaseModuleEvt; - } - - return this.eventHandlers[name] = _nativeModule.addListener(name, handler); + get sharedEventEmitter () { + return SharedEventEmitter; } - /** - * Remove a native module event subscription - * @param name - * @private - */ - _off(name): void { - const subscription = this.eventHandlers[name]; - if (!subscription) return; + get addListener() { + return SharedEventEmitter.addListener.bind(SharedEventEmitter); + } - subscription.remove(); - delete this.eventHandlers[name]; + get on() { + return SharedEventEmitter.addListener.bind(SharedEventEmitter); + } + + get emit() { + return SharedEventEmitter.emit.bind(SharedEventEmitter); + } + + get listeners() { + return SharedEventEmitter.listeners.bind(SharedEventEmitter); + } + + hasListeners(eventType: string): Boolean { + const subscriptions = SharedEventEmitter._subscriber.getSubscriptionsForType(eventType); + return subscriptions && subscriptions.length; + } + + get removeListener() { + return SharedEventEmitter.removeListener.bind(SharedEventEmitter); + } + + get removeAllListeners() { + return SharedEventEmitter.removeAllListeners.bind(SharedEventEmitter); } } export class ReferenceBase extends Base { - constructor(firebase: Object, path: string) { - super(firebase); + constructor(path: string) { + super(); this.path = path || '/'; } diff --git a/lib/utils/index.js b/lib/utils/index.js index dff4b02a..e32f5a55 100644 --- a/lib/utils/index.js +++ b/lib/utils/index.js @@ -326,3 +326,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; +} 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 ( + // + // + // + // ); } }