npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

novvy-ads-react-native

v1.1.0

Published

React Native SDK for Novvy Ads — short-drama vertical-video ad formats (interstitial, rewarded, mid, end, scroll, pause) wrapping the native Novvy Android & iOS SDKs.

Downloads

985

Readme

novvy-ads-react-native

React Native SDK for Novvy Ads — short-drama vertical-video ad formats (interstitial, rewarded, mid, end, scroll, pause). It wraps the native Novvy Android & iOS SDKs behind a single TypeScript API, mirroring the design of the official Flutter plugin.

  • New Architecture only (TurboModule + Fabric). Requires React Native ≥ 0.76.
  • Framework-agnostic — plain RN native module, zero Expo runtime dependency.
  • Ships an optional Expo config plugin (build-time only) for Expo hosts.

Installation

npm install novvy-ads-react-native
# or
yarn add novvy-ads-react-native

iOS

cd ios && pod install
  • The podspec downloads NovvyAds.xcframework (currently pinned to 1.1.5) from the NovvyAds cocoapods release automatically — no manual binary integration.
  • Minimum deployment target iOS 13.0. Raise your Podfile's platform :ios to 13.0+ if it's lower.
  • No AdMob / Google-Mobile-Ads-SDK dependency (removed in 1.1.0).
  • The vendored framework ships arm64 simulator only (x86_64 simulator is excluded), so simulator builds require an Apple Silicon Mac. Intel Macs can only build for physical devices.
  • Local development against an unreleased NovvyAds iOS SDK: set NOVVY_ADS_LOCAL_XCFRAMEWORK=<path> before pod install and the podspec will skip the download and use that path instead.

Android

Autolinking wires the module up, but two host-side changes are required — without the first, your app will not compile at all.

  • The native SDK ai.novvy.android:sdk:1.1.6 is pulled from Maven Central. Make sure mavenCentral() is in your root build.gradle's allprojects.repositories (default in RN templates).
  • Minimum minSdkVersion 24 (Android 7.0). Raise android/build.gradle's minSdkVersion if it's lower.
  • Compiles with JDK 17; compileSdkVersion 34+ recommended.

1. Add -Xskip-metadata-version-check to your app module. In android/app/build.gradle, inside android { }:

kotlinOptions {
    freeCompilerArgs += ["-Xskip-metadata-version-check"]
}

The native SDK is built with Kotlin 2.2.20, so kotlin-stdlib resolves to 2.2.20 across the whole dependency graph, while React Native 0.76 pins the Kotlin Gradle plugin to 1.9.24 — and a 1.9 compiler reads class metadata only up to 2.0.0. Without the flag every Kotlin file in your app fails to compile, starting with the MainApplication.kt the RN template generates:

e: MainApplication.kt: Class 'kotlin.Unit' was compiled with an incompatible version of Kotlin.

This library carries the same flag, but that covers only the library's own compilation. Raising the Kotlin version is not an alternative: KGP ≥ 2.1 turns KotlinTopLevelExtension into an interface, which RN 0.76's Gradle plugin cannot load.

2. Use android:launchMode="singleTop" on MainActivity. The React Native template ships singleTask, and under it, going to the launcher and returning via the app icon clears the task down to MainActivity — taking any full-screen ad Activity with it. The ad vanishes and onAdDismissed never fires. singleTop (or no launchMode) avoids this.

Expo (optional)

For managed / CNG (prebuild) Expo apps, add the config plugin. It only runs at build time and adds no runtime dependency. Non-Expo hosts do not need it.

{
  "expo": {
    "plugins": [
      ["novvy-ads-react-native", {
        "iosDeploymentTarget": "13.0",
        "androidMinSdkVersion": 24,
        "userTrackingUsageDescription": "We use your data to show relevant ads."
      }]
    ]
  }
}

Then run npx expo prebuild. All options are optional:

| Option | Type | Default | Effect | | ------ | ------ | ------ | ------ | | iosDeploymentTarget | string | "13.0" | Raises Podfile.properties's ios.deploymentTarget to at least this value | | androidMinSdkVersion | number | 24 | Raises gradle.properties's android.minSdkVersion to at least this value | | userTrackingUsageDescription | string | — | If set, injects NSUserTrackingUsageDescription into Info.plist (iOS ATT copy) |

New Architecture must be enabled (default on RN 0.76+). For Expo, ensure newArchEnabled is true.


Quick start

import {
  NovvyAds,
  NovvyRewardedAd,
} from 'novvy-ads-react-native';

// 1. Initialize once at app startup.
const ok = await NovvyAds.initialize({
  appId: 'YOUR_APP_ID',
  endpoint: 'https://bid.novvy.ai',
  apiKey: 'YOUR_API_KEY',
  userContext: { userId: 'u_123', isPaidUser: false },
});

// 2. Set content context before requesting ads (required for drama fill).
NovvyAds.setContentContext({ seriesName: 'My Drama', episodeNumber: 3 });

// 3. Create an ad, wire callbacks, load & show.
const ad = new NovvyRewardedAd('YOUR_AD_UNIT_ID');
let earned = false;
ad.onAdLoaded = () => ad.show();
ad.onUserEarnedReward = () => { earned = true; };
ad.onAdDismissed = () => { if (earned) grantReward(); };
ad.onAdFailedToLoad = (e) => console.warn('load failed', e);
ad.load();

Ad formats

| Class | Kind | Lifecycle | | --- | --- | --- | | NovvyInterstitialAd | full-screen | load()onAdLoadedshow()onAdDismissed | | NovvyRewardedAd | rewarded video | load()show()onUserEarnedRewardonAdDismissed | | NovvyMidAd | full-screen (mid-roll) | load()show()onAdDismissed | | NovvyEndAd | full-screen (end-roll) | load()show()onAdDismissed | | NovvyScrollAd | inline feed video | load()<NovvyScrollAdView>setPlaying() | | NovvyPauseAd | realtime pause card (260×90) | <NovvyPauseAdView> (no load()) | | NovvyInsertAd | narrative insertion (STORY) | <NovvyInsertAdView>setActive(true) (no load()) |

There is no banner / native / app-open format, and no click callback. NovvyRewardedAd grants a parameterless reward (no amount/type payload).

Full-screen (interstitial / mid / end)

import { NovvyInterstitialAd } from 'novvy-ads-react-native';

const ad = new NovvyInterstitialAd('AD_UNIT_ID');
ad.onAdLoaded = () => ad.show();
ad.onAdShowed = () => {};
ad.onAdDismissed = () => {}; // ad auto-destroys after dismiss
ad.onAdFailedToLoad = (e) => {};
ad.load();

Scroll (inline feed) ad

import { NovvyScrollAd, NovvyScrollAdView } from 'novvy-ads-react-native';

const ad = useMemo(() => new NovvyScrollAd('AD_UNIT_ID', /* episodeNumber */ 3), []);

useEffect(() => {
  ad.onAdLoaded = () => ad.setPlaying(true);
  ad.onAdPlaybackTick = (remaining) => {/* update your skip UI */};
  ad.load();
  return () => ad.destroy();
}, [ad]);

// Give the view a fixed height in your feed item:
<View style={{ height: 640 }}>
  <NovvyScrollAdView ad={ad} durationSeconds={5} useDefaultSwipeOverlay />
</View>

// Toggle playback from viewport visibility:
// ad.setPlaying(true)  // scrolled into view
// ad.setPlaying(false) // scrolled out

Pause card ad

NovvyPauseAd has no load step — mounting <NovvyPauseAdView> issues the request. You must supply a NovvyPlayerAdapter that reports your video player's position; the SDK uses it to detect resume and hide the card.

import { NovvyPauseAd, NovvyPauseAdView, type NovvyPlayerAdapter } from 'novvy-ads-react-native';

// Adapter backed by your player's current state (read live each access):
const player: NovvyPlayerAdapter = {
  get currentPositionMs() { return videoRef.current?.positionMs ?? 0; },
  get isPlaying() { return videoRef.current?.isPlaying ?? false; },
};

const ad = useMemo(() => new NovvyPauseAd('AD_UNIT_ID'), []);
useEffect(() => {
  ad.onAdShowed = () => {};
  ad.onAdHidden = () => ad.destroy(); // resumed
  return () => ad.destroy();
}, [ad]);

// Position within your player overlay (fixed 260×90):
<NovvyPauseAdView ad={ad} player={player} />

Narrative insertion (STORY) ad

A self-driving serial chain of ads inside one episode, drawn inside your own player. The creative's first frame is made for a specific moment of the drama, so the hand-off reads as part of the episode rather than a cutaway.

There is no load step: attach binds the player without requesting anything, and the chain starts at setActive(true) — instances for off-screen episodes cost nothing. All policy (which episode is active, when a chain ends, whether an insertion point is stale) lives in the native SDK; the host feeds player state and executes the commands sent back.

import {
  NovvyInsertAd,
  NovvyInsertAdView,
  type NovvyInsertPlayerBinding,
} from 'novvy-ads-react-native';

// Pull model: the getters must answer with the latest value, so back them with
// refs rather than state.
const binding: NovvyInsertPlayerBinding = {
  get currentPositionMs() { return positionMs.current; },
  get isPlaying() { return playing.current; },
  get durationMs() { return durationMs.current; },   // 0 ⇒ no end-of-episode creative
  pause: () => videoRef.current?.pause(),            // must work while an ad shows
  play: () => videoRef.current?.resume(),
  seekTo: (ms) => videoRef.current?.seek(ms / 1000),
};

const ad = useMemo(() => new NovvyInsertAd('AD_UNIT_ID', episodeNumber), [episodeNumber]);
useEffect(() => {
  ad.onAdShowed = () => {};
  ad.onAdClosed = () => {};        // one ad closed; the chain continues
  ad.onChainFinished = () => {};   // terminal for this episode
  return () => ad.destroy();       // only the page unmount destroys it
}, [ad]);

useEffect(() => {
  if (isOnScreen) ad.setActive(true);   // never pair with false
}, [ad, isOnScreen]);

// Mount it with the player, filling the player's area:
<NovvyInsertAdView ad={ad} binding={binding} />

Five rules a host must follow — each is easy to get wrong and fails quietly:

  1. Do not loop the player (repeat={false}); a looping player never finishes, so the end-of-episode creative can never fire.
  2. Only ever setActive(true), on the episode that just came on screen. The claim stands the previous instance down by itself, and that is what keeps its ad alive for a swipe back. Never pair it with setActive(false).
  3. Mount the ad view with the player, not after it is ready. attach claims this episode's slot; gating it behind a spinner leaves the previous episode active for a whole video initialisation.
  4. Ad and view live and die together, and keep the neighbours (±1 page). If the view goes while the instance is still the active chain, a creative loading afterwards mounts into an off-screen container — invisible, no impression — while the SDK still pauses the player at the insertion point.
  5. Leave the top-right corner clear for the close pill, and disable your own drag/tap-to-pause while an ad is showing.

pause / play / seekTo must keep working while an ad is on screen: your controls still float above it, and the usual progress-bar gesture is pause-on-press, play-on-release. An un-undone release plays the episode's audio under the ad — the picture is covered, so sound is the only symptom.

Full runnable version: example/StoryDemoScreen.tsx. Full reference: docs/api.md §5.7.


Testing

Four layers, each covering what the ones below it cannot reach:

| Layer | Command | Covers | | --- | --- | --- | | JS unit (Jest) | npm test | The whole JS wrapper: event multiplexing, every placement's state machine, the timers, the views. The bulk of the suite. | | Android unit (Robolectric) | npm run test:android | The Insert bookkeeping — that a chain outlives its view, and that a late disposal cannot unmount from the new mount. | | iOS unit (XCTest) | npm run test:ios | The same cases as the Android ones, so the platforms cannot drift. The target comes from the podspec's test_spec; run pod install in example/ios first. | | E2E (Maestro) | npm run e2e:ios / npm run e2e:android | UI level only: the app launches, a full-screen ad opens and closes, an insert ad reaches its moment. See e2e/README.md. |

Which iOS simulator. test:ios, build:ios and e2e:ios all resolve their device through scripts/ios-simulator.sh, so the target is defined once rather than repeated at each call site. It defaults to iPhone 11 / iOS 17.2 and passes a UDID rather than a name — on a machine with several Xcode installs the same device name exists under several runtimes, and xcodebuild rejects an ambiguous destination outright. Override per run:

IOS_DEVICE_NAME="iPhone 15" IOS_RUNTIME=17.5 npm run test:ios

CI overrides exactly that pair, because the macos-14 runner has no iPhone 11 under any runtime — its pre-created set stops at the iPhone 15 family plus the SE. Nothing in gate C is device-dependent, so the two are interchangeable there.

Static checks:

npm run typecheck           # the library
npm run typecheck:example   # the example app (separate: the root tsconfig excludes it)
npm run lint

About the gate baselines. npm run lint reports 2 warnings and compileDebugKotlin reports 1 Kotlin warning; all three are pre-existing and are the recorded baseline. The point of writing them down is that any new warning is then a regression by definition, rather than something that has to be argued about — the Kotlin one in particular only reprints under --rerun-tasks, since an up-to-date build stays silent and would otherwise read as "zero warnings".


SDK API (NovvyAds)

| Method | Description | | --- | --- | | initialize(opts) | Promise<boolean> — init with appId / endpoint / apiKey / optional userContext. | | setUserContext(ctx) / updateUserContext(partial) | Replace / merge user targeting context. | | setContentContext(ctx) / updateContentContext(partial) | Replace / merge series/episode context. Call before requesting drama ads. | | helperHashEmail(email) | SHA-256 of the trimmed/lowercased email (pure JS). |

Privacy / consent

There is no explicit GDPR/CCPA consent API. Privacy targeting is expressed through NovvyUserContext / NovvyContentContext. On iOS, present the ATT prompt yourself (the config plugin can add NSUserTrackingUsageDescription).


How it works

A single native → JS event (onAdEvent, carrying adObjectId) multiplexes every ad's lifecycle; each ad object filters the stream by its own id, exactly like the Flutter plugin. adObjectIds are generated JS-side. Inline formats use Fabric view components that register a native container keyed by adObjectId.

License

MIT