@ezoic/react-native-sdk 1.1.0 → 1.3.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.
Files changed (38) hide show
  1. package/EzoicReactNativeSdk.podspec +4 -1
  2. package/README.md +26 -2
  3. package/android/build.gradle +7 -1
  4. package/android/src/main/java/com/ezoic/reactnative/EzoicAdsModule.kt +144 -0
  5. package/android/src/main/java/com/ezoic/reactnative/EzoicNativeAdViewManager.kt +314 -0
  6. package/android/src/main/java/com/ezoic/reactnative/EzoicReactNativeSdkPackage.kt +4 -1
  7. package/ios/EzoicAdsImpl.swift +200 -44
  8. package/ios/EzoicNativeAdHostView.swift +181 -0
  9. package/ios/EzoicNativeAdViewComponentView.mm +99 -0
  10. package/ios/EzoicReactNativeSdk.mm +18 -1
  11. package/lib/module/EzoicInterstitialAd.js +108 -0
  12. package/lib/module/EzoicInterstitialAd.js.map +1 -0
  13. package/lib/module/EzoicNativeAdViewNativeComponent.ts +22 -0
  14. package/lib/module/EzoicRewardedAd.js +4 -1
  15. package/lib/module/EzoicRewardedAd.js.map +1 -1
  16. package/lib/module/NativeEzoicAds.js.map +1 -1
  17. package/lib/module/helpers.js.map +1 -1
  18. package/lib/module/index.js +29 -0
  19. package/lib/module/index.js.map +1 -1
  20. package/lib/typescript/src/EzoicInterstitialAd.d.ts +51 -0
  21. package/lib/typescript/src/EzoicInterstitialAd.d.ts.map +1 -0
  22. package/lib/typescript/src/EzoicNativeAdViewNativeComponent.d.ts +18 -0
  23. package/lib/typescript/src/EzoicNativeAdViewNativeComponent.d.ts.map +1 -0
  24. package/lib/typescript/src/EzoicRewardedAd.d.ts +1 -0
  25. package/lib/typescript/src/EzoicRewardedAd.d.ts.map +1 -1
  26. package/lib/typescript/src/NativeEzoicAds.d.ts +2 -0
  27. package/lib/typescript/src/NativeEzoicAds.d.ts.map +1 -1
  28. package/lib/typescript/src/helpers.d.ts +1 -1
  29. package/lib/typescript/src/helpers.d.ts.map +1 -1
  30. package/lib/typescript/src/index.d.ts +21 -0
  31. package/lib/typescript/src/index.d.ts.map +1 -1
  32. package/package.json +2 -2
  33. package/src/EzoicInterstitialAd.ts +126 -0
  34. package/src/EzoicNativeAdViewNativeComponent.ts +22 -0
  35. package/src/EzoicRewardedAd.ts +8 -2
  36. package/src/NativeEzoicAds.ts +3 -1
  37. package/src/helpers.ts +1 -1
  38. package/src/index.tsx +51 -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.1"
23
+ s.dependency "EzoicAdsSDK", "~> 1.3"
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 and rewarded 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 and an `EzoicBannerView` Fabric component.
8
+ `EzoicAds` TurboModule plus `EzoicBannerView` and `EzoicNativeAdView` Fabric
9
+ components.
9
10
 
10
11
  ## Requirements
11
12
 
@@ -68,6 +69,28 @@ 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
+
71
94
  ## API
72
95
 
73
96
  - `EzoicAds.initialize(config)` → `Promise<void>`
@@ -76,6 +99,7 @@ string or comma-separated list (e.g. `"300x250"`, `"300x250,320x50"`).
76
99
  - `EzoicAds.setSubjectToCOPPA(value)` → `void`
77
100
  - `EzoicAds.trackPageview()` → `Promise<boolean>`
78
101
  - `<EzoicBannerView adUnitIdentifier size onLoad onError onImpression onClick onOpen onClose />`
102
+ - `<EzoicNativeAdView adUnitIdentifier onLoad onError onImpression onClick onOpen onClose />`
79
103
 
80
104
  ## License
81
105
 
@@ -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.1.0"
71
+ implementation "com.ezoic.sdk:ezoic-ads-sdk:1.3.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,9 @@ 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.EzoicInterstitialAd
11
+ import com.ezoic.ads.sdk.adunits.EzoicInterstitialAdListener
12
+ import com.ezoic.ads.sdk.adunits.EzoicInterstitialAdListenerAdapter
10
13
  import com.ezoic.ads.sdk.adunits.EzoicReward
11
14
  import com.ezoic.ads.sdk.adunits.EzoicRewardedAd
12
15
  import com.ezoic.ads.sdk.adunits.EzoicRewardedAdListener
@@ -32,6 +35,22 @@ class EzoicAdsModule(reactContext: ReactApplicationContext) :
32
35
  @Volatile var reward: EzoicReward? = null
33
36
  }
34
37
 
38
+ /** Loaded interstitial ads awaiting `show`, keyed by ad unit id. */
39
+ private val interstitialAds = ConcurrentHashMap<Int, EzoicInterstitialAd>()
40
+
41
+ /** In-flight interstitial `show` calls, keyed by ad unit id. */
42
+ private val pendingInterstitialShows = ConcurrentHashMap<Int, InterstitialShow>()
43
+
44
+ private class InterstitialShow(val promise: Promise) {
45
+ @Volatile var settled = false
46
+ }
47
+
48
+ /** Ad unit ids with an in-flight rewarded `load`. */
49
+ private val loadingRewarded = ConcurrentHashMap.newKeySet<Int>()
50
+
51
+ /** Ad unit ids with an in-flight interstitial `load`. */
52
+ private val loadingInterstitial = ConcurrentHashMap.newKeySet<Int>()
53
+
35
54
  override fun initialize(config: ReadableMap, promise: Promise) {
36
55
  val domain = if (config.hasKey("domain")) config.getString("domain") else null
37
56
  if (domain.isNullOrEmpty()) {
@@ -79,7 +98,12 @@ class EzoicAdsModule(reactContext: ReactApplicationContext) :
79
98
  promise.reject("EzoicAds", "Invalid adUnitIdentifier: $adUnitIdentifier")
80
99
  return
81
100
  }
101
+ if (rewardedAds.containsKey(id) || !loadingRewarded.add(id)) {
102
+ promise.reject("EzoicAds", "An ad is already loaded/loading for ad unit $adUnitIdentifier")
103
+ return
104
+ }
82
105
  EzoicRewardedAd.load(reactApplicationContext, id) { result ->
106
+ loadingRewarded.remove(id)
83
107
  result.onSuccess { ad ->
84
108
  ad.listener = makeListener(adUnitIdentifier)
85
109
  rewardedAds[id] = ad
@@ -97,6 +121,10 @@ class EzoicAdsModule(reactContext: ReactApplicationContext) :
97
121
  promise.reject("EzoicAds", "Rewarded ad not loaded for $adUnitIdentifier")
98
122
  return
99
123
  }
124
+ if (pendingShows.containsKey(id)) {
125
+ promise.reject("EzoicAds", "A show is already in progress for ad unit $adUnitIdentifier")
126
+ return
127
+ }
100
128
  val activity = currentActivity
101
129
  if (activity == null) {
102
130
  promise.reject("EzoicAds", "No current Activity to present the rewarded ad")
@@ -138,6 +166,75 @@ class EzoicAdsModule(reactContext: ReactApplicationContext) :
138
166
  }
139
167
  }
140
168
 
169
+ override fun loadInterstitialAd(adUnitIdentifier: String, promise: Promise) {
170
+ val id = adUnitIdentifier.toIntOrNull()
171
+ if (id == null) {
172
+ promise.reject("EzoicAds", "Invalid adUnitIdentifier: $adUnitIdentifier")
173
+ return
174
+ }
175
+ if (interstitialAds.containsKey(id) || !loadingInterstitial.add(id)) {
176
+ promise.reject("EzoicAds", "An ad is already loaded/loading for ad unit $adUnitIdentifier")
177
+ return
178
+ }
179
+ EzoicInterstitialAd.load(reactApplicationContext, id) { result ->
180
+ loadingInterstitial.remove(id)
181
+ result.onSuccess { ad ->
182
+ ad.listener = makeInterstitialListener(adUnitIdentifier)
183
+ interstitialAds[id] = ad
184
+ promise.resolve(null)
185
+ }.onFailure { e ->
186
+ promise.reject("EzoicAds", e.message ?: "Interstitial ad failed to load", e)
187
+ }
188
+ }
189
+ }
190
+
191
+ override fun showInterstitialAd(adUnitIdentifier: String, promise: Promise) {
192
+ val id = adUnitIdentifier.toIntOrNull()
193
+ val ad = if (id != null) interstitialAds[id] else null
194
+ if (id == null || ad == null) {
195
+ promise.reject("EzoicAds", "Interstitial ad not loaded for $adUnitIdentifier")
196
+ return
197
+ }
198
+ if (pendingInterstitialShows.containsKey(id)) {
199
+ promise.reject("EzoicAds", "A show is already in progress for ad unit $adUnitIdentifier")
200
+ return
201
+ }
202
+ val activity = currentActivity
203
+ if (activity == null) {
204
+ promise.reject("EzoicAds", "No current Activity to present the interstitial ad")
205
+ return
206
+ }
207
+
208
+ pendingInterstitialShows[id] = InterstitialShow(promise)
209
+
210
+ // Native show(activity) has no completion lambda, so replace the load-time
211
+ // listener with one that settles the promise on terminal events
212
+ // (dismiss = resolve, failed-to-show = reject).
213
+ ad.listener = makeInterstitialListener(
214
+ adUnitIdentifier,
215
+ onDismiss = {
216
+ interstitialAds.remove(id)
217
+ val pending = pendingInterstitialShows.remove(id)
218
+ if (pending != null && !pending.settled) {
219
+ pending.settled = true
220
+ pending.promise.resolve(null)
221
+ }
222
+ },
223
+ onFailedToShow = { message ->
224
+ interstitialAds.remove(id)
225
+ val pending = pendingInterstitialShows.remove(id)
226
+ if (pending != null && !pending.settled) {
227
+ pending.settled = true
228
+ pending.promise.reject("EzoicAds", message)
229
+ }
230
+ }
231
+ )
232
+
233
+ activity.runOnUiThread {
234
+ ad.show(activity)
235
+ }
236
+ }
237
+
141
238
  override fun addListener(eventName: String) {
142
239
  // No-op: required by the React Native NativeEventEmitter contract.
143
240
  }
@@ -158,6 +255,7 @@ class EzoicAdsModule(reactContext: ReactApplicationContext) :
158
255
  override fun onRewardedAdFailedToShow(rewardedAd: EzoicRewardedAd, error: EzoicError) {
159
256
  emitRewardedEvent(adUnitIdentifier, "failedToShow") {
160
257
  putString("message", error.message)
258
+ putInt("code", error.code)
161
259
  }
162
260
  onFailedToShow?.invoke(error.message)
163
261
  }
@@ -197,11 +295,57 @@ class EzoicAdsModule(reactContext: ReactApplicationContext) :
197
295
  .emit(REWARDED_EVENT, map)
198
296
  }
199
297
 
298
+ private fun makeInterstitialListener(
299
+ adUnitIdentifier: String,
300
+ onDismiss: (() -> Unit)? = null,
301
+ onFailedToShow: ((String) -> Unit)? = null
302
+ ): EzoicInterstitialAdListener = object : EzoicInterstitialAdListenerAdapter() {
303
+ override fun onInterstitialAdShown(interstitialAd: EzoicInterstitialAd) {
304
+ emitInterstitialEvent(adUnitIdentifier, "shown")
305
+ }
306
+
307
+ override fun onInterstitialAdFailedToShow(interstitialAd: EzoicInterstitialAd, error: EzoicError) {
308
+ emitInterstitialEvent(adUnitIdentifier, "failedToShow") {
309
+ putString("message", error.message)
310
+ putInt("code", error.code)
311
+ }
312
+ onFailedToShow?.invoke(error.message)
313
+ }
314
+
315
+ override fun onInterstitialAdImpression(interstitialAd: EzoicInterstitialAd) {
316
+ emitInterstitialEvent(adUnitIdentifier, "impression")
317
+ }
318
+
319
+ override fun onInterstitialAdClicked(interstitialAd: EzoicInterstitialAd) {
320
+ emitInterstitialEvent(adUnitIdentifier, "clicked")
321
+ }
322
+
323
+ override fun onInterstitialAdDismissed(interstitialAd: EzoicInterstitialAd) {
324
+ emitInterstitialEvent(adUnitIdentifier, "dismissed")
325
+ onDismiss?.invoke()
326
+ }
327
+ }
328
+
329
+ private fun emitInterstitialEvent(
330
+ adUnitIdentifier: String,
331
+ type: String,
332
+ extra: (WritableMap.() -> Unit)? = null
333
+ ) {
334
+ val map = Arguments.createMap()
335
+ map.putString("adUnitIdentifier", adUnitIdentifier)
336
+ map.putString("type", type)
337
+ extra?.invoke(map)
338
+ reactApplicationContext
339
+ .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
340
+ .emit(INTERSTITIAL_EVENT, map)
341
+ }
342
+
200
343
  private fun ReadableMap.optBool(key: String, default: Boolean): Boolean =
201
344
  if (hasKey(key) && !isNull(key)) getBoolean(key) else default
202
345
 
203
346
  companion object {
204
347
  const val NAME = NativeEzoicAdsSpec.NAME
205
348
  private const val REWARDED_EVENT = "EzoicRewardedAdEvent"
349
+ private const val INTERSTITIAL_EVENT = "EzoicInterstitialAdEvent"
206
350
  }
207
351
  }
@@ -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
+ }
@@ -19,7 +19,10 @@ class EzoicReactNativeSdkPackage : BaseReactPackage() {
19
19
  override fun createViewManagers(
20
20
  reactContext: ReactApplicationContext
21
21
  ): List<ViewManager<*, *>> {
22
- return listOf(EzoicBannerViewManager(reactContext))
22
+ return listOf(
23
+ EzoicBannerViewManager(reactContext),
24
+ EzoicNativeAdViewManager(reactContext)
25
+ )
23
26
  }
24
27
 
25
28
  override fun getReactModuleInfoProvider() = ReactModuleInfoProvider {