Skip to content

feat(banner): Support maximum height for Inline Adaptive banners#663

Merged
dylancom merged 23 commits intoinvertase:mainfrom
OLIOEX:main
Jun 7, 2025
Merged

feat(banner): Support maximum height for Inline Adaptive banners#663
dylancom merged 23 commits intoinvertase:mainfrom
OLIOEX:main

Conversation

@markwilcox
Copy link
Contributor

@markwilcox markwilcox commented Nov 28, 2024

Description

Inline adaptive banners are the best variety to use on cell-based layouts like list views. They can also maximise revenue on scrollViews by taking advantage of more available inventory at different sizes. However they can grow very tall and ruin the UX in these situations.

The native iOS and Android SDKs support creating an inline adaptive banner with a maximum height to solve this issue. This PR is intended to add support for that behaviour from React Native.

Related issues

Fixes #541

Release Summary

Add support for optional maxHeight property on BannerAd

Checklist

  • I read the Contributor Guide
    and followed the process outlined there for submitting PRs.
    • Yes
  • My change supports the following platforms;
    • Android
    • iOS
  • My change includes tests;
    • e2e tests added or updated in __tests__e2e__
    • jest tests added or updated in __tests__
  • I have updated TypeScript types that are affected by my change.
  • This is a breaking change;
    • Yes
    • No

Test Plan

I couldn't see any relevant tests to add to in e2e or tests. The new prop is optional and only has any effect on an inline adaptive banner. I went for forgiving - e.g. setting the prop on the wrong banner format is ignored, setting a prop smaller than the minimum supported by the native SDKs automatically increases it to the minimum size (32px).

I added a test in the example app and ensured it worked there on old and new architectures. Let me know if you want any other tests added, or to be more strict validating use of the prop (e.g. against minimum size, or in combination with other ad formats).

I'm not 100% on this implementation because of the way we need both the sizes prop and the maxHeight prop to set a size in the native ad request. As far as I know the order of calling these isn't guaranteed, so I'm storing the original sizes array, and recalculating the sizes if the maxHeight prop gets set on the native side. It works, but I wonder if it will ultimately create 2 requests for each ad shown and impact the show rate? If you can suggest a better implementation strategy without changing the API too much I'm willing to give it a go.

If you think this approach is OK then I'll update the docs.

🔥

Think react-native-google-mobile-ads is great? Please consider supporting the project with any of the below:

  • 👉 Star this repo on GitHub ⭐️
  • 👉 Follow Invertase on Twitter

@docs-page
Copy link

docs-page bot commented Nov 28, 2024

To view this pull requests documentation preview, visit the following URL:

docs.page/invertase/react-native-google-mobile-ads~663

Documentation is deployed and generated using docs.page.

@CLAassistant
Copy link

CLAassistant commented Nov 28, 2024

CLA assistant check
All committers have signed the CLA.

@markwilcox markwilcox changed the title feat(banner) Support maximum height for Inline Adaptive banners feat(banner): Support maximum height for Inline Adaptive banners Nov 28, 2024
@codecov
Copy link

codecov bot commented Nov 29, 2024

Codecov Report

Attention: Patch coverage is 39.28571% with 34 lines in your changes missing coverage. Please review.

Project coverage is 37.33%. Comparing base (a34c7ba) to head (a2e3e5e).
Report is 148 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #663      +/-   ##
==========================================
- Coverage   43.72%   37.33%   -6.38%     
==========================================
  Files          30       36       +6     
  Lines         549      659     +110     
  Branches      151      174      +23     
==========================================
+ Hits          240      246       +6     
- Misses        309      413     +104     
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@dylancom
Copy link
Collaborator

dylancom commented Dec 2, 2024

Hi, thanks for the PR. I am not sure about the potential double requests. Also the extra "sizeStrings" array condition isn't very clear.

My current thinking is that we adapt the current size prop and create some helper to provide a max height:
<Banner size={getInlineAdaptiveBannerAdSize(320, 100)} />.
The helper method outputs a string that stringToAdSize (with small changes) will understand. So it could become something like: "INLINE_ADAPTIVE_BANNER_300x100" || "INLINE_ADAPTIVE_BANNER_300_100".

The Flutter implementation for e.g. has this methods:

  • AdSize.getCurrentOrientationInlineAdaptiveBannerAdSize(int width)
  • AdSize.getInlineAdaptiveBannerAdSize(int width, int maxHeight)

@mikehardy what do you think?

@markwilcox
Copy link
Contributor Author

My current thinking is that we adapt the current size prop and create some helper to provide a max height: <Banner size={getInlineAdaptiveBannerAdSize(320, 100)} />. The helper method outputs a string that stringToAdSize (with small changes) will understand. So it could become something like: "INLINE_ADAPTIVE_BANNER_300x100" || "INLINE_ADAPTIVE_BANNER_300_100".

Currently from React Native we can't set the width anywhere on the adaptive banners as it's hardcoded to use the screen width.

If I was going to change the API I'd change that too. Maybe keep the JS API but allow separate width and maxHeight to be passed, then make them all a single object with the size string that gets passed across the bridge (or set as a single prop in the new architecture)?

Going for conversion to strings on the JS side and then parsing those strings feels a bit messy. The alternative I guess would be to go for minimal change and only support a fixed set of heights. Probably revenue vs. UX impact the only sizes that will really matter for height restriction are 50px, 100px & 250px.

We'll probably experiment with sizes on the fork I've made in production (millions of monthly impressions) so I can answer some questions about double requests (ideally the native SDKs would cancel in-flight requests if you made another one immediately, but no idea if they do) / show rate, and eCPM impact at different sizes empirically.

@mikehardy
Copy link
Collaborator

🤔

Currently from React Native we can't set the width anywhere on the adaptive banners as it's hardcoded to use the screen width.

If I was going to change the API I'd change that too. Maybe keep the JS API but allow separate width and maxHeight to be passed, then make them all a single object with the size string that gets passed across the bridge (or set as a single prop in the new architecture)?

That was surprising to read for me. I don't use this package so I am a maybe-interesting combination of not that knowledgeable but also interested in helping the package do well. "maybe-interesting" because I have a fresh set of eyes for most of these things and even I understand that control over the UX in design is vital, and that means ability to constrain width and height, yes? Thus, surprising we don't permit real control here

Looking at Flutter implementation, it's in there - width and height are fundamental on the size supertype:

https://github.com/googleads/googleads-mobile-flutter/blob/6aa897e3dcc2b2d97f4df61192b015388ce876d6/packages/google_mobile_ads/android/src/main/java/io/flutter/plugins/googlemobileads/FlutterAdSize.java#L22-L25

...some types have extra stuff like orientation and what not, and they have a lot of machinery in there for factory constructor type things, but width and height are controllable

We...do not have that based on what I see, right?

export enum BannerAdSize {

So, I'd say this is a worthy API change as well. Backwards-compatible or not - if I understand things correctly it seems widthxheight control is critical and adding them would be "doing it right"

Going for conversion to strings on the JS side and then parsing those strings feels a bit messy

Agreed there - that seems like the wrong way to go vs allowing width and height to be specifiable first-class and passed separately from JS to native

@mikehardy
Copy link
Collaborator

mikehardy commented Dec 2, 2024

Even the Flutter example for inline adaptive takes great care to determine the size (in their case, meaning width) minus the gutters. It's so important to control the width they show it in their "here is our most basic imagined use so you can just try it" example

@dylancom
Copy link
Collaborator

dylancom commented Dec 2, 2024

Going for conversion to strings on the JS side and then parsing those strings feels a bit messy. The alternative I guess would be to go for minimal change and only support a fixed set of heights. Probably revenue vs. UX impact the only sizes that will really matter for height restriction are 50px, 100px & 250px.

I agree, that suggestion came from using what is already available in this lib (you can pass sizes such as "300x200")
which are old ideas now:

Size: BannerAdSize | string;
_The size of the banner. Can be a predefined size via `BannerAdSize` or custom dimensions, e.g. `300x200`_

So now is a great time to adjust our current api.
My personal preference goes to keep using the size prop we already have instead of adding additional props.
This is also how Flutter does it and I think we can take notes there.

Flutter example:

AdSize size = AdSize.getCurrentOrientationInlineAdaptiveBannerAdSize(
        _adWidth.truncate());`
        
// If you wish to limit the height of the banner, you can use the static method
// AdSize.getInlineAdaptiveBannerAdSize(int width, int maxHeight)

BannerAd(
      adUnitId: 'ca-app-pub-3940256099942544/9214589741',
      size: size,
      request: AdRequest()
)

@markwilcox
Copy link
Contributor Author

So now is a great time to adjust our current api. My personal preference goes to keep using the size prop we already have instead of adding additional props. This is also how Flutter does it and I think we can take notes there.

Flutter example:

AdSize size = AdSize.getCurrentOrientationInlineAdaptiveBannerAdSize(
        _adWidth.truncate());`
        
// If you wish to limit the height of the banner, you can use the static method
// AdSize.getInlineAdaptiveBannerAdSize(int width, int maxHeight)

BannerAd(
      adUnitId: 'ca-app-pub-3940256099942544/9214589741',
      size: size,
      request: AdRequest()
)

The Flutter API is doing a lower level wrapper around the native SDK AdSize creation methods. To me that seems like an unnecessary extra round trip to native land for a trivial bit of config. Generating the value of a prop in native land and then passing it back again isn't really "the React Native way" in my experience.

We could replicate that without the extra round trip by calling a helper function that's pure JS and just constructs an object with the necessary params to pass across to native. Or we could just have separate (optional) props on the BannerAd itself that get converted to the appropriate object to pass to native. I'd be OK with going for either of those. I think separate optional props feels more React-like as an API, but the helper function is closer to the native SDK & Flutter APIs if consistency there is more helpful.

If there's a consensus on which one to go for I can update the PR. Might need a couple of weeks to prioritise it again.

@dylancom
Copy link
Collaborator

dylancom commented Dec 3, 2024

Oke so my first preference went towards bundling all size related things in the "size" prop.
But I am fine with an extra width and maxHeight prop.

Thanks for contributing!

@dylancom dylancom force-pushed the main branch 3 times, most recently from 107d7be to 6c186c7 Compare December 17, 2024 08:14
@dylancom
Copy link
Collaborator

If we can't be sure that maxHeight is set before sizes we might group them in BaseAd: sizeConfig={{ sizes, maxHeight }}. So the native side receives them as a bundle but the user can just use seperate size and maxHeight props on the component. Thanks @mikehardy for the idea.

@markwilcox
Copy link
Contributor Author

If we can't be sure that maxHeight is set before sizes we might group them in BaseAd: sizeConfig={{ sizes, maxHeight }}. So the native side receives them as a bundle but the user can just use seperate size and maxHeight props on the component. Thanks @mikehardy for the idea.

Yes, I was going to do this with width in there as well.

@markwilcox
Copy link
Contributor Author

Updated this PR @SumitR9910 @dylancom

Sorry it has taken a long while. Work has been spread out across a lot of short sessions, so I won't be at all offended about any consistency nitpicks!

Seems to be working for me both platforms and old + new architecture in the test app. I'm going to try running the fork in our production app soon (currently old arch - planning a new arch migration very soon).

@dylancom dylancom requested a review from mikehardy May 2, 2025 06:01
@dylancom
Copy link
Collaborator

dylancom commented May 2, 2025

@markwilcox No problem and thanks again for the contribution! Hope @mikehardy can also have a look.

@dylancom
Copy link
Collaborator

dylancom commented May 7, 2025

Great work @markwilcox, will wait for @mikehardy for assistance on the Android part.


import android.content.Context;
import android.widget.FrameLayout;
import com.facebook.react.bridge.ReadableArray;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this import seems to have been added but there are not other diff + lines that reference it ?

import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.annotation.Nullable;
import android.util.Log;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this import was added but no lines reference it, likely vestige of implementation debugging

Copy link
Collaborator

@mikehardy mikehardy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good to me - I did leave two little notes but they were about seemingly-unused imports. If removing them still results in successful compilation then committing that fixup would be nice, but that's not a functional change and is really trivial. So this is a big +1 from me, this looks great. Sorry for the delay, thank you for your patience!

@mikehardy mikehardy requested a review from dylancom May 15, 2025 19:32
@markwilcox
Copy link
Contributor Author

Ah, sorry I missed the notification for this comment. Just saw it when coming back to check.

This looks good to me - I did leave two little notes but they were about seemingly-unused imports. If removing them still results in successful compilation then committing that fixup would be nice, but that's not a functional change and is really trivial. So this is a big +1 from me, this looks great. Sorry for the delay, thank you for your patience!

I'll remove those imports and fix the lint/prettier issues. I'm not sure exactly what's missing for the codecov/project check though? Is there somewhere that needs a new test adding? Or does it need updating with main? It says 6 new files in the report but I've not added 6 files to do this... 🤷‍♂️

@dylancom
Copy link
Collaborator

dylancom commented Jun 3, 2025

@markwilcox Removing the imports and fixing the lint/prettier issues will be enough to merge :)

@markwilcox
Copy link
Contributor Author

@dylancom Imports removed, lint/prettier issues fixed and PR updated to latest main.

@markwilcox
Copy link
Contributor Author

Sorry - Android lint had failed and I'd missed there isn't a lint:android:fix option.

You could just add:
"lint:android:fix": "google-java-format --replace --glob=\"android/**/*.java\"",

@markwilcox
Copy link
Contributor Author

Lint should all pass now if you wouldn't mind approving the workflows again @dylancom?

@dylancom dylancom merged commit 017ffbf into invertase:main Jun 7, 2025
9 of 11 checks passed
@dylancom
Copy link
Collaborator

dylancom commented Jun 7, 2025

Thanks for contributing @markwilcox, great work!

@mikehardy
Copy link
Collaborator

🎉 This PR is included in version 15.4.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature Request] Support the max height option for inline adaptive banners

5 participants