> For the complete documentation index, see [llms.txt](https://cmp.docs.sirdata.net/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://cmp.docs.sirdata.net/en/mobile-apps-native-sdk/mobile-sdk/react-native-setup.md).

# React Native — Setup

`@sirdata/react-native-cmp` is a thin JavaScript layer over the native Sirdata CMP SDKs for Android and iOS. The consent UI, the display decision and the IAB signals all live in the native SDKs: consent is written to native storage, so the ad and analytics SDKs inside your app read it exactly as they would in a native app. The JavaScript API is for your own logic: deciding when to show the UI, and reading the result.

This page describes version 2.0.2 of the package. 2.0.1 fixed a crash of 2.0.0 on iOS (section 2), and 2.0.2 adds a reopen mode for your settings screen (section 6). All three versions use the native SDKs 2.0.0, and share the other limitations this page describes. "The next release" means the next version of the native SDKs and of this package, not yet published.

## 1. Prerequisites

|               | Minimum                                                                             |
| ------------- | ----------------------------------------------------------------------------------- |
| React Native  | None declared (`peerDependencies` is `*`); 2.0.2 was verified with 0.86.3           |
| iOS           | 15.0, including your `Podfile`'s `platform :ios`                                    |
| Android       | API 23 (the module inherits your app's `minSdkVersion`)                             |
| Android build | JDK 17 and Android Gradle Plugin 8.2 or later                                       |
| Expo          | A development build; 2.0.2 was verified with SDK 57. Expo Go cannot load the module |

* **React Native.** The package declares no minimum React Native version. 2.0.2 was verified end to end in an Expo SDK 57 app (React Native 0.86.3, New Architecture), on an Android 17 emulator and on an iOS 26.5 simulator. With React Native 0.87.1, only the installation of 2.0.0 was checked: the package installs, and its native dependencies resolve, on iOS and on Android. Older React Native versions were not tested.
* **New Architecture.** The module is written with the classic native-module API (`ReactContextBaseJavaModule` on Android, `RCTEventEmitter` on iOS). React Native loads such modules on the New Architecture through its interop layer, so there is nothing to enable or configure.
* **iOS.** Both pods the package brings require iOS 15.0. The `Podfile` of a React Native 0.76 or later project uses `platform :ios, min_ios_version_supported`, which is 15.1, so it already qualifies, as does an Expo SDK 57 project, which targets iOS 16.4. If your `Podfile` names a lower version, raise it:

  ```ruby
  platform :ios, '15.0'
  ```
* **Android.** The SDK's core is Java 21 bytecode, built and dexed with Android Gradle Plugin 8.2.2; older plugins are not tested. The activity showing your React Native UI must be a `ComponentActivity`, which React Native's `ReactActivity` is.
* **Expo.** The package contains native code, so it needs a development build (`npx expo run:ios`, `npx expo run:android`, or EAS Build), and no config plugin: Expo autolinks it (section 2). In Expo Go every call fails with a linking error (section 12).

You also need a **partner ID** and a **config ID** from the [Sirdata Console](https://cmp.sirdata.io). They select the publisher configuration the SDK downloads at startup: texts, theme, purposes, vendors and capping. Shipping your app requires a valid, current subscription to the Sirdata Consent Management Platform (section 10).

## 2. Install

With npm (with Yarn, run `yarn add @sirdata/react-native-cmp` instead):

```bash
npm install @sirdata/react-native-cmp
```

Then install the iOS pods and rebuild the native app — a Metro reload is not enough after adding native code. From the project root (`npx pod-install` does the same):

```bash
cd ios && pod install
```

The module is autolinked on both platforms: `pod install` installs the `sirdata-cmp-react-native` and `SirdataCMP` pods, and Gradle picks up the Android module.

**With Expo**, install the package with `npx expo install`:

```bash
npx expo install @sirdata/react-native-cmp
```

Then rebuild the app. An Expo project usually has no `ios` folder to run `pod install` in: `npx expo run:ios` generates the native projects (prebuild) and installs the pods, `npx expo run:android` does the same for Android, and EAS Build does it on its servers. The module needs no config plugin and no entry in `app.json`.

What the package pulls in:

|         | Dependency                                                                                                                                              | From                                     |
| ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------- |
| iOS     | `sirdata-cmp-react-native`, the bridge                                                                                                                  | Your `node_modules`, through autolinking |
| iOS     | `SirdataCMP` `~> 2.0`, which the bridge depends on: the native SDK and its SwiftUI consent UI                                                           | CocoaPods trunk                          |
| iOS     | `React-Core`, which the bridge also depends on                                                                                                          | Your React Native installation           |
| Android | `com.sirdata.cmp:android-sdk:2.0.0`, which brings `com.sirdata.cmp:shared` and `shared-android`                                                         | Maven Central                            |
| Android | `kotlinx-coroutines-android` 1.8.0 and `androidx.activity` 1.8.2, declared by the bridge. Gradle resolves each to the highest version your app requests | Maven Central, Google                    |

There is nothing to add by hand: no `pod` line, no Gradle dependency, no manifest entry and no ProGuard rule. The Android SDK declares the `INTERNET` and `ACCESS_NETWORK_STATE` permissions and its consent activity in its own manifest, which Gradle merges into your app's.

The two platforms follow versions differently. On iOS the bridge accepts any `SirdataCMP` 2.x (`~> 2.0`): your `Podfile.lock` pins what `pod install` resolved, and `pod update` can bring a later 2.x. On Android the bridge is pinned to `android-sdk` 2.0.0.

**Upgrading from 2.0.0 or 2.0.1.** Use 2.0.2, which a new install gets.

* On iOS, 2.0.0 terminates the app as soon as `shouldCollectConsent()`, `acceptAll()` or `rejectAll()` is called, even after `initialize()` has resolved, so the `startConsent()` of section 3 crashes it at first launch: React Native calls this module's methods off the main thread, and the native SDK accepts these three calls only on the main thread. 2.0.1 makes them on the main thread.
* 2.0.2 lets a settings entry withdraw a stored choice: once a choice is stored, `showConsentUI()` opens the reopen mode, with a "Reject all" button (section 6).

The native SDKs do not change and the API stays compatible, so there is nothing to migrate, your settings entry included: upgrade the package, then install the pods and rebuild the app on both platforms, since the module's native code changed in 2.0.2.

```bash
npm install @sirdata/react-native-cmp@2.0.2
```

With Expo, run `npx expo install @sirdata/react-native-cmp@2.0.2`, then `npx expo run:ios` and `npx expo run:android`, or a new EAS build.

## 3. Initialize

Initialize once, at startup, and **await it before any other call**:

```ts
// consent.ts
import SirDataCMP from '@sirdata/react-native-cmp';

let started: Promise<void> | null = null;

// Safe to call from several places: the work runs once per JavaScript session.
export function startConsent(): Promise<void> {
  if (started === null) {
    started = (async () => {
      await SirDataCMP.initialize({
        partnerId: 1020, // example ID: use your own partner ID
        configId: 'hmDnl', // example ID: use your own config ID
      });
      if (await SirDataCMP.shouldCollectConsent()) {
        await SirDataCMP.showConsentUI();
      }
    })();
  }
  return started;
}
```

Call `startConsent()` from your root component's first effect (`useEffect`), once your first screen is mounted: `showConsentUI()` needs a screen to present from. `1020` and `'hmDnl'` are the example values used on these pages: replace them with your app's own IDs from the Sirdata Console.

The single shared promise matters: React's Strict Mode runs effects twice in development, and two concurrent `showConsentUI()` calls would present the consent UI twice.

**Await `initialize()` first.** Before it has resolved, `showConsentUI()` crashes the app on both platforms, and `acceptAll()` and `rejectAll()` crash it on iOS (the native SDK is not there yet, and on iOS an error raised in its Kotlin core terminates the process). The getters are safe at any time. In the next release (unreleased), on iOS, `showConsentUI()` shows a message with a Close button instead, and `acceptAll()` / `rejectAll()` reject; on Android `showConsentUI()` still crashes.

What `initialize()` does:

* it writes `IABTCF_CmpSdkID` (92) to native storage before resolving, then loads the publisher configuration and vendor lists in the background;
* once the configuration has loaded, before any choice, the SDK also writes `IABTCF_gdprApplies`, `IABTCF_PolicyVersion`, `IABTCF_PublisherCC`, `IABTCF_PurposeOneTreatment`, `IABTCF_UseNonStandardTexts` and the publisher bit strings, and for a user under the US regime the GPP keys with the US section. The TC string and the purpose and vendor consents appear only once a choice is recorded. Until then the publisher bit strings hold the SDK's defaults; from the next release they too wait for a choice;
* it is **idempotent within a process**. A JavaScript reload (Fast Refresh, reloading from Metro) runs your startup code again: the second call resolves without re-initializing and ignores its options. Changing `partnerId` or `configId` therefore needs an app restart.

Start the ad and analytics SDKs you control from JavaScript after it has resolved: on a first launch the IAB keys do not exist before it runs.

### Options

| Option                                      | Type                      | In 2.0.0 to 2.0.2                                                                                                                                                                                                  |
| ------------------------------------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `partnerId`                                 | `number`, required        | Your partner ID. Without it the promise rejects with `E_INIT`                                                                                                                                                      |
| `configId`                                  | `string`, required        | Your config ID. Without it the promise rejects with `E_INIT`                                                                                                                                                       |
| `gdprDefaultApplies`                        | `boolean`, default `true` | What `isGdprApplies()` answers before the configuration has loaded, when the device's country cannot be determined. Once the configuration is there, the server decides                                            |
| `apiRoot`, `cacheApiRoot`, `consentApiRoot` | `string`                  | Override the three hosts. Leave them unset to use `https://api.consentframework.com/api/v1/public`, `https://choices.consentframework.com/api/v1/public` and `https://mychoice.consentframework.com/api/v1/public` |
| `language`                                  | `string`                  | Accepted, but has **no effect**                                                                                                                                                                                    |
| `debugMode`                                 | `boolean`                 | **Do not set it on Android** — see below                                                                                                                                                                           |

**The UI language and look are not options.** The consent UI uses the language set in your Sirdata configuration if it sets one, otherwise the device language, otherwise the configuration's default language, otherwise English. Its theme (colors, position, buttons) comes from the configuration too, and dark mode selects the configuration's dark colors when it has some.

**`debugMode`: leave it unset.** In 2.0.0 to 2.0.2 the Android bridge calls the native `initialize` off the main thread. With `debugMode: true` the native SDK turns that into an error: `initialize()` rejects with `E_INIT` and the SDK is not initialized. Without it, Logcat shows one warning per launch under the tag `SirDataCMP`, starting with `[CMP] SirDataCMP.initialize() was called off the main thread`. It is a known issue of the Android bridge in 2.0.0 to 2.0.2, and there is nothing to change in your app. On iOS the bridge already calls `initialize` on the main thread, so the option has nothing to catch there either.

## 4. Show the consent UI

Let the SDK decide whether to show it, as `startConsent()` does above (*fragment*, inside an `async` function):

```ts
if (await SirDataCMP.shouldCollectConsent()) {
  await SirDataCMP.showConsentUI();
}
```

Do not test the TC string yourself: a partially answered consent must reopen the banner, an active capping window must keep it closed, and neither is visible from the string.

**`shouldCollectConsent()`** runs the same cascade as the web CMP: GDPR applicability, stored consent, cookie wall, the capping windows, and whether the stored choice still covers every active purpose and partner. It **waits for the publisher configuration** first, so you can call it right after `initialize()`. On a network that does not answer, that wait lasts until the HTTP timeouts expire (several tens of seconds), and the answer is then the provisional one: `true` when nothing is stored and GDPR seems to apply from the device. Do not hold a splash screen on it.

What it answers in the common cases:

* **first launch** in a GDPR country: `true`;
* **choice stored and still complete**: `false`. Depending on your configuration's capping setting, the banner can come back once its capping window has elapsed;
* **cookie wall** configured: `true` on every launch until purpose 1 is granted;
* **"Ask me later"** chosen in the banner, with nothing stored: `false` for one hour;
* **GDPR does not apply**, US users included: `false` (section 7).

**`showConsentUI()`** presents the native consent UI (banner, purposes, partners) in the workflow the cascade selects — a choice missing only at partner level opens it in "apply choices" mode, which asks only about what is left. On Android it opens the SDK's consent activity over yours; on iOS it presents the consent view full screen over React Native's topmost view controller, a presentation the user cannot swipe down.

The promise **resolves when the UI closes**, whatever closed it: a choice, the close cross, or the Android back button. The back button closes it without recording anything, so the banner comes back at the next decision. Re-read what you need after the promise resolves.

It rejects with:

| Code                  | Platform | Cause                                                |
| --------------------- | -------- | ---------------------------------------------------- |
| `E_NO_ACTIVITY`       | Android  | React Native has no current activity to present from |
| `E_ACTIVITY_TYPE`     | Android  | The current activity is not a `ComponentActivity`    |
| `E_SHOW`              | Android  | Launching the consent activity failed                |
| `E_NO_VIEWCONTROLLER` | iOS      | No view controller to present from                   |

`shouldCollectConsent()` rejects with `E_SHOULD_COLLECT` if the native decision fails.

**Known issue in 2.0.0 to 2.0.2, iOS.** If the configuration could not be loaded at this launch (no network, for example), the consent UI shows only a loading indicator, with no control to close it. `showConsentUI()` presents it full screen, where it cannot be swiped down either, so it stays on screen and its promise does not resolve until the app is restarted: the SDK does not retry the load during the launch. `startConsent()` runs into it on an offline first launch in a GDPR country, where `shouldCollectConsent()` answers `true`. On Android, the back button closes the consent UI. These versions give JavaScript no signal that the load failed: on iOS, do not call `showConsentUI()` when you know the device is offline. Fixed in the next release (unreleased): the consent UI shows a message and a Close button, which resolves the promise.

## 5. Read the consent

Every getter returns a promise, reads the same native storage the vendor SDKs read, and never throws for lack of a choice: before the first choice the TC string, the consent bit strings and the US Privacy string are empty. Under the US regime, the GPP string already carries the US section once the configuration has loaded (section 3).

```ts
import SirDataCMP from '@sirdata/react-native-cmp';

export async function readConsent(): Promise<{ device: boolean; google: boolean }> {
  const device = await SirDataCMP.hasPurposeConsent(1); // purpose 1: store or access information on a device
  const google = await SirDataCMP.hasVendorConsent(755); // 755 = Google in the Global Vendor List
  return { device, google };
}
```

| Getter                                           | Returns                                                                                                      |
| ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------ |
| `getTcString()`                                  | `IABTCF_TCString`                                                                                            |
| `getPurposeConsents()` / `getVendorConsents()`   | `IABTCF_PurposeConsents` / `IABTCF_VendorConsents`, bit strings where character *n* is purpose or vendor *n* |
| `hasPurposeConsent(id)` / `hasVendorConsent(id)` | Whether bit `id` (1-based) is set in those strings                                                           |
| `getGppString()`                                 | `IABGPP_HDR_GppString`                                                                                       |
| `getUspString()`                                 | `IABUSPrivacy_String`: empty until a US user saves the US screen                                             |
| `getAddtlConsent()`                              | `IABTCF_AddtlConsent`, Google's Additional Consent string                                                    |
| `isGdprApplies()`                                | The server's answer once the configuration has loaded; before, a guess from the device's country             |
| `isCcpaApplies()`                                | The same, for the US regime                                                                                  |

### React to changes

```ts
import SirDataCMP from '@sirdata/react-native-cmp';
import type { ConsentSnapshot, Subscription } from '@sirdata/react-native-cmp';

export function watchConsent(onChange: (s: ConsentSnapshot) => void): Subscription {
  let lastSeen: string | null = null; // one per subscription, never shared
  Promise.all([SirDataCMP.getTcString(), SirDataCMP.getGppString(), SirDataCMP.getUspString()])
    .then((v) => {
      if (lastSeen === null) lastSeen = v.join('|'); // start from the stored values
    })
    .catch(() => {}); // without them, the first event goes through: harmless
  return SirDataCMP.addConsentChangeListener((s) => {
    // Compare values: on Android the event also fires when nothing stored changed.
    const key = [s.tcString, s.gppString, s.uspString].join('|');
    if (key === lastSeen) return;
    lastSeen = key;
    onChange(s);
  });
}

// When you no longer need it:
// const sub = watchConsent(applyGating);
// sub.remove();
```

The event carries a snapshot of the stored values (`tcString`, `purposeConsents`, `vendorConsents`, `gppString`, `uspString`, `addtlConsent`, `gdprApplies`), but the two platforms do not fire it at the same moments. **Compare values rather than count events**:

|                 | Android                                                                                                                                              | iOS                                                                                                                                                                                               |
| --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Fires on        | Every update of the SDK's consent state: while the configuration loads, on each toggle inside the consent UI before the user saves, and on each save | Only when `IABTCF_TCString`, `IABUSPrivacy_String` or `IABGPP_HDR_GppString` actually changes in `UserDefaults`, which for a user under the US regime includes the configuration load (section 3) |
| US screen saved | Does not fire in 2.0.0 to 2.0.2: a US choice does not change the update counter the bridge observes. Fires from the next release                     | Fires                                                                                                                                                                                             |

Because of the last row, re-read the values you depend on when `showConsentUI()` resolves, rather than relying on the listener alone. Both platforms start emitting once `initialize()` has resolved.

**Known issue in 2.0.0 to 2.0.2, Android.** The events stop when the activity hosting React Native is destroyed while the JavaScript side keeps running: back on the root screen on Android 11 and lower, or a system language or font-size change. They start again only at the next `initialize()` call, which with `startConsent()` means a new process or a JavaScript reload. Re-read the values you depend on when the app becomes active again (`AppState` `'active'`), as well as when `showConsentUI()` resolves.

`addConsentUIClosedListener(() => …)` fires when a consent UI opened by `showConsentUI()` closes, at the moment its promise resolves. Both listeners return a subscription: call `remove()` on it when you are done.

## 6. Reopen from a settings screen

GDPR requires choices to be editable at any time. Give the user an entry point that calls `showConsentUI()` again:

```ts
import SirDataCMP from '@sirdata/react-native-cmp';

// e.g. the onPress of a "Manage cookies" button in your settings screen
export async function onManageConsentPress(): Promise<void> {
  await SirDataCMP.showConsentUI();
  // re-read and re-apply what you gate on consent here
}
```

From 2.0.2, `showConsentUI()` decides how to open as the web CMP does. When the consent UI is due, which is what `shouldCollectConsent()` answers `true` for, it opens in the workflow the display decision selects: that is what `startConsent()` needs. Otherwise (a complete choice is stored, a capping window is active, or GDPR does not apply) the opening comes from your app, so it opens in the native SDKs' reopen mode (`MANUAL_DISPLAY`), as the web's `displayUI()` does: a close cross at the top and a "Reject all" button whatever the refusal option your configuration sets, so the user can withdraw in one tap. The reopening restarts the capping window and counts as a revisit in Sirdata's statistics. Pass `{ reopen: true }` to open the reopen mode even when the UI is due, for example while a choice is only partly answered.

The close cross of the reopen mode changes no choice. Where the server applies the CNIL rules, France included, it records the choices on screen as it closes, as on the web: the TC string is then rewritten and the change listener can fire (section 5).

**Before 2.0.2**, `showConsentUI()` always opens in the workflow the display decision selects, so a settings entry reopens the banner as on a first visit, refusal button included, and that button does not always withdraw a stored choice. Which button it is depends on the refusal option your configuration sets in the Sirdata Console:

| Refusal option             | The reopened banner's refusal button, when a choice is stored                                                                 |
| -------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| Reject all and close       | "Reject all" rejects every purpose and vendor and records it: consent is withdrawn                                            |
| Continue without accepting | "Continue without accepting" records the choices on screen, which are the stored ones: an earlier "Accept all" stays recorded |
| Ask me later               | "Ask me later" closes the banner and records nothing                                                                          |
| Close button               | There is no refusal button. The close cross, if your configuration shows one, leaves the stored choice as it is               |

The option that applies is the one the server serves for the user's country, which can differ from the Console setting: in France, for example, "Ask me later" is served as "Continue without accepting". With 2.0.0 or 2.0.1, users can withdraw through **Customize my choices**, then **Reject all**, which records the refusal and closes the consent UI, or through an entry of yours that calls `rejectAll()`, which records it without showing any UI (section 11).

On iOS, if the configuration could not be loaded at this launch, the reopened UI cannot be closed (known issue in section 4).

## 7. US users

GDPR does not apply to US users, so `shouldCollectConsent()` answers `false` and no banner appears on its own. When the US regime applies, `showConsentUI()` presents the **US screen** instead of the TCF banner: the user can opt out, and saving it writes `IABUSPrivacy_String` and the US sections of the GPP string.

```ts
import SirDataCMP from '@sirdata/react-native-cmp';

// After startConsent() has resolved: the server's answer if the configuration loaded,
// otherwise a guess from the device's country.
export async function showsUsPrivacyEntry(): Promise<boolean> {
  return SirDataCMP.isCcpaApplies();
}
```

Offer US users an entry point in your settings, for example labeled "Your Privacy Choices", that calls `showConsentUI()`. The server decides the regime from the country and US state it locates the connection in, once the configuration has loaded; before that, `isCcpaApplies()` is a guess from the device's country. There is no `setCcpaOptOut` in JavaScript: the opt-out goes through the US screen. In 2.0.0 to 2.0.2, saving it does not fire `addConsentChangeListener` on Android (section 5): re-read `getUspString()` and `getGppString()` when `showConsentUI()` resolves. From the next release it fires on both platforms, and the snapshot carries `uspString` and `gppString`.

## 8. Vendor SDKs

Most vendor SDKs in a React Native app are native SDKs wrapped by a plugin, and they read consent from native storage as they would in a native app. There is nothing to pass from JavaScript, and the rules on [Sharing consent with vendor SDKs](/en/mobile-apps-native-sdk/mobile-sdk/sharing-consent.md) apply unchanged:

* IAB-compliant SDKs (Google Mobile Ads and others) need nothing;
* the consent adapters (Firebase Analytics, AppsFlyer, Adjust, Branch) look for the vendor's native classes at run time, as in a native app, so they apply to a vendor SDK that a React Native plugin links into your app;
* AppsFlyer still has to be told to read the IAB keys: use the option its React Native plugin exposes for that (see the plugin's documentation);
* SDKs that do not read the IAB keys, such as Meta's, are yours to gate: start them from JavaScript on `hasPurposeConsent(...)` / `hasVendorConsent(...)`, and re-evaluate on changes (section 5).

Collect consent before you load the first ad.

## 9. Test and verify

Use your own configuration for testing. If you start with the example IDs of these pages, switch to your own before release, and clear the app's data when you do (see the last point below).

To replay a first launch, uninstall the app, or on Android clear its data (replace `com.example.myapp` with your `applicationId`):

```bash
adb shell pm clear com.example.myapp
```

Check, on each platform:

1. **The native dependencies.** iOS: `pod install` installs `sirdata-cmp-react-native (2.0.2)` and `SirdataCMP` (2.0.0, or a later 2.x). Android:

   ```bash
   cd android && ./gradlew :app:dependencies --configuration releaseRuntimeClasspath | grep com.sirdata.cmp
   ```

   lists `android-sdk`, `shared` and `shared-android` at 2.0.0.
2. **First launch** (from a GDPR country): `shouldCollectConsent()` resolves `true` and the banner appears.
3. **After "Accept all"**: `getTcString()` is not empty and `hasPurposeConsent(1)` is `true`. For a user outside the US, `getUspString()` stays empty.
4. **Relaunch**: no banner.
5. **Settings entry**: once a choice is recorded, `showConsentUI()` opens the UI with a close cross at the top and a "Reject all" button, and resolves when it closes. After "Accept all", "Reject all" there leaves `hasPurposeConsent(1)` `false`.
6. **US screen**: from a connection the server places in a covered US state (California, for example), `isCcpaApplies()` is `true`, `showConsentUI()` shows the US screen, and after saving it `getUspString()` is no longer empty.
7. **Release build**: the banner still appears in a minified Android release build and in an iOS Release build.

A development-only helper:

```ts
import SirDataCMP from '@sirdata/react-native-cmp';

export async function logConsentState(): Promise<void> {
  if (!__DEV__) return; // consent describes a user's choices: keep it out of production logs
  const [initialized, gdpr, us, tc, gpp, usp] = await Promise.all([
    SirDataCMP.isInitialized(),
    SirDataCMP.isGdprApplies(),
    SirDataCMP.isCcpaApplies(),
    SirDataCMP.getTcString(),
    SirDataCMP.getGppString(),
    SirDataCMP.getUspString(),
  ]);
  console.log({ initialized, gdpr, us, tc, gpp, usp });
}
```

To look at the raw keys on Android, in a debuggable build:

```bash
adb shell run-as com.example.myapp cat shared_prefs/com.example.myapp_preferences.xml
```

`IABTCF_CmpSdkID` is `92` as soon as `initialize()` has resolved.

On Android, Logcat shows the SDK's own lines under the tags `SirDataCMP` and `SirDataCMPManager`, including `Config load failed: …` when the configuration cannot be loaded. On iOS, the Xcode console shows lines starting with `[CMP]`. See [Logging](/en/mobile-apps-native-sdk/mobile-sdk/logging.md).

**Switching partner or config ID** needs an app restart (section 3), and stored consent belongs to the configuration that collected it: clear the app's data or reinstall when you switch. There is no `clearConsent()` in JavaScript.

## 10. Release builds

* **Android minification.** Nothing to add: the SDK's R8 rules ship inside it (consumer rules) and apply to your minified release build, the consent adapters included. `-keep class com.sirdata.cmp.**` rules copied from older instructions are not needed.
* **`debugMode`.** Leave it unset (section 3).
* **Apple privacy manifest.** Neither the `SirdataCMP` pod 2.0.0 nor this package (2.0.0 to 2.0.2) ships a privacy manifest (`PrivacyInfo.xcprivacy`). The SDK stores consent in `UserDefaults`, which Apple lists as a required-reason API (category `NSPrivacyAccessedAPICategoryUserDefaults`, checked on developer.apple.com on 2026-10-05). In an Expo SDK 57 app, the manifest that prebuild generates already declares this category, with the reason `CA92.1`. Apple explains who declares what in [Describing use of required reason API](https://developer.apple.com/documentation/bundleresources/describing-use-of-required-reason-api): review it for your app before you submit.
* **Attribution notices.** The package carries `LICENSE`, `NOTICE-android`, `NOTICE-ios` and `THIRD-PARTY-LICENSES/` (in `node_modules/@sirdata/react-native-cmp/`). The Android and iOS builds embed different third-party components, hence two notices: both use components under the Apache License 2.0, the Android build also uses slf4j under the MIT licence, and the iOS build a component under the Boost Software License 1.0. If you distribute your app, make available to your users, for example in your open-source licences screen, the notice of each platform you ship (`NOTICE-android`, `NOTICE-ios`) and the licence texts in `THIRD-PARTY-LICENSES/`: it is required by section 26.1(b) of the `LICENSE`, and by the Apache License 2.0 (section 4) for the components it covers.
* **Licence.** Shipping your app requires a valid, current subscription to the Sirdata Consent Management Platform; one taken through the Sirdata Console is enough, and no signed agreement is required (section 25.1 of the `LICENSE`). Without a subscription, the licence is a 90-day evaluation grant: internal evaluation, development, testing and integration only, with no distribution to third parties or app stores (section 1.4). Subscriptions: [sirdata.com](https://sirdata.com); licence questions: <legal@sirdata.com>.

## 11. API reference

Import the default export. The package also exports the `SirDataCMPInitOptions`, `ShowConsentUIOptions` (from 2.0.2), `ConsentSnapshot` and `Subscription` types.

```ts
import SirDataCMP from '@sirdata/react-native-cmp';
```

Every method except the two listeners returns a promise. A rejected promise carries one of the codes below in its `code` property.

### Lifecycle

```ts
initialize(options: SirDataCMPInitOptions): Promise<void>;
```

Initializes the native SDK (section 3). Rejects with `E_INIT` when `partnerId` or `configId` is missing, and on Android when the native `initialize` throws, which includes `debugMode: true` in 2.0.0 to 2.0.2. On iOS the bridge avoids the two cases where the native call throws (already initialized, off the main thread). When the SDK is already initialized, it resolves without re-initializing.

```ts
interface SirDataCMPInitOptions {
  partnerId: number;
  configId: string;
  language?: string; // no effect in 2.0.0 to 2.0.2
  apiRoot?: string;
  cacheApiRoot?: string;
  consentApiRoot?: string;
  debugMode?: boolean; // do not set on Android in 2.0.0 to 2.0.2
  gdprDefaultApplies?: boolean;
}
```

```ts
isInitialized(): Promise<boolean>;
```

`true` once the native `initialize` has run. It does not mean the configuration has loaded: `shouldCollectConsent()` waits for that.

### Display

```ts
shouldCollectConsent(): Promise<boolean>;
```

Waits for the publisher configuration, then answers whether the consent UI must be shown now (section 4). Rejects with `E_SHOULD_COLLECT`.

```ts
showConsentUI(options?: ShowConsentUIOptions): Promise<void>;

interface ShowConsentUIOptions {
  reopen?: boolean; // always the reopen mode (section 6). Default false
}
```

Presents the consent UI and resolves when it closes: when the UI is due, in the workflow the cascade selects (section 4); otherwise, and always with `{ reopen: true }`, in the reopen mode for a settings screen (section 6). Both from 2.0.2: before, always in the workflow the cascade selects. Rejects with `E_NO_ACTIVITY`, `E_ACTIVITY_TYPE`, `E_SHOW` (Android) or `E_NO_VIEWCONTROLLER` (iOS). Await `initialize()` first.

### Programmatic choices

```ts
acceptAll(): Promise<void>;
rejectAll(): Promise<void>;
```

Accept or reject every purpose, vendor and feature, and record the choice — for a consent screen of your own. A choice is made on the vendor lists the configuration brings, so in 2.0.0 to 2.0.2, call them after `shouldCollectConsent()` has resolved. Called while the configuration is still loading, or after it failed to load, they record a valid but empty TC string, with no purpose and no vendor, and resolve: **Accept all** is then stored as a refusal. They reject with `E_ACCEPT_ALL` / `E_REJECT_ALL` only when the native call throws (on Android, for example, before `initialize()`; on iOS that case crashes the app, and rejects in the next release). A submission the native SDK reports as failed still resolves. To be sure a choice was recorded, read it back: `getTcString()` is not empty, and after `acceptAll()`, `getPurposeConsents()` contains a `1`.

From the next release, they wait for the configuration while it is loading, then record the choice on the real lists. They reject, recording nothing, when it could not be loaded, and whenever the native SDK reports the submission as failed.

### Getters

```ts
getTcString(): Promise<string>;
getPurposeConsents(): Promise<string>;
getVendorConsents(): Promise<string>;
getGppString(): Promise<string>;
getUspString(): Promise<string>;
getAddtlConsent(): Promise<string>;
hasPurposeConsent(purposeId: number): Promise<boolean>;
hasVendorConsent(vendorId: number): Promise<boolean>;
isGdprApplies(): Promise<boolean>;
isCcpaApplies(): Promise<boolean>;
```

See the table in section 5. On Android they reject with `E_CMP` if the native call throws; on iOS they never reject.

### Events

```ts
addConsentChangeListener(listener: (snapshot: ConsentSnapshot) => void): Subscription;
addConsentUIClosedListener(listener: () => void): Subscription;

interface ConsentSnapshot {
  tcString: string;
  purposeConsents: string;
  vendorConsents: string;
  gppString: string;
  uspString: string;
  addtlConsent: string;
  gdprApplies: boolean;
}

interface Subscription {
  remove(): void;
}
```

See section 5 for when each event fires on each platform.

### Not available from JavaScript in 2.0.0 to 2.0.2

| Native feature                                                                                                                              | Instead                                                                                   |
| ------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
| The display decision's workflow and reason (`awaitConsentDisplayDecision`)                                                                  | `shouldCollectConsent()` gives the yes or no; `showConsentUI()` picks the workflow itself |
| `saveConsent()`, `submitConsent()`, `recordAskLater()`                                                                                      | The consent UI's own buttons, or `acceptAll()` / `rejectAll()`                            |
| `setCcpaOptOut()`, `isCcpaOptedOut()`                                                                                                       | The US screen through `showConsentUI()`, then `getUspString()` (section 7)                |
| `clearConsent()`, `shutdown()`                                                                                                              | Clear the app's data or reinstall; restart the app to change IDs (section 9)              |
| The TCF, GPP and USP command APIs (`tcfApi()`, `gppApi()`, `uspApi()`)                                                                      | Vendor SDKs read native storage; use the getters for your own logic                       |
| Google Consent Mode state and the consent adapters (`getGoogleConsentState()`, `getLastForwardedConsentState()`, `getRegisteredAdapters()`) | The adapters run natively without any call (section 8)                                    |
| Initialization state and errors (`getConsentState()`, `getInitError()`, `getLastTcStringError()`)                                           | Android: Logcat, tag `SirDataCMPManager`. iOS: not observable from JavaScript             |
| Android `certificatePins`                                                                                                                   | Not configurable from JavaScript                                                          |

## 12. Troubleshooting

| Symptom                                                                                                                                                                                                                                                                    | Cause and fix                                                                                                                                                                                                                                                                                                                                                      |
| -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `The package '@sirdata/react-native-cmp' doesn't seem to be linked` on any call. On iOS also an `Invariant Violation` (`` `new NativeEventEmitter()` requires a non-null argument ``) when you subscribe to an event; on Android the subscription succeeds but never fires | The native module is not in the running app: it is Expo Go, or the app was not rebuilt after installing the package. Use a development build, rebuilt after installing the package: with Expo, `npx expo run:ios` or `npx expo run:android` (section 2); otherwise run `pod install` on iOS, then rebuild the app                                                  |
| `pod install`: "required a higher minimum deployment target"                                                                                                                                                                                                               | Your `Podfile`'s `platform :ios` is below 15.0. Raise it to at least `'15.0'`                                                                                                                                                                                                                                                                                      |
| `pod install`: "Unable to find a specification" or "could not find compatible versions" for `SirdataCMP`                                                                                                                                                                   | Your local CocoaPods specs are out of date: run `pod install --repo-update`. If it persists, delete `ios/Pods` and `ios/Podfile.lock`, then run `npx pod-install`                                                                                                                                                                                                  |
| Metro: `Unable to resolve "react-native"` from `…/react-native-cmp/src/index.js`                                                                                                                                                                                           | The package was installed from a local path (`file:` or `npm link`), which npm links as a symlink that Metro resolves outside your project. Install it from npm, or point Metro's `watchFolders` and `extraNodeModules` at your project's `react`, `react-native` and the package folder                                                                           |
| `initialize()` rejects with `E_INIT` and a "called off the main thread" message                                                                                                                                                                                            | `debugMode: true` on Android in 2.0.0 to 2.0.2. Remove the option (section 3)                                                                                                                                                                                                                                                                                      |
| Logcat warns `SirDataCMP.initialize() was called off the main thread`                                                                                                                                                                                                      | Expected with the Android bridge of 2.0.0 to 2.0.2 (section 3)                                                                                                                                                                                                                                                                                                     |
| iOS: the app terminates on `shouldCollectConsent()`, `acceptAll()` or `rejectAll()`, even after `initialize()` has resolved; the crash report shows `SirDataCmp.shouldCollectConsent` (or `acceptAll`, `rejectAll`) calling `Kotlin_ObjCExport_createContinuationArgument` | Version 2.0.0 of the package. Upgrade to 2.0.2 (section 2)                                                                                                                                                                                                                                                                                                         |
| The app crashes when the consent UI opens, or on `acceptAll()` / `rejectAll()` on iOS                                                                                                                                                                                      | Called before `initialize()` resolved. Await it first (section 3). Fixed on iOS in the next release                                                                                                                                                                                                                                                                |
| After `acceptAll()`, `getPurposeConsents()` contains no `1`                                                                                                                                                                                                                | In 2.0.0 to 2.0.2, `acceptAll()` called before the configuration has loaded records an empty choice: call it after `shouldCollectConsent()` has resolved (section 11). From the next release it waits for the configuration instead, and rejects with "Configuration not loaded: the choice was not recorded" when the configuration could not be loaded           |
| The consent UI appears twice                                                                                                                                                                                                                                               | Two concurrent `showConsentUI()` calls, often React Strict Mode running your startup effect twice. Share one promise (section 3)                                                                                                                                                                                                                                   |
| No banner for a US user                                                                                                                                                                                                                                                    | Expected: GDPR does not apply. Offer the US screen from your settings (section 7)                                                                                                                                                                                                                                                                                  |
| The settings entry shows the first-visit banner                                                                                                                                                                                                                            | In 2.0.0 and 2.0.1, expected: they have no reopen mode. Upgrade to 2.0.2 (section 6). With 2.0.2 the UI is due at that moment, a choice being missing or partly answered: pass `{ reopen: true }` to open the reopen mode anyway                                                                                                                                   |
| From the settings entry, "Continue without accepting" leaves an earlier "Accept all" recorded                                                                                                                                                                              | 2.0.0 and 2.0.1 reopen the first-visit banner, whose refusal button does not withdraw a stored choice unless the refusal option is "Reject all and close". Upgrade to 2.0.2: the settings entry then shows "Reject all". With 2.0.0 or 2.0.1, offer an entry that calls `rejectAll()`, or point users to **Customize my choices**, then **Reject all** (section 6) |
| Changing `partnerId` or `configId` has no effect                                                                                                                                                                                                                           | The SDK is already initialized in this process. Restart the app, and clear its data so consent from the old configuration is not reused (section 9)                                                                                                                                                                                                                |
| `showConsentUI()` rejects with `E_NO_ACTIVITY`                                                                                                                                                                                                                             | React Native had no current activity. Call it from a screen that is showing, not from a background task                                                                                                                                                                                                                                                            |
| iOS: the consent UI shows only a loading indicator that never goes away, and `showConsentUI()` never resolves                                                                                                                                                              | The configuration could not be loaded at this launch (known issue in 2.0.0 to 2.0.2, section 4). Restart the app with a network connection. Fixed in the next release                                                                                                                                                                                              |
| Android: `addConsentChangeListener` stops firing                                                                                                                                                                                                                           | The activity hosting React Native was destroyed while JavaScript kept running (known issue in 2.0.0 to 2.0.2, section 5). Re-read the values when the app becomes active                                                                                                                                                                                           |
| iOS 27: an Expo SDK 57 app built with Xcode 27 terminates at launch, before any JavaScript runs; the crash report shows `_UIApplicationEvaluateRuntimeIssueForNoSceneLifecycleAdoption`                                                                                    | Not caused by this package: iOS 27 stops an app that does not adopt the `UIScene` life cycle, and the app delegate that Expo SDK 57 generates does not adopt it. A blank Expo SDK 57 app, without this package, terminates the same way (checked on 2026-10-07 on an iOS 27 simulator), and the same build runs on iOS 26. Track Expo's support for iOS 27         |

For anything else, see [Troubleshooting](/en/mobile-apps-native-sdk/mobile-sdk/troubleshooting.md), and include the SDK version, the platform, your partner ID and config ID when you contact Sirdata.

## Next

* [Sharing consent with vendor SDKs](/en/mobile-apps-native-sdk/mobile-sdk/sharing-consent.md) — how ad SDKs read the consent you collect
* [Android setup](/en/mobile-apps-native-sdk/mobile-sdk/setup.md) and [iOS setup](/en/mobile-apps-native-sdk/mobile-sdk/setup-1.md) — the native SDKs underneath


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://cmp.docs.sirdata.net/en/mobile-apps-native-sdk/mobile-sdk/react-native-setup.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
