cloudx-react-native 3.10.1 → 3.11.1

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 (78) hide show
  1. package/android/build.gradle +1 -1
  2. package/android/src/main/java/com/cloudxreactnative/CloudXAdEvents.kt +9 -0
  3. package/android/src/main/java/com/cloudxreactnative/CloudXAdInfo.kt +76 -0
  4. package/android/src/main/java/com/cloudxreactnative/CloudXModuleImpl.kt +110 -0
  5. package/android/src/main/java/com/cloudxreactnative/CloudXNativeAdHolder.kt +184 -0
  6. package/android/src/main/java/com/cloudxreactnative/CloudXNativeAdView.kt +209 -70
  7. package/android/src/main/java/com/cloudxreactnative/CloudXNativeAdViewManagerImpl.kt +6 -0
  8. package/android/src/newarch/com/cloudxreactnative/CloudXModule.kt +9 -0
  9. package/android/src/newarch/com/cloudxreactnative/CloudXNativeAdViewManager.kt +2 -0
  10. package/android/src/oldarch/com/cloudxreactnative/CloudXModule.kt +14 -0
  11. package/android/src/oldarch/com/cloudxreactnative/CloudXNativeAdViewManager.kt +3 -0
  12. package/android/src/test/java/com/cloudxreactnative/CloudXNativeAdPreloadTest.kt +351 -0
  13. package/android/src/test/java/com/cloudxreactnative/FakeReadable.kt +2 -2
  14. package/cloudx-react-native.podspec +1 -1
  15. package/ios/CloudX.h +12 -0
  16. package/ios/CloudX.mm +94 -1
  17. package/ios/CloudXNativeAdHolder.h +66 -0
  18. package/ios/CloudXNativeAdHolder.mm +247 -0
  19. package/ios/CloudXNativeAdView.h +9 -0
  20. package/ios/CloudXNativeAdView.mm +308 -64
  21. package/ios/CloudXNativeAdViewManager.mm +7 -0
  22. package/lib/commonjs/CloudXNativeAd.js +178 -0
  23. package/lib/commonjs/CloudXNativeAd.js.map +1 -0
  24. package/lib/commonjs/CloudXNativeAdView.js +58 -11
  25. package/lib/commonjs/CloudXNativeAdView.js.map +1 -1
  26. package/lib/commonjs/hooks.js +203 -0
  27. package/lib/commonjs/hooks.js.map +1 -1
  28. package/lib/commonjs/index.js +13 -0
  29. package/lib/commonjs/index.js.map +1 -1
  30. package/lib/commonjs/specs/CloudXNativeAdViewNativeComponent.js +2 -0
  31. package/lib/commonjs/specs/CloudXNativeAdViewNativeComponent.js.map +1 -1
  32. package/lib/commonjs/specs/NativeCloudX.js.map +1 -1
  33. package/lib/commonjs/types/NativeAd.js +21 -1
  34. package/lib/commonjs/types/NativeAd.js.map +1 -1
  35. package/lib/commonjs/types/index.js.map +1 -1
  36. package/lib/commonjs/version.js +1 -1
  37. package/lib/module/CloudXNativeAd.js +170 -0
  38. package/lib/module/CloudXNativeAd.js.map +1 -0
  39. package/lib/module/CloudXNativeAdView.js +58 -11
  40. package/lib/module/CloudXNativeAdView.js.map +1 -1
  41. package/lib/module/hooks.js +202 -0
  42. package/lib/module/hooks.js.map +1 -1
  43. package/lib/module/index.js +2 -1
  44. package/lib/module/index.js.map +1 -1
  45. package/lib/module/specs/CloudXNativeAdViewNativeComponent.js +2 -0
  46. package/lib/module/specs/CloudXNativeAdViewNativeComponent.js.map +1 -1
  47. package/lib/module/specs/NativeCloudX.js.map +1 -1
  48. package/lib/module/types/NativeAd.js +26 -1
  49. package/lib/module/types/NativeAd.js.map +1 -1
  50. package/lib/module/types/index.js.map +1 -1
  51. package/lib/module/version.js +1 -1
  52. package/lib/typescript/src/CloudXNativeAd.d.ts +58 -0
  53. package/lib/typescript/src/CloudXNativeAd.d.ts.map +1 -0
  54. package/lib/typescript/src/CloudXNativeAdView.d.ts +6 -13
  55. package/lib/typescript/src/CloudXNativeAdView.d.ts.map +1 -1
  56. package/lib/typescript/src/hooks.d.ts +30 -0
  57. package/lib/typescript/src/hooks.d.ts.map +1 -1
  58. package/lib/typescript/src/index.d.ts +4 -3
  59. package/lib/typescript/src/index.d.ts.map +1 -1
  60. package/lib/typescript/src/specs/CloudXNativeAdViewNativeComponent.d.ts +23 -2
  61. package/lib/typescript/src/specs/CloudXNativeAdViewNativeComponent.d.ts.map +1 -1
  62. package/lib/typescript/src/specs/NativeCloudX.d.ts +13 -0
  63. package/lib/typescript/src/specs/NativeCloudX.d.ts.map +1 -1
  64. package/lib/typescript/src/types/NativeAd.d.ts +76 -13
  65. package/lib/typescript/src/types/NativeAd.d.ts.map +1 -1
  66. package/lib/typescript/src/types/index.d.ts +1 -1
  67. package/lib/typescript/src/types/index.d.ts.map +1 -1
  68. package/lib/typescript/src/version.d.ts +1 -1
  69. package/package.json +1 -1
  70. package/src/CloudXNativeAd.ts +177 -0
  71. package/src/CloudXNativeAdView.tsx +82 -15
  72. package/src/hooks.ts +236 -0
  73. package/src/index.ts +14 -1
  74. package/src/specs/CloudXNativeAdViewNativeComponent.ts +27 -2
  75. package/src/specs/NativeCloudX.ts +20 -0
  76. package/src/types/NativeAd.ts +85 -18
  77. package/src/types/index.ts +4 -0
  78. package/src/version.ts +1 -1
@@ -7,6 +7,7 @@ import android.view.View.MeasureSpec
7
7
  import android.view.ViewGroup
8
8
  import android.widget.ImageView
9
9
  import com.facebook.react.bridge.Arguments
10
+ import com.facebook.react.bridge.ReactContext
10
11
  import com.facebook.react.bridge.WritableMap
11
12
  import com.facebook.react.uimanager.ThemedReactContext
12
13
  import com.facebook.react.uimanager.UIManagerHelper
@@ -66,31 +67,52 @@ class CloudXNativeAdView(private val reactContext: ThemedReactContext) : ReactVi
66
67
  const val MEDIA_VIEW_RESIZE_RETRY_MS = 500L
67
68
  }
68
69
 
70
+ var nativeAdId: String? = null
71
+ set(value) {
72
+ if (field == value) return
73
+ field = value
74
+ boundHolder?.unbindView(this)
75
+ boundHolder = null
76
+ ownsLoader = (value == null)
77
+ if (value != null) {
78
+ boundHolder = resolveHolder(value)
79
+ }
80
+ if (isAttachedToWindow) {
81
+ if (value != null) {
82
+ bindPreloadedAd()
83
+ } else {
84
+ loadIfPossible()
85
+ }
86
+ }
87
+ }
88
+
89
+ internal var holderResolver: ((String) -> CloudXNativeAdHolder?)? = null
90
+
91
+ private var boundHolder: CloudXNativeAdHolder? = null
92
+
93
+ // Set once a preloaded ad has been presented here (template rendered, or
94
+ // the manual bind handed to JS), so a window re-attach (tab switch, screen
95
+ // transition) keeps the existing presentation instead of rendering again.
96
+ private var preloadedAdPresented = false
97
+ private var ownsLoader: Boolean = true
98
+
99
+ private fun resolveHolder(id: String): CloudXNativeAdHolder? {
100
+ holderResolver?.invoke(id)?.let { return it }
101
+ val reactContext = context as? ReactContext ?: return null
102
+ val module = reactContext.getNativeModule(CloudXModule::class.java)
103
+ ?: (reactContext as? ThemedReactContext)?.reactApplicationContext?.getNativeModule(CloudXModule::class.java)
104
+ return module?.getHolder(id)
105
+ }
106
+
69
107
  var adUnitId: String? = null
70
108
  set(value) {
71
109
  if (field == value) return
72
110
  field = value
73
- // The @ReactProp setters for `adUnitId` and `manualRender` fire in
74
- // declaration order, so eagerly loading from this setter would run
75
- // before `manualRender` has been applied — locking the view into
76
- // the template load path even when the publisher mounted asset-view
77
- // children. Defer to `onAttachedToWindow`, which RN guarantees to
78
- // fire after all props are set.
79
- //
80
- // A change AFTER a loader exists is a live re-target: the old
81
- // loader belongs to the old ad unit and must not keep serving
82
- // callbacks for this view.
83
111
  if (attachedLoader != null) {
84
112
  releaseLoaderState()
85
- // The same ordering trap described above applies to the
86
- // re-target: `placement`, `customData`, the video flags and the
87
- // extra parameters are applied AFTER `adUnitId`, so loading
88
- // synchronously here configured the new loader from the
89
- // previous unit's values. Post it instead, so the load runs
90
- // once the whole prop update has been applied.
91
113
  val target = value
92
- if (isAttachedToWindow) {
93
- post { if (adUnitId == target && isAttachedToWindow) loadIfPossible() }
114
+ if (isAttachedToWindow && nativeAdId == null) {
115
+ post { if (adUnitId == target && isAttachedToWindow && nativeAdId == null) loadIfPossible() }
94
116
  }
95
117
  }
96
118
  }
@@ -232,11 +254,7 @@ class CloudXNativeAdView(private val reactContext: ThemedReactContext) : ReactVi
232
254
  attachedLoader = loader
233
255
  loader.nativeAdListener = this
234
256
  loader.revenueListener = this
235
- loader.setPlacement(placement)
236
- loader.setCustomData(customData)
237
- loader.isVideoFullScreenDisabled = disableVideoFullScreen
238
- loader.isStartVideoUnmuted = startVideoUnmuted
239
- loader.isVideoMediaControlsHidden = hideVideoMediaControls
257
+ loader.applyLoadOptions(placement, customData, disableVideoFullScreen, startVideoUnmuted, hideVideoMediaControls)
240
258
  appliedExtraParameterKeys = emptySet()
241
259
  appliedLocalExtraParameterKeys = emptySet()
242
260
  applyExtraParametersIfReady()
@@ -277,7 +295,14 @@ class CloudXNativeAdView(private val reactContext: ThemedReactContext) : ReactVi
277
295
  .build()
278
296
  }
279
297
 
298
+ private fun createAndAttachTemplateView(): SdkNativeAdView {
299
+ val adView = SdkNativeAdView(context, buildBinder())
300
+ attachAdView(adView)
301
+ return adView
302
+ }
303
+
280
304
  private fun loadIfPossible() {
305
+ if (!ownsLoader || nativeAdId != null) return
281
306
  if (loadRequested) return
282
307
  val loader = currentLoader() ?: return
283
308
  loadRequested = true
@@ -298,19 +323,129 @@ class CloudXNativeAdView(private val reactContext: ThemedReactContext) : ReactVi
298
323
  // observer sees a window-backed view when it binds the creative;
299
324
  // attaching only from `onNativeAdLoaded` can miss the first
300
325
  // impression and its revenue callback.
301
- val adView = SdkNativeAdView(context, buildBinder())
302
- attachAdView(adView)
326
+ val adView = createAndAttachTemplateView()
303
327
  loader.loadAd(adView)
304
328
  }
305
329
  }
306
330
 
331
+ private fun bindPreloadedAd() {
332
+ val id = nativeAdId ?: return
333
+ val holder = boundHolder ?: resolveHolder(id)?.also { boundHolder = it }
334
+
335
+ if (preloadedAdPresented && holder != null && holder.boundView?.get() === this) return
336
+
337
+ if (holder == null || holder.isDestroyed) {
338
+ val error = CloudXErrorCode.AD_NOT_READY.toCloudXError("native ad destroyed")
339
+ dispatch(CloudXAdEvents.ON_NATIVE_AD_VIEW_DISPLAY_FAILED_EVENT, loadFailedPayload(adUnitId ?: "", error))
340
+ return
341
+ }
342
+
343
+ if (holder.expired) {
344
+ val error = CloudXErrorCode.ADAPTER_AD_EXPIRED.toCloudXError()
345
+ dispatch(CloudXAdEvents.ON_NATIVE_AD_VIEW_DISPLAY_FAILED_EVENT, loadFailedPayload(holder.adUnitId, error))
346
+ return
347
+ }
348
+
349
+ val ad = holder.ad
350
+ if (ad == null) {
351
+ val error = CloudXErrorCode.AD_NOT_READY.toCloudXError("native ad destroyed")
352
+ dispatch(CloudXAdEvents.ON_NATIVE_AD_VIEW_DISPLAY_FAILED_EVENT, loadFailedPayload(holder.adUnitId, error))
353
+ return
354
+ }
355
+
356
+ // Registering an ad with a second view lets some partners report a
357
+ // second impression, and this Android SDK counts it (CXD-6018). Refuse
358
+ // the re-show once the ad has registered with any view, rather than
359
+ // double-count; remove once the RN pin carries the core fix.
360
+ if (holder.registered && holder.boundView?.get() !== this) {
361
+ val error = CloudXErrorCode.AD_ALREADY_SHOWING.toCloudXError(
362
+ "native ad was already shown in another view; load a new ad to show it again"
363
+ )
364
+ Log.w(TAG, "Refusing to show native ad $id (ad unit ${holder.adUnitId}) in a second view: ${error.formattedMessage}")
365
+ dispatch(CloudXAdEvents.ON_NATIVE_AD_VIEW_DISPLAY_FAILED_EVENT, loadFailedPayload(holder.adUnitId, error))
366
+ return
367
+ }
368
+
369
+ if (manualRender) {
370
+ pendingManualAd = ad
371
+ loadedCycleAd = ad
372
+ holder.bindView(this)
373
+ preloadedAdPresented = true
374
+ dispatch(CloudXAdEvents.ON_NATIVE_AD_VIEW_LOADED_EVENT, infoPayload(ad))
375
+ } else {
376
+ // Take the ad from any previous view first: clearing that view
377
+ // removes the SDK media and options views it inserted, and those
378
+ // are the views render() is about to move into this template.
379
+ holder.bindView(this)
380
+ val adView = createAndAttachTemplateView()
381
+ holder.loader.render(adView, ad)
382
+ if (holder.expired) {
383
+ // An expiry fired by render() already cleared this bound view
384
+ // and reported it; report only if it did not reach us.
385
+ if (boundHolder != null) {
386
+ val error = CloudXErrorCode.ADAPTER_AD_EXPIRED.toCloudXError()
387
+ dispatch(CloudXAdEvents.ON_NATIVE_AD_VIEW_DISPLAY_FAILED_EVENT, loadFailedPayload(ad.adUnitId, error))
388
+ }
389
+ return
390
+ }
391
+ holder.markRegistered()
392
+ preloadedAdPresented = true
393
+ loadedCycleAd = ad
394
+ dispatch(CloudXAdEvents.ON_NATIVE_AD_VIEW_DISPLAYED_EVENT, infoPayload(ad))
395
+ }
396
+ }
397
+
307
398
  override fun onAttachedToWindow() {
308
399
  super.onAttachedToWindow()
309
- loadIfPossible()
400
+ if (nativeAdId != null) {
401
+ bindPreloadedAd()
402
+ } else {
403
+ loadIfPossible()
404
+ }
310
405
  }
311
406
 
407
+
312
408
  fun teardown() {
313
- releaseLoaderState()
409
+ if (!ownsLoader) {
410
+ boundHolder?.unbindView(this)
411
+ boundHolder = null
412
+ clearRenderedAd()
413
+ } else {
414
+ releaseLoaderState()
415
+ }
416
+ }
417
+
418
+ fun clearRenderedAd() {
419
+ // A preloaded ad leaving this view also takes its React-rendered text
420
+ // and images with it: tell the JS view so its asset components clear.
421
+ if (nativeAdId != null && (preloadedAdPresented || pendingManualAd != null || loadedCycleAd != null)) {
422
+ dispatch(CloudXAdEvents.ON_NATIVE_AD_VIEW_BINDING_CLEARED_EVENT, Arguments.createMap().apply { putString("nativeAdId", nativeAdId) })
423
+ }
424
+ preloadedAdPresented = false
425
+ removeInsertedSdkViews()
426
+ hostedAdView?.let { (it.parent as? ViewGroup)?.removeView(it) }
427
+ hostedAdView = null
428
+ pendingManualAd = null
429
+ loadedCycleAd = null
430
+ clickableViews.clear()
431
+ assetViewsByRole.clear()
432
+ }
433
+
434
+ fun onBoundNativeAdDestroyed() {
435
+ val unitId = adUnitId ?: boundHolder?.adUnitId ?: ""
436
+ boundHolder = null
437
+ clearRenderedAd()
438
+ val error = CloudXErrorCode.AD_NOT_READY.toCloudXError("native ad destroyed")
439
+ dispatch(CloudXAdEvents.ON_NATIVE_AD_VIEW_DISPLAY_FAILED_EVENT, loadFailedPayload(unitId, error))
440
+ }
441
+
442
+ /** The bound ad expired before its impression; the holder released it. */
443
+ fun onBoundNativeAdExpired() {
444
+ val unitId = boundHolder?.adUnitId ?: adUnitId ?: ""
445
+ boundHolder = null
446
+ clearRenderedAd()
447
+ val error = CloudXErrorCode.ADAPTER_AD_EXPIRED.toCloudXError("native ad expired")
448
+ dispatch(CloudXAdEvents.ON_NATIVE_AD_VIEW_DISPLAY_FAILED_EVENT, loadFailedPayload(unitId, error))
314
449
  }
315
450
 
316
451
  /**
@@ -320,10 +455,12 @@ class CloudXNativeAdView(private val reactContext: ThemedReactContext) : ReactVi
320
455
  * drops the view instance and when `adUnitId` is re-targeted live.
321
456
  */
322
457
  private fun releaseLoaderState() {
323
- attachedLoader?.let {
324
- it.nativeAdListener = null
325
- it.revenueListener = null
326
- it.destroy()
458
+ if (ownsLoader) {
459
+ attachedLoader?.let {
460
+ it.nativeAdListener = null
461
+ it.revenueListener = null
462
+ it.destroy()
463
+ }
327
464
  }
328
465
  attachedLoader = null
329
466
  removeInsertedSdkViews()
@@ -410,6 +547,30 @@ class CloudXNativeAdView(private val reactContext: ThemedReactContext) : ReactVi
410
547
  child.layout(0, 0, w, h)
411
548
  }
412
549
 
550
+ /**
551
+ * Sizes each child of this host that an adapter inserted to fill it. Some
552
+ * adapters register against a FrameLayout they insert into a
553
+ * non-FrameLayout container. This host never lays out children React did
554
+ * not declare, so that view stays 0x0 and the partner never sees its ad on
555
+ * screen: manual layouts reported no impression.
556
+ *
557
+ * React gives every view it creates its tag as the view id, so a view
558
+ * without an id was added natively. Only views that ask to match the host
559
+ * on both axes are sized: partners also insert their own small tracking
560
+ * views, and stretching one of those over the ad stops its impression.
561
+ */
562
+ private fun sizeAdapterInsertedChildren() {
563
+ for (i in 0 until childCount) {
564
+ val child = getChildAt(i)
565
+ val params = child.layoutParams ?: continue
566
+ val fillsHost = params.width == ViewGroup.LayoutParams.MATCH_PARENT &&
567
+ params.height == ViewGroup.LayoutParams.MATCH_PARENT
568
+ if (child.id == View.NO_ID && fillsHost && child !in insertedSdkViews) {
569
+ scheduleInsertedViewSizing(child, this, MEDIA_VIEW_RESIZE_RETRY_MS)
570
+ }
571
+ }
572
+ }
573
+
413
574
  /**
414
575
  * Sizes [child] on the next frame, and again after [retryDelayMs] when that
415
576
  * is non-zero. A single pass is not enough: the container's own bounds can
@@ -437,6 +598,7 @@ class CloudXNativeAdView(private val reactContext: ThemedReactContext) : ReactVi
437
598
  // Commands
438
599
 
439
600
  fun commandLoadAd() {
601
+ if (!ownsLoader) return
440
602
  loadRequested = false
441
603
  loadIfPossible()
442
604
  }
@@ -523,7 +685,7 @@ class CloudXNativeAdView(private val reactContext: ThemedReactContext) : ReactVi
523
685
  val ad = pendingManualAd
524
686
  if (ad == null) {
525
687
  val error = CloudXErrorCode.ADAPTER_INTERNAL_ERROR.toCloudXError("renderNativeAd called before ad loaded")
526
- dispatch(CloudXAdEvents.ON_NATIVE_AD_VIEW_DISPLAY_FAILED_EVENT, loadFailedPayload(adUnitId ?: "", error))
688
+ dispatch(CloudXAdEvents.ON_NATIVE_AD_VIEW_DISPLAY_FAILED_EVENT, loadFailedPayload(adUnitId ?: boundHolder?.adUnitId ?: "", error))
527
689
  return
528
690
  }
529
691
  val nativeAd = ad.nativeAd
@@ -538,15 +700,23 @@ class CloudXNativeAdView(private val reactContext: ThemedReactContext) : ReactVi
538
700
  return
539
701
  }
540
702
  val registered = nativeAd.prepareForInteraction(clickableViews.toList(), this)
703
+ if (registered) sizeAdapterInsertedChildren()
541
704
  if (!registered) {
542
705
  // A load that cannot register click/impression tracking is a
543
706
  // display failure: the ad renders but never produces revenue.
544
707
  val error = CloudXErrorCode.ADAPTER_INTERNAL_ERROR.toCloudXError("Native ad failed to register click/impression tracking")
545
708
  dispatch(CloudXAdEvents.ON_NATIVE_AD_VIEW_DISPLAY_FAILED_EVENT, loadFailedPayload(ad.adUnitId, error))
709
+ } else {
710
+ boundHolder?.let { holder ->
711
+ holder.bindView(this)
712
+ holder.markRegistered()
713
+ }
714
+ dispatch(CloudXAdEvents.ON_NATIVE_AD_VIEW_DISPLAYED_EVENT, infoPayload(ad))
546
715
  }
547
716
  }
548
717
 
549
718
  fun commandDestroyAd() {
719
+ if (!ownsLoader) return
550
720
  // Destroy whichever ad this cycle holds: manual mode stashes it in
551
721
  // pendingManualAd, template mode only in loadedCycleAd. Leaving the
552
722
  // template-mode ad alive would leak SDK-side resources.
@@ -594,6 +764,9 @@ class CloudXNativeAdView(private val reactContext: ThemedReactContext) : ReactVi
594
764
  adView?.let { attachAdView(it) }
595
765
  }
596
766
  dispatch(CloudXAdEvents.ON_NATIVE_AD_VIEW_LOADED_EVENT, infoPayload(ad))
767
+ if (!manualRender) {
768
+ dispatch(CloudXAdEvents.ON_NATIVE_AD_VIEW_DISPLAYED_EVENT, infoPayload(ad))
769
+ }
597
770
  }
598
771
 
599
772
  override fun onNativeAdLoadFailed(adUnitId: String, error: CloudXError) {
@@ -616,10 +789,12 @@ class CloudXNativeAdView(private val reactContext: ThemedReactContext) : ReactVi
616
789
  }
617
790
 
618
791
  override fun onNativeAdClicked(ad: CloudXAd) {
792
+ if (!ownsLoader) return
619
793
  dispatch(CloudXAdEvents.ON_NATIVE_AD_VIEW_CLICKED_EVENT, infoPayload(ad))
620
794
  }
621
795
 
622
796
  override fun onNativeAdExpired(ad: CloudXAd) {
797
+ if (!ownsLoader) return
623
798
  dispatch(CloudXAdEvents.ON_NATIVE_AD_VIEW_EXPIRED_EVENT, infoPayload(ad))
624
799
  // The ad is invalidated: release SDK-side resources and drop our
625
800
  // per-ad refs so the expired state can't leak into the next load
@@ -629,6 +804,7 @@ class CloudXNativeAdView(private val reactContext: ThemedReactContext) : ReactVi
629
804
  }
630
805
 
631
806
  override fun onNativeAdClosed(ad: CloudXAd) {
807
+ if (!ownsLoader) return
632
808
  dispatch(CloudXAdEvents.ON_NATIVE_AD_VIEW_CLOSED_EVENT, infoPayload(ad))
633
809
  // Mirrors onNativeAdExpired: the ad is done, so detach its view and
634
810
  // clear per-ad refs. Loader stays alive for the next load cycle.
@@ -639,6 +815,7 @@ class CloudXNativeAdView(private val reactContext: ThemedReactContext) : ReactVi
639
815
  // CloudXAdRevenueListener
640
816
 
641
817
  override fun onAdRevenuePaid(cloudXAd: CloudXAd) {
818
+ if (!ownsLoader) return
642
819
  dispatch(CloudXAdEvents.ON_NATIVE_AD_VIEW_REVENUE_PAID_EVENT, infoPayload(cloudXAd))
643
820
  }
644
821
 
@@ -677,42 +854,4 @@ class CloudXNativeAdView(private val reactContext: ThemedReactContext) : ReactVi
677
854
  map.putString("message", error.message)
678
855
  return map
679
856
  }
680
-
681
- /**
682
- * JSON-encoded native ad metadata for the codegen-typed `nativeAdJson`
683
- * string field (codegen cannot express a nested map here). Mirrors the
684
- * same field set as CloudXAdInfo.kt's imperative-event metadata map.
685
- */
686
- private fun nativeAdMetadataJson(nativeAd: CloudXNativeAd): String {
687
- val iconImageSource = nativeAd.icon?.let { icon ->
688
- if (icon.uri == null) {
689
- (icon.drawable as? BitmapDrawable)?.bitmap?.let(::bitmapToBase64Png)
690
- } else {
691
- null
692
- }
693
- }
694
- val json = JSONObject()
695
- json.put("title", nativeAd.title ?: JSONObject.NULL)
696
- json.put("advertiser", nativeAd.advertiser ?: JSONObject.NULL)
697
- json.put("body", nativeAd.body ?: JSONObject.NULL)
698
- json.put("callToAction", nativeAd.callToAction ?: JSONObject.NULL)
699
- nativeAd.starRating?.let { json.put("starRating", it) }
700
- if (nativeAd.mediaContentAspectRatio > 0f) {
701
- json.put("mediaContentAspectRatio", nativeAd.mediaContentAspectRatio.toDouble())
702
- }
703
- val iconAvailable = nativeAd.icon != null
704
- json.put("isIconImageAvailable", iconAvailable)
705
- json.put("isOptionsViewAvailable", nativeAd.optionsView != null)
706
- json.put("isMediaViewAvailable", nativeAd.mediaView != null)
707
- nativeAd.icon?.let { icon ->
708
- icon.uri?.let { json.put("url", it.toString()) }
709
- if (icon.uri == null) {
710
- iconImageSource?.let { json.put("imageSource", it) }
711
- }
712
- }
713
- json.put("isVideoContent", nativeAd.isVideoContent)
714
- json.put("videoDuration", nativeAd.videoDuration)
715
- json.put("isExpired", nativeAd.isExpired)
716
- return json.toString()
717
- }
718
857
  }
@@ -23,7 +23,9 @@ object CloudXNativeAdViewManagerImpl {
23
23
  mutableMapOf(
24
24
  CloudXAdEvents.ON_NATIVE_AD_VIEW_LOADED_EVENT to eventRegistration(CloudXAdEvents.ON_NATIVE_AD_VIEW_LOADED_EVENT),
25
25
  CloudXAdEvents.ON_NATIVE_AD_VIEW_LOAD_FAILED_EVENT to eventRegistration(CloudXAdEvents.ON_NATIVE_AD_VIEW_LOAD_FAILED_EVENT),
26
+ CloudXAdEvents.ON_NATIVE_AD_VIEW_DISPLAYED_EVENT to eventRegistration(CloudXAdEvents.ON_NATIVE_AD_VIEW_DISPLAYED_EVENT),
26
27
  CloudXAdEvents.ON_NATIVE_AD_VIEW_DISPLAY_FAILED_EVENT to eventRegistration(CloudXAdEvents.ON_NATIVE_AD_VIEW_DISPLAY_FAILED_EVENT),
28
+ CloudXAdEvents.ON_NATIVE_AD_VIEW_BINDING_CLEARED_EVENT to eventRegistration(CloudXAdEvents.ON_NATIVE_AD_VIEW_BINDING_CLEARED_EVENT),
27
29
  CloudXAdEvents.ON_NATIVE_AD_VIEW_CLICKED_EVENT to eventRegistration(CloudXAdEvents.ON_NATIVE_AD_VIEW_CLICKED_EVENT),
28
30
  CloudXAdEvents.ON_NATIVE_AD_VIEW_EXPIRED_EVENT to eventRegistration(CloudXAdEvents.ON_NATIVE_AD_VIEW_EXPIRED_EVENT),
29
31
  CloudXAdEvents.ON_NATIVE_AD_VIEW_CLOSED_EVENT to eventRegistration(CloudXAdEvents.ON_NATIVE_AD_VIEW_CLOSED_EVENT),
@@ -32,6 +34,10 @@ object CloudXNativeAdViewManagerImpl {
32
34
 
33
35
  private fun eventRegistration(name: String): Map<String, String> = MapBuilder.of("registrationName", name)
34
36
 
37
+ fun setNativeAdId(view: CloudXNativeAdView, nativeAdId: String?) {
38
+ view.nativeAdId = nativeAdId
39
+ }
40
+
35
41
  fun setAdUnitId(view: CloudXNativeAdView, adUnitId: String?) {
36
42
  view.adUnitId = adUnitId
37
43
  }
@@ -80,6 +80,15 @@ class CloudXModule(reactContext: ReactApplicationContext) :
80
80
  @ReactMethod override fun showRewarded(adUnitId: String, placement: String?, customData: String?) = impl.showRewarded(adUnitId, placement, customData)
81
81
  @ReactMethod override fun destroyRewarded(adUnitId: String) = impl.destroyRewarded(adUnitId)
82
82
 
83
+ @ReactMethod override fun loadNativeAd(adUnitId: String, options: ReadableMap, promise: Promise) = impl.loadNativeAd(adUnitId, options, promise)
84
+ @ReactMethod override fun destroyNativeAd(nativeAdId: String) = impl.destroyNativeAd(nativeAdId)
85
+
86
+ fun getHolder(nativeAdId: String): CloudXNativeAdHolder? = impl.getHolder(nativeAdId)
87
+
88
+ override fun invalidate() {
89
+ impl.invalidate()
90
+ super.invalidate()
91
+ }
83
92
 
84
93
  @ReactMethod override fun addListener(eventName: String) {}
85
94
  @ReactMethod override fun removeListeners(count: Double) {}
@@ -37,6 +37,8 @@ class CloudXNativeAdViewManager :
37
37
  override fun getExportedCustomDirectEventTypeConstants(): MutableMap<String, Any> =
38
38
  CloudXNativeAdViewManagerImpl.getExportedCustomDirectEventTypeConstants()
39
39
 
40
+ override fun setNativeAdId(view: CloudXNativeAdView, value: String?) = CloudXNativeAdViewManagerImpl.setNativeAdId(view, value)
41
+
40
42
  override fun setAdUnitId(view: CloudXNativeAdView, value: String?) = CloudXNativeAdViewManagerImpl.setAdUnitId(view, value)
41
43
 
42
44
  override fun setTemplateName(view: CloudXNativeAdView, value: String?) = CloudXNativeAdViewManagerImpl.setTemplateName(view, value)
@@ -6,7 +6,12 @@ import com.facebook.react.bridge.ReactApplicationContext
6
6
  import com.facebook.react.bridge.ReactContextBaseJavaModule
7
7
  import com.facebook.react.bridge.ReactMethod
8
8
  import com.facebook.react.bridge.ReadableMap
9
+ import com.facebook.react.module.annotations.ReactModule
9
10
 
11
+ // Preloaded native ad views find their holder through
12
+ // getNativeModule(CloudXModule::class.java), which the legacy bridge resolves
13
+ // from this annotation.
14
+ @ReactModule(name = CloudXModuleImpl.MODULE_NAME)
10
15
  class CloudXModule(reactContext: ReactApplicationContext) :
11
16
  ReactContextBaseJavaModule(reactContext) {
12
17
 
@@ -95,6 +100,15 @@ class CloudXModule(reactContext: ReactApplicationContext) :
95
100
  @ReactMethod fun showRewarded(adUnitId: String, placement: String?, customData: String?) = impl.showRewarded(adUnitId, placement, customData)
96
101
  @ReactMethod fun destroyRewarded(adUnitId: String) = impl.destroyRewarded(adUnitId)
97
102
 
103
+ @ReactMethod fun loadNativeAd(adUnitId: String, options: ReadableMap, promise: Promise) = impl.loadNativeAd(adUnitId, options, promise)
104
+ @ReactMethod fun destroyNativeAd(nativeAdId: String) = impl.destroyNativeAd(nativeAdId)
105
+
106
+ fun getHolder(nativeAdId: String): CloudXNativeAdHolder? = impl.getHolder(nativeAdId)
107
+
108
+ override fun invalidate() {
109
+ impl.invalidate()
110
+ super.invalidate()
111
+ }
98
112
 
99
113
  @ReactMethod fun addListener(eventName: String) {}
100
114
  @ReactMethod fun removeListeners(count: Double) {}
@@ -29,6 +29,9 @@ class CloudXNativeAdViewManager : ViewGroupManager<CloudXNativeAdView>() {
29
29
  override fun getExportedCustomDirectEventTypeConstants(): MutableMap<String, Any> =
30
30
  CloudXNativeAdViewManagerImpl.getExportedCustomDirectEventTypeConstants()
31
31
 
32
+ @ReactProp(name = "nativeAdId")
33
+ fun setNativeAdId(view: CloudXNativeAdView, value: String?) = CloudXNativeAdViewManagerImpl.setNativeAdId(view, value)
34
+
32
35
  @ReactProp(name = "adUnitId")
33
36
  fun setAdUnitId(view: CloudXNativeAdView, value: String?) = CloudXNativeAdViewManagerImpl.setAdUnitId(view, value)
34
37