Skip to content

Commit

Permalink
[google_adsense] Add optional init parameters. (#8297)
Browse files Browse the repository at this point in the history
Adds `AdSenseCodeParameters` configuration object for `adSense.initialize`.

Adds a 100ms delay to `adBreak` and `showAdFn` so on tap devices, when an ad renders on top of the flutter element that triggered the ad, the pointer up event doesn't trigger the ad immediately.

Fixes a typo in the `MATCHED_CONTENT_ROWS_NUM` and `MATCHED_CONTENT_COLUMNS_NUM` constants, that would have resulted in passing the wrong parameter name to the AdSense JS.

Continues the tightening of exports by making all the `export`s from barrel files **explicit**, so it's harder to accidentally expose any unintended API surface.

## Issues

* Continuation of: #8233
* Part of: flutter/flutter#40376
  • Loading branch information
ditman authored Dec 16, 2024
1 parent eb73582 commit 645621e
Show file tree
Hide file tree
Showing 14 changed files with 226 additions and 26 deletions.
6 changes: 6 additions & 0 deletions packages/google_adsense/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
14 changes: 10 additions & 4 deletions packages/google_adsense/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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

Expand Down
20 changes: 20 additions & 0 deletions packages/google_adsense/doc/initialization.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

<?code-excerpt "../example/lib/h5.dart (initialize-with-code-parameters)"?>
```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).
34 changes: 33 additions & 1 deletion packages/google_adsense/example/integration_test/core_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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()
Expand Down
10 changes: 7 additions & 3 deletions packages/google_adsense/example/integration_test/h5_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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');
Expand All @@ -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');
});
Expand Down Expand Up @@ -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);
Expand Down
10 changes: 9 additions & 1 deletion packages/google_adsense/example/lib/h5.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}

Expand Down
4 changes: 2 additions & 2 deletions packages/google_adsense/lib/src/adsense/ad_unit_params.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<br>
/// Sets the number of rows<br>
/// 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<br>
/// Sets the number of columns<br>
/// 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';
Expand Down
6 changes: 3 additions & 3 deletions packages/google_adsense/lib/src/adsense/adsense.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;
65 changes: 65 additions & 0 deletions packages/google_adsense/lib/src/core/adsense_code_parameters.dart
Original file line number Diff line number Diff line change
@@ -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 = <String, String>{
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<String, String> _adSenseCodeParameters;

/// `Map` representation of this configuration object.
Map<String, String> get toMap => _adSenseCodeParameters;
}
23 changes: 18 additions & 5 deletions packages/google_adsense/lib/src/core/google_adsense.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,34 @@ 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;

/// 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<void> initialize(
String adClient, {
AdSenseCodeParameters? adSenseCodeParameters,
@visibleForTesting bool skipJsLoader = false,
@visibleForTesting web.HTMLElement? jsLoaderTarget,
}) async {
Expand All @@ -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.');
}
Expand Down
22 changes: 21 additions & 1 deletion packages/google_adsense/lib/src/core/js_interop/js_loader.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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<void> loadJsSdk(String adClient, web.HTMLElement? target) async {
///
/// [dataAttributes] are used to configure the dataset `data-` attributes of the
/// created script element.
Future<void> loadJsSdk(
String adClient, {
web.HTMLElement? target,
Map<String, String>? dataAttributes,
}) async {
if (_sdkAlreadyLoaded(adClient, target)) {
debugLog('adsbygoogle.js already injected. Skipping call to loadJsSdk.');
return;
Expand Down Expand Up @@ -46,9 +54,21 @@ Future<void> 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<String, String>? 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
Expand Down
26 changes: 23 additions & 3 deletions packages/google_adsense/lib/src/h5/h5.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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.
Expand Down
Loading

0 comments on commit 645621e

Please sign in to comment.