diff --git a/packages/google_adsense/CHANGELOG.md b/packages/google_adsense/CHANGELOG.md index 91f12194eb37..57f202439cce 100644 --- a/packages/google_adsense/CHANGELOG.md +++ b/packages/google_adsense/CHANGELOG.md @@ -1,3 +1,9 @@ +## 0.1.1 + +* Adds `AdSenseCodeParameters` configuration object for `adSense.initialize`. +* Adds a 100ms delay to `adBreak` and `showAdFn`, so Flutter tapevents have time + to settle before an H5 Ad takes over the screen. + ## 0.1.0 * Adds H5 Games Ads API as `h5` library. diff --git a/packages/google_adsense/README.md b/packages/google_adsense/README.md index e34f60c90b9f..ccecc9285412 100644 --- a/packages/google_adsense/README.md +++ b/packages/google_adsense/README.md @@ -2,9 +2,11 @@ This package is only intended for use by web **games**. -Please apply to the beta using [this form]( https://adsense.google.com/start/h5-beta/?src=flutter). Once approved, you may use the package. +Please apply to the H5 Games Ads beta using [this form][h5-beta-form]. Once +approved, you may use the package. -Without approval, your code may not behave as expected, and your AdSense account may face policy issues. +**Without approval, your code may not behave as expected, and your AdSense +account may face policy issues.** # google_adsense @@ -13,8 +15,12 @@ Without approval, your code may not behave as expected, and your AdSense account This package provides a way to initialize and use AdSense on your Flutter Web app. It includes libraries for the following products: -* [H5 Games Ads](https://adsense.google.com/start/h5-games-ads/) (beta) -* (Experimental) [AdSense Ad Unit](https://support.google.com/adsense/answer/9183549) Widget +* [H5 Games Ads](https://adsense.google.com/start/h5-games-ads/) (in beta, please + apply using [this form][h5-beta-form]) +* [AdSense Ad Unit](https://support.google.com/adsense/answer/9183549) Widget + (experimental and invitation-only, not accepting applications now) + +[h5-beta-form]: https://adsense.google.com/start/h5-beta/?src=flutter ## Documentation diff --git a/packages/google_adsense/doc/initialization.md b/packages/google_adsense/doc/initialization.md index 9851e8a2441f..8b1b777b3022 100644 --- a/packages/google_adsense/doc/initialization.md +++ b/packages/google_adsense/doc/initialization.md @@ -31,3 +31,23 @@ void main() async { runApp(const MyApp()); } ``` + +## Configure additional AdSense code parameters + +You can pass an `AdSenseCodeParameters` object to the `adSense.initialize` call +to configure additional settings, like a custom channel ID, or for regulatory +compliance. + + +```dart +await adSense.initialize( + '0123456789012345', + adSenseCodeParameters: AdSenseCodeParameters( + adbreakTest: 'on', + adFrequencyHint: '30s', + ), +); +``` + +Check the Google AdSense Help for a complete list of +[AdSense code parameter descriptions](https://support.google.com/adsense/answer/9955214#adsense_code_parameter_descriptions). diff --git a/packages/google_adsense/example/integration_test/core_test.dart b/packages/google_adsense/example/integration_test/core_test.dart index abc4fc18f6c3..4193e414c1b7 100644 --- a/packages/google_adsense/example/integration_test/core_test.dart +++ b/packages/google_adsense/example/integration_test/core_test.dart @@ -33,7 +33,6 @@ void main() async { group('adSense.initialize', () { testWidgets('adds AdSense script tag.', (WidgetTester _) async { final web.HTMLElement target = web.HTMLDivElement(); - // Given await adSense.initialize(testClient, jsLoaderTarget: target); @@ -46,6 +45,39 @@ void main() async { expect(injected.async, true); }); + testWidgets('sets AdSenseCodeParameters in script tag.', + (WidgetTester _) async { + final web.HTMLElement target = web.HTMLDivElement(); + + await adSense.initialize(testClient, + jsLoaderTarget: target, + adSenseCodeParameters: AdSenseCodeParameters( + adHost: 'test-adHost', + admobInterstitialSlot: 'test-admobInterstitialSlot', + admobRewardedSlot: 'test-admobRewardedSlot', + adChannel: 'test-adChannel', + adbreakTest: 'test-adbreakTest', + tagForChildDirectedTreatment: 'test-tagForChildDirectedTreatment', + tagForUnderAgeOfConsent: 'test-tagForUnderAgeOfConsent', + adFrequencyHint: 'test-adFrequencyHint', + )); + + final web.HTMLScriptElement injected = + target.lastElementChild! as web.HTMLScriptElement; + + expect(injected.dataset['adHost'], 'test-adHost'); + expect(injected.dataset['admobInterstitialSlot'], + 'test-admobInterstitialSlot'); + expect(injected.dataset['admobRewardedSlot'], 'test-admobRewardedSlot'); + expect(injected.dataset['adChannel'], 'test-adChannel'); + expect(injected.dataset['adbreakTest'], 'test-adbreakTest'); + expect(injected.dataset['tagForChildDirectedTreatment'], + 'test-tagForChildDirectedTreatment'); + expect(injected.dataset['tagForUnderAgeOfConsent'], + 'test-tagForUnderAgeOfConsent'); + expect(injected.dataset['adFrequencyHint'], 'test-adFrequencyHint'); + }); + testWidgets('Skips initialization if script is already present.', (WidgetTester _) async { final web.HTMLScriptElement script = web.HTMLScriptElement() diff --git a/packages/google_adsense/example/integration_test/h5_test.dart b/packages/google_adsense/example/integration_test/h5_test.dart index 0ca816239a2e..28ab89b460fb 100644 --- a/packages/google_adsense/example/integration_test/h5_test.dart +++ b/packages/google_adsense/example/integration_test/h5_test.dart @@ -38,7 +38,8 @@ void main() { // Pump frames so we can see what happened with adBreak await tester.pump(); - await tester.pump(); + // Wait for the async bits of adBreak + await tester.pump(const Duration(milliseconds: 250)); expect(lastAdBreakPlacement, isNotNull); expect(lastAdBreakPlacement!.type?.toDart, 'reward'); @@ -70,7 +71,8 @@ void main() { // Pump frames so we can see what happened with adBreak await tester.pump(); - await tester.pump(); + // Wait for the async bits of adBreak + await tester.pump(const Duration(milliseconds: 250)); expect(lastPlacementInfo, isNotNull); expect(lastPlacementInfo!.breakName, 'ok-for-tests'); @@ -91,7 +93,8 @@ void main() { // Pump frames so we can see what happened with adBreak await tester.pump(); - await tester.pump(); + // Wait for the async bits of adBreak + await tester.pump(const Duration(milliseconds: 250)); expect(lastAdBreakPlacement!.name!.toDart, 'APFlutter-my-test-break'); }); @@ -119,6 +122,7 @@ void main() { // Pump frames so we can see what happened with adConfig await tester.pump(); + // adConfig doesn't have async bits await tester.pump(); expect(lastAdConfigParameters, isNotNull); diff --git a/packages/google_adsense/example/lib/h5.dart b/packages/google_adsense/example/lib/h5.dart index 0ddbd6478cee..f40cfef2f99e 100644 --- a/packages/google_adsense/example/lib/h5.dart +++ b/packages/google_adsense/example/lib/h5.dart @@ -12,7 +12,15 @@ import 'package:google_adsense/h5.dart'; // #enddocregion import-h5 void main() async { - await adSense.initialize('0123456789012345'); + // #docregion initialize-with-code-parameters + await adSense.initialize( + '0123456789012345', + adSenseCodeParameters: AdSenseCodeParameters( + adbreakTest: 'on', + adFrequencyHint: '30s', + ), + ); + // #enddocregion initialize-with-code-parameters runApp(const MyApp()); } diff --git a/packages/google_adsense/lib/src/adsense/ad_unit_params.dart b/packages/google_adsense/lib/src/adsense/ad_unit_params.dart index d31a7c2c5fa7..f023f242681e 100644 --- a/packages/google_adsense/lib/src/adsense/ad_unit_params.dart +++ b/packages/google_adsense/lib/src/adsense/ad_unit_params.dart @@ -32,12 +32,12 @@ class AdUnitParams { /// The ads inside a Multiplex ad unit are arranged in a grid. You can specify how many rows and columns you want to show within that grid
/// Sets the number of rows
/// Requires setting [AdUnitParams.MATCHED_CONTENT_UI_TYPE] - static const String MATCHED_CONTENT_ROWS_NUM = 'macthedContentRowsNum'; + static const String MATCHED_CONTENT_ROWS_NUM = 'matchedContentRowsNum'; /// The ads inside a Multiplex ad unit are arranged in a grid. You can specify how many rows and columns you want to show within that grid
/// Sets the number of columns
/// Requires setting [AdUnitParams.MATCHED_CONTENT_UI_TYPE] - static const String MATCHED_CONTENT_COLUMNS_NUM = 'macthedContentColumnsNum'; + static const String MATCHED_CONTENT_COLUMNS_NUM = 'matchedContentColumnsNum'; /// testing environment flag, defaults to kIsDebug static const String AD_TEST = 'adtest'; diff --git a/packages/google_adsense/lib/src/adsense/adsense.dart b/packages/google_adsense/lib/src/adsense/adsense.dart index 07781299c901..2429927cff21 100644 --- a/packages/google_adsense/lib/src/adsense/adsense.dart +++ b/packages/google_adsense/lib/src/adsense/adsense.dart @@ -2,6 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -export 'ad_unit_configuration.dart'; -export 'ad_unit_params.dart' hide AdStatus, AdUnitParams; -export 'ad_unit_widget.dart'; +export 'ad_unit_configuration.dart' show AdUnitConfiguration; +export 'ad_unit_params.dart' show AdFormat, AdLayout, MatchedContentUiType; +export 'ad_unit_widget.dart' show AdUnitWidget; diff --git a/packages/google_adsense/lib/src/core/adsense_code_parameters.dart b/packages/google_adsense/lib/src/core/adsense_code_parameters.dart new file mode 100644 index 000000000000..3ab48bfbe25f --- /dev/null +++ b/packages/google_adsense/lib/src/core/adsense_code_parameters.dart @@ -0,0 +1,65 @@ +// Copyright 2013 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. + +/// Configuration for various settings for game ads. +/// +/// These are set as `data`-attributes in the AdSense script tag. +class AdSenseCodeParameters { + /// Builds an AdSense code parameters object. + /// + /// The following parameters are available: + /// + /// * [adHost]: If you share your revenue with a host platform, use this parameter + /// to specify the host platform. + /// * [admobInterstitialSlot]: If your game runs in a mobile app, use this parameter + /// to request interstitial ads. + /// * [admobRewardedSlot]: If your game runs in a mobile app, use this parameter + /// to request rewarded ads. + /// * [adChannel]: You may include a + /// [custom channel ID](https://support.google.com/adsense/answer/10078316) + /// for tracking the performance of your ads. + /// * [adbreakTest]: Set this parameter to `'on'` to enable testing mode. This + /// lets you test your placements using fake ads. + /// * [tagForChildDirectedTreatment]: Use this parameter if you want to tag your + /// ad requests for treatment as child directed. For more information, refer to: + /// [Tag a site or ad request for child-directed treatment](https://support.google.com/adsense/answer/3248194). + /// * [tagForUnderAgeOfConsent]: Use this parameter if you want to tag your + /// European Economic Area (EEA), Switzerland, and UK ad requests for restricted + /// data processing treatment. For more information, refer to: + /// [Tag an ad request for EEA and UK users under the age of consent (TFUA)](https://support.google.com/adsense/answer/9009582). + /// * [adFrequencyHint]: The minimum average time interval between ads expressed + /// in seconds. If this value is `'120s'` then ads will not be shown more + /// frequently than once every two minutes on average. Note that this is a hint + /// that could be ignored or overridden by a server control in future. + /// + /// For more information about these parameters, check + /// [AdSense code parameter descriptions](https://support.google.com/adsense/answer/9955214#adsense_code_parameter_descriptions). + AdSenseCodeParameters({ + String? adHost, + String? admobInterstitialSlot, + String? admobRewardedSlot, + String? adChannel, + String? adbreakTest, + String? tagForChildDirectedTreatment, + String? tagForUnderAgeOfConsent, + String? adFrequencyHint, + }) : _adSenseCodeParameters = { + if (adHost != null) 'adHost': adHost, + if (admobInterstitialSlot != null) + 'admobInterstitialSlot': admobInterstitialSlot, + if (admobRewardedSlot != null) 'admobRewardedSlot': admobRewardedSlot, + if (adChannel != null) 'adChannel': adChannel, + if (adbreakTest != null) 'adbreakTest': adbreakTest, + if (tagForChildDirectedTreatment != null) + 'tagForChildDirectedTreatment': tagForChildDirectedTreatment, + if (tagForUnderAgeOfConsent != null) + 'tagForUnderAgeOfConsent': tagForUnderAgeOfConsent, + if (adFrequencyHint != null) 'adFrequencyHint': adFrequencyHint, + }; + + final Map _adSenseCodeParameters; + + /// `Map` representation of this configuration object. + Map get toMap => _adSenseCodeParameters; +} diff --git a/packages/google_adsense/lib/src/core/google_adsense.dart b/packages/google_adsense/lib/src/core/google_adsense.dart index 527ac38347e6..9f846e33c08e 100644 --- a/packages/google_adsense/lib/src/core/google_adsense.dart +++ b/packages/google_adsense/lib/src/core/google_adsense.dart @@ -6,8 +6,11 @@ import 'package:flutter/widgets.dart'; import 'package:web/web.dart' as web; import '../utils/logging.dart'; +import 'adsense_code_parameters.dart'; import 'js_interop/js_loader.dart'; +export 'adsense_code_parameters.dart' show AdSenseCodeParameters; + /// The web implementation of the AdSense API. class AdSense { bool _isInitialized = false; @@ -15,17 +18,22 @@ class AdSense { /// The [Publisher ID](https://support.google.com/adsense/answer/2923881). late String adClient; + /// The (optional) + /// [AdSense Code Parameters](https://support.google.com/adsense/answer/9955214#adsense_code_parameter_descriptions). + AdSenseCodeParameters? adSenseCodeParameters; + /// Initializes the AdSense SDK with your [adClient]. /// /// The [adClient] parameter is your AdSense [Publisher ID](https://support.google.com/adsense/answer/2923881). /// + /// The [adSenseCodeParameters] let you configure various settings for your + /// ads. All parameters are optional. See + /// [AdSense code parameter descriptions](https://support.google.com/adsense/answer/9955214#adsense_code_parameter_descriptions). + /// /// Should be called ASAP, ideally in the `main` method. - // - // TODO(dit): Add the "optional AdSense code parameters", and render them - // in the right location (the script tag for h5 + the ins for display ads). - // See: https://support.google.com/adsense/answer/9955214?hl=en#adsense_code_parameter_descriptions Future initialize( String adClient, { + AdSenseCodeParameters? adSenseCodeParameters, @visibleForTesting bool skipJsLoader = false, @visibleForTesting web.HTMLElement? jsLoaderTarget, }) async { @@ -34,8 +42,13 @@ class AdSense { return; } this.adClient = adClient; + this.adSenseCodeParameters = adSenseCodeParameters; if (!skipJsLoader) { - await loadJsSdk(adClient, jsLoaderTarget); + await loadJsSdk( + adClient, + target: jsLoaderTarget, + dataAttributes: adSenseCodeParameters?.toMap, + ); } else { debugLog('initialize called with skipJsLoader. Skipping loadJsSdk.'); } diff --git a/packages/google_adsense/lib/src/core/js_interop/js_loader.dart b/packages/google_adsense/lib/src/core/js_interop/js_loader.dart index 6c124ef5a5f7..12b44eacaa0b 100644 --- a/packages/google_adsense/lib/src/core/js_interop/js_loader.dart +++ b/packages/google_adsense/lib/src/core/js_interop/js_loader.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'dart:js_interop'; +import 'dart:js_interop_unsafe' show JSObjectUnsafeUtilExtension; import 'package:web/web.dart' as web; import '../../utils/logging.dart'; @@ -17,7 +18,14 @@ const String _URL = /// /// [target] can be used to specify a different injection target than /// `window.document.head`, and is normally used for tests. -Future loadJsSdk(String adClient, web.HTMLElement? target) async { +/// +/// [dataAttributes] are used to configure the dataset `data-` attributes of the +/// created script element. +Future loadJsSdk( + String adClient, { + web.HTMLElement? target, + Map? dataAttributes, +}) async { if (_sdkAlreadyLoaded(adClient, target)) { debugLog('adsbygoogle.js already injected. Skipping call to loadJsSdk.'); return; @@ -46,9 +54,21 @@ Future loadJsSdk(String adClient, web.HTMLElement? target) async { script.src = scriptUrl; } + _applyDataAttributes(script, dataAttributes); + (target ?? web.document.head)!.appendChild(script); } +// Applies a map of [attributes] to the `dataset` of [element]. +void _applyDataAttributes( + web.HTMLElement element, + Map? attributes, +) { + attributes?.forEach((String key, String value) { + element.dataset.setProperty(key.toJS, value.toJS); + }); +} + // Whether the script for [adClient] is already injected. // // [target] can be used to specify a different injection target than diff --git a/packages/google_adsense/lib/src/h5/h5.dart b/packages/google_adsense/lib/src/h5/h5.dart index 272ed387e23c..8b7e0a96842b 100644 --- a/packages/google_adsense/lib/src/h5/h5.dart +++ b/packages/google_adsense/lib/src/h5/h5.dart @@ -2,11 +2,26 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:async'; + import '../core/js_interop/adsbygoogle.dart'; import 'h5_js_interop.dart'; -export 'enums.dart' hide MaybeEnum, interstitialBreakType; -export 'h5_js_interop.dart' hide H5JsInteropExtension; +export 'enums.dart' + show BreakFormat, BreakStatus, BreakType, PreloadAdBreaks, SoundEnabled; +export 'h5_js_interop.dart' + show + AdBreakDonePlacementInfo, + AdBreakPlacement, + AdConfigParameters, + H5AdBreakDoneCallback, + H5AdDismissedCallback, + H5AdViewedCallback, + H5AfterAdCallback, + H5BeforeAdCallback, + H5BeforeRewardCallback, + H5OnReadyCallback, + H5ShowAdFn; /// A client to request H5 Games Ads (Ad Placement API). class H5GamesAdsClient { @@ -16,7 +31,12 @@ class H5GamesAdsClient { void adBreak( AdBreakPlacement placementConfig, ) { - adsbygoogle.adBreak(placementConfig); + // Delay the call to `adBreak` so tap users don't trigger a click on the ad + // on pointerup. This should leaves enough time for Flutter to settle its + // tap events, before triggering the H5 ad. + Timer(const Duration(milliseconds: 100), () { + adsbygoogle.adBreak(placementConfig); + }); } /// Communicates the app's current configuration to the Ad Placement API. diff --git a/packages/google_adsense/lib/src/h5/h5_js_interop.dart b/packages/google_adsense/lib/src/h5/h5_js_interop.dart index 4b645a78188c..5a0c4342e8e4 100644 --- a/packages/google_adsense/lib/src/h5/h5_js_interop.dart +++ b/packages/google_adsense/lib/src/h5/h5_js_interop.dart @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:async'; import 'dart:js_interop'; import 'package:flutter/widgets.dart' show visibleForTesting; @@ -117,9 +118,14 @@ extension type AdBreakPlacement._(JSObject _) implements JSObject { beforeAd: beforeAd?.toJS, afterAd: afterAd?.toJS, beforeReward: beforeReward != null - ? (JSFunction fn) { + ? (JSFunction showAdFn) { beforeReward(() { - fn.callAsFunction(); + // Delay the call to `showAdFn` so tap users don't trigger a click on the + // ad on pointerup. This should leaves enough time for Flutter to settle + // its tap events, before triggering the H5 ad. + Timer(const Duration(milliseconds: 100), () { + showAdFn.callAsFunction(); + }); }); }.toJS : null, diff --git a/packages/google_adsense/pubspec.yaml b/packages/google_adsense/pubspec.yaml index efc767669770..26b865b5f4d9 100644 --- a/packages/google_adsense/pubspec.yaml +++ b/packages/google_adsense/pubspec.yaml @@ -2,7 +2,7 @@ name: google_adsense description: A wrapper plugin with convenience APIs allowing easier inserting Google Adsense HTML snippets withing a Flutter UI Web application repository: https://github.com/flutter/packages/tree/main/packages/google_adsense issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+google_adsense%22 -version: 0.1.0 +version: 0.1.1 environment: sdk: ^3.4.0