diff --git a/.changeset/levelplay-ump-readme-link.md b/.changeset/levelplay-ump-readme-link.md
new file mode 100644
index 0000000..75b5007
--- /dev/null
+++ b/.changeset/levelplay-ump-readme-link.md
@@ -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).
diff --git a/.changeset/liftoff-ump-readme-link.md b/.changeset/liftoff-ump-readme-link.md
new file mode 100644
index 0000000..50766a5
--- /dev/null
+++ b/.changeset/liftoff-ump-readme-link.md
@@ -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).
diff --git a/.changeset/pangle-ump-readme-link.md b/.changeset/pangle-ump-readme-link.md
new file mode 100644
index 0000000..58505cb
--- /dev/null
+++ b/.changeset/pangle-ump-readme-link.md
@@ -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).
diff --git a/.changeset/ump-initial-release.md b/.changeset/ump-initial-release.md
new file mode 100644
index 0000000..50adac2
--- /dev/null
+++ b/.changeset/ump-initial-release.md
@@ -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).
diff --git a/.changeset/unity-ump-readme-link.md b/.changeset/unity-ump-readme-link.md
new file mode 100644
index 0000000..f550912
--- /dev/null
+++ b/.changeset/unity-ump-readme-link.md
@@ -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).
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 89afb13..cc360ea 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -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"
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index f3309ab..3342e96 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -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.
diff --git a/README.md b/README.md
index 6f2e10c..a8c441a 100644
--- a/README.md
+++ b/README.md
@@ -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.
@@ -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
diff --git a/packages/levelplay/README.md b/packages/levelplay/README.md
index d31c53b..871268b 100644
--- a/packages/levelplay/README.md
+++ b/packages/levelplay/README.md
@@ -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.
diff --git a/packages/liftoff/README.md b/packages/liftoff/README.md
index 797e868..87eec53 100644
--- a/packages/liftoff/README.md
+++ b/packages/liftoff/README.md
@@ -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.
diff --git a/packages/pangle/README.md b/packages/pangle/README.md
index 8c8bf09..c2c9fb8 100644
--- a/packages/pangle/README.md
+++ b/packages/pangle/README.md
@@ -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.
diff --git a/packages/ump/.gitignore b/packages/ump/.gitignore
new file mode 100644
index 0000000..67f3212
--- /dev/null
+++ b/packages/ump/.gitignore
@@ -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/
diff --git a/packages/ump/NitroUMPAds.podspec b/packages/ump/NitroUMPAds.podspec
new file mode 100644
index 0000000..ecf964a
--- /dev/null
+++ b/packages/ump/NitroUMPAds.podspec
@@ -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
diff --git a/packages/ump/README.md b/packages/ump/README.md
new file mode 100644
index 0000000..afa40a0
--- /dev/null
+++ b/packages/ump/README.md
@@ -0,0 +1,151 @@
+# @react-native-ads/ump
+
+Google User Messaging Platform (UMP) — GDPR/CCPA consent for React Native.
+
+[](https://www.npmjs.com/package/@react-native-ads/ump)
+[](../../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
+
+```
+
+iOS — add to `Info.plist`:
+
+```xml
+GADApplicationIdentifier
+YOUR_ADMOB_APP_ID
+```
+
+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` | Requests/refreshes consent info from Google's servers. Call on every app launch. |
+| `loadAndShowConsentFormIfRequired(): Promise` | Loads and, if required, presents the consent form. No-ops if none is required. |
+| `showForm(): Promise` | Unconditionally loads and presents the consent form, regardless of `status`. |
+| `showPrivacyOptionsForm(): Promise` | Presents the "privacy options" form so the user can revisit their choice later. |
+| `gatherConsent(options?: UMPConsentRequestOptions): Promise` | 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
diff --git a/packages/ump/android/CMakeLists.txt b/packages/ump/android/CMakeLists.txt
new file mode 100644
index 0000000..4d08179
--- /dev/null
+++ b/packages/ump/android/CMakeLists.txt
@@ -0,0 +1,20 @@
+project(NitroUMPAds)
+cmake_minimum_required(VERSION 3.9.0)
+
+set(PACKAGE_NAME NitroUMPAds)
+set(CMAKE_VERBOSE_MAKEFILE ON)
+set(CMAKE_CXX_STANDARD 20)
+
+add_library(${PACKAGE_NAME} SHARED
+ src/main/cpp/cpp-adapter.cpp
+)
+
+include(${CMAKE_SOURCE_DIR}/../nitrogen/generated/android/NitroUMPAds+autolinking.cmake)
+
+find_library(LOG_LIB log)
+
+target_link_libraries(
+ ${PACKAGE_NAME}
+ ${LOG_LIB}
+ android
+)
diff --git a/packages/ump/android/build.gradle b/packages/ump/android/build.gradle
new file mode 100644
index 0000000..6ce47f1
--- /dev/null
+++ b/packages/ump/android/build.gradle
@@ -0,0 +1,128 @@
+buildscript {
+ repositories {
+ google()
+ mavenCentral()
+ }
+
+ dependencies {
+ classpath "com.android.tools.build:gradle:8.12.1"
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:2.1.20"
+ }
+}
+
+def reactNativeArchitectures() {
+ def value = rootProject.getProperties().get("reactNativeArchitectures")
+ return value ? value.split(",") : ["armeabi-v7a", "x86", "x86_64", "arm64-v8a"]
+}
+
+def isNewArchitectureEnabled() {
+ return rootProject.hasProperty("newArchEnabled") && rootProject.getProperty("newArchEnabled") == "true"
+}
+
+apply plugin: "com.android.library"
+apply plugin: "org.jetbrains.kotlin.android"
+
+def nitroAutolinkingFile = file("../nitrogen/generated/android/NitroUMPAds+autolinking.gradle")
+if (nitroAutolinkingFile.exists()) {
+ apply from: nitroAutolinkingFile
+}
+apply from: "./fix-prefab.gradle"
+
+def getExtOrDefault(name) {
+ return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties["NitroUMPAds_" + name]
+}
+
+def getExtOrIntegerDefault(name) {
+ return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties["NitroUMPAds_" + name]).toInteger()
+}
+
+android {
+ namespace "com.margelo.nitro.umpads"
+
+ ndkVersion getExtOrDefault("ndkVersion")
+ compileSdkVersion getExtOrIntegerDefault("compileSdkVersion")
+
+ defaultConfig {
+ minSdkVersion getExtOrIntegerDefault("minSdkVersion")
+ targetSdkVersion getExtOrIntegerDefault("targetSdkVersion")
+ consumerProguardFiles "consumer-rules.pro"
+ buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString()
+
+ externalNativeBuild {
+ cmake {
+ cppFlags "-frtti -fexceptions -Wall -Wextra -fstack-protector-all"
+ arguments "-DANDROID_STL=c++_shared", "-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON"
+ abiFilters (*reactNativeArchitectures())
+
+ buildTypes {
+ debug {
+ cppFlags "-O1 -g"
+ }
+ release {
+ cppFlags "-O2"
+ }
+ }
+ }
+ }
+ }
+
+ externalNativeBuild {
+ cmake {
+ path "CMakeLists.txt"
+ }
+ }
+
+ packagingOptions {
+ excludes = [
+ "META-INF",
+ "META-INF/**",
+ "**/libc++_shared.so",
+ "**/libfbjni.so",
+ "**/libjsi.so",
+ "**/libfolly_json.so",
+ "**/libfolly_runtime.so",
+ "**/libglog.so",
+ "**/libhermes.so",
+ "**/libreactnative.so",
+ "**/libreactnativejni.so"
+ ]
+ }
+
+ buildFeatures {
+ buildConfig true
+ prefab true
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ }
+ }
+
+ lintOptions {
+ disable "GradleCompatible"
+ }
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_17
+ targetCompatibility JavaVersion.VERSION_17
+ }
+
+ kotlinOptions {
+ jvmTarget = "17"
+ }
+
+}
+
+repositories {
+ google()
+ mavenCentral()
+}
+
+dependencies {
+ implementation "com.facebook.react:react-android"
+ implementation project(":react-native-nitro-modules")
+
+ implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0"
+ implementation "com.google.android.ump:user-messaging-platform:4.0.0"
+}
diff --git a/packages/ump/android/consumer-rules.pro b/packages/ump/android/consumer-rules.pro
new file mode 100644
index 0000000..2b9a34f
--- /dev/null
+++ b/packages/ump/android/consumer-rules.pro
@@ -0,0 +1,2 @@
+-keep class com.margelo.nitro.umpads.** { *; }
+-keep class com.google.android.ump.** { *; }
diff --git a/packages/ump/android/fix-prefab.gradle b/packages/ump/android/fix-prefab.gradle
new file mode 100644
index 0000000..53797cb
--- /dev/null
+++ b/packages/ump/android/fix-prefab.gradle
@@ -0,0 +1,46 @@
+tasks.configureEach { task ->
+ def prefabConfigurePattern = ~/^prefab(.+)ConfigurePackage$/
+ def matcher = task.name =~ prefabConfigurePattern
+ if (matcher.matches()) {
+ def variantName = matcher[0][1]
+ task.outputs.upToDateWhen { false }
+ task.dependsOn("externalNativeBuild${variantName}")
+ }
+}
+
+afterEvaluate {
+ def abis = reactNativeArchitectures()
+ rootProject.allprojects.each { proj ->
+ if (proj === rootProject) return
+
+ def dependsOnThisLib = proj.configurations.findAll { it.canBeResolved }.any { config ->
+ config.dependencies.any { dep ->
+ dep.group == project.group && dep.name == project.name
+ }
+ }
+ if (!dependsOnThisLib && proj != project) return
+
+ if (!proj.plugins.hasPlugin("com.android.application") &&
+ !proj.plugins.hasPlugin("com.android.library")) {
+ return
+ }
+
+ def variants = proj.android.hasProperty("applicationVariants") ?
+ proj.android.applicationVariants : proj.android.libraryVariants
+ variants.all { variant ->
+ def variantName = variant.name
+ abis.each { abi ->
+ def searchDir = new File(proj.projectDir, ".cxx/${variantName}")
+ if (!searchDir.exists()) return
+ def matches = []
+ searchDir.eachDir { randomDir ->
+ def prefabFile = new File(randomDir, "${abi}/prefab_config.json")
+ if (prefabFile.exists()) matches << prefabFile
+ }
+ matches.each { prefabConfig ->
+ prefabConfig.setLastModified(System.currentTimeMillis())
+ }
+ }
+ }
+ }
+}
diff --git a/packages/ump/android/gradle.properties b/packages/ump/android/gradle.properties
new file mode 100644
index 0000000..5285a4a
--- /dev/null
+++ b/packages/ump/android/gradle.properties
@@ -0,0 +1,5 @@
+NitroUMPAds_kotlinVersion=2.1.20
+NitroUMPAds_minSdkVersion=24
+NitroUMPAds_targetSdkVersion=36
+NitroUMPAds_compileSdkVersion=36
+NitroUMPAds_ndkVersion=27.1.12297006
diff --git a/packages/ump/android/src/main/AndroidManifest.xml b/packages/ump/android/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..94cbbcf
--- /dev/null
+++ b/packages/ump/android/src/main/AndroidManifest.xml
@@ -0,0 +1 @@
+
diff --git a/packages/ump/android/src/main/cpp/cpp-adapter.cpp b/packages/ump/android/src/main/cpp/cpp-adapter.cpp
new file mode 100644
index 0000000..ec3672b
--- /dev/null
+++ b/packages/ump/android/src/main/cpp/cpp-adapter.cpp
@@ -0,0 +1,7 @@
+#include
+
+#include "NitroUMPAdsOnLoad.hpp"
+
+JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void*) {
+ return margelo::nitro::umpads::initialize(vm);
+}
diff --git a/packages/ump/android/src/main/java/com/margelo/nitro/umpads/HybridUMPAds.kt b/packages/ump/android/src/main/java/com/margelo/nitro/umpads/HybridUMPAds.kt
new file mode 100644
index 0000000..59f556c
--- /dev/null
+++ b/packages/ump/android/src/main/java/com/margelo/nitro/umpads/HybridUMPAds.kt
@@ -0,0 +1,183 @@
+package com.margelo.nitro.umpads
+
+import android.preference.PreferenceManager
+import androidx.annotation.Keep
+import com.facebook.proguard.annotations.DoNotStrip
+import com.facebook.react.bridge.ReactApplicationContext
+import com.google.android.ump.ConsentDebugSettings
+import com.google.android.ump.ConsentInformation
+import com.google.android.ump.ConsentRequestParameters
+import com.google.android.ump.FormError
+import com.google.android.ump.UserMessagingPlatform
+import com.margelo.nitro.NitroModules
+import com.margelo.nitro.core.Promise
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+
+@DoNotStrip
+@Keep
+class HybridUMPAds : HybridUMPAdsSpec() {
+ private val context: ReactApplicationContext by lazy {
+ NitroModules.applicationContext as? ReactApplicationContext
+ ?: error("UMP Ads requires a ReactApplicationContext")
+ }
+
+ private val consentInformation: ConsentInformation by lazy {
+ UserMessagingPlatform.getConsentInformation(context.applicationContext)
+ }
+
+ override fun requestConsentInfoUpdate(
+ options: UMPConsentRequestOptions?,
+ ): Promise = Promise.async {
+ val activity = context.requireCurrentActivity()
+
+ val params = ConsentRequestParameters.Builder()
+ .setTagForUnderAgeOfConsent(options?.tagForUnderAgeOfConsent ?: false)
+ .apply {
+ val debugGeography = options?.debugGeography
+ val testDeviceIds = options?.testDeviceIds
+ if (debugGeography != null || !testDeviceIds.isNullOrEmpty()) {
+ val debugSettings = ConsentDebugSettings.Builder(activity).apply {
+ if (debugGeography != null) setDebugGeography(debugGeography.toNative())
+ testDeviceIds?.forEach { addTestDeviceHashedId(it) }
+ }.build()
+ setConsentDebugSettings(debugSettings)
+ }
+ }
+ .build()
+
+ val deferred = CompletableDeferred()
+ withContext(Dispatchers.Main) {
+ consentInformation.requestConsentInfoUpdate(
+ activity,
+ params,
+ { deferred.complete(Unit) },
+ { formError ->
+ deferred.completeExceptionally(
+ umpException("requestConsentInfoUpdate", formError),
+ )
+ },
+ )
+ }
+ deferred.await()
+
+ consentInfo()
+ }
+
+ override fun loadAndShowConsentFormIfRequired(): Promise = Promise.async {
+ val activity = context.requireCurrentActivity()
+
+ val deferred = CompletableDeferred()
+ withContext(Dispatchers.Main) {
+ UserMessagingPlatform.loadAndShowConsentFormIfRequired(activity) { formError ->
+ if (formError != null) {
+ deferred.completeExceptionally(
+ umpException("loadAndShowConsentFormIfRequired", formError),
+ )
+ } else {
+ deferred.complete(Unit)
+ }
+ }
+ }
+ deferred.await()
+
+ consentInfo()
+ }
+
+ override fun showPrivacyOptionsForm(): Promise = Promise.async {
+ val activity = context.requireCurrentActivity()
+
+ val deferred = CompletableDeferred()
+ withContext(Dispatchers.Main) {
+ UserMessagingPlatform.showPrivacyOptionsForm(activity) { formError ->
+ if (formError != null) {
+ deferred.completeExceptionally(
+ umpException("showPrivacyOptionsForm", formError),
+ )
+ } else {
+ deferred.complete(Unit)
+ }
+ }
+ }
+ deferred.await()
+
+ consentInfo()
+ }
+
+ override fun showForm(): Promise = Promise.async {
+ val activity = context.requireCurrentActivity()
+
+ val deferred = CompletableDeferred()
+ withContext(Dispatchers.Main) {
+ UserMessagingPlatform.loadConsentForm(
+ context,
+ { consentForm ->
+ consentForm.show(activity) { formError ->
+ if (formError != null) {
+ deferred.completeExceptionally(umpException("showForm", formError))
+ } else {
+ deferred.complete(Unit)
+ }
+ }
+ },
+ { formError -> deferred.completeExceptionally(umpException("showForm", formError)) },
+ )
+ }
+ deferred.await()
+
+ consentInfo()
+ }
+
+ override fun getConsentInfo(): UMPConsentInfo = consentInfo()
+
+ override fun reset() {
+ consentInformation.reset()
+ }
+
+ override fun getTCString(): String = sharedPreferences().getString("IABTCF_TCString", "") ?: ""
+
+ override fun getGdprApplies(): Boolean =
+ sharedPreferences().getInt("IABTCF_gdprApplies", 0) == 1
+
+ override fun getPurposeConsents(): String =
+ sharedPreferences().getString("IABTCF_PurposeConsents", "") ?: ""
+
+ override fun getPurposeLegitimateInterests(): String =
+ sharedPreferences().getString("IABTCF_PurposeLegitimateInterests", "") ?: ""
+
+ private fun sharedPreferences() = PreferenceManager.getDefaultSharedPreferences(context)
+
+ private fun consentInfo(): UMPConsentInfo = UMPConsentInfo(
+ status = consentInformation.consentStatus.toUMPConsentStatus(),
+ isConsentFormAvailable = consentInformation.isConsentFormAvailable,
+ privacyOptionsRequirementStatus =
+ consentInformation.privacyOptionsRequirementStatus.toUMPStatus(),
+ canRequestAds = consentInformation.canRequestAds(),
+ )
+}
+
+private fun Int.toUMPConsentStatus(): UMPConsentStatus = when (this) {
+ ConsentInformation.ConsentStatus.REQUIRED -> UMPConsentStatus.REQUIRED
+ ConsentInformation.ConsentStatus.NOT_REQUIRED -> UMPConsentStatus.NOTREQUIRED
+ ConsentInformation.ConsentStatus.OBTAINED -> UMPConsentStatus.OBTAINED
+ else -> UMPConsentStatus.UNKNOWN
+}
+
+private fun ConsentInformation.PrivacyOptionsRequirementStatus.toUMPStatus():
+ UMPPrivacyOptionsRequirementStatus = when (this) {
+ ConsentInformation.PrivacyOptionsRequirementStatus.REQUIRED ->
+ UMPPrivacyOptionsRequirementStatus.REQUIRED
+ ConsentInformation.PrivacyOptionsRequirementStatus.NOT_REQUIRED ->
+ UMPPrivacyOptionsRequirementStatus.NOTREQUIRED
+ else -> UMPPrivacyOptionsRequirementStatus.UNKNOWN
+}
+
+private fun UMPDebugGeography.toNative(): Int = when (this) {
+ UMPDebugGeography.EEA -> ConsentDebugSettings.DebugGeography.DEBUG_GEOGRAPHY_EEA
+ UMPDebugGeography.NOTEEA -> ConsentDebugSettings.DebugGeography.DEBUG_GEOGRAPHY_NOT_EEA
+ UMPDebugGeography.DISABLED -> ConsentDebugSettings.DebugGeography.DEBUG_GEOGRAPHY_DISABLED
+}
+
+private fun umpException(stage: String, error: FormError) =
+ IllegalStateException("UMP $stage failed (${error.errorCode}): ${error.message}")
diff --git a/packages/ump/android/src/main/java/com/margelo/nitro/umpads/NitroUMPAdsPackage.java b/packages/ump/android/src/main/java/com/margelo/nitro/umpads/NitroUMPAdsPackage.java
new file mode 100644
index 0000000..442349d
--- /dev/null
+++ b/packages/ump/android/src/main/java/com/margelo/nitro/umpads/NitroUMPAdsPackage.java
@@ -0,0 +1,27 @@
+package com.margelo.nitro.umpads;
+
+import androidx.annotation.Nullable;
+
+import com.facebook.react.TurboReactPackage;
+import com.facebook.react.bridge.NativeModule;
+import com.facebook.react.bridge.ReactApplicationContext;
+import com.facebook.react.module.model.ReactModuleInfoProvider;
+
+import java.util.HashMap;
+
+public class NitroUMPAdsPackage extends TurboReactPackage {
+ @Nullable
+ @Override
+ public NativeModule getModule(String name, ReactApplicationContext reactContext) {
+ return null;
+ }
+
+ @Override
+ public ReactModuleInfoProvider getReactModuleInfoProvider() {
+ return HashMap::new;
+ }
+
+ static {
+ NitroUMPAdsOnLoad.initializeNative();
+ }
+}
diff --git a/packages/ump/android/src/main/java/com/margelo/nitro/umpads/UMPAndroidUtils.kt b/packages/ump/android/src/main/java/com/margelo/nitro/umpads/UMPAndroidUtils.kt
new file mode 100644
index 0000000..ffe9185
--- /dev/null
+++ b/packages/ump/android/src/main/java/com/margelo/nitro/umpads/UMPAndroidUtils.kt
@@ -0,0 +1,7 @@
+package com.margelo.nitro.umpads
+
+import android.app.Activity
+import com.facebook.react.bridge.ReactApplicationContext
+
+internal fun ReactApplicationContext.requireCurrentActivity(): Activity =
+ currentActivity ?: error("Cannot request/show UMP consent without an active Activity")
diff --git a/packages/ump/babel.config.js b/packages/ump/babel.config.js
new file mode 100644
index 0000000..325ca2a
--- /dev/null
+++ b/packages/ump/babel.config.js
@@ -0,0 +1 @@
+module.exports = require('../../babel.config.js');
diff --git a/packages/ump/example/.bundle/config b/packages/ump/example/.bundle/config
new file mode 100644
index 0000000..848943b
--- /dev/null
+++ b/packages/ump/example/.bundle/config
@@ -0,0 +1,2 @@
+BUNDLE_PATH: "vendor/bundle"
+BUNDLE_FORCE_RUBY_PLATFORM: 1
diff --git a/packages/ump/example/.ruby-version b/packages/ump/example/.ruby-version
new file mode 100644
index 0000000..4d541eb
--- /dev/null
+++ b/packages/ump/example/.ruby-version
@@ -0,0 +1 @@
+3.3.12
diff --git a/packages/ump/example/Gemfile b/packages/ump/example/Gemfile
new file mode 100644
index 0000000..5151523
--- /dev/null
+++ b/packages/ump/example/Gemfile
@@ -0,0 +1,17 @@
+source 'https://rubygems.org'
+
+# You may use http://rbenv.org/ or https://rvm.io/ to install and use this version
+ruby ">= 2.6.10"
+
+# Exclude problematic versions of cocoapods and activesupport that causes build failures.
+gem 'cocoapods', '>= 1.13', '!= 1.15.0', '!= 1.15.1'
+gem 'activesupport', '>= 6.1.7.5', '!= 7.1.0'
+gem 'xcodeproj', '< 1.26.0'
+gem 'concurrent-ruby', '< 1.3.4'
+
+# Ruby 3.4.0 has removed some libraries from the standard library.
+gem 'bigdecimal'
+gem 'logger'
+gem 'benchmark'
+gem 'mutex_m'
+gem 'nkf'
diff --git a/packages/ump/example/README.md b/packages/ump/example/README.md
new file mode 100644
index 0000000..3e2c3f8
--- /dev/null
+++ b/packages/ump/example/README.md
@@ -0,0 +1,97 @@
+This is a new [**React Native**](https://reactnative.dev) project, bootstrapped using [`@react-native-community/cli`](https://github.com/react-native-community/cli).
+
+# Getting Started
+
+> **Note**: Make sure you have completed the [Set Up Your Environment](https://reactnative.dev/docs/set-up-your-environment) guide before proceeding.
+
+## Step 1: Start Metro
+
+First, you will need to run **Metro**, the JavaScript build tool for React Native.
+
+To start the Metro dev server, run the following command from the root of your React Native project:
+
+```sh
+# Using npm
+npm start
+
+# OR using Yarn
+yarn start
+```
+
+## Step 2: Build and run your app
+
+With Metro running, open a new terminal window/pane from the root of your React Native project, and use one of the following commands to build and run your Android or iOS app:
+
+### Android
+
+```sh
+# Using npm
+npm run android
+
+# OR using Yarn
+yarn android
+```
+
+### iOS
+
+For iOS, remember to install CocoaPods dependencies (this only needs to be run on first clone or after updating native deps).
+
+The first time you create a new project, run the Ruby bundler to install CocoaPods itself:
+
+```sh
+bundle install
+```
+
+Then, and every time you update your native dependencies, run:
+
+```sh
+bundle exec pod install
+```
+
+For more information, please visit [CocoaPods Getting Started guide](https://guides.cocoapods.org/using/getting-started.html).
+
+```sh
+# Using npm
+npm run ios
+
+# OR using Yarn
+yarn ios
+```
+
+If everything is set up correctly, you should see your new app running in the Android Emulator, iOS Simulator, or your connected device.
+
+This is one way to run your app — you can also build it directly from Android Studio or Xcode.
+
+## Step 3: Modify your app
+
+Now that you have successfully run the app, let's make changes!
+
+Open `App.tsx` in your text editor of choice and make some changes. When you save, your app will automatically update and reflect these changes — this is powered by [Fast Refresh](https://reactnative.dev/docs/fast-refresh).
+
+When you want to forcefully reload, for example to reset the state of your app, you can perform a full reload:
+
+- **Android**: Press the R key twice or select **"Reload"** from the **Dev Menu**, accessed via Ctrl + M (Windows/Linux) or Cmd ⌘ + M (macOS).
+- **iOS**: Press R in iOS Simulator.
+
+## Congratulations! :tada:
+
+You've successfully run and modified your React Native App. :partying_face:
+
+### Now what?
+
+- If you want to add this new React Native code to an existing application, check out the [Integration guide](https://reactnative.dev/docs/integration-with-existing-apps).
+- If you're curious to learn more about React Native, check out the [docs](https://reactnative.dev/docs/getting-started).
+
+# Troubleshooting
+
+If you're having issues getting the above steps to work, see the [Troubleshooting](https://reactnative.dev/docs/troubleshooting) page.
+
+# Learn More
+
+To learn more about React Native, take a look at the following resources:
+
+- [React Native Website](https://reactnative.dev) - learn more about React Native.
+- [Getting Started](https://reactnative.dev/docs/environment-setup) - an **overview** of React Native and how setup your environment.
+- [Learn the Basics](https://reactnative.dev/docs/getting-started) - a **guided tour** of the React Native **basics**.
+- [Blog](https://reactnative.dev/blog) - read the latest official React Native **Blog** posts.
+- [`@facebook/react-native`](https://github.com/facebook/react-native) - the Open Source; GitHub **repository** for React Native.
diff --git a/packages/ump/example/android/app/build.gradle b/packages/ump/example/android/app/build.gradle
new file mode 100644
index 0000000..0e8f0f4
--- /dev/null
+++ b/packages/ump/example/android/app/build.gradle
@@ -0,0 +1,119 @@
+apply plugin: "com.android.application"
+apply plugin: "org.jetbrains.kotlin.android"
+apply plugin: "com.facebook.react"
+
+/**
+ * This is the configuration block to customize your React Native Android app.
+ * By default you don't need to apply any configuration, just uncomment the lines you need.
+ */
+react {
+ /* Folders */
+ // The root of your project, i.e. where "package.json" lives. Default is '../..'
+ // root = file("../../")
+ // The folder where the react-native NPM package is. Default is ../../node_modules/react-native
+ // reactNativeDir = file("../../node_modules/react-native")
+ // The folder where the react-native Codegen package is. Default is ../../node_modules/@react-native/codegen
+ // codegenDir = file("../../node_modules/@react-native/codegen")
+ // The cli.js file which is the React Native CLI entrypoint. Default is ../../node_modules/react-native/cli.js
+ // cliFile = file("../../node_modules/react-native/cli.js")
+
+ /* Variants */
+ // The list of variants to that are debuggable. For those we're going to
+ // skip the bundling of the JS bundle and the assets. Default is "debug", "debugOptimized".
+ // If you add flavors like lite, prod, etc. you'll have to list your debuggableVariants.
+ // debuggableVariants = ["liteDebug", "liteDebugOptimized", "prodDebug", "prodDebugOptimized"]
+
+ /* Bundling */
+ // A list containing the node command and its flags. Default is just 'node'.
+ // nodeExecutableAndArgs = ["node"]
+ //
+ // The command to run when bundling. By default is 'bundle'
+ // bundleCommand = "ram-bundle"
+ //
+ // The path to the CLI configuration file. Default is empty.
+ // bundleConfig = file(../rn-cli.config.js)
+ //
+ // The name of the generated asset file containing your JS bundle
+ // bundleAssetName = "MyApplication.android.bundle"
+ //
+ // The entry file for bundle generation. Default is 'index.android.js' or 'index.js'
+ // entryFile = file("../js/MyApplication.android.js")
+ //
+ // A list of extra flags to pass to the 'bundle' commands.
+ // See https://github.com/react-native-community/cli/blob/main/docs/commands.md#bundle
+ // extraPackagerArgs = []
+
+ /* Hermes Commands */
+ // The hermes compiler command to run. By default it is 'hermesc'
+ // hermesCommand = "$rootDir/my-custom-hermesc/bin/hermesc"
+ //
+ // The list of flags to pass to the Hermes compiler. By default is "-O", "-output-source-map"
+ // hermesFlags = ["-O", "-output-source-map"]
+
+ /* Autolinking */
+ autolinkLibrariesWithApp()
+}
+
+/**
+ * Set this to true to Run Proguard on Release builds to minify the Java bytecode.
+ */
+def enableProguardInReleaseBuilds = false
+
+/**
+ * The preferred build flavor of JavaScriptCore (JSC)
+ *
+ * For example, to use the international variant, you can use:
+ * `def jscFlavor = io.github.react-native-community:jsc-android-intl:2026004.+`
+ *
+ * The international variant includes ICU i18n library and necessary data
+ * allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that
+ * give correct results when using with locales other than en-US. Note that
+ * this variant is about 6MiB larger per architecture than default.
+ */
+def jscFlavor = 'io.github.react-native-community:jsc-android:2026004.+'
+
+android {
+ ndkVersion rootProject.ext.ndkVersion
+ buildToolsVersion rootProject.ext.buildToolsVersion
+ compileSdk rootProject.ext.compileSdkVersion
+
+ namespace "umpads.example"
+ defaultConfig {
+ applicationId "umpads.example"
+ minSdkVersion rootProject.ext.minSdkVersion
+ targetSdkVersion rootProject.ext.targetSdkVersion
+ versionCode 1
+ versionName "1.0"
+ }
+ signingConfigs {
+ debug {
+ storeFile file('debug.keystore')
+ storePassword 'android'
+ keyAlias 'androiddebugkey'
+ keyPassword 'android'
+ }
+ }
+ buildTypes {
+ debug {
+ signingConfig signingConfigs.debug
+ }
+ release {
+ // Caution! In production, you need to generate your own keystore file.
+ // see https://reactnative.dev/docs/signed-apk-android.
+ signingConfig signingConfigs.debug
+ minifyEnabled enableProguardInReleaseBuilds
+ proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
+ }
+ }
+}
+
+dependencies {
+ // The version of react-native is set by the React Native Gradle Plugin
+ implementation("com.facebook.react:react-android")
+
+ if (hermesEnabled.toBoolean()) {
+ implementation("com.facebook.react:hermes-android")
+ } else {
+ implementation jscFlavor
+ }
+}
diff --git a/packages/ump/example/android/app/debug.keystore b/packages/ump/example/android/app/debug.keystore
new file mode 100644
index 0000000..364e105
Binary files /dev/null and b/packages/ump/example/android/app/debug.keystore differ
diff --git a/packages/ump/example/android/app/proguard-rules.pro b/packages/ump/example/android/app/proguard-rules.pro
new file mode 100644
index 0000000..11b0257
--- /dev/null
+++ b/packages/ump/example/android/app/proguard-rules.pro
@@ -0,0 +1,10 @@
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the proguardFiles
+# directive in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
diff --git a/packages/ump/example/android/app/src/main/AndroidManifest.xml b/packages/ump/example/android/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..8d37f59
--- /dev/null
+++ b/packages/ump/example/android/app/src/main/AndroidManifest.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/ump/example/android/app/src/main/java/umpads/example/MainActivity.kt b/packages/ump/example/android/app/src/main/java/umpads/example/MainActivity.kt
new file mode 100644
index 0000000..fc7339a
--- /dev/null
+++ b/packages/ump/example/android/app/src/main/java/umpads/example/MainActivity.kt
@@ -0,0 +1,22 @@
+package umpads.example
+
+import com.facebook.react.ReactActivity
+import com.facebook.react.ReactActivityDelegate
+import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled
+import com.facebook.react.defaults.DefaultReactActivityDelegate
+
+class MainActivity : ReactActivity() {
+
+ /**
+ * Returns the name of the main component registered from JavaScript. This is used to schedule
+ * rendering of the component.
+ */
+ override fun getMainComponentName(): String = "UMPAdsExample"
+
+ /**
+ * Returns the instance of the [ReactActivityDelegate]. We use [DefaultReactActivityDelegate]
+ * which allows you to enable New Architecture with a single boolean flags [fabricEnabled]
+ */
+ override fun createReactActivityDelegate(): ReactActivityDelegate =
+ DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled)
+}
diff --git a/packages/ump/example/android/app/src/main/java/umpads/example/MainApplication.kt b/packages/ump/example/android/app/src/main/java/umpads/example/MainApplication.kt
new file mode 100644
index 0000000..ed63017
--- /dev/null
+++ b/packages/ump/example/android/app/src/main/java/umpads/example/MainApplication.kt
@@ -0,0 +1,27 @@
+package umpads.example
+
+import android.app.Application
+import com.facebook.react.PackageList
+import com.facebook.react.ReactApplication
+import com.facebook.react.ReactHost
+import com.facebook.react.ReactNativeApplicationEntryPoint.loadReactNative
+import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost
+
+class MainApplication : Application(), ReactApplication {
+
+ override val reactHost: ReactHost by lazy {
+ getDefaultReactHost(
+ context = applicationContext,
+ packageList =
+ PackageList(this).packages.apply {
+ // Packages that cannot be autolinked yet can be added manually here, for example:
+ // add(MyReactNativePackage())
+ },
+ )
+ }
+
+ override fun onCreate() {
+ super.onCreate()
+ loadReactNative(this)
+ }
+}
diff --git a/packages/ump/example/android/app/src/main/res/drawable/rn_edit_text_material.xml b/packages/ump/example/android/app/src/main/res/drawable/rn_edit_text_material.xml
new file mode 100644
index 0000000..5c25e72
--- /dev/null
+++ b/packages/ump/example/android/app/src/main/res/drawable/rn_edit_text_material.xml
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/ump/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/packages/ump/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..a2f5908
Binary files /dev/null and b/packages/ump/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/packages/ump/example/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/packages/ump/example/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 0000000..1b52399
Binary files /dev/null and b/packages/ump/example/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/packages/ump/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/packages/ump/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..ff10afd
Binary files /dev/null and b/packages/ump/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/packages/ump/example/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/packages/ump/example/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 0000000..115a4c7
Binary files /dev/null and b/packages/ump/example/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/packages/ump/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/packages/ump/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..dcd3cd8
Binary files /dev/null and b/packages/ump/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/packages/ump/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/packages/ump/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..459ca60
Binary files /dev/null and b/packages/ump/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/packages/ump/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/packages/ump/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..8ca12fe
Binary files /dev/null and b/packages/ump/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/packages/ump/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/packages/ump/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..8e19b41
Binary files /dev/null and b/packages/ump/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/packages/ump/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/packages/ump/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..b824ebd
Binary files /dev/null and b/packages/ump/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/packages/ump/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/packages/ump/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..4c19a13
Binary files /dev/null and b/packages/ump/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/packages/ump/example/android/app/src/main/res/values/strings.xml b/packages/ump/example/android/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000..51a465e
--- /dev/null
+++ b/packages/ump/example/android/app/src/main/res/values/strings.xml
@@ -0,0 +1,3 @@
+
+ UMPAdsExample
+
diff --git a/packages/ump/example/android/app/src/main/res/values/styles.xml b/packages/ump/example/android/app/src/main/res/values/styles.xml
new file mode 100644
index 0000000..7ba83a2
--- /dev/null
+++ b/packages/ump/example/android/app/src/main/res/values/styles.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
diff --git a/packages/ump/example/android/build.gradle b/packages/ump/example/android/build.gradle
new file mode 100644
index 0000000..dad99b0
--- /dev/null
+++ b/packages/ump/example/android/build.gradle
@@ -0,0 +1,21 @@
+buildscript {
+ ext {
+ buildToolsVersion = "36.0.0"
+ minSdkVersion = 24
+ compileSdkVersion = 36
+ targetSdkVersion = 36
+ ndkVersion = "27.1.12297006"
+ kotlinVersion = "2.1.20"
+ }
+ repositories {
+ google()
+ mavenCentral()
+ }
+ dependencies {
+ classpath("com.android.tools.build:gradle")
+ classpath("com.facebook.react:react-native-gradle-plugin")
+ classpath("org.jetbrains.kotlin:kotlin-gradle-plugin")
+ }
+}
+
+apply plugin: "com.facebook.react.rootproject"
diff --git a/packages/ump/example/android/gradle.properties b/packages/ump/example/android/gradle.properties
new file mode 100644
index 0000000..9afe615
--- /dev/null
+++ b/packages/ump/example/android/gradle.properties
@@ -0,0 +1,44 @@
+# Project-wide Gradle settings.
+
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+# Default value: -Xmx512m -XX:MaxMetaspaceSize=256m
+org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m
+
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
+
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app's APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+
+# Use this property to specify which architecture you want to build.
+# You can also override it from the CLI using
+# ./gradlew -PreactNativeArchitectures=x86_64
+reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64
+
+# Use this property to enable support to the new architecture.
+# This will allow you to use TurboModules and the Fabric render in
+# your application. You should enable this flag either if you want
+# to write custom TurboModules/Fabric components OR use libraries that
+# are providing them.
+newArchEnabled=true
+
+# Use this property to enable or disable the Hermes JS engine.
+# If set to false, you will be using JSC instead.
+hermesEnabled=true
+
+# Use this property to enable edge-to-edge display support.
+# This allows your app to draw behind system bars for an immersive UI.
+# Note: Only works with ReactActivity and should not be used with custom Activity.
+edgeToEdgeEnabled=false
diff --git a/packages/ump/example/android/gradle/wrapper/gradle-wrapper.jar b/packages/ump/example/android/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..61285a6
Binary files /dev/null and b/packages/ump/example/android/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/packages/ump/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/ump/example/android/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..37f78a6
--- /dev/null
+++ b/packages/ump/example/android/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,7 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip
+networkTimeout=10000
+validateDistributionUrl=true
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/packages/ump/example/android/gradlew b/packages/ump/example/android/gradlew
new file mode 100755
index 0000000..adff685
--- /dev/null
+++ b/packages/ump/example/android/gradlew
@@ -0,0 +1,248 @@
+#!/bin/sh
+
+#
+# Copyright © 2015 the original authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# SPDX-License-Identifier: Apache-2.0
+#
+
+##############################################################################
+#
+# Gradle start up script for POSIX generated by Gradle.
+#
+# Important for running:
+#
+# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+# noncompliant, but you have some other compliant shell such as ksh or
+# bash, then to run this script, type that shell name before the whole
+# command line, like:
+#
+# ksh Gradle
+#
+# Busybox and similar reduced shells will NOT work, because this script
+# requires all of these POSIX shell features:
+# * functions;
+# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+# * compound commands having a testable exit status, especially «case»;
+# * various built-in commands including «command», «set», and «ulimit».
+#
+# Important for patching:
+#
+# (2) This script targets any POSIX shell, so it avoids extensions provided
+# by Bash, Ksh, etc; in particular arrays are avoided.
+#
+# The "traditional" practice of packing multiple parameters into a
+# space-separated string is a well documented source of bugs and security
+# problems, so this is (mostly) avoided, by progressively accumulating
+# options in "$@", and eventually passing that to Java.
+#
+# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+# see the in-line comments for details.
+#
+# There are tweaks for specific operating systems such as AIX, CygWin,
+# Darwin, MinGW, and NonStop.
+#
+# (3) This script is generated from the Groovy template
+# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# within the Gradle project.
+#
+# You can find Gradle at https://github.com/gradle/gradle/.
+#
+##############################################################################
+
+# Attempt to set APP_HOME
+
+# Resolve links: $0 may be a link
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+ APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
+ [ -h "$app_path" ]
+do
+ ls=$( ls -ld "$app_path" )
+ link=${ls#*' -> '}
+ case $link in #(
+ /*) app_path=$link ;; #(
+ *) app_path=$APP_HOME$link ;;
+ esac
+done
+
+# This is normally unused
+# shellcheck disable=SC2034
+APP_BASE_NAME=${0##*/}
+# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
+APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD=maximum
+
+warn () {
+ echo "$*"
+} >&2
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+} >&2
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "$( uname )" in #(
+ CYGWIN* ) cygwin=true ;; #(
+ Darwin* ) darwin=true ;; #(
+ MSYS* | MINGW* ) msys=true ;; #(
+ NONSTOP* ) nonstop=true ;;
+esac
+
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD=$JAVA_HOME/jre/sh/java
+ else
+ JAVACMD=$JAVA_HOME/bin/java
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD=java
+ if ! command -v java >/dev/null 2>&1
+ then
+ die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+fi
+
+# Increase the maximum file descriptors if we can.
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+ case $MAX_FD in #(
+ max*)
+ # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ MAX_FD=$( ulimit -H -n ) ||
+ warn "Could not query maximum file descriptor limit"
+ esac
+ case $MAX_FD in #(
+ '' | soft) :;; #(
+ *)
+ # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ ulimit -n "$MAX_FD" ||
+ warn "Could not set maximum file descriptor limit to $MAX_FD"
+ esac
+fi
+
+# Collect all arguments for the java command, stacking in reverse order:
+# * args from the command line
+# * the main class name
+# * -classpath
+# * -D...appname settings
+# * --module-path (only if needed)
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+ APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+
+ JAVACMD=$( cygpath --unix "$JAVACMD" )
+
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ for arg do
+ if
+ case $arg in #(
+ -*) false ;; # don't mess with options #(
+ /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
+ [ -e "$t" ] ;; #(
+ *) false ;;
+ esac
+ then
+ arg=$( cygpath --path --ignore --mixed "$arg" )
+ fi
+ # Roll the args list around exactly as many times as the number of
+ # args, so each arg winds up back in the position where it started, but
+ # possibly modified.
+ #
+ # NB: a `for` loop captures its iteration list before it begins, so
+ # changing the positional parameters here affects neither the number of
+ # iterations, nor the values presented in `arg`.
+ shift # remove old arg
+ set -- "$@" "$arg" # push replacement arg
+ done
+fi
+
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Collect all arguments for the java command:
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
+# and any embedded shellness will be escaped.
+# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
+# treated as '${Hostname}' itself on the command line.
+
+set -- \
+ "-Dorg.gradle.appname=$APP_BASE_NAME" \
+ -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
+ "$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+ die "xargs is not available"
+fi
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+# set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+ printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+ xargs -n1 |
+ sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+ tr '\n' ' '
+ )" '"$@"'
+
+exec "$JAVACMD" "$@"
diff --git a/packages/ump/example/android/gradlew.bat b/packages/ump/example/android/gradlew.bat
new file mode 100644
index 0000000..4626b90
--- /dev/null
+++ b/packages/ump/example/android/gradlew.bat
@@ -0,0 +1,98 @@
+@REM Copyright (c) Meta Platforms, Inc. and affiliates.
+@REM
+@REM This source code is licensed under the MIT license found in the
+@REM LICENSE file in the root directory of this source tree.
+
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+@rem SPDX-License-Identifier: Apache-2.0
+@rem
+
+@if "%DEBUG%"=="" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if %ERRORLEVEL% equ 0 goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if %ERRORLEVEL% equ 0 goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/packages/ump/example/android/settings.gradle b/packages/ump/example/android/settings.gradle
new file mode 100644
index 0000000..2da979f
--- /dev/null
+++ b/packages/ump/example/android/settings.gradle
@@ -0,0 +1,6 @@
+pluginManagement { includeBuild("../node_modules/@react-native/gradle-plugin") }
+plugins { id("com.facebook.react.settings") }
+extensions.configure(com.facebook.react.ReactSettingsExtension){ ex -> ex.autolinkLibrariesFromCommand() }
+rootProject.name = 'umpads.example'
+include ':app'
+includeBuild('../node_modules/@react-native/gradle-plugin')
diff --git a/packages/ump/example/app.json b/packages/ump/example/app.json
new file mode 100644
index 0000000..344c947
--- /dev/null
+++ b/packages/ump/example/app.json
@@ -0,0 +1,4 @@
+{
+ "name": "UMPAdsExample",
+ "displayName": "UMPAdsExample"
+}
diff --git a/packages/ump/example/index.js b/packages/ump/example/index.js
new file mode 100644
index 0000000..117ddca
--- /dev/null
+++ b/packages/ump/example/index.js
@@ -0,0 +1,5 @@
+import { AppRegistry } from 'react-native';
+import App from './src/App';
+import { name as appName } from './app.json';
+
+AppRegistry.registerComponent(appName, () => App);
diff --git a/packages/ump/example/ios/.xcode.env b/packages/ump/example/ios/.xcode.env
new file mode 100644
index 0000000..3d5782c
--- /dev/null
+++ b/packages/ump/example/ios/.xcode.env
@@ -0,0 +1,11 @@
+# This `.xcode.env` file is versioned and is used to source the environment
+# used when running script phases inside Xcode.
+# To customize your local environment, you can create an `.xcode.env.local`
+# file that is not versioned.
+
+# NODE_BINARY variable contains the PATH to the node executable.
+#
+# Customize the NODE_BINARY variable here.
+# For example, to use nvm with brew, add the following line
+# . "$(brew --prefix nvm)/nvm.sh" --no-use
+export NODE_BINARY=$(command -v node)
diff --git a/packages/ump/example/ios/Podfile b/packages/ump/example/ios/Podfile
new file mode 100644
index 0000000..7262693
--- /dev/null
+++ b/packages/ump/example/ios/Podfile
@@ -0,0 +1,34 @@
+# Resolve react_native_pods.rb with node to allow for hoisting
+require Pod::Executable.execute_command('node', ['-p',
+ 'require.resolve(
+ "react-native/scripts/react_native_pods.rb",
+ {paths: [process.argv[1]]},
+ )', __dir__]).strip
+
+platform :ios, min_ios_version_supported
+prepare_react_native_project!
+
+linkage = ENV['USE_FRAMEWORKS']
+if linkage != nil
+ Pod::UI.puts "Configuring Pod with #{linkage}ally linked Frameworks".green
+ use_frameworks! :linkage => linkage.to_sym
+end
+
+target 'UMPAdsExample' do
+ config = use_native_modules!
+
+ use_react_native!(
+ :path => config[:reactNativePath],
+ # An absolute path to your application root.
+ :app_path => "#{Pod::Config.instance.installation_root}/.."
+ )
+
+ post_install do |installer|
+ react_native_post_install(
+ installer,
+ config[:reactNativePath],
+ :mac_catalyst_enabled => false,
+ # :ccache_enabled => true
+ )
+ end
+end
diff --git a/packages/ump/example/ios/UMPAdsExample.xcodeproj/project.pbxproj b/packages/ump/example/ios/UMPAdsExample.xcodeproj/project.pbxproj
new file mode 100644
index 0000000..0734fad
--- /dev/null
+++ b/packages/ump/example/ios/UMPAdsExample.xcodeproj/project.pbxproj
@@ -0,0 +1,475 @@
+// !$*UTF8*$!
+{
+ archiveVersion = 1;
+ classes = {
+ };
+ objectVersion = 54;
+ objects = {
+
+/* Begin PBXBuildFile section */
+ 0C80B921A6F3F58F76C31292 /* libPods-UMPAdsExample.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5DCACB8F33CDC322A6C60F78 /* libPods-UMPAdsExample.a */; };
+ 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
+ 761780ED2CA45674006654EE /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 761780EC2CA45674006654EE /* AppDelegate.swift */; };
+ 81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXFileReference section */
+ 13B07F961A680F5B00A75B9A /* UMPAdsExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = UMPAdsExample.app; sourceTree = BUILT_PRODUCTS_DIR; };
+ 13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = UMPAdsExample/Images.xcassets; sourceTree = ""; };
+ 13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = UMPAdsExample/Info.plist; sourceTree = ""; };
+ 13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = PrivacyInfo.xcprivacy; path = UMPAdsExample/PrivacyInfo.xcprivacy; sourceTree = ""; };
+ 3B4392A12AC88292D35C810B /* Pods-UMPAdsExample.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-UMPAdsExample.debug.xcconfig"; path = "Target Support Files/Pods-UMPAdsExample/Pods-UMPAdsExample.debug.xcconfig"; sourceTree = ""; };
+ 5709B34CF0A7D63546082F79 /* Pods-UMPAdsExample.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-UMPAdsExample.release.xcconfig"; path = "Target Support Files/Pods-UMPAdsExample/Pods-UMPAdsExample.release.xcconfig"; sourceTree = ""; };
+ 5DCACB8F33CDC322A6C60F78 /* libPods-UMPAdsExample.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-UMPAdsExample.a"; sourceTree = BUILT_PRODUCTS_DIR; };
+ 761780EC2CA45674006654EE /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = UMPAdsExample/AppDelegate.swift; sourceTree = ""; };
+ 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = UMPAdsExample/LaunchScreen.storyboard; sourceTree = ""; };
+ ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+ 13B07F8C1A680F5B00A75B9A /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 0C80B921A6F3F58F76C31292 /* libPods-UMPAdsExample.a in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+ 13B07FAE1A68108700A75B9A /* UMPAdsExample */ = {
+ isa = PBXGroup;
+ children = (
+ 13B07FB51A68108700A75B9A /* Images.xcassets */,
+ 761780EC2CA45674006654EE /* AppDelegate.swift */,
+ 13B07FB61A68108700A75B9A /* Info.plist */,
+ 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */,
+ 13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */,
+ );
+ name = UMPAdsExample;
+ sourceTree = "";
+ };
+ 2D16E6871FA4F8E400B85C8A /* Frameworks */ = {
+ isa = PBXGroup;
+ children = (
+ ED297162215061F000B7C4FE /* JavaScriptCore.framework */,
+ 5DCACB8F33CDC322A6C60F78 /* libPods-UMPAdsExample.a */,
+ );
+ name = Frameworks;
+ sourceTree = "";
+ };
+ 832341AE1AAA6A7D00B99B32 /* Libraries */ = {
+ isa = PBXGroup;
+ children = (
+ );
+ name = Libraries;
+ sourceTree = "";
+ };
+ 83CBB9F61A601CBA00E9B192 = {
+ isa = PBXGroup;
+ children = (
+ 13B07FAE1A68108700A75B9A /* UMPAdsExample */,
+ 832341AE1AAA6A7D00B99B32 /* Libraries */,
+ 83CBBA001A601CBA00E9B192 /* Products */,
+ 2D16E6871FA4F8E400B85C8A /* Frameworks */,
+ BBD78D7AC51CEA395F1C20DB /* Pods */,
+ );
+ indentWidth = 2;
+ sourceTree = "";
+ tabWidth = 2;
+ usesTabs = 0;
+ };
+ 83CBBA001A601CBA00E9B192 /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ 13B07F961A680F5B00A75B9A /* UMPAdsExample.app */,
+ );
+ name = Products;
+ sourceTree = "";
+ };
+ BBD78D7AC51CEA395F1C20DB /* Pods */ = {
+ isa = PBXGroup;
+ children = (
+ 3B4392A12AC88292D35C810B /* Pods-UMPAdsExample.debug.xcconfig */,
+ 5709B34CF0A7D63546082F79 /* Pods-UMPAdsExample.release.xcconfig */,
+ );
+ path = Pods;
+ sourceTree = "";
+ };
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+ 13B07F861A680F5B00A75B9A /* UMPAdsExample */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "UMPAdsExample" */;
+ buildPhases = (
+ C38B50BA6285516D6DCD4F65 /* [CP] Check Pods Manifest.lock */,
+ 13B07F871A680F5B00A75B9A /* Sources */,
+ 13B07F8C1A680F5B00A75B9A /* Frameworks */,
+ 13B07F8E1A680F5B00A75B9A /* Resources */,
+ 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */,
+ 00EEFC60759A1932668264C0 /* [CP] Embed Pods Frameworks */,
+ E235C05ADACE081382539298 /* [CP] Copy Pods Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ name = UMPAdsExample;
+ productName = UMPAdsExample;
+ productReference = 13B07F961A680F5B00A75B9A /* UMPAdsExample.app */;
+ productType = "com.apple.product-type.application";
+ };
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+ 83CBB9F71A601CBA00E9B192 /* Project object */ = {
+ isa = PBXProject;
+ attributes = {
+ LastUpgradeCheck = 1210;
+ TargetAttributes = {
+ 13B07F861A680F5B00A75B9A = {
+ LastSwiftMigration = 1120;
+ };
+ };
+ };
+ buildConfigurationList = 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "UMPAdsExample" */;
+ compatibilityVersion = "Xcode 12.0";
+ developmentRegion = en;
+ hasScannedForEncodings = 0;
+ knownRegions = (
+ en,
+ Base,
+ );
+ mainGroup = 83CBB9F61A601CBA00E9B192;
+ productRefGroup = 83CBBA001A601CBA00E9B192 /* Products */;
+ projectDirPath = "";
+ projectRoot = "";
+ targets = (
+ 13B07F861A680F5B00A75B9A /* UMPAdsExample */,
+ );
+ };
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+ 13B07F8E1A680F5B00A75B9A /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */,
+ 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXShellScriptBuildPhase section */
+ 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ "$(SRCROOT)/.xcode.env.local",
+ "$(SRCROOT)/.xcode.env",
+ );
+ name = "Bundle React Native code and images";
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "set -e\n\nWITH_ENVIRONMENT=\"$REACT_NATIVE_PATH/scripts/xcode/with-environment.sh\"\nREACT_NATIVE_XCODE=\"$REACT_NATIVE_PATH/scripts/react-native-xcode.sh\"\n\n/bin/sh -c \"\\\"$WITH_ENVIRONMENT\\\" \\\"$REACT_NATIVE_XCODE\\\"\"\n";
+ };
+ 00EEFC60759A1932668264C0 /* [CP] Embed Pods Frameworks */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ "${PODS_ROOT}/Target Support Files/Pods-UMPAdsExample/Pods-UMPAdsExample-frameworks-${CONFIGURATION}-input-files.xcfilelist",
+ );
+ name = "[CP] Embed Pods Frameworks";
+ outputFileListPaths = (
+ "${PODS_ROOT}/Target Support Files/Pods-UMPAdsExample/Pods-UMPAdsExample-frameworks-${CONFIGURATION}-output-files.xcfilelist",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-UMPAdsExample/Pods-UMPAdsExample-frameworks.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
+ C38B50BA6285516D6DCD4F65 /* [CP] Check Pods Manifest.lock */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ );
+ inputPaths = (
+ "${PODS_PODFILE_DIR_PATH}/Podfile.lock",
+ "${PODS_ROOT}/Manifest.lock",
+ );
+ name = "[CP] Check Pods Manifest.lock";
+ outputFileListPaths = (
+ );
+ outputPaths = (
+ "$(DERIVED_FILE_DIR)/Pods-UMPAdsExample-checkManifestLockResult.txt",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
+ showEnvVarsInLog = 0;
+ };
+ E235C05ADACE081382539298 /* [CP] Copy Pods Resources */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ "${PODS_ROOT}/Target Support Files/Pods-UMPAdsExample/Pods-UMPAdsExample-resources-${CONFIGURATION}-input-files.xcfilelist",
+ );
+ name = "[CP] Copy Pods Resources";
+ outputFileListPaths = (
+ "${PODS_ROOT}/Target Support Files/Pods-UMPAdsExample/Pods-UMPAdsExample-resources-${CONFIGURATION}-output-files.xcfilelist",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-UMPAdsExample/Pods-UMPAdsExample-resources.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
+/* End PBXShellScriptBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+ 13B07F871A680F5B00A75B9A /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 761780ED2CA45674006654EE /* AppDelegate.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXSourcesBuildPhase section */
+
+/* Begin XCBuildConfiguration section */
+ 13B07F941A680F5B00A75B9A /* Debug */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 3B4392A12AC88292D35C810B /* Pods-UMPAdsExample.debug.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CLANG_ENABLE_MODULES = YES;
+ CURRENT_PROJECT_VERSION = 1;
+ ENABLE_BITCODE = NO;
+ INFOPLIST_FILE = UMPAdsExample/Info.plist;
+ IPHONEOS_DEPLOYMENT_TARGET = 15.1;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ MARKETING_VERSION = 1.0;
+ OTHER_LDFLAGS = (
+ "$(inherited)",
+ "-ObjC",
+ "-lc++",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = "umpads.example";
+ PRODUCT_NAME = UMPAdsExample;
+ SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ VERSIONING_SYSTEM = "apple-generic";
+ };
+ name = Debug;
+ };
+ 13B07F951A680F5B00A75B9A /* Release */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 5709B34CF0A7D63546082F79 /* Pods-UMPAdsExample.release.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CLANG_ENABLE_MODULES = YES;
+ CURRENT_PROJECT_VERSION = 1;
+ INFOPLIST_FILE = UMPAdsExample/Info.plist;
+ IPHONEOS_DEPLOYMENT_TARGET = 15.1;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ MARKETING_VERSION = 1.0;
+ OTHER_LDFLAGS = (
+ "$(inherited)",
+ "-ObjC",
+ "-lc++",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = "umpads.example";
+ PRODUCT_NAME = UMPAdsExample;
+ SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ VERSIONING_SYSTEM = "apple-generic";
+ };
+ name = Release;
+ };
+ 83CBBA201A601CBA00E9B192 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
+ CLANG_CXX_LANGUAGE_STANDARD = "c++20";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+ COPY_PHASE_STRIP = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_TESTABILITY = YES;
+ "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "";
+ GCC_C_LANGUAGE_STANDARD = gnu99;
+ GCC_DYNAMIC_NO_PIC = NO;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_OPTIMIZATION_LEVEL = 0;
+ GCC_PREPROCESSOR_DEFINITIONS = (
+ "DEBUG=1",
+ "$(inherited)",
+ );
+ GCC_SYMBOLS_PRIVATE_EXTERN = NO;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 15.1;
+ LD_RUNPATH_SEARCH_PATHS = (
+ /usr/lib/swift,
+ "$(inherited)",
+ );
+ LIBRARY_SEARCH_PATHS = (
+ "\"$(SDKROOT)/usr/lib/swift\"",
+ "\"$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)\"",
+ "\"$(inherited)\"",
+ );
+ MTL_ENABLE_DEBUG_INFO = YES;
+ ONLY_ACTIVE_ARCH = YES;
+ OTHER_CPLUSPLUSFLAGS = (
+ "$(OTHER_CFLAGS)",
+ "-DFOLLY_NO_CONFIG",
+ "-DFOLLY_MOBILE=1",
+ "-DFOLLY_USE_LIBCPP=1",
+ "-DFOLLY_CFG_NO_COROUTINES=1",
+ "-DFOLLY_HAVE_CLOCK_GETTIME=1",
+ );
+ SDKROOT = iphoneos;
+ };
+ name = Debug;
+ };
+ 83CBBA211A601CBA00E9B192 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
+ CLANG_CXX_LANGUAGE_STANDARD = "c++20";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+ COPY_PHASE_STRIP = YES;
+ ENABLE_NS_ASSERTIONS = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "";
+ GCC_C_LANGUAGE_STANDARD = gnu99;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 15.1;
+ LD_RUNPATH_SEARCH_PATHS = (
+ /usr/lib/swift,
+ "$(inherited)",
+ );
+ LIBRARY_SEARCH_PATHS = (
+ "\"$(SDKROOT)/usr/lib/swift\"",
+ "\"$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)\"",
+ "\"$(inherited)\"",
+ );
+ MTL_ENABLE_DEBUG_INFO = NO;
+ OTHER_CPLUSPLUSFLAGS = (
+ "$(OTHER_CFLAGS)",
+ "-DFOLLY_NO_CONFIG",
+ "-DFOLLY_MOBILE=1",
+ "-DFOLLY_USE_LIBCPP=1",
+ "-DFOLLY_CFG_NO_COROUTINES=1",
+ "-DFOLLY_HAVE_CLOCK_GETTIME=1",
+ );
+ SDKROOT = iphoneos;
+ VALIDATE_PRODUCT = YES;
+ };
+ name = Release;
+ };
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+ 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "UMPAdsExample" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 13B07F941A680F5B00A75B9A /* Debug */,
+ 13B07F951A680F5B00A75B9A /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "UMPAdsExample" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 83CBBA201A601CBA00E9B192 /* Debug */,
+ 83CBBA211A601CBA00E9B192 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+/* End XCConfigurationList section */
+ };
+ rootObject = 83CBB9F71A601CBA00E9B192 /* Project object */;
+}
diff --git a/packages/ump/example/ios/UMPAdsExample.xcodeproj/xcshareddata/xcschemes/UMPAdsExample.xcscheme b/packages/ump/example/ios/UMPAdsExample.xcodeproj/xcshareddata/xcschemes/UMPAdsExample.xcscheme
new file mode 100644
index 0000000..6c7e6b6
--- /dev/null
+++ b/packages/ump/example/ios/UMPAdsExample.xcodeproj/xcshareddata/xcschemes/UMPAdsExample.xcscheme
@@ -0,0 +1,88 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/ump/example/ios/UMPAdsExample/AppDelegate.swift b/packages/ump/example/ios/UMPAdsExample/AppDelegate.swift
new file mode 100644
index 0000000..8ae350e
--- /dev/null
+++ b/packages/ump/example/ios/UMPAdsExample/AppDelegate.swift
@@ -0,0 +1,48 @@
+import UIKit
+import React
+import React_RCTAppDelegate
+import ReactAppDependencyProvider
+
+@main
+class AppDelegate: UIResponder, UIApplicationDelegate {
+ var window: UIWindow?
+
+ var reactNativeDelegate: ReactNativeDelegate?
+ var reactNativeFactory: RCTReactNativeFactory?
+
+ func application(
+ _ application: UIApplication,
+ didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
+ ) -> Bool {
+ let delegate = ReactNativeDelegate()
+ let factory = RCTReactNativeFactory(delegate: delegate)
+ delegate.dependencyProvider = RCTAppDependencyProvider()
+
+ reactNativeDelegate = delegate
+ reactNativeFactory = factory
+
+ window = UIWindow(frame: UIScreen.main.bounds)
+
+ factory.startReactNative(
+ withModuleName: "UMPAdsExample",
+ in: window,
+ launchOptions: launchOptions
+ )
+
+ return true
+ }
+}
+
+class ReactNativeDelegate: RCTDefaultReactNativeFactoryDelegate {
+ override func sourceURL(for bridge: RCTBridge) -> URL? {
+ self.bundleURL()
+ }
+
+ override func bundleURL() -> URL? {
+#if DEBUG
+ RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index")
+#else
+ Bundle.main.url(forResource: "main", withExtension: "jsbundle")
+#endif
+ }
+}
diff --git a/packages/ump/example/ios/UMPAdsExample/Images.xcassets/AppIcon.appiconset/Contents.json b/packages/ump/example/ios/UMPAdsExample/Images.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 0000000..8121323
--- /dev/null
+++ b/packages/ump/example/ios/UMPAdsExample/Images.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,53 @@
+{
+ "images" : [
+ {
+ "idiom" : "iphone",
+ "scale" : "2x",
+ "size" : "20x20"
+ },
+ {
+ "idiom" : "iphone",
+ "scale" : "3x",
+ "size" : "20x20"
+ },
+ {
+ "idiom" : "iphone",
+ "scale" : "2x",
+ "size" : "29x29"
+ },
+ {
+ "idiom" : "iphone",
+ "scale" : "3x",
+ "size" : "29x29"
+ },
+ {
+ "idiom" : "iphone",
+ "scale" : "2x",
+ "size" : "40x40"
+ },
+ {
+ "idiom" : "iphone",
+ "scale" : "3x",
+ "size" : "40x40"
+ },
+ {
+ "idiom" : "iphone",
+ "scale" : "2x",
+ "size" : "60x60"
+ },
+ {
+ "idiom" : "iphone",
+ "scale" : "3x",
+ "size" : "60x60"
+ },
+ {
+ "idiom" : "ios-marketing",
+ "scale" : "1x",
+ "size" : "1024x1024"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/packages/ump/example/ios/UMPAdsExample/Images.xcassets/Contents.json b/packages/ump/example/ios/UMPAdsExample/Images.xcassets/Contents.json
new file mode 100644
index 0000000..2d92bd5
--- /dev/null
+++ b/packages/ump/example/ios/UMPAdsExample/Images.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
diff --git a/packages/ump/example/ios/UMPAdsExample/Info.plist b/packages/ump/example/ios/UMPAdsExample/Info.plist
new file mode 100644
index 0000000..5a7d940
--- /dev/null
+++ b/packages/ump/example/ios/UMPAdsExample/Info.plist
@@ -0,0 +1,63 @@
+
+
+
+
+ CADisableMinimumFrameDurationOnPhone
+
+ CFBundleDevelopmentRegion
+ en
+ CFBundleDisplayName
+ UMPAdsExample
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ $(PRODUCT_NAME)
+ CFBundlePackageType
+ APPL
+ CFBundleShortVersionString
+ $(MARKETING_VERSION)
+ CFBundleSignature
+ ????
+ CFBundleVersion
+ $(CURRENT_PROJECT_VERSION)
+
+ GADApplicationIdentifier
+ ca-app-pub-3940256099942544~1458002511
+ LSRequiresIPhoneOS
+
+ NSAppTransportSecurity
+
+
+ NSAllowsArbitraryLoads
+
+ NSAllowsLocalNetworking
+
+
+ NSLocationWhenInUseUsageDescription
+
+ UILaunchStoryboardName
+ LaunchScreen
+ UIRequiredDeviceCapabilities
+
+ arm64
+
+ UISupportedInterfaceOrientations
+
+ UIInterfaceOrientationPortrait
+
+ UISupportedInterfaceOrientations~ipad
+
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationPortraitUpsideDown
+
+ UIViewControllerBasedStatusBarAppearance
+
+
+
diff --git a/packages/ump/example/ios/UMPAdsExample/LaunchScreen.storyboard b/packages/ump/example/ios/UMPAdsExample/LaunchScreen.storyboard
new file mode 100644
index 0000000..5c6a4bd
--- /dev/null
+++ b/packages/ump/example/ios/UMPAdsExample/LaunchScreen.storyboard
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/ump/example/ios/UMPAdsExample/PrivacyInfo.xcprivacy b/packages/ump/example/ios/UMPAdsExample/PrivacyInfo.xcprivacy
new file mode 100644
index 0000000..41b8317
--- /dev/null
+++ b/packages/ump/example/ios/UMPAdsExample/PrivacyInfo.xcprivacy
@@ -0,0 +1,37 @@
+
+
+
+
+ NSPrivacyAccessedAPITypes
+
+
+ NSPrivacyAccessedAPIType
+ NSPrivacyAccessedAPICategoryFileTimestamp
+ NSPrivacyAccessedAPITypeReasons
+
+ C617.1
+
+
+
+ NSPrivacyAccessedAPIType
+ NSPrivacyAccessedAPICategoryUserDefaults
+ NSPrivacyAccessedAPITypeReasons
+
+ CA92.1
+
+
+
+ NSPrivacyAccessedAPIType
+ NSPrivacyAccessedAPICategorySystemBootTime
+ NSPrivacyAccessedAPITypeReasons
+
+ 35F9.1
+
+
+
+ NSPrivacyCollectedDataTypes
+
+ NSPrivacyTracking
+
+
+
diff --git a/packages/ump/example/jest.config.js b/packages/ump/example/jest.config.js
new file mode 100644
index 0000000..294be30
--- /dev/null
+++ b/packages/ump/example/jest.config.js
@@ -0,0 +1,3 @@
+module.exports = {
+ preset: '@react-native/jest-preset',
+};
diff --git a/packages/ump/example/metro.config.js b/packages/ump/example/metro.config.js
new file mode 100644
index 0000000..763c285
--- /dev/null
+++ b/packages/ump/example/metro.config.js
@@ -0,0 +1,19 @@
+const path = require('path');
+const { getDefaultConfig } = require('@react-native/metro-config');
+const { withMetroConfig } = require('react-native-monorepo-config');
+
+const root = path.resolve(__dirname, '..');
+
+/**
+ * Metro configuration
+ * https://facebook.github.io/metro/docs/configuration
+ *
+ * @type {import('metro-config').MetroConfig}
+ */
+const config = withMetroConfig(getDefaultConfig(__dirname), {
+ root,
+ dirname: __dirname,
+ conditions: ['react-native-ads-ump-source'],
+});
+
+module.exports = config;
diff --git a/packages/ump/example/package.json b/packages/ump/example/package.json
new file mode 100644
index 0000000..b372da3
--- /dev/null
+++ b/packages/ump/example/package.json
@@ -0,0 +1,39 @@
+{
+ "name": "@react-native-ads/ump-example",
+ "version": "0.0.1",
+ "private": true,
+ "scripts": {
+ "android": "react-native run-android",
+ "ios": "react-native run-ios",
+ "start": "react-native start",
+ "build:android": "react-native build-android --extra-params \"--no-daemon --console=plain -PreactNativeArchitectures=arm64-v8a\"",
+ "build:ios": "react-native build-ios --mode Debug"
+ },
+ "dependencies": {
+ "@iabtcf/core": "^1.5.6",
+ "react": "19.2.3",
+ "react-native": "0.85.0",
+ "react-native-nitro-modules": "^0.29.6"
+ },
+ "devDependencies": {
+ "@babel/core": "^7.25.2",
+ "@babel/preset-env": "^7.25.3",
+ "@babel/runtime": "^7.25.0",
+ "@react-native-community/cli": "20.1.0",
+ "@react-native-community/cli-platform-android": "20.1.0",
+ "@react-native-community/cli-platform-ios": "20.1.0",
+ "@react-native/babel-preset": "0.85.0",
+ "@react-native/jest-preset": "0.85.0",
+ "@react-native/metro-config": "0.85.0",
+ "@react-native/typescript-config": "0.85.0",
+ "@types/react": "^19.2.0",
+ "react-native-builder-bob": "^0.43.0",
+ "react-native-monorepo-config": "^0.4.0"
+ },
+ "engines": {
+ "node": ">= 22.11.0"
+ },
+ "installConfig": {
+ "hoistingLimits": "workspaces"
+ }
+}
diff --git a/packages/ump/example/react-native.config.js b/packages/ump/example/react-native.config.js
new file mode 100644
index 0000000..59d9698
--- /dev/null
+++ b/packages/ump/example/react-native.config.js
@@ -0,0 +1,21 @@
+const path = require('path');
+const pkg = require('../package.json');
+
+module.exports = {
+ project: {
+ ios: {
+ automaticPodsInstallation: true,
+ },
+ },
+ dependencies: {
+ [pkg.name]: {
+ root: path.join(__dirname, '..'),
+ platforms: {
+ // Codegen script incorrectly fails without this
+ // So we explicitly specify the platforms with empty object
+ ios: {},
+ android: {},
+ },
+ },
+ },
+};
diff --git a/packages/ump/example/src/App.tsx b/packages/ump/example/src/App.tsx
new file mode 100644
index 0000000..9546ca6
--- /dev/null
+++ b/packages/ump/example/src/App.tsx
@@ -0,0 +1,107 @@
+import { useCallback, useEffect, useState } from 'react';
+import { Button, SafeAreaView, StyleSheet, Text, View } from 'react-native';
+import UMPAds, {
+ type UMPConsentInfo,
+ type UMPUserChoices,
+} from '@react-native-ads/ump';
+
+export default function App() {
+ const [status, setStatus] = useState('Requesting consent info…');
+ const [consentInfo, setConsentInfo] = useState(null);
+ const [userChoices, setUserChoices] = useState(null);
+
+ const gatherConsent = useCallback(async () => {
+ try {
+ const info = await UMPAds().gatherConsent({
+ // testDeviceIds: ['YOUR_TEST_DEVICE_ID'],
+ // debugGeography: 'EEA',
+ });
+
+ setConsentInfo(info);
+ setStatus(info.canRequestAds ? 'Ready to request ads' : 'Not ready');
+ } catch (error) {
+ setStatus(`Failed to gather consent: ${(error as Error).message}`);
+ }
+ }, []);
+
+ useEffect(() => {
+ gatherConsent();
+ }, [gatherConsent]);
+
+ const showForm = useCallback(() => {
+ UMPAds()
+ .showForm()
+ .then(setConsentInfo)
+ .catch((error) => setStatus(`Failed to show form: ${error.message}`));
+ }, []);
+
+ const showPrivacyOptions = useCallback(() => {
+ UMPAds()
+ .showPrivacyOptionsForm()
+ .then(setConsentInfo)
+ .catch((error) =>
+ setStatus(`Failed to show privacy options: ${error.message}`)
+ );
+ }, []);
+
+ const getUserChoices = useCallback(() => {
+ setUserChoices(UMPAds().getUserChoices());
+ }, []);
+
+ const reset = useCallback(() => {
+ UMPAds().reset();
+ setConsentInfo(null);
+ setUserChoices(null);
+ setStatus('Consent state reset — requesting again…');
+ gatherConsent();
+ }, [gatherConsent]);
+
+ return (
+
+ {status}
+ {consentInfo && (
+
+ status: {consentInfo.status}
+ {'\n'}
+ privacyOptionsRequirementStatus:{' '}
+ {consentInfo.privacyOptionsRequirementStatus}
+
+ )}
+ {userChoices && (
+
+ selectPersonalisedAds: {String(userChoices.selectPersonalisedAds)}
+ {'\n'}
+ usePreciseGeolocationData:{' '}
+ {String(userChoices.usePreciseGeolocationData)}
+
+ )}
+
+
+
+
+
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ alignItems: 'center',
+ justifyContent: 'center',
+ gap: 24,
+ },
+ status: {
+ textAlign: 'center',
+ paddingHorizontal: 24,
+ },
+ buttons: {
+ gap: 12,
+ },
+});
diff --git a/packages/ump/ios/HybridUMPAds.swift b/packages/ump/ios/HybridUMPAds.swift
new file mode 100644
index 0000000..c5ae6e4
--- /dev/null
+++ b/packages/ump/ios/HybridUMPAds.swift
@@ -0,0 +1,148 @@
+import Foundation
+import NitroModules
+import UIKit
+import UserMessagingPlatform
+
+final class HybridUMPAds: HybridUMPAdsSpec, @unchecked Sendable {
+ func requestConsentInfoUpdate(options: UMPConsentRequestOptions?) throws -> Promise {
+ Promise.async {
+ let parameters = RequestParameters()
+ parameters.isTaggedForUnderAgeOfConsent = options?.tagForUnderAgeOfConsent ?? false
+
+ if let debugGeography = options?.debugGeography {
+ let debugSettings = DebugSettings()
+ debugSettings.geography = debugGeography.toNative()
+ if let testDeviceIds = options?.testDeviceIds {
+ debugSettings.testDeviceIdentifiers = testDeviceIds
+ }
+ parameters.debugSettings = debugSettings
+ }
+
+ // Must run on the main thread/queue — the UMP SDK is not thread-safe
+ // and this call can synchronously create UI-related state.
+ try await Self.performRequestConsentInfoUpdate(parameters)
+
+ return Self.consentInfo()
+ }
+ }
+
+ func loadAndShowConsentFormIfRequired() throws -> Promise {
+ Promise.async {
+ let viewController = try await currentViewController()
+ // ConsentForm.loadAndPresentIfRequired() creates a WKWebView and must
+ // run on the main thread — `await`ing off of `currentViewController()`
+ // does not itself guarantee that, so hop explicitly.
+ try await Self.performLoadAndPresentIfRequired(from: viewController)
+ return Self.consentInfo()
+ }
+ }
+
+ func showPrivacyOptionsForm() throws -> Promise {
+ Promise.async {
+ let viewController = try await currentViewController()
+ try await Self.performPresentPrivacyOptionsForm(from: viewController)
+ return Self.consentInfo()
+ }
+ }
+
+ func showForm() throws -> Promise {
+ Promise.async {
+ let viewController = try await currentViewController()
+ try await Self.performShowForm(from: viewController)
+ return Self.consentInfo()
+ }
+ }
+
+ @MainActor
+ private static func performRequestConsentInfoUpdate(_ parameters: RequestParameters) async throws {
+ try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in
+ ConsentInformation.shared.requestConsentInfoUpdate(with: parameters) { error in
+ if let error {
+ continuation.resume(throwing: error)
+ } else {
+ continuation.resume()
+ }
+ }
+ }
+ }
+
+ @MainActor
+ private static func performLoadAndPresentIfRequired(from viewController: UIViewController) async throws {
+ try await ConsentForm.loadAndPresentIfRequired(from: viewController)
+ }
+
+ @MainActor
+ private static func performPresentPrivacyOptionsForm(from viewController: UIViewController) async throws {
+ try await ConsentForm.presentPrivacyOptionsForm(from: viewController)
+ }
+
+ @MainActor
+ private static func performShowForm(from viewController: UIViewController) async throws {
+ let form = try await ConsentForm.load()
+ try await form.present(from: viewController)
+ }
+
+ func getConsentInfo() throws -> UMPConsentInfo {
+ Self.consentInfo()
+ }
+
+ func reset() throws {
+ runOnMain { ConsentInformation.shared.reset() }
+ }
+
+ func getTCString() throws -> String {
+ UserDefaults.standard.string(forKey: "IABTCF_TCString") ?? ""
+ }
+
+ func getGdprApplies() throws -> Bool {
+ UserDefaults.standard.bool(forKey: "IABTCF_gdprApplies")
+ }
+
+ func getPurposeConsents() throws -> String {
+ UserDefaults.standard.string(forKey: "IABTCF_PurposeConsents") ?? ""
+ }
+
+ func getPurposeLegitimateInterests() throws -> String {
+ UserDefaults.standard.string(forKey: "IABTCF_PurposeLegitimateInterests") ?? ""
+ }
+
+ private static func consentInfo() -> UMPConsentInfo {
+ UMPConsentInfo(
+ status: ConsentInformation.shared.consentStatus.toUMP(),
+ isConsentFormAvailable: ConsentInformation.shared.formStatus == .available,
+ privacyOptionsRequirementStatus: ConsentInformation.shared.privacyOptionsRequirementStatus.toUMP(),
+ canRequestAds: ConsentInformation.shared.canRequestAds
+ )
+ }
+}
+
+private extension ConsentStatus {
+ func toUMP() -> UMPConsentStatus {
+ switch self {
+ case .required: return .required
+ case .notRequired: return .notrequired
+ case .obtained: return .obtained
+ default: return .unknown
+ }
+ }
+}
+
+private extension PrivacyOptionsRequirementStatus {
+ func toUMP() -> UMPPrivacyOptionsRequirementStatus {
+ switch self {
+ case .required: return .required
+ case .notRequired: return .notrequired
+ default: return .unknown
+ }
+ }
+}
+
+private extension UMPDebugGeography {
+ func toNative() -> DebugGeography {
+ switch self {
+ case .eea: return .EEA
+ case .noteea: return .notEEA
+ case .disabled: return .disabled
+ }
+ }
+}
diff --git a/packages/ump/ios/UMPSupport.swift b/packages/ump/ios/UMPSupport.swift
new file mode 100644
index 0000000..2e41936
--- /dev/null
+++ b/packages/ump/ios/UMPSupport.swift
@@ -0,0 +1,55 @@
+import Foundation
+import UIKit
+
+let umpErrorDomain = "NitroUMPAds"
+
+func umpError(_ message: String, code: Int = 1) -> NSError {
+ NSError(
+ domain: umpErrorDomain,
+ code: code,
+ userInfo: [NSLocalizedDescriptionKey: message]
+ )
+}
+
+func runOnMain(_ body: @escaping () -> Void) {
+ if Thread.isMainThread {
+ body()
+ } else {
+ DispatchQueue.main.sync(execute: body)
+ }
+}
+
+func currentViewController() async throws -> UIViewController {
+ try await withCheckedThrowingContinuation { continuation in
+ DispatchQueue.main.async {
+ let root = UIApplication.shared.connectedScenes
+ .compactMap { $0 as? UIWindowScene }
+ .flatMap(\.windows)
+ .first(where: \.isKeyWindow)?
+ .rootViewController
+
+ guard let root else {
+ continuation.resume(
+ throwing: umpError("Cannot request/show UMP consent without a view controller")
+ )
+ return
+ }
+ continuation.resume(returning: topViewController(root))
+ }
+ }
+}
+
+private func topViewController(_ viewController: UIViewController) -> UIViewController {
+ if let presented = viewController.presentedViewController {
+ return topViewController(presented)
+ }
+ if let navigation = viewController as? UINavigationController,
+ let visible = navigation.visibleViewController {
+ return topViewController(visible)
+ }
+ if let tab = viewController as? UITabBarController,
+ let selected = tab.selectedViewController {
+ return topViewController(selected)
+ }
+ return viewController
+}
diff --git a/packages/ump/jest.config.js b/packages/ump/jest.config.js
new file mode 100644
index 0000000..afc750d
--- /dev/null
+++ b/packages/ump/jest.config.js
@@ -0,0 +1,15 @@
+module.exports = {
+ testEnvironment: 'node',
+ testMatch: ['/src/**/*.test.ts'],
+ collectCoverageFrom: ['src/**/*.ts', '!src/**/*.test.ts', '!src/specs/**'],
+ coverageReporters: ['text'],
+ transform: {
+ '^.+\\.ts$': [
+ 'babel-jest',
+ {
+ presets: ['@babel/preset-typescript'],
+ plugins: ['@babel/plugin-transform-modules-commonjs'],
+ },
+ ],
+ },
+};
diff --git a/packages/ump/nitro.json b/packages/ump/nitro.json
new file mode 100644
index 0000000..836a945
--- /dev/null
+++ b/packages/ump/nitro.json
@@ -0,0 +1,18 @@
+{
+ "$schema": "https://nitro.margelo.com/nitro.schema.json",
+ "cxxNamespace": ["umpads"],
+ "ios": {
+ "iosModuleName": "NitroUMPAds"
+ },
+ "android": {
+ "androidNamespace": ["umpads"],
+ "androidCxxLibName": "NitroUMPAds"
+ },
+ "autolinking": {
+ "UMPAds": {
+ "swift": "HybridUMPAds",
+ "kotlin": "HybridUMPAds"
+ }
+ },
+ "ignorePaths": ["**/node_modules"]
+}
diff --git a/packages/ump/package.json b/packages/ump/package.json
new file mode 100644
index 0000000..fb29ba2
--- /dev/null
+++ b/packages/ump/package.json
@@ -0,0 +1,141 @@
+{
+ "name": "@react-native-ads/ump",
+ "version": "0.1.0",
+ "description": "Google User Messaging Platform (UMP) consent SDK for React Native",
+ "main": "./lib/module/index.js",
+ "types": "./lib/typescript/src/index.d.ts",
+ "exports": {
+ ".": {
+ "react-native-ads-ump-source": "./src/index.ts",
+ "types": "./lib/typescript/src/index.d.ts",
+ "default": "./lib/module/index.js"
+ },
+ "./package.json": "./package.json"
+ },
+ "files": [
+ "src",
+ "lib",
+ "android",
+ "ios",
+ "cpp",
+ "nitrogen",
+ "nitro.json",
+ "*.podspec",
+ "react-native.config.js",
+ "!ios/build",
+ "!android/build",
+ "!android/gradle",
+ "!android/gradlew",
+ "!android/gradlew.bat",
+ "!android/local.properties",
+ "!**/__tests__",
+ "!**/__fixtures__",
+ "!**/__mocks__",
+ "!**/.*"
+ ],
+ "scripts": {
+ "example": "yarn workspace @react-native-ads/ump-example",
+ "clean": "del-cli android/build example/android/build example/android/app/build example/ios/build lib",
+ "prepare": "bob build",
+ "build": "bob build",
+ "nitrogen": "nitrogen",
+ "typecheck": "tsc",
+ "lint": "eslint \"**/*.{js,ts,tsx}\"",
+ "test": "jest"
+ },
+ "keywords": [
+ "react-native",
+ "react-native-ads",
+ "ads",
+ "advertising",
+ "advertisement",
+ "mobile-ads",
+ "monetization",
+ "ump",
+ "consent",
+ "gdpr",
+ "ccpa",
+ "user-messaging-platform",
+ "admob",
+ "google-mobile-ads",
+ "ios",
+ "android"
+ ],
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/exzos28/react-native-ads.git",
+ "directory": "packages/ump"
+ },
+ "author": "exzos (https://github.com/exzos28)",
+ "license": "MIT",
+ "bugs": {
+ "url": "https://github.com/exzos28/react-native-ads/issues"
+ },
+ "homepage": "https://github.com/exzos28/react-native-ads/tree/main/packages/ump#readme",
+ "publishConfig": {
+ "registry": "https://registry.npmjs.org/"
+ },
+ "dependencies": {
+ "@babel/runtime": "^7.29.7",
+ "@iabtcf/core": "^1.5.6"
+ },
+ "devDependencies": {
+ "@babel/core": "^7.28.4",
+ "@babel/plugin-transform-modules-commonjs": "^7.27.1",
+ "@babel/preset-typescript": "^7.27.1",
+ "@types/react": "^19.2.0",
+ "babel-jest": "^29.7.0",
+ "nitrogen": "0.29.6",
+ "react": "19.2.3",
+ "react-native": "0.85.0",
+ "react-native-nitro-modules": "^0.29.6"
+ },
+ "peerDependencies": {
+ "react": "*",
+ "react-native": "*",
+ "react-native-nitro-modules": "^0.29.6"
+ },
+ "workspaces": [
+ "example"
+ ],
+ "installConfig": {
+ "hoistingLimits": "workspaces"
+ },
+ "packageManager": "yarn@4.11.0",
+ "react-native-builder-bob": {
+ "source": "src",
+ "output": "lib",
+ "targets": [
+ [
+ "custom",
+ {
+ "script": "nitrogen",
+ "clean": "nitrogen/"
+ }
+ ],
+ [
+ "module",
+ {
+ "esm": true
+ }
+ ],
+ [
+ "typescript",
+ {
+ "project": "tsconfig.build.json"
+ }
+ ]
+ ]
+ },
+ "create-react-native-library": {
+ "type": "nitro-module",
+ "languages": "kotlin-swift",
+ "tools": [
+ "eslint",
+ "jest",
+ "lefthook",
+ "release-it"
+ ],
+ "version": "0.63.0"
+ }
+}
diff --git a/packages/ump/react-native.config.js b/packages/ump/react-native.config.js
new file mode 100644
index 0000000..c36ba55
--- /dev/null
+++ b/packages/ump/react-native.config.js
@@ -0,0 +1,8 @@
+module.exports = {
+ dependency: {
+ platforms: {
+ ios: {},
+ android: {},
+ },
+ },
+};
diff --git a/packages/ump/src/index.test.ts b/packages/ump/src/index.test.ts
new file mode 100644
index 0000000..bcd2637
--- /dev/null
+++ b/packages/ump/src/index.test.ts
@@ -0,0 +1,137 @@
+jest.mock('react-native-nitro-modules', () => {
+ const defaultConsentInfo = {
+ status: 'obtained',
+ isConsentFormAvailable: false,
+ privacyOptionsRequirementStatus: 'notRequired',
+ canRequestAds: true,
+ };
+
+ const mockNativeUMPAds = {
+ requestConsentInfoUpdate: jest.fn(async () => defaultConsentInfo),
+ loadAndShowConsentFormIfRequired: jest.fn(async () => defaultConsentInfo),
+ showPrivacyOptionsForm: jest.fn(async () => defaultConsentInfo),
+ showForm: jest.fn(async () => defaultConsentInfo),
+ getConsentInfo: jest.fn(() => defaultConsentInfo),
+ reset: jest.fn(),
+ getTCString: jest.fn(() => ''),
+ getGdprApplies: jest.fn(() => true),
+ getPurposeConsents: jest.fn(() => ''),
+ getPurposeLegitimateInterests: jest.fn(() => ''),
+ };
+
+ return {
+ NitroModules: {
+ createHybridObject: () => mockNativeUMPAds,
+ },
+ };
+});
+
+import { NitroModules } from 'react-native-nitro-modules';
+import { UMPAds } from './index';
+
+const mockNativeUMPAds = NitroModules.createHybridObject(
+ 'UMPAds' as never
+) as unknown as Record;
+
+const defaultConsentInfo = {
+ status: 'obtained',
+ isConsentFormAvailable: false,
+ privacyOptionsRequirementStatus: 'notRequired',
+ canRequestAds: true,
+};
+
+beforeEach(() => {
+ Object.values(mockNativeUMPAds).forEach((mockFn) => mockFn.mockClear());
+});
+
+describe('UMPAds()', () => {
+ it('forwards requestConsentInfoUpdate options to the native module', async () => {
+ const options = {
+ tagForUnderAgeOfConsent: true,
+ debugGeography: 'EEA' as const,
+ testDeviceIds: ['device-1'],
+ };
+
+ const info = await UMPAds().requestConsentInfoUpdate(options);
+
+ expect(mockNativeUMPAds.requestConsentInfoUpdate).toHaveBeenCalledWith(
+ options
+ );
+ expect(info).toEqual(defaultConsentInfo);
+ });
+
+ it('delegates loadAndShowConsentFormIfRequired to the native module', async () => {
+ await UMPAds().loadAndShowConsentFormIfRequired();
+ expect(
+ mockNativeUMPAds.loadAndShowConsentFormIfRequired
+ ).toHaveBeenCalledTimes(1);
+ });
+
+ it('delegates showPrivacyOptionsForm to the native module', async () => {
+ await UMPAds().showPrivacyOptionsForm();
+ expect(mockNativeUMPAds.showPrivacyOptionsForm).toHaveBeenCalledTimes(1);
+ });
+
+ it('delegates getConsentInfo to the native module', () => {
+ const info = UMPAds().getConsentInfo();
+ expect(mockNativeUMPAds.getConsentInfo).toHaveBeenCalledTimes(1);
+ expect(info).toEqual(defaultConsentInfo);
+ });
+
+ it('delegates reset to the native module', () => {
+ UMPAds().reset();
+ expect(mockNativeUMPAds.reset).toHaveBeenCalledTimes(1);
+ });
+
+ it('delegates showForm to the native module', async () => {
+ await UMPAds().showForm();
+ expect(mockNativeUMPAds.showForm).toHaveBeenCalledTimes(1);
+ });
+
+ it('gatherConsent calls requestConsentInfoUpdate then loadAndShowConsentFormIfRequired', async () => {
+ const options = { tagForUnderAgeOfConsent: true };
+
+ await UMPAds().gatherConsent(options);
+
+ expect(mockNativeUMPAds.requestConsentInfoUpdate).toHaveBeenCalledWith(
+ options
+ );
+ expect(
+ mockNativeUMPAds.loadAndShowConsentFormIfRequired
+ ).toHaveBeenCalledTimes(1);
+ });
+
+ it('delegates getGdprApplies to the native module', () => {
+ expect(UMPAds().getGdprApplies()).toBe(true);
+ expect(mockNativeUMPAds.getGdprApplies).toHaveBeenCalledTimes(1);
+ });
+
+ it('getTCModel falls back to an empty TCModel when there is no TC string', () => {
+ const tcModel = UMPAds().getTCModel();
+ expect(tcModel.purposeConsents.has(1)).toBe(false);
+ });
+
+ it('getUserChoices reports every choice as false when there is no TC string', () => {
+ expect(UMPAds().getUserChoices()).toEqual({
+ storeAndAccessInformationOnDevice: false,
+ selectBasicAds: false,
+ createAPersonalisedAdsProfile: false,
+ selectPersonalisedAds: false,
+ createAPersonalisedContentProfile: false,
+ selectPersonalisedContent: false,
+ measureAdPerformance: false,
+ measureContentPerformance: false,
+ applyMarketResearchToGenerateAudienceInsights: false,
+ developAndImproveProducts: false,
+ usePreciseGeolocationData: false,
+ activelyScanDeviceCharacteristicsForIdentification: false,
+ });
+ });
+
+ it('getUserChoices falls back to all-false on an undecodable TC string', () => {
+ mockNativeUMPAds.getTCString!.mockReturnValueOnce('not-a-real-tc-string');
+ expect(UMPAds().getUserChoices().storeAndAccessInformationOnDevice).toBe(
+ false
+ );
+ });
+});
diff --git a/packages/ump/src/index.ts b/packages/ump/src/index.ts
new file mode 100644
index 0000000..64bc030
--- /dev/null
+++ b/packages/ump/src/index.ts
@@ -0,0 +1,162 @@
+import { NitroModules } from 'react-native-nitro-modules';
+import { TCModel, TCString } from '@iabtcf/core';
+
+import type {
+ UMPAds as NativeUMPAds,
+ UMPConsentInfo,
+ UMPConsentRequestOptions,
+} from './specs/UMPAds.nitro';
+
+/**
+ * IAB TCF v2 purpose IDs, as used by `getPurposeConsents()`/
+ * `getPurposeLegitimateInterests()` (1-indexed) and by `TCModel.purposeConsents`.
+ * https://github.com/InteractiveAdvertisingBureau/GDPR-Transparency-and-Consent-Framework
+ */
+export enum UMPPurpose {
+ STORE_AND_ACCESS_INFORMATION_ON_DEVICE = 1,
+ SELECT_BASIC_ADS = 2,
+ CREATE_A_PERSONALISED_ADS_PROFILE = 3,
+ SELECT_PERSONALISED_ADS = 4,
+ CREATE_A_PERSONALISED_CONTENT_PROFILE = 5,
+ SELECT_PERSONALISED_CONTENT = 6,
+ MEASURE_AD_PERFORMANCE = 7,
+ MEASURE_CONTENT_PERFORMANCE = 8,
+ APPLY_MARKET_RESEARCH_TO_GENERATE_AUDIENCE_INSIGHTS = 9,
+ DEVELOP_AND_IMPROVE_PRODUCTS = 10,
+}
+
+/** IAB TCF v2 special feature IDs, as used by `TCModel.specialFeatureOptins`. */
+export enum UMPSpecialFeature {
+ USE_PRECISE_GEOLOCATION_DATA = 1,
+ ACTIVELY_SCAN_DEVICE_CHARACTERISTICS_FOR_IDENTIFICATION = 2,
+}
+
+/** A user-friendly view of the 10 IAB TCF purposes + 2 special features. */
+export interface UMPUserChoices {
+ storeAndAccessInformationOnDevice: boolean;
+ selectBasicAds: boolean;
+ createAPersonalisedAdsProfile: boolean;
+ selectPersonalisedAds: boolean;
+ createAPersonalisedContentProfile: boolean;
+ selectPersonalisedContent: boolean;
+ measureAdPerformance: boolean;
+ measureContentPerformance: boolean;
+ applyMarketResearchToGenerateAudienceInsights: boolean;
+ developAndImproveProducts: boolean;
+ usePreciseGeolocationData: boolean;
+ activelyScanDeviceCharacteristicsForIdentification: boolean;
+}
+
+export interface UMPAdsModuleInterface {
+ requestConsentInfoUpdate(
+ options?: UMPConsentRequestOptions
+ ): Promise;
+ loadAndShowConsentFormIfRequired(): Promise;
+ showPrivacyOptionsForm(): Promise;
+ showForm(): Promise;
+ /**
+ * Convenience helper combining `requestConsentInfoUpdate` and
+ * `loadAndShowConsentFormIfRequired` — the common "on every app launch"
+ * flow in one call.
+ */
+ gatherConsent(options?: UMPConsentRequestOptions): Promise;
+ getConsentInfo(): UMPConsentInfo;
+ reset(): void;
+ getTCString(): string;
+ /** Parses `getTCString()` into a full IAB TCF `TCModel` (`@iabtcf/core`). */
+ getTCModel(): TCModel;
+ getGdprApplies(): boolean;
+ getPurposeConsents(): string;
+ getPurposeLegitimateInterests(): string;
+ /** A user-friendly, decoded view of `getTCModel()`'s purpose consents. */
+ getUserChoices(): UMPUserChoices;
+}
+
+const nativeUMPAds = NitroModules.createHybridObject('UMPAds');
+
+function decodeTCModel(): TCModel {
+ const tcString = nativeUMPAds.getTCString();
+ if (!tcString) {
+ return new TCModel();
+ }
+ try {
+ return TCString.decode(tcString);
+ } catch {
+ return new TCModel();
+ }
+}
+
+const umpAdsInstance: UMPAdsModuleInterface = {
+ requestConsentInfoUpdate: (options) =>
+ nativeUMPAds.requestConsentInfoUpdate(options),
+ loadAndShowConsentFormIfRequired: () =>
+ nativeUMPAds.loadAndShowConsentFormIfRequired(),
+ showPrivacyOptionsForm: () => nativeUMPAds.showPrivacyOptionsForm(),
+ showForm: () => nativeUMPAds.showForm(),
+ gatherConsent: async (options) => {
+ await nativeUMPAds.requestConsentInfoUpdate(options);
+ return nativeUMPAds.loadAndShowConsentFormIfRequired();
+ },
+ getConsentInfo: () => nativeUMPAds.getConsentInfo(),
+ reset: () => nativeUMPAds.reset(),
+ getTCString: () => nativeUMPAds.getTCString(),
+ getTCModel: decodeTCModel,
+ getGdprApplies: () => nativeUMPAds.getGdprApplies(),
+ getPurposeConsents: () => nativeUMPAds.getPurposeConsents(),
+ getPurposeLegitimateInterests: () =>
+ nativeUMPAds.getPurposeLegitimateInterests(),
+ getUserChoices: () => {
+ const tcModel = decodeTCModel();
+ return {
+ storeAndAccessInformationOnDevice: tcModel.purposeConsents.has(
+ UMPPurpose.STORE_AND_ACCESS_INFORMATION_ON_DEVICE
+ ),
+ selectBasicAds: tcModel.purposeConsents.has(UMPPurpose.SELECT_BASIC_ADS),
+ createAPersonalisedAdsProfile: tcModel.purposeConsents.has(
+ UMPPurpose.CREATE_A_PERSONALISED_ADS_PROFILE
+ ),
+ selectPersonalisedAds: tcModel.purposeConsents.has(
+ UMPPurpose.SELECT_PERSONALISED_ADS
+ ),
+ createAPersonalisedContentProfile: tcModel.purposeConsents.has(
+ UMPPurpose.CREATE_A_PERSONALISED_CONTENT_PROFILE
+ ),
+ selectPersonalisedContent: tcModel.purposeConsents.has(
+ UMPPurpose.SELECT_PERSONALISED_CONTENT
+ ),
+ measureAdPerformance: tcModel.purposeConsents.has(
+ UMPPurpose.MEASURE_AD_PERFORMANCE
+ ),
+ measureContentPerformance: tcModel.purposeConsents.has(
+ UMPPurpose.MEASURE_CONTENT_PERFORMANCE
+ ),
+ applyMarketResearchToGenerateAudienceInsights:
+ tcModel.purposeConsents.has(
+ UMPPurpose.APPLY_MARKET_RESEARCH_TO_GENERATE_AUDIENCE_INSIGHTS
+ ),
+ developAndImproveProducts: tcModel.purposeConsents.has(
+ UMPPurpose.DEVELOP_AND_IMPROVE_PRODUCTS
+ ),
+ usePreciseGeolocationData: tcModel.specialFeatureOptins.has(
+ UMPSpecialFeature.USE_PRECISE_GEOLOCATION_DATA
+ ),
+ activelyScanDeviceCharacteristicsForIdentification:
+ tcModel.specialFeatureOptins.has(
+ UMPSpecialFeature.ACTIVELY_SCAN_DEVICE_CHARACTERISTICS_FOR_IDENTIFICATION
+ ),
+ };
+ },
+};
+
+export const UMPAds = (): UMPAdsModuleInterface => umpAdsInstance;
+
+export default UMPAds;
+
+export type {
+ UMPAds as UMPAdsSpec,
+ UMPConsentInfo,
+ UMPConsentRequestOptions,
+ UMPConsentStatus,
+ UMPDebugGeography,
+ UMPPrivacyOptionsRequirementStatus,
+} from './specs/UMPAds.nitro';
diff --git a/packages/ump/src/specs/UMPAds.nitro.ts b/packages/ump/src/specs/UMPAds.nitro.ts
new file mode 100644
index 0000000..29b5a88
--- /dev/null
+++ b/packages/ump/src/specs/UMPAds.nitro.ts
@@ -0,0 +1,106 @@
+import type { HybridObject } from 'react-native-nitro-modules';
+
+/**
+ * Mirrors Google UMP's `ConsentStatus`/`ConsentInformation.ConsentStatus`.
+ */
+export type UMPConsentStatus =
+ 'unknown' | 'required' | 'notRequired' | 'obtained';
+
+/**
+ * Mirrors Google UMP's `PrivacyOptionsRequirementStatus`.
+ */
+export type UMPPrivacyOptionsRequirementStatus =
+ 'unknown' | 'required' | 'notRequired';
+
+/**
+ * Mirrors Google UMP's `DebugGeography`/`ConsentDebugSettings.DebugGeography`.
+ * For testing only — forces the SDK to behave as if the device were in the
+ * given region.
+ */
+export type UMPDebugGeography = 'disabled' | 'EEA' | 'notEEA';
+
+export interface UMPConsentRequestOptions {
+ /**
+ * If `true`, the SDK skips consent gathering entirely (COPPA/child users).
+ */
+ tagForUnderAgeOfConsent?: boolean;
+ /** Simulates a region for testing. Has no effect in production builds. */
+ debugGeography?: UMPDebugGeography;
+ /**
+ * Device IDs (hashed on Android, advertising identifier-derived on iOS)
+ * that are allowed to use `debugGeography`. Required by the SDK itself for
+ * the debug geography to take effect on a real device.
+ */
+ testDeviceIds?: string[];
+}
+
+export interface UMPConsentInfo {
+ status: UMPConsentStatus;
+ /** Whether a consent form is currently available to load/show. */
+ isConsentFormAvailable: boolean;
+ privacyOptionsRequirementStatus: UMPPrivacyOptionsRequirementStatus;
+ /** Whether the app is currently allowed to request ads. */
+ canRequestAds: boolean;
+}
+
+export interface UMPAds extends HybridObject<{
+ ios: 'swift';
+ android: 'kotlin';
+}> {
+ /**
+ * Requests/refreshes consent info from Google's servers. Call this on
+ * every app launch before deciding whether to show a consent form.
+ */
+ requestConsentInfoUpdate(
+ options?: UMPConsentRequestOptions
+ ): Promise;
+
+ /**
+ * Loads and, if required, presents the consent form. No-ops (resolves
+ * immediately) if no form is required.
+ */
+ loadAndShowConsentFormIfRequired(): Promise;
+
+ /**
+ * Presents the "privacy options" form, letting the user revisit their
+ * choice later (e.g. from a Settings screen). Only meaningful when
+ * `privacyOptionsRequirementStatus` is `'required'`.
+ */
+ showPrivacyOptionsForm(): Promise;
+
+ /**
+ * Unconditionally loads and presents the consent form, regardless of
+ * `status`. Prefer `loadAndShowConsentFormIfRequired` unless you have a
+ * specific reason to always show the form (e.g. a "reset consent" button).
+ */
+ showForm(): Promise;
+
+ /** Synchronously reads the last known consent info. */
+ getConsentInfo(): UMPConsentInfo;
+
+ /** Clears all consent state. Testing only. */
+ reset(): void;
+
+ /**
+ * The raw IAB TCF v2 consent string (`IABTCF_TCString`), or `''` if none
+ * has been written yet. Use `getUserChoices()`/`getTCModel()` for a
+ * parsed, human-readable view of this string.
+ */
+ getTCString(): string;
+
+ /** Whether GDPR applies to the current user (`IABTCF_gdprApplies`). */
+ getGdprApplies(): boolean;
+
+ /**
+ * Raw per-purpose consent bitfield (`IABTCF_PurposeConsents`) — one `'0'`
+ * or `'1'` character per IAB TCF purpose ID, 1-indexed at position 0.
+ */
+ getPurposeConsents(): string;
+
+ /**
+ * Raw per-purpose legitimate interest bitfield
+ * (`IABTCF_PurposeLegitimateInterests`), same shape as
+ * `getPurposeConsents()`.
+ */
+ getPurposeLegitimateInterests(): string;
+}
diff --git a/packages/ump/tsconfig.build.json b/packages/ump/tsconfig.build.json
new file mode 100644
index 0000000..3c0636a
--- /dev/null
+++ b/packages/ump/tsconfig.build.json
@@ -0,0 +1,4 @@
+{
+ "extends": "./tsconfig",
+ "exclude": ["example", "lib"]
+}
diff --git a/packages/ump/tsconfig.json b/packages/ump/tsconfig.json
new file mode 100644
index 0000000..3df0be4
--- /dev/null
+++ b/packages/ump/tsconfig.json
@@ -0,0 +1,15 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "rootDir": ".",
+ "paths": {
+ "@react-native-ads/ump": [
+ "./src/index"
+ ]
+ },
+ "customConditions": [
+ "react-native-ads-ump-source",
+ "react-native-strict-api"
+ ]
+ }
+}
diff --git a/packages/unity/README.md b/packages/unity/README.md
index 81d2758..5b80527 100644
--- a/packages/unity/README.md
+++ b/packages/unity/README.md
@@ -77,11 +77,9 @@ if (rewardResult.state === 'completed') {
- `result.state === 'completed'` is also how you know a rewarded ad earned
its reward; Unity 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/UnityAds.nitro.ts`](src/specs/UnityAds.nitro.ts) for the
full native interface and [`example`](example) for a runnable demo.
diff --git a/yarn.lock b/yarn.lock
index d2968c6..b7851ab 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1531,7 +1531,7 @@ __metadata:
languageName: node
linkType: hard
-"@babel/runtime@npm:^7.20.0, @babel/runtime@npm:^7.25.0":
+"@babel/runtime@npm:^7.20.0, @babel/runtime@npm:^7.25.0, @babel/runtime@npm:^7.29.7":
version: 7.29.7
resolution: "@babel/runtime@npm:7.29.7"
checksum: 10c0/ca11572f7146b21e0bde6a9ed4bb6a89eafbee5f0944c7eb54d0d8a2dac962c33638a1d611e14faa71dfbb92b4b5f9236232208568a6b7d5c6f3f39ddb91771e
@@ -2807,6 +2807,13 @@ __metadata:
languageName: node
linkType: hard
+"@iabtcf/core@npm:^1.5.6":
+ version: 1.5.6
+ resolution: "@iabtcf/core@npm:1.5.6"
+ checksum: 10c0/e7bb104fab82c5c2a051986b51bf31577f6cc9ca84b5561612d649d4503aa7db21113e667c241cb9ec93c8fe9ce65e1266396174e2f64dd1105f91a097ffa85c
+ languageName: node
+ linkType: hard
+
"@isaacs/fs-minipass@npm:^4.0.0":
version: 4.0.1
resolution: "@isaacs/fs-minipass@npm:4.0.1"
@@ -3349,6 +3356,52 @@ __metadata:
languageName: unknown
linkType: soft
+"@react-native-ads/ump-example@workspace:packages/ump/example":
+ version: 0.0.0-use.local
+ resolution: "@react-native-ads/ump-example@workspace:packages/ump/example"
+ dependencies:
+ "@babel/core": "npm:^7.25.2"
+ "@babel/preset-env": "npm:^7.25.3"
+ "@babel/runtime": "npm:^7.25.0"
+ "@iabtcf/core": "npm:^1.5.6"
+ "@react-native-community/cli": "npm:20.1.0"
+ "@react-native-community/cli-platform-android": "npm:20.1.0"
+ "@react-native-community/cli-platform-ios": "npm:20.1.0"
+ "@react-native/babel-preset": "npm:0.85.0"
+ "@react-native/jest-preset": "npm:0.85.0"
+ "@react-native/metro-config": "npm:0.85.0"
+ "@react-native/typescript-config": "npm:0.85.0"
+ "@types/react": "npm:^19.2.0"
+ react: "npm:19.2.3"
+ react-native: "npm:0.85.0"
+ react-native-builder-bob: "npm:^0.43.0"
+ react-native-monorepo-config: "npm:^0.4.0"
+ react-native-nitro-modules: "npm:^0.29.6"
+ languageName: unknown
+ linkType: soft
+
+"@react-native-ads/ump@workspace:packages/ump":
+ version: 0.0.0-use.local
+ resolution: "@react-native-ads/ump@workspace:packages/ump"
+ dependencies:
+ "@babel/core": "npm:^7.28.4"
+ "@babel/plugin-transform-modules-commonjs": "npm:^7.27.1"
+ "@babel/preset-typescript": "npm:^7.27.1"
+ "@babel/runtime": "npm:^7.29.7"
+ "@iabtcf/core": "npm:^1.5.6"
+ "@types/react": "npm:^19.2.0"
+ babel-jest: "npm:^29.7.0"
+ nitrogen: "npm:0.29.6"
+ react: "npm:19.2.3"
+ react-native: "npm:0.85.0"
+ react-native-nitro-modules: "npm:^0.29.6"
+ peerDependencies:
+ react: "*"
+ react-native: "*"
+ react-native-nitro-modules: ^0.29.6
+ languageName: unknown
+ linkType: soft
+
"@react-native-ads/unity-example@workspace:packages/unity/example":
version: 0.0.0-use.local
resolution: "@react-native-ads/unity-example@workspace:packages/unity/example"