# Capacitor

Source: https://docs.ezoic.com/docs/mobileapps/capacitor/


The Ezoic Capacitor SDK lets you display banner, native, outstream video, instream video, rewarded, and interstitial ads in Capacitor apps (including Ionic apps built with Angular, React, Vue, or plain web code) with Prebid header bidding, Google Ad Manager, remote configuration, consent handling, and pageview tracking, all from a single JavaScript API.

The package is a Capacitor plugin over Ezoic's native Android and iOS ad stacks, so ad loading and auctions run natively while you write only web code. This guide is self-contained: it covers everything you need to ship on both platforms.

## Requirements

- Capacitor 7 or 8
- Node.js 20 or higher
- For Android apps: Android SDK 24 or higher, Android Gradle Plugin 8.0 or higher, JDK 21 (required by Capacitor's Android library)
- For iOS apps: iOS 15.0 or higher, Xcode 26.0 or higher, CocoaPods 1.12 or higher
- A Google Mobile Ads application ID provided by Ezoic
- A domain configured for your site in Ezoic

Authentication is handled by the app's bundle/package identifier plus the configured domain. There is no client-side API key.

## Installation

Install the package and sync the native projects:

```sh
npm install @ezoic/capacitor-sdk@1.13.1
npx cap sync
```

The package ships the native Android and iOS Ezoic SDKs as transitive dependencies and registers itself through Capacitor's plugin discovery, so you do not add the native ad SDKs yourself.


The current Capacitor SDK version is 1.13.1, which uses native SDK 1.13.1.


### iOS pods

The native `EzoicAdsSDK` ships as a binary Swift framework that depends on `PrebidMobile` (a Swift source pod), which requires static linkage. The plugin's pod declares `static_framework = true`, so this works with the `use_frameworks!` line Capacitor already puts in `ios/App/Podfile`; you do not need to change it. `npx cap sync` runs `pod install` for you and resolves the native `EzoicAdsSDK` framework along with its Prebid Mobile and Google Mobile Ads dependencies.

If your project uses Swift Package Manager instead of CocoaPods (`npx cap add ios --packagemanager SPM`), the plugin's `Package.swift` declares the same dependencies and Capacitor adds it to your app's package automatically.

## Android Setup

The package's Gradle module declares the Ezoic Android SDK dependency automatically. Your app project must be able to resolve it, which standard Capacitor apps already do because they include Google's Maven repository and Maven Central.

If your `android/build.gradle` customizes repositories, confirm both are present:

```groovy
repositories {
    google()
    mavenCentral()
}
```

The plugin is written in Kotlin and declares its own Kotlin Gradle plugin, so no other changes to your app's Gradle files are needed.

### Google Mobile Ads application ID

Add your Google Mobile Ads application ID to `android/app/src/main/AndroidManifest.xml`. This ID is provided by Ezoic and can be found in your Ezoic dashboard. Use the value assigned to your app unless your Ezoic representative gives you a different one.

```xml
<manifest>
    <application>
        <meta-data
            android:name="com.google.android.gms.ads.APPLICATION_ID"
            android:value="ca-app-pub-XXXXXXXXXXXXXXXX~XXXXXXXXXX" />
    </application>
</manifest>
```

### Advertising ID permission

If your app targets Android 12 (API 31) or higher and uses the advertising ID, add the permission to `AndroidManifest.xml`:

```xml
<uses-permission android:name="com.google.android.gms.permission.AD_ID" />
```

## iOS Setup

### Google Mobile Ads application ID

Add your Google Mobile Ads application ID to `ios/App/App/Info.plist`. This ID is provided by Ezoic and can be found in your Ezoic dashboard. Use the value assigned to your app unless your Ezoic representative gives you a different one.

```xml
<key>GADApplicationIdentifier</key>
<string>ca-app-pub-XXXXXXXXXXXXXXXX~XXXXXXXXXX</string>
```


Also set `GADIsAdManagerApp` to `true` in `Info.plist` so Google Mobile Ads runs in Ad Manager mode.


### App Tracking Transparency

ATT has the single biggest impact on fill and CPM. Without it the IDFA is unavailable and much of programmatic demand will not bid or bids far lower.

Add the tracking usage description to `ios/App/App/Info.plist`. Apple requires this string in the app's `Info.plist` (it appears in the App Store privacy label and system prompt) — it cannot come from the SDK.

```xml
<key>NSUserTrackingUsageDescription</key>
<string>This identifier will be used to deliver personalized ads to you.</string>
```

**The SDK presents the ATT prompt for you on iOS.** When you call `EzoicAds.initialize`, the native iOS SDK shows the prompt (if undetermined) and waits for the decision before starting the ad stack, so the IDFA (if granted) is attached to the first ad request. You only need to add the `NSUserTrackingUsageDescription` string above.

```ts
import { EzoicAds } from '@ezoic/capacitor-sdk';

// The SDK requests ATT (if undetermined) before loading ads on iOS.
await EzoicAds.initialize({ domain: 'example.com' });
```

To drive ATT yourself instead (for a pre-prompt or custom timing), pass `requestATTBeforeAds: false` and request authorization before initializing — for example with the `capacitor-plugin-app-tracking-transparency` community plugin:

```ts
import { AppTrackingTransparency } from 'capacitor-plugin-app-tracking-transparency';
import { EzoicAds } from '@ezoic/capacitor-sdk';

await AppTrackingTransparency.requestPermission();
await EzoicAds.initialize({ domain: 'example.com', requestATTBeforeAds: false });
```

### SKAdNetwork

`SKAdNetworkItems` lets buyers attribute installs when the IDFA is unavailable; missing identifiers suppress demand. Apple reads this key only from the app's main `Info.plist` — it is **not** aggregated from frameworks or SDKs.

Add the `SKAdNetworkItems` array with Google's published identifiers (`cstr6suwn9.skadnetwork` plus participating third-party buyers). Copy the current, complete list from Google's [Prepare privacy strategies](https://developers.google.com/ad-manager/mobile-ads-sdk/ios/privacy/strategies#enable-skadnetwork-to-track-conversions) page; identifiers must be lowercase.

```xml
<key>SKAdNetworkItems</key>
<array>
    <dict>
        <key>SKAdNetworkIdentifier</key>
        <string>cstr6suwn9.skadnetwork</string>
    </dict>
    <!-- … plus the remaining identifiers from Google's list … -->
</array>
```

## app-ads.txt

For both platforms, host an `app-ads.txt` file at the root of the developer website listed on your app's store page (for example, `https://example.com/app-ads.txt`). It authorizes the buyers that may sell your inventory; a missing or incomplete file causes most programmatic demand to be filtered out. Ezoic provides the required entries — confirm the file is published and current.

## Initialize the SDK

Initialize Ezoic once, early in your app lifecycle, before loading any ads.

```ts
import { EzoicAds } from '@ezoic/capacitor-sdk';

await EzoicAds.initialize({
    domain: 'example.com',
    debugEnabled: false,
    testMode: false
});
```

`domain` must match the domain configured for your site in Ezoic. Set `debugEnabled: true` during development to surface verbose native logs in Logcat (Android) and the Xcode console (iOS). Use `testMode: true` only while integrating; remove it for production traffic.

`initialize` resolves once the native SDK has finished bootstrapping. On the web (for example `ionic serve` in a browser) every call in this SDK resolves as a no-op and ad loads report an error, so you can keep the same code path for browser development.

`initialize` accepts these fields:

| Field | Default | Description |
| --- | --- | --- |
| `domain` | (required) | Your Ezoic domain |
| `autoReadConsent` | `true` | Read `IABTCF_*` / `IABGPP_*` consent keys written by a CMP |
| `subjectToCOPPA` | `false` | Treat the user as subject to COPPA |
| `requestATTBeforeAds` | `true` | iOS only: request App Tracking Transparency before the first ad |
| `debugEnabled` | `false` | Verbose native logging |
| `testMode` | `false` | Ezoic $0.00 test ads on debug builds and simulators. Disable before release |
| `autoTrackPageviews` | `true` | Record a pageview automatically on native screen changes. See [Pageview Tracking](#pageview-tracking) |
| `cmpEnabled` | `true` | Enable the built-in TCF CMP. Set `false` if you run your own CMP. See [Privacy and Consent](#privacy-and-consent) |
| `autoPresentConsent` | `true` | Present the consent dialog (if required) right after `initialize` resolves |

## How Ad Views Are Displayed

Capacitor renders your app in a WebView, and native ad views cannot be placed inside the DOM. The SDK draws banner, native, and outstream ads on a transparent native layer **over** the WebView; taps outside an ad fall through to your page. Where a view appears is its **placement**:

| Placement | Description |
| --- | --- |
| `{ position: 'bottom', margin?, width?, height? }` | Anchored to the bottom safe-area edge, horizontally centered. The default |
| `{ position: 'top', margin?, width?, height? }` | Same, anchored to the top safe-area edge |
| `{ position: 'inline', frame: { x, y, width, height } }` | An exact frame in CSS pixels, relative to the viewport |

Banners size themselves to the filled creative. Outstream and native views default to full width and a height of 250 and 300 CSS pixels respectively; pass `width` and `height` to change that. Every view has `setPlacement(placement)` to move it, and `hide()` / `show()` to toggle it (for example while a modal is open).

For an ad that belongs inside your page content, place an empty element where the ad should appear and call `attachTo(element)`. The native view follows the element's bounding box through scrolling, resizing, and layout changes. With `autoHeight` (the default) the SDK also sets the element's `height` style from the creative size, so your layout reflows around the filled ad and collapses when there is no fill. Give the element a width in your CSS.


Because ads are drawn above the page, anything you render over the same spot (modals, drawers, sticky headers, Ionic overlays) appears **behind** the ad. Call `hide()` while such UI is open, or `destroy()` the view when its screen goes away. Ad views are not tied to a page's lifecycle: create them when a screen appears and destroy them when it leaves.


## Add a Banner Ad

Create an `EzoicBannerAd`, register listeners, and load it. This example anchors the banner to the bottom of the screen; it occupies no space until an ad fills.

```ts
import { EzoicBannerAd } from '@ezoic/capacitor-sdk';

const banner = await EzoicBannerAd.create({
    adUnitIdentifier: '12345',
    size: '320x50,300x250',
    collapseOnNoFill: true,
    placement: { position: 'bottom' }
});

banner.setListeners({
    onLoad: () => console.log('Ezoic banner loaded'),
    onError: (error) => console.warn('Ezoic banner failed', error),
    onSizeChange: ({ width, height }) => console.log('Ezoic banner size', width, height)
});

await banner.load();

// When the screen goes away:
await banner.destroy();
```

To place the banner inside your content instead, attach it to an element:

```html
<div id="ad-slot" style="width: 300px; margin: 16px auto;"></div>
```

```ts
const banner = await EzoicBannerAd.create({ adUnitIdentifier: '12345', size: '300x250' });
banner.attachTo(document.getElementById('ad-slot')!);
await banner.load();
```

Replace `12345` with your Ezoic ad unit identifier. The native SDKs fetch the Google Ad Manager ad unit, Prebid configuration, targeting values, supported sizes, and refresh interval from Ezoic servers, so you do not configure those in the app.

When no ad fills, the banner collapses (`collapseOnNoFill`, default `true`): the native view shrinks to zero height and, for an attached element, the element's height is set to `0`. If a refresh does not fill, the previous ad stays visible. `onSizeChange` reports the displayed size, or `{ width: 0, height: 0 }` when collapsed.


The native Ezoic ad unit identifier is numeric. You can pass it as a string or a number, for example `"12345"` or `12345`.


## Banner Sizes

Pass `size` as a `widthxheight` string. Common sizes are:

- `"320x50"`: Banner
- `"320x100"`: Large Banner
- `"300x250"`: Medium Rectangle
- `"468x60"`: Full Banner
- `"728x90"`: Leaderboard

You can also pass a comma-separated list to let the auction choose among several sizes, for example `size: '300x250,320x50'`. When you attach the banner to an element, give the element a width that can hold the largest size you request.

## Banner Events

`setListeners` on a banner accepts these callbacks, all optional:

- `onLoad`: the banner received an ad
- `onError`: the banner failed to load; receives an error object with `message` and `code`
- `onSizeChange`: the displayed size changed; receives `{ width, height }`
- `onImpression`: an impression was recorded
- `onClick`: the user tapped the ad
- `onOpen`: the ad opened a full-screen overlay
- `onClose`: the full-screen overlay was dismissed

## Add a Native Ad

Native ads deliver ad assets (headline, icon, media, body text, and call to action) rendered in a template designed to match the look and feel of your app content, rather than in a fixed banner or full-screen format. `EzoicNativeAd` works like `EzoicBannerAd`: create it, attach or place it, and load it.

```ts
import { EzoicNativeAd } from '@ezoic/capacitor-sdk';

const nativeAd = await EzoicNativeAd.create({ adUnitIdentifier: '12345' });

nativeAd.setListeners({
    onLoad: () => console.log('Ezoic native ad loaded'),
    onError: (error) => console.warn('Ezoic native ad failed', error),
    onImpression: () => console.log('Ezoic native ad impression'),
    onClick: () => console.log('Ezoic native ad clicked'),
    onOpen: () => console.log('Ezoic native ad opened an overlay'),
    onClose: () => console.log('Ezoic native ad overlay closed')
});

// Size the slot in CSS (for example width: 100%; height: 320px) and attach.
nativeAd.attachTo(document.getElementById('native-slot')!);
await nativeAd.load();
```

Replace `12345` with your Ezoic ad unit identifier. The native SDKs render the ad in a Google-built native ad template (`NativeAdView`) that lays out the headline, icon, media, body, and call-to-action for you. Unlike `EzoicBannerAd`, `EzoicNativeAd` has no `size` option; the template lays out its assets inside the bounds you give it through the placement `width` and `height` or the attached element's box. A native ad reports no `onSizeChange`, so when attaching one give the element its height in CSS.

`setListeners` on a native ad accepts `onLoad`, `onError`, `onImpression`, `onClick`, `onOpen`, and `onClose`.

## Add a Rewarded Ad

Rewarded ads are full-screen ads that grant an in-app reward when the user finishes watching. They have no placement: load one ahead of time (for example, at the start of a level), then present it at a natural break. `show()` resolves with the earned reward, or `null` if the user dismissed the ad early.

```ts
import { EzoicRewardedAd } from '@ezoic/capacitor-sdk';

async function runRewardedAd() {
    try {
        const ad = await EzoicRewardedAd.load('12345');

        // Optional: observe lifecycle events
        ad.setListeners({
            onDismissed: () => console.log('Rewarded ad closed'),
            onFailedToShow: (error) => console.warn('Show failed', error.message)
        });

        const reward = await ad.show();
        if (reward) {
            console.log(`Earned ${reward.amount} ${reward.type}`);
            grantReward(reward.amount);
        }

        // Rewarded ads are single-use — release the handle when done.
        await ad.destroy();
    } catch (error) {
        console.warn('Rewarded ad failed to load', error);
    }
}
```

Replace `12345` with your Ezoic ad unit identifier. Load and `show` are separate steps; calling `show()` before the ad has loaded rejects with an error. Load a new `EzoicRewardedAd` for each reward opportunity.

You can pass a reward name when you show the ad. The name appears in your reports.

```ts
const reward = await ad.show({ rewardName: 'extra life' });
```

The reward `type` and `amount` come from the reward configured on the Google Ad Manager rewarded ad unit. `setListeners` accepts `onShown`, `onFailedToShow`, `onImpression`, `onClicked`, `onUserEarnedReward`, and `onDismissed` — all optional.

## Add an Interstitial Ad

Interstitial ads are full-screen ads shown at natural transition points (for example, between levels or screens). Unlike rewarded ads, they grant no reward. Like rewarded ads, load one ahead of time, then present it at a natural break. `show()` resolves when the ad is dismissed, or rejects if it fails to present.

```ts
import { EzoicInterstitialAd } from '@ezoic/capacitor-sdk';

async function runInterstitialAd() {
    try {
        const ad = await EzoicInterstitialAd.load('12345');

        // Optional: observe lifecycle events
        ad.setListeners({
            onShown: () => console.log('Interstitial ad shown'),
            onFailedToShow: (error) => console.warn('Show failed', error.message)
        });

        await ad.show();
        console.log('Interstitial ad closed');
    } catch (error) {
        console.warn('Interstitial ad failed to load', error);
    }
}
```

Replace `12345` with your Ezoic ad unit identifier. Load and `show` are separate steps; calling `show()` before the ad has loaded rejects with an error. Interstitial ads are single-use and auto-destroy once dismissed — load a new `EzoicInterstitialAd` for each opportunity. Call `ad.destroy()` yourself only if you loaded an ad and never showed it.

`setListeners` accepts `onShown`, `onFailedToShow`, `onImpression`, `onClicked`, and `onDismissed` — all optional.

## Add an Outstream Video Ad

`EzoicOutstreamAd` loads and renders a self-contained outstream video ad. Like `EzoicNativeAd`, it has no `size` option: the native view lays the player out inside the placement (default 250 CSS pixels tall, full width) or the attached element's box.

```ts
import { EzoicOutstreamAd } from '@ezoic/capacitor-sdk';

const outstream = await EzoicOutstreamAd.create({
    adUnitIdentifier: '12345',
    placement: { position: 'top', margin: 8 }
});

outstream.setListeners({
    onLoad: () => console.log('Ezoic outstream ad loaded'),
    onError: (error) => console.warn('Ezoic outstream ad failed', error),
    onImpression: () => console.log('Ezoic outstream ad impression'),
    onClick: () => console.log('Ezoic outstream ad clicked'),
    onOpen: () => console.log('Ezoic outstream ad opened an overlay'),
    onClose: () => console.log('Ezoic outstream ad overlay closed'),
    onSizeChange: ({ width, height }) => console.log('Ezoic outstream size', width, height)
});

await outstream.load();
```

Replace `12345` with your Ezoic ad unit identifier. The native SDKs render the ad through Google Ad Manager at the size configured on the server.

`setListeners` on an outstream ad accepts the same callbacks as a banner: `onLoad`, `onError`, `onSizeChange`, `onImpression`, `onClick`, `onOpen`, and `onClose`. `EzoicOutstreamAd.create` also accepts `collapseOnNoFill` (default `true`) and collapses on no-fill the same way as `EzoicBannerAd`.

## Add an Instream Video Ad

`EzoicInstreamAd` is a view-less controller for instream (pre/mid/post-roll) video. Unlike the banner, native, and outstream views, it renders nothing: your app owns the video player and the Google IMA SDK, and its sole deliverable is a Google Ad Manager VAST ad-tag URL string you feed to your own IMA `AdsRequest`. Unlike rewarded and interstitial ads, a controller is multi-use — it is not auto-destroyed, so you `load()` it repeatedly and `destroy()` it yourself.

```ts
import { EzoicInstreamAd } from '@ezoic/capacitor-sdk';

async function runInstreamAd() {
    const instream = new EzoicInstreamAd('12345');

    try {
        const adTagUrl = await instream.load({ contentUrl: playingVideoUrl });
        adsLoader.requestAds({ adTagUrl });

        // On an IMA ad error, walk down the floor waterfall to the next tag.
        const next = await instream.getNextAdTagUrl(); // null once exhausted
        if (next) adsLoader.requestAds({ adTagUrl: next });

        // On the IMA STARTED event, fire the Ezoic impression pixel.
        await instream.reportImpression({ revenueUsd: 0.42 });
    } catch (error) {
        console.warn('Instream ad failed to load', error);
    } finally {
        await instream.destroy();
    }
}
```

Replace `12345` with your Ezoic ad unit identifier. `load({ contentUrl })` resolves with the tag URL, or rejects on no fill, an uninitialized SDK, or an overlapping load already in flight for this id; `contentUrl` is optional and, when supplied, is added to the tag for contextual targeting. `getNextAdTagUrl()` resolves to `null` once the waterfall is exhausted. `reportImpression({ revenueUsd })` records the Ezoic impression on the IMA `STARTED` event; `revenueUsd` is optional. Call `destroy()` when the ad unit is no longer needed — the controller otherwise stays alive and reusable across loads.

## Privacy and Consent

The native SDKs include an IAB TCF 2.4 consent management platform (CMP ID 299). It only acts for users in GDPR regions; elsewhere no dialog is shown and ads load as before.

The dialog is used only when both of these are true:

- Ezoic has enabled the consent dialog for your domain. This is a server-side setting.
- `cmpEnabled` is `true` in the `initialize` config. This is the default.


If your app already runs another CMP (for example UMP or OneTrust), you must set `cmpEnabled: false`. See [Using your own CMP](#using-your-own-cmp).


### Built-in consent dialog

The dialog is presented for you. Once `initialize` resolves, the SDK calls `presentConsentIfRequired()` once on your behalf (`autoPresentConsent: true`, the default). Outside GDPR regions, with `cmpEnabled: false`, when another CMP is present, when you called `setGDPRConsent` before `initialize`, or with `autoReadConsent: false`, nothing is shown.

You must also add a "Privacy settings" button or menu item that is always reachable and calls `presentConsentSettings()`. TCF policy requires users to be able to reopen the dialog and change or withdraw consent at any time.

```ts
document.getElementById('privacy-settings')!.addEventListener('click', () => {
    EzoicAds.presentConsentSettings();
});
```

`presentConsentSettings()` reopens the dialog with the user's stored choices in GDPR regions and resolves `notRequired` elsewhere, with `cmpEnabled: false`, or when another CMP is present.

### Choosing when the dialog appears

To control the timing or read the outcome, turn automatic presentation off and call `presentConsentIfRequired()` yourself, for example from your first screen:

```ts
import { EzoicAds } from '@ezoic/capacitor-sdk';

await EzoicAds.initialize({ domain: 'example.com', autoPresentConsent: false });

const outcome = await EzoicAds.presentConsentIfRequired();
switch (outcome.type) {
  case 'decided':
    console.log('User chose', outcome.decision); // 'acceptAll' | 'rejectAll' | 'custom'
    break;
  case 'failed':
    console.log('Consent UI failed', outcome.code, outcome.message);
    break;
  default:
    break; // 'notRequired' | 'alreadyDecided' | 'dismissed' | 'alreadyPresenting'
}
```

`presentConsentIfRequired()` can be called at any time, and repeat calls are harmless: you get `alreadyPresenting` while a dialog is in flight and `alreadyDecided` once a valid decision is stored. Called before initialization finishes, it waits for the init response. Present again whenever `isConsentRequired()` is `true` and no decision has been made (for example after `dismissed` or `failed`).

### Consent outcomes

`presentConsentIfRequired()` and `presentConsentSettings()` always resolve (never reject) with an `EzoicConsentOutcome`:

| `type` | When |
| --- | --- |
| `notRequired` | GDPR doesn't apply, the built-in CMP is disabled, another CMP owns consent, or consent is managed by the app (`setGDPRConsent`, or `autoReadConsent: false`) |
| `alreadyDecided` | A still-valid decision is stored; no dialog shown |
| `decided` | The user chose `decision` (`acceptAll`, `rejectAll` or `custom`); the choice is saved |
| `dismissed` | The dialog closed without a choice; ads stay gated for this session |
| `alreadyPresenting` | A consent dialog is already on screen or being prepared |
| `failed` | The dialog couldn't be shown. `code` and `message` come from the native error. `code: -1` with `message: 'No foreground Activity'` (on both platforms) means there was no foreground Activity or view controller: the native SDK was not called and ads stay gated, so call `presentConsentIfRequired()` again once a screen is showing |

Other consent calls:

- **`isConsentRequired()`** resolves `true` whenever GDPR applies and the built-in CMP is in charge (including after the user has decided), `false` otherwise, and `null` until the init request completes or when the server sent no consent information.
- **`resetConsent()`** deletes the stored decision so the dialog shows again. Ads are gated again until the user decides.

### How ad loads wait for consent

In GDPR regions, while the built-in CMP is in charge and no decision is stored, ad loads wait for the user's decision:

- While the consent dialog is loading or on screen, ad loads wait up to 5 minutes in total per dialog.
- While no dialog is in progress, the dialog is covered, or the app is in the background, ad loads wait up to 10 seconds.

If the wait runs out, the ad load fails with error code `5001` (`EzoicErrorCode.consentRequired`). If the native SDK can't show the dialog at all (for example a network error), it returns `failed` with the native error code and ads proceed without a TC string (limited ads). A `failed` outcome with `code: -1` is different: ads stay gated, wait up to 10 seconds, and then fail with `5001`.

### Handling error 5001

Consent is checked when an ad loads, so `5001` arrives as:

- `code` on the ad views' `onError` (banner, native, outstream).
- The result of `getEzoicErrorCode(error)` for rejected rewarded, interstitial, and instream `load()` promises. The rejection's `message` is the native one ("User consent is required to load ads.").

```ts
import {
  EzoicAds,
  EzoicBannerAd,
  EzoicErrorCode,
  EzoicRewardedAd,
  getEzoicErrorCode,
} from '@ezoic/capacitor-sdk';

const banner = await EzoicBannerAd.create({ adUnitIdentifier: '123456' });
banner.setListeners({
  onError: (e) => {
    if (e.code === EzoicErrorCode.consentRequired) {
      // The user hasn't decided yet; e.g. offer EzoicAds.presentConsentIfRequired().
    }
  },
});

try {
  const ad = await EzoicRewardedAd.load('123456');
  await ad.show();
} catch (e) {
  if (getEzoicErrorCode(e) === EzoicErrorCode.consentRequired) {
    await EzoicAds.presentConsentIfRequired();
  }
}
```

### Using your own CMP

Set `cmpEnabled: false`. The SDK then reads your CMP's `IABTCF_*` (TCF) and `IABGPP_*` (GPP) keys as before. The built-in CMP also stays out of the way automatically if it finds `IABTCF_CmpSdkID` set to another CMP's ID.

```ts
await EzoicAds.initialize({ domain: 'example.com', cmpEnabled: false });
```

Whichever CMP writes them, the native SDKs automatically read consent signals from platform storage — `UserDefaults` on iOS and `SharedPreferences` on Android:

- TCF v2 consent from `IABTCF_*` keys
- GPP consent from `IABGPP_*` keys
- US Privacy consent from the standard IAB US Privacy key


A CMP that runs inside the WebView (JavaScript only) writes its signals to web storage, which the native SDKs cannot read. Use a CMP with a native Capacitor plugin, or pass the signals yourself with `setGDPRConsent` / `setGPPConsent` as described below.


### Setting consent manually

`setGDPRConsent` and the built-in CMP are mutually exclusive. The value lasts for the current process only, so **call `setGDPRConsent` before `initialize` on every launch**, or set `cmpEnabled: false`. Otherwise each cold start begins with the built-in CMP in charge until your call lands, and it can gate ad loads, show its dialog, and write `IABTCF_*` keys. The SDK doesn't write your consent string to `IABTCF_*` keys for other SDKs, so your own CMP must do that.

```ts
await EzoicAds.setGDPRConsent(true, '<IAB TCF consent string>');
await EzoicAds.setGPPConsent('GPP_STRING', '7');
await EzoicAds.setSubjectToCOPPA(false);
await EzoicAds.initialize({ domain: 'example.com', cmpEnabled: false });
```

- `setGDPRConsent(applies, consentString)`: whether GDPR applies and the TCF consent string
- `setGPPConsent(gppString, sectionIds)`: the GPP string and applicable section IDs
- `setSubjectToCOPPA(value)`: whether the user is subject to COPPA

## Pageview Tracking

Ezoic reports app traffic per **screen**, the way it reports a website per URL. Apps have no URLs, so the SDK gives each screen one:

```
https://<your domain>/<bundle id>/<screen label>
```

That URL is what per-page reporting and placement optimization key on, so the screen label is what you will see in the Ezoic dashboard.

### Automatic tracking

The native SDKs track pageviews automatically, but they only see native screens. In a Capacitor app that is the single host Activity or view controller, so without labels every route in your web app lands in one bucket.

### Labeling screens

Call `trackPageview(screen)` when the user reaches a screen to give it a name. Hook it into your router's navigation event. With Vue Router:

```ts
import { EzoicAds } from '@ezoic/capacitor-sdk';

router.afterEach((to) => {
  EzoicAds.trackPageview(String(to.name ?? to.path));
});
```

With Angular:

```ts
import { NavigationEnd, Router } from '@angular/router';
import { filter } from 'rxjs/operators';
import { EzoicAds } from '@ezoic/capacitor-sdk';

router.events
  .pipe(filter((event): event is NavigationEnd => event instanceof NavigationEnd))
  .subscribe((event) => EzoicAds.trackPageview(event.urlAfterRedirects));
```

With React Router:

```tsx
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
import { EzoicAds } from '@ezoic/capacitor-sdk';

export function PageviewTracker() {
  const { pathname } = useLocation();
  useEffect(() => {
    EzoicAds.trackPageview(pathname);
  }, [pathname]);
  return null;
}
```

Labels are free text. Use `/` for hierarchy (`members/profile`); spaces become `-`, punctuation is dropped, and case is kept. The label is also attached to every ad request on that screen until the next pageview. A labeled pageview takes precedence over the automatic one for the same navigation, so there is no double counting. `trackPageview()` without a label records an unlabeled pageview.

### Manual tracking only

If you label every screen, turn off automatic tracking so pageviews come only from your calls:

```ts
await EzoicAds.initialize({ domain: 'example.com', autoTrackPageviews: false });
```

## Troubleshooting

### SDK Not Initializing

1. Confirm the configured `domain` matches your Ezoic dashboard.
2. Confirm the device or simulator has network access.
3. Enable `debugEnabled: true` and review native logs: Logcat for `EzoicAds` on Android, the Xcode console on iOS.
4. Confirm you are running on a device or simulator: in a browser every call is a no-op and ad loads report "Ezoic ads are only available on iOS and Android."
5. Run `npx cap sync` after installing the package so the native projects pick up the plugin.

### Ads Not Loading

1. Make sure `EzoicAds.initialize` runs and resolves before you create and load ad views.
2. Confirm the Ezoic ad unit identifier is configured in Ezoic.
3. Confirm the Google Mobile Ads application ID is present in `AndroidManifest.xml` (Android) and `Info.plist` (iOS).
4. When attaching a banner to an element, give the element a width large enough for the requested size.
5. Check consent configuration if your traffic is subject to privacy regulations. An error code of `5001` (`EzoicErrorCode.consentRequired`) means the built-in consent dialog is waiting for a decision. See [Handling error 5001](#handling-error-5001).

### Ads Cover Other UI

Ad views are drawn above the WebView. If a modal, drawer, or sticky header appears behind an ad, call `hide()` on the ad while that UI is open and `show()` afterwards, or `destroy()` the ad when its screen leaves.

### Build Failures

1. Android: confirm `google()` and `mavenCentral()` are available to your app's Gradle repositories and that Gradle runs on JDK 21.
2. iOS: delete `ios/App/Pods` and `ios/App/Podfile.lock`, then run `npx cap sync ios` again.
3. Confirm your Capacitor version is 7 or 8.

