@ezoic/react-native-sdk 1.2.0 → 1.4.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 +4 -1
- package/README.md +86 -2
- package/android/build.gradle +7 -1
- package/android/src/main/java/com/ezoic/reactnative/EzoicAdsModule.kt +129 -0
- package/android/src/main/java/com/ezoic/reactnative/EzoicNativeAdViewManager.kt +314 -0
- package/android/src/main/java/com/ezoic/reactnative/EzoicOutstreamAdViewManager.kt +191 -0
- package/android/src/main/java/com/ezoic/reactnative/EzoicReactNativeSdkPackage.kt +5 -1
- package/ios/EzoicAdsImpl.swift +160 -0
- package/ios/EzoicNativeAdHostView.swift +181 -0
- package/ios/EzoicNativeAdViewComponentView.mm +99 -0
- package/ios/EzoicOutstreamAdHostView.swift +86 -0
- package/ios/EzoicOutstreamAdViewComponentView.mm +105 -0
- package/ios/EzoicReactNativeSdk.mm +41 -0
- package/lib/module/EzoicInstreamAd.js +91 -0
- package/lib/module/EzoicInstreamAd.js.map +1 -0
- package/lib/module/EzoicNativeAdViewNativeComponent.ts +22 -0
- package/lib/module/EzoicOutstreamAdViewNativeComponent.ts +23 -0
- package/lib/module/NativeEzoicAds.js.map +1 -1
- package/lib/module/helpers.js.map +1 -1
- package/lib/module/index.js +58 -0
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/src/EzoicInstreamAd.d.ts +86 -0
- package/lib/typescript/src/EzoicInstreamAd.d.ts.map +1 -0
- package/lib/typescript/src/EzoicNativeAdViewNativeComponent.d.ts +18 -0
- package/lib/typescript/src/EzoicNativeAdViewNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/EzoicOutstreamAdViewNativeComponent.d.ts +18 -0
- package/lib/typescript/src/EzoicOutstreamAdViewNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/NativeEzoicAds.d.ts +4 -0
- package/lib/typescript/src/NativeEzoicAds.d.ts.map +1 -1
- package/lib/typescript/src/helpers.d.ts +1 -1
- package/lib/typescript/src/helpers.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +42 -0
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/EzoicInstreamAd.ts +110 -0
- package/src/EzoicNativeAdViewNativeComponent.ts +22 -0
- package/src/EzoicOutstreamAdViewNativeComponent.ts +23 -0
- package/src/NativeEzoicAds.ts +18 -0
- package/src/helpers.ts +1 -1
- package/src/index.tsx +100 -0
|
@@ -20,6 +20,9 @@ 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.4"
|
|
24
|
+
# The native-ad host imports GoogleMobileAds directly (NativeAdView,
|
|
25
|
+
# MediaView, NativeAd). Pin GMA 12 so the module is on the compile path.
|
|
26
|
+
s.dependency "Google-Mobile-Ads-SDK", "~> 12.0"
|
|
24
27
|
s.swift_version = "5.9"
|
|
25
28
|
end
|
package/README.md
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
# @ezoic/react-native-sdk
|
|
2
2
|
|
|
3
|
-
Ezoic Ads SDK for React Native (Prebid + Google Ad Manager banner ads).
|
|
3
|
+
Ezoic Ads SDK for React Native (Prebid + Google Ad Manager banner, native, interstitial, rewarded, outstream and instream video ads).
|
|
4
4
|
|
|
5
5
|
A thin React Native (New Architecture) wrapper over the native Ezoic Ads SDKs
|
|
6
6
|
for iOS (`EzoicAdsSDK`, via CocoaPods) and Android
|
|
7
7
|
(`com.ezoic.sdk:ezoic-ads-sdk`, via Maven Central). It exposes an imperative
|
|
8
|
-
`EzoicAds` TurboModule
|
|
8
|
+
`EzoicAds` TurboModule plus `EzoicBannerView`, `EzoicNativeAdView` and
|
|
9
|
+
`EzoicOutstreamAdView` Fabric components, and the `EzoicInstreamAd` controller.
|
|
9
10
|
|
|
10
11
|
## Requirements
|
|
11
12
|
|
|
@@ -68,6 +69,82 @@ const tracked = await EzoicAds.trackPageview();
|
|
|
68
69
|
`adUnitIdentifier` is a string coerced to a native integer. `size` is a `"WxH"`
|
|
69
70
|
string or comma-separated list (e.g. `"300x250"`, `"300x250,320x50"`).
|
|
70
71
|
|
|
72
|
+
### Native ads
|
|
73
|
+
|
|
74
|
+
`EzoicNativeAdView` loads a native ad and renders it in an SDK-built template
|
|
75
|
+
`NativeAdView` (headline, icon, media, body and a call-to-action). Unlike the
|
|
76
|
+
banner it has no `size` prop — size it with `style` and the template lays its
|
|
77
|
+
assets out inside those bounds.
|
|
78
|
+
|
|
79
|
+
```tsx
|
|
80
|
+
import { EzoicAds, EzoicNativeAdView } from '@ezoic/react-native-sdk';
|
|
81
|
+
|
|
82
|
+
<EzoicNativeAdView
|
|
83
|
+
adUnitIdentifier="123456"
|
|
84
|
+
style={{ width: '100%', height: 300 }}
|
|
85
|
+
onLoad={() => console.log('loaded')}
|
|
86
|
+
onError={(e) => console.log('error', e.message, e.code)}
|
|
87
|
+
onImpression={() => console.log('impression')}
|
|
88
|
+
onClick={() => console.log('click')}
|
|
89
|
+
onOpen={() => console.log('open')}
|
|
90
|
+
onClose={() => console.log('close')}
|
|
91
|
+
/>;
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Outstream video
|
|
95
|
+
|
|
96
|
+
`EzoicOutstreamAdView` loads and renders a self-contained outstream video ad.
|
|
97
|
+
Like the native ad it has no `size` prop — size it with `style` and the native
|
|
98
|
+
view lays the player out inside those bounds. It is view-managed: mounting the
|
|
99
|
+
component loads the ad, unmounting destroys it.
|
|
100
|
+
|
|
101
|
+
```tsx
|
|
102
|
+
import { EzoicAds, EzoicOutstreamAdView } from '@ezoic/react-native-sdk';
|
|
103
|
+
|
|
104
|
+
<EzoicOutstreamAdView
|
|
105
|
+
adUnitIdentifier="123456"
|
|
106
|
+
style={{ width: '100%', height: 250 }}
|
|
107
|
+
onLoad={() => console.log('loaded')}
|
|
108
|
+
onError={(e) => console.log('error', e.message, e.code)}
|
|
109
|
+
onImpression={() => console.log('impression')}
|
|
110
|
+
onClick={() => console.log('click')}
|
|
111
|
+
onOpen={() => console.log('open')}
|
|
112
|
+
onClose={() => console.log('close')}
|
|
113
|
+
/>;
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Instream video
|
|
117
|
+
|
|
118
|
+
`EzoicInstreamAd` is a view-less controller for instream (pre/mid/post-roll)
|
|
119
|
+
video. **The host owns the video player and the Google IMA SDK** — the SDK
|
|
120
|
+
renders nothing; its sole deliverable is a GAM VAST ad-tag URL string you feed
|
|
121
|
+
to your own IMA `AdsRequest`. A controller is multi-use and prefetchable: it is
|
|
122
|
+
not auto-destroyed, so you `load()` it repeatedly and `destroy()` it yourself.
|
|
123
|
+
|
|
124
|
+
```tsx
|
|
125
|
+
import { EzoicInstreamAd } from '@ezoic/react-native-sdk';
|
|
126
|
+
|
|
127
|
+
const instream = new EzoicInstreamAd('123456');
|
|
128
|
+
|
|
129
|
+
// Resolve the VAST ad-tag URL and hand it to your IMA player.
|
|
130
|
+
const adTagUrl = await instream.load({ contentUrl: playingVideoUrl });
|
|
131
|
+
adsLoader.requestAds({ adTagUrl });
|
|
132
|
+
|
|
133
|
+
// On an IMA ad error, walk down the floor waterfall to the next tag.
|
|
134
|
+
const next = await instream.getNextAdTagUrl(); // null once exhausted
|
|
135
|
+
if (next) adsLoader.requestAds({ adTagUrl: next });
|
|
136
|
+
|
|
137
|
+
// On the IMA STARTED event, fire the Ezoic impression pixel.
|
|
138
|
+
await instream.reportImpression({ revenueUsd: 0.012 });
|
|
139
|
+
|
|
140
|
+
// Release the native controller when done.
|
|
141
|
+
await instream.destroy();
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
`load()` rejects on no fill, an uninitialized SDK, or an overlapping load
|
|
145
|
+
already in flight for this id; it is safe to call again after a previous load
|
|
146
|
+
resolves. `contentUrl` and `revenueUsd` are optional.
|
|
147
|
+
|
|
71
148
|
## API
|
|
72
149
|
|
|
73
150
|
- `EzoicAds.initialize(config)` → `Promise<void>`
|
|
@@ -76,6 +153,13 @@ string or comma-separated list (e.g. `"300x250"`, `"300x250,320x50"`).
|
|
|
76
153
|
- `EzoicAds.setSubjectToCOPPA(value)` → `void`
|
|
77
154
|
- `EzoicAds.trackPageview()` → `Promise<boolean>`
|
|
78
155
|
- `<EzoicBannerView adUnitIdentifier size onLoad onError onImpression onClick onOpen onClose />`
|
|
156
|
+
- `<EzoicNativeAdView adUnitIdentifier onLoad onError onImpression onClick onOpen onClose />`
|
|
157
|
+
- `<EzoicOutstreamAdView adUnitIdentifier onLoad onError onImpression onClick onOpen onClose />`
|
|
158
|
+
- `new EzoicInstreamAd(adUnitIdentifier)`
|
|
159
|
+
- `.load({ contentUrl? })` → `Promise<string>` (GAM VAST ad-tag URL)
|
|
160
|
+
- `.getNextAdTagUrl()` → `Promise<string | null>`
|
|
161
|
+
- `.reportImpression({ revenueUsd? })` → `Promise<void>`
|
|
162
|
+
- `.destroy()` → `Promise<void>`
|
|
79
163
|
|
|
80
164
|
## License
|
|
81
165
|
|
package/android/build.gradle
CHANGED
|
@@ -68,5 +68,11 @@ 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.4.0"
|
|
72
|
+
|
|
73
|
+
// The native-ad wrapper references GMA's NativeAdView/MediaView/NativeAd
|
|
74
|
+
// types at compile time. The Ezoic SDK POM scopes Google Mobile Ads at
|
|
75
|
+
// runtime only, so pin the compile-time classpath here (runtime resolution
|
|
76
|
+
// still comes transitively from the Ezoic SDK).
|
|
77
|
+
compileOnly "com.google.android.gms:play-services-ads:22.6.0"
|
|
72
78
|
}
|
|
@@ -7,6 +7,8 @@ import com.facebook.react.bridge.ReactApplicationContext
|
|
|
7
7
|
import com.facebook.react.bridge.ReadableMap
|
|
8
8
|
import com.facebook.react.bridge.WritableMap
|
|
9
9
|
import com.facebook.react.modules.core.DeviceEventManagerModule
|
|
10
|
+
import com.ezoic.ads.sdk.adunits.EzoicInstreamAd
|
|
11
|
+
import com.ezoic.ads.sdk.adunits.EzoicInstreamAdListener
|
|
10
12
|
import com.ezoic.ads.sdk.adunits.EzoicInterstitialAd
|
|
11
13
|
import com.ezoic.ads.sdk.adunits.EzoicInterstitialAdListener
|
|
12
14
|
import com.ezoic.ads.sdk.adunits.EzoicInterstitialAdListenerAdapter
|
|
@@ -18,6 +20,7 @@ import com.ezoic.ads.sdk.core.EzoicAds
|
|
|
18
20
|
import com.ezoic.ads.sdk.core.EzoicConfiguration
|
|
19
21
|
import com.ezoic.ads.sdk.core.EzoicError
|
|
20
22
|
import java.util.concurrent.ConcurrentHashMap
|
|
23
|
+
import java.util.concurrent.atomic.AtomicBoolean
|
|
21
24
|
|
|
22
25
|
class EzoicAdsModule(reactContext: ReactApplicationContext) :
|
|
23
26
|
NativeEzoicAdsSpec(reactContext) {
|
|
@@ -51,6 +54,28 @@ class EzoicAdsModule(reactContext: ReactApplicationContext) :
|
|
|
51
54
|
/** Ad unit ids with an in-flight interstitial `load`. */
|
|
52
55
|
private val loadingInterstitial = ConcurrentHashMap.newKeySet<Int>()
|
|
53
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Instream controllers, keyed by ad unit id. Instream is multi-use and
|
|
59
|
+
* prefetchable, so a controller is created-or-reused per id and NOT
|
|
60
|
+
* auto-destroyed — it lives until [destroyInstreamAd] or module teardown.
|
|
61
|
+
*/
|
|
62
|
+
private val instreamAds = ConcurrentHashMap<Int, EzoicInstreamAd>()
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* In-flight instream `load` calls, keyed by ad unit id. Doubles as the
|
|
66
|
+
* duplicate-load guard: the native `load` is a SILENT no-op while already
|
|
67
|
+
* loading, so an overlapping load must be rejected here or its promise hangs
|
|
68
|
+
* forever.
|
|
69
|
+
*/
|
|
70
|
+
private val loadingInstream = ConcurrentHashMap<Int, InstreamLoad>()
|
|
71
|
+
|
|
72
|
+
private class InstreamLoad(val promise: Promise) {
|
|
73
|
+
// destroy/invalidate run on the JS thread while listener callbacks run on
|
|
74
|
+
// main, so settling must be an atomic claim (compareAndSet) rather than a
|
|
75
|
+
// check-then-set, or both sides can settle the same promise.
|
|
76
|
+
val settled = AtomicBoolean(false)
|
|
77
|
+
}
|
|
78
|
+
|
|
54
79
|
override fun initialize(config: ReadableMap, promise: Promise) {
|
|
55
80
|
val domain = if (config.hasKey("domain")) config.getString("domain") else null
|
|
56
81
|
if (domain.isNullOrEmpty()) {
|
|
@@ -235,6 +260,110 @@ class EzoicAdsModule(reactContext: ReactApplicationContext) :
|
|
|
235
260
|
}
|
|
236
261
|
}
|
|
237
262
|
|
|
263
|
+
override fun loadInstreamAd(adUnitIdentifier: Double, contentUrl: String?, promise: Promise) {
|
|
264
|
+
val id = adUnitIdentifier.toInt()
|
|
265
|
+
if (id <= 0) {
|
|
266
|
+
promise.reject("EzoicAds", "Invalid adUnitIdentifier: $adUnitIdentifier")
|
|
267
|
+
return
|
|
268
|
+
}
|
|
269
|
+
// Reject overlapping loads for this id: the native load silently no-ops
|
|
270
|
+
// while loading (a second load on the reused controller, or a load from a
|
|
271
|
+
// fresh JS instance sharing the id), which would hang the promise.
|
|
272
|
+
if (loadingInstream.containsKey(id)) {
|
|
273
|
+
promise.reject("EzoicAds", "An instream ad is already loading for ad unit $id")
|
|
274
|
+
return
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Create-or-reuse: instream is multi-use, so a repeat load on the same id
|
|
278
|
+
// reuses the existing native controller (preserving its tag/waterfall state).
|
|
279
|
+
val ad = instreamAds.getOrPut(id) { EzoicInstreamAd(id) }
|
|
280
|
+
|
|
281
|
+
val holder = InstreamLoad(promise)
|
|
282
|
+
loadingInstream[id] = holder
|
|
283
|
+
// The native load uses the context only for parity (it is not retained);
|
|
284
|
+
// pass the same reactApplicationContext the rewarded/interstitial loads use.
|
|
285
|
+
// The native SDK already posts these callbacks to main, so settle directly
|
|
286
|
+
// here — no need to re-post via a Handler.
|
|
287
|
+
ad.load(reactApplicationContext, contentUrl, object : EzoicInstreamAdListener {
|
|
288
|
+
override fun onAdTagReady(adTagUrl: String) {
|
|
289
|
+
// CAS guard: after a destroy->reload a stale callback must not evict
|
|
290
|
+
// the newer load's holder (only the winning settle removes it).
|
|
291
|
+
if (holder.settled.compareAndSet(false, true)) {
|
|
292
|
+
loadingInstream.remove(id)
|
|
293
|
+
promise.resolve(adTagUrl)
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
override fun onAdFailedToLoad(error: EzoicError) {
|
|
298
|
+
if (holder.settled.compareAndSet(false, true)) {
|
|
299
|
+
loadingInstream.remove(id)
|
|
300
|
+
val userInfo = Arguments.createMap()
|
|
301
|
+
userInfo.putInt("code", error.code)
|
|
302
|
+
promise.reject("EzoicAds", error.message ?: "Instream ad failed to load", userInfo)
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
})
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
override fun getInstreamNextAdTagUrl(adUnitIdentifier: Double, promise: Promise) {
|
|
309
|
+
val id = adUnitIdentifier.toInt()
|
|
310
|
+
if (id <= 0) {
|
|
311
|
+
promise.reject("EzoicAds", "Invalid adUnitIdentifier: $adUnitIdentifier")
|
|
312
|
+
return
|
|
313
|
+
}
|
|
314
|
+
// Synchronous native call; resolves the next waterfall tag or null when
|
|
315
|
+
// exhausted / before a successful load / after destroy.
|
|
316
|
+
promise.resolve(instreamAds[id]?.getNextAdTagUrl())
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
override fun reportInstreamImpression(
|
|
320
|
+
adUnitIdentifier: Double,
|
|
321
|
+
revenueUsd: Double?,
|
|
322
|
+
promise: Promise
|
|
323
|
+
) {
|
|
324
|
+
val id = adUnitIdentifier.toInt()
|
|
325
|
+
if (id <= 0) {
|
|
326
|
+
promise.reject("EzoicAds", "Invalid adUnitIdentifier: $adUnitIdentifier")
|
|
327
|
+
return
|
|
328
|
+
}
|
|
329
|
+
// No-op natively when no tag has been delivered or after destroy.
|
|
330
|
+
instreamAds[id]?.reportImpression(revenueUsd)
|
|
331
|
+
promise.resolve(null)
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
override fun destroyInstreamAd(adUnitIdentifier: Double, promise: Promise) {
|
|
335
|
+
val id = adUnitIdentifier.toInt()
|
|
336
|
+
if (id <= 0) {
|
|
337
|
+
promise.reject("EzoicAds", "Invalid adUnitIdentifier: $adUnitIdentifier")
|
|
338
|
+
return
|
|
339
|
+
}
|
|
340
|
+
// The native SDK suppresses load callbacks once destroyed (load tokens), so
|
|
341
|
+
// settle any pending load's promise HERE first or it hangs forever. The CAS
|
|
342
|
+
// claim also disarms a listener callback racing in on main concurrently.
|
|
343
|
+
val pending = loadingInstream.remove(id)
|
|
344
|
+
if (pending != null && pending.settled.compareAndSet(false, true)) {
|
|
345
|
+
pending.promise.reject("EzoicAds", "Instream ad was destroyed while loading")
|
|
346
|
+
}
|
|
347
|
+
instreamAds.remove(id)?.destroy()
|
|
348
|
+
promise.resolve(null)
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
override fun invalidate() {
|
|
352
|
+
// Module teardown: settle every pending instream load with an error and
|
|
353
|
+
// destroy every controller so no promise hangs and no native resource leaks.
|
|
354
|
+
for ((_, holder) in loadingInstream) {
|
|
355
|
+
if (holder.settled.compareAndSet(false, true)) {
|
|
356
|
+
holder.promise.reject("EzoicAds", "Module was destroyed while loading")
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
loadingInstream.clear()
|
|
360
|
+
for ((_, ad) in instreamAds) {
|
|
361
|
+
ad.destroy()
|
|
362
|
+
}
|
|
363
|
+
instreamAds.clear()
|
|
364
|
+
super.invalidate()
|
|
365
|
+
}
|
|
366
|
+
|
|
238
367
|
override fun addListener(eventName: String) {
|
|
239
368
|
// No-op: required by the React Native NativeEventEmitter contract.
|
|
240
369
|
}
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
package com.ezoic.reactnative
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.graphics.Typeface
|
|
5
|
+
import android.view.View
|
|
6
|
+
import android.view.ViewGroup
|
|
7
|
+
import android.widget.Button
|
|
8
|
+
import android.widget.FrameLayout
|
|
9
|
+
import android.widget.ImageView
|
|
10
|
+
import android.widget.LinearLayout
|
|
11
|
+
import android.widget.TextView
|
|
12
|
+
import com.facebook.react.bridge.Arguments
|
|
13
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
14
|
+
import com.facebook.react.bridge.WritableMap
|
|
15
|
+
import com.facebook.react.module.annotations.ReactModule
|
|
16
|
+
import com.facebook.react.uimanager.SimpleViewManager
|
|
17
|
+
import com.facebook.react.uimanager.ThemedReactContext
|
|
18
|
+
import com.facebook.react.uimanager.annotations.ReactProp
|
|
19
|
+
import com.facebook.react.uimanager.events.RCTEventEmitter
|
|
20
|
+
import com.ezoic.ads.sdk.adunits.EzoicNativeAd
|
|
21
|
+
import com.ezoic.ads.sdk.adunits.EzoicNativeAdListener
|
|
22
|
+
import com.ezoic.ads.sdk.adunits.EzoicNativeAdLoadListener
|
|
23
|
+
import com.ezoic.ads.sdk.core.EzoicError
|
|
24
|
+
import com.google.android.gms.ads.nativead.MediaView
|
|
25
|
+
import com.google.android.gms.ads.nativead.NativeAd
|
|
26
|
+
import com.google.android.gms.ads.nativead.NativeAdView
|
|
27
|
+
|
|
28
|
+
@ReactModule(name = EzoicNativeAdViewManager.NAME)
|
|
29
|
+
class EzoicNativeAdViewManager(private val ctx: ReactApplicationContext) :
|
|
30
|
+
SimpleViewManager<EzoicNativeAdViewManager.NativeAdContainer>() {
|
|
31
|
+
|
|
32
|
+
override fun getName() = NAME
|
|
33
|
+
|
|
34
|
+
override fun createViewInstance(reactContext: ThemedReactContext): NativeAdContainer {
|
|
35
|
+
val container = NativeAdContainer(reactContext)
|
|
36
|
+
container.layoutParams = ViewGroup.LayoutParams(
|
|
37
|
+
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT
|
|
38
|
+
)
|
|
39
|
+
return container
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
@ReactProp(name = "adUnitIdentifier")
|
|
43
|
+
fun setAdUnitIdentifier(view: NativeAdContainer, value: String?) {
|
|
44
|
+
val newAdUnitId = value?.toIntOrNull() ?: 0
|
|
45
|
+
// Ad unit changed after a load already started: tear down the loaded/loading
|
|
46
|
+
// ad and clear the started flag so onAfterUpdateTransaction's maybeLoad
|
|
47
|
+
// starts a fresh load for the new id.
|
|
48
|
+
if (newAdUnitId != view.adUnitId && view.loadStarted) {
|
|
49
|
+
view.ezoicNativeAd?.destroy()
|
|
50
|
+
view.ezoicNativeAd = null
|
|
51
|
+
view.removeAllViews()
|
|
52
|
+
view.loadStarted = false
|
|
53
|
+
view.loadGeneration++
|
|
54
|
+
}
|
|
55
|
+
view.adUnitId = newAdUnitId
|
|
56
|
+
view.rawAdUnitId = value ?: ""
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Fabric sets every prop for the mount transaction before this runs, so the
|
|
60
|
+
// ad unit id is final here. `view.post` escapes the mount transaction so the
|
|
61
|
+
// synchronous native-SDK failure path (uninitialized) can't reenter the
|
|
62
|
+
// mounting layer; the started flag survives repeated transactions.
|
|
63
|
+
override fun onAfterUpdateTransaction(view: NativeAdContainer) {
|
|
64
|
+
super.onAfterUpdateTransaction(view)
|
|
65
|
+
view.post { maybeLoad(view) }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
private fun maybeLoad(view: NativeAdContainer) {
|
|
69
|
+
if (view.loadStarted || view.disposed) return
|
|
70
|
+
if (view.adUnitId <= 0) {
|
|
71
|
+
// Non-numeric or missing id coerces to 0 in setAdUnitIdentifier. Only
|
|
72
|
+
// emit an error when a prop was actually supplied (non-empty raw
|
|
73
|
+
// string); an unset prop should stay silent.
|
|
74
|
+
if (view.rawAdUnitId.isNotEmpty()) {
|
|
75
|
+
view.loadStarted = true
|
|
76
|
+
emit(view, "topError", errorMap("Invalid ad unit identifier", 0))
|
|
77
|
+
}
|
|
78
|
+
return
|
|
79
|
+
}
|
|
80
|
+
view.loadStarted = true
|
|
81
|
+
val generation = view.loadGeneration
|
|
82
|
+
EzoicNativeAd.load(view.context, view.adUnitId, object : EzoicNativeAdLoadListener {
|
|
83
|
+
override fun onNativeAdLoaded(nativeAd: EzoicNativeAd) {
|
|
84
|
+
// dispose() and this callback both arrive on the main thread, so the
|
|
85
|
+
// check is race-free. A late-arriving ad is destroyed, not rendered.
|
|
86
|
+
// A generation mismatch means the ad unit changed mid-load (mirrors
|
|
87
|
+
// the SDK's isCurrentLoad token pattern): destroy and emit nothing.
|
|
88
|
+
if (view.disposed || view.loadGeneration != generation) {
|
|
89
|
+
nativeAd.destroy()
|
|
90
|
+
return
|
|
91
|
+
}
|
|
92
|
+
val gmaAd = nativeAd.nativeAd ?: run {
|
|
93
|
+
// Empty-content ad: destroy it instead of keeping an unrenderable,
|
|
94
|
+
// errored ad alive on the view.
|
|
95
|
+
nativeAd.destroy()
|
|
96
|
+
emit(view, "topError", errorMap("Native ad loaded without content", 0))
|
|
97
|
+
return
|
|
98
|
+
}
|
|
99
|
+
view.ezoicNativeAd = nativeAd
|
|
100
|
+
// Attach the lifecycle listener before the rendered NativeAdView
|
|
101
|
+
// registers — the impression fires as soon as the view is displayed.
|
|
102
|
+
nativeAd.listener = object : EzoicNativeAdListener {
|
|
103
|
+
override fun onNativeAdImpression(nativeAd: EzoicNativeAd) =
|
|
104
|
+
emit(view, "topImpression", Arguments.createMap())
|
|
105
|
+
override fun onNativeAdClicked(nativeAd: EzoicNativeAd) =
|
|
106
|
+
emit(view, "topAdClick", Arguments.createMap())
|
|
107
|
+
override fun onNativeAdOpened(nativeAd: EzoicNativeAd) =
|
|
108
|
+
emit(view, "topOpen", Arguments.createMap())
|
|
109
|
+
override fun onNativeAdClosed(nativeAd: EzoicNativeAd) =
|
|
110
|
+
emit(view, "topClose", Arguments.createMap())
|
|
111
|
+
}
|
|
112
|
+
val adView = buildTemplate(view.context, gmaAd)
|
|
113
|
+
view.removeAllViews()
|
|
114
|
+
view.addView(adView)
|
|
115
|
+
emit(view, "topLoad", Arguments.createMap())
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
override fun onNativeAdFailedToLoad(error: EzoicError) {
|
|
119
|
+
if (view.disposed || view.loadGeneration != generation) return
|
|
120
|
+
emit(view, "topError", errorMap(error.message, error.code))
|
|
121
|
+
}
|
|
122
|
+
})
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Builds a template [NativeAdView] entirely in code (the module ships no
|
|
127
|
+
* `res/` layouts). Layout: a vertical column of a header row (icon +
|
|
128
|
+
* headline/advertiser), a [MediaView], the body text and a call-to-action
|
|
129
|
+
* button. Only the asset views actually present on [gmaAd] are created and
|
|
130
|
+
* registered; [NativeAdView.setNativeAd] is called last, as GMA requires.
|
|
131
|
+
*/
|
|
132
|
+
private fun buildTemplate(context: Context, gmaAd: NativeAd): NativeAdView {
|
|
133
|
+
val adView = NativeAdView(context)
|
|
134
|
+
|
|
135
|
+
val root = LinearLayout(context).apply {
|
|
136
|
+
orientation = LinearLayout.VERTICAL
|
|
137
|
+
layoutParams = FrameLayout.LayoutParams(
|
|
138
|
+
FrameLayout.LayoutParams.MATCH_PARENT,
|
|
139
|
+
FrameLayout.LayoutParams.WRAP_CONTENT,
|
|
140
|
+
)
|
|
141
|
+
val pad = dp(context, 8)
|
|
142
|
+
setPadding(pad, pad, pad, pad)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
val headerRow = LinearLayout(context).apply {
|
|
146
|
+
orientation = LinearLayout.HORIZONTAL
|
|
147
|
+
layoutParams = LinearLayout.LayoutParams(
|
|
148
|
+
LinearLayout.LayoutParams.MATCH_PARENT,
|
|
149
|
+
LinearLayout.LayoutParams.WRAP_CONTENT,
|
|
150
|
+
)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
var iconView: ImageView? = null
|
|
154
|
+
gmaAd.icon?.drawable?.let { drawable ->
|
|
155
|
+
val iv = ImageView(context).apply {
|
|
156
|
+
layoutParams = LinearLayout.LayoutParams(dp(context, 40), dp(context, 40))
|
|
157
|
+
setImageDrawable(drawable)
|
|
158
|
+
}
|
|
159
|
+
headerRow.addView(iv)
|
|
160
|
+
iconView = iv
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
val textColumn = LinearLayout(context).apply {
|
|
164
|
+
orientation = LinearLayout.VERTICAL
|
|
165
|
+
layoutParams = LinearLayout.LayoutParams(
|
|
166
|
+
0,
|
|
167
|
+
LinearLayout.LayoutParams.WRAP_CONTENT,
|
|
168
|
+
1f,
|
|
169
|
+
).apply { leftMargin = dp(context, 8) }
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
var headlineView: TextView? = null
|
|
173
|
+
gmaAd.headline?.let { text ->
|
|
174
|
+
val tv = TextView(context).apply {
|
|
175
|
+
this.text = text
|
|
176
|
+
setTypeface(typeface, Typeface.BOLD)
|
|
177
|
+
textSize = 16f
|
|
178
|
+
}
|
|
179
|
+
textColumn.addView(tv)
|
|
180
|
+
headlineView = tv
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
var advertiserView: TextView? = null
|
|
184
|
+
gmaAd.advertiser?.let { text ->
|
|
185
|
+
val tv = TextView(context).apply {
|
|
186
|
+
this.text = text
|
|
187
|
+
textSize = 12f
|
|
188
|
+
}
|
|
189
|
+
textColumn.addView(tv)
|
|
190
|
+
advertiserView = tv
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
headerRow.addView(textColumn)
|
|
194
|
+
root.addView(headerRow)
|
|
195
|
+
|
|
196
|
+
var mediaView: MediaView? = null
|
|
197
|
+
gmaAd.mediaContent?.let { content ->
|
|
198
|
+
val mv = MediaView(context).apply {
|
|
199
|
+
layoutParams = LinearLayout.LayoutParams(
|
|
200
|
+
LinearLayout.LayoutParams.MATCH_PARENT,
|
|
201
|
+
dp(context, 175),
|
|
202
|
+
).apply { topMargin = dp(context, 8) }
|
|
203
|
+
mediaContent = content
|
|
204
|
+
}
|
|
205
|
+
root.addView(mv)
|
|
206
|
+
mediaView = mv
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
var bodyView: TextView? = null
|
|
210
|
+
gmaAd.body?.let { text ->
|
|
211
|
+
val tv = TextView(context).apply {
|
|
212
|
+
this.text = text
|
|
213
|
+
textSize = 14f
|
|
214
|
+
setPadding(0, dp(context, 8), 0, 0)
|
|
215
|
+
}
|
|
216
|
+
root.addView(tv)
|
|
217
|
+
bodyView = tv
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
var callToActionView: Button? = null
|
|
221
|
+
gmaAd.callToAction?.let { text ->
|
|
222
|
+
val btn = Button(context).apply {
|
|
223
|
+
this.text = text
|
|
224
|
+
layoutParams = LinearLayout.LayoutParams(
|
|
225
|
+
LinearLayout.LayoutParams.MATCH_PARENT,
|
|
226
|
+
LinearLayout.LayoutParams.WRAP_CONTENT,
|
|
227
|
+
).apply { topMargin = dp(context, 8) }
|
|
228
|
+
}
|
|
229
|
+
root.addView(btn)
|
|
230
|
+
callToActionView = btn
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
adView.addView(root)
|
|
234
|
+
// Register only the asset views that were populated; a null assignment
|
|
235
|
+
// leaves that asset unregistered.
|
|
236
|
+
adView.headlineView = headlineView
|
|
237
|
+
adView.bodyView = bodyView
|
|
238
|
+
adView.iconView = iconView
|
|
239
|
+
adView.advertiserView = advertiserView
|
|
240
|
+
adView.callToActionView = callToActionView
|
|
241
|
+
adView.mediaView = mediaView
|
|
242
|
+
adView.setNativeAd(gmaAd)
|
|
243
|
+
return adView
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
override fun onDropViewInstance(view: NativeAdContainer) {
|
|
247
|
+
super.onDropViewInstance(view)
|
|
248
|
+
view.disposed = true
|
|
249
|
+
view.loadGeneration++
|
|
250
|
+
view.ezoicNativeAd?.destroy()
|
|
251
|
+
view.ezoicNativeAd = null
|
|
252
|
+
view.removeAllViews()
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
private fun emit(view: NativeAdContainer, event: String, payload: WritableMap) {
|
|
256
|
+
if (view.disposed) return
|
|
257
|
+
ctx.getJSModule(RCTEventEmitter::class.java).receiveEvent(view.id, event, payload)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
private fun errorMap(message: String?, code: Int): WritableMap {
|
|
261
|
+
val map = Arguments.createMap()
|
|
262
|
+
map.putString("message", message ?: "")
|
|
263
|
+
map.putInt("code", code)
|
|
264
|
+
return map
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
private fun dp(context: Context, value: Int): Int =
|
|
268
|
+
(value * context.resources.displayMetrics.density).toInt()
|
|
269
|
+
|
|
270
|
+
override fun getExportedCustomBubblingEventTypeConstants(): Map<String, Any> {
|
|
271
|
+
fun reg(on: String) = mapOf("phasedRegistrationNames" to mapOf("bubbled" to on))
|
|
272
|
+
return mapOf(
|
|
273
|
+
"topLoad" to reg("onLoad"),
|
|
274
|
+
"topError" to reg("onError"),
|
|
275
|
+
"topImpression" to reg("onImpression"),
|
|
276
|
+
"topAdClick" to reg("onAdClick"),
|
|
277
|
+
"topOpen" to reg("onOpen"),
|
|
278
|
+
"topClose" to reg("onClose")
|
|
279
|
+
)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Container for the template ad view. RN lays out only Yoga-managed views;
|
|
284
|
+
* the template [NativeAdView] is added from native code and stays unmeasured
|
|
285
|
+
* (blank) unless we force a measure/layout pass. Overriding [requestLayout]
|
|
286
|
+
* to post a manual measure(EXACTLY)+layout of the current bounds is the
|
|
287
|
+
* proven fix (react-native-google-mobile-ads uses the same approach).
|
|
288
|
+
*/
|
|
289
|
+
class NativeAdContainer(context: Context) : FrameLayout(context) {
|
|
290
|
+
var adUnitId: Int = 0
|
|
291
|
+
var rawAdUnitId: String = ""
|
|
292
|
+
var loadStarted: Boolean = false
|
|
293
|
+
var disposed: Boolean = false
|
|
294
|
+
var loadGeneration: Int = 0
|
|
295
|
+
var ezoicNativeAd: EzoicNativeAd? = null
|
|
296
|
+
|
|
297
|
+
private val measureAndLayout = Runnable {
|
|
298
|
+
measure(
|
|
299
|
+
View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
|
|
300
|
+
View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY)
|
|
301
|
+
)
|
|
302
|
+
layout(left, top, right, bottom)
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
override fun requestLayout() {
|
|
306
|
+
super.requestLayout()
|
|
307
|
+
post(measureAndLayout)
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
companion object {
|
|
312
|
+
const val NAME = "EzoicNativeAdView"
|
|
313
|
+
}
|
|
314
|
+
}
|