cloudx-react-native 3.11.0 → 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 (77) 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/ios/CloudX.h +12 -0
  15. package/ios/CloudX.mm +94 -1
  16. package/ios/CloudXNativeAdHolder.h +66 -0
  17. package/ios/CloudXNativeAdHolder.mm +247 -0
  18. package/ios/CloudXNativeAdView.h +9 -0
  19. package/ios/CloudXNativeAdView.mm +308 -64
  20. package/ios/CloudXNativeAdViewManager.mm +7 -0
  21. package/lib/commonjs/CloudXNativeAd.js +178 -0
  22. package/lib/commonjs/CloudXNativeAd.js.map +1 -0
  23. package/lib/commonjs/CloudXNativeAdView.js +58 -11
  24. package/lib/commonjs/CloudXNativeAdView.js.map +1 -1
  25. package/lib/commonjs/hooks.js +203 -0
  26. package/lib/commonjs/hooks.js.map +1 -1
  27. package/lib/commonjs/index.js +13 -0
  28. package/lib/commonjs/index.js.map +1 -1
  29. package/lib/commonjs/specs/CloudXNativeAdViewNativeComponent.js +2 -0
  30. package/lib/commonjs/specs/CloudXNativeAdViewNativeComponent.js.map +1 -1
  31. package/lib/commonjs/specs/NativeCloudX.js.map +1 -1
  32. package/lib/commonjs/types/NativeAd.js +21 -1
  33. package/lib/commonjs/types/NativeAd.js.map +1 -1
  34. package/lib/commonjs/types/index.js.map +1 -1
  35. package/lib/commonjs/version.js +1 -1
  36. package/lib/module/CloudXNativeAd.js +170 -0
  37. package/lib/module/CloudXNativeAd.js.map +1 -0
  38. package/lib/module/CloudXNativeAdView.js +58 -11
  39. package/lib/module/CloudXNativeAdView.js.map +1 -1
  40. package/lib/module/hooks.js +202 -0
  41. package/lib/module/hooks.js.map +1 -1
  42. package/lib/module/index.js +2 -1
  43. package/lib/module/index.js.map +1 -1
  44. package/lib/module/specs/CloudXNativeAdViewNativeComponent.js +2 -0
  45. package/lib/module/specs/CloudXNativeAdViewNativeComponent.js.map +1 -1
  46. package/lib/module/specs/NativeCloudX.js.map +1 -1
  47. package/lib/module/types/NativeAd.js +26 -1
  48. package/lib/module/types/NativeAd.js.map +1 -1
  49. package/lib/module/types/index.js.map +1 -1
  50. package/lib/module/version.js +1 -1
  51. package/lib/typescript/src/CloudXNativeAd.d.ts +58 -0
  52. package/lib/typescript/src/CloudXNativeAd.d.ts.map +1 -0
  53. package/lib/typescript/src/CloudXNativeAdView.d.ts +6 -13
  54. package/lib/typescript/src/CloudXNativeAdView.d.ts.map +1 -1
  55. package/lib/typescript/src/hooks.d.ts +30 -0
  56. package/lib/typescript/src/hooks.d.ts.map +1 -1
  57. package/lib/typescript/src/index.d.ts +4 -3
  58. package/lib/typescript/src/index.d.ts.map +1 -1
  59. package/lib/typescript/src/specs/CloudXNativeAdViewNativeComponent.d.ts +23 -2
  60. package/lib/typescript/src/specs/CloudXNativeAdViewNativeComponent.d.ts.map +1 -1
  61. package/lib/typescript/src/specs/NativeCloudX.d.ts +13 -0
  62. package/lib/typescript/src/specs/NativeCloudX.d.ts.map +1 -1
  63. package/lib/typescript/src/types/NativeAd.d.ts +76 -13
  64. package/lib/typescript/src/types/NativeAd.d.ts.map +1 -1
  65. package/lib/typescript/src/types/index.d.ts +1 -1
  66. package/lib/typescript/src/types/index.d.ts.map +1 -1
  67. package/lib/typescript/src/version.d.ts +1 -1
  68. package/package.json +1 -1
  69. package/src/CloudXNativeAd.ts +177 -0
  70. package/src/CloudXNativeAdView.tsx +82 -15
  71. package/src/hooks.ts +236 -0
  72. package/src/index.ts +14 -1
  73. package/src/specs/CloudXNativeAdViewNativeComponent.ts +27 -2
  74. package/src/specs/NativeCloudX.ts +20 -0
  75. package/src/types/NativeAd.ts +85 -18
  76. package/src/types/index.ts +4 -0
  77. package/src/version.ts +1 -1
@@ -158,7 +158,7 @@ repositories {
158
158
  dependencies {
159
159
  implementation "com.facebook.react:react-native:0.76.2"
160
160
 
161
- implementation "io.cloudx:sdk:4.10.0"
161
+ implementation "io.cloudx:sdk:4.12.0"
162
162
 
163
163
  testImplementation "junit:junit:4.13.2"
164
164
  testImplementation "io.mockk:mockk:1.13.10"
@@ -52,12 +52,21 @@ object CloudXAdEvents {
52
52
  const val ON_REWARDED_AD_RECEIVED_REWARD_EVENT = "OnRewardedAdReceivedRewardEvent"
53
53
  const val ON_REWARDED_AD_REVENUE_PAID = "OnRewardedAdRevenuePaid"
54
54
 
55
+ // Preloaded Native Ad Module Events
56
+ const val ON_NATIVE_AD_CLICKED_EVENT = "OnNativeAdClickedEvent"
57
+ const val ON_NATIVE_AD_EXPIRED_EVENT = "OnNativeAdExpiredEvent"
58
+ const val ON_NATIVE_AD_CLOSED_EVENT = "OnNativeAdClosedEvent"
59
+ const val ON_NATIVE_AD_REVENUE_PAID = "OnNativeAdRevenuePaid"
55
60
 
56
61
  // In-tree Native Ad View component (CloudXNativeAdView) per-view direct events.
57
62
  // These strings are both the event key and the JS prop registrationName.
58
63
  const val ON_NATIVE_AD_VIEW_LOADED_EVENT = "onAdLoadedEvent"
59
64
  const val ON_NATIVE_AD_VIEW_LOAD_FAILED_EVENT = "onAdLoadFailedEvent"
65
+ const val ON_NATIVE_AD_VIEW_DISPLAYED_EVENT = "onAdDisplayedEvent"
60
66
  const val ON_NATIVE_AD_VIEW_DISPLAY_FAILED_EVENT = "onAdDisplayFailedEvent"
67
+ // Internal: native released the view's ad (destroy, expiry, or a newer view
68
+ // took it), so the JS view clears its asset components. Not a publisher callback.
69
+ const val ON_NATIVE_AD_VIEW_BINDING_CLEARED_EVENT = "onAdBindingClearedEvent"
61
70
  const val ON_NATIVE_AD_VIEW_CLICKED_EVENT = "onAdClickedEvent"
62
71
  const val ON_NATIVE_AD_VIEW_EXPIRED_EVENT = "onAdExpiredEvent"
63
72
  const val ON_NATIVE_AD_VIEW_CLOSED_EVENT = "onAdClosedEvent"
@@ -1,11 +1,14 @@
1
1
  package com.cloudxreactnative
2
2
 
3
3
  import android.graphics.Bitmap
4
+ import android.graphics.drawable.BitmapDrawable
4
5
  import android.util.Base64
5
6
  import com.facebook.react.bridge.Arguments
6
7
  import com.facebook.react.bridge.WritableMap
7
8
  import io.cloudx.sdk.CloudXAd
8
9
  import io.cloudx.sdk.CloudXError
10
+ import io.cloudx.sdk.CloudXNativeAd
11
+ import org.json.JSONObject
9
12
  import java.io.ByteArrayOutputStream
10
13
 
11
14
  fun getAdInfo(ad: CloudXAd): WritableMap {
@@ -22,6 +25,79 @@ fun getAdInfo(ad: CloudXAd): WritableMap {
22
25
  return map
23
26
  }
24
27
 
28
+ /**
29
+ * Single source of truth for native ad metadata JSON serialization.
30
+ * Used by both CloudXNativeAdView's nativeAdJson string field and the
31
+ * preloaded module's nativeAd WritableMap dictionary.
32
+ */
33
+ fun nativeAdMetadataJsonObject(nativeAd: CloudXNativeAd): JSONObject {
34
+ val iconImageSource = nativeAd.icon?.let { icon ->
35
+ if (icon.uri == null) {
36
+ (icon.drawable as? BitmapDrawable)?.bitmap?.let(::bitmapToBase64Png)
37
+ } else {
38
+ null
39
+ }
40
+ }
41
+ val json = JSONObject()
42
+ json.put("title", nativeAd.title ?: JSONObject.NULL)
43
+ json.put("advertiser", nativeAd.advertiser ?: JSONObject.NULL)
44
+ json.put("body", nativeAd.body ?: JSONObject.NULL)
45
+ json.put("callToAction", nativeAd.callToAction ?: JSONObject.NULL)
46
+ nativeAd.starRating?.let { json.put("starRating", it) }
47
+ if (nativeAd.mediaContentAspectRatio > 0f) {
48
+ json.put("mediaContentAspectRatio", nativeAd.mediaContentAspectRatio.toDouble())
49
+ }
50
+ val iconAvailable = nativeAd.icon != null
51
+ json.put("isIconImageAvailable", iconAvailable)
52
+ json.put("isOptionsViewAvailable", nativeAd.optionsView != null)
53
+ json.put("isMediaViewAvailable", nativeAd.mediaView != null)
54
+ nativeAd.icon?.let { icon ->
55
+ icon.uri?.let { json.put("url", it.toString()) }
56
+ if (icon.uri == null) {
57
+ iconImageSource?.let { json.put("imageSource", it) }
58
+ }
59
+ }
60
+ json.put("isVideoContent", nativeAd.isVideoContent)
61
+ json.put("videoDuration", nativeAd.videoDuration)
62
+ json.put("isExpired", nativeAd.isExpired)
63
+ return json
64
+ }
65
+
66
+ fun nativeAdMetadataJson(nativeAd: CloudXNativeAd): String =
67
+ nativeAdMetadataJsonObject(nativeAd).toString()
68
+
69
+ fun nativeAdMetadataMap(nativeAd: CloudXNativeAd): WritableMap {
70
+ val json = nativeAdMetadataJsonObject(nativeAd)
71
+ val map = Arguments.createMap()
72
+ val keys = json.keys()
73
+ while (keys.hasNext()) {
74
+ val key = keys.next()
75
+ if (json.isNull(key)) {
76
+ map.putNull(key)
77
+ } else {
78
+ when (val value = json.get(key)) {
79
+ is Boolean -> map.putBoolean(key, value)
80
+ is Double -> map.putDouble(key, value)
81
+ is Number -> map.putDouble(key, value.toDouble())
82
+ is String -> map.putString(key, value)
83
+ else -> map.putString(key, value.toString())
84
+ }
85
+ }
86
+ }
87
+ return map
88
+ }
89
+
90
+ fun getNativeAdInfo(ad: CloudXAd, nativeAdId: String? = null): WritableMap {
91
+ val map = getAdInfo(ad)
92
+ ad.nativeAd?.let { nativeAd ->
93
+ map.putMap("nativeAd", nativeAdMetadataMap(nativeAd))
94
+ }
95
+ if (nativeAdId != null) {
96
+ map.putString("nativeAdId", nativeAdId)
97
+ }
98
+ return map
99
+ }
100
+
25
101
  /**
26
102
  * Icon encoding for CloudXNativeAdView's codegen-typed `nativeAdJson`
27
103
  * direct-event field, which can't carry a nested WritableMap. Capped at 96px
@@ -30,6 +30,7 @@ import io.cloudx.sdk.CloudXArbiterListener
30
30
  import io.cloudx.sdk.CloudXArbiterPrecision
31
31
  import io.cloudx.sdk.CloudXArbiterResult
32
32
  import io.cloudx.sdk.CloudXError
33
+ import io.cloudx.sdk.CloudXErrorCode
33
34
  import io.cloudx.sdk.CloudXInitializationConfiguration
34
35
  import io.cloudx.sdk.CloudXInitializationListener
35
36
  import io.cloudx.sdk.CloudXAppOpenAd
@@ -44,7 +45,9 @@ import io.cloudx.sdk.CloudXReward
44
45
  import io.cloudx.sdk.CloudXRewardedAd
45
46
  import io.cloudx.sdk.CloudXRewardedListener
46
47
  import io.cloudx.sdk.CloudXSdkConfiguration
48
+ import io.cloudx.sdk.CloudXNativeAdLoader
47
49
  import java.util.Locale
50
+ import java.util.UUID
48
51
  import java.util.concurrent.ConcurrentHashMap
49
52
 
50
53
  class CloudXModuleImpl(private val reactApplicationContext: ReactApplicationContext) : LifecycleEventListener {
@@ -56,6 +59,7 @@ class CloudXModuleImpl(private val reactApplicationContext: ReactApplicationCont
56
59
  private val interstitials = ConcurrentHashMap<String, CloudXInterstitialAd>()
57
60
  private val appOpenAds = ConcurrentHashMap<String, CloudXAppOpenAd>()
58
61
  private val rewardedAds = ConcurrentHashMap<String, CloudXRewardedAd>()
62
+ private val nativeAdHolders = ConcurrentHashMap<String, CloudXNativeAdHolder>()
59
63
  private val adViews = ConcurrentHashMap<String, SdkAdView>()
60
64
  private val adViewAdFormats = ConcurrentHashMap<String, CloudXAdFormat>()
61
65
  private val adViewPositions = ConcurrentHashMap<String, String>()
@@ -448,6 +452,91 @@ class CloudXModuleImpl(private val reactApplicationContext: ReactApplicationCont
448
452
  }
449
453
  }
450
454
 
455
+ // =========================================================================
456
+ // Native (Preloaded)
457
+ // =========================================================================
458
+
459
+ fun getHolder(nativeAdId: String): CloudXNativeAdHolder? = nativeAdHolders[nativeAdId]
460
+
461
+ fun loadNativeAd(adUnitId: String, options: ReadableMap, promise: Promise) {
462
+ onUiThread {
463
+ if (!CloudX.isInitialized()) {
464
+ promise.reject(
465
+ CloudXErrorCode.NOT_INITIALIZED.code.toString(),
466
+ "CloudX SDK is not initialized"
467
+ )
468
+ return@onUiThread
469
+ }
470
+ if (adUnitId.isBlank()) {
471
+ promise.reject(
472
+ CloudXErrorCode.INVALID_AD_UNIT.code.toString(),
473
+ "Invalid or empty adUnitId"
474
+ )
475
+ return@onUiThread
476
+ }
477
+
478
+ val nativeAdId = UUID.randomUUID().toString()
479
+ val loader = CloudX.createNativeAdLoader(reactApplicationContext, adUnitId)
480
+ applyNativeAdOptions(loader, options)
481
+
482
+ val holder = CloudXNativeAdHolder(
483
+ nativeAdId = nativeAdId,
484
+ adUnitId = adUnitId,
485
+ loader = loader,
486
+ onEvent = { eventName, payload -> sendReactNativeEvent(eventName, payload) },
487
+ onLoadSuccess = { adInfo ->
488
+ val result = Arguments.createMap()
489
+ result.putString("nativeAdId", nativeAdId)
490
+ result.putMap("adInfo", adInfo)
491
+ promise.resolve(result)
492
+ },
493
+ onLoadFailure = { error ->
494
+ promise.reject(error.code.code.toString(), error.message)
495
+ nativeAdHolders.remove(nativeAdId)
496
+ }
497
+ )
498
+
499
+ nativeAdHolders[nativeAdId] = holder
500
+ loader.nativeAdListener = holder
501
+ loader.revenueListener = holder
502
+
503
+ try {
504
+ loader.loadAd()
505
+ } catch (e: Exception) {
506
+ Log.e(TAG, "loadNativeAd: failed to start load", e)
507
+ promise.reject(CloudXErrorCode.INTERNAL_ERROR.code.toString(), e.message ?: "Internal error")
508
+ nativeAdHolders.remove(nativeAdId)
509
+ holder.destroy()
510
+ }
511
+ }
512
+ }
513
+
514
+ fun destroyNativeAd(nativeAdId: String) {
515
+ onUiThread {
516
+ if (nativeAdId.isBlank()) return@onUiThread
517
+ val holder = nativeAdHolders.remove(nativeAdId) ?: return@onUiThread
518
+ holder.destroy()
519
+ }
520
+ }
521
+
522
+ private fun applyNativeAdOptions(loader: CloudXNativeAdLoader, options: ReadableMap) {
523
+ fun string(key: String) = if (options.hasKey(key) && !options.isNull(key)) options.getString(key) else null
524
+ fun flag(key: String) = options.hasKey(key) && !options.isNull(key) && options.getBoolean(key)
525
+ // Extras arrive as {key, value} entries, the wire format CloudXNativeAdView also receives.
526
+ fun entries(key: String) =
527
+ if (options.hasKey(key) && !options.isNull(key)) CloudXExtraParameters.entryArrayToMap(options.getArray(key)) else emptyMap()
528
+
529
+ loader.applyLoadOptions(
530
+ placement = string("placement"),
531
+ customData = string("customData"),
532
+ disableVideoFullScreen = flag("disableVideoFullScreen"),
533
+ startVideoUnmuted = flag("startVideoUnmuted"),
534
+ hideVideoMediaControls = flag("hideVideoMediaControls"),
535
+ )
536
+ for ((key, value) in entries("extraParameters")) loader.setExtraParameter(key, value)
537
+ for ((key, value) in entries("localExtraParameters")) loader.setLocalExtraParameter(key, value)
538
+ }
539
+
451
540
  // =========================================================================
452
541
  // Listener Factories
453
542
  // =========================================================================
@@ -1205,6 +1294,23 @@ class CloudXModuleImpl(private val reactApplicationContext: ReactApplicationCont
1205
1294
  }
1206
1295
  rewardedAds.clear()
1207
1296
  }
1297
+ // Preloaded native ads are not released here. React Native calls
1298
+ // onHostDestroy whenever the Activity goes away while JS lives on (a
1299
+ // configuration change, or leaving the app and coming back), and a
1300
+ // held ad is the handle's until destroyNativeAd, expiry or module
1301
+ // invalidation. Their loaders use the application context, so they
1302
+ // hold nothing tied to the Activity.
1303
+ }
1304
+
1305
+ fun invalidate() {
1306
+ onHostDestroy()
1307
+ // JS is going away with the module, so no handle remains to own them.
1308
+ onUiThread {
1309
+ for (holder in nativeAdHolders.values) {
1310
+ holder.destroy()
1311
+ }
1312
+ nativeAdHolders.clear()
1313
+ }
1208
1314
  }
1209
1315
 
1210
1316
  // =========================================================================
@@ -1256,5 +1362,9 @@ class CloudXModuleImpl(private val reactApplicationContext: ReactApplicationCont
1256
1362
  "ON_REWARDED_AD_HIDDEN_EVENT" to CloudXAdEvents.ON_REWARDED_AD_HIDDEN_EVENT,
1257
1363
  "ON_REWARDED_AD_RECEIVED_REWARD_EVENT" to CloudXAdEvents.ON_REWARDED_AD_RECEIVED_REWARD_EVENT,
1258
1364
  "ON_REWARDED_AD_REVENUE_PAID" to CloudXAdEvents.ON_REWARDED_AD_REVENUE_PAID,
1365
+ "ON_NATIVE_AD_CLICKED_EVENT" to CloudXAdEvents.ON_NATIVE_AD_CLICKED_EVENT,
1366
+ "ON_NATIVE_AD_EXPIRED_EVENT" to CloudXAdEvents.ON_NATIVE_AD_EXPIRED_EVENT,
1367
+ "ON_NATIVE_AD_CLOSED_EVENT" to CloudXAdEvents.ON_NATIVE_AD_CLOSED_EVENT,
1368
+ "ON_NATIVE_AD_REVENUE_PAID" to CloudXAdEvents.ON_NATIVE_AD_REVENUE_PAID,
1259
1369
  )
1260
1370
  }
@@ -0,0 +1,184 @@
1
+ package com.cloudxreactnative
2
+
3
+ import android.util.Log
4
+ import com.facebook.react.bridge.WritableMap
5
+ import io.cloudx.sdk.CloudXAd
6
+ import io.cloudx.sdk.CloudXAdRevenueListener
7
+ import io.cloudx.sdk.CloudXError
8
+ import io.cloudx.sdk.CloudXErrorCode
9
+ import io.cloudx.sdk.CloudXNativeAdListener
10
+ import io.cloudx.sdk.CloudXNativeAdLoader
11
+ import io.cloudx.sdk.CloudXNativeAdView as SdkNativeAdView
12
+ import io.cloudx.sdk.toCloudXError
13
+ import java.lang.ref.WeakReference
14
+
15
+ /**
16
+ * Holds a preloaded native ad loaded without a view.
17
+ *
18
+ * Implements [CloudXNativeAdListener] and [CloudXAdRevenueListener] so all lifecycle
19
+ * and revenue events fire on the module handle. Bound views borrow the held ad for
20
+ * display without taking ownership of the loader or ad lifecycle.
21
+ */
22
+ class CloudXNativeAdHolder(
23
+ val nativeAdId: String,
24
+ val adUnitId: String,
25
+ val loader: CloudXNativeAdLoader,
26
+ private val onEvent: (String, WritableMap) -> Unit,
27
+ private var onLoadSuccess: ((WritableMap) -> Unit)? = null,
28
+ private var onLoadFailure: ((CloudXError) -> Unit)? = null,
29
+ ) : CloudXNativeAdListener, CloudXAdRevenueListener {
30
+
31
+ private companion object {
32
+ const val TAG = "CloudXNativeAdHolder"
33
+ }
34
+
35
+ var ad: CloudXAd? = null
36
+ var expired: Boolean = false
37
+
38
+ // Set by the ad's first revenue, which CloudX reports on its impression.
39
+ // Rendering alone does not count: an ad rendered into a view nobody saw
40
+ // can still move to another view, and its expiry is still reported.
41
+ var impressed: Boolean = false
42
+ private set
43
+
44
+ // Set once the ad has registered with a view for click and impression
45
+ // tracking. The Android one-view rule keys on this, not on the impression:
46
+ // the SDK tracks an impression before its revenue reaches this holder, so
47
+ // an impression-keyed rule leaves a window where a second view could
48
+ // register the ad and a partner could report it twice (CXD-6018).
49
+ var registered: Boolean = false
50
+ private set
51
+
52
+ fun markRegistered() {
53
+ registered = true
54
+ }
55
+ var boundView: WeakReference<CloudXNativeAdView>? = null
56
+ var isDestroyed: Boolean = false
57
+ private set
58
+
59
+ private var expiredEventEmitted: Boolean = false
60
+
61
+ override fun onNativeAdLoaded(adView: SdkNativeAdView?, ad: CloudXAd) {
62
+ if (isDestroyed) return
63
+ this.ad = ad
64
+ val callback = onLoadSuccess
65
+ onLoadSuccess = null
66
+ onLoadFailure = null
67
+ callback?.invoke(getNativeAdInfo(ad, nativeAdId))
68
+ }
69
+
70
+ override fun onNativeAdLoadFailed(adUnitId: String, error: CloudXError) {
71
+ if (isDestroyed) return
72
+ val callback = onLoadFailure
73
+ onLoadSuccess = null
74
+ onLoadFailure = null
75
+ callback?.invoke(error)
76
+ destroy()
77
+ }
78
+
79
+ override fun onNativeAdClicked(ad: CloudXAd) {
80
+ if (isDestroyed) return
81
+ val payload = getNativeAdInfo(ad, nativeAdId)
82
+ onEvent(CloudXAdEvents.ON_NATIVE_AD_CLICKED_EVENT, payload)
83
+ }
84
+
85
+ override fun onAdRevenuePaid(cloudXAd: CloudXAd) {
86
+ if (isDestroyed) return
87
+ impressed = true
88
+ val payload = getNativeAdInfo(cloudXAd, nativeAdId)
89
+ onEvent(CloudXAdEvents.ON_NATIVE_AD_REVENUE_PAID, payload)
90
+ }
91
+
92
+ override fun onNativeAdClosed(ad: CloudXAd) {
93
+ if (isDestroyed) return
94
+ val payload = getNativeAdInfo(ad, nativeAdId)
95
+ onEvent(CloudXAdEvents.ON_NATIVE_AD_CLOSED_EVENT, payload)
96
+ }
97
+
98
+ override fun onNativeAdExpired(ad: CloudXAd) {
99
+ expired = true
100
+ if (impressed) {
101
+ Log.d(TAG, "Native ad $nativeAdId expired after its impression; ignoring.")
102
+ return
103
+ }
104
+
105
+ if (!expiredEventEmitted) {
106
+ expiredEventEmitted = true
107
+ val payload = getNativeAdInfo(ad, nativeAdId)
108
+ onEvent(CloudXAdEvents.ON_NATIVE_AD_EXPIRED_EVENT, payload)
109
+ }
110
+
111
+ // A view may still show the ad (rendered, never seen): clear it there
112
+ // as well, so nothing keeps showing an ad that can no longer pay.
113
+ val view = boundView?.get()
114
+ boundView = null
115
+ view?.onBoundNativeAdExpired()
116
+
117
+ val currentAd = this.ad ?: ad
118
+ loader.destroy(currentAd)
119
+ this.ad = null
120
+ }
121
+
122
+ /**
123
+ * Binds a view to this holder.
124
+ * One holder -> at most ONE bound view at a time. Latest bind wins:
125
+ * the previous view clears its rendered ad without emitting an event.
126
+ */
127
+ fun bindView(newView: CloudXNativeAdView) {
128
+ val previousView = boundView?.get()
129
+ if (previousView != null && previousView !== newView) {
130
+ previousView.clearRenderedAd()
131
+ }
132
+ boundView = WeakReference(newView)
133
+ }
134
+
135
+ fun unbindView(view: CloudXNativeAdView) {
136
+ if (boundView?.get() === view) {
137
+ boundView = null
138
+ }
139
+ }
140
+
141
+ /**
142
+ * Clears delegates, notifies any bound view of destruction, destroys the ad
143
+ * and loader, and marks this holder destroyed.
144
+ */
145
+ fun destroy() {
146
+ if (isDestroyed) return
147
+ isDestroyed = true
148
+
149
+ onLoadFailure?.invoke(CloudXErrorCode.AD_NOT_READY.toCloudXError("native ad destroyed"))
150
+ onLoadSuccess = null
151
+ onLoadFailure = null
152
+
153
+ loader.nativeAdListener = null
154
+ loader.revenueListener = null
155
+
156
+ val view = boundView?.get()
157
+ boundView = null
158
+ view?.onBoundNativeAdDestroyed()
159
+
160
+ ad?.let { loader.destroy(it) }
161
+ ad = null
162
+
163
+ loader.destroy()
164
+ }
165
+ }
166
+
167
+ /**
168
+ * Applies load-time options to a native ad loader. Shared by the module's
169
+ * preload path and the view's auto-load path so both configure loaders the
170
+ * same way.
171
+ */
172
+ internal fun CloudXNativeAdLoader.applyLoadOptions(
173
+ placement: String?,
174
+ customData: String?,
175
+ disableVideoFullScreen: Boolean,
176
+ startVideoUnmuted: Boolean,
177
+ hideVideoMediaControls: Boolean,
178
+ ) {
179
+ setPlacement(placement)
180
+ setCustomData(customData)
181
+ isVideoFullScreenDisabled = disableVideoFullScreen
182
+ isStartVideoUnmuted = startVideoUnmuted
183
+ isVideoMediaControlsHidden = hideVideoMediaControls
184
+ }