Add iAB Standard Size Experiences to a React Native app

Render Genuin IAB ad slots in a React Native Android app with genuin-react-native-ads-v3 1.4.0.

genuin-react-native-ads-v3 puts a Genuin IAB ad slot (an Interactive Advertising Bureau standard size such as 300×250) on screen as one React component. You pass a tag ID, and the package hosts the native ad view from the iAB Standard Size Experience SDK for Android 0.v3.11, which it pins. The SDK fetches the creative, plays it, tracks impressions and viewability, and reports analytics on its own; there's no player to wire up and no measurement code to add.

The package is Android only. On iOS, <GenuinAd /> renders nothing and takes no space, so a shared screen stays mountable. For the native SDK underneath, see Add iAB Standard Size Experiences to an Android app; for the iOS SDK, see Add iAB Standard Size Experiences to an iOS app.

Requirements

Minimum
React NativeAny version is accepted as a peer dependency; the package is built and tested against 0.83.10. It targets the New Architecture and keeps a legacy module for apps on 0.74 to 0.81 with newArchEnabled=false.
AndroidminSdk 24, compileSdk and targetSdk 36
Kotlin2.0.21 or later. The Genuin AARs carry Kotlin 2.0 metadata, which Kotlin 1.9 can't read.
Android Gradle Plugin8.9.1 or later
Java17
Core-library desugaringRequired by the Media3 IMA extension the SDK plays ads with
Native SDKcom.begenuin genuin-sdk, feed, core-v3, and instream-ads 0.v3.11, pulled in by the package
CredentialsA brand API key and at least one tag ID

The API key comes from Brand Control Center under Build > Publish Brand Community > SDK Integration, with a Live Environment and a QA Environment key. A tag ID identifies one ad placement. It's the id of an iAB Standard Tag, created in Brand Control Center under Grow > Off Site > Extended > iAB Standard Tag and accepted for your brand. Your Genuin team passes it to you.

Step 1: Install the package

Terminal
bash
npm install genuin-react-native-ads-v3   # or: yarn add genuin-react-native-ads-v3

Autolinking registers the native module and the BGInstreamAdView view manager. There's nothing to add to MainApplication or MainActivity, and no manifest edit.

Step 2: Configure the Android build

  1. Set the toolchain versions in android/build.gradle. React Native libraries read them from rootProject.ext, so your app decides what the module compiles with:
android/build.gradle
groovy
buildscript {
    ext {
        minSdkVersion = 24
        compileSdkVersion = 36
        targetSdkVersion = 36
        kotlinVersion = "2.0.21"
    }
}
  1. Enable core-library desugaring and Java 17 in android/app/build.gradle. The React Native 0.83 template already has this block with the same artifact; check before adding it twice:
android/app/build.gradle
groovy
android {
    compileOptions {
        coreLibraryDesugaringEnabled true
        sourceCompatibility JavaVersion.VERSION_17
        targetCompatibility JavaVersion.VERSION_17
    }
}

dependencies {
    coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.5'
}

Without it the build fails with Invoke-customs are only supported starting with Android O, or the app crashes on the first ad with a java.time class missing. Neither message names Genuin.

  1. Keep google() and mavenCentral() in your app's repositories; the React Native template declares both. The Genuin artifacts resolve from Maven Central.
  2. Keep the INTERNET permission the template declares. The Genuin libraries don't declare it themselves, and ACCESS_NETWORK_STATE arrives transitively. The SDK's instream-ads module merges in com.google.android.gms.permission.AD_ID to resolve advertising-ID macros in VAST tags; declare advertising-ID use in the Play Console Data safety form, or the submission is rejected.

Note: Release builds need no ProGuard work. The package ships consumer R8 rules for the Genuin SDK, Google IMA, and Retrofit, verified against a minified build with R8 full mode.

Step 3: Initialize the SDK

initialize must resolve before any slot mounts. A slot that mounts first throws IllegalStateException: SDK not initialized natively, so gate rendering on state:

App.tsx
tsx
import { useEffect, useState } from 'react';
import { Platform } from 'react-native';
import { initialize, setLoggingEnabled } from 'genuin-react-native-ads-v3';

export function useGenuinAds(): boolean {
  const [ready, setReady] = useState(false);
  useEffect(() => {
    if (Platform.OS !== 'android') return;
    if (__DEV__) setLoggingEnabled(true);           // before initialize, to catch start-up lines
    initialize({ apiKey: '<<YOUR_API_KEY>>', env: 'PROD' })
      .then(() => setReady(true))
      .catch(console.error);
  }, []);
  return ready;
}
OptionTypeRequiredDefaultDescription
apiKeystringYes—The brand API key. A blank value rejects the promise with code GENUIN_SDK_INIT_ERROR.
env'PROD' | 'QA'Yes—The environment the key and tags belong to. Any other string falls back to PROD.
fontFamilystringNoSDK defaultA font family for text the SDK renders
audioBehaviorConfig.duckVolumeDuringTransientCanDucknumberNo0.15Volume held while another app takes transient audio focus, from 0 to 1. Out-of-range values fall back to the default instead of failing initialization.
audioBehaviorConfig.focusLossHandling'MUTE_OUTPUT' | 'PAUSE_PLAYBACK'No'MUTE_OUTPUT'What happens on a full loss of audio focus

initialize is safe to call more than once; the SDK builds its graph on the first call only. isInitialized() returns true once it has completed in this process.

setLoggingEnabled(true) turns on the SDK's own diagnostic log, which is off in every published build however your app is built. Read it with adb logcat -s GenuinSDK. The wrapper mirrors each slot's events to a second tag, GenuinAdEvent, in debug builds. Gate logging on __DEV__, because the log names your tag IDs.

To initialize natively instead, before the first React render, call GenuinSDKPluginModule.initSDK(application, "<<YOUR_API_KEY>>") from MainActivity.onCreate. Pass true as a third argument for the QA environment. Both paths land in the same native call.

Step 4: Render an ad slot

ArticleScreen.tsx
tsx
import { GenuinAd, GenuinAdFormat, type GenuinAdEvent } from 'genuin-react-native-ads-v3';

export default function ArticleScreen() {
  const ready = useGenuinAds();
  return (
    <>
      <ArticleBody />
      {ready && (
        <GenuinAd
          tagId="{{TAG_ID}}"
          format={GenuinAdFormat.SIZE_300X250}
          onAdEvent={(event: GenuinAdEvent) => console.log(event)}
        />
      )}
    </>
  );
}

tagId is the only required prop. You don't size the slot. The component applies the format's fixed dp size itself, because the native view lays its content out at that size whatever box React Native gives it.

PropTypeRequiredDefaultDescription
tagIdstringYes—The IAB tag for this placement. A blank value renders the placeholder and makes no request.
formatGenuinAdFormatNoSIZE_300X250The slot size. GenuinAdFormat is exported as a value, so GenuinAdFormat.SIZE_320X50 autocompletes; the bare string "SIZE_320X50" also type-checks.
uniqueIdstringNo—Set a different value for each slot that shares a tagId and format on one screen. The SDK keys per-slot state on iab_<tagId>_<format>_<uniqueId>.
geoLocationGenuinGeoLocationNo—Geo targeting for this slot only, layered over setGeoLocation field by field. See Step 5.
onAdEvent(event: GenuinAdEvent) => voidNo—Lifecycle events. Analytics don't need it. See Step 6.
styleViewStyleNo—Placement only: margins and alignSelf. A width or height here overrides the format's size and clips the ad or leaves dead space.

Two slots with the same tag and format need distinct uniqueId values, or they fight over one piece of native state:

Two slots
tsx
<GenuinAd tagId="{{TAG_ID}}" format={GenuinAdFormat.SIZE_320X50} uniqueId="header" />
<GenuinAd tagId="{{TAG_ID}}" format={GenuinAdFormat.SIZE_320X50} uniqueId="footer" />

GENUIN_AD_SIZES exports each format's width, height, and, for the collapsible format, collapsedHeight, so a list can reserve the row height before the slot mounts.

Ad formats

formatSize (dp)Use
SIZE_320X50320 × 50Standard banner for headers and footers
SIZE_320X100320 × 100Large banner
SIZE_300X250300 × 250Medium rectangle, in-article or mid-feed
SIZE_320X480320 × 480Half-page portrait, interstitial-style blocks
SIZE_300X600300 × 600Half-page filmstrip
SIZE_300X250_COLLAPSIBLE300 × 250, collapsing to 300 × 50Sticky placements that shrink after the first view
SIZE_1280X7201280 × 720Landscape video at a fixed size; wider than any phone in portrait
SIZE_FS_LANDSCAPEAdapts to its parentThe same landscape surface sized to the screen

Three formats need layout care:

  • SIZE_300X250_COLLAPSIBLE resizes its own box. React Native doesn't re-lay-out a native child that changes size, so the component listens for the collapsed event and switches between height and collapsedHeight. You still receive the event to resize a container around the slot. Setting height in style opts out and pins the box.
  • SIZE_1280X720 is declared at its full IAB width, so in a portrait container the box overflows sideways. Give it a landscape or full-screen parent. The native view clamps its own content; the overflow is the React Native box.
  • SIZE_FS_LANDSCAPE gets flex: 1 instead of a dp box. In a full-screen parent it needs no style. In a parent that doesn't size its children, such as a ScrollView content container, it renders 0 × 0; give it { width: '100%', aspectRatio: 16 / 9 } or explicit values. The ratio is honoured between 1.25:1 and 2.4:1, and a box outside that band is letter-boxed or pillar-boxed.

Step 5: Supply geo targeting (Optional)

Ad URLs carry geo macros that the ad server expands, such as [DMA_CODE] and [USER_GEO_CITY]. By default the SDK resolves them from the request IP, so most apps need nothing. Supply geo yourself when you know better than the IP, or when you need a DMA code, which the IP lookup can't provide.

Geo targeting
ts
import { setGeoLocation } from 'genuin-react-native-ads-v3';

setGeoLocation({
  country: 'US',
  region: 'NY',
  city: 'New York',
  dmaCode: '501',
  latitude: 40.7128,
  longitude: -74.006,
});
FieldTypeMacros resolvedFallback
countrystring[COUNTRY_ID], [USER_GEO_COUNTRY], [APP_COUNTRY]IP lookup
regionstring[REGION], [USER_GEO_REGION], [APP_REGION]IP lookup
citystring[CITY], [USER_GEO_CITY], [APP_LOC]IP lookup
dmaCodestring[DMA_CODE], [USER_GEO_DMA], [APP_METRO]None. The DMA macros stay unresolved unless you set it.
latitudenumber[LOCATION_LAT], [USER_GEO_LAT], [APP_LAT]IP lookup
longitudenumber[LOCATION_LON], [USER_GEO_LON], [APP_LONG]IP lookup

Every field is optional and resolves independently, so setting only dmaCode leaves the rest to the IP lookup. setGeoLocation() with no argument clears the value. Setting both latitude and longitude also reports the geo type as device location rather than IP-derived; one coordinate alone is ignored for that purpose.

Timing is forgiving: setGeoLocation doesn't need initialize to have run, and each ad request reads the geo current when it builds its URL. A location that arrives after the first slot is on screen reaches every later request; requests already in flight keep their geo. For one slot that needs a different geo, pass the geoLocation prop; its fields win over the app-wide value, which wins over the IP lookup.

Step 6: Observe lifecycle events

onAdEvent receives a discriminated union with 11 members. Ad positions are 1-based.

Lifecycle events
tsx
<GenuinAd
  tagId="{{TAG_ID}}"
  onAdEvent={(event) => {
    switch (event.type) {
      case 'noAds':
      case 'allAdsCompleted':
        setShowAd(false);                      // reclaim the space yourself
        break;
      case 'started':
        console.log(`ad ${event.index}/${event.total} (${event.adType}) started`);
        break;
    }
  }}
/>
event.typePayloadFires when
loading—The feed began loading
loadedcountThe feed resolved with count renderable ads
noAds—The feed resolved but empty, or the request failed
appearedindex, total, adTypeAd index of total became active on screen
startedindex, total, adTypePlayback began
adCompletedindex, total, adTypeThis ad played to the end. Only a real playout reports it; an ad that never filled reports noFill instead.
changedfrom, toThe pager moved between ads
loadTimeoutindex, total, adType, timeoutMsThe 500 ms waterfall budget elapsed before this ad resolved. Informational: the waterfall continues and still reports started or noFill.
noFillindex, total, adTypeThe ad waterfall produced no fill
allAdsCompletedtotalEvery ad in the slot is done and none is pending. Slot-level, and the cue to collapse the slot or resume your own content. Fires once per completed pass.
collapsedisCollapsedThe collapsible slot was collapsed or expanded. Never fires for the fixed sizes.

adType reports what the slot led with: 'audio' (a VAST audio ad), 'external' (Pangle or Google Mobile Ads), 'sponsored', 'video', or 'unknown'. It's typed as an open string because the SDK adds labels over time.

Step 7: Enable an external ad network (Optional)

Genuin can fill slots from Pangle (ByteDance) or Google Mobile Ads as well as its own inventory. Both are off by default and need no React Native code: <GenuinAd /> and its events are unchanged, and slots the server books against an external network start filling with adType: 'external'.

  1. Turn on the network in android/gradle.properties:
gradle.properties
properties
genuinEnablePangle=true
genuinEnableGmaAds=true
  1. For Pangle, add ByteDance's repository inside allprojects { repositories { ... } } in android/build.gradle. Pangle isn't on Maven Central, and a POM can't contribute a repository, so the module's own declaration isn't enough:
android/build.gradle
groovy
maven {
    url 'https://artifact.bytedance.com/repository/pangle/'
    content { includeGroupByRegex 'com\\.pangle.*' }
}

If the flag is on and the repository is missing, the build fails at configuration time with a message that names this step.

  1. For Google Mobile Ads, declare your application ID in android/app/src/main/AndroidManifest.xml. Google's SDK resolves from the google() repository you already have.
AndroidManifest.xml
xml
<application>
    <meta-data
        android:name="com.google.android.gms.ads.APPLICATION_ID"
        android:value="{{GMA_APPLICATION_ID}}" />
</application>

Before you ship Pangle, three things are yours to handle. Pangle ships native libraries for arm64-v8a and armeabi-v7a only, so on an x86_64 emulator it never fills and looks like a no-fill; test on a device or an arm64 image. Pangle collects device and advertising identifiers and adds the TikTok App Events SDK transitively, so your Play Data safety declaration grows. The module requests personalised ads without a consent signal, so talk to Genuin before shipping Pangle under GDPR or a similar regime.

To confirm a network module is in your APK, turn on logging and look for BGAdNetworkRegistry: ad networks available = [pangle] in logcat. If your app uses Expo prebuild, android/ is generated; put the repository in a config plugin or re-apply it after expo prebuild --clean.

Troubleshooting

SymptomCauseFix
Build fails with Kotlin metadata errorskotlinVersion is below 2.0.21Set kotlinVersion = "2.0.21" or later in rootProject.ext (Step 2)
Build fails with Invoke-customs or java.time errorsCore-library desugaring is offEnable it (Step 2)
genuinEnablePangle=true, but the ByteDance Maven repository is not declaredThe app's allprojects repositories lack the Pangle repositoryAdd it (Step 7), or set the flag to false
Crash with IllegalStateException: SDK not initializedA slot mounted before initialize resolvedRender slots only after the promise resolves (Step 3)
Every slot shows "No ads available" and logcat is silentLogging is off; then the key, env, or tag is wrongCall setLoggingEnabled(true) before initialize, then check that the key matches the account, env is right, and the tag is active
No ads in a release build, but debug worksR8 stripped Retrofit's generic signatures: ClassCastException: java.lang.Class cannot be cast to java.lang.reflect.ParameterizedTypeThe package ships the keep rules; check that your proguardFiles setting doesn't drop consumer rules
The ad overflows sidewaysSIZE_1280X720 in a portrait containerHost it in a landscape or full-screen parent
SIZE_FS_LANDSCAPE takes no spaceIts flex: 1 has nothing to fillHost it in a full-screen parent or give it dimensions in style
The ad is clipped or sits in empty spaceA width or height in style overrides the format's sizeRemove the dimensions
Metro logs Codegen didn't run for BGInstreamAdViewThe view is registered through the interop layer, not Fabric codegenExpected in development; no action
Pangle never fills, log shows error 40060Your package isn't registered on Pangle's platformRegister the app with Pangle; this isn't a no-fill

Limitations

  • Android only: GenuinAd renders nothing on iOS, and the package has no web target.
  • Initialize before mounting: a slot mounted before initialize resolves throws natively.
  • No pause or resume: playback follows the Android Activity lifecycle, and a JavaScript-only router keeps the Activity resumed, so audio can continue after you navigate away. Unmounting the component stops it.
  • Fixed slot sizes: seven formats draw at their IAB dp size; only SIZE_FS_LANDSCAPE adapts, and only between 1.25:1 and 2.4:1.
  • Pinned native SDK: the package fixes com.begenuin at 0.v3.11; override with -PgenuinSdkVersion=<version> only on Genuin's advice, because older lines lack events the wrapper references.
  • App size: the four required 0.v3.11 artifacts total 6.0 MB of AARs before Media3 and the Google IMA SDK, which are shared if your app already uses them.

See also

TRUST AND COMPLIANCE

© 2026 Genuin Inc.

Genuin Footer