cloudx-react-native 3.7.0 → 3.8.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.
- package/android/build.gradle +2 -1
- package/android/src/main/java/com/cloudxreactnative/CloudXModuleImpl.kt +171 -101
- package/android/src/test/java/com/cloudxreactnative/CloudXModuleImplThreadingTest.kt +170 -0
- package/cloudx-react-native.podspec +1 -1
- package/lib/commonjs/version.js +1 -1
- package/lib/module/version.js +1 -1
- package/lib/typescript/src/version.d.ts +1 -1
- package/package.json +1 -1
- package/src/version.ts +1 -1
package/android/build.gradle
CHANGED
|
@@ -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.
|
|
161
|
+
implementation "io.cloudx:sdk:4.6.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
|
}
|
|
@@ -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,67 @@ 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
|
-
|
|
113
|
+
onUiThread {
|
|
114
|
+
promise.resolve(CloudX.isInitialized())
|
|
115
|
+
}
|
|
96
116
|
}
|
|
97
117
|
|
|
98
118
|
fun initialize(pluginVersion: String, appKey: String, promise: Promise) {
|
|
99
|
-
|
|
100
|
-
.
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
119
|
+
onUiThread {
|
|
120
|
+
val config = CloudXInitializationConfiguration.builder(appKey)
|
|
121
|
+
.setPluginVersion(pluginVersion)
|
|
122
|
+
.build()
|
|
123
|
+
|
|
124
|
+
CloudX.initialize(reactApplicationContext, config, object : CloudXInitializationListener {
|
|
125
|
+
override fun onInitialized(configuration: CloudXSdkConfiguration) {
|
|
126
|
+
setupOrientationListener()
|
|
127
|
+
val result = Arguments.createMap()
|
|
128
|
+
result.putBoolean("success", true)
|
|
129
|
+
result.putString("message", "SDK initialized successfully")
|
|
130
|
+
promise.resolve(result)
|
|
131
|
+
}
|
|
111
132
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
133
|
+
override fun onInitializationFailed(cloudXError: CloudXError) {
|
|
134
|
+
Log.e(TAG, "CloudX SDK initialization failed: ${cloudXError.message}")
|
|
135
|
+
promise.reject(cloudXError.code.name, cloudXError.message, null)
|
|
136
|
+
}
|
|
137
|
+
})
|
|
138
|
+
}
|
|
117
139
|
}
|
|
118
140
|
|
|
119
141
|
fun isTablet(promise: Promise) {
|
|
120
|
-
|
|
121
|
-
|
|
142
|
+
onUiThread {
|
|
143
|
+
val isTablet = reactApplicationContext.resources.configuration.smallestScreenWidthDp >= 600
|
|
144
|
+
promise.resolve(isTablet)
|
|
145
|
+
}
|
|
122
146
|
}
|
|
123
147
|
|
|
124
148
|
fun getVersion(promise: Promise) {
|
|
125
|
-
|
|
149
|
+
onUiThread {
|
|
150
|
+
promise.resolve(CloudX.getVersion())
|
|
151
|
+
}
|
|
126
152
|
}
|
|
127
153
|
|
|
128
154
|
// =========================================================================
|
|
@@ -130,16 +156,18 @@ class CloudXModuleImpl(private val reactApplicationContext: ReactApplicationCont
|
|
|
130
156
|
// =========================================================================
|
|
131
157
|
|
|
132
158
|
fun setMinLogLevel(level: String) {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
159
|
+
onUiThread {
|
|
160
|
+
val logLevel = when (level.lowercase()) {
|
|
161
|
+
"verbose" -> CloudXLogLevel.VERBOSE
|
|
162
|
+
"debug" -> CloudXLogLevel.DEBUG
|
|
163
|
+
"info" -> CloudXLogLevel.INFO
|
|
164
|
+
"warn" -> CloudXLogLevel.WARN
|
|
165
|
+
"error" -> CloudXLogLevel.ERROR
|
|
166
|
+
"none" -> CloudXLogLevel.NONE
|
|
167
|
+
else -> CloudXLogLevel.INFO
|
|
168
|
+
}
|
|
169
|
+
CloudX.setMinLogLevel(logLevel)
|
|
141
170
|
}
|
|
142
|
-
CloudX.setMinLogLevel(logLevel)
|
|
143
171
|
}
|
|
144
172
|
|
|
145
173
|
fun setVisualDebuggingEnabled(enabled: Boolean) {
|
|
@@ -151,19 +179,27 @@ class CloudXModuleImpl(private val reactApplicationContext: ReactApplicationCont
|
|
|
151
179
|
// =========================================================================
|
|
152
180
|
|
|
153
181
|
fun setHashedUserID(hashedUserID: String) {
|
|
154
|
-
|
|
182
|
+
onUiThread {
|
|
183
|
+
CloudX.setHashedUserId(hashedUserID)
|
|
184
|
+
}
|
|
155
185
|
}
|
|
156
186
|
|
|
157
187
|
fun setUserKeyValue(key: String, value: String) {
|
|
158
|
-
|
|
188
|
+
onUiThread {
|
|
189
|
+
CloudX.setUserKeyValue(key, value)
|
|
190
|
+
}
|
|
159
191
|
}
|
|
160
192
|
|
|
161
193
|
fun setAppKeyValue(key: String, value: String) {
|
|
162
|
-
|
|
194
|
+
onUiThread {
|
|
195
|
+
CloudX.setAppKeyValue(key, value)
|
|
196
|
+
}
|
|
163
197
|
}
|
|
164
198
|
|
|
165
199
|
fun clearAllKeyValues() {
|
|
166
|
-
|
|
200
|
+
onUiThread {
|
|
201
|
+
CloudX.clearAllKeyValues()
|
|
202
|
+
}
|
|
167
203
|
}
|
|
168
204
|
|
|
169
205
|
// =========================================================================
|
|
@@ -171,11 +207,15 @@ class CloudXModuleImpl(private val reactApplicationContext: ReactApplicationCont
|
|
|
171
207
|
// =========================================================================
|
|
172
208
|
|
|
173
209
|
fun setHasUserConsent(hasUserConsent: Boolean?) {
|
|
174
|
-
|
|
210
|
+
onUiThread {
|
|
211
|
+
CloudX.setHasUserConsent(hasUserConsent)
|
|
212
|
+
}
|
|
175
213
|
}
|
|
176
214
|
|
|
177
215
|
fun setDoNotSell(isDoNotSell: Boolean?) {
|
|
178
|
-
|
|
216
|
+
onUiThread {
|
|
217
|
+
CloudX.setDoNotSell(isDoNotSell)
|
|
218
|
+
}
|
|
179
219
|
}
|
|
180
220
|
|
|
181
221
|
// =========================================================================
|
|
@@ -183,27 +223,31 @@ class CloudXModuleImpl(private val reactApplicationContext: ReactApplicationCont
|
|
|
183
223
|
// =========================================================================
|
|
184
224
|
|
|
185
225
|
fun arbiter(configuration: ReadableMap, promise: Promise) {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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())
|
|
226
|
+
onUiThread {
|
|
227
|
+
val bids = arbiterBids(configuration)
|
|
228
|
+
if (bids.isEmpty()) {
|
|
229
|
+
promise.resolve(emptyArbiterResult())
|
|
230
|
+
return@onUiThread
|
|
196
231
|
}
|
|
197
|
-
|
|
232
|
+
|
|
233
|
+
val arbiterConfiguration = CloudXArbiterConfiguration.builder(bids).build()
|
|
234
|
+
CloudX.arbiter(arbiterConfiguration, object : CloudXArbiterListener {
|
|
235
|
+
override fun onCompleted(result: CloudXArbiterResult) {
|
|
236
|
+
promise.resolve(result.toWritableMap())
|
|
237
|
+
}
|
|
238
|
+
})
|
|
239
|
+
}
|
|
198
240
|
}
|
|
199
241
|
|
|
200
242
|
fun reportRevenueData(data: ReadableMap, promise: Promise) {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
243
|
+
onUiThread {
|
|
244
|
+
val revenueData = data.toRevenueData()
|
|
245
|
+
if (revenueData == null) {
|
|
246
|
+
promise.resolve(false)
|
|
247
|
+
return@onUiThread
|
|
248
|
+
}
|
|
249
|
+
promise.resolve(CloudX.reportRevenueData(revenueData))
|
|
205
250
|
}
|
|
206
|
-
promise.resolve(CloudX.reportRevenueData(revenueData))
|
|
207
251
|
}
|
|
208
252
|
|
|
209
253
|
// =========================================================================
|
|
@@ -216,8 +260,8 @@ class CloudXModuleImpl(private val reactApplicationContext: ReactApplicationCont
|
|
|
216
260
|
fun setBannerPlacement(adUnitId: String, placement: String?) = setAdViewPlacement(adUnitId, CloudXAdFormat.BANNER, placement)
|
|
217
261
|
fun setBannerCustomData(adUnitId: String, customData: String?) = setAdViewCustomData(adUnitId, CloudXAdFormat.BANNER, customData)
|
|
218
262
|
fun setBannerExtraParameters(adUnitId: String, extraParameters: ReadableMap) = setAdViewExtraParameters(adUnitId, CloudXAdFormat.BANNER, extraParameters)
|
|
219
|
-
fun updateBannerPosition(adUnitId: String, position: String) = updateAdViewPosition(adUnitId, CloudXAdFormat.BANNER, position,
|
|
220
|
-
fun updateBannerOffsets(adUnitId: String, xOffset: Double, yOffset: Double) = updateAdViewPosition(adUnitId, CloudXAdFormat.BANNER,
|
|
263
|
+
fun updateBannerPosition(adUnitId: String, position: String) = updateAdViewPosition(adUnitId, CloudXAdFormat.BANNER, position, null)
|
|
264
|
+
fun updateBannerOffsets(adUnitId: String, xOffset: Double, yOffset: Double) = updateAdViewPosition(adUnitId, CloudXAdFormat.BANNER, null, Pair(xOffset.toFloat(), yOffset.toFloat()))
|
|
221
265
|
fun startBannerAutoRefresh(adUnitId: String) = startAdViewAutoRefresh(adUnitId, CloudXAdFormat.BANNER)
|
|
222
266
|
fun stopBannerAutoRefresh(adUnitId: String) = stopAdViewAutoRefresh(adUnitId, CloudXAdFormat.BANNER)
|
|
223
267
|
fun showBanner(adUnitId: String) = showAdView(adUnitId, CloudXAdFormat.BANNER)
|
|
@@ -234,8 +278,8 @@ class CloudXModuleImpl(private val reactApplicationContext: ReactApplicationCont
|
|
|
234
278
|
fun setMRECPlacement(adUnitId: String, placement: String?) = setAdViewPlacement(adUnitId, CloudXAdFormat.MREC, placement)
|
|
235
279
|
fun setMRECCustomData(adUnitId: String, customData: String?) = setAdViewCustomData(adUnitId, CloudXAdFormat.MREC, customData)
|
|
236
280
|
fun setMRECExtraParameters(adUnitId: String, extraParameters: ReadableMap) = setAdViewExtraParameters(adUnitId, CloudXAdFormat.MREC, extraParameters)
|
|
237
|
-
fun updateMRECPosition(adUnitId: String, position: String) = updateAdViewPosition(adUnitId, CloudXAdFormat.MREC, position,
|
|
238
|
-
fun updateMRECOffsets(adUnitId: String, xOffset: Double, yOffset: Double) = updateAdViewPosition(adUnitId, CloudXAdFormat.MREC,
|
|
281
|
+
fun updateMRECPosition(adUnitId: String, position: String) = updateAdViewPosition(adUnitId, CloudXAdFormat.MREC, position, null)
|
|
282
|
+
fun updateMRECOffsets(adUnitId: String, xOffset: Double, yOffset: Double) = updateAdViewPosition(adUnitId, CloudXAdFormat.MREC, null, Pair(xOffset.toFloat(), yOffset.toFloat()))
|
|
239
283
|
fun startMRECAutoRefresh(adUnitId: String) = startAdViewAutoRefresh(adUnitId, CloudXAdFormat.MREC)
|
|
240
284
|
fun stopMRECAutoRefresh(adUnitId: String) = stopAdViewAutoRefresh(adUnitId, CloudXAdFormat.MREC)
|
|
241
285
|
fun showMREC(adUnitId: String) = showAdView(adUnitId, CloudXAdFormat.MREC)
|
|
@@ -247,21 +291,27 @@ class CloudXModuleImpl(private val reactApplicationContext: ReactApplicationCont
|
|
|
247
291
|
// =========================================================================
|
|
248
292
|
|
|
249
293
|
fun loadInterstitial(adUnitId: String) {
|
|
250
|
-
|
|
294
|
+
onUiThread {
|
|
295
|
+
retrieveInterstitial(adUnitId).load()
|
|
296
|
+
}
|
|
251
297
|
}
|
|
252
298
|
|
|
253
299
|
fun setInterstitialExtraParameters(adUnitId: String, extraParameters: ReadableMap) {
|
|
254
|
-
|
|
255
|
-
|
|
300
|
+
onUiThread {
|
|
301
|
+
CloudXExtraParameters.forEach(extraParameters) { key, value ->
|
|
302
|
+
retrieveInterstitial(adUnitId).setExtraParameter(key, value)
|
|
303
|
+
}
|
|
256
304
|
}
|
|
257
305
|
}
|
|
258
306
|
|
|
259
307
|
fun isInterstitialReady(adUnitId: String, promise: Promise) {
|
|
260
|
-
|
|
308
|
+
onUiThread {
|
|
309
|
+
promise.resolve(retrieveInterstitial(adUnitId).isAdReady)
|
|
310
|
+
}
|
|
261
311
|
}
|
|
262
312
|
|
|
263
313
|
fun showInterstitial(adUnitId: String, placement: String?, customData: String?) {
|
|
264
|
-
|
|
314
|
+
onUiThread {
|
|
265
315
|
val interstitial = retrieveInterstitial(adUnitId)
|
|
266
316
|
val activity = maybeGetCurrentActivity()
|
|
267
317
|
if (activity == null) {
|
|
@@ -270,14 +320,14 @@ class CloudXModuleImpl(private val reactApplicationContext: ReactApplicationCont
|
|
|
270
320
|
CloudXAdEvents.ON_INTERSTITIAL_DISPLAY_FAILED_EVENT,
|
|
271
321
|
getNoActivityDisplayFailedInfo(adUnitId),
|
|
272
322
|
)
|
|
273
|
-
return@
|
|
323
|
+
return@onUiThread
|
|
274
324
|
}
|
|
275
325
|
interstitial.show(activity, placement, customData)
|
|
276
326
|
}
|
|
277
327
|
}
|
|
278
328
|
|
|
279
329
|
fun destroyInterstitial(adUnitId: String) {
|
|
280
|
-
|
|
330
|
+
onUiThread {
|
|
281
331
|
interstitials.remove(adUnitId)?.let {
|
|
282
332
|
it.listener = null
|
|
283
333
|
it.revenueListener = null
|
|
@@ -291,21 +341,27 @@ class CloudXModuleImpl(private val reactApplicationContext: ReactApplicationCont
|
|
|
291
341
|
// =========================================================================
|
|
292
342
|
|
|
293
343
|
fun loadAppOpen(adUnitId: String) {
|
|
294
|
-
|
|
344
|
+
onUiThread {
|
|
345
|
+
retrieveAppOpen(adUnitId).load()
|
|
346
|
+
}
|
|
295
347
|
}
|
|
296
348
|
|
|
297
349
|
fun setAppOpenExtraParameters(adUnitId: String, extraParameters: ReadableMap) {
|
|
298
|
-
|
|
299
|
-
|
|
350
|
+
onUiThread {
|
|
351
|
+
CloudXExtraParameters.forEach(extraParameters) { key, value ->
|
|
352
|
+
retrieveAppOpen(adUnitId).setExtraParameter(key, value)
|
|
353
|
+
}
|
|
300
354
|
}
|
|
301
355
|
}
|
|
302
356
|
|
|
303
357
|
fun isAppOpenReady(adUnitId: String, promise: Promise) {
|
|
304
|
-
|
|
358
|
+
onUiThread {
|
|
359
|
+
promise.resolve(retrieveAppOpen(adUnitId).isAdReady)
|
|
360
|
+
}
|
|
305
361
|
}
|
|
306
362
|
|
|
307
363
|
fun showAppOpen(adUnitId: String, placement: String?, customData: String?) {
|
|
308
|
-
|
|
364
|
+
onUiThread {
|
|
309
365
|
val appOpen = retrieveAppOpen(adUnitId)
|
|
310
366
|
val activity = maybeGetCurrentActivity()
|
|
311
367
|
if (activity == null) {
|
|
@@ -314,14 +370,14 @@ class CloudXModuleImpl(private val reactApplicationContext: ReactApplicationCont
|
|
|
314
370
|
CloudXAdEvents.ON_APPOPEN_DISPLAY_FAILED_EVENT,
|
|
315
371
|
getNoActivityDisplayFailedInfo(adUnitId),
|
|
316
372
|
)
|
|
317
|
-
return@
|
|
373
|
+
return@onUiThread
|
|
318
374
|
}
|
|
319
375
|
appOpen.show(activity, placement, customData)
|
|
320
376
|
}
|
|
321
377
|
}
|
|
322
378
|
|
|
323
379
|
fun destroyAppOpen(adUnitId: String) {
|
|
324
|
-
|
|
380
|
+
onUiThread {
|
|
325
381
|
appOpenAds.remove(adUnitId)?.let {
|
|
326
382
|
it.listener = null
|
|
327
383
|
it.revenueListener = null
|
|
@@ -335,21 +391,27 @@ class CloudXModuleImpl(private val reactApplicationContext: ReactApplicationCont
|
|
|
335
391
|
// =========================================================================
|
|
336
392
|
|
|
337
393
|
fun loadRewarded(adUnitId: String) {
|
|
338
|
-
|
|
394
|
+
onUiThread {
|
|
395
|
+
retrieveRewarded(adUnitId).load()
|
|
396
|
+
}
|
|
339
397
|
}
|
|
340
398
|
|
|
341
399
|
fun setRewardedExtraParameters(adUnitId: String, extraParameters: ReadableMap) {
|
|
342
|
-
|
|
343
|
-
|
|
400
|
+
onUiThread {
|
|
401
|
+
CloudXExtraParameters.forEach(extraParameters) { key, value ->
|
|
402
|
+
retrieveRewarded(adUnitId).setExtraParameter(key, value)
|
|
403
|
+
}
|
|
344
404
|
}
|
|
345
405
|
}
|
|
346
406
|
|
|
347
407
|
fun isRewardedReady(adUnitId: String, promise: Promise) {
|
|
348
|
-
|
|
408
|
+
onUiThread {
|
|
409
|
+
promise.resolve(retrieveRewarded(adUnitId).isAdReady)
|
|
410
|
+
}
|
|
349
411
|
}
|
|
350
412
|
|
|
351
413
|
fun showRewarded(adUnitId: String, placement: String?, customData: String?) {
|
|
352
|
-
|
|
414
|
+
onUiThread {
|
|
353
415
|
val rewarded = retrieveRewarded(adUnitId)
|
|
354
416
|
val activity = maybeGetCurrentActivity()
|
|
355
417
|
if (activity == null) {
|
|
@@ -358,14 +420,14 @@ class CloudXModuleImpl(private val reactApplicationContext: ReactApplicationCont
|
|
|
358
420
|
CloudXAdEvents.ON_REWARDED_AD_DISPLAY_FAILED_EVENT,
|
|
359
421
|
getNoActivityDisplayFailedInfo(adUnitId),
|
|
360
422
|
)
|
|
361
|
-
return@
|
|
423
|
+
return@onUiThread
|
|
362
424
|
}
|
|
363
425
|
rewarded.show(activity, placement, customData)
|
|
364
426
|
}
|
|
365
427
|
}
|
|
366
428
|
|
|
367
429
|
fun destroyRewarded(adUnitId: String) {
|
|
368
|
-
|
|
430
|
+
onUiThread {
|
|
369
431
|
rewardedAds.remove(adUnitId)?.let {
|
|
370
432
|
it.listener = null
|
|
371
433
|
it.revenueListener = null
|
|
@@ -513,7 +575,7 @@ class CloudXModuleImpl(private val reactApplicationContext: ReactApplicationCont
|
|
|
513
575
|
|
|
514
576
|
private fun createAdView(adUnitId: String, adFormat: CloudXAdFormat, position: String, xOffset: Double, yOffset: Double) {
|
|
515
577
|
// Run on main thread to ensure there are no concurrency issues with other ad view methods
|
|
516
|
-
|
|
578
|
+
onUiThread {
|
|
517
579
|
// Intentionally load the ad view while detached from any window.
|
|
518
580
|
// Attaching-then-hiding-then-loading causes the underlying banner
|
|
519
581
|
// to take a prefetch code path whose hidden->visible transition
|
|
@@ -522,7 +584,7 @@ class CloudXModuleImpl(private val reactApplicationContext: ReactApplicationCont
|
|
|
522
584
|
// (~30s). Detached loads use the "display when added to a window"
|
|
523
585
|
// path, so showAdView attaches and displays immediately.
|
|
524
586
|
val adView = retrieveAdView(adUnitId, adFormat, position, Pair(xOffset.toFloat(), yOffset.toFloat()))
|
|
525
|
-
?: return@
|
|
587
|
+
?: return@onUiThread
|
|
526
588
|
|
|
527
589
|
adViewAdFormats[adUnitId] = adFormat
|
|
528
590
|
applyPendingExtraParameters(adUnitId, adView)
|
|
@@ -538,27 +600,27 @@ class CloudXModuleImpl(private val reactApplicationContext: ReactApplicationCont
|
|
|
538
600
|
}
|
|
539
601
|
|
|
540
602
|
private fun setAdViewPlacement(adUnitId: String, adFormat: CloudXAdFormat, placement: String?) {
|
|
541
|
-
|
|
542
|
-
val adView = retrieveAdView(adUnitId, adFormat, "", DEFAULT_AD_VIEW_OFFSET_DP) ?: return@
|
|
603
|
+
onUiThread {
|
|
604
|
+
val adView = retrieveAdView(adUnitId, adFormat, "", DEFAULT_AD_VIEW_OFFSET_DP) ?: return@onUiThread
|
|
543
605
|
adView.setPlacement(placement)
|
|
544
606
|
}
|
|
545
607
|
}
|
|
546
608
|
|
|
547
609
|
private fun setAdViewCustomData(adUnitId: String, adFormat: CloudXAdFormat, customData: String?) {
|
|
548
|
-
|
|
549
|
-
val adView = retrieveAdView(adUnitId, adFormat, "", DEFAULT_AD_VIEW_OFFSET_DP) ?: return@
|
|
610
|
+
onUiThread {
|
|
611
|
+
val adView = retrieveAdView(adUnitId, adFormat, "", DEFAULT_AD_VIEW_OFFSET_DP) ?: return@onUiThread
|
|
550
612
|
adView.setCustomData(customData)
|
|
551
613
|
}
|
|
552
614
|
}
|
|
553
615
|
|
|
554
616
|
private fun setAdViewExtraParameters(adUnitId: String, adFormat: CloudXAdFormat, extraParameters: ReadableMap) {
|
|
555
|
-
|
|
617
|
+
onUiThread {
|
|
556
618
|
val adView = retrieveAdView(adUnitId, adFormat)
|
|
557
619
|
if (adView != null) {
|
|
558
620
|
CloudXExtraParameters.forEach(extraParameters) { key, value ->
|
|
559
621
|
adView.setExtraParameter(key, value)
|
|
560
622
|
}
|
|
561
|
-
return@
|
|
623
|
+
return@onUiThread
|
|
562
624
|
}
|
|
563
625
|
|
|
564
626
|
val pending = adViewExtraParameters.getOrPut(adUnitId) { ConcurrentHashMap() }
|
|
@@ -840,21 +902,29 @@ class CloudXModuleImpl(private val reactApplicationContext: ReactApplicationCont
|
|
|
840
902
|
override val adValues: Map<String, String>,
|
|
841
903
|
) : CloudXAd
|
|
842
904
|
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
905
|
+
/*
|
|
906
|
+
* A null [position] or [offsetsDp] means "keep the current value". The current value is read
|
|
907
|
+
* inside the posted block, so a position update followed by an offsets update from JS cannot
|
|
908
|
+
* overwrite the first call's position with the value that was current before it ran.
|
|
909
|
+
*/
|
|
910
|
+
private fun updateAdViewPosition(adUnitId: String, adFormat: CloudXAdFormat, position: String?, offsetsDp: Pair<Float, Float>?) {
|
|
911
|
+
onUiThread {
|
|
912
|
+
val adView = retrieveAdView(adUnitId, adFormat) ?: return@onUiThread
|
|
913
|
+
adViewPositions[adUnitId] = position ?: adViewPositions[adUnitId] ?: return@onUiThread
|
|
914
|
+
adViewOffsetsDp[adUnitId] = offsetsDp ?: adViewOffsetsDp[adUnitId] ?: DEFAULT_AD_VIEW_OFFSET_DP
|
|
848
915
|
positionAdView(adUnitId, adFormat)
|
|
849
916
|
}
|
|
850
917
|
}
|
|
851
918
|
|
|
919
|
+
@VisibleForTesting
|
|
920
|
+
internal fun adViewPositionForTesting(adUnitId: String): String? = adViewPositions[adUnitId]
|
|
921
|
+
|
|
852
922
|
private fun showAdView(adUnitId: String, adFormat: CloudXAdFormat) {
|
|
853
|
-
|
|
923
|
+
onUiThread {
|
|
854
924
|
val adView = retrieveAdView(adUnitId, adFormat)
|
|
855
925
|
if (adView == null) {
|
|
856
926
|
adUnitIdsToShowAfterCreate.add(adUnitId)
|
|
857
|
-
return@
|
|
927
|
+
return@onUiThread
|
|
858
928
|
}
|
|
859
929
|
|
|
860
930
|
// Lazily attach and position on first show. Attaching here — not
|
|
@@ -872,18 +942,18 @@ class CloudXModuleImpl(private val reactApplicationContext: ReactApplicationCont
|
|
|
872
942
|
}
|
|
873
943
|
|
|
874
944
|
private fun hideAdView(adUnitId: String, adFormat: CloudXAdFormat) {
|
|
875
|
-
|
|
945
|
+
onUiThread {
|
|
876
946
|
adUnitIdsToShowAfterCreate.remove(adUnitId)
|
|
877
|
-
val adView = retrieveAdView(adUnitId, adFormat) ?: return@
|
|
947
|
+
val adView = retrieveAdView(adUnitId, adFormat) ?: return@onUiThread
|
|
878
948
|
adView.visibility = View.GONE
|
|
879
949
|
adView.stopAutoRefresh()
|
|
880
950
|
}
|
|
881
951
|
}
|
|
882
952
|
|
|
883
953
|
private fun destroyAdView(adUnitId: String, adFormat: CloudXAdFormat) {
|
|
884
|
-
|
|
954
|
+
onUiThread {
|
|
885
955
|
val adView = retrieveAdView(adUnitId, adFormat)
|
|
886
|
-
if (adView == null) return@
|
|
956
|
+
if (adView == null) return@onUiThread
|
|
887
957
|
|
|
888
958
|
val parent = adView.parent
|
|
889
959
|
if (parent is ViewGroup) {
|
|
@@ -904,8 +974,8 @@ class CloudXModuleImpl(private val reactApplicationContext: ReactApplicationCont
|
|
|
904
974
|
}
|
|
905
975
|
|
|
906
976
|
private fun setAdViewBackgroundColor(adUnitId: String, adFormat: CloudXAdFormat, hexColorString: String) {
|
|
907
|
-
|
|
908
|
-
val adView = retrieveAdView(adUnitId, adFormat) ?: return@
|
|
977
|
+
onUiThread {
|
|
978
|
+
val adView = retrieveAdView(adUnitId, adFormat) ?: return@onUiThread
|
|
909
979
|
try {
|
|
910
980
|
adView.setBackgroundColor(Color.parseColor(hexColorString))
|
|
911
981
|
} catch (e: IllegalArgumentException) {
|
|
@@ -915,11 +985,11 @@ class CloudXModuleImpl(private val reactApplicationContext: ReactApplicationCont
|
|
|
915
985
|
}
|
|
916
986
|
|
|
917
987
|
private fun startAdViewAutoRefresh(adUnitId: String, adFormat: CloudXAdFormat) {
|
|
918
|
-
|
|
988
|
+
onUiThread { retrieveAdView(adUnitId, adFormat)?.startAutoRefresh() }
|
|
919
989
|
}
|
|
920
990
|
|
|
921
991
|
private fun stopAdViewAutoRefresh(adUnitId: String, adFormat: CloudXAdFormat) {
|
|
922
|
-
|
|
992
|
+
onUiThread { retrieveAdView(adUnitId, adFormat)?.stopAutoRefresh() }
|
|
923
993
|
}
|
|
924
994
|
|
|
925
995
|
// =========================================================================
|
|
@@ -1105,7 +1175,7 @@ class CloudXModuleImpl(private val reactApplicationContext: ReactApplicationCont
|
|
|
1105
1175
|
destroyAdView(adUnitId, adFormat)
|
|
1106
1176
|
}
|
|
1107
1177
|
|
|
1108
|
-
|
|
1178
|
+
onUiThread {
|
|
1109
1179
|
for (interstitial in interstitials.values) {
|
|
1110
1180
|
interstitial.destroy()
|
|
1111
1181
|
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
package com.cloudxreactnative
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
4
|
+
import io.cloudx.sdk.CloudX
|
|
5
|
+
import io.cloudx.sdk.CloudXAppOpenAd
|
|
6
|
+
import io.cloudx.sdk.CloudXInterstitialAd
|
|
7
|
+
import io.cloudx.sdk.CloudXRewardedAd
|
|
8
|
+
import io.cloudx.sdk.CloudXAdView
|
|
9
|
+
import io.mockk.every
|
|
10
|
+
import io.mockk.mockk
|
|
11
|
+
import io.mockk.mockkStatic
|
|
12
|
+
import io.mockk.unmockkAll
|
|
13
|
+
import io.mockk.verify
|
|
14
|
+
import kotlinx.coroutines.Dispatchers
|
|
15
|
+
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
|
16
|
+
import kotlinx.coroutines.test.UnconfinedTestDispatcher
|
|
17
|
+
import kotlinx.coroutines.test.resetMain
|
|
18
|
+
import kotlinx.coroutines.test.setMain
|
|
19
|
+
import org.junit.After
|
|
20
|
+
import org.junit.Assert.assertEquals
|
|
21
|
+
import org.junit.Before
|
|
22
|
+
import org.junit.Test
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Covers the module's UI-thread invariant: every public method posts its body via
|
|
26
|
+
* [ReactApplicationContext.runOnUiQueueThread], so JS call order is preserved and a destroy
|
|
27
|
+
* followed by a load always operates on a fresh native ad instance.
|
|
28
|
+
*/
|
|
29
|
+
@OptIn(ExperimentalCoroutinesApi::class)
|
|
30
|
+
class CloudXModuleImplThreadingTest {
|
|
31
|
+
|
|
32
|
+
private val adUnitId = "unit-1"
|
|
33
|
+
private val queuedRunnables = mutableListOf<Runnable>()
|
|
34
|
+
|
|
35
|
+
@Before
|
|
36
|
+
fun setUp() {
|
|
37
|
+
// CloudX's static initializer resolves Dispatchers.Main, which needs a main looper on the JVM.
|
|
38
|
+
Dispatchers.setMain(UnconfinedTestDispatcher())
|
|
39
|
+
mockkStatic(CloudX::class)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
@After
|
|
43
|
+
fun tearDown() {
|
|
44
|
+
unmockkAll()
|
|
45
|
+
Dispatchers.resetMain()
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Simulates the UI thread's FIFO queue by running every posted runnable immediately.
|
|
49
|
+
private fun moduleWithInlineUiThread(): CloudXModuleImpl {
|
|
50
|
+
val context = mockk<ReactApplicationContext>(relaxed = true)
|
|
51
|
+
every { context.runOnUiQueueThread(any()) } answers { firstArg<Runnable>().run() }
|
|
52
|
+
return CloudXModuleImpl(context)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Swallows every posted runnable, so anything that runs is running off the UI thread.
|
|
56
|
+
private fun moduleWithStalledUiThread(): CloudXModuleImpl {
|
|
57
|
+
val context = mockk<ReactApplicationContext>(relaxed = true)
|
|
58
|
+
every { context.runOnUiQueueThread(any()) } returns Unit
|
|
59
|
+
return CloudXModuleImpl(context)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Queues posted runnables into [queuedRunnables]; call [drainUiThread] to run them in FIFO order.
|
|
63
|
+
private fun moduleWithQueuedUiThread(): CloudXModuleImpl {
|
|
64
|
+
val context = mockk<ReactApplicationContext>(relaxed = true)
|
|
65
|
+
every { context.runOnUiQueueThread(any()) } answers { queuedRunnables.add(firstArg()) }
|
|
66
|
+
return CloudXModuleImpl(context)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private fun drainUiThread() {
|
|
70
|
+
while (queuedRunnables.isNotEmpty()) queuedRunnables.removeAt(0).run()
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
@Test
|
|
74
|
+
fun `destroy then load on same ad unit loads a fresh interstitial`() {
|
|
75
|
+
// Arrange
|
|
76
|
+
val first = mockk<CloudXInterstitialAd>(relaxed = true)
|
|
77
|
+
val second = mockk<CloudXInterstitialAd>(relaxed = true)
|
|
78
|
+
every { CloudX.createInterstitial(any(), any()) } returnsMany listOf(first, second)
|
|
79
|
+
val subject = moduleWithInlineUiThread()
|
|
80
|
+
subject.loadInterstitial(adUnitId)
|
|
81
|
+
|
|
82
|
+
// Act
|
|
83
|
+
subject.destroyInterstitial(adUnitId)
|
|
84
|
+
subject.loadInterstitial(adUnitId)
|
|
85
|
+
|
|
86
|
+
// Assert
|
|
87
|
+
verify(exactly = 1) { first.load() }
|
|
88
|
+
verify(exactly = 1) { first.destroy() }
|
|
89
|
+
verify(exactly = 1) { second.load() }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
@Test
|
|
93
|
+
fun `destroy then load on same ad unit loads a fresh app open ad`() {
|
|
94
|
+
// Arrange
|
|
95
|
+
val first = mockk<CloudXAppOpenAd>(relaxed = true)
|
|
96
|
+
val second = mockk<CloudXAppOpenAd>(relaxed = true)
|
|
97
|
+
every { CloudX.createAppOpen(any(), any()) } returnsMany listOf(first, second)
|
|
98
|
+
val subject = moduleWithInlineUiThread()
|
|
99
|
+
subject.loadAppOpen(adUnitId)
|
|
100
|
+
|
|
101
|
+
// Act
|
|
102
|
+
subject.destroyAppOpen(adUnitId)
|
|
103
|
+
subject.loadAppOpen(adUnitId)
|
|
104
|
+
|
|
105
|
+
// Assert
|
|
106
|
+
verify(exactly = 1) { first.load() }
|
|
107
|
+
verify(exactly = 1) { first.destroy() }
|
|
108
|
+
verify(exactly = 1) { second.load() }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
@Test
|
|
112
|
+
fun `destroy then load on same ad unit loads a fresh rewarded ad`() {
|
|
113
|
+
// Arrange
|
|
114
|
+
val first = mockk<CloudXRewardedAd>(relaxed = true)
|
|
115
|
+
val second = mockk<CloudXRewardedAd>(relaxed = true)
|
|
116
|
+
every { CloudX.createRewarded(any(), any()) } returnsMany listOf(first, second)
|
|
117
|
+
val subject = moduleWithInlineUiThread()
|
|
118
|
+
subject.loadRewarded(adUnitId)
|
|
119
|
+
|
|
120
|
+
// Act
|
|
121
|
+
subject.destroyRewarded(adUnitId)
|
|
122
|
+
subject.loadRewarded(adUnitId)
|
|
123
|
+
|
|
124
|
+
// Assert
|
|
125
|
+
verify(exactly = 1) { first.load() }
|
|
126
|
+
verify(exactly = 1) { first.destroy() }
|
|
127
|
+
verify(exactly = 1) { second.load() }
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
@Test
|
|
131
|
+
fun `load does not touch the SDK before the UI runnable runs`() {
|
|
132
|
+
// Arrange
|
|
133
|
+
val subject = moduleWithStalledUiThread()
|
|
134
|
+
|
|
135
|
+
// Act
|
|
136
|
+
subject.loadInterstitial(adUnitId)
|
|
137
|
+
|
|
138
|
+
// Assert
|
|
139
|
+
verify(exactly = 0) { CloudX.createInterstitial(any(), any()) }
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
@Test
|
|
143
|
+
fun `position update followed by offsets update keeps the new position`() {
|
|
144
|
+
// Arrange
|
|
145
|
+
every { CloudX.createBanner(any(), any()) } returns mockk<CloudXAdView>(relaxed = true)
|
|
146
|
+
val subject = moduleWithQueuedUiThread()
|
|
147
|
+
subject.createBanner(adUnitId, "top")
|
|
148
|
+
drainUiThread()
|
|
149
|
+
|
|
150
|
+
// Act
|
|
151
|
+
subject.updateBannerPosition(adUnitId, "bottom")
|
|
152
|
+
subject.updateBannerOffsets(adUnitId, 1.0, 2.0)
|
|
153
|
+
drainUiThread()
|
|
154
|
+
|
|
155
|
+
// Assert
|
|
156
|
+
assertEquals("bottom", subject.adViewPositionForTesting(adUnitId))
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
@Test
|
|
160
|
+
fun `config setter does not touch the SDK before the UI runnable runs`() {
|
|
161
|
+
// Arrange
|
|
162
|
+
val subject = moduleWithStalledUiThread()
|
|
163
|
+
|
|
164
|
+
// Act
|
|
165
|
+
subject.setHashedUserID("hashed")
|
|
166
|
+
|
|
167
|
+
// Assert
|
|
168
|
+
verify(exactly = 0) { CloudX.setHashedUserId(any()) }
|
|
169
|
+
}
|
|
170
|
+
}
|
package/lib/commonjs/version.js
CHANGED
|
@@ -9,5 +9,5 @@ exports.PLUGIN_VERSION = void 0;
|
|
|
9
9
|
// This value is reported to the native SDK in CloudX.initialize() and is used
|
|
10
10
|
// for wrapper-version telemetry/attribution, so a stale value mis-attributes
|
|
11
11
|
// installs to the wrong release.
|
|
12
|
-
const PLUGIN_VERSION = exports.PLUGIN_VERSION = '3.
|
|
12
|
+
const PLUGIN_VERSION = exports.PLUGIN_VERSION = '3.8.0';
|
|
13
13
|
//# sourceMappingURL=version.js.map
|
package/lib/module/version.js
CHANGED
|
@@ -3,5 +3,5 @@
|
|
|
3
3
|
// This value is reported to the native SDK in CloudX.initialize() and is used
|
|
4
4
|
// for wrapper-version telemetry/attribution, so a stale value mis-attributes
|
|
5
5
|
// installs to the wrong release.
|
|
6
|
-
export const PLUGIN_VERSION = '3.
|
|
6
|
+
export const PLUGIN_VERSION = '3.8.0';
|
|
7
7
|
//# sourceMappingURL=version.js.map
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const PLUGIN_VERSION = "3.
|
|
1
|
+
export declare const PLUGIN_VERSION = "3.8.0";
|
|
2
2
|
//# sourceMappingURL=version.d.ts.map
|
package/package.json
CHANGED
package/src/version.ts
CHANGED
|
@@ -3,4 +3,4 @@
|
|
|
3
3
|
// This value is reported to the native SDK in CloudX.initialize() and is used
|
|
4
4
|
// for wrapper-version telemetry/attribution, so a stale value mis-attributes
|
|
5
5
|
// installs to the wrong release.
|
|
6
|
-
export const PLUGIN_VERSION = '3.
|
|
6
|
+
export const PLUGIN_VERSION = '3.8.0';
|