# React Native Consent UI (/docs/consent-management/mobile-consent/react-native-consent-ui)

## Transcend React Native UI SDK Guide [#transcend-react-native-ui-sdk-guide]

The **Transcend** **React Native UI SDK** provides a consent management banner API for iOS and Android apps. Import the package as `@transcend-io/cm-mobile-ui-react-native`. All Transcend API calls are accessed through the `useConsent` or `useConsentUI` hooks inside a `ConsentProvider`.

**Requirements:** Migration off of Legacy Banners and onto Consent Experiences, React Native 0.75+, React 18+, `react-native-svg`

<Callout type="info" title="Architecture note">
  ConsentProvider keeps only two pieces of React state on top of headless usage — consent and isReady. All other non-display SDK methods (setConsent, getSdkConsentStatus, etc.) are passthroughs to the native Transcend Headless SDK.
</Callout>

## Installation [#installation]

Install the package and its peer dependency:

```shell
yarn add @transcend-io/cm-mobile-ui-react-native react-native-svg
# or
npm install @transcend-io/cm-mobile-ui-react-native react-native-svg
```

### iOS [#ios]

Install pods after adding the package:

```shell
cd ios && pod install
```

The native iOS Headless SDK is pulled in automatically via Swift Package Manager.

### Android [#android]

No extra repository configuration is required. The native Android Headless SDK (`io.transcend.sdk.headless:headless`) is included as a transitive dependency. `INTERNET` and `ACCESS_NETWORK_STATE` permissions are declared automatically.

## Initialization [#initialization]

Wrap your app in `ConsentProvider` and pass a `config` object. The provider initializes the native SDK on mount and exposes readiness through `isReady`.

```tsx
import { ConsentProvider } from '@transcend-io/cm-mobile-ui-react-native';

export default function App() {
  return (
    <ConsentProvider
      config={{
        bundleId: 'YOUR_BUNDLE_ID', // Required — Transcend bundle ID
        mobileAppId: 'com.example.myapp', // Required — your app's mobile identifier from https://app.transcend.io/consent-manager/native/applications
        isTest: false, // Use test or production deployment
        cdnUrl: 'https://transcend-cdn.com', // Optional — defaults to Transcend's CDN
        token: 'AUTH_TOKEN', // Optional — auth token for backend sync
        settingsOverrides: {
          // Optional — override remote settings at init
          country: 'EU',
          regime: 'GDPR',
        },
      }}
      onError={(error) => {
        // Fires when SDK initialization fails
        console.error('Consent SDK init failed:', error);
      }}
    >
      <YourApp />
    </ConsentProvider>
  );
}
```

### Handling initialization errors [#handling-initialization-errors]

Pass an `onError` callback to `ConsentProvider` to be notified when initialization fails (e.g. invalid config, network error, or native SDK init rejection). When `onError` fires, `isReady` remains `false` and consent APIs should not be called.

```tsx
<ConsentProvider
  config={{ bundleId: '...', mobileAppId: '...' }}
  onError={(error) => {
    // Log, report to your error tracker, or show a fallback UI
    reportError('consent_init_failed', error);
  }}
>
  <YourApp />
</ConsentProvider>
```

`onError` is invoked once per failed init attempt, use it alongside the `isReady` check in child components. `isReady` tells you when it is safe to proceed; `onError` tells you why initialization did not succeed.

### `config` fields (`InitSdkOptions`) [#config-fields-initsdkoptions]

| Field               | Required | Description                                                            |
| ------------------- | -------- | ---------------------------------------------------------------------- |
| `bundleId`          | Yes      | Transcend bundle ID.                                                   |
| `mobileAppId`       | Yes      | Native app identifier registered in Transcend.                         |
| `isTest`            | No       | Use test deployment. Default: `false`.                                 |
| `cdnUrl`            | No       | CDN base URL. Default: `https://transcend-cdn.com`.                    |
| `token`             | No       | Auth token for backend sync. Required for sync to work.                |
| `settingsOverrides` | No       | Runtime overrides for remote settings (see Registering Overrides).     |
| `destroyOnClose`    | No       | Tear down the native SDK when the provider unmounts. Default: `false`. |

## Checking Readiness [#checking-readiness]

Use the `isReady` signal from `useConsent` to know when the SDK has finished loading settings and (optionally) completed its initial sync. Do not call consent APIs until `isReady` is `true`.

```tsx
import { useEffect } from 'react';
import { useConsent } from '@transcend-io/cm-mobile-ui-react-native';

function TrackerLoader() {
  const { isReady, getSdkConsentStatus } = useConsent();

  useEffect(() => {
    if (!isReady) return;

    void getSdkConsentStatus('analytics_sdk').then((status) => {
      if (status === 'ALLOW') {
        // Safe to initialize the analytics tracker
      }
    });
  }, [isReady, getSdkConsentStatus]);

  return null;
}
```

If initialization fails, `isReady` remains `false` and the `onError` callback passed to `ConsentProvider` is invoked with the error. Use `onError` to log or surface the failure; gate all consent and tracker logic on `isReady`.

## Displaying the Banner [#displaying-the-banner]

Once the SDK has been properly initialized and `isReady` is returning `true`, use the `autoShowConsentManager` and `showConsentManager` methods in the `useConsentUI` context in order to display the banner. Unlike web airgap.js implementations, mobile flows will not automatically display banners on initialization.

```tsx
const {
  showConsentManager, // Open the bundled consent UI
  hideConsentManager, // Close the bundled consent UI
  autoShowConsentManager, // Show UI if auto-prompt conditions are met
} = useConsentUI();

showConsentManager(); // Opens default experience
showConsentManager({ viewState: 'banner-into-modal' }); // Opens a specific experience
```

## Getting and Setting Consent [#getting-and-setting-consent]

### Get consent [#get-consent]

Read the current consent from the `consent` object exposed by `useConsent`. This is React state kept in sync after initialization, sync, and after `setConsent` calls.

```tsx
const { consent } = useConsent();

// consent.confirmed       — boolean
// consent.prompted        — boolean
// consent.purposes        — Record<string, boolean>
// consent.timestamp       — ISO 8601 string of when consent was given
```

### Set consent [#set-consent]

Pass a map of purpose names to boolean values. Syncs to the backend by default when a `token` was provided at init.

```tsx
const { setConsent } = useConsent();

const success = await setConsent(
  {
    Analytics: true,
    Functional: true,
    Advertising: false,
  },
  {
    autoSync: true, // Sync to backend after setting (default: true)
    confirmed: true, // Mark consent as confirmed (default: true)
    timestamp: undefined, // ISO Timestamp associated with the consent change (default: now)
    updated: true, // Mark consent as updated (default: true)
  }
);
```

After `setConsent` resolves, the `consent` object in context is refreshed automatically.

## Registering Overrides [#registering-overrides]

Pass overrides in `config.settingsOverrides` at initialization to test specific regimes, disable sync, or force configuration values.

```tsx
<ConsentProvider
  config={{
    bundleId: 'YOUR_BUNDLE_ID',
    mobileAppId: 'com.example.myapp',
    settingsOverrides: {
      regime: 'GDPR',
    },
  }}
>
  <YourApp />
</ConsentProvider>
```

Common override keys include: `"partition"`, `"regime"`, `"defaultRegime"`.

## Syncing Consent [#syncing-consent]

**Backend sync requires an auth token.** Pass `token` in `config` at init, otherwise sync is skipped and consent is not pushed to or pulled from the backend. Generate a token on your backend server following the [Transcend Preference Store docs](../../preference-management/storing-consent-preferences.mdx).

```tsx
<ConsentProvider
  config={{
    bundleId: 'YOUR_BUNDLE_ID',
    mobileAppId: 'com.example.myapp',
    token: 'AUTH_TOKEN', // Required for sync
  }}
>
  <YourApp />
</ConsentProvider>
```

The native SDK syncs consent automatically in two cases:

1. **During initialization** using the `token` from `config`.
2. **After** **`setConsent`** when `autoSync` is `true` (the default).

There is no separate public `sync()` method. To trigger a sync, call `setConsent` with `autoSync: true` and ensure a valid `token` was provided at init.

Sync is also skipped when `SYNC_DISABLED` is `true` in an override.

After a successful sync during init, `isReady` becomes `true` and `consent` reflects any remote changes.

## SDK Consent Status [#sdk-consent-status]

Use these APIs to determine whether a third-party SDK (identified by a service ID) is allowed to run given the current consent state. Gate tracker initialization on `isReady`, then check status before loading each SDK.

### Recommended pattern [#recommended-pattern]

```tsx
import { useEffect } from 'react';
import { useConsent } from '@transcend-io/cm-mobile-ui-react-native';

function AnalyticsTracker() {
  const { isReady, getSdkConsentStatus } = useConsent();

  useEffect(() => {
    if (!isReady) return;

    void getSdkConsentStatus('analytics_sdk').then((status) => {
      if (status === 'ALLOW') {
        // Initialize analytics SDK
      }
    });
  }, [isReady, getSdkConsentStatus]);

  return null;
}
```

Re-run tracker checks whenever `isReady` transitions to `true` (e.g. after init) or when `consent` changes and you need to re-evaluate SDK permissions.

### Single service [#single-service]

```tsx
const { getSdkConsentStatus } = useConsent();

const status = await getSdkConsentStatus('analytics_sdk');

switch (status) {
  case 'ALLOW':
    // SDK may run
    break;
  case 'BLOCK':
    // User denied required purpose(s)
    break;
  case 'NO_SDK_FOUND':
    // Service ID not in purpose map
    break;
  case 'INTERNAL_ERROR':
    // Error occurred
    break;
}
```

### Batch lookup [#batch-lookup]

Pass an array of service IDs to evaluate multiple SDKs at once:

```tsx
const { batchGetSdkConsentStatuses } = useConsent();

const statusMap = await batchGetSdkConsentStatuses([
  'analytics_sdk',
  'ads_sdk',
]);

// e.g. { analytics_sdk: 'ALLOW', ads_sdk: 'BLOCK' }
```

### `ConsentStatus` values [#consentstatus-values]

| Status           | Meaning                                                                              |
| ---------------- | ------------------------------------------------------------------------------------ |
| `ALLOW`          | All required purposes for this SDK are granted (or SDK is Essential).                |
| `BLOCK`          | At least one required purpose was explicitly denied.                                 |
| `NO_SDK_FOUND`   | The service ID is not in the SDK purpose map.                                        |
| `INTERNAL_ERROR` | Consent resolution encountered an error while reconciling the service ID's purposes. |

The SDK purpose map is loaded from settings and cached by the native SDK (refreshed every 3 hours).

## Direct Headless Usage [#direct-headless-usage]

For direct native access without the React context (advanced usage), import `NativeConsent` from the package. This bypasses the `consent` / `isReady` React state and should only be used when you want to manage headless state yourself.

## Error Handling [#error-handling]

| Scenario                               | Behavior                                                                           |
| -------------------------------------- | ---------------------------------------------------------------------------------- |
| Missing `bundleId` or `mobileAppId`    | `onError` invoked; `isReady` stays `false`.                                        |
| Native init failure                    | `onError` invoked with the error; also logged to console; `isReady` stays `false`. |
| API called before ready                | Native SDK may throw; always gate on `isReady` first.                              |
| `useConsent` outside `ConsentProvider` | Throws: `"useConsent must be used inside ConsentProvider"`.                        |
