cloudx-react-native 3.7.0 → 3.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/android/build.gradle +2 -1
  2. package/android/src/main/java/com/cloudxreactnative/CloudXAdInfo.kt +1 -0
  3. package/android/src/main/java/com/cloudxreactnative/CloudXAdView.kt +1 -0
  4. package/android/src/main/java/com/cloudxreactnative/CloudXModuleImpl.kt +185 -101
  5. package/android/src/newarch/com/cloudxreactnative/CloudXModule.kt +1 -0
  6. package/android/src/oldarch/com/cloudxreactnative/CloudXModule.kt +1 -0
  7. package/android/src/test/java/com/cloudxreactnative/CloudXModuleImplArbiterBidsTest.kt +3 -3
  8. package/android/src/test/java/com/cloudxreactnative/CloudXModuleImplThreadingTest.kt +261 -0
  9. package/android/src/test/java/com/cloudxreactnative/FakeReadable.kt +2 -2
  10. package/cloudx-react-native.podspec +2 -1
  11. package/ios/CloudX.mm +6 -0
  12. package/ios/CloudXAdView.mm +2 -0
  13. package/ios/PrivacyInfo.xcprivacy +20 -0
  14. package/lib/commonjs/CloudX.js +4 -0
  15. package/lib/commonjs/CloudX.js.map +1 -1
  16. package/lib/commonjs/CloudXAdViewBase.js +1 -0
  17. package/lib/commonjs/CloudXAdViewBase.js.map +1 -1
  18. package/lib/commonjs/CloudXErrorCode.js +2 -0
  19. package/lib/commonjs/CloudXErrorCode.js.map +1 -1
  20. package/lib/commonjs/specs/CloudXAdViewNativeComponent.js.map +1 -1
  21. package/lib/commonjs/specs/NativeCloudX.js.map +1 -1
  22. package/lib/commonjs/version.js +1 -1
  23. package/lib/commonjs/version.js.map +1 -1
  24. package/lib/module/CloudX.js +4 -0
  25. package/lib/module/CloudX.js.map +1 -1
  26. package/lib/module/CloudXAdViewBase.js +1 -0
  27. package/lib/module/CloudXAdViewBase.js.map +1 -1
  28. package/lib/module/CloudXErrorCode.js +2 -0
  29. package/lib/module/CloudXErrorCode.js.map +1 -1
  30. package/lib/module/specs/CloudXAdViewNativeComponent.js.map +1 -1
  31. package/lib/module/specs/NativeCloudX.js.map +1 -1
  32. package/lib/module/version.js +1 -1
  33. package/lib/module/version.js.map +1 -1
  34. package/lib/typescript/src/CloudX.d.ts.map +1 -1
  35. package/lib/typescript/src/CloudXAdViewBase.d.ts.map +1 -1
  36. package/lib/typescript/src/CloudXErrorCode.d.ts +2 -0
  37. package/lib/typescript/src/CloudXErrorCode.d.ts.map +1 -1
  38. package/lib/typescript/src/specs/CloudXAdViewNativeComponent.d.ts +1 -0
  39. package/lib/typescript/src/specs/CloudXAdViewNativeComponent.d.ts.map +1 -1
  40. package/lib/typescript/src/specs/NativeCloudX.d.ts +1 -0
  41. package/lib/typescript/src/specs/NativeCloudX.d.ts.map +1 -1
  42. package/lib/typescript/src/types/AdInfo.d.ts +2 -0
  43. package/lib/typescript/src/types/AdInfo.d.ts.map +1 -1
  44. package/lib/typescript/src/types/CloudX.d.ts +2 -0
  45. package/lib/typescript/src/types/CloudX.d.ts.map +1 -1
  46. package/lib/typescript/src/version.d.ts +1 -1
  47. package/lib/typescript/src/version.d.ts.map +1 -1
  48. package/package.json +1 -1
  49. package/src/CloudX.ts +5 -0
  50. package/src/CloudXAdViewBase.tsx +1 -0
  51. package/src/CloudXErrorCode.ts +3 -0
  52. package/src/specs/CloudXAdViewNativeComponent.ts +1 -0
  53. package/src/specs/NativeCloudX.ts +1 -0
  54. package/src/types/AdInfo.ts +3 -0
  55. package/src/types/CloudX.ts +3 -0
  56. package/src/version.ts +1 -1
@@ -158,8 +158,9 @@ repositories {
158
158
  dependencies {
159
159
  implementation "com.facebook.react:react-native:0.76.2"
160
160
 
161
- implementation "io.cloudx:sdk:4.5.0"
161
+ implementation "io.cloudx:sdk:4.9.0"
162
162
 
163
163
  testImplementation "junit:junit:4.13.2"
164
164
  testImplementation "io.mockk:mockk:1.13.10"
165
+ testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.2"
165
166
  }
@@ -12,6 +12,7 @@ fun getAdInfo(ad: CloudXAd): WritableMap {
12
12
  map.putString("networkName", ad.networkName)
13
13
  map.putString("networkPlacement", ad.networkPlacement ?: "")
14
14
  map.putString("placement", ad.placement ?: "")
15
+ map.putString("creativeId", ad.creativeId ?: "")
15
16
  map.putDouble("revenue", ad.revenue)
16
17
  map.putMap("adValues", ad.adValues.toWritableMap())
17
18
  return map
@@ -209,6 +209,7 @@ class CloudXAdView(private val reactContext: ThemedReactContext) : FrameLayout(r
209
209
  map.putString("networkName", ad.networkName)
210
210
  map.putString("networkPlacement", ad.networkPlacement ?: "")
211
211
  map.putString("placement", ad.placement ?: "")
212
+ map.putString("creativeId", ad.creativeId ?: "")
212
213
  map.putDouble("revenue", ad.revenue)
213
214
  /*
214
215
  * The SDK types adValues as Map<String, String>, so the cast is safe
@@ -9,6 +9,7 @@ import android.view.View
9
9
  import android.view.ViewGroup
10
10
  import android.widget.LinearLayout
11
11
  import android.widget.RelativeLayout
12
+ import androidx.annotation.VisibleForTesting
12
13
  import com.facebook.react.bridge.Arguments
13
14
  import com.facebook.react.bridge.LifecycleEventListener
14
15
  import com.facebook.react.bridge.Promise
@@ -87,42 +88,79 @@ class CloudXModuleImpl(private val reactApplicationContext: ReactApplicationCont
87
88
  return currentActivity
88
89
  }
89
90
 
91
+ // =========================================================================
92
+ // Threading
93
+ // =========================================================================
94
+
95
+ /*
96
+ * Every public module method runs its whole body on the UI thread, including reads of the
97
+ * module's own state. React Native invokes Android native-module methods on a shared
98
+ * background thread, so a method that did work inline could execute before a previously
99
+ * posted one (e.g. loadInterstitial() running on an instance that a queued
100
+ * destroyInterstitial() then destroys). Posting everything here keeps JS call order intact,
101
+ * makes the lazy retrieveX() create paths single-threaded, and matches the iOS module, whose
102
+ * methodQueue is the main queue.
103
+ */
104
+ private inline fun onUiThread(crossinline block: () -> Unit) {
105
+ reactApplicationContext.runOnUiQueueThread { block() }
106
+ }
107
+
90
108
  // =========================================================================
91
109
  // Initialization
92
110
  // =========================================================================
93
111
 
94
112
  fun isInitialized(promise: Promise) {
95
- promise.resolve(CloudX.isInitialized())
113
+ onUiThread {
114
+ promise.resolve(CloudX.isInitialized())
115
+ }
96
116
  }
97
117
 
98
- fun initialize(pluginVersion: String, appKey: String, promise: Promise) {
99
- val config = CloudXInitializationConfiguration.builder(appKey)
100
- .setPluginVersion(pluginVersion)
101
- .build()
102
-
103
- CloudX.initialize(reactApplicationContext, config, object : CloudXInitializationListener {
104
- override fun onInitialized(configuration: CloudXSdkConfiguration) {
105
- setupOrientationListener()
106
- val result = Arguments.createMap()
107
- result.putBoolean("success", true)
108
- result.putString("message", "SDK initialized successfully")
109
- promise.resolve(result)
118
+ fun showMediationDebugger(promise: Promise) {
119
+ onUiThread {
120
+ if (!CloudX.isInitialized()) {
121
+ promise.resolve(false)
122
+ return@onUiThread
110
123
  }
111
124
 
112
- override fun onInitializationFailed(cloudXError: CloudXError) {
113
- Log.e(TAG, "CloudX SDK initialization failed: ${cloudXError.message}")
114
- promise.reject(cloudXError.code.name, cloudXError.message, null)
115
- }
116
- })
125
+ CloudX.showMediationDebugger(maybeGetCurrentActivity() ?: reactApplicationContext)
126
+ promise.resolve(true)
127
+ }
128
+ }
129
+
130
+ fun initialize(pluginVersion: String, appKey: String, promise: Promise) {
131
+ onUiThread {
132
+ val config = CloudXInitializationConfiguration.builder(appKey)
133
+ .setPluginVersion(pluginVersion)
134
+ .build()
135
+
136
+ CloudX.initialize(reactApplicationContext, config, object : CloudXInitializationListener {
137
+ override fun onInitialized(configuration: CloudXSdkConfiguration) {
138
+ setupOrientationListener()
139
+ val result = Arguments.createMap()
140
+ result.putBoolean("success", true)
141
+ result.putString("message", "SDK initialized successfully")
142
+ promise.resolve(result)
143
+ }
144
+
145
+ override fun onInitializationFailed(cloudXError: CloudXError) {
146
+ Log.e(TAG, "CloudX SDK initialization failed: ${cloudXError.message}")
147
+ promise.reject(cloudXError.code.name, cloudXError.message, null)
148
+ }
149
+ })
150
+ }
117
151
  }
118
152
 
119
153
  fun isTablet(promise: Promise) {
120
- val isTablet = reactApplicationContext.resources.configuration.smallestScreenWidthDp >= 600
121
- promise.resolve(isTablet)
154
+ onUiThread {
155
+ val isTablet = reactApplicationContext.resources.configuration.smallestScreenWidthDp >= 600
156
+ promise.resolve(isTablet)
157
+ }
122
158
  }
123
159
 
124
160
  fun getVersion(promise: Promise) {
125
- promise.resolve(CloudX.getVersion())
161
+ onUiThread {
162
+ promise.resolve(CloudX.getVersion())
163
+ }
126
164
  }
127
165
 
128
166
  // =========================================================================
@@ -130,16 +168,18 @@ class CloudXModuleImpl(private val reactApplicationContext: ReactApplicationCont
130
168
  // =========================================================================
131
169
 
132
170
  fun setMinLogLevel(level: String) {
133
- val logLevel = when (level.lowercase()) {
134
- "verbose" -> CloudXLogLevel.VERBOSE
135
- "debug" -> CloudXLogLevel.DEBUG
136
- "info" -> CloudXLogLevel.INFO
137
- "warn" -> CloudXLogLevel.WARN
138
- "error" -> CloudXLogLevel.ERROR
139
- "none" -> CloudXLogLevel.NONE
140
- else -> CloudXLogLevel.INFO
171
+ onUiThread {
172
+ val logLevel = when (level.lowercase()) {
173
+ "verbose" -> CloudXLogLevel.VERBOSE
174
+ "debug" -> CloudXLogLevel.DEBUG
175
+ "info" -> CloudXLogLevel.INFO
176
+ "warn" -> CloudXLogLevel.WARN
177
+ "error" -> CloudXLogLevel.ERROR
178
+ "none" -> CloudXLogLevel.NONE
179
+ else -> CloudXLogLevel.INFO
180
+ }
181
+ CloudX.setMinLogLevel(logLevel)
141
182
  }
142
- CloudX.setMinLogLevel(logLevel)
143
183
  }
144
184
 
145
185
  fun setVisualDebuggingEnabled(enabled: Boolean) {
@@ -151,19 +191,27 @@ class CloudXModuleImpl(private val reactApplicationContext: ReactApplicationCont
151
191
  // =========================================================================
152
192
 
153
193
  fun setHashedUserID(hashedUserID: String) {
154
- CloudX.setHashedUserId(hashedUserID)
194
+ onUiThread {
195
+ CloudX.setHashedUserId(hashedUserID)
196
+ }
155
197
  }
156
198
 
157
199
  fun setUserKeyValue(key: String, value: String) {
158
- CloudX.setUserKeyValue(key, value)
200
+ onUiThread {
201
+ CloudX.setUserKeyValue(key, value)
202
+ }
159
203
  }
160
204
 
161
205
  fun setAppKeyValue(key: String, value: String) {
162
- CloudX.setAppKeyValue(key, value)
206
+ onUiThread {
207
+ CloudX.setAppKeyValue(key, value)
208
+ }
163
209
  }
164
210
 
165
211
  fun clearAllKeyValues() {
166
- CloudX.clearAllKeyValues()
212
+ onUiThread {
213
+ CloudX.clearAllKeyValues()
214
+ }
167
215
  }
168
216
 
169
217
  // =========================================================================
@@ -171,11 +219,15 @@ class CloudXModuleImpl(private val reactApplicationContext: ReactApplicationCont
171
219
  // =========================================================================
172
220
 
173
221
  fun setHasUserConsent(hasUserConsent: Boolean?) {
174
- CloudX.setHasUserConsent(hasUserConsent)
222
+ onUiThread {
223
+ CloudX.setHasUserConsent(hasUserConsent)
224
+ }
175
225
  }
176
226
 
177
227
  fun setDoNotSell(isDoNotSell: Boolean?) {
178
- CloudX.setDoNotSell(isDoNotSell)
228
+ onUiThread {
229
+ CloudX.setDoNotSell(isDoNotSell)
230
+ }
179
231
  }
180
232
 
181
233
  // =========================================================================
@@ -183,27 +235,31 @@ class CloudXModuleImpl(private val reactApplicationContext: ReactApplicationCont
183
235
  // =========================================================================
184
236
 
185
237
  fun arbiter(configuration: ReadableMap, promise: Promise) {
186
- val bids = arbiterBids(configuration)
187
- if (bids.isEmpty()) {
188
- promise.resolve(emptyArbiterResult())
189
- return
190
- }
191
-
192
- val arbiterConfiguration = CloudXArbiterConfiguration.builder(bids).build()
193
- CloudX.arbiter(arbiterConfiguration, object : CloudXArbiterListener {
194
- override fun onCompleted(result: CloudXArbiterResult) {
195
- promise.resolve(result.toWritableMap())
238
+ onUiThread {
239
+ val bids = arbiterBids(configuration)
240
+ if (bids.isEmpty()) {
241
+ promise.resolve(emptyArbiterResult())
242
+ return@onUiThread
196
243
  }
197
- })
244
+
245
+ val arbiterConfiguration = CloudXArbiterConfiguration.builder(bids).build()
246
+ CloudX.arbiter(arbiterConfiguration, object : CloudXArbiterListener {
247
+ override fun onCompleted(result: CloudXArbiterResult) {
248
+ promise.resolve(result.toWritableMap())
249
+ }
250
+ })
251
+ }
198
252
  }
199
253
 
200
254
  fun reportRevenueData(data: ReadableMap, promise: Promise) {
201
- val revenueData = data.toRevenueData()
202
- if (revenueData == null) {
203
- promise.resolve(false)
204
- return
255
+ onUiThread {
256
+ val revenueData = data.toRevenueData()
257
+ if (revenueData == null) {
258
+ promise.resolve(false)
259
+ return@onUiThread
260
+ }
261
+ promise.resolve(CloudX.reportRevenueData(revenueData))
205
262
  }
206
- promise.resolve(CloudX.reportRevenueData(revenueData))
207
263
  }
208
264
 
209
265
  // =========================================================================
@@ -216,8 +272,8 @@ class CloudXModuleImpl(private val reactApplicationContext: ReactApplicationCont
216
272
  fun setBannerPlacement(adUnitId: String, placement: String?) = setAdViewPlacement(adUnitId, CloudXAdFormat.BANNER, placement)
217
273
  fun setBannerCustomData(adUnitId: String, customData: String?) = setAdViewCustomData(adUnitId, CloudXAdFormat.BANNER, customData)
218
274
  fun setBannerExtraParameters(adUnitId: String, extraParameters: ReadableMap) = setAdViewExtraParameters(adUnitId, CloudXAdFormat.BANNER, extraParameters)
219
- fun updateBannerPosition(adUnitId: String, position: String) = updateAdViewPosition(adUnitId, CloudXAdFormat.BANNER, position, adViewOffsetsDp[adUnitId] ?: DEFAULT_AD_VIEW_OFFSET_DP)
220
- fun updateBannerOffsets(adUnitId: String, xOffset: Double, yOffset: Double) = updateAdViewPosition(adUnitId, CloudXAdFormat.BANNER, adViewPositions[adUnitId], Pair(xOffset.toFloat(), yOffset.toFloat()))
275
+ fun updateBannerPosition(adUnitId: String, position: String) = updateAdViewPosition(adUnitId, CloudXAdFormat.BANNER, position, null)
276
+ fun updateBannerOffsets(adUnitId: String, xOffset: Double, yOffset: Double) = updateAdViewPosition(adUnitId, CloudXAdFormat.BANNER, null, Pair(xOffset.toFloat(), yOffset.toFloat()))
221
277
  fun startBannerAutoRefresh(adUnitId: String) = startAdViewAutoRefresh(adUnitId, CloudXAdFormat.BANNER)
222
278
  fun stopBannerAutoRefresh(adUnitId: String) = stopAdViewAutoRefresh(adUnitId, CloudXAdFormat.BANNER)
223
279
  fun showBanner(adUnitId: String) = showAdView(adUnitId, CloudXAdFormat.BANNER)
@@ -234,8 +290,8 @@ class CloudXModuleImpl(private val reactApplicationContext: ReactApplicationCont
234
290
  fun setMRECPlacement(adUnitId: String, placement: String?) = setAdViewPlacement(adUnitId, CloudXAdFormat.MREC, placement)
235
291
  fun setMRECCustomData(adUnitId: String, customData: String?) = setAdViewCustomData(adUnitId, CloudXAdFormat.MREC, customData)
236
292
  fun setMRECExtraParameters(adUnitId: String, extraParameters: ReadableMap) = setAdViewExtraParameters(adUnitId, CloudXAdFormat.MREC, extraParameters)
237
- fun updateMRECPosition(adUnitId: String, position: String) = updateAdViewPosition(adUnitId, CloudXAdFormat.MREC, position, adViewOffsetsDp[adUnitId] ?: DEFAULT_AD_VIEW_OFFSET_DP)
238
- fun updateMRECOffsets(adUnitId: String, xOffset: Double, yOffset: Double) = updateAdViewPosition(adUnitId, CloudXAdFormat.MREC, adViewPositions[adUnitId], Pair(xOffset.toFloat(), yOffset.toFloat()))
293
+ fun updateMRECPosition(adUnitId: String, position: String) = updateAdViewPosition(adUnitId, CloudXAdFormat.MREC, position, null)
294
+ fun updateMRECOffsets(adUnitId: String, xOffset: Double, yOffset: Double) = updateAdViewPosition(adUnitId, CloudXAdFormat.MREC, null, Pair(xOffset.toFloat(), yOffset.toFloat()))
239
295
  fun startMRECAutoRefresh(adUnitId: String) = startAdViewAutoRefresh(adUnitId, CloudXAdFormat.MREC)
240
296
  fun stopMRECAutoRefresh(adUnitId: String) = stopAdViewAutoRefresh(adUnitId, CloudXAdFormat.MREC)
241
297
  fun showMREC(adUnitId: String) = showAdView(adUnitId, CloudXAdFormat.MREC)
@@ -247,21 +303,27 @@ class CloudXModuleImpl(private val reactApplicationContext: ReactApplicationCont
247
303
  // =========================================================================
248
304
 
249
305
  fun loadInterstitial(adUnitId: String) {
250
- retrieveInterstitial(adUnitId).load()
306
+ onUiThread {
307
+ retrieveInterstitial(adUnitId).load()
308
+ }
251
309
  }
252
310
 
253
311
  fun setInterstitialExtraParameters(adUnitId: String, extraParameters: ReadableMap) {
254
- CloudXExtraParameters.forEach(extraParameters) { key, value ->
255
- retrieveInterstitial(adUnitId).setExtraParameter(key, value)
312
+ onUiThread {
313
+ CloudXExtraParameters.forEach(extraParameters) { key, value ->
314
+ retrieveInterstitial(adUnitId).setExtraParameter(key, value)
315
+ }
256
316
  }
257
317
  }
258
318
 
259
319
  fun isInterstitialReady(adUnitId: String, promise: Promise) {
260
- promise.resolve(retrieveInterstitial(adUnitId).isAdReady)
320
+ onUiThread {
321
+ promise.resolve(retrieveInterstitial(adUnitId).isAdReady)
322
+ }
261
323
  }
262
324
 
263
325
  fun showInterstitial(adUnitId: String, placement: String?, customData: String?) {
264
- reactApplicationContext.runOnUiQueueThread {
326
+ onUiThread {
265
327
  val interstitial = retrieveInterstitial(adUnitId)
266
328
  val activity = maybeGetCurrentActivity()
267
329
  if (activity == null) {
@@ -270,14 +332,14 @@ class CloudXModuleImpl(private val reactApplicationContext: ReactApplicationCont
270
332
  CloudXAdEvents.ON_INTERSTITIAL_DISPLAY_FAILED_EVENT,
271
333
  getNoActivityDisplayFailedInfo(adUnitId),
272
334
  )
273
- return@runOnUiQueueThread
335
+ return@onUiThread
274
336
  }
275
337
  interstitial.show(activity, placement, customData)
276
338
  }
277
339
  }
278
340
 
279
341
  fun destroyInterstitial(adUnitId: String) {
280
- reactApplicationContext.runOnUiQueueThread {
342
+ onUiThread {
281
343
  interstitials.remove(adUnitId)?.let {
282
344
  it.listener = null
283
345
  it.revenueListener = null
@@ -291,21 +353,27 @@ class CloudXModuleImpl(private val reactApplicationContext: ReactApplicationCont
291
353
  // =========================================================================
292
354
 
293
355
  fun loadAppOpen(adUnitId: String) {
294
- retrieveAppOpen(adUnitId).load()
356
+ onUiThread {
357
+ retrieveAppOpen(adUnitId).load()
358
+ }
295
359
  }
296
360
 
297
361
  fun setAppOpenExtraParameters(adUnitId: String, extraParameters: ReadableMap) {
298
- CloudXExtraParameters.forEach(extraParameters) { key, value ->
299
- retrieveAppOpen(adUnitId).setExtraParameter(key, value)
362
+ onUiThread {
363
+ CloudXExtraParameters.forEach(extraParameters) { key, value ->
364
+ retrieveAppOpen(adUnitId).setExtraParameter(key, value)
365
+ }
300
366
  }
301
367
  }
302
368
 
303
369
  fun isAppOpenReady(adUnitId: String, promise: Promise) {
304
- promise.resolve(retrieveAppOpen(adUnitId).isAdReady)
370
+ onUiThread {
371
+ promise.resolve(retrieveAppOpen(adUnitId).isAdReady)
372
+ }
305
373
  }
306
374
 
307
375
  fun showAppOpen(adUnitId: String, placement: String?, customData: String?) {
308
- reactApplicationContext.runOnUiQueueThread {
376
+ onUiThread {
309
377
  val appOpen = retrieveAppOpen(adUnitId)
310
378
  val activity = maybeGetCurrentActivity()
311
379
  if (activity == null) {
@@ -314,14 +382,14 @@ class CloudXModuleImpl(private val reactApplicationContext: ReactApplicationCont
314
382
  CloudXAdEvents.ON_APPOPEN_DISPLAY_FAILED_EVENT,
315
383
  getNoActivityDisplayFailedInfo(adUnitId),
316
384
  )
317
- return@runOnUiQueueThread
385
+ return@onUiThread
318
386
  }
319
387
  appOpen.show(activity, placement, customData)
320
388
  }
321
389
  }
322
390
 
323
391
  fun destroyAppOpen(adUnitId: String) {
324
- reactApplicationContext.runOnUiQueueThread {
392
+ onUiThread {
325
393
  appOpenAds.remove(adUnitId)?.let {
326
394
  it.listener = null
327
395
  it.revenueListener = null
@@ -335,21 +403,27 @@ class CloudXModuleImpl(private val reactApplicationContext: ReactApplicationCont
335
403
  // =========================================================================
336
404
 
337
405
  fun loadRewarded(adUnitId: String) {
338
- retrieveRewarded(adUnitId).load()
406
+ onUiThread {
407
+ retrieveRewarded(adUnitId).load()
408
+ }
339
409
  }
340
410
 
341
411
  fun setRewardedExtraParameters(adUnitId: String, extraParameters: ReadableMap) {
342
- CloudXExtraParameters.forEach(extraParameters) { key, value ->
343
- retrieveRewarded(adUnitId).setExtraParameter(key, value)
412
+ onUiThread {
413
+ CloudXExtraParameters.forEach(extraParameters) { key, value ->
414
+ retrieveRewarded(adUnitId).setExtraParameter(key, value)
415
+ }
344
416
  }
345
417
  }
346
418
 
347
419
  fun isRewardedReady(adUnitId: String, promise: Promise) {
348
- promise.resolve(retrieveRewarded(adUnitId).isAdReady)
420
+ onUiThread {
421
+ promise.resolve(retrieveRewarded(adUnitId).isAdReady)
422
+ }
349
423
  }
350
424
 
351
425
  fun showRewarded(adUnitId: String, placement: String?, customData: String?) {
352
- reactApplicationContext.runOnUiQueueThread {
426
+ onUiThread {
353
427
  val rewarded = retrieveRewarded(adUnitId)
354
428
  val activity = maybeGetCurrentActivity()
355
429
  if (activity == null) {
@@ -358,14 +432,14 @@ class CloudXModuleImpl(private val reactApplicationContext: ReactApplicationCont
358
432
  CloudXAdEvents.ON_REWARDED_AD_DISPLAY_FAILED_EVENT,
359
433
  getNoActivityDisplayFailedInfo(adUnitId),
360
434
  )
361
- return@runOnUiQueueThread
435
+ return@onUiThread
362
436
  }
363
437
  rewarded.show(activity, placement, customData)
364
438
  }
365
439
  }
366
440
 
367
441
  fun destroyRewarded(adUnitId: String) {
368
- reactApplicationContext.runOnUiQueueThread {
442
+ onUiThread {
369
443
  rewardedAds.remove(adUnitId)?.let {
370
444
  it.listener = null
371
445
  it.revenueListener = null
@@ -513,7 +587,7 @@ class CloudXModuleImpl(private val reactApplicationContext: ReactApplicationCont
513
587
 
514
588
  private fun createAdView(adUnitId: String, adFormat: CloudXAdFormat, position: String, xOffset: Double, yOffset: Double) {
515
589
  // Run on main thread to ensure there are no concurrency issues with other ad view methods
516
- reactApplicationContext.runOnUiQueueThread {
590
+ onUiThread {
517
591
  // Intentionally load the ad view while detached from any window.
518
592
  // Attaching-then-hiding-then-loading causes the underlying banner
519
593
  // to take a prefetch code path whose hidden->visible transition
@@ -522,7 +596,7 @@ class CloudXModuleImpl(private val reactApplicationContext: ReactApplicationCont
522
596
  // (~30s). Detached loads use the "display when added to a window"
523
597
  // path, so showAdView attaches and displays immediately.
524
598
  val adView = retrieveAdView(adUnitId, adFormat, position, Pair(xOffset.toFloat(), yOffset.toFloat()))
525
- ?: return@runOnUiQueueThread
599
+ ?: return@onUiThread
526
600
 
527
601
  adViewAdFormats[adUnitId] = adFormat
528
602
  applyPendingExtraParameters(adUnitId, adView)
@@ -538,27 +612,27 @@ class CloudXModuleImpl(private val reactApplicationContext: ReactApplicationCont
538
612
  }
539
613
 
540
614
  private fun setAdViewPlacement(adUnitId: String, adFormat: CloudXAdFormat, placement: String?) {
541
- reactApplicationContext.runOnUiQueueThread {
542
- val adView = retrieveAdView(adUnitId, adFormat, "", DEFAULT_AD_VIEW_OFFSET_DP) ?: return@runOnUiQueueThread
615
+ onUiThread {
616
+ val adView = retrieveAdView(adUnitId, adFormat, "", DEFAULT_AD_VIEW_OFFSET_DP) ?: return@onUiThread
543
617
  adView.setPlacement(placement)
544
618
  }
545
619
  }
546
620
 
547
621
  private fun setAdViewCustomData(adUnitId: String, adFormat: CloudXAdFormat, customData: String?) {
548
- reactApplicationContext.runOnUiQueueThread {
549
- val adView = retrieveAdView(adUnitId, adFormat, "", DEFAULT_AD_VIEW_OFFSET_DP) ?: return@runOnUiQueueThread
622
+ onUiThread {
623
+ val adView = retrieveAdView(adUnitId, adFormat, "", DEFAULT_AD_VIEW_OFFSET_DP) ?: return@onUiThread
550
624
  adView.setCustomData(customData)
551
625
  }
552
626
  }
553
627
 
554
628
  private fun setAdViewExtraParameters(adUnitId: String, adFormat: CloudXAdFormat, extraParameters: ReadableMap) {
555
- reactApplicationContext.runOnUiQueueThread {
629
+ onUiThread {
556
630
  val adView = retrieveAdView(adUnitId, adFormat)
557
631
  if (adView != null) {
558
632
  CloudXExtraParameters.forEach(extraParameters) { key, value ->
559
633
  adView.setExtraParameter(key, value)
560
634
  }
561
- return@runOnUiQueueThread
635
+ return@onUiThread
562
636
  }
563
637
 
564
638
  val pending = adViewExtraParameters.getOrPut(adUnitId) { ConcurrentHashMap() }
@@ -840,21 +914,29 @@ class CloudXModuleImpl(private val reactApplicationContext: ReactApplicationCont
840
914
  override val adValues: Map<String, String>,
841
915
  ) : CloudXAd
842
916
 
843
- private fun updateAdViewPosition(adUnitId: String, adFormat: CloudXAdFormat, position: String?, offsetsDp: Pair<Float, Float>) {
844
- reactApplicationContext.runOnUiQueueThread {
845
- val adView = retrieveAdView(adUnitId, adFormat) ?: return@runOnUiQueueThread
846
- adViewPositions[adUnitId] = position ?: return@runOnUiQueueThread
847
- adViewOffsetsDp[adUnitId] = offsetsDp
917
+ /*
918
+ * A null [position] or [offsetsDp] means "keep the current value". The current value is read
919
+ * inside the posted block, so a position update followed by an offsets update from JS cannot
920
+ * overwrite the first call's position with the value that was current before it ran.
921
+ */
922
+ private fun updateAdViewPosition(adUnitId: String, adFormat: CloudXAdFormat, position: String?, offsetsDp: Pair<Float, Float>?) {
923
+ onUiThread {
924
+ val adView = retrieveAdView(adUnitId, adFormat) ?: return@onUiThread
925
+ adViewPositions[adUnitId] = position ?: adViewPositions[adUnitId] ?: return@onUiThread
926
+ adViewOffsetsDp[adUnitId] = offsetsDp ?: adViewOffsetsDp[adUnitId] ?: DEFAULT_AD_VIEW_OFFSET_DP
848
927
  positionAdView(adUnitId, adFormat)
849
928
  }
850
929
  }
851
930
 
931
+ @VisibleForTesting
932
+ internal fun adViewPositionForTesting(adUnitId: String): String? = adViewPositions[adUnitId]
933
+
852
934
  private fun showAdView(adUnitId: String, adFormat: CloudXAdFormat) {
853
- reactApplicationContext.runOnUiQueueThread {
935
+ onUiThread {
854
936
  val adView = retrieveAdView(adUnitId, adFormat)
855
937
  if (adView == null) {
856
938
  adUnitIdsToShowAfterCreate.add(adUnitId)
857
- return@runOnUiQueueThread
939
+ return@onUiThread
858
940
  }
859
941
 
860
942
  // Lazily attach and position on first show. Attaching here — not
@@ -872,18 +954,18 @@ class CloudXModuleImpl(private val reactApplicationContext: ReactApplicationCont
872
954
  }
873
955
 
874
956
  private fun hideAdView(adUnitId: String, adFormat: CloudXAdFormat) {
875
- reactApplicationContext.runOnUiQueueThread {
957
+ onUiThread {
876
958
  adUnitIdsToShowAfterCreate.remove(adUnitId)
877
- val adView = retrieveAdView(adUnitId, adFormat) ?: return@runOnUiQueueThread
959
+ val adView = retrieveAdView(adUnitId, adFormat) ?: return@onUiThread
878
960
  adView.visibility = View.GONE
879
961
  adView.stopAutoRefresh()
880
962
  }
881
963
  }
882
964
 
883
965
  private fun destroyAdView(adUnitId: String, adFormat: CloudXAdFormat) {
884
- reactApplicationContext.runOnUiQueueThread {
966
+ onUiThread {
885
967
  val adView = retrieveAdView(adUnitId, adFormat)
886
- if (adView == null) return@runOnUiQueueThread
968
+ if (adView == null) return@onUiThread
887
969
 
888
970
  val parent = adView.parent
889
971
  if (parent is ViewGroup) {
@@ -904,8 +986,8 @@ class CloudXModuleImpl(private val reactApplicationContext: ReactApplicationCont
904
986
  }
905
987
 
906
988
  private fun setAdViewBackgroundColor(adUnitId: String, adFormat: CloudXAdFormat, hexColorString: String) {
907
- reactApplicationContext.runOnUiQueueThread {
908
- val adView = retrieveAdView(adUnitId, adFormat) ?: return@runOnUiQueueThread
989
+ onUiThread {
990
+ val adView = retrieveAdView(adUnitId, adFormat) ?: return@onUiThread
909
991
  try {
910
992
  adView.setBackgroundColor(Color.parseColor(hexColorString))
911
993
  } catch (e: IllegalArgumentException) {
@@ -915,11 +997,11 @@ class CloudXModuleImpl(private val reactApplicationContext: ReactApplicationCont
915
997
  }
916
998
 
917
999
  private fun startAdViewAutoRefresh(adUnitId: String, adFormat: CloudXAdFormat) {
918
- reactApplicationContext.runOnUiQueueThread { retrieveAdView(adUnitId, adFormat)?.startAutoRefresh() }
1000
+ onUiThread { retrieveAdView(adUnitId, adFormat)?.startAutoRefresh() }
919
1001
  }
920
1002
 
921
1003
  private fun stopAdViewAutoRefresh(adUnitId: String, adFormat: CloudXAdFormat) {
922
- reactApplicationContext.runOnUiQueueThread { retrieveAdView(adUnitId, adFormat)?.stopAutoRefresh() }
1004
+ onUiThread { retrieveAdView(adUnitId, adFormat)?.stopAutoRefresh() }
923
1005
  }
924
1006
 
925
1007
  // =========================================================================
@@ -1099,13 +1181,15 @@ class CloudXModuleImpl(private val reactApplicationContext: ReactApplicationCont
1099
1181
  override fun onHostPause() {}
1100
1182
 
1101
1183
  override fun onHostDestroy() {
1184
+ // React Native drops its Activity here, so the cached one must go too.
1185
+ currentActivity = null
1102
1186
  // Make copy because destroyAdView() removes from adViews
1103
1187
  val adViewsCopy = HashMap(adViewAdFormats)
1104
1188
  for ((adUnitId, adFormat) in adViewsCopy) {
1105
1189
  destroyAdView(adUnitId, adFormat)
1106
1190
  }
1107
1191
 
1108
- reactApplicationContext.runOnUiQueueThread {
1192
+ onUiThread {
1109
1193
  for (interstitial in interstitials.values) {
1110
1194
  interstitial.destroy()
1111
1195
  }
@@ -17,6 +17,7 @@ class CloudXModule(reactContext: ReactApplicationContext) :
17
17
 
18
18
  @ReactMethod override fun isInitialized(promise: Promise) = impl.isInitialized(promise)
19
19
  @ReactMethod override fun initialize(pluginVersion: String, appKey: String, promise: Promise) = impl.initialize(pluginVersion, appKey, promise)
20
+ @ReactMethod override fun showMediationDebugger(promise: Promise) = impl.showMediationDebugger(promise)
20
21
  @ReactMethod override fun isTablet(promise: Promise) = impl.isTablet(promise)
21
22
  @ReactMethod override fun getVersion(promise: Promise) = impl.getVersion(promise)
22
23
 
@@ -16,6 +16,7 @@ class CloudXModule(reactContext: ReactApplicationContext) :
16
16
 
17
17
  @ReactMethod fun isInitialized(promise: Promise) = impl.isInitialized(promise)
18
18
  @ReactMethod fun initialize(pluginVersion: String, appKey: String, promise: Promise) = impl.initialize(pluginVersion, appKey, promise)
19
+ @ReactMethod fun showMediationDebugger(promise: Promise) = impl.showMediationDebugger(promise)
19
20
  @ReactMethod fun isTablet(promise: Promise) = impl.isTablet(promise)
20
21
  @ReactMethod fun getVersion(promise: Promise) = impl.getVersion(promise)
21
22
 
@@ -141,7 +141,7 @@ class CloudXModuleImplArbiterBidsTest {
141
141
  }
142
142
 
143
143
  @Test
144
- fun `forwards a negative manual price unchanged`() {
144
+ fun `drops a negative manual price`() {
145
145
  val bids = bidsFrom(
146
146
  mapOf(
147
147
  "platform" to "ADMOB",
@@ -150,8 +150,8 @@ class CloudXModuleImplArbiterBidsTest {
150
150
  )
151
151
  )
152
152
 
153
- // The wrapper is transparent about the value it was handed; the native SDK decides to drop it.
154
- assertEquals(-1.5, bids[0].field("manualRevenuePerImpressionUSD") as Double, 1e-9)
153
+ // The wrapper forwards the value unchanged; the native AdMob bid factory drops negative prices.
154
+ assertNull(bids[0].field("manualRevenuePerImpressionUSD"))
155
155
  }
156
156
 
157
157
  @Test