diff --git a/packages/android_intent/CHANGELOG.md b/packages/android_intent/CHANGELOG.md index ea1b8f85c626..7a818f38548a 100644 --- a/packages/android_intent/CHANGELOG.md +++ b/packages/android_intent/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.3.3 + +* Added "flags" option to call intent.addFlags(int) in native. + ## 0.3.2 * Added "action_location_source_settings" action to start Location Settings Activity. diff --git a/packages/android_intent/android/src/main/java/io/flutter/plugins/androidintent/AndroidIntentPlugin.java b/packages/android_intent/android/src/main/java/io/flutter/plugins/androidintent/AndroidIntentPlugin.java index 9c924d6fa524..4eba37d00d1d 100644 --- a/packages/android_intent/android/src/main/java/io/flutter/plugins/androidintent/AndroidIntentPlugin.java +++ b/packages/android_intent/android/src/main/java/io/flutter/plugins/androidintent/AndroidIntentPlugin.java @@ -126,6 +126,9 @@ public void onMethodCall(MethodCall call, Result result) { if (mRegistrar.activity() == null) { intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); } + if (call.argument("flag") != null) { + intent.addFlags((Integer) call.argument("flags"))); + } if (call.argument("category") != null) { intent.addCategory((String) call.argument("category")); } diff --git a/packages/android_intent/example/ios/Runner.xcodeproj/project.pbxproj b/packages/android_intent/example/ios/Runner.xcodeproj/project.pbxproj index 57d70edda3b5..010be3a19cdb 100644 --- a/packages/android_intent/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/android_intent/example/ios/Runner.xcodeproj/project.pbxproj @@ -9,7 +9,6 @@ /* Begin PBXBuildFile section */ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; - 2D5378261FAA1A9400D5DBA9 /* flutter_assets in Resources */ = {isa = PBXBuildFile; fileRef = 2D5378251FAA1A9400D5DBA9 /* flutter_assets */; }; 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, ); }; }; 3FC5CBD67A867C34C8CFD7E1 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7ABB9ACA70E30025F77BB759 /* libPods-Runner.a */; }; @@ -43,12 +42,12 @@ 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 = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 2D5378251FAA1A9400D5DBA9 /* flutter_assets */ = {isa = PBXFileReference; lastKnownFileType = folder; name = flutter_assets; path = Flutter/flutter_assets; sourceTree = SOURCE_ROOT; }; 3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; sourceTree = ""; }; 7ABB9ACA70E30025F77BB759 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 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 = ""; }; + 852BECFF20A657D67F1A9E8B /* 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 = ""; }; 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 = ""; }; @@ -58,6 +57,7 @@ 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 = ""; }; + EE7816E2354AA8646253B944 /* 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 = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -77,6 +77,8 @@ 2C36A917BF8B34817D5A406D /* Pods */ = { isa = PBXGroup; children = ( + EE7816E2354AA8646253B944 /* Pods-Runner.debug.xcconfig */, + 852BECFF20A657D67F1A9E8B /* Pods-Runner.release.xcconfig */, ); name = Pods; sourceTree = ""; @@ -94,7 +96,6 @@ children = ( 3B80C3931E831B6300D905FE /* App.framework */, 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, - 2D5378251FAA1A9400D5DBA9 /* flutter_assets */, 9740EEBA1CF902C7004384FC /* Flutter.framework */, 9740EEB21CF90195004384FC /* Debug.xcconfig */, 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, @@ -161,7 +162,6 @@ 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, 4B2738B48C3E53795176CD79 /* [CP] Embed Pods Frameworks */, - B23D1C01D32617384EBE7F0E /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -212,7 +212,6 @@ 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, 9740EEB51CF90195004384FC /* Generated.xcconfig 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 */, @@ -248,7 +247,7 @@ ); 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; }; 9740EEB61CF901F6004384FC /* Run Script */ = { @@ -265,34 +264,22 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; - B23D1C01D32617384EBE7F0E /* [CP] Copy Pods Resources */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "[CP] Copy Pods Resources"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; - showEnvVarsInLog = 0; - }; ECD6A6833016AB689F7B8471 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", ); name = "[CP] Check Pods Manifest.lock"; outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n"; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; /* End PBXShellScriptBuildPhase section */ diff --git a/packages/android_intent/example/lib/main.dart b/packages/android_intent/example/lib/main.dart index c94ffe50aef5..becf3d6e1e75 100644 --- a/packages/android_intent/example/lib/main.dart +++ b/packages/android_intent/example/lib/main.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'package:android_intent/android_intent.dart'; +import 'package:android_intent/flag.dart'; import 'package:flutter/material.dart'; import 'package:platform/platform.dart'; @@ -117,6 +118,15 @@ class ExplicitIntentsWidget extends StatelessWidget { intent.launch(); } + void _startActivityInNewTask() { + final AndroidIntent intent = AndroidIntent( + action: 'action_view', + data: Uri.encodeFull('https://flutter.io'), + flags: [Flag.FLAG_ACTIVITY_NEW_TASK], + ); + intent.launch(); + } + void _testExplicitIntentFallback() { final AndroidIntent intent = AndroidIntent( action: 'action_view', @@ -162,6 +172,10 @@ class ExplicitIntentsWidget extends StatelessWidget { child: const Text('Tap here to open link in Google Chrome.'), onPressed: _openLinkInGoogleChrome, ), + RaisedButton( + child: const Text('Tap here to start activity in new task.'), + onPressed: _startActivityInNewTask, + ), RaisedButton( child: const Text( 'Tap here to test explicit intent fallback to implicit.'), diff --git a/packages/android_intent/lib/android_intent.dart b/packages/android_intent/lib/android_intent.dart index 5e00b30d7d43..f0f1d8bf8b0d 100644 --- a/packages/android_intent/lib/android_intent.dart +++ b/packages/android_intent/lib/android_intent.dart @@ -14,6 +14,7 @@ const String kChannelName = 'plugins.flutter.io/android_intent'; class AndroidIntent { /// Builds an Android intent with the following parameters /// [action] refers to the action parameter of the intent. + /// [flags] is the list of strings that will be converted to the flags. /// [category] refers to the category of the intent, can be null. /// [data] refers to the string format of the URI that will be passed to /// intent. @@ -24,6 +25,7 @@ class AndroidIntent { /// If not null, then [package] but also be provided. const AndroidIntent({ @required this.action, + this.flags, this.category, this.data, this.arguments, @@ -34,7 +36,22 @@ class AndroidIntent { _channel = const MethodChannel(kChannelName), _platform = platform ?? const LocalPlatform(); + @visibleForTesting + AndroidIntent.private({ + @required this.action, + @required Platform platform, + @required MethodChannel channel, + this.flags, + this.category, + this.data, + this.arguments, + this.package, + this.componentName, + }) : _channel = channel, + _platform = platform; + final String action; + final List flags; final String category; final String data; final Map arguments; @@ -43,13 +60,34 @@ class AndroidIntent { final MethodChannel _channel; final Platform _platform; + bool _isPowerOfTwo(int x) { + /* First x in the below expression is for the case when x is 0 */ + return x != 0 && ((x & (x - 1)) == 0); + } + + @visibleForTesting + int convertFlags(List flags) { + int finalValue = 0; + for (int i = 0; i < flags.length; i++) { + if (!_isPowerOfTwo(flags[i])) { + throw ArgumentError.value(flags[i], 'flag\'s value must be power of 2'); + } + finalValue |= flags[i]; + } + return finalValue; + } + /// Launch the intent. /// - /// This works only on Android platforms. Please guard the call so that your - /// iOS app does not crash. Checked mode will throw an assert exception. + /// This works only on Android platforms. Future launch() async { - assert(_platform.isAndroid); + if (!_platform.isAndroid) { + return; + } final Map args = {'action': action}; + if (flags != null) { + args['flags'] = convertFlags(flags); + } if (category != null) { args['category'] = category; } diff --git a/packages/android_intent/lib/flag.dart b/packages/android_intent/lib/flag.dart new file mode 100644 index 000000000000..b4e6ed100146 --- /dev/null +++ b/packages/android_intent/lib/flag.dart @@ -0,0 +1,37 @@ +// flag values from https://developer.android.com/reference/android/content/Intent.html +class Flag { + static const int FLAG_ACTIVITY_BROUGHT_TO_FRONT = 4194304; + static const int FLAG_ACTIVITY_CLEAR_TASK = 32768; + static const int FLAG_ACTIVITY_CLEAR_TOP = 67108864; + static const int FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET = 524288; + static const int FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS = 8388608; + static const int FLAG_ACTIVITY_FORWARD_RESULT = 33554432; + static const int FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY = 1048576; + static const int FLAG_ACTIVITY_LAUNCH_ADJACENT = 4096; + static const int FLAG_ACTIVITY_MATCH_EXTERNAL = 2048; + static const int FLAG_ACTIVITY_MULTIPLE_TASK = 134217728; + static const int FLAG_ACTIVITY_NEW_DOCUMENT = 524288; + static const int FLAG_ACTIVITY_NEW_TASK = 268435456; + static const int FLAG_ACTIVITY_NO_ANIMATION = 65536; + static const int FLAG_ACTIVITY_NO_HISTORY = 1073741824; + static const int FLAG_ACTIVITY_NO_USER_ACTION = 262144; + static const int FLAG_ACTIVITY_PREVIOUS_IS_TOP = 16777216; + static const int FLAG_ACTIVITY_REORDER_TO_FRONT = 131072; + static const int FLAG_ACTIVITY_RESET_TASK_IF_NEEDED = 2097152; + static const int FLAG_ACTIVITY_RETAIN_IN_RECENTS = 8192; + static const int FLAG_ACTIVITY_SINGLE_TOP = 536870912; + static const int FLAG_ACTIVITY_TASK_ON_HOME = 16384; + static const int FLAG_DEBUG_LOG_RESOLUTION = 8; + static const int FLAG_EXCLUDE_STOPPED_PACKAGES = 16; + static const int FLAG_FROM_BACKGROUND = 4; + static const int FLAG_GRANT_PERSISTABLE_URI_PERMISSION = 64; + static const int FLAG_GRANT_PREFIX_URI_PERMISSION = 128; + static const int FLAG_GRANT_READ_URI_PERMISSION = 1; + static const int FLAG_GRANT_WRITE_URI_PERMISSION = 2; + static const int FLAG_INCLUDE_STOPPED_PACKAGES = 32; + static const int FLAG_RECEIVER_FOREGROUND = 268435456; + static const int FLAG_RECEIVER_NO_ABORT = 134217728; + static const int FLAG_RECEIVER_REGISTERED_ONLY = 1073741824; + static const int FLAG_RECEIVER_REPLACE_PENDING = 536870912; + static const int FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS = 2097152; +} diff --git a/packages/android_intent/pubspec.yaml b/packages/android_intent/pubspec.yaml index cf205a693daf..11cbc319bbf0 100644 --- a/packages/android_intent/pubspec.yaml +++ b/packages/android_intent/pubspec.yaml @@ -2,7 +2,7 @@ name: android_intent description: Flutter plugin for launching Android Intents. Not supported on iOS. author: Flutter Team homepage: https://github.com/flutter/plugins/tree/master/packages/android_intent -version: 0.3.2 +version: 0.3.3 flutter: plugin: @@ -15,7 +15,11 @@ dependencies: sdk: flutter platform: ^2.0.0 meta: ^1.0.5 - +dev_dependencies: + test: ^1.3.0 + mockito: ^3.0.0 + flutter_test: + sdk: flutter environment: sdk: ">=2.0.0-dev.28.0 <3.0.0" flutter: ">=1.2.0 <2.0.0" diff --git a/packages/android_intent/test/android_intent_test.dart b/packages/android_intent/test/android_intent_test.dart new file mode 100644 index 000000000000..315c46456511 --- /dev/null +++ b/packages/android_intent/test/android_intent_test.dart @@ -0,0 +1,94 @@ +// Copyright 2019 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:android_intent/flag.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:android_intent/android_intent.dart'; +import 'package:mockito/mockito.dart'; +import 'package:platform/platform.dart'; + +void main() { + AndroidIntent androidIntent; + MockMethodChannel mockChannel; + // const MethodChannel channel = + // MethodChannel('plugins.flutter.io/android_intent'); + final List log = []; + setUp(() { + mockChannel = MockMethodChannel(); + + // channel.setMockMethodCallHandler((MethodCall methodCall) async { + // log.add(methodCall); + // return ''; + // }); + log.clear(); + }); + group('AndroidIntent', () { + test('pass right params', () async { + androidIntent = AndroidIntent.private( + action: 'action_view', + data: Uri.encodeFull('https://flutter.io'), + flags: [Flag.FLAG_ACTIVITY_NEW_TASK], + channel: mockChannel, + platform: FakePlatform(operatingSystem: 'android')); + androidIntent.launch(); + verify(mockChannel.invokeMethod('launch', { + 'action': 'action_view', + 'data': Uri.encodeFull('https://flutter.io'), + 'flags': androidIntent.convertFlags([Flag.FLAG_ACTIVITY_NEW_TASK]), + })); + }); + test('pass null value to action param', () async { + androidIntent = AndroidIntent.private( + action: null, + channel: mockChannel, + platform: FakePlatform(operatingSystem: 'android')); + androidIntent.launch(); + verify(mockChannel.invokeMethod('launch', { + 'action': null, + })); + }); + + test('call in ios platform', () async { + androidIntent = AndroidIntent.private( + action: null, + channel: mockChannel, + platform: FakePlatform(operatingSystem: 'ios')); + androidIntent.launch(); + verifyZeroInteractions(mockChannel); + }); + }); + group('convertFlags ', () { + androidIntent = const AndroidIntent( + action: 'action_view', + ); + test('add filled flag list', () async { + final List flags = []; + flags.add(Flag.FLAG_ACTIVITY_NEW_TASK); + flags.add(Flag.FLAG_ACTIVITY_NEW_DOCUMENT); + expect( + androidIntent.convertFlags(flags), + 268959744, + ); + }); + test('add flags whose values are not power of 2', () async { + final List flags = []; + flags.add(100); + flags.add(10); + expect( + () => androidIntent.convertFlags(flags), + throwsArgumentError, + ); + }); + test('add empty flag list', () async { + final List flags = []; + expect( + androidIntent.convertFlags(flags), + 0, + ); + }); + }); +} + +class MockMethodChannel extends Mock implements MethodChannel {} diff --git a/packages/firebase_admob/CHANGELOG.md b/packages/firebase_admob/CHANGELOG.md new file mode 100644 index 000000000000..7a44bfb131f5 --- /dev/null +++ b/packages/firebase_admob/CHANGELOG.md @@ -0,0 +1,165 @@ +## 0.9.0+4 + +* Add the ability to horizontally adjust the ads banner location by specifying a pixel offset from the centre. + +## 0.9.0+3 + +* Update google-services Android gradle plugin to 4.3.0 in documentation and examples. + +## 0.9.0+2 + +* On Android, no longer crashes when registering the plugin if no activity is available. + +## 0.9.0+1 + +* Add missing template type parameter to `invokeMethod` calls. +* Bump minimum Flutter version to 1.5.0. + +## 0.9.0 + +* Update Android dependencies to latest. + +## 0.8.0+4 + +* Update documentation to add AdMob App ID in Info.plist +* Add iOS AdMob App ID in Info.plist in example project + +## 0.8.0+3 + +* Log messages about automatic configuration of the default app are now less confusing. + +## 0.8.0+2 + +* Remove categories. + +## 0.8.0+1 + +* Log a more detailed warning at build time about the previous AndroidX + migration. + +## 0.8.0 + +* **Breaking change**. Migrate from the deprecated original Android Support + Library to AndroidX. This shouldn't result in any functional changes, but it + requires any Android apps using this plugin to [also + migrate](https://developer.android.com/jetpack/androidx/migrate) if they're + using the original support library. + +## 0.7.0 + +* Mark Dart code as deprecated where the newer version AdMob deprecates features (Birthday, Gender, and Family targeting). +* Update gradle dependencies. +* Add documentation for new AndroidManifest requirements. + +## 0.6.1+1 + +* Bump Android dependencies to latest. +* __THIS WAS AN UNINTENTIONAL BREAKING CHANGE__. Users should consume 0.6.1 instead if they need the old API, or 0.7.0 for the bumped version. +* Guide how to fix crash with admob version 17.0.0 in README + +## 0.6.1 + +* listener on MobileAd shouldn't be final. +* Ad listeners can to be set in or out of Ad initialization. + +## 0.6.0 + +* Add nonPersonalizedAds option to MobileAdTargetingInfo + +## 0.5.7 + +* Bumped mockito dependency to pick up Dart 2 support. + +## 0.5.6 + +* Bump Android and Firebase dependency versions. + +## 0.5.5 + +* Updated Gradle tooling to match Android Studio 3.1.2. + +## 0.5.4+1 + +* Graduate to beta. + +## 0.5.4 + +* Fixed a bug that was causing rewarded video failure event to be called on the wrong listener. + +## 0.5.3 + +* Updated Google Play Services dependencies to version 15.0.0. +* Added handling of rewarded video completion event. + +## 0.5.2 + +* Simplified podspec for Cocoapods 1.5.0, avoiding link issues in app archives. + +## 0.5.1 + +* Fixed Dart 2 type errors. + +## 0.5.0 + +* **Breaking change**. The BannerAd constructor now requires an AdSize + parameter. BannerAds can be created with AdSize.smartBanner, or one of + the other predefined AdSize values. Previously BannerAds were always + defined with the smartBanner size. + +## 0.4.0 + +* **Breaking change**. Set SDK constraints to match the Flutter beta release. + +## 0.3.2 + +* Fixed Dart 2 type errors. + +## 0.3.1 + +* Enabled use in Swift projects. + +## 0.3.0 + +* Added support for rewarded video ads. +* **Breaking change**. The properties and parameters named "unitId" in BannerAd + and InterstitialAd have been renamed to "adUnitId" to better match AdMob's + documentation and UI. + +## 0.2.3 + +* Simplified and upgraded Android project template to Android SDK 27. +* Updated package description. + +## 0.2.2 + +* Added platform-specific App IDs and ad unit IDs to example. +* Separated load and show functionality for interstitials in example. + +## 0.2.1 + +* Use safe area layout to place ad in iOS 11 + +## 0.2.0 + +* **Breaking change**. MobileAd TargetingInfo requestAgent is now hardcoded to 'flutter-alpha'. + +## 0.1.0 + +* **Breaking change**. Upgraded to Gradle 4.1 and Android Studio Gradle plugin + 3.0.1. Older Flutter projects need to upgrade their Gradle setup as well in + order to use this version of the plugin. Instructions can be found + [here](https://github.com/flutter/flutter/wiki/Updating-Flutter-projects-to-Gradle-4.1-and-Android-Studio-Gradle-plugin-3.0.1). +* Relaxed GMS dependency to [11.4.0,12.0[ + +## 0.0.3 + +* Add FLT prefix to iOS types +* Change GMS dependency to 11.4.+ + +## 0.0.2 + +* Change GMS dependency to 11.+ + +## 0.0.1 + +* Initial Release: not ready for production use diff --git a/packages/firebase_admob/README.md b/packages/firebase_admob/README.md new file mode 100644 index 000000000000..a7a285249c49 --- /dev/null +++ b/packages/firebase_admob/README.md @@ -0,0 +1,216 @@ +# firebase_admob + +A plugin for [Flutter](https://flutter.io) that supports loading and +displaying banner, interstitial (full-screen), and rewarded video ads using the +[Firebase AdMob API](https://firebase.google.com/docs/admob/). + +*Note*: This plugin is in beta, and may still have a few issues and missing APIs. +[Feedback](https://github.com/flutter/flutter/issues) and +[Pull Requests](https://github.com/flutter/plugins/pulls) are welcome. + +## AndroidManifest changes + +AdMob 17 requires the App ID to be included in the `AndroidManifest.xml`. Failure +to do so will result in a crash on launch of your app. The line should look like: + +```xml + +``` + +where `[ADMOB_APP_ID]` is your App ID. You must pass the same value when you +initialize the plugin in your Dart code. + +See https://goo.gl/fQ2neu for more information about configuring `AndroidManifest.xml` +and setting up your App ID. + +## Info.plist changes + +Admob 7.42.0 requires the App ID to be included in `Info.plist`. Failure to do so will result in a crash on launch of your app. The lines should look like: + +```xml +GADApplicationIdentifier +[ADMOB_APP_ID] +``` + +where `[ADMOB_APP_ID]` is your App ID. You must pass the same value when you initialize the plugin in your Dart code. + +See https://developers.google.com/admob/ios/quick-start#update_your_infoplist for more information about configuring `Info.plist` and setting up your App ID. + +## Initializing the plugin +The AdMob plugin must be initialized with an AdMob App ID. + +```dart +FirebaseAdMob.instance.initialize(appId: appId); +``` +### Android +Starting in version 17.0.0, if you are an AdMob publisher you are now required to add your AdMob app ID in your **AndroidManifest.xml** file. Once you find your AdMob app ID in the AdMob UI, add it to your manifest adding the following tag: + +```xml + + + + + + +``` + +Failure to add this tag will result in the app crashing at app launch with a message starting with *"The Google Mobile Ads SDK was initialized incorrectly."* + +On Android, this value must be the same as the App ID value set in your +`AndroidManifest.xml`. + +### iOS +Starting in version 7.42.0, you are required to add your AdMob app ID in your **Info.plist** file under the Runner directory. You can add it using Xcode or edit the file manually: + +```xml + + GADApplicationIdentifier + ca-app-pub-################~########## + +``` + +Failure to add this tag will result in the app crashing at app launch with a message including *"GADVerifyApplicationID."* + +## Using banners and interstitials +Banner and interstitial ads can be configured with target information. +And in the example below, the ads are given test ad unit IDs for a quick start. + +```dart +MobileAdTargetingInfo targetingInfo = MobileAdTargetingInfo( + keywords: ['flutterio', 'beautiful apps'], + contentUrl: 'https://flutter.io', + birthday: DateTime.now(), + childDirected: false, + designedForFamilies: false, + gender: MobileAdGender.male, // or MobileAdGender.female, MobileAdGender.unknown + testDevices: [], // Android emulators are considered test devices +); + +BannerAd myBanner = BannerAd( + // Replace the testAdUnitId with an ad unit id from the AdMob dash. + // https://developers.google.com/admob/android/test-ads + // https://developers.google.com/admob/ios/test-ads + adUnitId: BannerAd.testAdUnitId, + size: AdSize.smartBanner, + targetingInfo: targetingInfo, + listener: (MobileAdEvent event) { + print("BannerAd event is $event"); + }, +); + +InterstitialAd myInterstitial = InterstitialAd( + // Replace the testAdUnitId with an ad unit id from the AdMob dash. + // https://developers.google.com/admob/android/test-ads + // https://developers.google.com/admob/ios/test-ads + adUnitId: InterstitialAd.testAdUnitId, + targetingInfo: targetingInfo, + listener: (MobileAdEvent event) { + print("InterstitialAd event is $event"); + }, +); +``` + +Ads must be loaded before they're shown. +```dart +myBanner + // typically this happens well before the ad is shown + ..load() + ..show( + // Positions the banner ad 60 pixels from the bottom of the screen + anchorOffset: 60.0, + // Positions the banner ad 10 pixels from the center of the screen to the right + horizontalCenterOffset: 10.0, + // Banner Position + anchorType: AnchorType.bottom, + ); +``` + +Ads must be loaded before they're shown. +```dart +myBanner + // typically this happens well before the ad is shown + ..load() + ..show( + // Positions the banner ad 60 pixels from the bottom of the screen + anchorOffset: 60.0, + // Positions the banner ad 10 pixels from the center of the screen to the left + horizontalCenterOffset: -10.0, + // Banner Position + anchorType: AnchorType.bottom, + ); +``` + +```dart +myInterstitial + ..load() + ..show( + anchorType: AnchorType.bottom, + anchorOffset: 0.0, + horizontalCenterOffset: 0.0, + ); +``` + +`BannerAd` and `InterstitialAd` objects can be disposed to free up plugin +resources. Disposing a banner ad that's been shown removes it from the screen. +Interstitial ads, however, can't be programmatically removed from view. + +Banner and interstitial ads can be created with a `MobileAdEvent` listener. The +listener can be used to detect when the ad has actually finished loading +(or failed to load at all). + +## Using rewarded video ads + +Unlike banners and interstitials, rewarded video ads are loaded one at a time +via a singleton object, `RewardedVideoAd.instance`. Its `load` method takes an +AdMob ad unit ID and an instance of `MobileAdTargetingInfo`: +```dart +RewardedVideoAd.instance.load(myAdMobAdUnitId, targetingInfo); +``` + +To listen for events in the rewarded video ad lifecycle, apps can define a +function matching the `RewardedVideoAdListener` typedef, and assign it to the +`listener` instance variable in `RewardedVideoAd`. If set, the `listener` +function will be invoked whenever one of the events in the `RewardedVideAdEvent` +enum occurs. After a rewarded video ad loads, for example, the +`RewardedVideoAdEvent.loaded` is sent. Any time after that, apps can show the ad +by calling `show`: +```dart +RewardedVideoAd.instance.show(); +``` + +When the AdMob SDK decides it's time to grant an in-app reward, it does so via +the `RewardedVideoAdEvent.rewarded` event: +```dart +RewardedVideoAd.instance.listener = + (RewardedVideoAdEvent event, {String rewardType, int rewardAmount}) { + if (event == RewardedVideoAdEvent.rewarded) { + setState(() { + // Here, apps should update state to reflect the reward. + _goldCoins += rewardAmount; + }); + } +}; +``` + +Because `RewardedVideoAd` is a singleton object, it does not offer a `dispose` +method. + +## Limitations + +This is just an initial version of the plugin. There are still some +limitations: + +- Banner ads cannot be animated into view. +- It's not possible to specify a banner ad's size. +- There's no support for native ads. +- The existing tests are fairly rudimentary. +- There is no API doc. +- The example should demonstrate how to show gate a route push with an + interstitial ad + +For Flutter plugins for other Firebase products, see +[FlutterFire.md](https://github.com/flutter/plugins/blob/master/FlutterFire.md). 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 new file mode 100644 index 000000000000..62e6fe4befeb --- /dev/null +++ b/packages/firebase_admob/android/src/main/java/io/flutter/plugins/firebaseadmob/FirebaseAdMobPlugin.java @@ -0,0 +1,236 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.firebaseadmob; + +import android.app.Activity; +import android.view.Gravity; +import com.google.android.gms.ads.AdSize; +import com.google.android.gms.ads.MobileAds; +import com.google.firebase.FirebaseApp; +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; +import io.flutter.plugin.common.MethodChannel.MethodCallHandler; +import io.flutter.plugin.common.MethodChannel.Result; +import io.flutter.plugin.common.PluginRegistry.Registrar; +import java.util.Map; + +public class FirebaseAdMobPlugin implements MethodCallHandler { + + private final Registrar registrar; + private final MethodChannel channel; + + RewardedVideoAdWrapper rewardedWrapper; + + public static void registerWith(Registrar registrar) { + if (registrar.activity() == null) { + // If a background Flutter view tries to register the plugin, there will be no activity from the registrar. + // We stop the registering process immediately because the firebase_admob requires an activity. + return; + } + final MethodChannel channel = + new MethodChannel(registrar.messenger(), "plugins.flutter.io/firebase_admob"); + channel.setMethodCallHandler(new FirebaseAdMobPlugin(registrar, channel)); + } + + private FirebaseAdMobPlugin(Registrar registrar, MethodChannel channel) { + this.registrar = registrar; + this.channel = channel; + FirebaseApp.initializeApp(registrar.context()); + rewardedWrapper = new RewardedVideoAdWrapper(registrar.activity(), channel); + } + + private void callInitialize(MethodCall call, Result result) { + String appId = call.argument("appId"); + if (appId == null || appId.isEmpty()) { + result.error("no_app_id", "a null or empty AdMob appId was provided", null); + return; + } + MobileAds.initialize(registrar.context(), appId); + result.success(Boolean.TRUE); + } + + private void callLoadBannerAd( + int id, Activity activity, MethodChannel channel, MethodCall call, Result result) { + String adUnitId = call.argument("adUnitId"); + if (adUnitId == null || adUnitId.isEmpty()) { + result.error("no_unit_id", "a null or empty adUnitId was provided for ad id=" + id, null); + return; + } + + int width = call.argument("width"); + int height = call.argument("height"); + String adSizeType = call.argument("adSizeType"); + + if (!adSizeType.equals("AdSizeType.WidthAndHeight") + && !adSizeType.equals("AdSizeType.SmartBanner")) { + String errMsg = + String.format("an invalid adSizeType (%s) was provided for banner id=%d", adSizeType, id); + result.error("invalid_adsizetype", errMsg, null); + } + + if (adSizeType.equals("AdSizeType.WidthAndHeight") && (width <= 0 || height <= 0)) { + String errMsg = + String.format( + "an invalid AdSize (%d, %d) was provided for banner id=%d", width, height, id); + result.error("invalid_adsize", errMsg, null); + } + + AdSize adSize; + if (adSizeType.equals("AdSizeType.SmartBanner")) { + adSize = AdSize.SMART_BANNER; + } else { + adSize = new AdSize(width, height); + } + + MobileAd.Banner banner = MobileAd.createBanner(id, adSize, activity, channel); + + if (banner.status != MobileAd.Status.CREATED) { + if (banner.status == MobileAd.Status.FAILED) + result.error("load_failed_ad", "cannot reload a failed ad, id=" + id, null); + else result.success(Boolean.TRUE); // The ad was already loaded. + return; + } + + Map targetingInfo = call.argument("targetingInfo"); + banner.load(adUnitId, targetingInfo); + result.success(Boolean.TRUE); + } + + private void callLoadInterstitialAd(MobileAd ad, MethodCall call, Result result) { + if (ad.status != MobileAd.Status.CREATED) { + if (ad.status == MobileAd.Status.FAILED) + result.error("load_failed_ad", "cannot reload a failed ad, id=" + ad.id, null); + else result.success(Boolean.TRUE); // The ad was already loaded. + return; + } + + String adUnitId = call.argument("adUnitId"); + if (adUnitId == null || adUnitId.isEmpty()) { + result.error( + "no_adunit_id", "a null or empty adUnitId was provided for ad id=" + ad.id, null); + return; + } + Map targetingInfo = call.argument("targetingInfo"); + ad.load(adUnitId, targetingInfo); + result.success(Boolean.TRUE); + } + + private void callLoadRewardedVideoAd(MethodCall call, Result result) { + if (rewardedWrapper.getStatus() != RewardedVideoAdWrapper.Status.CREATED + && rewardedWrapper.getStatus() != RewardedVideoAdWrapper.Status.FAILED) { + result.success(Boolean.TRUE); // The ad was already loading or loaded. + return; + } + + String adUnitId = call.argument("adUnitId"); + if (adUnitId == null || adUnitId.isEmpty()) { + result.error( + "no_ad_unit_id", "a non-empty adUnitId was not provided for rewarded video", null); + return; + } + + Map targetingInfo = call.argument("targetingInfo"); + if (targetingInfo == null) { + result.error( + "no_targeting_info", "a null targetingInfo object was provided for rewarded video", null); + return; + } + + rewardedWrapper.load(adUnitId, targetingInfo); + result.success(Boolean.TRUE); + } + + private void callShowAd(int id, MethodCall call, Result result) { + MobileAd ad = MobileAd.getAdForId(id); + if (ad == null) { + result.error("ad_not_loaded", "show failed, the specified ad was not loaded id=" + id, null); + return; + } + if (call.argument("anchorOffset") != null) { + ad.anchorOffset = Double.parseDouble((String) call.argument("anchorOffset")); + } + if (call.argument("horizontalCenterOffset") != null) { + ad.horizontalCenterOffset = + Double.parseDouble((String) call.argument("horizontalCenterOffset")); + } + if (call.argument("anchorType") != null) { + ad.anchorType = call.argument("anchorType").equals("bottom") ? Gravity.BOTTOM : Gravity.TOP; + } + + ad.show(); + result.success(Boolean.TRUE); + } + + private void callIsAdLoaded(int id, MethodCall call, Result result) { + MobileAd ad = MobileAd.getAdForId(id); + if (ad == null) { + result.error("no_ad_for_id", "isAdLoaded failed, no add exists for id=" + id, null); + return; + } + result.success(ad.status == MobileAd.Status.LOADED ? Boolean.TRUE : Boolean.FALSE); + } + + private void callShowRewardedVideoAd(MethodCall call, Result result) { + if (rewardedWrapper.getStatus() == RewardedVideoAdWrapper.Status.LOADED) { + rewardedWrapper.show(); + result.success(Boolean.TRUE); + } else { + result.error("ad_not_loaded", "show failed for rewarded video, no ad was loaded", null); + } + } + + private void callDisposeAd(int id, MethodCall call, Result result) { + MobileAd ad = MobileAd.getAdForId(id); + if (ad == null) { + result.error("no_ad_for_id", "dispose failed, no add exists for id=" + id, null); + return; + } + + ad.dispose(); + result.success(Boolean.TRUE); + } + + @Override + public void onMethodCall(MethodCall call, Result result) { + if (call.method.equals("initialize")) { + callInitialize(call, result); + return; + } + + Activity activity = registrar.activity(); + if (activity == null) { + result.error("no_activity", "firebase_admob plugin requires a foreground activity", null); + return; + } + + Integer id = call.argument("id"); + + switch (call.method) { + case "loadBannerAd": + callLoadBannerAd(id, activity, channel, call, result); + break; + case "loadInterstitialAd": + callLoadInterstitialAd(MobileAd.createInterstitial(id, activity, channel), call, result); + break; + case "loadRewardedVideoAd": + callLoadRewardedVideoAd(call, result); + break; + case "showAd": + callShowAd(id, call, result); + break; + case "showRewardedVideoAd": + callShowRewardedVideoAd(call, result); + break; + case "disposeAd": + callDisposeAd(id, call, result); + break; + case "isAdLoaded": + callIsAdLoaded(id, call, result); + break; + default: + result.notImplemented(); + } + } +} diff --git a/packages/firebase_admob/android/src/main/java/io/flutter/plugins/firebaseadmob/MobileAd.java b/packages/firebase_admob/android/src/main/java/io/flutter/plugins/firebaseadmob/MobileAd.java new file mode 100644 index 000000000000..a13573194c94 --- /dev/null +++ b/packages/firebase_admob/android/src/main/java/io/flutter/plugins/firebaseadmob/MobileAd.java @@ -0,0 +1,225 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.firebaseadmob; + +import android.app.Activity; +import android.util.Log; +import android.util.SparseArray; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; +import com.google.android.gms.ads.AdListener; +import com.google.android.gms.ads.AdSize; +import com.google.android.gms.ads.AdView; +import com.google.android.gms.ads.InterstitialAd; +import io.flutter.plugin.common.MethodChannel; +import java.util.HashMap; +import java.util.Map; + +abstract class MobileAd extends AdListener { + private static final String TAG = "flutter"; + private static SparseArray allAds = new SparseArray(); + + final Activity activity; + final MethodChannel channel; + final int id; + Status status; + double anchorOffset; + double horizontalCenterOffset; + int anchorType; + + enum Status { + CREATED, + LOADING, + FAILED, + PENDING, // The ad will be shown when status is changed to LOADED. + LOADED, + } + + private MobileAd(int id, Activity activity, MethodChannel channel) { + this.id = id; + this.activity = activity; + this.channel = channel; + this.status = Status.CREATED; + this.anchorOffset = 0.0; + this.horizontalCenterOffset = 0.0; + this.anchorType = Gravity.BOTTOM; + allAds.put(id, this); + } + + static Banner createBanner(Integer id, AdSize adSize, Activity activity, MethodChannel channel) { + MobileAd ad = getAdForId(id); + return (ad != null) ? (Banner) ad : new Banner(id, adSize, activity, channel); + } + + static Interstitial createInterstitial(Integer id, Activity activity, MethodChannel channel) { + MobileAd ad = getAdForId(id); + return (ad != null) ? (Interstitial) ad : new Interstitial(id, activity, channel); + } + + static MobileAd getAdForId(Integer id) { + return allAds.get(id); + } + + Status getStatus() { + return status; + } + + abstract void load(String adUnitId, Map targetingInfo); + + abstract void show(); + + void dispose() { + allAds.remove(id); + } + + private Map argumentsMap(Object... args) { + Map arguments = new HashMap(); + arguments.put("id", id); + for (int i = 0; i < args.length; i += 2) arguments.put(args[i].toString(), args[i + 1]); + return arguments; + } + + @Override + public void onAdLoaded() { + boolean statusWasPending = status == Status.PENDING; + status = Status.LOADED; + channel.invokeMethod("onAdLoaded", argumentsMap()); + if (statusWasPending) show(); + } + + @Override + public void onAdFailedToLoad(int errorCode) { + Log.w(TAG, "onAdFailedToLoad: " + errorCode); + status = Status.FAILED; + channel.invokeMethod("onAdFailedToLoad", argumentsMap("errorCode", errorCode)); + } + + @Override + public void onAdOpened() { + channel.invokeMethod("onAdOpened", argumentsMap()); + } + + @Override + public void onAdClicked() { + channel.invokeMethod("onAdClicked", argumentsMap()); + } + + @Override + public void onAdImpression() { + channel.invokeMethod("onAdImpression", argumentsMap()); + } + + @Override + public void onAdLeftApplication() { + channel.invokeMethod("onAdLeftApplication", argumentsMap()); + } + + @Override + public void onAdClosed() { + channel.invokeMethod("onAdClosed", argumentsMap()); + } + + static class Banner extends MobileAd { + private AdView adView; + private AdSize adSize; + + private Banner(Integer id, AdSize adSize, Activity activity, MethodChannel channel) { + super(id, activity, channel); + this.adSize = adSize; + } + + @Override + void load(String adUnitId, Map targetingInfo) { + if (status != Status.CREATED) return; + status = Status.LOADING; + + adView = new AdView(activity); + adView.setAdSize(adSize); + adView.setAdUnitId(adUnitId); + adView.setAdListener(this); + + AdRequestBuilderFactory factory = new AdRequestBuilderFactory(targetingInfo); + adView.loadAd(factory.createAdRequestBuilder().build()); + } + + @Override + void show() { + if (status == Status.LOADING) { + status = Status.PENDING; + return; + } + if (status != Status.LOADED) return; + + if (activity.findViewById(id) == null) { + LinearLayout content = new LinearLayout(activity); + content.setId(id); + content.setOrientation(LinearLayout.VERTICAL); + content.setGravity(anchorType); + content.addView(adView); + final float scale = activity.getResources().getDisplayMetrics().density; + + int left = horizontalCenterOffset > 0 ? (int) (horizontalCenterOffset * scale) : 0; + int right = + horizontalCenterOffset < 0 ? (int) (Math.abs(horizontalCenterOffset) * scale) : 0; + if (anchorType == Gravity.BOTTOM) { + content.setPadding(left, 0, right, (int) (anchorOffset * scale)); + } else { + content.setPadding(left, (int) (anchorOffset * scale), right, 0); + } + + activity.addContentView( + content, + new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + } + } + + @Override + void dispose() { + super.dispose(); + + View contentView = activity.findViewById(id); + if (contentView == null || !(contentView.getParent() instanceof ViewGroup)) return; + + adView.destroy(); + + ViewGroup contentParent = (ViewGroup) (contentView.getParent()); + contentParent.removeView(contentView); + } + } + + static class Interstitial extends MobileAd { + private InterstitialAd interstitial = null; + + private Interstitial(int id, Activity activity, MethodChannel channel) { + super(id, activity, channel); + } + + @Override + void load(String adUnitId, Map targetingInfo) { + status = Status.LOADING; + + interstitial = new InterstitialAd(activity); + interstitial.setAdUnitId(adUnitId); + + interstitial.setAdListener(this); + AdRequestBuilderFactory factory = new AdRequestBuilderFactory(targetingInfo); + interstitial.loadAd(factory.createAdRequestBuilder().build()); + } + + @Override + void show() { + if (status == Status.LOADING) { + status = Status.PENDING; + return; + } + interstitial.show(); + } + + // It is not possible to hide/remove/destroy an AdMob interstitial Ad. + } +} diff --git a/packages/firebase_admob/example/lib/main.dart b/packages/firebase_admob/example/lib/main.dart new file mode 100644 index 000000000000..e5014d652f13 --- /dev/null +++ b/packages/firebase_admob/example/lib/main.dart @@ -0,0 +1,153 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:firebase_admob/firebase_admob.dart'; + +// You can also test with your own ad unit IDs by registering your device as a +// test device. Check the logs for your device's ID value. +const String testDevice = 'YOUR_DEVICE_ID'; + +class MyApp extends StatefulWidget { + @override + _MyAppState createState() => _MyAppState(); +} + +class _MyAppState extends State { + static const MobileAdTargetingInfo targetingInfo = MobileAdTargetingInfo( + testDevices: testDevice != null ? [testDevice] : null, + keywords: ['foo', 'bar'], + contentUrl: 'http://foo.com/bar.html', + childDirected: true, + nonPersonalizedAds: true, + ); + + BannerAd _bannerAd; + InterstitialAd _interstitialAd; + int _coins = 0; + + BannerAd createBannerAd() { + return BannerAd( + adUnitId: BannerAd.testAdUnitId, + size: AdSize.banner, + targetingInfo: targetingInfo, + listener: (MobileAdEvent event) { + print("BannerAd event $event"); + }, + ); + } + + InterstitialAd createInterstitialAd() { + return InterstitialAd( + adUnitId: InterstitialAd.testAdUnitId, + targetingInfo: targetingInfo, + listener: (MobileAdEvent event) { + print("InterstitialAd event $event"); + }, + ); + } + + @override + void initState() { + super.initState(); + FirebaseAdMob.instance.initialize(appId: FirebaseAdMob.testAppId); + _bannerAd = createBannerAd()..load(); + RewardedVideoAd.instance.listener = + (RewardedVideoAdEvent event, {String rewardType, int rewardAmount}) { + print("RewardedVideoAd event $event"); + if (event == RewardedVideoAdEvent.rewarded) { + setState(() { + _coins += rewardAmount; + }); + } + }; + } + + @override + void dispose() { + _bannerAd?.dispose(); + _interstitialAd?.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Scaffold( + appBar: AppBar( + title: const Text('AdMob Plugin example app'), + ), + body: SingleChildScrollView( + child: Center( + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + RaisedButton( + child: const Text('SHOW BANNER'), + onPressed: () { + _bannerAd ??= createBannerAd(); + _bannerAd + ..load() + ..show(); + }), + RaisedButton( + child: const Text('SHOW BANNER WITH OFFSET'), + onPressed: () { + _bannerAd ??= createBannerAd(); + _bannerAd + ..load() + ..show(horizontalCenterOffset: -50, anchorOffset: 100); + }), + RaisedButton( + child: const Text('REMOVE BANNER'), + onPressed: () { + _bannerAd?.dispose(); + _bannerAd = null; + }), + RaisedButton( + child: const Text('LOAD INTERSTITIAL'), + onPressed: () { + _interstitialAd?.dispose(); + _interstitialAd = createInterstitialAd()..load(); + }, + ), + RaisedButton( + child: const Text('SHOW INTERSTITIAL'), + onPressed: () { + _interstitialAd?.show(); + }, + ), + RaisedButton( + child: const Text('LOAD REWARDED VIDEO'), + onPressed: () { + RewardedVideoAd.instance.load( + adUnitId: RewardedVideoAd.testAdUnitId, + targetingInfo: targetingInfo); + }, + ), + RaisedButton( + child: const Text('SHOW REWARDED VIDEO'), + onPressed: () { + RewardedVideoAd.instance.show(); + }, + ), + Text("You have $_coins coins."), + ].map((Widget button) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: button, + ); + }).toList(), + ), + ), + ), + ), + ); + } +} + +void main() { + runApp(MyApp()); +} diff --git a/packages/firebase_admob/ios/Classes/FLTMobileAd.h b/packages/firebase_admob/ios/Classes/FLTMobileAd.h new file mode 100644 index 000000000000..1979be13eb4e --- /dev/null +++ b/packages/firebase_admob/ios/Classes/FLTMobileAd.h @@ -0,0 +1,36 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import +#import "GoogleMobileAds/GoogleMobileAds.h" + +typedef enum : NSUInteger { + CREATED, + LOADING, + FAILED, + PENDING, // Will be shown when status is changed to LOADED. + LOADED, +} FLTMobileAdStatus; + +@interface FLTMobileAd : NSObject ++ (void)configureWithAppId:(NSString *)appId; ++ (FLTMobileAd *)getAdForId:(NSNumber *)mobileAdId; +- (FLTMobileAdStatus)status; +- (void)loadWithAdUnitId:(NSString *)adUnitId targetingInfo:(NSDictionary *)targetingInfo; +- (void)show; +- (void)showAtOffset:(double)anchorOffset + hCenterOffset:(double)horizontalCenterOffset + fromAnchor:(int)anchorType; +- (void)dispose; +@end + +@interface FLTBannerAd : FLTMobileAd ++ (instancetype)withId:(NSNumber *)mobileAdId + adSize:(GADAdSize)adSize + channel:(FlutterMethodChannel *)channel; +@end + +@interface FLTInterstitialAd : FLTMobileAd ++ (instancetype)withId:(NSNumber *)mobileAdId channel:(FlutterMethodChannel *)channel; +@end diff --git a/packages/firebase_admob/ios/Classes/FLTMobileAd.m b/packages/firebase_admob/ios/Classes/FLTMobileAd.m new file mode 100644 index 000000000000..9e263406d620 --- /dev/null +++ b/packages/firebase_admob/ios/Classes/FLTMobileAd.m @@ -0,0 +1,289 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "FLTMobileAd.h" +#import "FLTRequestFactory.h" +#import "FirebaseAdMobPlugin.h" + +static NSMutableDictionary *allAds = nil; +static NSDictionary *statusToString = nil; + +@implementation FLTMobileAd +NSNumber *_mobileAdId; +FlutterMethodChannel *_channel; +FLTMobileAdStatus _status; +double _anchorOffset; +double _horizontalCenterOffset; +int _anchorType; + ++ (void)initialize { + if (allAds == nil) { + allAds = [[NSMutableDictionary alloc] init]; + } + _anchorType = 0; + _anchorOffset = 0; + _horizontalCenterOffset = 0; + + if (statusToString == nil) { + statusToString = @{ + @(CREATED) : @"CREATED", + @(LOADING) : @"LOADING", + @(FAILED) : @"FAILED", + @(PENDING) : @"PENDING", + @(LOADED) : @"LOADED" + }; + } +} + ++ (void)configureWithAppId:(NSString *)appId { + [GADMobileAds configureWithApplicationID:appId]; +} + ++ (FLTMobileAd *)getAdForId:(NSNumber *)mobileAdId { + return allAds[mobileAdId]; +} + ++ (UIViewController *)rootViewController { + return [UIApplication sharedApplication].delegate.window.rootViewController; +} + +- (instancetype)initWithId:(NSNumber *)mobileAdId channel:(FlutterMethodChannel *)channel { + self = [super init]; + if (self) { + _mobileAdId = mobileAdId; + _channel = channel; + _status = CREATED; + _anchorOffset = 0; + _horizontalCenterOffset = 0; + _anchorType = 0; + allAds[mobileAdId] = self; + } + return self; +} + +- (FLTMobileAdStatus)status { + return _status; +} + +- (void)loadWithAdUnitId:(NSString *)adUnitId targetingInfo:(NSDictionary *)targetingInfo { + // Implemented by the Banner and Interstitial subclasses +} + +- (void)showAtOffset:(double)anchorOffset + hCenterOffset:(double)horizontalCenterOffset + fromAnchor:(int)anchorType { + _anchorType = anchorType; + _anchorOffset = anchorOffset; + if (_anchorType == 0) { + _anchorOffset = -_anchorOffset; + } + _horizontalCenterOffset = horizontalCenterOffset; + [self show]; +} + +- (void)show { + // Implemented by the Banner and Interstitial subclasses +} + +- (void)dispose { + [allAds removeObjectForKey:_mobileAdId]; +} + +- (NSDictionary *)argumentsMap { + return @{@"id" : _mobileAdId}; +} + +- (NSString *)description { + NSString *statusString = (NSString *)statusToString[[NSNumber numberWithInt:_status]]; + return [NSString + stringWithFormat:@"%@ %@ mobileAdId:%@", super.description, statusString, _mobileAdId]; +} +@end + +@implementation FLTBannerAd +GADBannerView *_banner; +GADAdSize _adSize; + ++ (instancetype)withId:(NSNumber *)mobileAdId + adSize:(GADAdSize)adSize + channel:(FlutterMethodChannel *)channel { + FLTMobileAd *ad = [FLTMobileAd getAdForId:mobileAdId]; + return ad != nil ? (FLTBannerAd *)ad + : [[FLTBannerAd alloc] initWithId:mobileAdId adSize:adSize channel:channel]; +} + +- (instancetype)initWithId:mobileAdId + adSize:(GADAdSize)adSize + channel:(FlutterMethodChannel *)channel { + self = [super initWithId:mobileAdId channel:channel]; + if (self) { + _adSize = adSize; + return self; + } + + return nil; +} + +- (void)loadWithAdUnitId:(NSString *)adUnitId targetingInfo:(NSDictionary *)targetingInfo { + if (_status != CREATED) return; + _status = LOADING; + _banner = [[GADBannerView alloc] initWithAdSize:_adSize]; + _banner.delegate = self; + _banner.adUnitID = adUnitId; + _banner.rootViewController = [FLTMobileAd rootViewController]; + FLTRequestFactory *factory = [[FLTRequestFactory alloc] initWithTargetingInfo:targetingInfo]; + [_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); + [_channel invokeMethod:@"onAdFailedToLoad" arguments:[self argumentsMap]]; +} + +- (void)adViewWillPresentScreen:(GADBannerView *)adView { + [_channel invokeMethod:@"onAdClicked" arguments:[self argumentsMap]]; +} + +- (void)adViewWillDismissScreen:(GADBannerView *)adView { + [_channel invokeMethod:@"onAdImpression" arguments:[self argumentsMap]]; +} + +- (void)adViewDidDismissScreen:(GADBannerView *)adView { + [_channel invokeMethod:@"onAdClosed" arguments:[self argumentsMap]]; +} + +- (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]; +} +@end + +@implementation FLTInterstitialAd +GADInterstitial *_interstitial; + ++ (instancetype)withId:(NSNumber *)mobileAdId channel:(FlutterMethodChannel *)channel { + FLTMobileAd *ad = [FLTMobileAd getAdForId:mobileAdId]; + return ad != nil ? (FLTInterstitialAd *)ad + : [[FLTInterstitialAd alloc] initWithId:mobileAdId channel:channel]; +} + +- (void)loadWithAdUnitId:(NSString *)adUnitId targetingInfo:(NSDictionary *)targetingInfo { + if (_status != CREATED) return; + _status = LOADING; + + _interstitial = [[GADInterstitial alloc] initWithAdUnitID:adUnitId]; + _interstitial.delegate = self; + FLTRequestFactory *factory = [[FLTRequestFactory alloc] initWithTargetingInfo:targetingInfo]; + [_interstitial loadRequest:[factory createRequest]]; +} + +- (void)show { + if (_status == LOADING) { + _status = PENDING; + return; + } + if (_status != LOADED) return; + + [_interstitial presentFromRootViewController:[FLTMobileAd rootViewController]]; +} + +- (void)interstitialDidReceiveAd:(GADInterstitial *)ad { + bool statusWasPending = _status == PENDING; + _status = LOADED; + [_channel invokeMethod:@"onAdLoaded" arguments:[self argumentsMap]]; + if (statusWasPending) [self show]; +} + +- (void)interstitial:(GADInterstitial *)ad didFailToReceiveAdWithError:(GADRequestError *)error { + FLTLogWarning(@"interstitial:didFailToReceiveAdWithError: %@ (MobileAd %@)", + [error localizedDescription], self); + [_channel invokeMethod:@"onAdFailedToLoad" arguments:[self argumentsMap]]; +} + +- (void)interstitialWillPresentScreen:(GADInterstitial *)ad { + [_channel invokeMethod:@"onAdClicked" arguments:[self argumentsMap]]; +} + +- (void)interstitialWillDismissScreen:(GADInterstitial *)ad { + [_channel invokeMethod:@"onAdImpression" arguments:[self argumentsMap]]; +} + +- (void)interstitialDidDismissScreen:(GADInterstitial *)ad { + [_channel invokeMethod:@"onAdClosed" arguments:[self argumentsMap]]; +} + +- (void)interstitialWillLeaveApplication:(GADInterstitial *)ad { + [_channel invokeMethod:@"onAdLeftApplication" arguments:[self argumentsMap]]; +} + +- (void)dispose { + // It is not possible to hide/remove/destroy an AdMob interstitial Ad. + _interstitial = nil; + [super dispose]; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"%@ for: %@", super.description, _interstitial]; +} +@end diff --git a/packages/firebase_admob/ios/Classes/FirebaseAdMobPlugin.m b/packages/firebase_admob/ios/Classes/FirebaseAdMobPlugin.m new file mode 100644 index 000000000000..d94762b9b61f --- /dev/null +++ b/packages/firebase_admob/ios/Classes/FirebaseAdMobPlugin.m @@ -0,0 +1,291 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "FirebaseAdMobPlugin.h" + +#import + +#import "FLTMobileAd.h" +#import "FLTRewardedVideoAdWrapper.h" +#import "Firebase/Firebase.h" + +@interface FLTFirebaseAdMobPlugin () +@property(nonatomic, retain) FlutterMethodChannel *channel; +@property(nonatomic, strong) FLTRewardedVideoAdWrapper *rewardedWrapper; +@end + +@implementation FLTFirebaseAdMobPlugin + ++ (void)registerWithRegistrar:(NSObject *)registrar { + FLTFirebaseAdMobPlugin *instance = [[FLTFirebaseAdMobPlugin alloc] init]; + 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]; +} + +- (instancetype)init { + self = [super init]; + if (self && ![FIRApp appNamed:@"__FIRAPP_DEFAULT"]) { + NSLog(@"Configuring the default Firebase app..."); + [FIRApp configure]; + NSLog(@"Configured the default Firebase app %@.", [FIRApp defaultApp].name); + } + return self; +} + +- (void)dealloc { + [self.channel setMethodCallHandler:nil]; + self.channel = nil; +} + +- (void)callInitialize:(FlutterMethodCall *)call result:(FlutterResult)result { + NSString *appId = (NSString *)call.arguments[@"appId"]; + if (appId == nil || [appId length] == 0) { + result([FlutterError errorWithCode:@"no_app_id" + message:@"a non-empty AdMob appId was not provided" + details:nil]); + return; + } + [FLTMobileAd configureWithAppId:appId]; + result([NSNumber numberWithBool:YES]); +} + +- (void)callLoadBannerAdWithId:(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; + } + + NSNumber *widthArg = (NSNumber *)call.arguments[@"width"]; + NSNumber *heightArg = (NSNumber *)call.arguments[@"height"]; + + if (widthArg == nil || heightArg == nil) { + NSString *message = + [NSString stringWithFormat:@"a null height or width was provided for banner id=%@", id]; + result([FlutterError errorWithCode:@"invalid_adsize" message:message details:nil]); + return; + } + + NSString *adSizeTypeArg = (NSString *)call.arguments[@"adSizeType"]; + FLTLogWarning(@"Size Type: %@", adSizeTypeArg); + if (adSizeTypeArg == nil || (![adSizeTypeArg isEqualToString:@"AdSizeType.SmartBanner"] && + ![adSizeTypeArg isEqualToString:@"AdSizeType.WidthAndHeight"])) { + NSString *message = [NSString + stringWithFormat:@"a null or invalid ad size type was provided for banner id=%@", id]; + result([FlutterError errorWithCode:@"invalid_adsizetype" message:message details:nil]); + return; + } + + int width = [widthArg intValue]; + int height = [heightArg intValue]; + + if ([adSizeTypeArg isEqualToString:@"AdSizeType.WidthAndHeight"] && (width <= 0 || height <= 0)) { + NSString *message = + [NSString stringWithFormat:@"an invalid AdSize (%d, %d) was provided for banner id=%@", + width, height, id]; + result([FlutterError errorWithCode:@"invalid_adsize" message:message details:nil]); + return; + } + + GADAdSize adSize; + if ([adSizeTypeArg isEqualToString:@"AdSizeType.SmartBanner"]) { + if (UIDeviceOrientationIsPortrait([UIDevice currentDevice].orientation)) { + adSize = kGADAdSizeSmartBannerPortrait; + } else { + adSize = kGADAdSizeSmartBannerLandscape; + } + } else { + adSize = GADAdSizeFromCGSize(CGSizeMake(width, height)); + } + + FLTBannerAd *banner = [FLTBannerAd withId:id adSize:adSize channel:self.channel]; + + if (banner.status != CREATED) { + if (banner.status == FAILED) { + NSString *message = [NSString stringWithFormat:@"cannot reload a failed ad=%@", banner]; + result([FlutterError errorWithCode:@"load_failed_ad" message:message details:nil]); + } else { + result([NSNumber numberWithBool:YES]); // The ad was already loaded. + } + } + + NSDictionary *targetingInfo = (NSDictionary *)call.arguments[@"targetingInfo"]; + [banner loadWithAdUnitId:adUnitId targetingInfo:targetingInfo]; + result([NSNumber numberWithBool:YES]); +} + +- (void)callLoadInterstitialAd:(FLTMobileAd *)ad + call:(FlutterMethodCall *)call + result:(FlutterResult)result { + if (ad.status != CREATED) { + if (ad.status == FAILED) { + NSString *message = [NSString stringWithFormat:@"cannot reload a failed ad=%@", ad]; + result([FlutterError errorWithCode:@"load_failed_ad" message:message details:nil]); + } else { + result([NSNumber numberWithBool:YES]); // The ad was already loaded. + } + } + + NSString *adUnitId = (NSString *)call.arguments[@"adUnitId"]; + if (adUnitId == nil || [adUnitId length] == 0) { + NSString *message = + [NSString stringWithFormat:@"a null or emtpy adUnitId was provided for %@", ad]; + result([FlutterError errorWithCode:@"no_adunit_id" message:message details:nil]); + return; + } + + NSDictionary *targetingInfo = (NSDictionary *)call.arguments[@"targetingInfo"]; + [ad loadWithAdUnitId:adUnitId targetingInfo:targetingInfo]; + result([NSNumber numberWithBool:YES]); +} + +- (void)callLoadRewardedVideoAd:(FlutterMethodCall *)call result:(FlutterResult)result { + if (self.rewardedWrapper.status == FLTRewardedVideoAdStatusLoading || + self.rewardedWrapper.status == FLTRewardedVideoAdStatusLoaded) { + result([NSNumber numberWithBool:YES]); // The ad is loaded or about to be. + } + + NSString *adUnitId = (NSString *)call.arguments[@"adUnitId"]; + if (adUnitId == nil || [adUnitId length] == 0) { + result([FlutterError errorWithCode:@"no_ad_unit_id" + message:@"a non-empty adUnitId was not provided for rewarded video." + details:nil]); + return; + } + + NSDictionary *targetingInfo = (NSDictionary *)call.arguments[@"targetingInfo"]; + if (targetingInfo == nil) { + result([FlutterError + errorWithCode:@"no_targeting_info" + message:@"a null targetingInfo object was provided for rewarded video." + details:nil]); + return; + } + + [self.rewardedWrapper loadWithAdUnitId:adUnitId targetingInfo:targetingInfo]; + result([NSNumber numberWithBool:YES]); +} + +- (void)callShowAd:(NSNumber *)mobileAdId + call:(FlutterMethodCall *)call + result:(FlutterResult)result { + FLTMobileAd *ad = [FLTMobileAd getAdForId:mobileAdId]; + if (ad == nil) { + NSString *message = + [NSString stringWithFormat:@"show failed, the specified ad was not loaded id=%d", + mobileAdId.intValue]; + result([FlutterError errorWithCode:@"ad_not_loaded" message:message details:nil]); + } + + double offset = 0.0; + double horizontalCenterOffset = 0.0; + int type = 0; + if (call.arguments[@"anchorOffset"] != nil) { + offset = [call.arguments[@"anchorOffset"] doubleValue]; + } + if (call.arguments[@"horizontalCenterOffset"] != nil) { + horizontalCenterOffset = [call.arguments[@"horizontalCenterOffset"] doubleValue]; + } + if (call.arguments[@"anchorType"] != nil) { + type = [call.arguments[@"anchorType"] isEqualToString:@"bottom"] ? 0 : 1; + } + + [ad showAtOffset:offset hCenterOffset:horizontalCenterOffset fromAnchor:type]; + result([NSNumber numberWithBool:YES]); +} + +- (void)callIsAdLoaded:(NSNumber *)mobileAdId + call:(FlutterMethodCall *)call + result:(FlutterResult)result { + FLTMobileAd *ad = [FLTMobileAd getAdForId:mobileAdId]; + if (ad == nil) { + NSString *message = [NSString + stringWithFormat:@"isAdLoaded failed, no ad exists for id=%d", mobileAdId.intValue]; + result([FlutterError errorWithCode:@"no_ad_for_id" message:message details:nil]); + return; + } + if (ad.status == LOADED) { + result([NSNumber numberWithBool:YES]); + } else { + result([NSNumber numberWithBool:NO]); + } +} + +- (void)callShowRewardedVideoAd:(FlutterMethodCall *)call result:(FlutterResult)result { + if (self.rewardedWrapper.status != FLTRewardedVideoAdStatusLoaded) { + result([FlutterError errorWithCode:@"ad_not_loaded" + message:@"show failed for rewarded video, no ad was loaded" + details:nil]); + return; + } + + [self.rewardedWrapper show]; + result([NSNumber numberWithBool:YES]); +} + +- (void)callDisposeAd:(NSNumber *)mobileAdId + call:(FlutterMethodCall *)call + result:(FlutterResult)result { + FLTMobileAd *ad = [FLTMobileAd getAdForId:mobileAdId]; + if (ad == nil) { + NSString *message = + [NSString stringWithFormat:@"dispose failed, no ad exists for id=%d", mobileAdId.intValue]; + result([FlutterError errorWithCode:@"no_ad_for_id" message:message details:nil]); + } + + [ad dispose]; + result([NSNumber numberWithBool:YES]); +} + +- (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result { + if ([call.method isEqualToString:@"initialize"]) { + [self callInitialize:call result:result]; + return; + } + + if ([call.method isEqualToString:@"loadRewardedVideoAd"]) { + [self callLoadRewardedVideoAd:call result:result]; + return; + } + + if ([call.method isEqualToString:@"showRewardedVideoAd"]) { + [self callShowRewardedVideoAd:call result:result]; + return; + } + + NSNumber *mobileAdId = (NSNumber *)call.arguments[@"id"]; + if (mobileAdId == nil) { + NSString *message = @"FirebaseAdMobPlugin method calls for banners and " + @"interstitials must specify an " + @"integer mobile ad id"; + result([FlutterError errorWithCode:@"no_id" message:message details:nil]); + return; + } + + if ([call.method isEqualToString:@"loadBannerAd"]) { + [self callLoadBannerAdWithId:mobileAdId channel:self.channel call:call result:result]; + } else if ([call.method isEqualToString:@"loadInterstitialAd"]) { + [self callLoadInterstitialAd:[FLTInterstitialAd withId: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"]) { + [self callIsAdLoaded:mobileAdId call:call result:result]; + } else if ([call.method isEqualToString:@"disposeAd"]) { + [self callDisposeAd:mobileAdId call:call result:result]; + } else { + result(FlutterMethodNotImplemented); + } +} + +@end diff --git a/packages/firebase_admob/lib/firebase_admob.dart b/packages/firebase_admob/lib/firebase_admob.dart new file mode 100644 index 000000000000..70289b2b5dc2 --- /dev/null +++ b/packages/firebase_admob/lib/firebase_admob.dart @@ -0,0 +1,528 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: deprecated_member_use_from_same_package + +import 'dart:async'; +import 'dart:io' show Platform; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:meta/meta.dart'; + +/// [MobileAd] status changes reported to [MobileAdListener]s. +/// +/// Applications can wait until an ad is [MobileAdEvent.loaded] before showing +/// it, to ensure that the ad is displayed promptly. +enum MobileAdEvent { + loaded, + failedToLoad, + clicked, + impression, + opened, + leftApplication, + closed, +} + +/// The user's gender for the sake of ad targeting using [MobileAdTargetingInfo]. +// Warning: the index values of the enums must match the values of the corresponding +// AdMob constants. For example MobileAdGender.female.index == kGADGenderFemale. +@Deprecated('This functionality is deprecated in AdMob without replacement.') +enum MobileAdGender { + unknown, + male, + female, +} + +/// Signature for a [MobileAd] status change callback. +typedef void MobileAdListener(MobileAdEvent event); + +/// Targeting info per the native AdMob API. +/// +/// This class's properties mirror the native AdRequest API. See for example: +/// [AdRequest.Builder for Android](https://firebase.google.com/docs/reference/android/com/google/android/gms/ads/AdRequest.Builder). +class MobileAdTargetingInfo { + const MobileAdTargetingInfo( + {this.keywords, + this.contentUrl, + @Deprecated('This functionality is deprecated in AdMob without replacement.') + this.birthday, + @Deprecated('This functionality is deprecated in AdMob without replacement.') + this.gender, + @Deprecated('Use `childDirected` instead.') + this.designedForFamilies, + this.childDirected, + this.testDevices, + this.nonPersonalizedAds}); + + final List keywords; + final String contentUrl; + @Deprecated('This functionality is deprecated in AdMob without replacement.') + final DateTime birthday; + @Deprecated('This functionality is deprecated in AdMob without replacement.') + final MobileAdGender gender; + @Deprecated( + 'This functionality is deprecated in AdMob. Use `childDirected` instead.') + final bool designedForFamilies; + final bool childDirected; + final List testDevices; + final bool nonPersonalizedAds; + + Map toJson() { + final Map json = { + 'requestAgent': 'flutter-alpha', + }; + + if (keywords != null && keywords.isNotEmpty) { + assert(keywords.every((String s) => s != null && s.isNotEmpty)); + json['keywords'] = keywords; + } + if (nonPersonalizedAds != null) + json['nonPersonalizedAds'] = nonPersonalizedAds; + if (contentUrl != null && contentUrl.isNotEmpty) + json['contentUrl'] = contentUrl; + if (birthday != null) json['birthday'] = birthday.millisecondsSinceEpoch; + if (gender != null) json['gender'] = gender.index; + if (designedForFamilies != null) + json['designedForFamilies'] = designedForFamilies; + if (childDirected != null) json['childDirected'] = childDirected; + if (testDevices != null && testDevices.isNotEmpty) { + assert(testDevices.every((String s) => s != null && s.isNotEmpty)); + json['testDevices'] = testDevices; + } + + return json; + } +} + +enum AnchorType { bottom, top } + +// The types of ad sizes supported for banners. The names of the values are used +// in MethodChannel calls to iOS and Android, and should not be changed. +enum AdSizeType { + WidthAndHeight, + SmartBanner, +} + +/// [AdSize] represents the size of a banner ad. There are six sizes available, +/// which are the same for both iOS and Android. See the guides for banners on +/// [Android](https://developers.google.com/admob/android/banner#banner_sizes) +/// and [iOS](https://developers.google.com/admob/ios/banner#banner_sizes) for +/// additional details. +class AdSize { + // Private constructor. Apps should use the static constants rather than + // create their own instances of [AdSize]. + const AdSize._({ + @required this.width, + @required this.height, + @required this.adSizeType, + }); + + final int height; + final int width; + final AdSizeType adSizeType; + + /// The standard banner (320x50) size. + static const AdSize banner = AdSize._( + width: 320, + height: 50, + adSizeType: AdSizeType.WidthAndHeight, + ); + + /// The large banner (320x100) size. + static const AdSize largeBanner = AdSize._( + width: 320, + height: 100, + adSizeType: AdSizeType.WidthAndHeight, + ); + + /// The medium rectangle (300x250) size. + static const AdSize mediumRectangle = AdSize._( + width: 300, + height: 250, + adSizeType: AdSizeType.WidthAndHeight, + ); + + /// The full banner (468x60) size. + static const AdSize fullBanner = AdSize._( + width: 468, + height: 60, + adSizeType: AdSizeType.WidthAndHeight, + ); + + /// The leaderboard (728x90) size. + static const AdSize leaderboard = AdSize._( + width: 728, + height: 90, + adSizeType: AdSizeType.WidthAndHeight, + ); + + /// The smart banner size. Smart banners are unique in that the width and + /// height values declared here aren't used. At runtime, the Mobile Ads SDK + /// will automatically adjust the banner's width to match the width of the + /// displaying device's screen. It will also set the banner's height using a + /// calculation based on the displaying device's height. For more info see the + /// [Android](https://developers.google.com/admob/android/banner) and + /// [iOS](https://developers.google.com/admob/ios/banner) banner ad guides. + static const AdSize smartBanner = AdSize._( + width: 0, + height: 0, + adSizeType: AdSizeType.SmartBanner, + ); +} + +/// A mobile [BannerAd] or [InterstitialAd] for the [FirebaseAdMobPlugin]. +/// +/// A [MobileAd] must be loaded with [load] before it is shown with [show]. +/// +/// A valid [adUnitId] is required. +abstract class MobileAd { + /// Default constructor, used by subclasses. + MobileAd( + {@required this.adUnitId, + MobileAdTargetingInfo targetingInfo, + this.listener}) + : _targetingInfo = targetingInfo ?? const MobileAdTargetingInfo() { + assert(adUnitId != null && adUnitId.isNotEmpty); + assert(_allAds[id] == null); + _allAds[id] = this; + } + + static final Map _allAds = {}; + + /// Optional targeting info per the native AdMob API. + MobileAdTargetingInfo get targetingInfo => _targetingInfo; + final MobileAdTargetingInfo _targetingInfo; + + /// Identifies the source of ads for your application. + /// + /// For testing use a [sample ad unit](https://developers.google.com/admob/ios/test-ads#sample_ad_units). + final String adUnitId; + + /// Called when the status of the ad changes. + MobileAdListener listener; + + /// An internal id that identifies this mobile ad to the native AdMob plugin. + /// + /// Plugin log messages will identify this property as the ad's `mobileAdId`. + int get id => hashCode; + + /// Start loading this ad. + Future load(); + + /// Show this ad. + /// + /// The ad must have been loaded with [load] first. If loading hasn't finished + /// the ad will not actually appear until the ad has finished loading. + /// + /// The [listener] will be notified when the ad has finished loading or fails + /// to do so. An ad that fails to load will not be shown. + /// + /// anchorOffset is the logical pixel offset from the edge of the screen (default 0.0) + /// anchorType place advert at top or bottom of screen (default bottom) + Future show( + {double anchorOffset = 0.0, + double horizontalCenterOffset = 0.0, + AnchorType anchorType = AnchorType.bottom}) { + return _invokeBooleanMethod("showAd", { + 'id': id, + 'anchorOffset': anchorOffset.toString(), + 'horizontalCenterOffset': horizontalCenterOffset.toString(), + 'anchorType': describeEnum(anchorType) + }); + } + + /// Free the plugin resources associated with this ad. + /// + /// Disposing a banner ad that's been shown removes it from the screen. + /// Interstitial ads can't be programmatically removed from view. + Future dispose() { + assert(_allAds[id] != null); + _allAds[id] = null; + return _invokeBooleanMethod("disposeAd", {'id': id}); + } + + Future isLoaded() { + return _invokeBooleanMethod("isAdLoaded", { + 'id': id, + }); + } +} + +/// A banner ad for the [FirebaseAdMobPlugin]. +class BannerAd extends MobileAd { + /// Create a BannerAd. + /// + /// A valid [adUnitId] is required. + BannerAd({ + @required String adUnitId, + @required this.size, + MobileAdTargetingInfo targetingInfo, + MobileAdListener listener, + }) : super( + adUnitId: adUnitId, + targetingInfo: targetingInfo, + listener: listener); + + final AdSize size; + + /// These are AdMob's test ad unit IDs, which always return test ads. You're + /// encouraged to use them for testing in your own apps. + static final String testAdUnitId = Platform.isAndroid + ? 'ca-app-pub-3940256099942544/6300978111' + : 'ca-app-pub-3940256099942544/2934735716'; + + @override + Future load() { + return _invokeBooleanMethod("loadBannerAd", { + 'id': id, + 'adUnitId': adUnitId, + 'targetingInfo': targetingInfo?.toJson(), + 'width': size.width, + 'height': size.height, + 'adSizeType': size.adSizeType.toString(), + }); + } +} + +/// A full-screen interstitial ad for the [FirebaseAdMobPlugin]. +class InterstitialAd extends MobileAd { + /// Create an Interstitial. + /// + /// A valid [adUnitId] is required. + InterstitialAd({ + String adUnitId, + MobileAdTargetingInfo targetingInfo, + MobileAdListener listener, + }) : super( + adUnitId: adUnitId, + targetingInfo: targetingInfo, + listener: listener); + + /// A platform-specific AdMob test ad unit ID for interstitials. This ad unit + /// has been specially configured to always return test ads, and developers + /// are encouraged to use it while building and testing their apps. + static final String testAdUnitId = Platform.isAndroid + ? 'ca-app-pub-3940256099942544/1033173712' + : 'ca-app-pub-3940256099942544/4411468910'; + + @override + Future load() { + return _invokeBooleanMethod("loadInterstitialAd", { + 'id': id, + 'adUnitId': adUnitId, + 'targetingInfo': targetingInfo?.toJson(), + }); + } +} + +/// [RewardedVideoAd] status changes reported to [RewardedVideoAdListener]s. +/// +/// The [rewarded] event is particularly important, since it indicates that the +/// user has watched a video for long enough to be given an in-app reward. +enum RewardedVideoAdEvent { + loaded, + failedToLoad, + opened, + leftApplication, + closed, + rewarded, + started, + completed, +} + +/// Signature for a [RewardedVideoAd] status change callback. The optional +/// parameters are only used when the [RewardedVideoAdEvent.rewarded] event +/// is sent, when they'll contain the reward amount and reward type that were +/// configured for the AdMob ad unit when it was created. They will be null for +/// all other events. +typedef void RewardedVideoAdListener(RewardedVideoAdEvent event, + {String rewardType, int rewardAmount}); + +/// An AdMob rewarded video ad. +/// +/// This class is a singleton, and [RewardedVideoAd.instance] provides a +/// reference to the single instance, which is created at launch. The native +/// Android and iOS APIs for AdMob use a singleton to manage rewarded video ad +/// objects, and that pattern is reflected here. +/// +/// Apps should assign a callback function to [RewardedVideoAd]'s listener +/// property in order to receive reward notifications from the AdMob SDK: +/// ``` +/// RewardedVideoAd.instance.listener = (RewardedVideoAdEvent event, +/// [String rewardType, int rewardAmount]) { +/// print("You were rewarded with $rewardAmount $rewardType!"); +/// } +/// }; +/// ``` +/// +/// The function will be invoked when any of the events in +/// [RewardedVideoAdEvent] occur. +/// +/// To load and show ads, call the load method: +/// ``` +/// RewardedVideoAd.instance.load(myAdUnitString, myTargetingInfoObj); +/// ``` +/// +/// Later (any point after your listener callback receives the +/// RewardedVideoAdEvent.loaded event), call the show method: +/// ``` +/// RewardedVideoAd.instance.show(); +/// ``` +/// +/// Only one rewarded video ad can be loaded at a time. Because the video assets +/// are so large, it's a good idea to start loading an ad well in advance of +/// when it's likely to be needed. +class RewardedVideoAd { + RewardedVideoAd._(); + + /// A platform-specific AdMob test ad unit ID for rewarded video ads. This ad + /// unit has been specially configured to always return test ads, and + /// developers are encouraged to use it while building and testing their apps. + static final String testAdUnitId = Platform.isAndroid + ? 'ca-app-pub-3940256099942544/5224354917' + : 'ca-app-pub-3940256099942544/1712485313'; + + static final RewardedVideoAd _instance = RewardedVideoAd._(); + + /// The one and only instance of this class. + static RewardedVideoAd get instance => _instance; + + /// Callback invoked for events in the rewarded video ad lifecycle. + RewardedVideoAdListener listener; + + /// Shows a rewarded video ad if one has been loaded. + Future show() { + return _invokeBooleanMethod("showRewardedVideoAd"); + } + + /// Loads a rewarded video ad using the provided ad unit ID. + Future load( + {@required String adUnitId, MobileAdTargetingInfo targetingInfo}) { + assert(adUnitId.isNotEmpty); + return _invokeBooleanMethod("loadRewardedVideoAd", { + 'adUnitId': adUnitId, + 'targetingInfo': targetingInfo?.toJson(), + }); + } +} + +/// Support for Google AdMob mobile ads. +/// +/// Before loading or showing an ad the plugin must be initialized with +/// an AdMob app id: +/// ``` +/// FirebaseAdMob.instance.initialize(appId: myAppId); +/// ``` +/// +/// Apps can create, load, and show mobile ads. For example: +/// ``` +/// BannerAd myBanner = BannerAd(unitId: myBannerAdUnitId) +/// ..load() +/// ..show(); +/// ``` +/// +/// See also: +/// +/// * The example associated with this plugin. +/// * [BannerAd], a small rectangular ad displayed at the bottom of the screen. +/// * [InterstitialAd], a full screen ad that must be dismissed by the user. +/// * [RewardedVideoAd], a full screen video ad that provides in-app user +/// rewards. +class FirebaseAdMob { + @visibleForTesting + FirebaseAdMob.private(MethodChannel channel) : _channel = channel { + _channel.setMethodCallHandler(_handleMethod); + } + + // A placeholder AdMob App ID for testing. AdMob App IDs and ad unit IDs are + // specific to a single operating system, so apps building for both Android and + // iOS will need a set for each platform. + static final String testAppId = Platform.isAndroid + ? 'ca-app-pub-3940256099942544~3347511713' + : 'ca-app-pub-3940256099942544~1458002511'; + + static final FirebaseAdMob _instance = FirebaseAdMob.private( + const MethodChannel('plugins.flutter.io/firebase_admob'), + ); + + /// The single shared instance of this plugin. + static FirebaseAdMob get instance => _instance; + + final MethodChannel _channel; + + static const Map _methodToMobileAdEvent = + { + 'onAdLoaded': MobileAdEvent.loaded, + 'onAdFailedToLoad': MobileAdEvent.failedToLoad, + 'onAdClicked': MobileAdEvent.clicked, + 'onAdImpression': MobileAdEvent.impression, + 'onAdOpened': MobileAdEvent.opened, + 'onAdLeftApplication': MobileAdEvent.leftApplication, + 'onAdClosed': MobileAdEvent.closed, + }; + + static const Map _methodToRewardedVideoAdEvent = + { + 'onRewarded': RewardedVideoAdEvent.rewarded, + 'onRewardedVideoAdClosed': RewardedVideoAdEvent.closed, + 'onRewardedVideoAdFailedToLoad': RewardedVideoAdEvent.failedToLoad, + 'onRewardedVideoAdLeftApplication': RewardedVideoAdEvent.leftApplication, + 'onRewardedVideoAdLoaded': RewardedVideoAdEvent.loaded, + 'onRewardedVideoAdOpened': RewardedVideoAdEvent.opened, + 'onRewardedVideoStarted': RewardedVideoAdEvent.started, + 'onRewardedVideoCompleted': RewardedVideoAdEvent.completed, + }; + + /// Initialize this plugin for the AdMob app specified by `appId`. + Future initialize( + {@required String appId, + String trackingId, + bool analyticsEnabled = false}) { + assert(appId != null && appId.isNotEmpty); + assert(analyticsEnabled != null); + return _invokeBooleanMethod("initialize", { + 'appId': appId, + 'trackingId': trackingId, + 'analyticsEnabled': analyticsEnabled, + }); + } + + Future _handleMethod(MethodCall call) { + assert(call.arguments is Map); + final Map argumentsMap = call.arguments; + final RewardedVideoAdEvent rewardedEvent = + _methodToRewardedVideoAdEvent[call.method]; + if (rewardedEvent != null) { + if (RewardedVideoAd.instance.listener != null) { + if (rewardedEvent == RewardedVideoAdEvent.rewarded) { + RewardedVideoAd.instance.listener(rewardedEvent, + rewardType: argumentsMap['rewardType'], + rewardAmount: argumentsMap['rewardAmount']); + } else { + RewardedVideoAd.instance.listener(rewardedEvent); + } + } + } else { + final int id = argumentsMap['id']; + if (id != null && MobileAd._allAds[id] != null) { + final MobileAd ad = MobileAd._allAds[id]; + final MobileAdEvent mobileAdEvent = _methodToMobileAdEvent[call.method]; + if (mobileAdEvent != null && ad.listener != null) { + ad.listener(mobileAdEvent); + } + } + } + + return Future.value(null); + } +} + +Future _invokeBooleanMethod(String method, [dynamic arguments]) async { + final bool result = await FirebaseAdMob.instance._channel.invokeMethod( + method, + arguments, + ); + return result; +} diff --git a/packages/firebase_admob/pubspec.yaml b/packages/firebase_admob/pubspec.yaml new file mode 100644 index 000000000000..e6ba7fa7271a --- /dev/null +++ b/packages/firebase_admob/pubspec.yaml @@ -0,0 +1,28 @@ +name: firebase_admob +description: Flutter plugin for Firebase AdMob, supporting + banner, interstitial (full-screen), and rewarded video ads +author: Flutter Team +homepage: https://github.com/flutter/plugins/tree/master/packages/firebase_admob +version: 0.9.0+4 + +flutter: + plugin: + androidPackage: io.flutter.plugins.firebaseadmob + iosPrefix: FLT + pluginClass: FirebaseAdMobPlugin + +dependencies: + meta: ^1.0.4 + platform: ^2.0.0 + flutter: + sdk: flutter + +dev_dependencies: + mockito: ^3.0.0 + flutter_test: + sdk: flutter + firebase_core: ^0.4.0 + +environment: + sdk: ">=2.0.0-dev.28.0 <3.0.0" + flutter: ">=1.5.0 <2.0.0" diff --git a/packages/firebase_admob/test/firebase_admob_test.dart b/packages/firebase_admob/test/firebase_admob_test.dart new file mode 100644 index 000000000000..4d85664fa614 --- /dev/null +++ b/packages/firebase_admob/test/firebase_admob_test.dart @@ -0,0 +1,140 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:firebase_admob/firebase_admob.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('FirebaseAdMob', () { + const MethodChannel channel = + MethodChannel('plugins.flutter.io/firebase_admob'); + + final List log = []; + final FirebaseAdMob admob = FirebaseAdMob.private(channel); + + setUp(() async { + channel.setMockMethodCallHandler((MethodCall methodCall) async { + log.add(methodCall); + switch (methodCall.method) { + case 'initialize': + case 'loadBannerAd': + case 'loadInterstitialAd': + case 'loadRewardedVideoAd': + case 'showAd': + case 'showRewardedVideoAd': + case 'disposeAd': + return Future.value(true); + default: + assert(false); + return null; + } + }); + }); + + test('initialize', () async { + log.clear(); + + expect(await admob.initialize(appId: FirebaseAdMob.testAppId), true); + expect(log, [ + isMethodCall('initialize', arguments: { + 'appId': FirebaseAdMob.testAppId, + 'trackingId': null, + 'analyticsEnabled': false, + }), + ]); + }); + + test('banner', () async { + log.clear(); + + final BannerAd banner = BannerAd( + adUnitId: BannerAd.testAdUnitId, + size: AdSize.banner, + ); + final int id = banner.id; + + expect(await banner.load(), true); + expect(await banner.show(), true); + expect(await banner.dispose(), true); + + expect(log, [ + isMethodCall('loadBannerAd', arguments: { + 'id': id, + 'adUnitId': BannerAd.testAdUnitId, + 'targetingInfo': {'requestAgent': 'flutter-alpha'}, + 'width': 320, + 'height': 50, + 'adSizeType': 'AdSizeType.WidthAndHeight', + }), + isMethodCall('showAd', arguments: { + 'id': id, + 'anchorOffset': '0.0', + 'horizontalCenterOffset': '0.0', + 'anchorType': 'bottom', + }), + isMethodCall('disposeAd', arguments: { + 'id': id, + }), + ]); + }); + + test('interstitial', () async { + log.clear(); + + final InterstitialAd interstitial = InterstitialAd( + adUnitId: InterstitialAd.testAdUnitId, + ); + final int id = interstitial.id; + + expect(await interstitial.load(), true); + expect( + await interstitial.show( + anchorOffset: 60.0, + horizontalCenterOffset: 10.0, + anchorType: AnchorType.top), + true); + expect(await interstitial.dispose(), true); + + expect(log, [ + isMethodCall('loadInterstitialAd', arguments: { + 'id': id, + 'adUnitId': InterstitialAd.testAdUnitId, + 'targetingInfo': {'requestAgent': 'flutter-alpha'}, + }), + isMethodCall('showAd', arguments: { + 'id': id, + 'anchorOffset': '60.0', + 'horizontalCenterOffset': '10.0', + 'anchorType': 'top', + }), + isMethodCall('disposeAd', arguments: { + 'id': id, + }), + ]); + }); + + test('rewarded', () async { + log.clear(); + + expect( + await RewardedVideoAd.instance.load( + adUnitId: RewardedVideoAd.testAdUnitId, + targetingInfo: const MobileAdTargetingInfo()), + true); + + expect(await RewardedVideoAd.instance.show(), true); + + expect(log, [ + isMethodCall('loadRewardedVideoAd', arguments: { + 'adUnitId': RewardedVideoAd.testAdUnitId, + 'targetingInfo': {'requestAgent': 'flutter-alpha'}, + }), + isMethodCall('showRewardedVideoAd', arguments: null), + ]); + }); + }); +} diff --git a/packages/firebase_auth/CHANGELOG.md b/packages/firebase_auth/CHANGELOG.md new file mode 100644 index 000000000000..d2b0aa1a4e34 --- /dev/null +++ b/packages/firebase_auth/CHANGELOG.md @@ -0,0 +1,465 @@ +## 0.14.0+2 + +* Reduce compiler warnings on iOS port by replacing `int` with `long` backing in returned timestamps. + +## 0.14.0+1 + +* Add dependency on `androidx.annotation:annotation:1.0.0`. + +## 0.14.0 + +* Added new `IdTokenResult` class. +* **Breaking Change**. `getIdToken()` method now returns `IdTokenResult` instead of a token `String`. + Use the `token` property of `IdTokenResult` to retrieve the token `String`. +* Added integration testing for `getIdToken()`. + +## 0.13.1+1 + +* Update authentication example in README. + +## 0.13.1 + +* Fixed a crash on iOS when sign-in fails. +* Additional integration testing. +* Updated documentation for `FirebaseUser.delete()` to include error codes. +* Updated Firebase project to match other Flutterfire apps. + +## 0.13.0 + +* **Breaking change**: Replace `FirebaseUserMetadata.creationTimestamp` and + `FirebaseUserMetadata.lastSignInTimestamp` with `creationTime` and `lastSignInTime`. + Previously on iOS `creationTimestamp` and `lastSignInTimestamp` returned in + seconds and on Android in milliseconds. Now, both platforms provide values as a + `DateTime`. + +## 0.12.0+1 + +* Fixes iOS sign-in exceptions when `additionalUserInfo` is `nil` or has `nil` fields. +* Additional integration testing. + +## 0.12.0 + +* Added new `AuthResult` and `AdditionalUserInfo` classes. +* **Breaking Change**. Sign-in methods now return `AuthResult` instead of `FirebaseUser`. + Retrieve the `FirebaseUser` using the `user` property of `AuthResult`. + +## 0.11.1+12 + +* Update google-services Android gradle plugin to 4.3.0 in documentation and examples. + +## 0.11.1+11 + +* On iOS, `getIdToken()` now uses the `refresh` parameter instead of always using `true`. + +## 0.11.1+10 + +* On Android, `providerData` now includes `UserInfo` for the phone authentication provider. + +## 0.11.1+9 + +* Update README to clarify importance of filling out all fields for OAuth consent screen. + +## 0.11.1+8 + +* Automatically register for iOS notifications, ensuring that phone authentication + will work even if Firebase method swizzling is disabled. + +## 0.11.1+7 + +* Automatically use version from pubspec.yaml when reporting usage to Firebase. + +## 0.11.1+6 + +* Add documentation of support email requirement to README. + +## 0.11.1+5 + +* Fix `updatePhoneNumberCredential` on Android. + +## 0.11.1+4 + +* Fix `updatePhoneNumberCredential` on iOS. + +## 0.11.1+3 + +* Add missing template type parameter to `invokeMethod` calls. +* Bump minimum Flutter version to 1.5.0. +* Replace invokeMethod with invokeMapMethod wherever necessary. +* FirebaseUser private constructor takes `Map` instead of `Map`. + +## 0.11.1+2 + +* Suppress deprecation warning for BinaryMessages. See: https://github.com/flutter/flutter/issues/33446 + +## 0.11.1+1 + +* Updated the error code documentation for `linkWithCredential`. + +## 0.11.1 + +* Support for `updatePhoneNumberCredential`. + +## 0.11.0 + +* **Breaking change**: `linkWithCredential` is now a function of `FirebaseUser`instead of + `FirebaseAuth`. +* Added test for newer `linkWithCredential` function. + +## 0.10.0+1 + +* Increase Firebase/Auth CocoaPod dependency to '~> 6.0'. + +## 0.10.0 + +* Update firebase_dynamic_links dependency. +* Update Android dependencies to latest. + +## 0.9.0 + +* **Breaking change**: `PhoneVerificationCompleted` now provides an `AuthCredential` that can + be used with `signInWithCredential` or `linkWithCredential` instead of signing in automatically. +* **Breaking change**: Remove internal counter `nextHandle` from public API. + +## 0.8.4+5 + +* Increase Firebase/Auth CocoaPod dependency to '~> 5.19'. + +## 0.8.4+4 + +* Update FirebaseAuth CocoaPod dependency to ensure availability of `FIRAuthErrorUserInfoNameKey`. + +## 0.8.4+3 + +* Updated deprecated API usage on iOS to use non-deprecated versions. +* Updated FirebaseAuth CocoaPod dependency to ensure a minimum version of 5.0. + +## 0.8.4+2 + +* Fixes an error in the documentation of createUserWithEmailAndPassword. + +## 0.8.4+1 + +* Adds credential for email authentication with link. + +## 0.8.4 + +* Adds support for email link authentication. + +## 0.8.3 + +* Make providerId 'const String' to use in 'case' statement. + +## 0.8.2+1 + +* Fixed bug where `PhoneCodeAutoRetrievalTimeout` callback was never called. + +## 0.8.2 + +* Fixed `linkWithCredential` on Android. + +## 0.8.1+5 + +* Added a driver test. + +## 0.8.1+4 + +* Update README. +* Update the example app with separate pages for registration and sign-in. + +## 0.8.1+3 + +* Reduce compiler warnings in Android plugin +* Raise errors early when accessing methods that require a Firebase User + +## 0.8.1+2 + +* Log messages about automatic configuration of the default app are now less confusing. + +## 0.8.1+1 + +* Remove categories. + +## 0.8.1 + +* Fixes Firebase auth phone sign-in for Android. + +## 0.8.0+3 + +* Log a more detailed warning at build time about the previous AndroidX + migration. + +## 0.8.0+2 + +* Update Google sign-in example in the README. + +## 0.8.0+1 + +* Update a broken dependency. + +## 0.8.0 + +* **Breaking change**. Migrate from the deprecated original Android Support + Library to AndroidX. This shouldn't result in any functional changes, but it + requires any Android apps using this plugin to [also + migrate](https://developer.android.com/jetpack/androidx/migrate) if they're + using the original support library. + +## 0.7.0 + +* Introduce third-party auth provider classes that generate `AuthCredential`s +* **Breaking Change** Signing in, linking, and reauthenticating now require an `AuthCredential` +* **Breaking Change** Unlinking now uses providerId +* **Breaking Change** Moved reauthentication to FirebaseUser + +## 0.6.7 + +* `FirebaseAuth` and `FirebaseUser` are now fully documented. +* `PlatformExceptions` now report error codes as stated in docs. +* Credentials can now be unlinked from Accounts with new methods on `FirebaseUser`. + +## 0.6.6 + +* Users can now reauthenticate in response to operations that require a recent sign-in. + +## 0.6.5 + +* Fixing async method `verifyPhoneNumber`, that would never return even in a successful call. + +## 0.6.4 + +* Added support for Github signin and linking Github accounts to existing users. + +## 0.6.3 + +* Add multi app support. + +## 0.6.2+1 + +* Bump Android dependencies to latest. + +## 0.6.2 + +* Add access to user metadata. + +## 0.6.1 + +* Adding support for linkWithTwitterCredential in FirebaseAuth. + +## 0.6.0 + +* Added support for `updatePassword` in `FirebaseUser`. +* **Breaking Change** Moved `updateEmail` and `updateProfile` to `FirebaseUser`. + This brings the `firebase_auth` package inline with other implementations and documentation. + +## 0.5.20 + +* Replaced usages of guava's: ImmutableList and ImmutableMap with platform +Collections.unmodifiableList() and Collections.unmodifiableMap(). + +## 0.5.19 + +* Update test package dependency to pick up Dart 2 support. +* Modified dependency on google_sign_in to point to a published + version instead of a relative path. + +## 0.5.18 + +* Adding support for updateEmail in FirebaseAuth. + +## 0.5.17 + +* Adding support for FirebaseUser.delete. + +## 0.5.16 + +* Adding support for setLanguageCode in FirebaseAuth. + +## 0.5.15 + +* Bump Android and Firebase dependency versions. + +## 0.5.14 + +* Fixed handling of auto phone number verification. + +## 0.5.13 + +* Add support for phone number authentication. + +## 0.5.12 + +* Fixed ArrayIndexOutOfBoundsException in handleStopListeningAuthState + +## 0.5.11 + +* Updated Gradle tooling to match Android Studio 3.1.2. + +## 0.5.10 + +* Updated iOS implementation to reflect Firebase API changes. + +## 0.5.9 + +* Added support for signing in with a Twitter account. + +## 0.5.8 + +* Added support to reload firebase user + +## 0.5.7 + +* Added support to sendEmailVerification + +## 0.5.6 + +* Added support for linkWithFacebookCredential + +## 0.5.5 + +* Updated Google Play Services dependencies to version 15.0.0. + +## 0.5.4 + +* Simplified podspec for Cocoapods 1.5.0, avoiding link issues in app archives. + +## 0.5.3 + +* Secure fetchProvidersForEmail (no providers) + +## 0.5.2 + +* Fixed Dart 2 type error in fetchProvidersForEmail. + +## 0.5.1 + +* Added support to fetchProvidersForEmail + +## 0.5.0 + +* **Breaking change**. Set SDK constraints to match the Flutter beta release. + +## 0.4.7 + +* Fixed Dart 2 type errors. + +## 0.4.6 + +* Fixed Dart 2 type errors. + +## 0.4.5 + +* Enabled use in Swift projects. + +## 0.4.4 + +* Added support for sendPasswordResetEmail + +## 0.4.3 + +* Moved to the io.flutter.plugins organization. + +## 0.4.2 + +* Added support for changing user data + +## 0.4.1 + +* Simplified and upgraded Android project template to Android SDK 27. +* Updated package description. + +## 0.4.0 + +* **Breaking change**. Upgraded to Gradle 4.1 and Android Studio Gradle plugin + 3.0.1. Older Flutter projects need to upgrade their Gradle setup as well in + order to use this version of the plugin. Instructions can be found + [here](https://github.com/flutter/flutter/wiki/Updating-Flutter-projects-to-Gradle-4.1-and-Android-Studio-Gradle-plugin-3.0.1). +* Relaxed GMS dependency to [11.4.0,12.0[ + +## 0.3.2 + +* Added FLT prefix to iOS types +* Change GMS dependency to 11.4.+ + +## 0.3.1 + +* Change GMS dependency to 11.+ + +## 0.3.0 + +* **Breaking Change**: Method FirebaseUser getToken was renamed to getIdToken. + +## 0.2.5 + +* Added support for linkWithCredential with Google credential + +## 0.2.4 + +* Added support for `signInWithCustomToken` +* Added `Stream onAuthStateChanged` event to listen when the user change + +## 0.2.3+1 + +* Aligned author name with rest of repo. + +## 0.2.3 + +* Remove dependency on Google/SignIn + +## 0.2.2 + +* Remove dependency on FirebaseUI + +## 0.2.1 + +* Added support for linkWithEmailAndPassword + +## 0.2.0 + +* **Breaking Change**: Method currentUser is async now. + +## 0.1.2 + +* Added support for signInWithFacebook + +## 0.1.1 + +* Updated to Firebase SDK to always use latest patch version for 11.0.x builds + +## 0.1.0 + +* Updated to Firebase SDK Version 11.0.1 +* **Breaking Change**: You need to add a maven section with the "https://maven.google.com" endpoint to the repository section of your `android/build.gradle`. For example: +```gradle +allprojects { + repositories { + jcenter() + maven { // NEW + url "https://maven.google.com" // NEW + } // NEW + } +} +``` + +## 0.0.4 + +* Add method getToken() to FirebaseUser + +## 0.0.3+1 + +* Updated README.md + +## 0.0.3 + +* Added support for createUserWithEmailAndPassword, signInWithEmailAndPassword, and signOut Firebase methods + +## 0.0.2+1 + +* Updated README.md + +## 0.0.2 + +* Bump buildToolsVersion to 25.0.3 + +## 0.0.1 + +* Initial Release diff --git a/packages/firebase_auth/ios/Classes/FirebaseAuthPlugin.m b/packages/firebase_auth/ios/Classes/FirebaseAuthPlugin.m new file mode 100644 index 000000000000..28a3f3e18b03 --- /dev/null +++ b/packages/firebase_auth/ios/Classes/FirebaseAuthPlugin.m @@ -0,0 +1,483 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "FirebaseAuthPlugin.h" +#import "UserAgent.h" + +#import "Firebase/Firebase.h" + +static NSString *getFlutterErrorCode(NSError *error) { + NSString *code = [error userInfo][FIRAuthErrorUserInfoNameKey]; + if (code != nil) { + return code; + } + return [NSString stringWithFormat:@"ERROR_%d", (int)error.code]; +} + +NSDictionary *toDictionary(id userInfo) { + return @{ + @"providerId" : userInfo.providerID, + @"displayName" : userInfo.displayName ?: [NSNull null], + @"uid" : userInfo.uid ?: [NSNull null], + @"photoUrl" : userInfo.photoURL.absoluteString ?: [NSNull null], + @"email" : userInfo.email ?: [NSNull null], + @"phoneNumber" : userInfo.phoneNumber ?: [NSNull null], + }; +} + +@interface FLTFirebaseAuthPlugin () +@property(nonatomic, retain) NSMutableDictionary *authStateChangeListeners; +@property(nonatomic, retain) FlutterMethodChannel *channel; +@end + +@implementation FLTFirebaseAuthPlugin + +// Handles are ints used as indexes into the NSMutableDictionary of active observers +int nextHandle = 0; + ++ (void)registerWithRegistrar:(NSObject *)registrar { + FlutterMethodChannel *channel = + [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/firebase_auth" + binaryMessenger:[registrar messenger]]; + FLTFirebaseAuthPlugin *instance = [[FLTFirebaseAuthPlugin alloc] init]; + instance.channel = channel; + instance.authStateChangeListeners = [[NSMutableDictionary alloc] init]; + [registrar addApplicationDelegate:instance]; + [registrar addMethodCallDelegate:instance channel:channel]; + + SEL sel = NSSelectorFromString(@"registerLibrary:withVersion:"); + if ([FIRApp respondsToSelector:sel]) { + [FIRApp performSelector:sel withObject:LIBRARY_NAME withObject:LIBRARY_VERSION]; + } +} + +- (instancetype)init { + self = [super init]; + if (self) { + if (![FIRApp appNamed:@"__FIRAPP_DEFAULT"]) { + NSLog(@"Configuring the default Firebase app..."); + [FIRApp configure]; + NSLog(@"Configured the default Firebase app %@.", [FIRApp defaultApp].name); + } + } + return self; +} + +- (FIRAuth *_Nullable)getAuth:(NSDictionary *)args { + NSString *appName = [args objectForKey:@"app"]; + return [FIRAuth authWithApp:[FIRApp appNamed:appName]]; +} + +- (bool)application:(UIApplication *)application + didReceiveRemoteNotification:(NSDictionary *)notification + fetchCompletionHandler:(void (^)(UIBackgroundFetchResult result))completionHandler { + if ([[FIRAuth auth] canHandleNotification:notification]) { + completionHandler(UIBackgroundFetchResultNoData); + return YES; + } + return NO; +} + +- (void)application:(UIApplication *)application + didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken { + [[FIRAuth auth] setAPNSToken:deviceToken type:FIRAuthAPNSTokenTypeProd]; +} + +- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary *)options { + return [[FIRAuth auth] canHandleURL:url]; +} + +// TODO(jackson): We should use the renamed versions of the following methods +// when they are available in the Firebase SDK that this plugin is dependent on. +// * fetchSignInMethodsForEmail:completion: +// * reauthenticateAndRetrieveDataWithCredential:completion: +// * linkAndRetrieveDataWithCredential:completion: +// * signInAndRetrieveDataWithCredential:completion: +// See discussion at https://github.com/flutter/plugins/pull/1487 +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" +- (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result { + if ([@"currentUser" isEqualToString:call.method]) { + id __block listener = [[self getAuth:call.arguments] + addAuthStateDidChangeListener:^(FIRAuth *_Nonnull auth, FIRUser *_Nullable user) { + [self sendResult:result forUser:user error:nil]; + [auth removeAuthStateDidChangeListener:listener]; + }]; + } else if ([@"signInAnonymously" isEqualToString:call.method]) { + [[self getAuth:call.arguments] + signInAnonymouslyWithCompletion:^(FIRAuthDataResult *authResult, NSError *error) { + [self sendResult:result forAuthDataResult:authResult error:error]; + }]; + } else if ([@"signInWithCredential" isEqualToString:call.method]) { + [[self getAuth:call.arguments] + signInAndRetrieveDataWithCredential:[self getCredential:call.arguments] + completion:^(FIRAuthDataResult *authResult, NSError *error) { + [self sendResult:result + forAuthDataResult:authResult + error:error]; + }]; + } else if ([@"createUserWithEmailAndPassword" isEqualToString:call.method]) { + NSString *email = call.arguments[@"email"]; + NSString *password = call.arguments[@"password"]; + [[self getAuth:call.arguments] + createUserWithEmail:email + password:password + completion:^(FIRAuthDataResult *authResult, NSError *error) { + [self sendResult:result forAuthDataResult:authResult error:error]; + }]; + } else if ([@"fetchSignInMethodsForEmail" isEqualToString:call.method]) { + NSString *email = call.arguments[@"email"]; + [[self getAuth:call.arguments] + fetchProvidersForEmail:email + completion:^(NSArray *providers, NSError *error) { + [self sendResult:result forObject:providers error:error]; + }]; + } else if ([@"sendEmailVerification" isEqualToString:call.method]) { + [[self getAuth:call.arguments].currentUser + sendEmailVerificationWithCompletion:^(NSError *_Nullable error) { + [self sendResult:result forObject:nil error:error]; + }]; + } else if ([@"reload" isEqualToString:call.method]) { + [[self getAuth:call.arguments].currentUser reloadWithCompletion:^(NSError *_Nullable error) { + [self sendResult:result forObject:nil error:error]; + }]; + } else if ([@"delete" isEqualToString:call.method]) { + [[self getAuth:call.arguments].currentUser deleteWithCompletion:^(NSError *_Nullable error) { + [self sendResult:result forObject:nil error:error]; + }]; + } else if ([@"sendPasswordResetEmail" isEqualToString:call.method]) { + NSString *email = call.arguments[@"email"]; + [[self getAuth:call.arguments] sendPasswordResetWithEmail:email + completion:^(NSError *error) { + [self sendResult:result + forObject:nil + error:error]; + }]; + } else if ([@"sendLinkToEmail" isEqualToString:call.method]) { + NSString *email = call.arguments[@"email"]; + FIRActionCodeSettings *actionCodeSettings = [FIRActionCodeSettings new]; + actionCodeSettings.URL = [NSURL URLWithString:call.arguments[@"url"]]; + actionCodeSettings.handleCodeInApp = call.arguments[@"handleCodeInApp"]; + [actionCodeSettings setIOSBundleID:call.arguments[@"iOSBundleID"]]; + [actionCodeSettings setAndroidPackageName:call.arguments[@"androidPackageName"] + installIfNotAvailable:call.arguments[@"androidInstallIfNotAvailable"] + minimumVersion:call.arguments[@"androidMinimumVersion"]]; + [[self getAuth:call.arguments] sendSignInLinkToEmail:email + actionCodeSettings:actionCodeSettings + completion:^(NSError *_Nullable error) { + [self sendResult:result forObject:nil error:error]; + }]; + } else if ([@"isSignInWithEmailLink" isEqualToString:call.method]) { + NSString *link = call.arguments[@"link"]; + BOOL status = [[self getAuth:call.arguments] isSignInWithEmailLink:link]; + [self sendResult:result forObject:[NSNumber numberWithBool:status] error:nil]; + } else if ([@"signInWithEmailAndLink" isEqualToString:call.method]) { + NSString *email = call.arguments[@"email"]; + NSString *link = call.arguments[@"link"]; + [[self getAuth:call.arguments] + signInWithEmail:email + link:link + completion:^(FIRAuthDataResult *_Nullable authResult, NSError *_Nullable error) { + [self sendResult:result forAuthDataResult:authResult error:error]; + }]; + } else if ([@"signInWithEmailAndPassword" isEqualToString:call.method]) { + NSString *email = call.arguments[@"email"]; + NSString *password = call.arguments[@"password"]; + [[self getAuth:call.arguments] + signInWithEmail:email + password:password + completion:^(FIRAuthDataResult *authResult, NSError *error) { + [self sendResult:result forAuthDataResult:authResult error:error]; + }]; + } else if ([@"signOut" isEqualToString:call.method]) { + NSError *signOutError; + BOOL status = [[self getAuth:call.arguments] signOut:&signOutError]; + if (!status) { + NSLog(@"Error signing out: %@", signOutError); + [self sendResult:result forObject:nil error:signOutError]; + } else { + [self sendResult:result forObject:nil error:nil]; + } + } else if ([@"getIdToken" isEqualToString:call.method]) { + NSDictionary *args = call.arguments; + BOOL refresh = [args objectForKey:@"refresh"]; + [[self getAuth:call.arguments].currentUser + getIDTokenResultForcingRefresh:refresh + completion:^(FIRAuthTokenResult *_Nullable tokenResult, + NSError *_Nullable error) { + NSMutableDictionary *tokenData = nil; + if (tokenResult != nil) { + long expirationTimestamp = + [tokenResult.expirationDate timeIntervalSince1970]; + long authTimestamp = [tokenResult.authDate timeIntervalSince1970]; + long issuedAtTimestamp = + [tokenResult.issuedAtDate timeIntervalSince1970]; + + tokenData = [[NSMutableDictionary alloc] initWithDictionary:@{ + @"token" : tokenResult.token, + @"expirationTimestamp" : + [NSNumber numberWithLong:expirationTimestamp], + @"authTimestamp" : [NSNumber numberWithLong:authTimestamp], + @"issuedAtTimestamp" : + [NSNumber numberWithLong:issuedAtTimestamp], + @"claims" : tokenResult.claims, + }]; + + if (tokenResult.signInProvider != nil) { + tokenData[@"signInProvider"] = tokenResult.signInProvider; + } + } + + [self sendResult:result forObject:tokenData error:error]; + }]; + } else if ([@"reauthenticateWithCredential" isEqualToString:call.method]) { + [[self getAuth:call.arguments].currentUser + reauthenticateAndRetrieveDataWithCredential:[self getCredential:call.arguments] + completion:^(FIRAuthDataResult *r, + NSError *_Nullable error) { + [self sendResult:result forObject:nil error:error]; + }]; + } else if ([@"linkWithCredential" isEqualToString:call.method]) { + [[self getAuth:call.arguments].currentUser + linkAndRetrieveDataWithCredential:[self getCredential:call.arguments] + completion:^(FIRAuthDataResult *authResult, NSError *error) { + [self sendResult:result forAuthDataResult:authResult error:error]; + }]; + } else if ([@"unlinkFromProvider" isEqualToString:call.method]) { + NSString *provider = call.arguments[@"provider"]; + [[self getAuth:call.arguments].currentUser + unlinkFromProvider:provider + completion:^(FIRUser *_Nullable user, NSError *_Nullable error) { + [self sendResult:result forUser:user error:error]; + }]; + } else if ([@"updateEmail" isEqualToString:call.method]) { + NSString *email = call.arguments[@"email"]; + [[self getAuth:call.arguments].currentUser updateEmail:email + completion:^(NSError *error) { + [self sendResult:result + forObject:nil + error:error]; + }]; + } else if ([@"updatePhoneNumberCredential" isEqualToString:call.method]) { + FIRPhoneAuthCredential *credential = + (FIRPhoneAuthCredential *)[self getCredential:call.arguments]; + [[self getAuth:call.arguments].currentUser + updatePhoneNumberCredential:credential + completion:^(NSError *_Nullable error) { + [self sendResult:result forObject:nil error:error]; + }]; + } else if ([@"updatePassword" isEqualToString:call.method]) { + NSString *password = call.arguments[@"password"]; + [[self getAuth:call.arguments].currentUser updatePassword:password + completion:^(NSError *error) { + [self sendResult:result + forObject:nil + error:error]; + }]; + } else if ([@"updateProfile" isEqualToString:call.method]) { + FIRUserProfileChangeRequest *changeRequest = + [[self getAuth:call.arguments].currentUser profileChangeRequest]; + if (call.arguments[@"displayName"]) { + changeRequest.displayName = call.arguments[@"displayName"]; + } + if (call.arguments[@"photoUrl"]) { + changeRequest.photoURL = [NSURL URLWithString:call.arguments[@"photoUrl"]]; + } + [changeRequest commitChangesWithCompletion:^(NSError *error) { + [self sendResult:result forObject:nil error:error]; + }]; + } else if ([@"signInWithCustomToken" isEqualToString:call.method]) { + NSString *token = call.arguments[@"token"]; + [[self getAuth:call.arguments] + signInWithCustomToken:token + completion:^(FIRAuthDataResult *authResult, NSError *error) { + [self sendResult:result forAuthDataResult:authResult error:error]; + }]; + + } else if ([@"startListeningAuthState" isEqualToString:call.method]) { + NSNumber *identifier = [NSNumber numberWithInteger:nextHandle++]; + + FIRAuthStateDidChangeListenerHandle listener = [[self getAuth:call.arguments] + addAuthStateDidChangeListener:^(FIRAuth *_Nonnull auth, FIRUser *_Nullable user) { + NSMutableDictionary *response = [[NSMutableDictionary alloc] init]; + response[@"id"] = identifier; + if (user) { + response[@"user"] = [self dictionaryFromUser:user]; + } + [self.channel invokeMethod:@"onAuthStateChanged" arguments:response]; + }]; + [self.authStateChangeListeners setObject:listener forKey:identifier]; + result(identifier); + } else if ([@"stopListeningAuthState" isEqualToString:call.method]) { + NSNumber *identifier = + [NSNumber numberWithInteger:[call.arguments[@"id"] unsignedIntegerValue]]; + + FIRAuthStateDidChangeListenerHandle listener = self.authStateChangeListeners[identifier]; + if (listener) { + [[self getAuth:call.arguments] + removeAuthStateDidChangeListener:self.authStateChangeListeners]; + [self.authStateChangeListeners removeObjectForKey:identifier]; + result(nil); + } else { + result([FlutterError + errorWithCode:@"ERROR_LISTENER_NOT_FOUND" + message:[NSString stringWithFormat:@"Listener with identifier '%d' not found.", + identifier.intValue] + details:nil]); + } + } else if ([@"verifyPhoneNumber" isEqualToString:call.method]) { + NSString *phoneNumber = call.arguments[@"phoneNumber"]; + NSNumber *handle = call.arguments[@"handle"]; + [[FIRPhoneAuthProvider provider] + verifyPhoneNumber:phoneNumber + UIDelegate:nil + completion:^(NSString *verificationID, NSError *error) { + if (error) { + [self.channel invokeMethod:@"phoneVerificationFailed" + arguments:@{ + @"exception" : [self mapVerifyPhoneError:error], + @"handle" : handle + }]; + } else { + [self.channel + invokeMethod:@"phoneCodeSent" + arguments:@{@"verificationId" : verificationID, @"handle" : handle}]; + } + }]; + result(nil); + } else if ([@"signInWithPhoneNumber" isEqualToString:call.method]) { + NSString *verificationId = call.arguments[@"verificationId"]; + NSString *smsCode = call.arguments[@"smsCode"]; + + FIRPhoneAuthCredential *credential = + [[FIRPhoneAuthProvider provider] credentialWithVerificationID:verificationId + verificationCode:smsCode]; + [[self getAuth:call.arguments] + signInAndRetrieveDataWithCredential:credential + completion:^(FIRAuthDataResult *authResult, + NSError *_Nullable error) { + [self sendResult:result + forAuthDataResult:authResult + error:error]; + }]; + } else if ([@"setLanguageCode" isEqualToString:call.method]) { + NSString *language = call.arguments[@"language"]; + [[self getAuth:call.arguments] setLanguageCode:language]; + [self sendResult:result forObject:nil error:nil]; + } else { + result(FlutterMethodNotImplemented); + } +} + +- (NSMutableDictionary *)dictionaryFromUser:(FIRUser *)user { + NSMutableArray *> *providerData = + [NSMutableArray arrayWithCapacity:user.providerData.count]; + for (id userInfo in user.providerData) { + [providerData addObject:toDictionary(userInfo)]; + } + + long creationDate = [user.metadata.creationDate timeIntervalSince1970] * 1000; + long lastSignInDate = [user.metadata.lastSignInDate timeIntervalSince1970] * 1000; + + NSMutableDictionary *userData = [toDictionary(user) mutableCopy]; + userData[@"creationTimestamp"] = [NSNumber numberWithLong:creationDate]; + userData[@"lastSignInTimestamp"] = [NSNumber numberWithLong:lastSignInDate]; + userData[@"isAnonymous"] = [NSNumber numberWithBool:user.isAnonymous]; + userData[@"isEmailVerified"] = [NSNumber numberWithBool:user.isEmailVerified]; + userData[@"providerData"] = providerData; + return userData; +} +#pragma clang diagnostic pop + +- (void)sendResult:(FlutterResult)result + forAuthDataResult:(FIRAuthDataResult *)authResult + error:(NSError *)error { + FIRUser *user = authResult.user; + FIRAdditionalUserInfo *additionalUserInfo = authResult.additionalUserInfo; + [self sendResult:result + forObject:@{ + @"user" : (user != nil ? [self dictionaryFromUser:user] : [NSNull null]), + @"additionalUserInfo" : additionalUserInfo ? @{ + @"isNewUser" : [NSNumber numberWithBool:additionalUserInfo.isNewUser], + @"username" : additionalUserInfo.username ?: [NSNull null], + @"providerId" : additionalUserInfo.providerID ?: [NSNull null], + @"profile" : additionalUserInfo.profile ?: [NSNull null], + } + : [NSNull null], + } + error:error]; +} + +- (void)sendResult:(FlutterResult)result forUser:(FIRUser *)user error:(NSError *)error { + [self sendResult:result + forObject:(user != nil ? [self dictionaryFromUser:user] : nil) + error:error]; +} + +- (void)sendResult:(FlutterResult)result forObject:(NSObject *)object error:(NSError *)error { + if (error != nil) { + result([FlutterError errorWithCode:getFlutterErrorCode(error) + message:error.localizedDescription + details:nil]); + } else if (object == nil) { + result(nil); + } else { + result(object); + } +} + +- (id)mapVerifyPhoneError:(NSError *)error { + NSString *errorCode = @"verifyPhoneNumberError"; + + if (error.code == FIRAuthErrorCodeCaptchaCheckFailed) { + errorCode = @"captchaCheckFailed"; + } else if (error.code == FIRAuthErrorCodeQuotaExceeded) { + errorCode = @"quotaExceeded"; + } else if (error.code == FIRAuthErrorCodeInvalidPhoneNumber) { + errorCode = @"invalidPhoneNumber"; + } else if (error.code == FIRAuthErrorCodeMissingPhoneNumber) { + errorCode = @"missingPhoneNumber"; + } + return @{@"code" : errorCode, @"message" : error.localizedDescription}; +} + +- (FIRAuthCredential *)getCredential:(NSDictionary *)arguments { + NSString *provider = arguments[@"provider"]; + NSDictionary *data = arguments[@"data"]; + FIRAuthCredential *credential; + if ([FIREmailAuthProviderID isEqualToString:provider]) { + NSString *email = data[@"email"]; + if ([data objectForKey:@"password"]) { + NSString *password = data[@"password"]; + credential = [FIREmailAuthProvider credentialWithEmail:email password:password]; + } else { + NSString *link = data[@"link"]; + credential = [FIREmailAuthProvider credentialWithEmail:email link:link]; + } + } else if ([FIRGoogleAuthProviderID isEqualToString:provider]) { + NSString *idToken = data[@"idToken"]; + NSString *accessToken = data[@"accessToken"]; + credential = [FIRGoogleAuthProvider credentialWithIDToken:idToken accessToken:accessToken]; + } else if ([FIRFacebookAuthProviderID isEqualToString:provider]) { + NSString *accessToken = data[@"accessToken"]; + credential = [FIRFacebookAuthProvider credentialWithAccessToken:accessToken]; + } else if ([FIRTwitterAuthProviderID isEqualToString:provider]) { + NSString *authToken = data[@"authToken"]; + NSString *authTokenSecret = data[@"authTokenSecret"]; + credential = [FIRTwitterAuthProvider credentialWithToken:authToken secret:authTokenSecret]; + } else if ([FIRGitHubAuthProviderID isEqualToString:provider]) { + NSString *token = data[@"token"]; + credential = [FIRGitHubAuthProvider credentialWithToken:token]; + } else if ([FIRPhoneAuthProviderID isEqualToString:provider]) { + NSString *verificationId = data[@"verificationId"]; + NSString *smsCode = data[@"smsCode"]; + credential = [[FIRPhoneAuthProvider providerWithAuth:[self getAuth:arguments]] + credentialWithVerificationID:verificationId + verificationCode:smsCode]; + } else { + NSLog(@"Support for an auth provider with identifier '%@' is not implemented.", provider); + } + return credential; +} +@end diff --git a/packages/firebase_auth/pubspec.yaml b/packages/firebase_auth/pubspec.yaml new file mode 100755 index 000000000000..163957b97e9b --- /dev/null +++ b/packages/firebase_auth/pubspec.yaml @@ -0,0 +1,34 @@ +name: firebase_auth +description: Flutter plugin for Firebase Auth, enabling Android and iOS + authentication using passwords, phone numbers and identity providers + like Google, Facebook and Twitter. +author: Flutter Team +homepage: https://github.com/flutter/plugins/tree/master/packages/firebase_auth +version: 0.14.0+2 + +flutter: + plugin: + androidPackage: io.flutter.plugins.firebaseauth + iosPrefix: FLT + pluginClass: FirebaseAuthPlugin + +dependencies: + meta: ^1.0.4 + firebase_core: ^0.4.0 + + flutter: + sdk: flutter + +dev_dependencies: + google_sign_in: ^3.0.4 + firebase_dynamic_links: ^0.3.0 + uuid: ^2.0.2 + test: ^1.3.0 + flutter_test: + sdk: flutter + flutter_driver: + sdk: flutter + +environment: + sdk: ">=2.0.0-dev.28.0 <3.0.0" + flutter: ">=1.5.0 <2.0.0" diff --git a/packages/google_maps_flutter/CHANGELOG.md b/packages/google_maps_flutter/CHANGELOG.md index 2f5983e0cbcf..2f0c54acb3cf 100644 --- a/packages/google_maps_flutter/CHANGELOG.md +++ b/packages/google_maps_flutter/CHANGELOG.md @@ -1,7 +1,10 @@ +<<<<<<< HEAD +======= ## 0.5.20+6 * Adds support for toggling the traffic layer +>>>>>>> e168ef575a5b5add8b3619b8b6c1fea09b81d19b ## 0.5.20+5 * Allow (de-)serialization of CameraPosition diff --git a/packages/google_maps_flutter/pubspec.yaml b/packages/google_maps_flutter/pubspec.yaml index 7504c1dd41a6..3cfab7a4f971 100644 --- a/packages/google_maps_flutter/pubspec.yaml +++ b/packages/google_maps_flutter/pubspec.yaml @@ -2,7 +2,11 @@ name: google_maps_flutter description: A Flutter plugin for integrating Google Maps in iOS and Android applications. author: Flutter Team homepage: https://github.com/flutter/plugins/tree/master/packages/google_maps_flutter +<<<<<<< HEAD +version: 0.5.20+5 +======= version: 0.5.20+6 +>>>>>>> e168ef575a5b5add8b3619b8b6c1fea09b81d19b dependencies: flutter: diff --git a/packages/image_picker/CHANGELOG.md b/packages/image_picker/CHANGELOG.md index 78d44e594eb5..622d299bbbe2 100644 --- a/packages/image_picker/CHANGELOG.md +++ b/packages/image_picker/CHANGELOG.md @@ -1,8 +1,11 @@ +<<<<<<< HEAD +======= ## 0.6.1+4 * Android: Fix a regression where the `retrieveLostImage` does not work anymore. * Set up Android unit test to test `ImagePickerCache` and added image quality caching tests. +>>>>>>> e168ef575a5b5add8b3619b8b6c1fea09b81d19b ## 0.6.1+3 * Bugfix iOS: Fix orientation of the picked image after scaling. diff --git a/packages/image_picker/pubspec.yaml b/packages/image_picker/pubspec.yaml index c5d1647bb410..daffb3378257 100755 --- a/packages/image_picker/pubspec.yaml +++ b/packages/image_picker/pubspec.yaml @@ -5,7 +5,11 @@ authors: - Flutter Team - Rhodes Davis Jr. homepage: https://github.com/flutter/plugins/tree/master/packages/image_picker +<<<<<<< HEAD +version: 0.6.1+3 +======= version: 0.6.1+4 +>>>>>>> e168ef575a5b5add8b3619b8b6c1fea09b81d19b flutter: plugin: diff --git a/temp_packages/plugins b/temp_packages/plugins new file mode 160000 index 000000000000..3ed97e789e60 --- /dev/null +++ b/temp_packages/plugins @@ -0,0 +1 @@ +Subproject commit 3ed97e789e60e09250487ce6d79ec17127148a39