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.
- package/android/build.gradle +1 -1
- package/android/src/main/java/com/cloudxreactnative/CloudXAdEvents.kt +9 -0
- package/android/src/main/java/com/cloudxreactnative/CloudXAdInfo.kt +76 -0
- package/android/src/main/java/com/cloudxreactnative/CloudXModuleImpl.kt +110 -0
- package/android/src/main/java/com/cloudxreactnative/CloudXNativeAdHolder.kt +184 -0
- package/android/src/main/java/com/cloudxreactnative/CloudXNativeAdView.kt +209 -70
- package/android/src/main/java/com/cloudxreactnative/CloudXNativeAdViewManagerImpl.kt +6 -0
- package/android/src/newarch/com/cloudxreactnative/CloudXModule.kt +9 -0
- package/android/src/newarch/com/cloudxreactnative/CloudXNativeAdViewManager.kt +2 -0
- package/android/src/oldarch/com/cloudxreactnative/CloudXModule.kt +14 -0
- package/android/src/oldarch/com/cloudxreactnative/CloudXNativeAdViewManager.kt +3 -0
- package/android/src/test/java/com/cloudxreactnative/CloudXNativeAdPreloadTest.kt +351 -0
- package/android/src/test/java/com/cloudxreactnative/FakeReadable.kt +2 -2
- package/cloudx-react-native.podspec +1 -1
- package/ios/CloudX.h +12 -0
- package/ios/CloudX.mm +94 -1
- package/ios/CloudXNativeAdHolder.h +66 -0
- package/ios/CloudXNativeAdHolder.mm +247 -0
- package/ios/CloudXNativeAdView.h +9 -0
- package/ios/CloudXNativeAdView.mm +308 -64
- package/ios/CloudXNativeAdViewManager.mm +7 -0
- package/lib/commonjs/CloudXNativeAd.js +178 -0
- package/lib/commonjs/CloudXNativeAd.js.map +1 -0
- package/lib/commonjs/CloudXNativeAdView.js +58 -11
- package/lib/commonjs/CloudXNativeAdView.js.map +1 -1
- package/lib/commonjs/hooks.js +203 -0
- package/lib/commonjs/hooks.js.map +1 -1
- package/lib/commonjs/index.js +13 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/specs/CloudXNativeAdViewNativeComponent.js +2 -0
- package/lib/commonjs/specs/CloudXNativeAdViewNativeComponent.js.map +1 -1
- package/lib/commonjs/specs/NativeCloudX.js.map +1 -1
- package/lib/commonjs/types/NativeAd.js +21 -1
- package/lib/commonjs/types/NativeAd.js.map +1 -1
- package/lib/commonjs/types/index.js.map +1 -1
- package/lib/commonjs/version.js +1 -1
- package/lib/module/CloudXNativeAd.js +170 -0
- package/lib/module/CloudXNativeAd.js.map +1 -0
- package/lib/module/CloudXNativeAdView.js +58 -11
- package/lib/module/CloudXNativeAdView.js.map +1 -1
- package/lib/module/hooks.js +202 -0
- package/lib/module/hooks.js.map +1 -1
- package/lib/module/index.js +2 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/specs/CloudXNativeAdViewNativeComponent.js +2 -0
- package/lib/module/specs/CloudXNativeAdViewNativeComponent.js.map +1 -1
- package/lib/module/specs/NativeCloudX.js.map +1 -1
- package/lib/module/types/NativeAd.js +26 -1
- package/lib/module/types/NativeAd.js.map +1 -1
- package/lib/module/types/index.js.map +1 -1
- package/lib/module/version.js +1 -1
- package/lib/typescript/src/CloudXNativeAd.d.ts +58 -0
- package/lib/typescript/src/CloudXNativeAd.d.ts.map +1 -0
- package/lib/typescript/src/CloudXNativeAdView.d.ts +6 -13
- package/lib/typescript/src/CloudXNativeAdView.d.ts.map +1 -1
- package/lib/typescript/src/hooks.d.ts +30 -0
- package/lib/typescript/src/hooks.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +4 -3
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/specs/CloudXNativeAdViewNativeComponent.d.ts +23 -2
- package/lib/typescript/src/specs/CloudXNativeAdViewNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/specs/NativeCloudX.d.ts +13 -0
- package/lib/typescript/src/specs/NativeCloudX.d.ts.map +1 -1
- package/lib/typescript/src/types/NativeAd.d.ts +76 -13
- package/lib/typescript/src/types/NativeAd.d.ts.map +1 -1
- package/lib/typescript/src/types/index.d.ts +1 -1
- package/lib/typescript/src/types/index.d.ts.map +1 -1
- package/lib/typescript/src/version.d.ts +1 -1
- package/package.json +1 -1
- package/src/CloudXNativeAd.ts +177 -0
- package/src/CloudXNativeAdView.tsx +82 -15
- package/src/hooks.ts +236 -0
- package/src/index.ts +14 -1
- package/src/specs/CloudXNativeAdViewNativeComponent.ts +27 -2
- package/src/specs/NativeCloudX.ts +20 -0
- package/src/types/NativeAd.ts +85 -18
- package/src/types/index.ts +4 -0
- 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
|
}
|
|
@@ -14,7 +14,7 @@ Pod::Spec.new do |s|
|
|
|
14
14
|
|
|
15
15
|
s.requires_arc = true
|
|
16
16
|
|
|
17
|
-
s.dependency "CloudXCore", "~> 3.
|
|
17
|
+
s.dependency "CloudXCore", "~> 3.11.0"
|
|
18
18
|
|
|
19
19
|
s.source_files = "ios/**/*.{h,mm}"
|
|
20
20
|
s.resource_bundles = { 'CloudXReactNativePrivacy' => ['ios/PrivacyInfo.xcprivacy'] }
|
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
|