Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/levelplay-ump-readme-link.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@react-native-ads/levelplay": patch
---

README: point to [`@react-native-ads/ump`](https://www.npmjs.com/package/@react-native-ads/ump) for consent instead of `react-native-google-mobile-ads` (#6).
5 changes: 5 additions & 0 deletions .changeset/liftoff-ump-readme-link.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@react-native-ads/liftoff": patch
---

README: point to [`@react-native-ads/ump`](https://www.npmjs.com/package/@react-native-ads/ump) for consent instead of `react-native-google-mobile-ads` (#6).
5 changes: 5 additions & 0 deletions .changeset/pangle-ump-readme-link.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@react-native-ads/pangle": patch
---

README: point to [`@react-native-ads/ump`](https://www.npmjs.com/package/@react-native-ads/ump) for consent instead of `react-native-google-mobile-ads` (#6).
5 changes: 5 additions & 0 deletions .changeset/ump-initial-release.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@react-native-ads/ump": patch
---

Add `@react-native-ads/ump`: a Nitro-module wrapper around Google's User Messaging Platform SDK for GDPR/CCPA consent — `requestConsentInfoUpdate`, `loadAndShowConsentFormIfRequired`, `showPrivacyOptionsForm`, `showForm`, `gatherConsent`, plus IAB TCF helpers (`getTCString`, `getGdprApplies`, `getPurposeConsents`, `getPurposeLegitimateInterests`, `getTCModel`, `getUserChoices`) — mirroring `react-native-google-mobile-ads`'s `AdsConsent` API (#6).
5 changes: 5 additions & 0 deletions .changeset/unity-ump-readme-link.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@react-native-ads/unity": patch
---

README: point to [`@react-native-ads/ump`](https://www.npmjs.com/package/@react-native-ads/ump) for consent instead of `react-native-google-mobile-ads` (#6).
4 changes: 3 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,14 @@ jobs:
- 'packages/liftoff/**'
levelplay:
- 'packages/levelplay/**'
ump:
- 'packages/ump/**'

- name: Build affected-packages matrix
id: list
run: |
packages=$(node -e "
const filters = { unity: '${{ steps.filter.outputs.unity }}', pangle: '${{ steps.filter.outputs.pangle }}', liftoff: '${{ steps.filter.outputs.liftoff }}', levelplay: '${{ steps.filter.outputs.levelplay }}' };
const filters = { unity: '${{ steps.filter.outputs.unity }}', pangle: '${{ steps.filter.outputs.pangle }}', liftoff: '${{ steps.filter.outputs.liftoff }}', levelplay: '${{ steps.filter.outputs.levelplay }}', ump: '${{ steps.filter.outputs.ump }}' };
console.log(JSON.stringify(Object.keys(filters).filter((k) => filters[k] === 'true')));
")
echo "packages=$packages" >> "$GITHUB_OUTPUT"
Expand Down
1 change: 1 addition & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ This is a monorepo managed using [Yarn workspaces](https://yarnpkg.com/features/
- `packages/pangle` — `@react-native-ads/pangle`
- `packages/liftoff` — `@react-native-ads/liftoff`
- `packages/levelplay` — `@react-native-ads/levelplay`
- `packages/ump` — `@react-native-ads/ump` (Google UMP consent SDK, not an ad network)

Each package has its own `example/` app you use to test changes to that package.

Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
| [`packages/pangle`](packages/pangle) | Pangle | [`@react-native-ads/pangle`](https://www.npmjs.com/package/@react-native-ads/pangle) |
| [`packages/liftoff`](packages/liftoff) | Liftoff Monetize (Vungle) | [`@react-native-ads/liftoff`](https://www.npmjs.com/package/@react-native-ads/liftoff) |
| [`packages/levelplay`](packages/levelplay) | LevelPlay (ironSource) mediation | [`@react-native-ads/levelplay`](https://www.npmjs.com/package/@react-native-ads/levelplay) |
| [`packages/ump`](packages/ump) | Google UMP (consent) | [`@react-native-ads/ump`](https://www.npmjs.com/package/@react-native-ads/ump) |

Each package is independent — install only the ones you need. See each package's own README for install instructions and API usage.

Expand All @@ -17,6 +18,7 @@ packages/
pangle/ @react-native-ads/pangle + its own example app
liftoff/ @react-native-ads/liftoff + its own example app
levelplay/ @react-native-ads/levelplay + its own example app
ump/ @react-native-ads/ump + its own example app
apps/
expo-example/ Expo (dev-client) app demonstrating all four packages together
rn-cli-example/ Bare React Native CLI app demonstrating all four packages together
Expand Down
8 changes: 3 additions & 5 deletions packages/levelplay/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,11 +101,9 @@ if (rewardResult.state === 'completed') {
- `result.state === 'completed'` is also how you know a rewarded ad earned
its reward; LevelPlay doesn't send a distinct "reward earned" signal.
- Call `load()` again after each `show()` to prepare the next impression.
- No built-in consent UI (no UMP equivalent) — plug your own and forward the
result via `setGDPRConsent`/`setCCPAConsent`/`setCOPPA`. If your app already
uses [`react-native-google-mobile-ads`](https://docs.page/invertase/react-native-google-mobile-ads),
its `AdsConsent` module (wraps Google's UMP SDK) is a good source for these
booleans — no need to build a separate consent flow just for this package.
- No built-in consent UI — gather consent with
[`@react-native-ads/ump`](../ump) (or another CMP) and forward the result
via `setGDPRConsent`/`setCCPAConsent`/`setCOPPA`.

See [`src/specs/LevelPlayAds.nitro.ts`](src/specs/LevelPlayAds.nitro.ts) for
the full native interface and [`example`](example) for a runnable demo.
Expand Down
8 changes: 3 additions & 5 deletions packages/liftoff/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,11 +111,9 @@ rewarded.load();
payloads are always `undefined` — treat them as signals only.
- Use `RewardedAdEventType.LOADED`, not `AdEventType.LOADED`, on a
`RewardedAd`.
- No built-in consent UI (no UMP equivalent) — plug your own and forward the
result via `setGDPRConsent`/`setCCPAConsent`/`setCOPPA`. If your app already
uses [`react-native-google-mobile-ads`](https://docs.page/invertase/react-native-google-mobile-ads),
its `AdsConsent` module (wraps Google's UMP SDK) is a good source for these
booleans — no need to build a separate consent flow just for this package.
- No built-in consent UI — gather consent with
[`@react-native-ads/ump`](../ump) (or another CMP) and forward the result
via `setGDPRConsent`/`setCCPAConsent`/`setCOPPA`.

See [`src/specs/LiftoffAds.nitro.ts`](src/specs/LiftoffAds.nitro.ts) for the
full native interface and [`example`](example) for a runnable demo.
Expand Down
8 changes: 3 additions & 5 deletions packages/pangle/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,11 +132,9 @@ rewarded.load({mediaExtra: 'your-user-id'}); // optional, for SSV
payloads are always `undefined` — treat them as signals only.
- Use `RewardedAdEventType.LOADED`, not `AdEventType.LOADED`, on a
`RewardedAd`.
- No built-in consent UI (no UMP equivalent) — plug your own and forward the
result via `setGDPRConsent`/`setCCPAConsent`/`setCOPPA`. If your app already
uses [`react-native-google-mobile-ads`](https://docs.page/invertase/react-native-google-mobile-ads),
its `AdsConsent` module (wraps Google's UMP SDK) is a good source for these
booleans — no need to build a separate consent flow just for this package.
- No built-in consent UI — gather consent with
[`@react-native-ads/ump`](../ump) (or another CMP) and forward the result
via `setGDPRConsent`/`setCCPAConsent`/`setCOPPA`.

See [`src/specs/PangleAds.nitro.ts`](src/specs/PangleAds.nitro.ts) for the
full native interface and [`example`](example) for a runnable demo.
Expand Down
86 changes: 86 additions & 0 deletions packages/ump/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# OSX
#
.DS_Store

# XDE
.expo/

# VSCode
.vscode/
jsconfig.json

# Xcode
#
build/
*.pbxuser
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3
xcuserdata
*.xccheckout
*.moved-aside
DerivedData
*.hmap
*.ipa
*.xcuserstate
project.xcworkspace
**/.xcode.env.local

# Android/IJ
#
.classpath
.cxx
.gradle
.idea
.project
.settings
local.properties
android.iml

# Cocoapods
#
example/ios/Pods

# Ruby
example/vendor/

# node.js
#
node_modules/
npm-debug.log
yarn-debug.log
yarn-error.log

# BUCK
buck-out/
\.buckd/
android/app/libs
android/keystores/debug.keystore

# Yarn
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions

# Expo
.expo/

# Turborepo
.turbo/

# generated by bob
lib/

# React Native Codegen
ios/generated
android/generated

# React Native Nitro Modules
nitrogen/
26 changes: 26 additions & 0 deletions packages/ump/NitroUMPAds.podspec
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
require "json"

package = JSON.parse(File.read(File.join(__dir__, "package.json")))

Pod::Spec.new do |s|
s.name = "NitroUMPAds"
s.version = package["version"]
s.summary = package["description"]
s.homepage = "https://github.com/exzos28/react-native-ads/tree/main/packages/ump"
s.license = { :type => "MIT" }
s.authors = { "exzos" => "oleksandr.kurinnyi.work@gmail.com" }
s.platforms = { :ios => "15.1" }
s.source = { :path => "." }

s.source_files = ["ios/**/*.{swift,m,mm}"]

load "nitrogen/generated/ios/NitroUMPAds+autolinking.rb"
add_nitrogen_files(s)

s.dependency "React-Core"
s.dependency "React-jsi"
s.dependency "React-callinvoker"
s.dependency "GoogleUserMessagingPlatform", "3.1.0"

install_modules_dependencies(s)
end
151 changes: 151 additions & 0 deletions packages/ump/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
# @react-native-ads/ump

Google User Messaging Platform (UMP) — GDPR/CCPA consent for React Native.

[![npm version](https://img.shields.io/npm/v/@react-native-ads/ump.svg)](https://www.npmjs.com/package/@react-native-ads/ump)
[![license](https://img.shields.io/npm/l/@react-native-ads/ump.svg)](../../LICENSE)

## Features

- 🔐 Google UMP — GDPR/CCPA consent gathering and the privacy options form
- ⚡️ [Nitro Modules](https://nitro.margelo.com/) — direct JSI bindings, no bridge overhead
- 📱 iOS & Android
- 🧩 Works with Expo out of the box — no config plugin needed
- 🔒 Fully typed API

## Installation

### React Native

```sh
npm install @react-native-ads/ump react-native-nitro-modules
cd ios && pod install
```

### Expo

```sh
npx expo install @react-native-ads/ump react-native-nitro-modules
npx expo prebuild
```

### Required native setup

The UMP SDK refuses to run without a Google-issued AdMob App ID, even if you
don't use AdMob/Google Mobile Ads — omitting this throws
`The UMP SDK requires a valid application ID`.

Android — add to `android/app/src/main/AndroidManifest.xml`:

```xml
<meta-data
android:name="com.google.android.gms.ads.APPLICATION_ID"
android:value="YOUR_ADMOB_APP_ID" />
```

iOS — add to `Info.plist`:

```xml
<key>GADApplicationIdentifier</key>
<string>YOUR_ADMOB_APP_ID</string>
```

Don't have an AdMob app? Create one for free in the
[AdMob console](https://apps.admob.com/) to get an App ID, or use Google's
public test IDs while developing (`ca-app-pub-3940256099942544~3347511713`
on Android, `ca-app-pub-3940256099942544~1458002511` on iOS — see the
[`example`](example) app).

## API

### `UMPAds()`

| Method | Description |
| --- | --- |
| `requestConsentInfoUpdate(options?: UMPConsentRequestOptions): Promise<UMPConsentInfo>` | Requests/refreshes consent info from Google's servers. Call on every app launch. |
| `loadAndShowConsentFormIfRequired(): Promise<UMPConsentInfo>` | Loads and, if required, presents the consent form. No-ops if none is required. |
| `showForm(): Promise<UMPConsentInfo>` | Unconditionally loads and presents the consent form, regardless of `status`. |
| `showPrivacyOptionsForm(): Promise<UMPConsentInfo>` | Presents the "privacy options" form so the user can revisit their choice later. |
| `gatherConsent(options?: UMPConsentRequestOptions): Promise<UMPConsentInfo>` | Convenience helper: `requestConsentInfoUpdate` + `loadAndShowConsentFormIfRequired` in one call. |
| `getConsentInfo(): UMPConsentInfo` | Synchronously reads the last known consent info. |
| `reset(): void` | Clears all consent state. Testing only. |
| `getTCString(): string` | The raw [IAB TCF v2](https://github.com/InteractiveAdvertisingBureau/GDPR-Transparency-and-Consent-Framework) consent string, or `''` if none has been written yet. |
| `getTCModel(): TCModel` | Parses `getTCString()` into a full [`TCModel`](https://www.npmjs.com/package/@iabtcf/core). |
| `getGdprApplies(): boolean` | Whether GDPR applies to the current user. |
| `getPurposeConsents(): string` | Raw per-purpose consent bitfield (`'0'`/`'1'` per TCF purpose ID). |
| `getPurposeLegitimateInterests(): string` | Raw per-purpose legitimate-interest bitfield, same shape as `getPurposeConsents()`. |
| `getUserChoices(): UMPUserChoices` | A named, decoded view of the 10 TCF purposes + 2 special features (e.g. `selectPersonalisedAds`, `usePreciseGeolocationData`). |

### `UMPConsentRequestOptions`

| Field | Description |
| --- | --- |
| `tagForUnderAgeOfConsent?: boolean` | If `true`, the SDK skips consent gathering entirely (COPPA/child users). |
| `debugGeography?: 'disabled' \| 'EEA' \| 'notEEA'` | Simulates a region for testing. Has no effect in production builds. |
| `testDeviceIds?: string[]` | Device IDs allowed to use `debugGeography`. Required by the SDK for the debug geography to take effect on a real device. |

### `UMPConsentInfo`

| Field | Description |
| --- | --- |
| `status: 'unknown' \| 'required' \| 'notRequired' \| 'obtained'` | The current consent status. |
| `isConsentFormAvailable: boolean` | Whether a consent form is currently available to load/show. |
| `privacyOptionsRequirementStatus: 'unknown' \| 'required' \| 'notRequired'` | Whether the "privacy options" entry point is required in your UI. |
| `canRequestAds: boolean` | Whether the app is currently allowed to request ads. |

## Usage

```ts
import UMPAds from '@react-native-ads/ump';

// requestConsentInfoUpdate() + loadAndShowConsentFormIfRequired() in one call
const info = await UMPAds().gatherConsent();

if (info.canRequestAds) {
// safe to initialize your ad SDK(s) now
}

// "Manage consent" / "Privacy options" button in your Settings screen:
if (info.privacyOptionsRequirementStatus === 'required') {
await UMPAds().showPrivacyOptionsForm();
}

// Inspect the user's actual TCF choices, e.g. for analytics gating:
const { selectPersonalisedAds } = UMPAds().getUserChoices();
```

`gatherConsent()` is equivalent to:

```ts
await UMPAds().requestConsentInfoUpdate();
const info = await UMPAds().loadAndShowConsentFormIfRequired();
```

## Notes

- Call `requestConsentInfoUpdate()` on every app launch — Google's servers
decide whether the user still needs to see a form.
- `canRequestAds` is the source of truth for whether it's safe to initialize
ad SDKs (e.g. [`@react-native-ads/unity`](../unity),
[`@react-native-ads/pangle`](../pangle)) — it can be `true` even before a
form is shown (e.g. outside GDPR/CCPA-regulated regions).
- This package only gathers consent — it doesn't forward the result to any
ad network. Read `canRequestAds`/`status` yourself and call the relevant
`setGDPRConsent`/`setCCPAConsent` on each ad package you use.
- `debugGeography`/`testDeviceIds` only take effect on devices/simulators
registered as test devices with Google — never in production.
- `getTCString`/`getPurposeConsents`/`getPurposeLegitimateInterests` read the
standard `IABTCF_*` keys the UMP SDK writes to `UserDefaults`/
`SharedPreferences` as an IAB-certified CMP — they reflect the last
`requestConsentInfoUpdate()`/form interaction, not live state.
- `getTCModel()`/`getUserChoices()` decode that string via
[`@iabtcf/core`](https://www.npmjs.com/package/@iabtcf/core) (a peer-free
dependency of this package) and fall back to an empty/all-`false` model if
no string has been written yet or it fails to decode.

See [`src/specs/UMPAds.nitro.ts`](src/specs/UMPAds.nitro.ts) for the full
native interface and [`example`](example) for a runnable demo.

## License

MIT
Loading
Loading