@ezoic/react-native-sdk 1.11.0 → 1.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/EzoicReactNativeSdk.podspec +2 -2
- package/README.md +244 -9
- package/android/build.gradle +1 -1
- package/android/src/main/java/com/ezoic/reactnative/EzoicAdsModule.kt +120 -10
- package/ios/EzoicAdsImpl.swift +122 -8
- package/ios/EzoicReactNativeSdk.mm +27 -2
- package/lib/module/EzoicConsent.js +61 -0
- package/lib/module/EzoicConsent.js.map +1 -0
- package/lib/module/NativeEzoicAds.js +7 -0
- package/lib/module/NativeEzoicAds.js.map +1 -1
- package/lib/module/helpers.js +3 -0
- package/lib/module/helpers.js.map +1 -1
- package/lib/module/index.js +52 -2
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/src/EzoicConsent.d.ts +42 -0
- package/lib/typescript/src/EzoicConsent.d.ts.map +1 -0
- package/lib/typescript/src/NativeEzoicAds.d.ts +23 -1
- package/lib/typescript/src/NativeEzoicAds.d.ts.map +1 -1
- package/lib/typescript/src/helpers.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +40 -1
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/EzoicConsent.ts +69 -0
- package/src/NativeEzoicAds.ts +26 -1
- package/src/helpers.ts +5 -0
- package/src/index.tsx +62 -2
|
@@ -11,7 +11,7 @@ Pod::Spec.new do |s|
|
|
|
11
11
|
s.authors = package["author"]
|
|
12
12
|
|
|
13
13
|
s.platforms = { :ios => min_ios_version_supported }
|
|
14
|
-
s.source = { :git => "https://github.com/ezoic/react-native-sdk.git", :tag => "#{s.version}" }
|
|
14
|
+
s.source = { :git => "https://github.com/ezoic/react-native-sdk.git", :tag => "v#{s.version}" }
|
|
15
15
|
|
|
16
16
|
s.source_files = "ios/**/*.{h,m,mm,swift,cpp}"
|
|
17
17
|
s.private_header_files = "ios/**/*.h"
|
|
@@ -20,7 +20,7 @@ Pod::Spec.new do |s|
|
|
|
20
20
|
|
|
21
21
|
# Native Ezoic Ads SDK (vends the `EzoicAdsSDKBinary` module). Brings in
|
|
22
22
|
# PrebidMobile + Google-Mobile-Ads-SDK transitively.
|
|
23
|
-
s.dependency "EzoicAdsSDK", "~> 1.
|
|
23
|
+
s.dependency "EzoicAdsSDK", "~> 1.13.0"
|
|
24
24
|
# The native-ad host imports GoogleMobileAds directly (NativeAdView,
|
|
25
25
|
# MediaView, NativeAd). Pin GMA 12 so the module is on the compile path.
|
|
26
26
|
s.dependency "Google-Mobile-Ads-SDK", "~> 12.0"
|
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ for iOS (`EzoicAdsSDK`, via CocoaPods) and Android
|
|
|
11
11
|
## Requirements
|
|
12
12
|
|
|
13
13
|
- React Native 0.76+ with the New Architecture enabled.
|
|
14
|
-
- iOS
|
|
14
|
+
- iOS 15.0+ and Xcode 26+ (required by the native `EzoicAdsSDK` 1.13), Android `minSdk` 24+.
|
|
15
15
|
|
|
16
16
|
## Installation
|
|
17
17
|
|
|
@@ -41,16 +41,16 @@ cd ios && RCT_NEW_ARCH_ENABLED=1 pod install
|
|
|
41
41
|
```tsx
|
|
42
42
|
import { EzoicAds, EzoicBannerView } from '@ezoic/react-native-sdk';
|
|
43
43
|
|
|
44
|
-
// Initialize once, early in app startup.
|
|
44
|
+
// Initialize once, early in app startup. In GDPR regions the built-in consent
|
|
45
|
+
// dialog is presented automatically once this resolves (see "Privacy & consent").
|
|
45
46
|
await EzoicAds.initialize({ domain: 'example.com' });
|
|
46
47
|
|
|
47
|
-
// Optional
|
|
48
|
-
EzoicAds.setGDPRConsent(true, '<IAB TCF consent string>');
|
|
48
|
+
// Optional privacy signals.
|
|
49
49
|
EzoicAds.setGPPConsent('<GPP string>', '7');
|
|
50
50
|
EzoicAds.setSubjectToCOPPA(false);
|
|
51
51
|
|
|
52
|
-
//
|
|
53
|
-
const tracked = await EzoicAds.trackPageview();
|
|
52
|
+
// Label the current screen in reporting (see "Pageview labelling").
|
|
53
|
+
const tracked = await EzoicAds.trackPageview('Home');
|
|
54
54
|
|
|
55
55
|
// Render a banner. A hard-coded height is optional: on no-fill the view
|
|
56
56
|
// collapses to height 0 (default), and `onSizeChange` reports the creative
|
|
@@ -155,13 +155,248 @@ await instream.destroy();
|
|
|
155
155
|
already in flight for this id; it is safe to call again after a previous load
|
|
156
156
|
resolves. `contentUrl` and `revenueUsd` are optional.
|
|
157
157
|
|
|
158
|
+
## Configuration
|
|
159
|
+
|
|
160
|
+
`EzoicAds.initialize(config)` accepts:
|
|
161
|
+
|
|
162
|
+
| Field | Default | |
|
|
163
|
+
|---|---|---|
|
|
164
|
+
| `domain` | (required) | Your Ezoic domain. |
|
|
165
|
+
| `autoReadConsent` | `true` | Read `IABTCF_*` / `IABGPP_*` consent keys written by a CMP. |
|
|
166
|
+
| `subjectToCOPPA` | `false` | Treat the user as subject to COPPA. |
|
|
167
|
+
| `requestATTBeforeAds` | `true` | iOS only: request App Tracking Transparency before the first ad. |
|
|
168
|
+
| `debugEnabled` | `false` | Verbose native logging. |
|
|
169
|
+
| `testMode` | `false` | Ezoic $0.00 test ads on debug builds / simulators. Disable before release. |
|
|
170
|
+
| `autoTrackPageviews` | `true` | Record a pageview automatically on native screen changes. See [Pageview labelling](#pageview-labelling). |
|
|
171
|
+
| `cmpEnabled` | `true` | Enable the built-in TCF CMP. Set `false` if you run your own CMP. |
|
|
172
|
+
| `autoPresentConsent` | `true` | Present the consent dialog (if required) right after `initialize` resolves. |
|
|
173
|
+
|
|
174
|
+
## Privacy & consent
|
|
175
|
+
|
|
176
|
+
### Built-in CMP (GDPR / TCF 2.4)
|
|
177
|
+
|
|
178
|
+
The native SDK includes an IAB TCF 2.4 consent management platform (CMP ID
|
|
179
|
+
299). It is on by default (`cmpEnabled: true`) and only does anything for users
|
|
180
|
+
in GDPR regions; elsewhere nothing is shown and ads load as before.
|
|
181
|
+
|
|
182
|
+
> **If your app already runs another CMP (UMP, OneTrust, …) you _must_ set
|
|
183
|
+
> `cmpEnabled: false`.** See [Using your own CMP](#using-your-own-cmp).
|
|
184
|
+
>
|
|
185
|
+
> **Upgrading from 1.11.x:** `cmpEnabled` and `autoPresentConsent` default to
|
|
186
|
+
> `true`, so GDPR-region users now see the built-in dialog after `initialize`
|
|
187
|
+
> and ad loads wait for their decision. If you pass consent yourself, move
|
|
188
|
+
> `setGDPRConsent` **before** `initialize` (earlier versions of this README
|
|
189
|
+
> showed it after) and set `cmpEnabled: false`; see
|
|
190
|
+
> [Manual consent](#manual-consent).
|
|
191
|
+
|
|
192
|
+
**The dialog is presented for you.** Once `initialize` resolves, the wrapper
|
|
193
|
+
calls `presentConsentIfRequired()` once on your behalf (`autoPresentConsent:
|
|
194
|
+
true`). Outside GDPR regions, with `cmpEnabled: false`, when another CMP is
|
|
195
|
+
present, when you called `setGDPRConsent` before `initialize`, or with
|
|
196
|
+
`autoReadConsent: false`, the native SDK returns `notRequired` and nothing is
|
|
197
|
+
shown.
|
|
198
|
+
|
|
199
|
+
In GDPR regions, ad loads wait while the consent dialog is loading or on screen
|
|
200
|
+
(at most 5 minutes in total per dialog), and up to 10 seconds while no dialog is
|
|
201
|
+
in progress, the dialog is covered, or the app is in the background, then fail
|
|
202
|
+
with error code `5001` ([`EzoicErrorCode.consentRequired`](#error-code-5001)).
|
|
203
|
+
If native can't show the dialog at all (e.g. network error), it returns
|
|
204
|
+
`failed` with the native error code and ads proceed without a TC string
|
|
205
|
+
(limited ads). A `failed` outcome with `code: -1` is different: the wrapper had
|
|
206
|
+
no foreground screen to present from, native was never called, and ads stay
|
|
207
|
+
gated (they wait up to 10 seconds, then fail with `5001`).
|
|
208
|
+
|
|
209
|
+
To control the timing or read the outcome, turn auto-presentation off and call
|
|
210
|
+
`presentConsentIfRequired()` yourself, e.g. from your first screen:
|
|
211
|
+
|
|
212
|
+
```tsx
|
|
213
|
+
import { EzoicAds } from '@ezoic/react-native-sdk';
|
|
214
|
+
|
|
215
|
+
await EzoicAds.initialize({ domain: 'example.com', autoPresentConsent: false });
|
|
216
|
+
|
|
217
|
+
const outcome = await EzoicAds.presentConsentIfRequired();
|
|
218
|
+
switch (outcome.type) {
|
|
219
|
+
case 'decided':
|
|
220
|
+
console.log('User chose', outcome.decision); // 'acceptAll' | 'rejectAll' | 'custom'
|
|
221
|
+
break;
|
|
222
|
+
case 'failed':
|
|
223
|
+
console.log('Consent UI failed', outcome.code, outcome.message);
|
|
224
|
+
break;
|
|
225
|
+
default:
|
|
226
|
+
break; // 'notRequired' | 'alreadyDecided' | 'dismissed' | 'alreadyPresenting'
|
|
227
|
+
}
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
`presentConsentIfRequired()` can be called at any time, and repeat calls are
|
|
231
|
+
harmless: you get `alreadyPresenting` while a dialog is in flight and
|
|
232
|
+
`alreadyDecided` once a valid decision is stored. Re-present whenever
|
|
233
|
+
`isConsentRequired()` is `true` and no decision has been made (e.g. after
|
|
234
|
+
`dismissed` or `failed`). Called before initialization finishes, it waits for
|
|
235
|
+
the init response. If `initialize` resolves before any screen is showing (no
|
|
236
|
+
foreground Activity on Android, no presented view controller on iOS, e.g. a very
|
|
237
|
+
early cold start), the automatic presentation is skipped; call
|
|
238
|
+
`presentConsentIfRequired()` from your first screen.
|
|
239
|
+
|
|
240
|
+
The promise always resolves (never rejects) with an `EzoicConsentOutcome`:
|
|
241
|
+
|
|
242
|
+
| `type` | When |
|
|
243
|
+
|---|---|
|
|
244
|
+
| `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`) |
|
|
245
|
+
| `alreadyDecided` | A still-valid decision is stored; no dialog shown |
|
|
246
|
+
| `decided` | The user chose `decision` (`acceptAll`, `rejectAll` or `custom`); the choice is saved |
|
|
247
|
+
| `dismissed` | The dialog closed without a choice; ads stay gated for this session |
|
|
248
|
+
| `alreadyPresenting` | A consent dialog is already on screen or being prepared |
|
|
249
|
+
| `failed` | The dialog couldn't be shown. `code`/`message` come from the native error. `code: -1` with `message: 'No foreground Activity'` (on both platforms) is wrapper-side: there was no foreground Activity / view controller, native was not called and ads stay gated, so call `presentConsentIfRequired()` again once a screen is showing |
|
|
250
|
+
|
|
251
|
+
- **`isConsentRequired()`** resolves `true` whenever GDPR applies and the
|
|
252
|
+
built-in CMP is in charge (including after the user has decided), `false`
|
|
253
|
+
otherwise, and `null` until the init request completes or when the server
|
|
254
|
+
sent no consent information.
|
|
255
|
+
- **`resetConsent()`** deletes the stored decision so the dialog shows again
|
|
256
|
+
(ads re-gate until the user decides).
|
|
257
|
+
|
|
258
|
+
### Privacy settings button (required)
|
|
259
|
+
|
|
260
|
+
TCF policy requires users to be able to reopen the dialog and change or
|
|
261
|
+
withdraw consent at any time. Wire `presentConsentSettings()` to a menu item or
|
|
262
|
+
button that is always reachable:
|
|
263
|
+
|
|
264
|
+
```tsx
|
|
265
|
+
<Button
|
|
266
|
+
title="Privacy settings"
|
|
267
|
+
onPress={() => EzoicAds.presentConsentSettings()}
|
|
268
|
+
/>
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
It reopens the dialog with the user's stored choices in GDPR regions and
|
|
272
|
+
resolves `notRequired` elsewhere, with `cmpEnabled: false`, or when another CMP
|
|
273
|
+
is present.
|
|
274
|
+
|
|
275
|
+
### Using your own CMP
|
|
276
|
+
|
|
277
|
+
Set `cmpEnabled: false`. The SDK then reads your CMP's `IABTCF_*` (TCF) and
|
|
278
|
+
`IABGPP_*` (GPP) keys exactly as before. The built-in CMP also stays out of the
|
|
279
|
+
way automatically if it finds `IABTCF_CmpSdkID` set to another CMP's ID.
|
|
280
|
+
|
|
281
|
+
```tsx
|
|
282
|
+
await EzoicAds.initialize({ domain: 'example.com', cmpEnabled: false });
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
### Manual consent
|
|
286
|
+
|
|
287
|
+
`setGDPRConsent` and the built-in CMP are mutually exclusive. The override
|
|
288
|
+
lasts for the current process only, so **call `setGDPRConsent` before
|
|
289
|
+
`initialize` on every launch** (or set `cmpEnabled: false`). Otherwise each cold
|
|
290
|
+
start begins with the built-in CMP in charge until your call lands: it can gate
|
|
291
|
+
ad loads, show its dialog and write `IABTCF_*` keys. The SDK doesn't write your
|
|
292
|
+
consent string to `IABTCF_*` keys for other SDKs, so your own CMP must do that.
|
|
293
|
+
|
|
294
|
+
```tsx
|
|
295
|
+
EzoicAds.setGDPRConsent(true, '<IAB TCF consent string>');
|
|
296
|
+
await EzoicAds.initialize({ domain: 'example.com', cmpEnabled: false });
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
### Error code 5001
|
|
300
|
+
|
|
301
|
+
When GDPR applies and the user hasn't decided, ad loads fail after the wait
|
|
302
|
+
described above with code `5001`, exported as `EzoicErrorCode.consentRequired`.
|
|
303
|
+
Consent is checked when an ad loads, so the code arrives as:
|
|
304
|
+
|
|
305
|
+
- `code` on the ad views' `onError` (banner, native, outstream);
|
|
306
|
+
- `error.userInfo.code` on rejected rewarded, interstitial and instream
|
|
307
|
+
`load()` promises. The rejection's own `code` stays the string `'EzoicAds'`
|
|
308
|
+
and its `message` is the native one ("User consent is required to load
|
|
309
|
+
ads.").
|
|
310
|
+
|
|
311
|
+
```tsx
|
|
312
|
+
import {
|
|
313
|
+
EzoicAds,
|
|
314
|
+
EzoicBannerView,
|
|
315
|
+
EzoicErrorCode,
|
|
316
|
+
EzoicRewardedAd,
|
|
317
|
+
} from '@ezoic/react-native-sdk';
|
|
318
|
+
|
|
319
|
+
<EzoicBannerView
|
|
320
|
+
adUnitIdentifier="123456"
|
|
321
|
+
onError={(e) => {
|
|
322
|
+
if (e.code === EzoicErrorCode.consentRequired) {
|
|
323
|
+
// The user hasn't decided yet; e.g. offer EzoicAds.presentConsentIfRequired().
|
|
324
|
+
}
|
|
325
|
+
}}
|
|
326
|
+
/>;
|
|
327
|
+
|
|
328
|
+
try {
|
|
329
|
+
const ad = await EzoicRewardedAd.load('123456');
|
|
330
|
+
await ad.show();
|
|
331
|
+
} catch (e: any) {
|
|
332
|
+
if (e?.userInfo?.code === EzoicErrorCode.consentRequired) {
|
|
333
|
+
await EzoicAds.presentConsentIfRequired();
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
## Pageview labelling
|
|
339
|
+
|
|
340
|
+
Ezoic reports app traffic per *screen*, the way it reports a site per URL. Apps
|
|
341
|
+
have no URLs, so the SDK builds one from a label:
|
|
342
|
+
`https://<your domain>/<bundle id>/<screen label>`.
|
|
343
|
+
|
|
344
|
+
The native SDK tracks pageviews automatically, but it only sees native screens:
|
|
345
|
+
in a React Native app that is the single host Activity / view controller, so
|
|
346
|
+
every JS screen lands in one bucket. Call `trackPageview(screen)` when the user
|
|
347
|
+
reaches a screen to give it a name. With React Navigation, do it from the
|
|
348
|
+
container's `onStateChange`:
|
|
349
|
+
|
|
350
|
+
```tsx
|
|
351
|
+
import { useRef } from 'react';
|
|
352
|
+
import {
|
|
353
|
+
NavigationContainer,
|
|
354
|
+
useNavigationContainerRef,
|
|
355
|
+
} from '@react-navigation/native';
|
|
356
|
+
import { EzoicAds } from '@ezoic/react-native-sdk';
|
|
357
|
+
|
|
358
|
+
export default function App() {
|
|
359
|
+
const navigationRef = useNavigationContainerRef();
|
|
360
|
+
const lastRouteName = useRef<string | undefined>(undefined);
|
|
361
|
+
const trackCurrentRoute = () => {
|
|
362
|
+
const name = navigationRef.getCurrentRoute()?.name;
|
|
363
|
+
if (name && name !== lastRouteName.current) {
|
|
364
|
+
lastRouteName.current = name;
|
|
365
|
+
EzoicAds.trackPageview(name);
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
return (
|
|
369
|
+
<NavigationContainer
|
|
370
|
+
ref={navigationRef}
|
|
371
|
+
onReady={trackCurrentRoute}
|
|
372
|
+
onStateChange={trackCurrentRoute}
|
|
373
|
+
>
|
|
374
|
+
{/* ... */}
|
|
375
|
+
</NavigationContainer>
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
Labels are free text: use `/` for hierarchy (`members/profile`), spaces become
|
|
381
|
+
`-`, punctuation is dropped, case is kept. The label is also attached to every
|
|
382
|
+
ad request on that screen until the next pageview. A labelled pageview takes
|
|
383
|
+
precedence over the automatic one for the same navigation, so there is no
|
|
384
|
+
double counting. If you label every screen, set `autoTrackPageviews: false` so
|
|
385
|
+
pageviews come only from your calls. `trackPageview()` without a label records
|
|
386
|
+
an unlabelled pageview.
|
|
387
|
+
|
|
158
388
|
## API
|
|
159
389
|
|
|
160
|
-
- `EzoicAds.initialize(config)` → `Promise<void>`
|
|
161
|
-
- `EzoicAds.setGDPRConsent(applies, consentString?)` → `void`
|
|
390
|
+
- `EzoicAds.initialize(config)` → `Promise<void>` (see [Configuration](#configuration))
|
|
391
|
+
- `EzoicAds.setGDPRConsent(applies, consentString?)` → `void` (call before `initialize`)
|
|
162
392
|
- `EzoicAds.setGPPConsent(gppString?, sectionIds?)` → `void`
|
|
163
393
|
- `EzoicAds.setSubjectToCOPPA(value)` → `void`
|
|
164
|
-
- `EzoicAds.trackPageview()` → `Promise<boolean>`
|
|
394
|
+
- `EzoicAds.trackPageview(screen?)` → `Promise<boolean>`
|
|
395
|
+
- `EzoicAds.presentConsentIfRequired()` → `Promise<EzoicConsentOutcome>`
|
|
396
|
+
- `EzoicAds.presentConsentSettings()` → `Promise<EzoicConsentOutcome>`
|
|
397
|
+
- `EzoicAds.isConsentRequired()` → `Promise<boolean | null>`
|
|
398
|
+
- `EzoicAds.resetConsent()` → `void`
|
|
399
|
+
- `EzoicErrorCode.consentRequired` = `5001`
|
|
165
400
|
- `<EzoicBannerView adUnitIdentifier size collapseOnNoFill onSizeChange onLoad onError onImpression onClick onOpen onClose />`
|
|
166
401
|
- `<EzoicNativeAdView adUnitIdentifier onLoad onError onImpression onClick onOpen onClose />`
|
|
167
402
|
- `<EzoicOutstreamAdView adUnitIdentifier collapseOnNoFill onSizeChange onLoad onError onImpression onClick onOpen onClose />`
|
package/android/build.gradle
CHANGED
|
@@ -68,7 +68,7 @@ dependencies {
|
|
|
68
68
|
// Native Ezoic Ads SDK (resolved from Maven Central). Brings in Google Mobile
|
|
69
69
|
// Ads + Prebid transitively. Requires mavenCentral() + google() in the
|
|
70
70
|
// consuming app's repositories (the RN template provides both).
|
|
71
|
-
implementation "com.ezoic.sdk:ezoic-ads-sdk:1.
|
|
71
|
+
implementation "com.ezoic.sdk:ezoic-ads-sdk:1.13.0"
|
|
72
72
|
|
|
73
73
|
// The native-ad wrapper references GMA's NativeAdView/MediaView/NativeAd
|
|
74
74
|
// types at compile time. The Ezoic SDK POM scopes Google Mobile Ads at
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
package com.ezoic.reactnative
|
|
2
2
|
|
|
3
|
+
import android.app.Activity
|
|
3
4
|
import android.app.Application
|
|
5
|
+
import android.util.Log
|
|
4
6
|
import com.facebook.react.bridge.Arguments
|
|
5
7
|
import com.facebook.react.bridge.Promise
|
|
6
8
|
import com.facebook.react.bridge.ReactApplicationContext
|
|
7
9
|
import com.facebook.react.bridge.ReadableMap
|
|
10
|
+
import com.facebook.react.bridge.UiThreadUtil
|
|
8
11
|
import com.facebook.react.bridge.WritableMap
|
|
9
12
|
import com.facebook.react.modules.core.DeviceEventManagerModule
|
|
10
13
|
import com.ezoic.ads.sdk.adunits.EzoicInstreamAd
|
|
@@ -19,6 +22,8 @@ import com.ezoic.ads.sdk.adunits.EzoicRewardedAdListenerAdapter
|
|
|
19
22
|
import com.ezoic.ads.sdk.core.EzoicAds
|
|
20
23
|
import com.ezoic.ads.sdk.core.EzoicConfiguration
|
|
21
24
|
import com.ezoic.ads.sdk.core.EzoicError
|
|
25
|
+
import com.ezoic.ads.sdk.privacy.cmp.ConsentDecisionType
|
|
26
|
+
import com.ezoic.ads.sdk.privacy.cmp.ConsentOutcome
|
|
22
27
|
import java.util.concurrent.ConcurrentHashMap
|
|
23
28
|
import java.util.concurrent.atomic.AtomicBoolean
|
|
24
29
|
|
|
@@ -93,11 +98,35 @@ class EzoicAdsModule(reactContext: ReactApplicationContext) :
|
|
|
93
98
|
subjectToCOPPA = config.optBool("subjectToCOPPA", false),
|
|
94
99
|
requestATTBeforeAds = config.optBool("requestATTBeforeAds", true),
|
|
95
100
|
debugEnabled = config.optBool("debugEnabled", false),
|
|
96
|
-
testMode = config.optBool("testMode", false)
|
|
101
|
+
testMode = config.optBool("testMode", false),
|
|
102
|
+
autoTrackPageviews = config.optBool("autoTrackPageviews", true),
|
|
103
|
+
cmpEnabled = config.optBool("cmpEnabled", true)
|
|
97
104
|
)
|
|
105
|
+
val autoPresentConsent = config.optBool("autoPresentConsent", true)
|
|
98
106
|
EzoicAds.instance.initialize(app, configuration) { result ->
|
|
99
|
-
result.onSuccess {
|
|
100
|
-
|
|
107
|
+
result.onSuccess {
|
|
108
|
+
promise.resolve(null)
|
|
109
|
+
if (autoPresentConsent) presentConsentAfterInit(configuration.debugEnabled)
|
|
110
|
+
}.onFailure { e -> promise.reject("EzoicAds", e.message, e) }
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Presents the consent dialog once after a successful `initialize`. Native
|
|
116
|
+
* returns `NotRequired` outside GDPR / with `cmpEnabled = false` / with
|
|
117
|
+
* another CMP or manual consent, so this is a no-op there. The outcome is
|
|
118
|
+
* only logged; publishers wanting it call `presentConsentIfRequired`.
|
|
119
|
+
*/
|
|
120
|
+
private fun presentConsentAfterInit(debug: Boolean) {
|
|
121
|
+
val activity = currentActivity
|
|
122
|
+
if (activity == null) {
|
|
123
|
+
if (debug) Log.d(NAME, "autoPresentConsent skipped: no foreground Activity")
|
|
124
|
+
return
|
|
125
|
+
}
|
|
126
|
+
activity.runOnUiThread {
|
|
127
|
+
EzoicAds.instance.presentConsentIfRequired(activity) { outcome ->
|
|
128
|
+
if (debug) Log.d(NAME, "autoPresentConsent outcome: $outcome")
|
|
129
|
+
}
|
|
101
130
|
}
|
|
102
131
|
}
|
|
103
132
|
|
|
@@ -113,8 +142,50 @@ class EzoicAdsModule(reactContext: ReactApplicationContext) :
|
|
|
113
142
|
EzoicAds.instance.setSubjectToCOPPA(value)
|
|
114
143
|
}
|
|
115
144
|
|
|
116
|
-
override fun trackPageview(promise: Promise) {
|
|
117
|
-
|
|
145
|
+
override fun trackPageview(screen: String?, promise: Promise) {
|
|
146
|
+
if (screen.isNullOrEmpty()) {
|
|
147
|
+
EzoicAds.instance.trackPageview { success -> promise.resolve(success) }
|
|
148
|
+
} else {
|
|
149
|
+
EzoicAds.instance.trackPageview(screen) { success -> promise.resolve(success) }
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
override fun presentConsentIfRequired(promise: Promise) {
|
|
154
|
+
presentConsent(promise) { activity, callback ->
|
|
155
|
+
EzoicAds.instance.presentConsentIfRequired(activity, callback)
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
override fun presentConsentSettings(promise: Promise) {
|
|
160
|
+
presentConsent(promise) { activity, callback ->
|
|
161
|
+
EzoicAds.instance.presentConsentSettings(activity, callback)
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
override fun isConsentRequired(promise: Promise) {
|
|
166
|
+
UiThreadUtil.runOnUiThread { promise.resolve(EzoicAds.instance.isConsentRequired) }
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
override fun resetConsent() {
|
|
170
|
+
UiThreadUtil.runOnUiThread { EzoicAds.instance.resetConsent() }
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Presents from the foreground Activity on the UI thread and always resolves
|
|
175
|
+
* with an outcome map; with no Activity it resolves `failed(-1)`.
|
|
176
|
+
*/
|
|
177
|
+
private fun presentConsent(
|
|
178
|
+
promise: Promise,
|
|
179
|
+
present: (Activity, (ConsentOutcome) -> Unit) -> Unit
|
|
180
|
+
) {
|
|
181
|
+
val activity = currentActivity
|
|
182
|
+
if (activity == null) {
|
|
183
|
+
promise.resolve(consentFailureMap(NO_ACTIVITY_CODE, NO_ACTIVITY_MESSAGE))
|
|
184
|
+
return
|
|
185
|
+
}
|
|
186
|
+
activity.runOnUiThread {
|
|
187
|
+
present(activity) { outcome -> promise.resolve(outcome.toWritableMap()) }
|
|
188
|
+
}
|
|
118
189
|
}
|
|
119
190
|
|
|
120
191
|
override fun loadRewardedAd(adUnitIdentifier: String, promise: Promise) {
|
|
@@ -134,7 +205,7 @@ class EzoicAdsModule(reactContext: ReactApplicationContext) :
|
|
|
134
205
|
rewardedAds[id] = ad
|
|
135
206
|
promise.resolve(null)
|
|
136
207
|
}.onFailure { e ->
|
|
137
|
-
promise.
|
|
208
|
+
promise.rejectLoad(e, "Rewarded ad failed to load")
|
|
138
209
|
}
|
|
139
210
|
}
|
|
140
211
|
}
|
|
@@ -216,7 +287,7 @@ class EzoicAdsModule(reactContext: ReactApplicationContext) :
|
|
|
216
287
|
interstitialAds[id] = ad
|
|
217
288
|
promise.resolve(null)
|
|
218
289
|
}.onFailure { e ->
|
|
219
|
-
promise.
|
|
290
|
+
promise.rejectLoad(e, "Interstitial ad failed to load")
|
|
220
291
|
}
|
|
221
292
|
}
|
|
222
293
|
}
|
|
@@ -305,9 +376,7 @@ class EzoicAdsModule(reactContext: ReactApplicationContext) :
|
|
|
305
376
|
override fun onAdFailedToLoad(error: EzoicError) {
|
|
306
377
|
if (holder.settled.compareAndSet(false, true)) {
|
|
307
378
|
loadingInstream.remove(id)
|
|
308
|
-
|
|
309
|
-
userInfo.putInt("code", error.code)
|
|
310
|
-
promise.reject("EzoicAds", error.message ?: "Instream ad failed to load", userInfo)
|
|
379
|
+
promise.rejectLoad(error, "Instream ad failed to load")
|
|
311
380
|
}
|
|
312
381
|
}
|
|
313
382
|
})
|
|
@@ -477,11 +546,52 @@ class EzoicAdsModule(reactContext: ReactApplicationContext) :
|
|
|
477
546
|
.emit(INTERSTITIAL_EVENT, map)
|
|
478
547
|
}
|
|
479
548
|
|
|
549
|
+
/**
|
|
550
|
+
* Rejects a load with the string code "EzoicAds" and, for native errors, the
|
|
551
|
+
* numeric `EzoicError.code` in `userInfo` (JS: `error.userInfo.code`).
|
|
552
|
+
*/
|
|
553
|
+
private fun Promise.rejectLoad(e: Throwable, fallbackMessage: String) {
|
|
554
|
+
val userInfo = (e as? EzoicError)?.let { error ->
|
|
555
|
+
Arguments.createMap().apply { putInt("code", error.code) }
|
|
556
|
+
}
|
|
557
|
+
reject("EzoicAds", e.message ?: fallbackMessage, e, userInfo)
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
private fun consentFailureMap(code: Int, message: String): WritableMap =
|
|
561
|
+
Arguments.createMap().apply {
|
|
562
|
+
putString("type", "failed")
|
|
563
|
+
putInt("code", code)
|
|
564
|
+
putString("message", message)
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
private fun ConsentOutcome.toWritableMap(): WritableMap = when (this) {
|
|
568
|
+
ConsentOutcome.NotRequired -> consentTypeMap("notRequired")
|
|
569
|
+
ConsentOutcome.AlreadyDecided -> consentTypeMap("alreadyDecided")
|
|
570
|
+
ConsentOutcome.Dismissed -> consentTypeMap("dismissed")
|
|
571
|
+
ConsentOutcome.AlreadyPresenting -> consentTypeMap("alreadyPresenting")
|
|
572
|
+
is ConsentOutcome.Decided -> consentTypeMap("decided").apply {
|
|
573
|
+
putString(
|
|
574
|
+
"decision",
|
|
575
|
+
when (decision) {
|
|
576
|
+
ConsentDecisionType.ACCEPT_ALL -> "acceptAll"
|
|
577
|
+
ConsentDecisionType.REJECT_ALL -> "rejectAll"
|
|
578
|
+
ConsentDecisionType.CUSTOM -> "custom"
|
|
579
|
+
}
|
|
580
|
+
)
|
|
581
|
+
}
|
|
582
|
+
is ConsentOutcome.Failed -> consentFailureMap(error.code, error.message)
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
private fun consentTypeMap(type: String): WritableMap =
|
|
586
|
+
Arguments.createMap().apply { putString("type", type) }
|
|
587
|
+
|
|
480
588
|
private fun ReadableMap.optBool(key: String, default: Boolean): Boolean =
|
|
481
589
|
if (hasKey(key) && !isNull(key)) getBoolean(key) else default
|
|
482
590
|
|
|
483
591
|
companion object {
|
|
484
592
|
const val NAME = NativeEzoicAdsSpec.NAME
|
|
593
|
+
private const val NO_ACTIVITY_CODE = -1
|
|
594
|
+
private const val NO_ACTIVITY_MESSAGE = "No foreground Activity"
|
|
485
595
|
private const val REWARDED_EVENT = "EzoicRewardedAdEvent"
|
|
486
596
|
private const val INTERSTITIAL_EVENT = "EzoicInterstitialAdEvent"
|
|
487
597
|
}
|