diff --git a/README.md b/README.md index f97eb555..18105476 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,21 @@ React Native Google Mobile Ads is built with three key principals in mind; - 📄 **Well documented** - full reference & installation documentation alongside detailed guides and FAQs +## Migrating to the New Architecture Status (backwards compatible) +This package can be used in both The Old and [The New Architecture](https://reactnative.dev/docs/the-new-architecture/landing-page). +When using The New Architecture, some legacy code will still be used though. See status below: + +- **iOS** + - Mobile Ads SDK Methods (Turbo Native Module) 🟢🟢🟢🟢 + - Banners (Fabric Native Component) 🟢🟢🟢🟢 + - Full Screen Ads (Turbo Native Module) ⚪⚪⚪⚪ + - User Messaging Platform (Turbo Native Module) ⚪⚪⚪⚪ +- **Android** + - Mobile Ads SDK Methods (Turbo Native Module) ⚪⚪⚪⚪ + - Banners (Fabric Native Component) ⚪⚪⚪⚪ + - Full Screen Ads (Turbo Native Module) ⚪⚪⚪⚪ + - User Messaging Platform (Turbo Native Module) ⚪⚪⚪⚪ + ## Documentation - [Installation](https://docs.page/invertase/react-native-google-mobile-ads) diff --git a/RNGoogleMobileAds.podspec b/RNGoogleMobileAds.podspec index 4fd3214b..b1266545 100644 --- a/RNGoogleMobileAds.podspec +++ b/RNGoogleMobileAds.podspec @@ -18,11 +18,11 @@ Pod::Spec.new do |s| s.source = { :git => "#{package["repository"]["url"]}.git", :tag => "v#{s.version}" } s.social_media_url = 'http://twitter.com/invertaseio' s.ios.deployment_target = "10.0" - s.source_files = 'ios/**/*.{h,m,swift}' + s.source_files = "ios/**/*.{h,m,mm,swift}" s.weak_frameworks = "AppTrackingTransparency" # React Native dependencies - s.dependency 'React-Core' + install_modules_dependencies(s) # Other dependencies if defined?($RNGoogleUmpSDKVersion) diff --git a/__tests__/banner.test.tsx b/__tests__/banner.test.tsx index 99be754c..6bc82168 100644 --- a/__tests__/banner.test.tsx +++ b/__tests__/banner.test.tsx @@ -26,4 +26,14 @@ describe('Google Mobile Ads Banner', function () { "BannerAd: 'size(s)' expected a valid BannerAdSize or custom size string.", ); }); + + it('throws if requestOptions is invalid.', function () { + let errorMsg; + try { + render(); + } catch (e) { + errorMsg = e.message; + } + expect(errorMsg).toEqual("BannerAd: 'options' expected an object value"); + }); }); diff --git a/__tests__/googleMobileAds.test.ts b/__tests__/googleMobileAds.test.ts index bbe7817d..235d97a4 100644 --- a/__tests__/googleMobileAds.test.ts +++ b/__tests__/googleMobileAds.test.ts @@ -1,4 +1,5 @@ import admob, { MaxAdContentRating } from '../src'; +import RNGoogleMobileAdsModule from '../src/NativeGoogleMobileAdsModule'; describe('Admob', function () { describe('setRequestConfiguration()', function () { @@ -61,6 +62,26 @@ describe('Admob', function () { }); describe('testDebugMenu', function () { + it('does call native initialize method', () => { + admob().initialize(); + expect(RNGoogleMobileAdsModule.initialize).toBeCalledTimes(1); + }); + + it('does call native setRequestConfiguration method', () => { + admob().setRequestConfiguration({ tagForChildDirectedTreatment: true }); + expect(RNGoogleMobileAdsModule.setRequestConfiguration).toBeCalledTimes(1); + }); + + it('does call native openAdInspector method', () => { + admob().openAdInspector(); + expect(RNGoogleMobileAdsModule.openAdInspector).toBeCalledTimes(1); + }); + + it('does call native openDebugMenu method', () => { + admob().openDebugMenu('12345'); + expect(RNGoogleMobileAdsModule.openDebugMenu).toBeCalledTimes(1); + }); + it('throws if adUnit is empty', function () { expect(() => { admob().openDebugMenu(''); diff --git a/android/build.gradle b/android/build.gradle index 761ed837..a393ca0f 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -41,6 +41,13 @@ def jsonMinSdk = packageJson['sdkVersions']['android']['minSdk'] def jsonTargetSdk = packageJson['sdkVersions']['android']['targetSdk'] def jsonCompileSdk = packageJson['sdkVersions']['android']['compileSdk'] def jsonBuildTools = packageJson['sdkVersions']['android']['buildTools'] +def isNewArchitectureEnabled() { + return project.hasProperty("newArchEnabled") && project.newArchEnabled == "true" +} + +if (isNewArchitectureEnabled()) { + apply plugin: "com.facebook.react" +} project.ext { set('react-native', [ @@ -100,6 +107,7 @@ android { appJSONGoogleMobileAdsOptimizeInitialization : appJSONGoogleMobileAdsOptimizeInitializationBool, appJSONGoogleMobileAdsOptimizeAdLoading : appJSONGoogleMobileAdsOptimizeAdLoadingBool ] + buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString() } lintOptions { disable 'GradleCompatible' diff --git a/android/src/main/java/io/invertase/googlemobileads/OnNativeEvent.kt b/android/src/main/java/io/invertase/googlemobileads/OnNativeEvent.kt new file mode 100644 index 00000000..33d722cc --- /dev/null +++ b/android/src/main/java/io/invertase/googlemobileads/OnNativeEvent.kt @@ -0,0 +1,21 @@ +package io.invertase.googlemobileads + +import com.facebook.react.bridge.WritableMap +import com.facebook.react.uimanager.events.Event + +class OnNativeEvent(viewId: Int, private val event: WritableMap) : Event(viewId) { + + override fun getEventName(): String { + return EVENT_NAME + } + + override fun getCoalescingKey(): Short = 0 + + override fun getEventData(): WritableMap? { + return event + } + + companion object { + const val EVENT_NAME = "topNative" + } +} \ No newline at end of file diff --git a/android/src/main/java/io/invertase/googlemobileads/ReactNativeGoogleMobileAdsBannerAdViewManager.java b/android/src/main/java/io/invertase/googlemobileads/ReactNativeGoogleMobileAdsBannerAdViewManager.java index 89e6a3a5..0a98025f 100644 --- a/android/src/main/java/io/invertase/googlemobileads/ReactNativeGoogleMobileAdsBannerAdViewManager.java +++ b/android/src/main/java/io/invertase/googlemobileads/ReactNativeGoogleMobileAdsBannerAdViewManager.java @@ -22,14 +22,14 @@ import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.ReactContext; import com.facebook.react.bridge.ReadableArray; -import com.facebook.react.bridge.ReadableMap; 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.UIManagerHelper; import com.facebook.react.uimanager.annotations.ReactProp; -import com.facebook.react.uimanager.events.RCTEventEmitter; +import com.facebook.react.uimanager.events.EventDispatcher; import com.facebook.react.views.view.ReactViewGroup; import com.google.android.gms.ads.AdListener; import com.google.android.gms.ads.AdRequest; @@ -40,12 +40,15 @@ import com.google.android.gms.ads.admanager.AdManagerAdView; import com.google.android.gms.ads.admanager.AppEventListener; import io.invertase.googlemobileads.common.ReactNativeAdView; +import io.invertase.googlemobileads.common.SharedUtils; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Objects; import javax.annotation.Nonnull; import javax.annotation.Nullable; +import org.json.JSONException; +import org.json.JSONObject; public class ReactNativeGoogleMobileAdsBannerAdViewManager extends SimpleViewManager { @@ -73,7 +76,7 @@ public ReactNativeAdView createViewInstance(@Nonnull ThemedReactContext themedRe @Override public Map getExportedCustomDirectEventTypeConstants() { MapBuilder.Builder builder = MapBuilder.builder(); - builder.put("onNativeEvent", MapBuilder.of("registrationName", "onNativeEvent")); + builder.put(OnNativeEvent.EVENT_NAME, MapBuilder.of("registrationName", "onNativeEvent")); return builder.build(); } @@ -104,9 +107,15 @@ public void setUnitId(ReactNativeAdView reactViewGroup, String value) { } @ReactProp(name = "request") - public void setRequest(ReactNativeAdView reactViewGroup, ReadableMap value) { - reactViewGroup.setRequest(ReactNativeGoogleMobileAdsCommon.buildAdRequest(value)); - reactViewGroup.setPropsChanged(true); + public void setRequest(ReactNativeAdView reactViewGroup, String value) { + try { + JSONObject jsonObject = new JSONObject(value); + WritableMap writableMap = SharedUtils.jsonObjectToWritableMap(jsonObject); + reactViewGroup.setRequest(ReactNativeGoogleMobileAdsCommon.buildAdRequest(writableMap)); + reactViewGroup.setPropsChanged(true); + } catch (JSONException e) { + e.printStackTrace(); + } } @ReactProp(name = "sizes") @@ -274,8 +283,11 @@ private void sendEvent(ReactNativeAdView reactViewGroup, String type, WritableMa event.merge(payload); } - ((ThemedReactContext) reactViewGroup.getContext()) - .getJSModule(RCTEventEmitter.class) - .receiveEvent(reactViewGroup.getId(), "onNativeEvent", event); + ThemedReactContext themedReactContext = ((ThemedReactContext) reactViewGroup.getContext()); + EventDispatcher eventDispatcher = + UIManagerHelper.getEventDispatcherForReactTag(themedReactContext, reactViewGroup.getId()); + if (eventDispatcher != null) { + eventDispatcher.dispatchEvent(new OnNativeEvent(reactViewGroup.getId(), event)); + } } } diff --git a/android/src/main/java/io/invertase/googlemobileads/common/SharedUtils.java b/android/src/main/java/io/invertase/googlemobileads/common/SharedUtils.java index 7b5db7de..59fd0490 100644 --- a/android/src/main/java/io/invertase/googlemobileads/common/SharedUtils.java +++ b/android/src/main/java/io/invertase/googlemobileads/common/SharedUtils.java @@ -25,6 +25,7 @@ import android.os.Build; import android.util.Log; import com.facebook.react.bridge.*; +import com.facebook.react.bridge.WritableNativeMap; import com.facebook.react.common.LifecycleState; import com.facebook.react.modules.core.DeviceEventManagerModule; import java.io.File; @@ -239,28 +240,29 @@ public static Boolean hasPackageClass(String packageName, String className) { } public static WritableMap jsonObjectToWritableMap(JSONObject jsonObject) throws JSONException { - Iterator iterator = jsonObject.keys(); - WritableMap writableMap = Arguments.createMap(); + WritableMap map = new WritableNativeMap(); + Iterator iterator = jsonObject.keys(); while (iterator.hasNext()) { String key = iterator.next(); Object value = jsonObject.get(key); - if (value instanceof Float || value instanceof Double) { - writableMap.putDouble(key, jsonObject.getDouble(key)); - } else if (value instanceof Number) { - writableMap.putInt(key, jsonObject.getInt(key)); - } else if (value instanceof String) { - writableMap.putString(key, jsonObject.getString(key)); - } else if (value instanceof JSONObject) { - writableMap.putMap(key, jsonObjectToWritableMap(jsonObject.getJSONObject(key))); + if (value instanceof JSONObject) { + map.putMap(key, jsonObjectToWritableMap((JSONObject) value)); } else if (value instanceof JSONArray) { - writableMap.putArray(key, jsonArrayToWritableArray(jsonObject.getJSONArray(key))); - } else if (value == JSONObject.NULL) { - writableMap.putNull(key); + map.putArray(key, jsonArrayToWritableArray((JSONArray) value)); + } else if (value instanceof Boolean) { + map.putBoolean(key, (Boolean) value); + } else if (value instanceof Integer) { + map.putInt(key, (Integer) value); + } else if (value instanceof Double) { + map.putDouble(key, (Double) value); + } else if (value instanceof String) { + map.putString(key, (String) value); + } else { + map.putString(key, value.toString()); } } - - return writableMap; + return map; } public static WritableArray jsonArrayToWritableArray(JSONArray jsonArray) throws JSONException { diff --git a/ios/RNGoogleMobileAds/RNGoogleMobileAdsBannerComponent.m b/ios/RNGoogleMobileAds/RNGoogleMobileAdsBannerComponent.m index 60a029be..e611e251 100644 --- a/ios/RNGoogleMobileAds/RNGoogleMobileAdsBannerComponent.m +++ b/ios/RNGoogleMobileAds/RNGoogleMobileAdsBannerComponent.m @@ -66,8 +66,13 @@ - (void)setSizes:(NSArray *)sizes { _propsChanged = true; } -- (void)setRequest:(NSDictionary *)request { - _request = request; +- (void)setRequest:(NSString *)request { + NSData *jsonData = [request dataUsingEncoding:NSUTF8StringEncoding]; + NSError *error = nil; + _request = [NSJSONSerialization JSONObjectWithData:jsonData options:kNilOptions error:&error]; + if (error) { + NSLog(@"Error parsing JSON: %@", error.localizedDescription); + } _propsChanged = true; } diff --git a/ios/RNGoogleMobileAds/RNGoogleMobileAdsBannerView.h b/ios/RNGoogleMobileAds/RNGoogleMobileAdsBannerView.h new file mode 100644 index 00000000..a8c55b8c --- /dev/null +++ b/ios/RNGoogleMobileAds/RNGoogleMobileAdsBannerView.h @@ -0,0 +1,30 @@ +// This guard prevent this file to be compiled in the old architecture. +#ifdef RCT_NEW_ARCH_ENABLED +#import +#import +#import +#import +#import + +#ifndef NativeComponentExampleComponentView_h +#define NativeComponentExampleComponentView_h + +NS_ASSUME_NONNULL_BEGIN + +@interface RNGoogleMobileAdsBannerView + : RCTViewComponentView + +@property GADBannerView *banner; +@property(nonatomic, assign) BOOL requested; + +@property(nonatomic, copy) NSArray *sizes; +@property(nonatomic, copy) NSString *unitId; +@property(nonatomic, copy) NSDictionary *request; +@property(nonatomic, copy) NSNumber *manualImpressionsEnabled; + +@end + +NS_ASSUME_NONNULL_END + +#endif /* NativeComponentExampleComponentView_h */ +#endif /* RCT_NEW_ARCH_ENABLED */ diff --git a/ios/RNGoogleMobileAds/RNGoogleMobileAdsBannerView.mm b/ios/RNGoogleMobileAds/RNGoogleMobileAdsBannerView.mm new file mode 100644 index 00000000..cd0234c7 --- /dev/null +++ b/ios/RNGoogleMobileAds/RNGoogleMobileAdsBannerView.mm @@ -0,0 +1,220 @@ +// This guard prevent the code from being compiled in the old architecture +#ifdef RCT_NEW_ARCH_ENABLED +#import "RNGoogleMobileAdsBannerView.h" +#import "RNGoogleMobileAdsCommon.h" + +#import +#import +#import +#import + +#import "RCTFabricComponentsPlugins.h" + +using namespace facebook::react; + +@interface RNGoogleMobileAdsBannerView () + +@end + +@implementation RNGoogleMobileAdsBannerView + ++ (ComponentDescriptorProvider)componentDescriptorProvider { + return concreteComponentDescriptorProvider(); +} + +- (instancetype)initWithFrame:(CGRect)frame { + if (self = [super initWithFrame:frame]) { + static const auto defaultProps = std::make_shared(); + _props = defaultProps; + } + + return self; +} + +- (void)prepareForRecycle { + [super prepareForRecycle]; + static const auto defaultProps = std::make_shared(); + _props = defaultProps; +} + +- (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps { + const auto &oldViewProps = + *std::static_pointer_cast(_props); + const auto &newViewProps = + *std::static_pointer_cast(props); + + BOOL propsChanged = false; + + if (oldViewProps.unitId != newViewProps.unitId) { + _unitId = [[NSString alloc] initWithUTF8String:newViewProps.unitId.c_str()]; + propsChanged = true; + } + + if (oldViewProps.sizes != newViewProps.sizes) { + NSMutableArray *adSizes = [NSMutableArray arrayWithCapacity:newViewProps.sizes.size()]; + for (auto i = 0; i < newViewProps.sizes.size(); i++) { + NSString *jsonValue = [[NSString alloc] initWithUTF8String:newViewProps.sizes[i].c_str()]; + GADAdSize adSize = [RNGoogleMobileAdsCommon stringToAdSize:jsonValue]; + if (GADAdSizeEqualToSize(adSize, GADAdSizeInvalid)) { + RCTLogWarn(@"Invalid adSize %@", jsonValue); + } else { + [adSizes addObject:NSValueFromGADAdSize(adSize)]; + } + } + _sizes = adSizes; + propsChanged = true; + } + + if (_request == nil) { + _request = [NSDictionary dictionary]; + } + if (oldViewProps.request != newViewProps.request) { + NSString *jsonString = [[NSString alloc] initWithUTF8String:newViewProps.request.c_str()]; + NSData *jsonData = [jsonString dataUsingEncoding:NSUTF8StringEncoding]; + NSError *error = nil; + _request = [NSJSONSerialization JSONObjectWithData:jsonData options:kNilOptions error:&error]; + if (error) { + NSLog(@"Error parsing JSON: %@", error.localizedDescription); + } + propsChanged = true; + } + + if (_manualImpressionsEnabled == nil) { + _manualImpressionsEnabled = [NSNumber numberWithBool:oldViewProps.manualImpressionsEnabled]; + } + if (oldViewProps.manualImpressionsEnabled != newViewProps.manualImpressionsEnabled) { + _manualImpressionsEnabled = [NSNumber numberWithBool:newViewProps.manualImpressionsEnabled]; + propsChanged = true; + } + + if (propsChanged) { + [self requestAd]; + } + + [super updateProps:props oldProps:oldProps]; +} + +#pragma mark - Methods + +- (void)initBanner:(GADAdSize)adSize { + if (_requested) { + [_banner removeFromSuperview]; + } + if ([RNGoogleMobileAdsCommon isAdManagerUnit:_unitId]) { + _banner = [[GAMBannerView alloc] initWithAdSize:adSize]; + + ((GAMBannerView *)_banner).validAdSizes = _sizes; + ((GAMBannerView *)_banner).appEventDelegate = self; + ((GAMBannerView *)_banner).enableManualImpressions = [_manualImpressionsEnabled boolValue]; + } else { + _banner = [[GADBannerView alloc] initWithAdSize:adSize]; + } + _banner.rootViewController = [UIApplication sharedApplication].delegate.window.rootViewController; + _banner.delegate = self; +} + +- (void)requestAd { +#ifndef __LP64__ + return; // prevent crash on 32bit +#endif + + if (_unitId == nil || _sizes == nil || _request == nil || _manualImpressionsEnabled == nil) { + [self setRequested:NO]; + return; + } else { + [self initBanner:GADAdSizeFromNSValue(_sizes[0])]; + [self addSubview:_banner]; + _banner.adUnitID = _unitId; + [self setRequested:YES]; + [_banner loadRequest:[RNGoogleMobileAdsCommon buildAdRequest:_request]]; + if (_eventEmitter != nullptr) { + std::dynamic_pointer_cast( + _eventEmitter) + ->onNativeEvent(facebook::react::RNGoogleMobileAdsBannerViewEventEmitter::OnNativeEvent{ + .type = "onSizeChange", + .width = _banner.bounds.size.width, + .height = _banner.bounds.size.height}); + } + } +} + +- (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args { + if ([commandName isEqual:@"recordManualImpression"]) { + [self recordManualImpression]; + } +} + +- (void)recordManualImpression { + if ([_banner class] == [GAMBannerView class]) { + [((GAMBannerView *)_banner) recordImpression]; + } +} + +#pragma mark - Events + +- (void)bannerViewDidReceiveAd:(GADBannerView *)bannerView { + if (_eventEmitter != nullptr) { + std::dynamic_pointer_cast( + _eventEmitter) + ->onNativeEvent(facebook::react::RNGoogleMobileAdsBannerViewEventEmitter::OnNativeEvent{ + .type = "onAdLoaded", + .width = bannerView.bounds.size.width, + .height = bannerView.bounds.size.height}); + } +} + +- (void)bannerView:(GADBannerView *)bannerView didFailToReceiveAdWithError:(NSError *)error { + NSDictionary *errorAndMessage = [RNGoogleMobileAdsCommon getCodeAndMessageFromAdError:error]; + if (_eventEmitter != nullptr) { + std::dynamic_pointer_cast( + _eventEmitter) + ->onNativeEvent(facebook::react::RNGoogleMobileAdsBannerViewEventEmitter::OnNativeEvent{ + .type = "onAdFailedToLoad", + .code = std::string([[errorAndMessage valueForKey:@"code"] UTF8String]), + .message = std::string([[errorAndMessage valueForKey:@"message"] UTF8String])}); + } +} + +- (void)bannerViewWillPresentScreen:(GADBannerView *)bannerView { + if (_eventEmitter != nullptr) { + std::dynamic_pointer_cast( + _eventEmitter) + ->onNativeEvent(facebook::react::RNGoogleMobileAdsBannerViewEventEmitter::OnNativeEvent{ + .type = "onAdOpened"}); + } +} + +- (void)bannerViewWillDismissScreen:(GADBannerView *)bannerView { + // not in use +} + +- (void)bannerViewDidDismissScreen:(GADBannerView *)bannerView { + if (_eventEmitter != nullptr) { + std::dynamic_pointer_cast( + _eventEmitter) + ->onNativeEvent(facebook::react::RNGoogleMobileAdsBannerViewEventEmitter::OnNativeEvent{ + .type = "onAdClosed"}); + } +} + +- (void)adView:(nonnull GADBannerView *)banner + didReceiveAppEvent:(nonnull NSString *)name + withInfo:(nullable NSString *)info { + if (_eventEmitter != nullptr) { + std::dynamic_pointer_cast( + _eventEmitter) + ->onNativeEvent(facebook::react::RNGoogleMobileAdsBannerViewEventEmitter::OnNativeEvent{ + .type = "onAppEvent", + .name = std::string([name UTF8String]), + .data = std::string([info UTF8String])}); + } +} + +#pragma mark - RNGoogleMobileAdsBannerViewCls + +Class RNGoogleMobileAdsBannerViewCls(void) { + return RNGoogleMobileAdsBannerView.class; +} + +@end +#endif diff --git a/ios/RNGoogleMobileAds/RNGoogleMobileAdsBannerViewManager.h b/ios/RNGoogleMobileAds/RNGoogleMobileAdsBannerViewManager.h deleted file mode 100644 index 489efbc6..00000000 --- a/ios/RNGoogleMobileAds/RNGoogleMobileAdsBannerViewManager.h +++ /dev/null @@ -1,23 +0,0 @@ -// -/** - * Copyright (c) 2016-present Invertase Limited & Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this library except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -#import - -@interface RNGoogleMobileAdsBannerViewManager : RCTViewManager - -@end diff --git a/ios/RNGoogleMobileAds/RNGoogleMobileAdsBannerViewManager.m b/ios/RNGoogleMobileAds/RNGoogleMobileAdsBannerViewManager.mm similarity index 89% rename from ios/RNGoogleMobileAds/RNGoogleMobileAdsBannerViewManager.m rename to ios/RNGoogleMobileAds/RNGoogleMobileAdsBannerViewManager.mm index bbaf8921..3d3f8c45 100644 --- a/ios/RNGoogleMobileAds/RNGoogleMobileAdsBannerViewManager.m +++ b/ios/RNGoogleMobileAds/RNGoogleMobileAdsBannerViewManager.mm @@ -16,9 +16,16 @@ * */ -#import "RNGoogleMobileAdsBannerViewManager.h" #import +#import +#ifdef RCT_NEW_ARCH_ENABLE + +#else #import "RNGoogleMobileAdsBannerComponent.h" +#endif + +@interface RNGoogleMobileAdsBannerViewManager : RCTViewManager +@end @implementation RNGoogleMobileAdsBannerViewManager @@ -28,7 +35,7 @@ @implementation RNGoogleMobileAdsBannerViewManager RCT_EXPORT_VIEW_PROPERTY(unitId, NSString); -RCT_EXPORT_VIEW_PROPERTY(request, NSDictionary); +RCT_EXPORT_VIEW_PROPERTY(request, NSString); RCT_EXPORT_VIEW_PROPERTY(manualImpressionsEnabled, BOOL); @@ -48,6 +55,9 @@ @implementation RNGoogleMobileAdsBannerViewManager #endif } +#ifdef RCT_NEW_ARCH_ENABLE + +#else @synthesize bridge = _bridge; - (UIView *)view { @@ -62,5 +72,6 @@ - (UIView *)view { - (dispatch_queue_t)methodQueue { return dispatch_get_main_queue(); } +#endif @end diff --git a/ios/RNGoogleMobileAds/RNGoogleMobileAdsCommon.h b/ios/RNGoogleMobileAds/RNGoogleMobileAdsCommon.h index 5c47ca34..2ee019aa 100644 --- a/ios/RNGoogleMobileAds/RNGoogleMobileAdsCommon.h +++ b/ios/RNGoogleMobileAds/RNGoogleMobileAdsCommon.h @@ -18,8 +18,8 @@ #if !TARGET_OS_MACCATALYST +#import #import -@import GoogleMobileAds; @interface RNGoogleMobileAdsCommon : NSObject diff --git a/ios/RNGoogleMobileAds/RNGoogleMobileAdsCommon.m b/ios/RNGoogleMobileAds/RNGoogleMobileAdsCommon.mm similarity index 100% rename from ios/RNGoogleMobileAds/RNGoogleMobileAdsCommon.m rename to ios/RNGoogleMobileAds/RNGoogleMobileAdsCommon.mm diff --git a/ios/RNGoogleMobileAds/RNGoogleMobileAdsModule.h b/ios/RNGoogleMobileAds/RNGoogleMobileAdsModule.h index 13c1c1df..5aeb0b3b 100644 --- a/ios/RNGoogleMobileAds/RNGoogleMobileAdsModule.h +++ b/ios/RNGoogleMobileAds/RNGoogleMobileAdsModule.h @@ -17,8 +17,16 @@ #import -#import +#ifdef RCT_NEW_ARCH_ENABLED + +#import +@interface RNGoogleMobileAdsModule : NSObject + +#else +#import @interface RNGoogleMobileAdsModule : NSObject +#endif + @end diff --git a/ios/RNGoogleMobileAds/RNGoogleMobileAdsModule.m b/ios/RNGoogleMobileAds/RNGoogleMobileAdsModule.mm similarity index 86% rename from ios/RNGoogleMobileAds/RNGoogleMobileAdsModule.m rename to ios/RNGoogleMobileAds/RNGoogleMobileAdsModule.mm index d4941d15..a2f47e4d 100644 --- a/ios/RNGoogleMobileAds/RNGoogleMobileAdsModule.m +++ b/ios/RNGoogleMobileAds/RNGoogleMobileAdsModule.mm @@ -21,6 +21,9 @@ #import #import "RNGoogleMobileAdsModule.h" +#ifdef RCT_NEW_ARCH_ENABLED +#import "RNGoogleMobileAdsSpec.h" +#endif #import "common/RNSharedUtils.h" @implementation RNGoogleMobileAdsModule @@ -37,6 +40,41 @@ - (dispatch_queue_t)methodQueue { #pragma mark Google Mobile Ads Methods RCT_EXPORT_METHOD(initialize : (RCTPromiseResolveBlock)resolve : (RCTPromiseRejectBlock)reject) { + [self initialize:resolve reject:reject]; +} + +RCT_EXPORT_METHOD(setRequestConfiguration + : (NSDictionary *)requestConfiguration + : (RCTPromiseResolveBlock)resolve + : (RCTPromiseRejectBlock)reject) { + [self setRequestConfiguration:requestConfiguration resolve:resolve reject:reject]; +} + +RCT_EXPORT_METHOD(openAdInspector + : (RCTPromiseResolveBlock)resolve + : (RCTPromiseRejectBlock)reject) { + [self openAdInspector:resolve reject:reject]; +} + +RCT_EXPORT_METHOD(openDebugMenu : (NSString *)adUnit) { +#if !TARGET_OS_MACCATALYST + GADDebugOptionsViewController *debugOptionsViewController = + [GADDebugOptionsViewController debugOptionsViewControllerWithAdUnitID:adUnit]; + [RCTSharedApplication().delegate.window.rootViewController + presentViewController:debugOptionsViewController + animated:YES + completion:nil]; +#endif +} + +#ifdef RCT_NEW_ARCH_ENABLED +- (std::shared_ptr)getTurboModule: + (const facebook::react::ObjCTurboModule::InitParams &)params { + return std::make_shared(params); +} +#endif + +- (void)initialize:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { #if !TARGET_OS_MACCATALYST [[GADMobileAds sharedInstance] startWithCompletionHandler:^(GADInitializationStatus *_Nonnull status) { @@ -56,15 +94,9 @@ - (dispatch_queue_t)methodQueue { #endif } -RCT_EXPORT_METHOD(setRequestConfiguration - : (NSDictionary *)requestConfiguration - : (RCTPromiseResolveBlock)resolve - : (RCTPromiseRejectBlock)reject) { - [self setRequestConfiguration:requestConfiguration]; - resolve([NSNull null]); -} - -- (void)setRequestConfiguration:(NSDictionary *)requestConfiguration { +- (void)setRequestConfiguration:(NSDictionary *)requestConfiguration + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject { #if !TARGET_OS_MACCATALYST if (requestConfiguration[@"maxAdContentRating"]) { NSString *rating = requestConfiguration[@"maxAdContentRating"]; @@ -104,12 +136,12 @@ - (void)setRequestConfiguration:(NSDictionary *)requestConfiguration { } GADMobileAds.sharedInstance.requestConfiguration.testDeviceIdentifiers = devices; } + + resolve([NSNull null]); #endif } -RCT_EXPORT_METHOD(openAdInspector - : (RCTPromiseResolveBlock)resolve - : (RCTPromiseRejectBlock)reject) { +- (void)openAdInspector:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { #if !TARGET_OS_MACCATALYST [GADMobileAds.sharedInstance presentAdInspectorFromViewController:RCTSharedApplication().delegate.window.rootViewController @@ -129,15 +161,4 @@ - (void)setRequestConfiguration:(NSDictionary *)requestConfiguration { #endif } -RCT_EXPORT_METHOD(openDebugMenu : (NSString *)adUnit) { -#if !TARGET_OS_MACCATALYST - GADDebugOptionsViewController *debugOptionsViewController = - [GADDebugOptionsViewController debugOptionsViewControllerWithAdUnitID:adUnit]; - [RCTSharedApplication().delegate.window.rootViewController - presentViewController:debugOptionsViewController - animated:YES - completion:nil]; -#endif -} - @end diff --git a/jest.setup.ts b/jest.setup.ts index 7c821350..3f3a2896 100644 --- a/jest.setup.ts +++ b/jest.setup.ts @@ -27,6 +27,16 @@ jest.doMock('react-native', () => { RNGoogleMobileAdsRewardedModule: {}, RNGoogleMobileAdsConsentModule: {}, }, + TurboModuleRegistry: { + getEnforcing: () => { + return { + initialize: jest.fn(), + setRequestConfiguration: jest.fn(), + openAdInspector: jest.fn(), + openDebugMenu: jest.fn(), + }; + }, + }, }, ReactNative, ); diff --git a/package.json b/package.json index 37ddb42e..6a084b68 100644 --- a/package.json +++ b/package.json @@ -153,5 +153,13 @@ }, "publishConfig": { "access": "public" + }, + "codegenConfig": { + "name": "RNGoogleMobileAdsSpec", + "type": "all", + "jsSrcsDir": "./src", + "android": { + "javaPackageName": "io.invertase.googlemobileads" + } } } diff --git a/src/MobileAds.ts b/src/MobileAds.ts index 6730929b..318d2509 100644 --- a/src/MobileAds.ts +++ b/src/MobileAds.ts @@ -1,13 +1,10 @@ -import { NativeModules } from 'react-native'; - +import RNGoogleMobileAdsModule from './NativeGoogleMobileAdsModule'; import { validateAdRequestConfiguration } from './validateAdRequestConfiguration'; import { SharedEventEmitter } from './internal/SharedEventEmitter'; import { GoogleMobileAdsNativeEventEmitter } from './internal/GoogleMobileAdsNativeEventEmitter'; import { MobileAdsModuleInterface } from './types/MobileAdsModule.interface'; import { RequestConfiguration } from './types/RequestConfiguration'; -const { RNGoogleMobileAdsModule } = NativeModules; - const NATIVE_MODULE_EVENT_SUBSCRIPTIONS: Record = {}; const nativeEvents = [ diff --git a/src/NativeGoogleMobileAdsModule.ts b/src/NativeGoogleMobileAdsModule.ts new file mode 100644 index 00000000..a5308222 --- /dev/null +++ b/src/NativeGoogleMobileAdsModule.ts @@ -0,0 +1,14 @@ +import type { TurboModule } from 'react-native'; +import { TurboModuleRegistry } from 'react-native'; +import { UnsafeObject } from 'react-native/Libraries/Types/CodegenTypes'; + +import { AdapterStatus } from './types'; + +export interface Spec extends TurboModule { + initialize(): Promise; + setRequestConfiguration(requestConfiguration?: UnsafeObject): Promise; + openAdInspector(): Promise; + openDebugMenu(adUnit: string): void; +} + +export default TurboModuleRegistry.getEnforcing('RNGoogleMobileAdsModule'); diff --git a/src/ads/BaseAd.tsx b/src/ads/BaseAd.tsx index 20352548..ea8cdf72 100644 --- a/src/ads/BaseAd.tsx +++ b/src/ads/BaseAd.tsx @@ -17,142 +17,127 @@ */ import React, { useState, useEffect } from 'react'; -import { NativeMethods, requireNativeComponent } from 'react-native'; +import { NativeSyntheticEvent } from 'react-native'; import { isFunction } from '../common'; import { NativeError } from '../internal/NativeError'; +import GoogleMobileAdsBannerView from './GoogleMobileAdsBannerViewNativeComponent'; +import type { NativeEvent } from './GoogleMobileAdsBannerViewNativeComponent'; import { BannerAdSize, GAMBannerAdSize } from '../BannerAdSize'; import { validateAdRequestOptions } from '../validateAdRequestOptions'; import { GAMBannerAdProps } from '../types/BannerAdProps'; -import { RequestOptions } from '../types/RequestOptions'; - -type NativeEvent = - | { - type: 'onAdLoaded' | 'onSizeChange'; - width: number; - height: number; - } - | { type: 'onAdOpened' | 'onAdClosed' } - | { - type: 'onAdFailedToLoad'; - code: string; - message: string; - } - | { - type: 'onAppEvent'; - name: string; - data?: string; - }; const sizeRegex = /([0-9]+)x([0-9]+)/; -export const BaseAd = React.forwardRef( - ({ unitId, sizes, requestOptions, manualImpressionsEnabled, ...props }, ref) => { - const [dimensions, setDimensions] = useState<(number | string)[]>([0, 0]); +export const BaseAd = React.forwardRef< + React.ElementRef, + GAMBannerAdProps +>(({ unitId, sizes, requestOptions, manualImpressionsEnabled, ...props }, ref) => { + const [dimensions, setDimensions] = useState<(number | string)[]>([0, 0]); - useEffect(() => { - if (!unitId) { - throw new Error("BannerAd: 'unitId' expected a valid string unit ID."); - } - }, [unitId]); + useEffect(() => { + if (!unitId) { + throw new Error("BannerAd: 'unitId' expected a valid string unit ID."); + } + }, [unitId]); - useEffect(() => { - if ( - sizes.length === 0 || - !sizes.every( - size => size in BannerAdSize || size in GAMBannerAdSize || sizeRegex.test(size), - ) - ) { - throw new Error("BannerAd: 'size(s)' expected a valid BannerAdSize or custom size string."); - } - }, [sizes]); + useEffect(() => { + if ( + sizes.length === 0 || + !sizes.every(size => size in BannerAdSize || size in GAMBannerAdSize || sizeRegex.test(size)) + ) { + throw new Error("BannerAd: 'size(s)' expected a valid BannerAdSize or custom size string."); + } + }, [sizes]); - const parsedRequestOptions = JSON.stringify(requestOptions); + const parsedRequestOptions = JSON.stringify(requestOptions); - useEffect(() => { - if (requestOptions) { - try { - validateAdRequestOptions(requestOptions); - } catch (e) { - if (e instanceof Error) { - throw new Error(`BannerAd: ${e.message}`); - } + useEffect(() => { + if (requestOptions) { + try { + validateAdRequestOptions(requestOptions); + } catch (e) { + if (e instanceof Error) { + throw new Error(`BannerAd: ${e.message}`); } } - }, [parsedRequestOptions]); - - function onNativeEvent({ nativeEvent }: { nativeEvent: NativeEvent }) { - const { type } = nativeEvent; + } + }, [parsedRequestOptions]); - if (type !== 'onSizeChange' && isFunction(props[type])) { - let eventHandler, eventPayload; - switch (type) { - case 'onAdLoaded': - eventPayload = { - width: nativeEvent.width, - height: nativeEvent.height, - }; - if ((eventHandler = props[type])) eventHandler(eventPayload); - break; - case 'onAdFailedToLoad': - eventPayload = NativeError.fromEvent(nativeEvent, 'googleMobileAds'); - if ((eventHandler = props[type])) eventHandler(eventPayload); - break; - case 'onAppEvent': - eventPayload = { - name: nativeEvent.name, - data: nativeEvent.data, - }; - if ((eventHandler = props[type])) eventHandler(eventPayload); - break; - default: - if ((eventHandler = props[type])) eventHandler(); + function onNativeEvent(event: NativeSyntheticEvent) { + const nativeEvent = event.nativeEvent as + | { + type: 'onAdLoaded' | 'onSizeChange'; + width: number; + height: number; } + | { type: 'onAdOpened' | 'onAdClosed' } + | { + type: 'onAdFailedToLoad'; + code: string; + message: string; + } + | { + type: 'onAppEvent'; + name: string; + data?: string; + }; + const { type } = nativeEvent; + + if (type !== 'onSizeChange' && isFunction(props[type])) { + let eventHandler, eventPayload; + switch (type) { + case 'onAdLoaded': + eventPayload = { + width: nativeEvent.width, + height: nativeEvent.height, + }; + if ((eventHandler = props[type])) eventHandler(eventPayload); + break; + case 'onAdFailedToLoad': + eventPayload = NativeError.fromEvent(nativeEvent, 'googleMobileAds'); + if ((eventHandler = props[type])) eventHandler(eventPayload); + break; + case 'onAppEvent': + eventPayload = { + name: nativeEvent.name, + data: nativeEvent.data, + }; + if ((eventHandler = props[type])) eventHandler(eventPayload); + break; + default: + if ((eventHandler = props[type])) eventHandler(); } + } - if (type === 'onAdLoaded' || type === 'onSizeChange') { - const { width, height } = nativeEvent; - if (width && height) setDimensions([width, height]); + if (type === 'onAdLoaded' || type === 'onSizeChange') { + const width = Math.ceil(nativeEvent.width); + const height = Math.ceil(nativeEvent.height); + if (width && height && JSON.stringify([width, height]) !== JSON.stringify(dimensions)) { + setDimensions([width, height]); } } + } - const style = sizes.includes(GAMBannerAdSize.FLUID) - ? { - width: '100%', - height: dimensions[1], - } - : { - width: dimensions[0], - height: dimensions[1], - }; + const style = sizes.includes(GAMBannerAdSize.FLUID) + ? { + width: '100%', + height: dimensions[1], + } + : { + width: dimensions[0], + height: dimensions[1], + }; - return ( - - ); - }, -); + return ( + + ); +}); BaseAd.displayName = 'BaseAd'; - -interface NativeBannerProps { - sizes: GAMBannerAdProps['sizes']; - style: { - width?: number | string; - height?: number | string; - }; - unitId: string; - request: RequestOptions; - manualImpressionsEnabled: boolean; - onNativeEvent: (event: { nativeEvent: NativeEvent }) => void; -} - -const GoogleMobileAdsBannerView = requireNativeComponent( - 'RNGoogleMobileAdsBannerView', -); -export type GoogleMobileAdsBannerView = React.Component & NativeMethods; diff --git a/src/ads/GAMBannerAd.tsx b/src/ads/GAMBannerAd.tsx index 2cc6e490..d8a0214f 100644 --- a/src/ads/GAMBannerAd.tsx +++ b/src/ads/GAMBannerAd.tsx @@ -16,20 +16,17 @@ */ import React, { createRef } from 'react'; -import { findNodeHandle, Platform, UIManager } from 'react-native'; import { GAMBannerAdProps } from '../types/BannerAdProps'; -import { BaseAd, GoogleMobileAdsBannerView } from './BaseAd'; +import { BaseAd } from './BaseAd'; +import GoogleMobileAdsBannerView, { Commands } from './GoogleMobileAdsBannerViewNativeComponent'; export class GAMBannerAd extends React.Component { - private ref = createRef(); + private ref = createRef>(); recordManualImpression() { - let commandID: string | number = UIManager.getViewManagerConfig('RNGoogleMobileAdsBannerView') - .Commands.recordManualImpression; - if (Platform.OS === 'android') { - commandID = commandID.toString(); + if (this.ref.current) { + Commands.recordManualImpression(this.ref.current); } - UIManager.dispatchViewManagerCommand(findNodeHandle(this.ref.current), commandID, undefined); } render() { diff --git a/src/ads/GoogleMobileAdsBannerViewNativeComponent.ts b/src/ads/GoogleMobileAdsBannerViewNativeComponent.ts new file mode 100644 index 00000000..de0ddeb8 --- /dev/null +++ b/src/ads/GoogleMobileAdsBannerViewNativeComponent.ts @@ -0,0 +1,37 @@ +import type * as React from 'react'; +import type { HostComponent, ViewProps } from 'react-native'; +import type { BubblingEventHandler, Float } from 'react-native/Libraries/Types/CodegenTypes'; +import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent'; +import codegenNativeCommands from 'react-native/Libraries/Utilities/codegenNativeCommands'; + +export type NativeEvent = { + type: string; + width?: Float; + height?: Float; + code?: string; + message?: string; + name?: string; + data?: string; +}; + +export interface NativeProps extends ViewProps { + sizes: string[]; + unitId: string; + request: string; + manualImpressionsEnabled: boolean; + onNativeEvent: BubblingEventHandler; +} + +export type ComponentType = HostComponent; + +interface NativeCommands { + recordManualImpression: (viewRef: React.ElementRef) => void; +} + +export const Commands: NativeCommands = codegenNativeCommands({ + supportedCommands: ['recordManualImpression'], +}); + +export default codegenNativeComponent( + 'RNGoogleMobileAdsBannerView', +) as HostComponent; diff --git a/src/types/GoogleMobileAdsNativeModule.ts b/src/types/GoogleMobileAdsNativeModule.ts deleted file mode 100644 index da12711f..00000000 --- a/src/types/GoogleMobileAdsNativeModule.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { AdapterStatus } from './AdapterStatus'; -import { RequestConfiguration } from './RequestConfiguration'; - -export interface GoogleMobileAdsNativeModule { - initialize(): Promise; - setRequestConfiguration(requestConfiguration?: RequestConfiguration): Promise; - openAdInspector(): Promise; - openDebugMenu(adUnit: string): void; -}