diff --git a/packages/firebase_admob/README.md b/packages/firebase_admob/README.md index c7db26805adc..276c33e4b727 100644 --- a/packages/firebase_admob/README.md +++ b/packages/firebase_admob/README.md @@ -195,9 +195,10 @@ Since Native Ads require UI components native to a platform, this feature requir for Android and iOS: ### Android -The Android Admob Plugin requires a class that implements `NativeAdFactory` which implements `createNativeAd( -[UnifiedNativeAd](https://developers.google.com/android/reference/com/google/android/gms/ads/formats/UnifiedNativeAd) nativeAd, -Map customOptions)` and returns a +The Android Admob Plugin requires a class that implements `NativeAdFactory` which contains a method +that takes a +[UnifiedNativeAd](https://developers.google.com/android/reference/com/google/android/gms/ads/formats/UnifiedNativeAd) +and custom options and returns a [UnifiedNativeAdView](https://developers.google.com/android/reference/com/google/android/gms/ads/formats/UnifiedNativeAdView). You can implement this in your `MainActivity.java` or create a separate class in the same directory @@ -285,8 +286,53 @@ An example of displaying a `UnifiedNativeAd` with a `UnifiedNativeAdView` can be a custom layout and displays the test Native ad. ### iOS +Native Ads for iOS require a class that implements the protocol `FLTNativeAdFactory` which has a +single method `createNativeAd:customOptions:`. -Currently unsupported. +You can have your `AppDelegate` implement this protocol or create a separate class as seen below: + +```objectivec +/* AppDelegate.m */ + +#import "FLTFirebaseAdMobPlugin.h" + +@interface NativeAdFactoryExample : NSObject +@end + +@implementation NativeAdFactoryExample +- (GADUnifiedNativeAdView *)createNativeAd:(GADUnifiedNativeAd *)nativeAd + customOptions:(NSDictionary *)customOptions { + // Create GADUnifiedNativeAdView +} +@end +``` + +Once there is an implementation of `FLTNativeAdFactory`, it must be added to the +`FLTFirebaseAdMobPlugin`. This is done by importing `FLTFirebaseAdMobPlugin.h` and calling +`registerNativeAdFactory:factoryId:nativeAdFactory:` with a `FlutterPluginRegistry`, a unique +identifier for the factory, and the factory itself. The factory also *MUST* be added after +`[GeneratedPluginRegistrant registerWithRegistry:self];` has been called. + +If this is done in `AppDelegate.m`, it should look similar to: + +```objectivec +#import "FLTFirebaseAdMobPlugin.h" + +@implementation AppDelegate +- (BOOL)application:(UIApplication *)application + didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + [GeneratedPluginRegistrant registerWithRegistry:self]; + + NativeAdFactoryExample *nativeAdFactory = [[NativeAdFactoryExample alloc] init]; + [FLTFirebaseAdMobPlugin registerNativeAdFactory:self + factoryId:@"adFactoryExample" + nativeAdFactory:nativeAdFactory]; + + return [super application:application didFinishLaunchingWithOptions:launchOptions]; +} + +@end +``` ### Dart Example diff --git a/packages/firebase_admob/android/src/main/java/io/flutter/plugins/firebaseadmob/FirebaseAdMobPlugin.java b/packages/firebase_admob/android/src/main/java/io/flutter/plugins/firebaseadmob/FirebaseAdMobPlugin.java index 1053cb952a5b..32f6fcb3aecd 100644 --- a/packages/firebase_admob/android/src/main/java/io/flutter/plugins/firebaseadmob/FirebaseAdMobPlugin.java +++ b/packages/firebase_admob/android/src/main/java/io/flutter/plugins/firebaseadmob/FirebaseAdMobPlugin.java @@ -92,8 +92,8 @@ public static void registerWith(Registrar registrar) { /** * Adds a {@link io.flutter.plugins.firebaseadmob.FirebaseAdMobPlugin.NativeAdFactory} used to - * create a {@link com.google.android.gms.ads.formats.UnifiedNativeAdView}s from a Native Ad - * created in Dart. + * create {@link com.google.android.gms.ads.formats.UnifiedNativeAdView}s from a Native Ad created + * in Dart. * * @param registry maintains access to a FirebaseAdMobPlugin instance. * @param factoryId a unique identifier for the ad factory. The Native Ad created in Dart includes @@ -110,7 +110,7 @@ public static boolean registerNativeAdFactory( /** * Registers a {@link io.flutter.plugins.firebaseadmob.FirebaseAdMobPlugin.NativeAdFactory} used - * to create a {@link com.google.android.gms.ads.formats.UnifiedNativeAdView}s from a Native Ad + * to create {@link com.google.android.gms.ads.formats.UnifiedNativeAdView}s from a Native Ad * created in Dart. * * @param engine maintains access to a FirebaseAdMobPlugin instance. @@ -142,7 +142,7 @@ private static boolean registerNativeAdFactory( /** * Unregisters a {@link io.flutter.plugins.firebaseadmob.FirebaseAdMobPlugin.NativeAdFactory} used - * to create a {@link com.google.android.gms.ads.formats.UnifiedNativeAdView}s from a Native Ad + * to create {@link com.google.android.gms.ads.formats.UnifiedNativeAdView}s from a Native Ad * created in Dart. * * @param registry maintains access to a FirebaseAdMobPlugin instance. @@ -162,7 +162,7 @@ public static NativeAdFactory unregisterNativeAdFactory( /** * Unregisters a {@link io.flutter.plugins.firebaseadmob.FirebaseAdMobPlugin.NativeAdFactory} used - * to create a {@link com.google.android.gms.ads.formats.UnifiedNativeAdView}s from a Native Ad + * to create {@link com.google.android.gms.ads.formats.UnifiedNativeAdView}s from a Native Ad * created in Dart. * * @param engine maintains access to a FirebaseAdMobPlugin instance. diff --git a/packages/firebase_admob/example/ios/Runner.xcodeproj/project.pbxproj b/packages/firebase_admob/example/ios/Runner.xcodeproj/project.pbxproj index e2d0ec0f0e29..4d5d8f5cc836 100644 --- a/packages/firebase_admob/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/firebase_admob/example/ios/Runner.xcodeproj/project.pbxproj @@ -8,13 +8,9 @@ /* Begin PBXBuildFile section */ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; - 2D5378261FAA1A9400D5DBA9 /* flutter_assets in Resources */ = {isa = PBXBuildFile; fileRef = 2D5378251FAA1A9400D5DBA9 /* flutter_assets */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; - 3B80C3941E831B6300D905FE /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; }; - 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 6E27B1151F0DAFA70028FD65 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 6E27B1141F0DAFA70028FD65 /* GoogleService-Info.plist */; }; - 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; }; - 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 8FC897F52411A9F100415930 /* UnifiedNativeAdView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 8FC897F42411A9F100415930 /* UnifiedNativeAdView.xib */; }; 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 9740EEB21CF90195004384FC /* Debug.xcconfig */; }; 9740EEB51CF90195004384FC /* Generated.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 9740EEB31CF90195004384FC /* Generated.xcconfig */; }; 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; @@ -32,8 +28,6 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( - 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */, - 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */, ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; @@ -43,23 +37,23 @@ /* Begin PBXFileReference section */ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; - 2D5378251FAA1A9400D5DBA9 /* flutter_assets */ = {isa = PBXFileReference; lastKnownFileType = folder; name = flutter_assets; path = Flutter/flutter_assets; sourceTree = SOURCE_ROOT; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; sourceTree = ""; }; 6E27B1141F0DAFA70028FD65 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; 83F369F228D3A43519CEE308 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 8FC897F42411A9F100415930 /* UnifiedNativeAdView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = UnifiedNativeAdView.xib; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; - 9740EEBA1CF902C7004384FC /* Flutter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Flutter.framework; path = Flutter/Flutter.framework; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; 97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + CE82265EF05E2A9632B25E60 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + F74051031B69F8124E38352E /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -67,8 +61,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */, - 3B80C3941E831B6300D905FE /* App.framework in Frameworks */, FBE669D215209F1F44CEEB21 /* libPods-Runner.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -79,6 +71,8 @@ 07A52D07C2C05D9527204891 /* Pods */ = { isa = PBXGroup; children = ( + CE82265EF05E2A9632B25E60 /* Pods-Runner.debug.xcconfig */, + F74051031B69F8124E38352E /* Pods-Runner.release.xcconfig */, ); name = Pods; sourceTree = ""; @@ -94,10 +88,7 @@ 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( - 3B80C3931E831B6300D905FE /* App.framework */, 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, - 2D5378251FAA1A9400D5DBA9 /* flutter_assets */, - 9740EEBA1CF902C7004384FC /* Flutter.framework */, 9740EEB21CF90195004384FC /* Debug.xcconfig */, 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 9740EEB31CF90195004384FC /* Generated.xcconfig */, @@ -127,6 +118,7 @@ 97C146F01CF9000F007C117D /* Runner */ = { isa = PBXGroup; children = ( + 8FC897F42411A9F100415930 /* UnifiedNativeAdView.xib */, 6E27B1141F0DAFA70028FD65 /* GoogleService-Info.plist */, 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */, 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */, @@ -193,6 +185,7 @@ developmentRegion = English; hasScannedForEncodings = 0; knownRegions = ( + English, en, Base, ); @@ -214,8 +207,8 @@ 6E27B1151F0DAFA70028FD65 /* GoogleService-Info.plist in Resources */, 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, 9740EEB51CF90195004384FC /* Generated.xcconfig in Resources */, + 8FC897F52411A9F100415930 /* UnifiedNativeAdView.xib in Resources */, 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, - 2D5378261FAA1A9400D5DBA9 /* flutter_assets in Resources */, 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */, 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, @@ -255,7 +248,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" thin"; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; @@ -277,16 +270,13 @@ files = ( ); inputPaths = ( - "${SRCROOT}/Pods/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh", - "${PODS_ROOT}/.symlinks/flutter/ios/Flutter.framework", ); name = "[CP] Embed Pods Frameworks"; outputPaths = ( - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Flutter.framework", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; /* End PBXShellScriptBuildPhase section */ diff --git a/packages/firebase_admob/example/ios/Runner/AppDelegate.m b/packages/firebase_admob/example/ios/Runner/AppDelegate.m index f08675707182..770ae634a0ba 100644 --- a/packages/firebase_admob/example/ios/Runner/AppDelegate.m +++ b/packages/firebase_admob/example/ios/Runner/AppDelegate.m @@ -3,14 +3,67 @@ // found in the LICENSE file. #include "AppDelegate.h" +#import "FLTFirebaseAdMobPlugin.h" #include "GeneratedPluginRegistrant.h" -@implementation AppDelegate +@interface NativeAdFactoryExample : NSObject +@end + +// The UnifiedNativeAdView.xib and example GADUnifiedNativeAdView is provided and +// explained by https://developers.google.com/admob/ios/native/advanced. +@implementation NativeAdFactoryExample +- (GADUnifiedNativeAdView *)createNativeAd:(GADUnifiedNativeAd *)nativeAd + customOptions:(NSDictionary *)customOptions { + // Create and place ad in view hierarchy. + GADUnifiedNativeAdView *adView = + [[NSBundle mainBundle] loadNibNamed:@"UnifiedNativeAdView" owner:nil options:nil].firstObject; + + // Associate the native ad view with the native ad object. This is + // required to make the ad clickable. + adView.nativeAd = nativeAd; + + // Populate the native ad view with the native ad assets. + // The headline is guaranteed to be present in every native ad. + ((UILabel *)adView.headlineView).text = nativeAd.headline; + + // These assets are not guaranteed to be present. Check that they are before + // showing or hiding them. + ((UILabel *)adView.bodyView).text = nativeAd.body; + adView.bodyView.hidden = nativeAd.body ? NO : YES; + + [((UIButton *)adView.callToActionView) setTitle:nativeAd.callToAction + forState:UIControlStateNormal]; + adView.callToActionView.hidden = nativeAd.callToAction ? NO : YES; + + ((UIImageView *)adView.iconView).image = nativeAd.icon.image; + adView.iconView.hidden = nativeAd.icon ? NO : YES; + + ((UILabel *)adView.storeView).text = nativeAd.store; + adView.storeView.hidden = nativeAd.store ? NO : YES; + ((UILabel *)adView.priceView).text = nativeAd.price; + adView.priceView.hidden = nativeAd.price ? NO : YES; + + ((UILabel *)adView.advertiserView).text = nativeAd.advertiser; + adView.advertiserView.hidden = nativeAd.advertiser ? NO : YES; + + // In order for the SDK to process touch events properly, user interaction + // should be disabled. + adView.callToActionView.userInteractionEnabled = NO; + + return adView; +} +@end + +@implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { [GeneratedPluginRegistrant registerWithRegistry:self]; - // Override point for customization after application launch. + + NativeAdFactoryExample *nativeAdFactory = [[NativeAdFactoryExample alloc] init]; + [FLTFirebaseAdMobPlugin registerNativeAdFactory:self + factoryId:@"adFactoryExample" + nativeAdFactory:nativeAdFactory]; return [super application:application didFinishLaunchingWithOptions:launchOptions]; } diff --git a/packages/firebase_admob/example/ios/Runner/UnifiedNativeAdView.xib b/packages/firebase_admob/example/ios/Runner/UnifiedNativeAdView.xib new file mode 100644 index 000000000000..f2052ba44489 --- /dev/null +++ b/packages/firebase_admob/example/ios/Runner/UnifiedNativeAdView.xib @@ -0,0 +1,134 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/firebase_admob/example/lib/main.dart b/packages/firebase_admob/example/lib/main.dart index 819fa7d41680..e0d70bf45956 100644 --- a/packages/firebase_admob/example/lib/main.dart +++ b/packages/firebase_admob/example/lib/main.dart @@ -4,6 +4,8 @@ // ignore_for_file: public_member_api_docs +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:firebase_admob/firebase_admob.dart'; @@ -140,7 +142,11 @@ class _MyAppState extends State { _nativeAd ??= createNativeAd(); _nativeAd ..load() - ..show(); + ..show( + anchorType: Platform.isAndroid + ? AnchorType.bottom + : AnchorType.top, + ); }, ), RaisedButton( diff --git a/packages/firebase_admob/ios/Classes/FLTFirebaseAdMobPlugin.h b/packages/firebase_admob/ios/Classes/FLTFirebaseAdMobPlugin.h index e09cedc6bbcb..c71f9ba09970 100644 --- a/packages/firebase_admob/ios/Classes/FLTFirebaseAdMobPlugin.h +++ b/packages/firebase_admob/ios/Classes/FLTFirebaseAdMobPlugin.h @@ -4,7 +4,59 @@ #import +#import "Firebase/Firebase.h" + #define FLTLogWarning(format, ...) NSLog((@"FirebaseAdMobPlugin " format), ##__VA_ARGS__) +/** + * Creates a `GADUnifiedNativeAdView` to be shown in a Flutter app. + * + * When a Native Ad is created in Dart, this protocol is responsible for building the + * `GADUnifiedNativeAdView`. Register a class that implements this with a `FLTFirebaseAdMobPlugin` + * to use in conjunction with Flutter. + */ +@protocol FLTNativeAdFactory +@required +/** + * Creates a `GADUnifiedNativeAdView` with a `GADUnifiedNativeAd`. + * + * @param nativeAd Ad information used to create a `GADUnifiedNativeAdView` + * @param customOptions Used to pass additional custom options to create the + * `GADUnifiedNativeAdView`. Nullable. + * @return a `GADUnifiedNativeAdView` that is overlaid on top of the FlutterView. + */ +- (GADUnifiedNativeAdView *)createNativeAd:(GADUnifiedNativeAd *)nativeAd + customOptions:(NSDictionary *)customOptions; +@end + +/** + * Flutter plugin providing access to the Firebase Admob API. + */ @interface FLTFirebaseAdMobPlugin : NSObject +/** + * Adds a `FLTNativeAdFactory` used to create a `GADUnifiedNativeAdView`s from a Native Ad created + * in Dart. + * + * @param registry maintains access to a `FLTFirebaseAdMobPlugin`` instance. + * @param factoryId a unique identifier for the ad factory. The Native Ad created in Dart includes + * a parameter that refers to this. + * @param nativeAdFactory creates `GADUnifiedNativeAdView`s when a Native Ad is created in Dart. + * @return whether the factoryId is unique and the nativeAdFactory was successfully added. + */ ++ (BOOL)registerNativeAdFactory:(NSObject *)registry + factoryId:(NSString *)factoryId + nativeAdFactory:(NSObject *)nativeAdFactory; + +/** + * Unregisters a `FLTNativeAdFactory` used to create `GADUnifiedNativeAdView`s from a Native Ad + * created in Dart. + * + * @param registry maintains access to a `FLTFirebaseAdMobPlugin `instance. + * @param factoryId a unique identifier for the ad factory. The Native Ad created in Dart includes + * a parameter that refers to this. + * @return the previous `FLTNativeAdFactory` associated with this factoryId, or null if there was + * none for this factoryId. + */ ++ (id)unregisterNativeAdFactory:(NSObject *)registry + factoryId:(NSString *)factoryId; @end diff --git a/packages/firebase_admob/ios/Classes/FLTFirebaseAdMobPlugin.m b/packages/firebase_admob/ios/Classes/FLTFirebaseAdMobPlugin.m index b0e307e8e9a4..108c886318c6 100644 --- a/packages/firebase_admob/ios/Classes/FLTFirebaseAdMobPlugin.m +++ b/packages/firebase_admob/ios/Classes/FLTFirebaseAdMobPlugin.m @@ -8,22 +8,24 @@ #import "FLTMobileAd.h" #import "FLTRewardedVideoAdWrapper.h" -#import "Firebase/Firebase.h" @interface FLTFirebaseAdMobPlugin () @property(nonatomic, retain) FlutterMethodChannel *channel; @property(nonatomic, strong) FLTRewardedVideoAdWrapper *rewardedWrapper; +@property NSMutableDictionary> *nativeAdFactories; @end @implementation FLTFirebaseAdMobPlugin - + (void)registerWithRegistrar:(NSObject *)registrar { FLTFirebaseAdMobPlugin *instance = [[FLTFirebaseAdMobPlugin alloc] init]; + [registrar publish:instance]; + instance.channel = [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/firebase_admob" binaryMessenger:[registrar messenger]]; - [registrar addMethodCallDelegate:instance channel:instance.channel]; instance.rewardedWrapper = [[FLTRewardedVideoAdWrapper alloc] initWithChannel:instance.channel]; + + [registrar addMethodCallDelegate:instance channel:instance.channel]; } - (instancetype)init { @@ -33,6 +35,8 @@ - (instancetype)init { [FIRApp configure]; NSLog(@"Configured the default Firebase app %@.", [FIRApp defaultApp].name); } + + if (self) _nativeAdFactories = [NSMutableDictionary dictionary]; return self; } @@ -41,6 +45,38 @@ - (void)dealloc { self.channel = nil; } ++ (BOOL)registerNativeAdFactory:(NSObject *)registry + factoryId:(NSString *)factoryId + nativeAdFactory:(NSObject *)nativeAdFactory { + NSString *pluginClassName = NSStringFromClass([FLTFirebaseAdMobPlugin class]); + FLTFirebaseAdMobPlugin *adMobPlugin = + (FLTFirebaseAdMobPlugin *)[registry valuePublishedByPlugin:pluginClassName]; + if (!adMobPlugin) { + NSString *reason = [NSString + stringWithFormat:@"Could not find a %@ instance. The plugin may have not been registered.", + pluginClassName]; + [NSException exceptionWithName:NSInvalidArgumentException reason:reason userInfo:nil]; + } + + if (adMobPlugin.nativeAdFactories[factoryId]) { + NSLog(@"A NativeAdFactory with the following factoryId already exists: %@", factoryId); + return NO; + } + + [adMobPlugin.nativeAdFactories setValue:nativeAdFactory forKey:factoryId]; + return YES; +} + ++ (id)unregisterNativeAdFactory:(NSObject *)registry + factoryId:(NSString *)factoryId { + FLTFirebaseAdMobPlugin *adMobPlugin = (FLTFirebaseAdMobPlugin *)[registry + valuePublishedByPlugin:NSStringFromClass([FLTFirebaseAdMobPlugin class])]; + + id factory = adMobPlugin.nativeAdFactories[factoryId]; + if (factory) [adMobPlugin.nativeAdFactories removeObjectForKey:factoryId]; + return factory; +} + - (void)callInitialize:(FlutterMethodCall *)call result:(FlutterResult)result { NSString *appId = (NSString *)call.arguments[@"appId"]; if (appId == nil || [appId length] == 0) { @@ -148,6 +184,42 @@ - (void)callLoadInterstitialAd:(FLTMobileAd *)ad result([NSNumber numberWithBool:YES]); } +- (void)callLoadNativeAdWithId:(NSNumber *)id + channel:(FlutterMethodChannel *)channel + call:(FlutterMethodCall *)call + result:(FlutterResult)result { + NSString *adUnitId = (NSString *)call.arguments[@"adUnitId"]; + + if (adUnitId == nil || [adUnitId length] == 0) { + NSString *message = + [NSString stringWithFormat:@"a null or empty adUnitId was provided for %@", id]; + result([FlutterError errorWithCode:@"no_adunit_id" message:message details:nil]); + return; + } + + NSDictionary *customOptions = (NSDictionary *)call.arguments[@"customOptions"]; + NSString *factoryId = (NSString *)call.arguments[@"factoryId"]; + + FLTNativeAd *nativeAd = [FLTNativeAd withId:id + channel:self.channel + nativeAdFactory:_nativeAdFactories[factoryId] + customOptions:customOptions]; + + if (nativeAd.status != CREATED) { + if (nativeAd.status == FAILED) { + NSString *message = [NSString stringWithFormat:@"cannot reload a failed ad=%@", nativeAd]; + result([FlutterError errorWithCode:@"load_failed_ad" message:message details:nil]); + } else { + result([NSNumber numberWithBool:YES]); // The ad was already loaded. + } + return; + } + + NSDictionary *targetingInfo = (NSDictionary *)call.arguments[@"targetingInfo"]; + [nativeAd loadWithAdUnitId:adUnitId targetingInfo:targetingInfo]; + result([NSNumber numberWithBool:YES]); +} + - (void)callLoadRewardedVideoAd:(FlutterMethodCall *)call result:(FlutterResult)result { if (self.rewardedWrapper.status == FLTRewardedVideoAdStatusLoading || self.rewardedWrapper.status == FLTRewardedVideoAdStatusLoaded) { @@ -301,6 +373,8 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result [self callLoadInterstitialAd:[FLTInterstitialAd withId:mobileAdId channel:self.channel] call:call result:result]; + } else if ([call.method isEqualToString:@"loadNativeAd"]) { + [self callLoadNativeAdWithId:mobileAdId channel:self.channel call:call result:result]; } else if ([call.method isEqualToString:@"showAd"]) { [self callShowAd:mobileAdId call:call result:result]; } else if ([call.method isEqualToString:@"isAdLoaded"]) { diff --git a/packages/firebase_admob/ios/Classes/FLTMobileAd.h b/packages/firebase_admob/ios/Classes/FLTMobileAd.h index 1979be13eb4e..e2c9d37e9c6d 100644 --- a/packages/firebase_admob/ios/Classes/FLTMobileAd.h +++ b/packages/firebase_admob/ios/Classes/FLTMobileAd.h @@ -3,6 +3,7 @@ // found in the LICENSE file. #import +#import "FLTFirebaseAdMobPlugin.h" #import "GoogleMobileAds/GoogleMobileAds.h" typedef enum : NSUInteger { @@ -25,7 +26,11 @@ typedef enum : NSUInteger { - (void)dispose; @end -@interface FLTBannerAd : FLTMobileAd +@interface FLTMobileAdWithView : FLTMobileAd +- (UIView *)adView; +@end + +@interface FLTBannerAd : FLTMobileAdWithView + (instancetype)withId:(NSNumber *)mobileAdId adSize:(GADAdSize)adSize channel:(FlutterMethodChannel *)channel; @@ -34,3 +39,11 @@ typedef enum : NSUInteger { @interface FLTInterstitialAd : FLTMobileAd + (instancetype)withId:(NSNumber *)mobileAdId channel:(FlutterMethodChannel *)channel; @end + +@interface FLTNativeAd + : FLTMobileAdWithView ++ (instancetype)withId:(NSNumber *)mobileAdId + channel:(FlutterMethodChannel *)channel + nativeAdFactory:(id)nativeAdFactory + customOptions:(NSDictionary *)customOptions; +@end diff --git a/packages/firebase_admob/ios/Classes/FLTMobileAd.m b/packages/firebase_admob/ios/Classes/FLTMobileAd.m index c4ba6e1a3d1f..e01350406ce0 100644 --- a/packages/firebase_admob/ios/Classes/FLTMobileAd.m +++ b/packages/firebase_admob/ios/Classes/FLTMobileAd.m @@ -83,7 +83,7 @@ - (void)showAtOffset:(double)anchorOffset } - (void)show { - // Implemented by the Banner and Interstitial subclasses + // Implemented by the FLTMobileAdWithView and Interstitial subclasses } - (void)dispose { @@ -101,6 +101,74 @@ - (NSString *)description { } @end +@implementation FLTMobileAdWithView +- (UIView *)adView { + // We cause a crash if this method is not overriden by subclasses. + [self doesNotRecognizeSelector:_cmd]; + return nil; +} + +- (void)show { + if (_status == LOADING) { + _status = PENDING; + return; + } + + if (_status != LOADED) return; + + UIView *screen = [FLTMobileAd rootViewController].view; + [screen addSubview:self.adView]; + +// UIView.safeAreaLayoutGuide is only available on iOS 11.0+ +#if defined(__IPHONE_11_0) && (__IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_11_0) + if (@available(ios 11.0, *)) { + self.adView.translatesAutoresizingMaskIntoConstraints = NO; + + UILayoutGuide *guide = screen.safeAreaLayoutGuide; + [NSLayoutConstraint activateConstraints:@[ + [self.adView.centerXAnchor constraintEqualToAnchor:guide.centerXAnchor + constant:_horizontalCenterOffset], + + [self.adView.leftAnchor constraintGreaterThanOrEqualToAnchor:guide.leftAnchor], + [self.adView.rightAnchor constraintLessThanOrEqualToAnchor:guide.rightAnchor], + ]]; + + if (_anchorType == 0) { + [NSLayoutConstraint activateConstraints:@[ + [self.adView.bottomAnchor constraintEqualToAnchor:guide.bottomAnchor + constant:_anchorOffset], + ]]; + } else { + [NSLayoutConstraint activateConstraints:@[ + [self.adView.topAnchor constraintEqualToAnchor:guide.topAnchor constant:_anchorOffset], + ]]; + } + } +#endif + + // We find the left most point that aligns the view to the horizontal center. + CGFloat x = + screen.frame.size.width / 2 - self.adView.frame.size.width / 2 + _horizontalCenterOffset; + // We find the top point that anchors the view to the top/bottom depending on anchorType. + CGFloat y; + if (_anchorType == 0) { + y = screen.frame.size.height - self.adView.frame.size.height + _anchorOffset; + } else { + y = _anchorOffset; + } + self.adView.frame = (CGRect){{x, self.adView.frame.origin.y}, self.adView.frame.size}; +} + +- (void)dispose { + if (self.adView.superview) [self.adView removeFromSuperview]; + [super dispose]; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"%@ for: %@", super.description, self.adView]; +} +@end + @implementation FLTBannerAd GADBannerView *_banner; GADAdSize _adSize; @@ -125,6 +193,10 @@ - (instancetype)initWithId:mobileAdId return nil; } +- (UIView *)adView { + return _banner; +} + - (void)loadWithAdUnitId:(NSString *)adUnitId targetingInfo:(NSDictionary *)targetingInfo { if (_status != CREATED) return; _status = LOADING; @@ -136,56 +208,6 @@ - (void)loadWithAdUnitId:(NSString *)adUnitId targetingInfo:(NSDictionary *)targ [_banner loadRequest:[factory createRequest]]; } -- (void)show { - if (_status == LOADING) { - _status = PENDING; - return; - } - - if (_status != LOADED) return; - - _banner.translatesAutoresizingMaskIntoConstraints = NO; - UIView *screen = [FLTMobileAd rootViewController].view; - [screen addSubview:_banner]; - -#if defined(__IPHONE_11_0) && (__IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_11_0) - if (@available(ios 11.0, *)) { - UILayoutGuide *guide = screen.safeAreaLayoutGuide; - [NSLayoutConstraint activateConstraints:@[ - [_banner.centerXAnchor constraintEqualToAnchor:guide.centerXAnchor - constant:_horizontalCenterOffset], - [_banner.bottomAnchor - constraintEqualToAnchor:_anchorType == 0 ? guide.bottomAnchor : guide.topAnchor - constant:_anchorOffset] - ]]; - } else { - [self placeBannerPreIos11]; - } -#else - [self placeBannerPreIos11]; -#endif -} - -- (void)placeBannerPreIos11 { - UIView *screen = [FLTMobileAd rootViewController].view; - CGFloat x = screen.frame.size.width / 2 - _banner.frame.size.width / 2 + _horizontalCenterOffset; - CGFloat y; - if (_anchorType == 0) { - y = screen.frame.size.height - _banner.frame.size.height + _anchorOffset; - } else { - y = _anchorOffset; - } - _banner.frame = (CGRect){{x, y}, _banner.frame.size}; - [screen addSubview:_banner]; -} - -- (void)adViewDidReceiveAd:(GADBannerView *)adView { - bool statusWasPending = _status == PENDING; - _status = LOADED; - [_channel invokeMethod:@"onAdLoaded" arguments:[self argumentsMap]]; - if (statusWasPending) [self show]; -} - - (void)adView:(GADBannerView *)adView didFailToReceiveAdWithError:(GADRequestError *)error { FLTLogWarning(@"adView:didFailToReceiveAdWithError: %@ (MobileAd %@)", [error localizedDescription], self); @@ -208,14 +230,11 @@ - (void)adViewWillLeaveApplication:(GADBannerView *)adView { [_channel invokeMethod:@"onAdLeftApplication" arguments:[self argumentsMap]]; } -- (void)dispose { - if (_banner.superview) [_banner removeFromSuperview]; - _banner = nil; - [super dispose]; -} - -- (NSString *)description { - return [NSString stringWithFormat:@"%@ for: %@", super.description, _banner]; +- (void)adViewDidReceiveAd:(GADBannerView *)adView { + bool statusWasPending = _status == PENDING; + _status = LOADED; + [_channel invokeMethod:@"onAdLoaded" arguments:[self argumentsMap]]; + if (statusWasPending) [self show]; } @end @@ -287,3 +306,87 @@ - (NSString *)description { return [NSString stringWithFormat:@"%@ for: %@", super.description, _interstitial]; } @end + +@implementation FLTNativeAd { + GADAdLoader *_adLoader; + GADUnifiedNativeAdView *_nativeAd; + id _nativeAdFactory; + NSDictionary *_customOptions; +} + ++ (instancetype)withId:(NSNumber *)mobileAdId + channel:(FlutterMethodChannel *)channel + nativeAdFactory:(id)nativeAdFactory + customOptions:(NSDictionary *)customOptions { + FLTMobileAd *ad = [FLTMobileAd getAdForId:mobileAdId]; + return ad != nil ? (FLTNativeAd *)ad + : [[FLTNativeAd alloc] initWithId:mobileAdId + channel:channel + nativeAdFactory:nativeAdFactory + customOptions:customOptions]; +} + +- (instancetype)initWithId:mobileAdId + channel:(FlutterMethodChannel *)channel + nativeAdFactory:(id)nativeAdFactory + customOptions:(NSDictionary *)customOptions { + self = [super initWithId:mobileAdId channel:channel]; + if (self) { + _nativeAdFactory = nativeAdFactory; + _customOptions = customOptions; + } + return self; +} + +- (UIView *)adView { + return _nativeAd; +} + +- (void)loadWithAdUnitId:(NSString *)adUnitId targetingInfo:(NSDictionary *)targetingInfo { + if (_status != CREATED) return; + _status = LOADING; + + _adLoader = [[GADAdLoader alloc] initWithAdUnitID:adUnitId + rootViewController:[FLTMobileAd rootViewController] + adTypes:@[ kGADAdLoaderAdTypeUnifiedNative ] + options:@[]]; + _adLoader.delegate = self; + + FLTRequestFactory *factory = [[FLTRequestFactory alloc] initWithTargetingInfo:targetingInfo]; + [_adLoader loadRequest:[factory createRequest]]; +} + +- (void)adLoader:(nonnull GADAdLoader *)adLoader + didFailToReceiveAdWithError:(nonnull GADRequestError *)error { + FLTLogWarning(@"adLoader:didFailToReceiveAdWithError: %@ (MobileAd %@)", + [error localizedDescription], self); + [_channel invokeMethod:@"onAdFailedToLoad" arguments:[self argumentsMap]]; +} + +- (void)adLoader:(nonnull GADAdLoader *)adLoader + didReceiveUnifiedNativeAd:(nonnull GADUnifiedNativeAd *)nativeAd { + nativeAd.delegate = self; + _nativeAd = [_nativeAdFactory createNativeAd:nativeAd customOptions:_customOptions]; + + bool statusWasPending = _status == PENDING; + _status = LOADED; + [_channel invokeMethod:@"onAdLoaded" arguments:[self argumentsMap]]; + if (statusWasPending) [self show]; +} + +- (void)nativeAdWillPresentScreen:(GADUnifiedNativeAd *)nativeAd { + [_channel invokeMethod:@"onAdClicked" arguments:[self argumentsMap]]; +} + +- (void)nativeAdWillDismissScreen:(GADUnifiedNativeAd *)nativeAd { + [_channel invokeMethod:@"onAdImpression" arguments:[self argumentsMap]]; +} + +- (void)nativeAdDidDismissScreen:(GADUnifiedNativeAd *)nativeAd { + [_channel invokeMethod:@"onAdClosed" arguments:[self argumentsMap]]; +} + +- (void)nativeAdWillLeaveApplication:(GADUnifiedNativeAd *)nativeAd { + [_channel invokeMethod:@"onAdLeftApplication" arguments:[self argumentsMap]]; +} +@end diff --git a/packages/firebase_admob/lib/firebase_admob.dart b/packages/firebase_admob/lib/firebase_admob.dart index 36a8b8a63282..2e3322159138 100644 --- a/packages/firebase_admob/lib/firebase_admob.dart +++ b/packages/firebase_admob/lib/firebase_admob.dart @@ -315,8 +315,6 @@ class BannerAd extends MobileAd { /// Ads SDK) is then responsible for displaying them. /// /// See the README for more details on using Native Ads. -/// -/// Note: This is currently only supported for Android. class NativeAd extends MobileAd { NativeAd({ @required String adUnitId, @@ -325,15 +323,10 @@ class NativeAd extends MobileAd { MobileAdListener listener, this.customOptions, }) : super( - adUnitId: adUnitId, - targetingInfo: targetingInfo, - listener: listener) { - if (!Platform.isAndroid) { - throw UnimplementedError( - '$NativeAd is currently only available for Android', - ); - } - } + adUnitId: adUnitId, + targetingInfo: targetingInfo, + listener: listener, + ); /// Optional options used to create the [NativeAd]. ///