genuin-react-native-ads-v3
v1.3.0
Published
Genuin Instream Ads (IAB) for React Native
Maintainers
Readme
Integrate Genuin Ads React Native SDK
genuin-react-native-ads-v3 puts a Genuin IAB ad slot on screen as a single React
component. You supply a tag ID; the package renders the SDK's real ad experience and reports the
full analytics funnel on its own.
Before you start
You need a brand API key and at least one IAB tag ID from the Genuin dashboard. The tag ID is the only per-slot input.
Step 1 — Install
npm install genuin-react-native-ads-v3
# or
yarn add genuin-react-native-ads-v3Step 2 — Android setup
Two things in your app's Gradle. The package brings the SDK artifacts with it at a pinned version.
Toolchain versions. React Native libraries read these from rootProject.ext, so your app
decides what this module compiles with. Setting them is not optional.
// android/build.gradle
buildscript {
ext {
minSdkVersion = 24
compileSdkVersion = 36
targetSdkVersion = 36
kotlinVersion = "2.0.21"
}
}Kotlin must be 2.0.21 or newer — the SDK is compiled with it and Kotlin 1.9.x cannot read 2.x
metadata. compileSdk must be 36. Java/JVM target must be 17. Android Gradle Plugin 8.9.1+.
Core-library desugaring. This is the step most likely to bite you, and the build error it
produces (Invoke-customs, or missing java.time) names nothing about Genuin.
// android/app/build.gradle
android {
compileOptions {
coreLibraryDesugaringEnabled true
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
}
dependencies {
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.5'
}React Native 0.83's template already enables this with exactly this artifact, so on a current scaffold it is usually already there — check before assuming. A bare app on an older React Native, or a hand-rolled Gradle setup, may not have it.
Permissions. Nothing to add. The SDK merges in what it needs, including INTERNET and
ACCESS_NETWORK_STATE. One item does need a decision from you: the ads engine brings
com.google.android.gms.permission.AD_ID, and an app shipping it must declare advertising-ID use
in the Play Console Data Safety form or the submission is rejected.
Step 3 — Initialize
initialize() must resolve before any slot mounts. Until it does, the native view throws
IllegalStateException("SDK not initialized…"), so gate on state rather than firing and forgetting.
import { Platform } from 'react-native';
import { initialize, isInitialized } from 'genuin-react-native-ads-v3';
if (Platform.OS === 'android') {
await initialize({ apiKey: 'YOUR_BRAND_API_KEY', env: 'PROD' });
}You can check initialisation of SDK via isInitialized().
Step 4 — Render a slot
import { GenuinAd, GenuinAdFormat } from 'genuin-react-native-ads-v3';
<GenuinAd tagId="YOUR_TAG_ID" format={GenuinAdFormat.SIZE_300X250} />;tagId is the only required prop. You do not size the slot — the component applies the
format's fixed dp size itself, because the native view lays its content out at that size regardless
of the box React Native gives it.
Example implementation
import { useEffect, useState } from 'react';
import { Platform } from 'react-native';
import {
initialize,
GenuinAd,
GenuinAdFormat,
type GenuinAdEvent,
} from 'genuin-react-native-ads-v3';
export default function ArticleScreen() {
const [ready, setReady] = useState(false);
useEffect(() => {
if (Platform.OS !== 'android') return;
initialize({ apiKey: 'YOUR_BRAND_API_KEY', env: 'QA' })
.then(() => setReady(true))
.catch(console.error);
}, []);
return (
<>
<ArticleBody />
{ready && (
<GenuinAd
tagId="YOUR_TAG_ID"
format={GenuinAdFormat.SIZE_300X250}
onAdEvent={(event: GenuinAdEvent) => console.log(event)}
/>
)}
</>
);
}The repo's example/ app mirrors the native SDK's storybook screen: one slot driven by Ad
Format and Ad Size dropdowns, with a live delegate log beneath it. The two landscape formats,
SIZE_1280X720 and SIZE_FS_LANDSCAPE, open as their own landscape page rather than inline —
neither fits a portrait phone. Run it with yarn example android.
Useful scripts while working on the package:
| Command | What it does |
| --- | --- |
| yarn example android | Build and run the example |
| yarn native:compile | Compile just the Kotlin — fastest feedback on native changes |
| yarn native:build | Build the library AAR |
| yarn example logcat | Stream the ad event log (GenuinAdEvent tag) |
| yarn typecheck / yarn test / yarn lint | The JS checks |
Key configuration parameters
initialize(options)
| Option | Type | Notes |
| --- | --- | --- |
| apiKey | string | Required. Brand API key from the dashboard. |
| env | 'PROD' \| 'QA' | Required. |
| fontFamily | string | Omit for the SDK default. |
| audioBehaviorConfig.duckVolumeDuringTransientCanDuck | number | 0..1, default 0.15. Out-of-range values fall back to the default rather than crashing init. |
| audioBehaviorConfig.focusLossHandling | 'MUTE_OUTPUT' \| 'PAUSE_PLAYBACK' | Default MUTE_OUTPUT. |
setGeoLocation(geoLocation)
| Field | Type | Notes |
| --- | --- | --- |
| country | string | Overrides the IP-derived country. |
| city | string | Overrides the IP-derived city. |
| region | string | Region or state. |
| dmaCode | string | Nielsen DMA / metro. No IP fallback — the only way to resolve the DMA macros. |
| latitude | number | Pair with longitude to report device-derived geo. |
| longitude | number | Pair with latitude to report device-derived geo. |
All optional, resolved field by field. Call it any time, as often as you like; no argument clears it. See Geo targeting.
setLoggingEnabled(enabled)
| Parameter | Type | Notes |
| --- | --- | --- |
| enabled | boolean | Turns the SDK's diagnostic log on or off. Off by default; errors are logged either way. |
Safe to call at any point, and independent of initialize() — call it first to catch SDK start-up.
See Diagnostics.
<GenuinAd />
| Prop | Type | Notes |
| --- | --- | --- |
| tagId | string | Required. IAB tag ID for the placement. |
| format | GenuinAdFormat | Defaults to SIZE_300X250. Exported as a value, so format={GenuinAdFormat.SIZE_320X50} gives you autocomplete; format="SIZE_320X50" also typechecks. |
| uniqueId | string | Required only when two slots share a tagId and format on one screen. |
| geoLocation | GenuinGeoLocation | Per-slot geo override, layered over setGeoLocation(...) field by field. Omit it unless this slot needs a geo different from the rest of the app. |
| onAdEvent | (event: GenuinAdEvent) => void | Lifecycle stream. Optional — analytics do not depend on it. |
| style | ViewStyle | Placement only: margins, alignSelf. Dimensions are applied for you — including the automatic resize on SIZE_300X250_COLLAPSIBLE — so a width/height here overrides them. |
Multiple slots on one screen need distinct uniqueIds, because the SDK keys its per-slot state on
iab_<tagId>_<format>_<uniqueId>:
<GenuinAd tagId={TAG} format={GenuinAdFormat.SIZE_320X50} uniqueId="header" />
<GenuinAd tagId={TAG} format={GenuinAdFormat.SIZE_320X50} uniqueId="footer" />Geo targeting
Ad urls carry geo macros the ad server expands to target a request: [DMA_CODE], [USER_GEO_CITY],
[APP_COUNTRY] and so on. The SDK resolves most of them itself from the caller's IP, so out of the
box you need do nothing. Supply geo yourself when you know better than the IP does — a user who
picked a market manually, a VPN'd connection, a station the app is scoped to — or when you need a
DMA code, which is the one field the IP lookup cannot provide.
Set it once, app-wide:
import { setGeoLocation } from 'genuin-react-native-ads-v3';
setGeoLocation({
country: 'US',
region: 'NY',
city: 'New York',
dmaCode: '501',
latitude: 40.7128,
longitude: -74.006,
});Every field is optional and resolved independently: set only dmaCode and the rest still comes from
the IP lookup. setGeoLocation() with no argument clears it.
Timing is forgiving on purpose. It does not need initialize() to have run, and it is not
snapshotted when a slot mounts — each ad request reads whatever is current at the moment it builds
its url. So the usual shape, where location arrives asynchronously well after the first slot is on
screen, needs no coordination:
useEffect(() => {
Geolocation.getCurrentPosition(({ coords }) =>
setGeoLocation({ latitude: coords.latitude, longitude: coords.longitude })
);
}, []);Requests already in flight keep the geo they were built with — no ad is re-requested to pick up a newer location.
Per-slot override
Most apps only need the app-wide call. When one screen needs a different geo from the rest of the
app — a market picker, a preview of another region — pass geoLocation to that slot. It overrides
the app-wide value field by field, so a global DMA plus per-slot coordinates yields both:
<GenuinAd tagId={TAG} geoLocation={{ city: 'Chicago', dmaCode: '602' }} />Precedence, highest first: the slot's geoLocation prop → setGeoLocation(...) → the SDK's
IP-derived lookup.
Field to macro mapping
| Field | Type | Macro tokens |
| --- | --- | --- |
| country | string | [COUNTRY_ID], [USER_GEO_COUNTRY], [APP_COUNTRY] |
| city | string | [CITY], [USER_GEO_CITY], [APP_LOC] |
| region | string | [REGION], [USER_GEO_REGION], [APP_REGION] |
| dmaCode | string | [DMA_CODE], [USER_GEO_DMA], [APP_METRO] |
| latitude | number | [LOCATION_LAT], [USER_GEO_LAT], [APP_LAT] |
| longitude | number | [LOCATION_LON], [USER_GEO_LON], [APP_LONG] |
Two asymmetries are worth knowing. dmaCode has no IP-derived fallback, so those three tokens are
left unresolved unless you set it. And [IP], [ZIP_CODE] and [USER_GEO_ZIP] come only from the
IP lookup — there is no field here to set them.
Passing both latitude and longitude also reports the geo type ([USER_GEO_TYPE] /
[DEVICE_GEO_TYPE]) as device location services (1) instead of IP-derived (2). One coordinate
without the other is ignored for that purpose.
Every other macro — device, carrier, app, session, player size — is built by the SDK and has no host override.
Diagnostics
The SDK keeps its own diagnostic log — SDK start-up, the ad-network registry, the per-slot request
and its outcome. It is off by default, and the default is not about how you built your app: the
logger gates on the SDK library's build type, which is release in every published artifact, so
these lines were invisible to a React Native app however it was built. setLoggingEnabled is the
switch.
import { setLoggingEnabled, initialize } from 'genuin-react-native-ads-v3';
if (__DEV__) setLoggingEnabled(true);
await initialize({ apiKey: 'YOUR_BRAND_API_KEY', env: 'PROD' });Call it before initialize() to catch start-up, then read the log:
adb logcat -s GenuinSDKTwo tags, two different things. GenuinSDK is the SDK's own log, above. GenuinAdEvent is this
wrapper's per-slot event log — the same events onAdEvent delivers, mirrored to logcat so you can
watch a slot without wiring a handler (yarn example logcat follows it). Errors are logged on both
regardless of this setting.
Reach for this when a slot is not filling. An unfilled request looks the same from the outside whether the cause is no inventory, a missing credential, an unsupported device ABI or a misconfigured placement — the ordered trail of log lines is what tells them apart.
Gate it on __DEV__ rather than shipping it enabled: the log is verbose and names your ad tags.
Pangle ads
Genuin can fill ad slots from Pangle (ByteDance) as well as its own VAST inventory. This is
opt-in, off by default, and needs no code: there is no PangleAd component, no init call and no
prop. <GenuinAd> is unchanged — slots the server books against an external network simply start
filling, and report through the same onAdEvent stream.
Turning it on is two lines, and the second is the one that gets missed.
1. Enable the dependency in your app's android/gradle.properties:
genuinEnablePangle=true2. Add ByteDance's Maven repository to your app's android/build.gradle, inside
allprojects { repositories { … } }:
maven {
url 'https://artifact.bytedance.com/repository/pangle/'
content { includeGroupByRegex 'com\.pangle.*' }
}Step 2 cannot be done for you. Pangle is not published to Maven Central, and a Gradle POM cannot
contribute a repository — repositories are resolution configuration, not dependency metadata — so
pangle-ads declaring pag-sdk is not enough to make it resolvable. Miss it and the build fails
with a pointed error naming this section, not a bare "could not find com.pangle.global:pag-sdk".
Turning it back off is equally complete: set the flag to false and every Pangle candidate is
skipped, no error, no blank slot, the feed carries on.
If your app uses Expo prebuild, remember that android/ is generated: expo prebuild --clean
discards the repository declaration along with everything else you added there. Put it in a config
plugin, or re-apply it after a clean prebuild — the same caveat that applies to any manual edit
under android/.
Before you ship it
Three things the wrapper cannot handle for you.
Emulators lie about this feature. Pangle ships native libraries for arm64-v8a and
armeabi-v7a only. On a standard x86_64 emulator there are no native libraries at all, and the
result is indistinguishable from a no-fill — no crash, no error, just an empty slot. Test on a
physical device or an arm64 system image. If your app sets ndk.abiFilters, keep arm64-v8a in
it.
Your Play Data Safety declaration grows. Pangle collects device and advertising identifiers and
pulls in the TikTok App Events SDK transitively, and its manifest contributes
com.google.android.gms.permission.AD_ID through manifest merge. The declaration is per app
listing, so it is yours to update. To strip the ad-id permission (and accept lower fill):
<uses-permission android:name="com.google.android.gms.permission.AD_ID" tools:node="remove" />Consent is not wired up yet. The SDK currently requests personalised ads unconditionally, because the alternative suppresses fill entirely rather than just personalisation, and there is no host-facing consent API yet. If you operate under GDPR or a similar regime, treat this as a gap and talk to Genuin before shipping. The SDK never raises a consent prompt of its own — whether and when to ask is your app's call.
Which formats can serve Pangle
Which Pangle format a slot asks for is derived from the slot's own dp size, not from anything you pass. The SDK matches the size exactly against the two banner sizes Pangle defines, then falls back to native:
| format | Asks Pangle for |
| --- | --- |
| SIZE_320X50 | banner 320×50, then native |
| SIZE_300X250 | banner 300×250, then native |
| SIZE_300X250_COLLAPSIBLE | banner 300×250, then native |
| every other format | native only |
So only three of the eight formats can serve a Pangle banner. The rest depend on the server's
candidate carrying a native_slot_id; without one they go dry and report noFill. An inline slot
never asks for interstitial or rewarded — a small card seizing the whole screen because it scrolled
past is not a trade the SDK makes. Those two are reachable only from the full-screen surface.
Checking it works
Pangle slots report through the ordinary event stream, with adType: 'external' on the slot that
led with an external network.
The example app has a Pangle entry in its "Ad Format" dropdown, pointing at a QA tag booked with a Pangle candidate. Pair it with the 320×50 size — that is the combination verified to fill on device. Which kind of demand a tag serves is a server-side booking, so a tag cannot be made to serve Pangle from the client. To confirm the module is actually on the classpath, turn on the SDK log (see Diagnostics) and look for the registry line:
BGAdNetworkRegistry: ad networks available = [pangle]An empty list means the dependency is not resolving into your APK — check the flag and the
repository. If the list is right but nothing fills, the usual cause on a first integration is that
the host package is not yet registered on Pangle's dashboard; that surfaces as error 40060 in the
log and is not a no-fill.
Ad formats
| format | Size (dp) | Typical use |
| --- | --- | --- |
| SIZE_320X50 | 320 × 50 | Standard banner — headers, footers |
| SIZE_320X100 | 320 × 100 | Large banner |
| SIZE_300X250 | 300 × 250 | Medium rectangle — in-article, 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 first view |
| SIZE_1280X720 | 1280 × 720 | Landscape video at a fixed size |
| SIZE_FS_LANDSCAPE | adapts to its container | The same landscape surface, full-screen: it sizes itself to the display |
Lifecycle events
onAdEvent receives a discriminated union. Ad positions are 1-based.
Events carrying an adType report what the slot led with: 'audio' (a VAST audio waterfall —
the ordinary instream ad), 'external' (an external ad network such as Pangle), 'sponsored',
'video', or 'unknown'. It is typed as an open string, not a union, because the SDK adds labels
as it grows — 'external' arrived in 0.v3.10 — and a closed union would break every exhaustive
switch each time. A slot whose leading step no-fills and falls through to the other kind keeps
reporting the leading label.
| event.type | Payload | Meaning |
| --- | --- | --- |
| loading | — | Feed began loading |
| loaded | count | Feed resolved with renderable ads |
| noAds | — | Feed resolved but empty |
| appeared | index, total, adType | Ad became active on-screen |
| started | index, total, adType | Playback began |
| adCompleted | index, total, adType | This ad played through to the end. Only a real playout reports it — an ad that never filled reports noFill instead. Per-ad; the slot-level counterpart is allAdsCompleted. |
| changed | from, to | Pager moved between ads |
| loadTimeout | index, total, adType, timeoutMs | The waterfall budget elapsed before this ad resolved. Informational — the waterfall is not cancelled and still reports started or noFill afterwards. |
| noFill | index, total, adType | Ad waterfall produced no fill |
| allAdsCompleted | total | Every ad in the slot is done and none is pending. Slot-level rather than per-ad — the cue to collapse the slot or resume your own content. Fires once per completed pass, so a slot that resolves a fresh ad reports it again. |
| collapsed | isCollapsed | The collapsible slot was collapsed or expanded; isCollapsed is the resulting state. Never fires for the fixed sizes. |
These eleven are the complete set.
Specs and limitations
On web, importing the package throws, because the module resolves a native module at import time.
Initialize before mounting. A slot mounted before initialize() resolves throws natively.
The collapsible format resizes itself. React Native does not re-lay-out a native child that
changes its own size, so a collapsed ad would otherwise leave 200 dp of dead space. GenuinAd
listens for the collapsed event and switches its box between the format's height and its
collapsedHeight — nothing to wire up. You still receive the event, so you can resize a container
around the slot if your layout needs it. Setting height in style opts out and pins the box.
SIZE_1280X720 is wider than any phone. It is declared at its full IAB width, and the component
sizes the box to 1280 dp as it does for every fixed format, so on a narrow host the slot overflows
horizontally. Give it a landscape or full-screen container — the example app opens it as its own
page and locks the device to landscape. The native view clamps its own content width, so the ad
still renders; it is the React Native box that overflows.
SIZE_FS_LANDSCAPE is the full-screen format. It renders the same landscape surface as
SIZE_1280X720, but instead of a fixed dp size it adapts to the screen and is built to cover it.
The component gives it flex: 1 rather than a dp box, so in a full-screen parent it needs no
style at all — the example app opens it as a landscape page and lets it take the whole stage. In
a parent that does not size its children (a ScrollView content container, an auto-height View)
flex: 1 resolves to nothing and the slot renders 0 × 0; give it dimensions there —
{ width: '100%', aspectRatio: 16 / 9 } or explicit values.
The ratio it ends up with is honoured only between 1.25:1 and 2.4:1. The design is a centred portrait video panel flanked by two card columns, and outside that band the columns collapse or stretch past readability, so a box outside it is letter- or pillar-boxed down to the nearest allowed ratio — the slot may render smaller than the box you gave it rather than distorting. A box too narrow for the side cards falls back to the same full-bleed surface the other immersive sizes use.
FAQs
No ads in a release build, but debug works. Look for
ClassCastException: java.lang.Class cannot be cast to java.lang.reflect.ParameterizedType in
logcat. That is R8 stripping the generic signatures Retrofit reflects on, and it surfaces as
"Ad feed request returned no data" with a single-digit-millisecond latency — an empty ad response,
not a client error. This package ships the keep rules that prevent it, so if you hit it, something
is excluding them: check that you have not overridden proguardFiles in a way that drops consumer
rules.
Every slot shows "no ads available" and logcat is silent. Turn the SDK's own log on first —
setLoggingEnabled(true) before initialize(), then adb logcat -s GenuinSDK. Silence up to now
is expected rather than diagnostic: the SDK logs nothing below error level until you ask it to (see
Diagnostics). With the log on, check that initialize() resolved before the slot
mounted, that the apiKey matches the account, that env is right, and that the tagId is
non-blank and active in the dashboard.
The ad overflows sideways. You are on SIZE_1280X720 in a portrait container. It needs a
landscape or full-screen host; see Specs and limitations.
SIZE_FS_LANDSCAPE takes up no space. Its flex: 1 has nothing to fill, because the parent
does not size its children — a ScrollView content container or an auto-height View. Either host
it in a full-screen parent, or give it dimensions in style.
The ad is clipped, or sits in a block of empty space. A width or height in your style is
overriding the format's own size. Drop the dimensions. The exception is the collapsible format,
where the box stays at full height until you resize it from the collapsed event.
The build fails with Invoke-customs or java.time errors. Core-library desugaring is off; see
step 2.
Metro logs Codegen didn't run for BGInstreamAdView. Expected and harmless in development.
Kotlin metadata errors when building. Your app's kotlinVersion is below 2.0.21.
