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 Native | Any 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. |
| Android | minSdk 24, compileSdk and targetSdk 36 |
| Kotlin | 2.0.21 or later. The Genuin AARs carry Kotlin 2.0 metadata, which Kotlin 1.9 can't read. |
| Android Gradle Plugin | 8.9.1 or later |
| Java | 17 |
| Core-library desugaring | Required by the Media3 IMA extension the SDK plays ads with |
| Native SDK | com.begenuin genuin-sdk, feed, core-v3, and instream-ads 0.v3.11, pulled in by the package |
| Credentials | A 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
npm install genuin-react-native-ads-v3 # or: yarn add genuin-react-native-ads-v3Autolinking 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
- Set the toolchain versions in
android/build.gradle. React Native libraries read them fromrootProject.ext, so your app decides what the module compiles with:
buildscript {
ext {
minSdkVersion = 24
compileSdkVersion = 36
targetSdkVersion = 36
kotlinVersion = "2.0.21"
}
}- 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 {
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.
- Keep
google()andmavenCentral()in your app's repositories; the React Native template declares both. The Genuin artifacts resolve from Maven Central. - Keep the
INTERNETpermission the template declares. The Genuin libraries don't declare it themselves, andACCESS_NETWORK_STATEarrives transitively. The SDK'sinstream-adsmodule merges incom.google.android.gms.permission.AD_IDto 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:
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;
}| Option | Type | Required | Default | Description |
|---|---|---|---|---|
| apiKey | string | Yes | — | 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. |
| fontFamily | string | No | SDK default | A font family for text the SDK renders |
| audioBehaviorConfig.duckVolumeDuringTransientCanDuck | number | No | 0.15 | Volume 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
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.
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
| tagId | string | Yes | — | The IAB tag for this placement. A blank value renders the placeholder and makes no request. |
| format | GenuinAdFormat | No | SIZE_300X250 | The slot size. GenuinAdFormat is exported as a value, so GenuinAdFormat.SIZE_320X50 autocompletes; the bare string "SIZE_320X50" also type-checks. |
| uniqueId | string | No | — | 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>. |
| geoLocation | GenuinGeoLocation | No | — | Geo targeting for this slot only, layered over setGeoLocation field by field. See Step 5. |
| onAdEvent | (event: GenuinAdEvent) => void | No | — | Lifecycle events. Analytics don't need it. See Step 6. |
| style | ViewStyle | No | — | 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:
<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
| format | Size (dp) | Use |
|---|---|---|
| SIZE_320X50 | 320 × 50 | Standard banner for headers and footers |
| SIZE_320X100 | 320 × 100 | Large banner |
| SIZE_300X250 | 300 × 250 | Medium rectangle, in-article or mid-feed |
| SIZE_320X480 | 320 × 480 | Half-page portrait, interstitial-style blocks |
| SIZE_300X600 | 300 × 600 | Half-page filmstrip |
| SIZE_300X250_COLLAPSIBLE | 300 × 250, collapsing to 300 × 50 | Sticky placements that shrink after the first view |
| SIZE_1280X720 | 1280 × 720 | Landscape video at a fixed size; wider than any phone in portrait |
| SIZE_FS_LANDSCAPE | Adapts to its parent | The same landscape surface sized to the screen |
Three formats need layout care:
SIZE_300X250_COLLAPSIBLEresizes its own box. React Native doesn't re-lay-out a native child that changes size, so the component listens for thecollapsedevent and switches betweenheightandcollapsedHeight. You still receive the event to resize a container around the slot. Settingheightinstyleopts out and pins the box.SIZE_1280X720is 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_LANDSCAPEgetsflex: 1instead of a dp box. In a full-screen parent it needs nostyle. In a parent that doesn't size its children, such as aScrollViewcontent 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.
import { setGeoLocation } from 'genuin-react-native-ads-v3';
setGeoLocation({
country: 'US',
region: 'NY',
city: 'New York',
dmaCode: '501',
latitude: 40.7128,
longitude: -74.006,
});| Field | Type | Macros resolved | Fallback |
|---|---|---|---|
| country | string | [COUNTRY_ID], [USER_GEO_COUNTRY], [APP_COUNTRY] | IP lookup |
| region | string | [REGION], [USER_GEO_REGION], [APP_REGION] | IP lookup |
| city | string | [CITY], [USER_GEO_CITY], [APP_LOC] | IP lookup |
| dmaCode | string | [DMA_CODE], [USER_GEO_DMA], [APP_METRO] | None. The DMA macros stay unresolved unless you set it. |
| latitude | number | [LOCATION_LAT], [USER_GEO_LAT], [APP_LAT] | IP lookup |
| longitude | number | [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.
<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.type | Payload | Fires when |
|---|---|---|
| loading | — | The feed began loading |
| loaded | count | The feed resolved with count renderable ads |
| noAds | — | The feed resolved but empty, or the request failed |
| appeared | index, total, adType | Ad index of total became active on screen |
| started | index, total, adType | Playback began |
| adCompleted | index, total, adType | This ad played to the end. Only a real playout reports it; an ad that never filled reports noFill instead. |
| changed | from, to | The pager moved between ads |
| loadTimeout | index, total, adType, timeoutMs | The 500 ms waterfall budget elapsed before this ad resolved. Informational: the waterfall continues and still reports started or noFill. |
| noFill | index, total, adType | The ad waterfall produced no fill |
| allAdsCompleted | total | Every 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. |
| collapsed | isCollapsed | The 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'.
- Turn on the network in
android/gradle.properties:
genuinEnablePangle=true
genuinEnableGmaAds=true- For Pangle, add ByteDance's repository inside
allprojects { repositories { ... } }inandroid/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:
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.
- For Google Mobile Ads, declare your application ID in
android/app/src/main/AndroidManifest.xml. Google's SDK resolves from thegoogle()repository you already have.
<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
| Symptom | Cause | Fix |
|---|---|---|
| Build fails with Kotlin metadata errors | kotlinVersion is below 2.0.21 | Set kotlinVersion = "2.0.21" or later in rootProject.ext (Step 2) |
| Build fails with Invoke-customs or java.time errors | Core-library desugaring is off | Enable it (Step 2) |
| genuinEnablePangle=true, but the ByteDance Maven repository is not declared | The app's allprojects repositories lack the Pangle repository | Add it (Step 7), or set the flag to false |
| Crash with IllegalStateException: SDK not initialized | A slot mounted before initialize resolved | Render slots only after the promise resolves (Step 3) |
| Every slot shows "No ads available" and logcat is silent | Logging is off; then the key, env, or tag is wrong | Call 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 works | R8 stripped Retrofit's generic signatures: ClassCastException: java.lang.Class cannot be cast to java.lang.reflect.ParameterizedType | The package ships the keep rules; check that your proguardFiles setting doesn't drop consumer rules |
| The ad overflows sideways | SIZE_1280X720 in a portrait container | Host it in a landscape or full-screen parent |
| SIZE_FS_LANDSCAPE takes no space | Its flex: 1 has nothing to fill | Host it in a full-screen parent or give it dimensions in style |
| The ad is clipped or sits in empty space | A width or height in style overrides the format's size | Remove the dimensions |
| Metro logs Codegen didn't run for BGInstreamAdView | The view is registered through the interop layer, not Fabric codegen | Expected in development; no action |
| Pangle never fills, log shows error 40060 | Your package isn't registered on Pangle's platform | Register the app with Pangle; this isn't a no-fill |
Limitations
- Android only:
GenuinAdrenders nothing on iOS, and the package has no web target. - Initialize before mounting: a slot mounted before
initializeresolves 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_LANDSCAPEadapts, and only between 1.25:1 and 2.4:1. - Pinned native SDK: the package fixes
com.begenuinat 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.

