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
@@ -0,0 +1,351 @@
1
+ package com.cloudxreactnative
2
+
3
+ import com.facebook.react.bridge.Arguments
4
+ import com.facebook.react.bridge.Promise
5
+ import com.facebook.react.bridge.ReactApplicationContext
6
+ import com.facebook.react.bridge.WritableMap
7
+ import io.cloudx.sdk.CloudX
8
+ import io.cloudx.sdk.CloudXAd
9
+ import io.cloudx.sdk.CloudXAdFormat
10
+ import io.cloudx.sdk.CloudXNativeAdLoader
11
+ import io.mockk.every
12
+ import io.mockk.mockk
13
+ import io.mockk.mockkStatic
14
+ import io.mockk.unmockkAll
15
+ import io.mockk.verify
16
+ import kotlinx.coroutines.Dispatchers
17
+ import kotlinx.coroutines.ExperimentalCoroutinesApi
18
+ import kotlinx.coroutines.test.UnconfinedTestDispatcher
19
+ import kotlinx.coroutines.test.resetMain
20
+ import kotlinx.coroutines.test.setMain
21
+ import org.junit.After
22
+ import org.junit.Assert.assertEquals
23
+ import org.junit.Assert.assertFalse
24
+ import org.junit.Assert.assertNotNull
25
+ import org.junit.Assert.assertNull
26
+ import org.junit.Assert.assertSame
27
+ import org.junit.Assert.assertTrue
28
+ import org.junit.Before
29
+ import org.junit.Test
30
+
31
+ @OptIn(ExperimentalCoroutinesApi::class)
32
+ class CloudXNativeAdPreloadTest {
33
+
34
+ @Before
35
+ fun setUp() {
36
+ Dispatchers.setMain(UnconfinedTestDispatcher())
37
+ mockkStatic(CloudX::class)
38
+ mockkStatic(Arguments::class)
39
+ every { Arguments.createMap() } answers { mockk<WritableMap>(relaxed = true) }
40
+ }
41
+
42
+ @After
43
+ fun tearDown() {
44
+ unmockkAll()
45
+ Dispatchers.resetMain()
46
+ }
47
+
48
+ private fun mockAd(unitId: String = "unit-1"): CloudXAd {
49
+ val ad = mockk<CloudXAd>(relaxed = true)
50
+ val format = mockk<CloudXAdFormat>(relaxed = true)
51
+ every { format.name } returns "NATIVE"
52
+ every { ad.adUnitId } returns unitId
53
+ every { ad.adFormat } returns format
54
+ every { ad.networkName } returns "test-network"
55
+ every { ad.revenue } returns 0.05
56
+ every { ad.adValues } returns emptyMap()
57
+ every { ad.nativeAd } returns null
58
+ return ad
59
+ }
60
+
61
+ @Test
62
+ fun `loadNativeAd registers holder and getHolder retrieves it`() {
63
+ val context = mockk<ReactApplicationContext>(relaxed = true)
64
+ every { context.runOnUiQueueThread(any()) } answers { firstArg<Runnable>().run() }
65
+ val module = CloudXModuleImpl(context)
66
+
67
+ every { CloudX.isInitialized() } returns true
68
+ val loader = mockk<CloudXNativeAdLoader>(relaxed = true)
69
+ val listenerSlot = io.mockk.slot<io.cloudx.sdk.CloudXNativeAdListener>()
70
+ every { loader.nativeAdListener = capture(listenerSlot) } returns Unit
71
+ every { CloudX.createNativeAdLoader(any(), any()) } returns loader
72
+
73
+ val promise = mockk<Promise>(relaxed = true)
74
+ val options = FakeReadableMap(emptyMap())
75
+
76
+ module.loadNativeAd("ad-unit-1", options, promise)
77
+
78
+ verify(exactly = 1) { loader.loadAd() }
79
+
80
+ val holder = listenerSlot.captured as CloudXNativeAdHolder
81
+ assertEquals("ad-unit-1", holder.adUnitId)
82
+ val nativeAdId = holder.nativeAdId
83
+ assertNotNull(nativeAdId)
84
+
85
+ val foundHolder = module.getHolder(nativeAdId)
86
+ assertSame(holder, foundHolder)
87
+
88
+ // Simulate successful ad load
89
+ val ad = mockAd("ad-unit-1")
90
+ holder.onNativeAdLoaded(null, ad)
91
+
92
+ verify(exactly = 1) { promise.resolve(any()) }
93
+ }
94
+
95
+ @Test
96
+ fun `destroy is idempotent and releases loader and ad`() {
97
+ val loader = mockk<CloudXNativeAdLoader>(relaxed = true)
98
+ val ad = mockAd()
99
+ val onEvent = mockk<(String, WritableMap) -> Unit>(relaxed = true)
100
+
101
+ val holder = CloudXNativeAdHolder(
102
+ nativeAdId = "test-id",
103
+ adUnitId = "unit-1",
104
+ loader = loader,
105
+ onEvent = onEvent
106
+ )
107
+ holder.ad = ad
108
+
109
+ assertFalse(holder.isDestroyed)
110
+
111
+ // First destroy
112
+ holder.destroy()
113
+
114
+ assertTrue(holder.isDestroyed)
115
+ assertNull(holder.ad)
116
+ verify(exactly = 1) { loader.destroy(ad) }
117
+ verify(exactly = 1) { loader.destroy() }
118
+ verify(exactly = 1) { loader.nativeAdListener = null }
119
+ verify(exactly = 1) { loader.revenueListener = null }
120
+
121
+ // Second destroy (idempotent)
122
+ holder.destroy()
123
+
124
+ verify(exactly = 1) { loader.destroy(ad) }
125
+ verify(exactly = 1) { loader.destroy() }
126
+ }
127
+
128
+ @Test
129
+ fun `destroy while bound notifies bound view`() {
130
+ val loader = mockk<CloudXNativeAdLoader>(relaxed = true)
131
+ val view = mockk<CloudXNativeAdView>(relaxed = true)
132
+ val onEvent = mockk<(String, WritableMap) -> Unit>(relaxed = true)
133
+
134
+ val holder = CloudXNativeAdHolder(
135
+ nativeAdId = "test-id",
136
+ adUnitId = "unit-1",
137
+ loader = loader,
138
+ onEvent = onEvent
139
+ )
140
+ holder.bindView(view)
141
+ assertSame(view, holder.boundView?.get())
142
+
143
+ holder.destroy()
144
+
145
+ verify(exactly = 1) { view.onBoundNativeAdDestroyed() }
146
+ assertNull(holder.boundView)
147
+ }
148
+
149
+ @Test
150
+ fun `latest-bind-wins clears previous view without event`() {
151
+ val loader = mockk<CloudXNativeAdLoader>(relaxed = true)
152
+ val view1 = mockk<CloudXNativeAdView>(relaxed = true)
153
+ val view2 = mockk<CloudXNativeAdView>(relaxed = true)
154
+ val onEvent = mockk<(String, WritableMap) -> Unit>(relaxed = true)
155
+
156
+ val holder = CloudXNativeAdHolder(
157
+ nativeAdId = "test-id",
158
+ adUnitId = "unit-1",
159
+ loader = loader,
160
+ onEvent = onEvent
161
+ )
162
+
163
+ // Bind first view
164
+ holder.bindView(view1)
165
+ assertSame(view1, holder.boundView?.get())
166
+ verify(exactly = 0) { view1.clearRenderedAd() }
167
+
168
+ // Bind second view (latest wins)
169
+ holder.bindView(view2)
170
+ assertSame(view2, holder.boundView?.get())
171
+ verify(exactly = 1) { view1.clearRenderedAd() }
172
+ verify(exactly = 0) { view2.clearRenderedAd() }
173
+ }
174
+
175
+ @Test
176
+ fun `expiry before the impression emits expired once, clears the bound view and retains tombstone`() {
177
+ val loader = mockk<CloudXNativeAdLoader>(relaxed = true)
178
+ val ad = mockAd("unit-1")
179
+ val view = mockk<CloudXNativeAdView>(relaxed = true)
180
+ val events = mutableListOf<String>()
181
+
182
+ val holder = CloudXNativeAdHolder(
183
+ nativeAdId = "test-id",
184
+ adUnitId = "unit-1",
185
+ loader = loader,
186
+ onEvent = { eventName, _ -> events.add(eventName) }
187
+ )
188
+ holder.ad = ad
189
+ // Rendered into a view but never seen: no impression yet.
190
+ holder.bindView(view)
191
+ assertFalse(holder.impressed)
192
+
193
+ // First expiry
194
+ holder.onNativeAdExpired(ad)
195
+
196
+ assertTrue(holder.expired)
197
+ assertNull(holder.ad)
198
+ assertNull(holder.boundView)
199
+ verify(exactly = 1) { view.onBoundNativeAdExpired() }
200
+ verify(exactly = 1) { loader.destroy(ad) }
201
+ assertEquals(listOf(CloudXAdEvents.ON_NATIVE_AD_EXPIRED_EVENT), events)
202
+
203
+ // Second expiry: should not emit again
204
+ holder.onNativeAdExpired(ad)
205
+
206
+ assertEquals(1, events.size)
207
+ }
208
+
209
+ @Test
210
+ fun `expiry after the impression is ignored`() {
211
+ val loader = mockk<CloudXNativeAdLoader>(relaxed = true)
212
+ val ad = mockAd("unit-1")
213
+ val events = mutableListOf<String>()
214
+
215
+ val holder = CloudXNativeAdHolder(
216
+ nativeAdId = "test-id",
217
+ adUnitId = "unit-1",
218
+ loader = loader,
219
+ onEvent = { eventName, _ -> events.add(eventName) }
220
+ )
221
+ holder.ad = ad
222
+ // CloudX reports an impression with its revenue.
223
+ holder.onAdRevenuePaid(ad)
224
+ assertTrue(holder.impressed)
225
+
226
+ holder.onNativeAdExpired(ad)
227
+
228
+ assertTrue(holder.expired)
229
+ // Ad must NOT be destroyed or nulled out because it has been seen
230
+ assertSame(ad, holder.ad)
231
+ verify(exactly = 0) { loader.destroy(any<CloudXAd>()) }
232
+ assertEquals(listOf(CloudXAdEvents.ON_NATIVE_AD_REVENUE_PAID), events)
233
+ }
234
+
235
+ @Test
236
+ fun `an Activity going away keeps preloaded ads, invalidate releases them`() {
237
+ val context = mockk<ReactApplicationContext>(relaxed = true)
238
+ every { context.runOnUiQueueThread(any()) } answers { firstArg<Runnable>().run() }
239
+ val module = CloudXModuleImpl(context)
240
+
241
+ val loader = mockk<CloudXNativeAdLoader>(relaxed = true)
242
+ val slot = io.mockk.slot<io.cloudx.sdk.CloudXNativeAdListener>()
243
+ every { loader.nativeAdListener = capture(slot) } returns Unit
244
+ every { CloudX.isInitialized() } returns true
245
+ every { CloudX.createNativeAdLoader(any(), any()) } returns loader
246
+
247
+ module.loadNativeAd("unit-1", FakeReadableMap(emptyMap()), mockk<Promise>(relaxed = true))
248
+ val holder = slot.captured as CloudXNativeAdHolder
249
+
250
+ // A configuration change, or leaving the app and coming back: the
251
+ // Activity is destroyed while JS, and the handle, live on.
252
+ module.onHostDestroy()
253
+
254
+ assertFalse(holder.isDestroyed)
255
+ assertSame(holder, module.getHolder(holder.nativeAdId))
256
+ verify(exactly = 0) { loader.destroy() }
257
+
258
+ module.invalidate()
259
+
260
+ assertTrue(holder.isDestroyed)
261
+ assertNull(module.getHolder(holder.nativeAdId))
262
+ verify(exactly = 1) { loader.destroy() }
263
+ }
264
+
265
+ @Test
266
+ fun `module invalidate destroys all registered holders`() {
267
+ val context = mockk<ReactApplicationContext>(relaxed = true)
268
+ every { context.runOnUiQueueThread(any()) } answers { firstArg<Runnable>().run() }
269
+ val module = CloudXModuleImpl(context)
270
+
271
+ val loader1 = mockk<CloudXNativeAdLoader>(relaxed = true)
272
+ val loader2 = mockk<CloudXNativeAdLoader>(relaxed = true)
273
+ val slot1 = io.mockk.slot<io.cloudx.sdk.CloudXNativeAdListener>()
274
+ val slot2 = io.mockk.slot<io.cloudx.sdk.CloudXNativeAdListener>()
275
+ every { loader1.nativeAdListener = capture(slot1) } returns Unit
276
+ every { loader2.nativeAdListener = capture(slot2) } returns Unit
277
+ every { CloudX.isInitialized() } returns true
278
+ every { CloudX.createNativeAdLoader(any(), any()) } returnsMany listOf(loader1, loader2)
279
+
280
+ val promise1 = mockk<Promise>(relaxed = true)
281
+ val promise2 = mockk<Promise>(relaxed = true)
282
+ val options = FakeReadableMap(emptyMap())
283
+
284
+ module.loadNativeAd("unit-1", options, promise1)
285
+ module.loadNativeAd("unit-2", options, promise2)
286
+
287
+ val holder1 = slot1.captured as CloudXNativeAdHolder
288
+ val holder2 = slot2.captured as CloudXNativeAdHolder
289
+
290
+ assertNotNull(module.getHolder(holder1.nativeAdId))
291
+ assertNotNull(module.getHolder(holder2.nativeAdId))
292
+
293
+ // Invalidate
294
+ module.invalidate()
295
+
296
+ assertTrue(holder1.isDestroyed)
297
+ assertTrue(holder2.isDestroyed)
298
+ verify(exactly = 1) { loader1.destroy() }
299
+ verify(exactly = 1) { loader2.destroy() }
300
+
301
+ assertNull(module.getHolder(holder1.nativeAdId))
302
+ assertNull(module.getHolder(holder2.nativeAdId))
303
+ }
304
+
305
+ @Test
306
+ fun `destroyNativeAd destroys holder and removes from registry`() {
307
+ val context = mockk<ReactApplicationContext>(relaxed = true)
308
+ every { context.runOnUiQueueThread(any()) } answers { firstArg<Runnable>().run() }
309
+ val module = CloudXModuleImpl(context)
310
+
311
+ val loader = mockk<CloudXNativeAdLoader>(relaxed = true)
312
+ val slot = io.mockk.slot<io.cloudx.sdk.CloudXNativeAdListener>()
313
+ every { loader.nativeAdListener = capture(slot) } returns Unit
314
+ every { CloudX.isInitialized() } returns true
315
+ every { CloudX.createNativeAdLoader(any(), any()) } returns loader
316
+
317
+ val loadPromise = mockk<Promise>(relaxed = true)
318
+ module.loadNativeAd("unit-1", FakeReadableMap(emptyMap()), loadPromise)
319
+
320
+ val holder = slot.captured as CloudXNativeAdHolder
321
+ val nativeAdId = holder.nativeAdId
322
+
323
+ module.destroyNativeAd(nativeAdId)
324
+
325
+ assertTrue(holder.isDestroyed)
326
+ assertNull(module.getHolder(nativeAdId))
327
+ }
328
+
329
+ @Test
330
+ fun `holder click revenue and closed events route to onEvent with nativeAdId`() {
331
+ val loader = mockk<CloudXNativeAdLoader>(relaxed = true)
332
+ val ad = mockAd("unit-1")
333
+ val events = mutableListOf<Pair<String, WritableMap>>()
334
+
335
+ val holder = CloudXNativeAdHolder(
336
+ nativeAdId = "test-id",
337
+ adUnitId = "unit-1",
338
+ loader = loader,
339
+ onEvent = { eventName, map -> events.add(eventName to map) }
340
+ )
341
+
342
+ holder.onNativeAdClicked(ad)
343
+ holder.onAdRevenuePaid(ad)
344
+ holder.onNativeAdClosed(ad)
345
+
346
+ assertEquals(3, events.size)
347
+ assertEquals(CloudXAdEvents.ON_NATIVE_AD_CLICKED_EVENT, events[0].first)
348
+ assertEquals(CloudXAdEvents.ON_NATIVE_AD_REVENUE_PAID, events[1].first)
349
+ assertEquals(CloudXAdEvents.ON_NATIVE_AD_CLOSED_EVENT, events[2].first)
350
+ }
351
+ }
@@ -58,7 +58,7 @@ internal class FakeReadableMap(private val values: Map<String, Any?>) : Readable
58
58
  override fun getDynamic(name: String): Dynamic =
59
59
  throw UnsupportedOperationException("not used by the bridge")
60
60
 
61
- override fun toHashMap(): HashMap<String, Any?> =
61
+ override fun toHashMap(): HashMap<String, Any> =
62
62
  throw UnsupportedOperationException("not used by the bridge")
63
63
  }
64
64
 
@@ -96,6 +96,6 @@ internal class FakeReadableArray(private val values: List<Any?>) : ReadableArray
96
96
  override fun getDynamic(index: Int): Dynamic =
97
97
  throw UnsupportedOperationException("not used by the bridge")
98
98
 
99
- override fun toArrayList(): ArrayList<Any?> =
99
+ override fun toArrayList(): ArrayList<Any> =
100
100
  throw UnsupportedOperationException("not used by the bridge")
101
101
  }
package/ios/CloudX.h CHANGED
@@ -9,6 +9,13 @@
9
9
 
10
10
  #define DEVICE_SPECIFIC_ADVIEW_AD_FORMAT CLXBannerTypeW320H50
11
11
 
12
+ @class CloudXNativeAdHolder;
13
+
14
+ extern NSString *const ON_NATIVE_AD_CLICKED_EVENT;
15
+ extern NSString *const ON_NATIVE_AD_EXPIRED_EVENT;
16
+ extern NSString *const ON_NATIVE_AD_CLOSED_EVENT;
17
+ extern NSString *const ON_NATIVE_AD_REVENUE_PAID;
18
+
12
19
  #ifdef RCT_NEW_ARCH_ENABLED
13
20
  #import <CloudXSpec/CloudXSpec.h>
14
21
  @interface CloudX : RCTEventEmitter<NativeCloudXSpec, CLXInterstitialDelegate, CLXAppOpenDelegate, CLXRewardedDelegate, CLXBannerDelegate, CLXAdRevenueDelegate>
@@ -21,8 +28,13 @@ NS_ASSUME_NONNULL_BEGIN
21
28
 
22
29
  @property (nonatomic, strong, readonly, class) CloudX *shared;
23
30
 
31
+ /** Preloaded native ads held for this module's JS context, keyed by nativeAdId. Main thread only. */
32
+ @property (nonatomic, strong, readonly) NSMutableDictionary<NSString *, CloudXNativeAdHolder *> *nativeAdHolders;
33
+
24
34
  - (void)log:(NSString *)format, ...;
25
35
 
36
+ - (void)sendReactNativeEventWithName:(NSString *)name body:(NSDictionary<NSString *, id> *)body;
37
+
26
38
  /**
27
39
  * Builds the standard ad-info event payload from a @c CLXAd object.
28
40
  * Shared by native delegate callbacks so the JS layer always receives
package/ios/CloudX.mm CHANGED
@@ -5,6 +5,7 @@
5
5
 
6
6
  #import "CloudX.h"
7
7
  #import "CloudXExtraParameters.h"
8
+ #import "CloudXNativeAdHolder.h"
8
9
  #import <React/RCTUtils.h>
9
10
  #import <math.h>
10
11
 
@@ -61,6 +62,7 @@
61
62
  @property (nonatomic, strong) NSMutableDictionary<NSString *, CLXInterstitial *> *interstitials;
62
63
  @property (nonatomic, strong) NSMutableDictionary<NSString *, CLXAppOpen *> *appOpenAds;
63
64
  @property (nonatomic, strong) NSMutableDictionary<NSString *, CLXRewarded *> *rewardedAds;
65
+ @property (nonatomic, strong, readwrite) NSMutableDictionary<NSString *, CloudXNativeAdHolder *> *nativeAdHolders;
64
66
 
65
67
  // Overlay banner/MREC state — each ad unit ID owns exactly one CLXBannerAdView,
66
68
  // positioned window-level via NSLayoutConstraints against the root view controller.
@@ -127,6 +129,11 @@ static NSString *const ON_REWARDED_AD_HIDDEN_EVENT = @"OnRewardedAdHid
127
129
  static NSString *const ON_REWARDED_AD_RECEIVED_REWARD_EVENT = @"OnRewardedAdReceivedRewardEvent";
128
130
  static NSString *const ON_REWARDED_AD_REVENUE_PAID = @"OnRewardedAdRevenuePaid";
129
131
 
132
+ NSString *const ON_NATIVE_AD_CLICKED_EVENT = @"OnNativeAdClickedEvent";
133
+ NSString *const ON_NATIVE_AD_EXPIRED_EVENT = @"OnNativeAdExpiredEvent";
134
+ NSString *const ON_NATIVE_AD_CLOSED_EVENT = @"OnNativeAdClosedEvent";
135
+ NSString *const ON_NATIVE_AD_REVENUE_PAID = @"OnNativeAdRevenuePaid";
136
+
130
137
  static NSString *const TOP_CENTER = @"top_center";
131
138
  static NSString *const TOP_LEFT = @"top_left";
132
139
  static NSString *const TOP_RIGHT = @"top_right";
@@ -176,6 +183,7 @@ RCT_EXPORT_MODULE(CloudX)
176
183
  self.interstitials = [NSMutableDictionary dictionaryWithCapacity: 2];
177
184
  self.appOpenAds = [NSMutableDictionary dictionaryWithCapacity: 2];
178
185
  self.rewardedAds = [NSMutableDictionary dictionaryWithCapacity: 2];
186
+ self.nativeAdHolders = [NSMutableDictionary dictionary];
179
187
 
180
188
  self.adViews = [NSMutableDictionary dictionaryWithCapacity: 2];
181
189
  self.adViewAdFormats = [NSMutableDictionary dictionaryWithCapacity: 2];
@@ -223,6 +231,10 @@ RCT_EXPORT_MODULE(CloudX)
223
231
  {
224
232
  [self teardownAdViewForAdUnitIdentifier: adUnitIdentifier];
225
233
  }
234
+
235
+ // Only this module's own holders: by the time this block runs after a
236
+ // reload, the new module may already hold ads for the new JS context.
237
+ [CloudXNativeAdHolder destroyAllHoldersInRegistry: self.nativeAdHolders];
226
238
  } );
227
239
  }
228
240
 
@@ -654,6 +666,77 @@ RCT_EXPORT_METHOD(destroyRewarded:(NSString *)adUnitIdentifier)
654
666
  }
655
667
  }
656
668
 
669
+ #pragma mark - Native (Preload)
670
+
671
+ - (void)applyOptions:(NSDictionary<NSString *, id> *)options toNativeAdLoader:(CLXNativeAdLoader *)loader
672
+ {
673
+ NSDictionary *opts = [options isKindOfClass: [NSDictionary class]] ? options : @{};
674
+ NSString *(^string)(NSString *) = ^NSString *(NSString *key) {
675
+ id value = opts[key];
676
+ return [value isKindOfClass: [NSString class]] ? value : nil;
677
+ };
678
+ BOOL (^flag)(NSString *) = ^BOOL(NSString *key) {
679
+ id value = opts[key];
680
+ return [value respondsToSelector: @selector(boolValue)] && [value boolValue];
681
+ };
682
+ NSArray *(^entries)(NSString *) = ^NSArray *(NSString *key) {
683
+ id value = opts[key];
684
+ return [value isKindOfClass: [NSArray class]] ? value : nil;
685
+ };
686
+
687
+ [CloudXNativeAdHolder configureLoader: loader
688
+ placement: string(@"placement")
689
+ customData: string(@"customData")
690
+ disableVideoFullScreen: flag(@"disableVideoFullScreen")
691
+ startVideoUnmuted: flag(@"startVideoUnmuted")
692
+ hideVideoMediaControls: flag(@"hideVideoMediaControls")
693
+ extraParameterEntries: entries(@"extraParameters")
694
+ localExtraParameterEntries: entries(@"localExtraParameters")];
695
+ }
696
+
697
+ RCT_EXPORT_METHOD(loadNativeAd:(NSString *)adUnitId
698
+ options:(NSDictionary<NSString *, id> *)options
699
+ resolve:(RCTPromiseResolveBlock)resolve
700
+ reject:(RCTPromiseRejectBlock)reject)
701
+ {
702
+ if ( ![self isPluginInitialized] || ![CloudXCore shared].isInitialized )
703
+ {
704
+ reject(@"200", @"CloudX SDK is not initialized", nil);
705
+ return;
706
+ }
707
+
708
+ if ( adUnitId.length == 0 )
709
+ {
710
+ reject(@"300", @"Invalid or empty adUnitId", nil);
711
+ return;
712
+ }
713
+
714
+ NSString *nativeAdId = [[NSUUID UUID] UUIDString];
715
+ CLXNativeAdLoader *loader = [[CloudXCore shared] createNativeAdLoaderWithAdUnitIdentifier: adUnitId];
716
+ [self applyOptions: options toNativeAdLoader: loader];
717
+
718
+ CloudXNativeAdHolder *holder = [[CloudXNativeAdHolder alloc] initWithNativeAdId: nativeAdId
719
+ adUnitId: adUnitId
720
+ loader: loader];
721
+ loader.nativeAdDelegate = holder;
722
+ loader.revenueDelegate = holder;
723
+ holder.resolveBlock = resolve;
724
+ holder.rejectBlock = reject;
725
+
726
+ [CloudXNativeAdHolder registerHolder: holder];
727
+
728
+ [loader loadAd];
729
+ }
730
+
731
+ RCT_EXPORT_METHOD(destroyNativeAd:(NSString *)nativeAdId)
732
+ {
733
+ if ( nativeAdId.length == 0 ) return;
734
+ CloudXNativeAdHolder *holder = [CloudXNativeAdHolder holderForId: nativeAdId];
735
+ if ( !holder ) return;
736
+ [holder destroy];
737
+ [CloudXNativeAdHolder removeHolderForId: nativeAdId];
738
+ }
739
+
657
740
  #pragma mark - Ad Callbacks
658
741
 
659
742
  - (void)didLoadAd:(CLXAd *)ad
@@ -2219,7 +2302,12 @@ static const CGFloat kCloudXNativeIconMaxDimension = 96.0;
2219
2302
  ON_REWARDED_AD_DISPLAY_FAILED_EVENT,
2220
2303
  ON_REWARDED_AD_HIDDEN_EVENT,
2221
2304
  ON_REWARDED_AD_RECEIVED_REWARD_EVENT,
2222
- ON_REWARDED_AD_REVENUE_PAID];
2305
+ ON_REWARDED_AD_REVENUE_PAID,
2306
+
2307
+ ON_NATIVE_AD_CLICKED_EVENT,
2308
+ ON_NATIVE_AD_EXPIRED_EVENT,
2309
+ ON_NATIVE_AD_CLOSED_EVENT,
2310
+ ON_NATIVE_AD_REVENUE_PAID];
2223
2311
  }
2224
2312
 
2225
2313
  - (NSDictionary *)constantsToExport
@@ -2263,6 +2351,11 @@ static const CGFloat kCloudXNativeIconMaxDimension = 96.0;
2263
2351
  @"ON_REWARDED_AD_RECEIVED_REWARD_EVENT": ON_REWARDED_AD_RECEIVED_REWARD_EVENT,
2264
2352
  @"ON_REWARDED_AD_REVENUE_PAID": ON_REWARDED_AD_REVENUE_PAID,
2265
2353
 
2354
+ @"ON_NATIVE_AD_CLICKED_EVENT": ON_NATIVE_AD_CLICKED_EVENT,
2355
+ @"ON_NATIVE_AD_EXPIRED_EVENT": ON_NATIVE_AD_EXPIRED_EVENT,
2356
+ @"ON_NATIVE_AD_CLOSED_EVENT": ON_NATIVE_AD_CLOSED_EVENT,
2357
+ @"ON_NATIVE_AD_REVENUE_PAID": ON_NATIVE_AD_REVENUE_PAID,
2358
+
2266
2359
  @"TOP_CENTER_POSITION": TOP_CENTER,
2267
2360
  @"TOP_LEFT_POSITION": TOP_LEFT,
2268
2361
  @"TOP_RIGHT_POSITION": TOP_RIGHT,
@@ -0,0 +1,66 @@
1
+ //
2
+ // CloudXNativeAdHolder.h
3
+ // cloudx-react-native
4
+ //
5
+ // Retains a preloaded native ad and its loader, routes lifecycle/revenue
6
+ // events to the module event emitter, and manages view binding.
7
+ //
8
+
9
+ #import <Foundation/Foundation.h>
10
+ #import <CloudXCore/CloudXCore.h>
11
+ #import <React/RCTBridgeModule.h>
12
+
13
+ NS_ASSUME_NONNULL_BEGIN
14
+
15
+ @class CloudXNativeAdView;
16
+
17
+ @interface CloudXNativeAdHolder : NSObject <CLXNativeAdDelegate, CLXAdRevenueDelegate>
18
+
19
+ @property (nonatomic, copy, readonly) NSString *nativeAdId;
20
+ @property (nonatomic, copy, readonly) NSString *adUnitId;
21
+ @property (nonatomic, strong, readonly) CLXNativeAdLoader *loader;
22
+
23
+ @property (nonatomic, strong, nullable) CLXAd *ad;
24
+ @property (nonatomic, assign, getter=isExpired) BOOL expired;
25
+ // Set by the ad's first revenue, which CloudX reports on its impression.
26
+ // Rendering alone does not count, so a rendered ad that nobody saw still
27
+ // reports its expiry.
28
+ @property (nonatomic, assign, readonly, getter=isImpressed) BOOL impressed;
29
+ @property (nonatomic, weak, nullable) CloudXNativeAdView *boundView;
30
+
31
+ @property (nonatomic, copy, nullable) RCTPromiseResolveBlock resolveBlock;
32
+ @property (nonatomic, copy, nullable) RCTPromiseRejectBlock rejectBlock;
33
+
34
+ - (instancetype)initWithNativeAdId:(NSString *)nativeAdId
35
+ adUnitId:(NSString *)adUnitId
36
+ loader:(CLXNativeAdLoader *)loader NS_DESIGNATED_INITIALIZER;
37
+ - (instancetype)init NS_UNAVAILABLE;
38
+
39
+ - (void)destroy;
40
+
41
+ /**
42
+ * Applies load-time options to a native ad loader. Shared by the module's
43
+ * preload path and the view's auto-load path so both configure loaders
44
+ * identically. Extra parameters arrive as `{key, value}` entry arrays, the one
45
+ * wire format both callers receive from JS.
46
+ */
47
+ + (void)configureLoader:(CLXNativeAdLoader *)loader
48
+ placement:(nullable NSString *)placement
49
+ customData:(nullable NSString *)customData
50
+ disableVideoFullScreen:(BOOL)disableVideoFullScreen
51
+ startVideoUnmuted:(BOOL)startVideoUnmuted
52
+ hideVideoMediaControls:(BOOL)hideVideoMediaControls
53
+ extraParameterEntries:(nullable NSArray<NSDictionary *> *)extraParameterEntries
54
+ localExtraParameterEntries:(nullable NSArray<NSDictionary *> *)localExtraParameterEntries;
55
+
56
+ #pragma mark - Registry (main-thread confined, owned by the current CloudX module)
57
+
58
+ + (void)registerHolder:(CloudXNativeAdHolder *)holder;
59
+ + (nullable CloudXNativeAdHolder *)holderForId:(NSString *)nativeAdId;
60
+ + (void)removeHolderForId:(NSString *)nativeAdId;
61
+ /** Destroys and removes every holder in `registry`. */
62
+ + (void)destroyAllHoldersInRegistry:(NSMutableDictionary<NSString *, CloudXNativeAdHolder *> *)registry;
63
+
64
+ @end
65
+
66
+ NS_ASSUME_NONNULL_END