@ezoic/react-native-sdk 1.11.1 → 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.
@@ -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.11.0"
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 15.0+ (Xcode 27 enforces a 15.0 floor), Android `minSdk` 24+.
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 consent / privacy signals.
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
- // Track a pageview.
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 />`
@@ -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.11.0"
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 { promise.resolve(null) }
100
- .onFailure { e -> promise.reject("EzoicAds", e.message, e) }
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
- EzoicAds.instance.trackPageview { success -> promise.resolve(success) }
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.reject("EzoicAds", e.message ?: "Rewarded ad failed to load", e)
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.reject("EzoicAds", e.message ?: "Interstitial ad failed to load", e)
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
- val userInfo = Arguments.createMap()
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
  }