@bigcrunch/react-native-ads 0.4.0 → 0.5.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 (57) hide show
  1. package/README.md +5 -5
  2. package/android/bigcrunch-ads/com/bigcrunch/ads/BigCrunchAds.kt +434 -0
  3. package/android/bigcrunch-ads/com/bigcrunch/ads/BigCrunchBannerView.kt +484 -0
  4. package/android/bigcrunch-ads/com/bigcrunch/ads/BigCrunchInterstitial.kt +403 -0
  5. package/android/bigcrunch-ads/com/bigcrunch/ads/BigCrunchRewarded.kt +409 -0
  6. package/android/bigcrunch-ads/com/bigcrunch/ads/adapters/GoogleAdsAdapter.kt +592 -0
  7. package/android/bigcrunch-ads/com/bigcrunch/ads/core/AdOrchestrator.kt +623 -0
  8. package/android/bigcrunch-ads/com/bigcrunch/ads/core/AnalyticsClient.kt +719 -0
  9. package/android/bigcrunch-ads/com/bigcrunch/ads/core/BidRequestClient.kt +364 -0
  10. package/android/bigcrunch-ads/com/bigcrunch/ads/core/ConfigManager.kt +301 -0
  11. package/android/bigcrunch-ads/com/bigcrunch/ads/core/DeviceContext.kt +385 -0
  12. package/android/bigcrunch-ads/com/bigcrunch/ads/core/RewardedCallback.kt +42 -0
  13. package/android/bigcrunch-ads/com/bigcrunch/ads/core/SessionManager.kt +330 -0
  14. package/android/bigcrunch-ads/com/bigcrunch/ads/internal/DeviceHelper.kt +60 -0
  15. package/android/bigcrunch-ads/com/bigcrunch/ads/internal/HttpClient.kt +114 -0
  16. package/android/bigcrunch-ads/com/bigcrunch/ads/internal/Logger.kt +71 -0
  17. package/android/bigcrunch-ads/com/bigcrunch/ads/internal/PrivacyStore.kt +125 -0
  18. package/android/bigcrunch-ads/com/bigcrunch/ads/internal/Storage.kt +88 -0
  19. package/android/bigcrunch-ads/com/bigcrunch/ads/listeners/BannerAdListener.kt +55 -0
  20. package/android/bigcrunch-ads/com/bigcrunch/ads/listeners/InterstitialAdListener.kt +55 -0
  21. package/android/bigcrunch-ads/com/bigcrunch/ads/listeners/RewardedAdListener.kt +58 -0
  22. package/android/bigcrunch-ads/com/bigcrunch/ads/models/AdEvent.kt +880 -0
  23. package/android/bigcrunch-ads/com/bigcrunch/ads/models/AppConfig.kt +90 -0
  24. package/android/bigcrunch-ads/com/bigcrunch/ads/models/DeviceData.kt +18 -0
  25. package/android/bigcrunch-ads/com/bigcrunch/ads/models/PlacementConfig.kt +70 -0
  26. package/android/bigcrunch-ads/com/bigcrunch/ads/models/SessionInfo.kt +21 -0
  27. package/android/build.gradle +22 -10
  28. package/android/settings.gradle +2 -6
  29. package/ios/BigCrunchAds/Sources/Adapters/GoogleAdsAdapter.swift +512 -0
  30. package/ios/BigCrunchAds/Sources/BigCrunchAds.swift +387 -0
  31. package/ios/BigCrunchAds/Sources/BigCrunchBannerView.swift +448 -0
  32. package/ios/BigCrunchAds/Sources/BigCrunchInterstitial.swift +412 -0
  33. package/ios/BigCrunchAds/Sources/BigCrunchRewarded.swift +523 -0
  34. package/ios/BigCrunchAds/Sources/Core/AdOrchestrator.swift +514 -0
  35. package/ios/BigCrunchAds/Sources/Core/AnalyticsClient.swift +874 -0
  36. package/ios/BigCrunchAds/Sources/Core/BidRequestClient.swift +344 -0
  37. package/ios/BigCrunchAds/Sources/Core/ConfigManager.swift +306 -0
  38. package/ios/BigCrunchAds/Sources/Core/DeviceContext.swift +284 -0
  39. package/ios/BigCrunchAds/Sources/Core/SessionManager.swift +392 -0
  40. package/ios/BigCrunchAds/Sources/Internal/HTTPClient.swift +146 -0
  41. package/ios/BigCrunchAds/Sources/Internal/Logger.swift +62 -0
  42. package/ios/BigCrunchAds/Sources/Internal/PrivacyStore.swift +129 -0
  43. package/ios/BigCrunchAds/Sources/Internal/Storage.swift +73 -0
  44. package/ios/BigCrunchAds/Sources/Models/AdEvent.swift +784 -0
  45. package/ios/BigCrunchAds/Sources/Models/AppConfig.swift +100 -0
  46. package/ios/BigCrunchAds/Sources/Models/DeviceData.swift +68 -0
  47. package/ios/BigCrunchAds/Sources/Models/PlacementConfig.swift +137 -0
  48. package/ios/BigCrunchAds/Sources/Models/SessionInfo.swift +48 -0
  49. package/ios/BigCrunchAdsModule.swift +0 -1
  50. package/ios/BigCrunchBannerViewManager.swift +0 -1
  51. package/lib/index.d.ts +1 -1
  52. package/lib/index.d.ts.map +1 -1
  53. package/lib/index.js +3 -2
  54. package/package.json +8 -2
  55. package/react-native-bigcrunch-ads.podspec +0 -1
  56. package/scripts/inject-version.js +55 -0
  57. package/src/index.ts +3 -2
@@ -0,0 +1,484 @@
1
+ package com.bigcrunch.ads
2
+
3
+ import android.content.Context
4
+ import android.os.Handler
5
+ import android.os.Looper
6
+ import android.util.AttributeSet
7
+ import android.view.View
8
+ import android.widget.FrameLayout
9
+ import com.bigcrunch.ads.adapters.GoogleAdsAdapter
10
+ import com.bigcrunch.ads.core.AdOrchestrator
11
+ import com.bigcrunch.ads.core.BidRequestClient
12
+ import com.bigcrunch.ads.core.BannerCallback
13
+ import com.bigcrunch.ads.internal.BCLogger
14
+ import com.bigcrunch.ads.listeners.BannerAdListener
15
+ import com.bigcrunch.ads.models.AdSize
16
+ import com.bigcrunch.ads.models.RefreshConfig
17
+
18
+ /**
19
+ * BigCrunch Banner View - UI component for displaying banner ads
20
+ *
21
+ * Usage:
22
+ * ```kotlin
23
+ * val bannerView = BigCrunchBannerView(context)
24
+ * bannerView.configure("banner_placement_id")
25
+ * bannerView.listener = object : BigCrunchBannerView.Listener {
26
+ * override fun onAdLoaded() { /* ad loaded */ }
27
+ * override fun onAdFailedToLoad(error: String) { /* handle error */ }
28
+ * override fun onAdClicked() { /* ad clicked */ }
29
+ * override fun onAdImpression() { /* impression recorded */ }
30
+ * }
31
+ * bannerView.loadAd()
32
+ *
33
+ * // When done:
34
+ * bannerView.destroy()
35
+ * ```
36
+ *
37
+ * Add to layout XML:
38
+ * ```xml
39
+ * <com.bigcrunch.ads.BigCrunchBannerView
40
+ * android:id="@+id/banner_ad"
41
+ * android:layout_width="match_parent"
42
+ * android:layout_height="wrap_content" />
43
+ * ```
44
+ */
45
+ class BigCrunchBannerView @JvmOverloads constructor(
46
+ context: Context,
47
+ attrs: AttributeSet? = null,
48
+ defStyleAttr: Int = 0
49
+ ) : FrameLayout(context, attrs, defStyleAttr) {
50
+
51
+ companion object {
52
+ private const val TAG = "BigCrunchBannerView"
53
+ private const val MIN_REFRESH_INTERVAL_MS = 10000L // 10 seconds minimum
54
+
55
+ /**
56
+ * Safely execute a block, catching exceptions to prevent SDK crashes from affecting the host app.
57
+ */
58
+ private inline fun safeExecute(block: () -> Unit) {
59
+ try {
60
+ block()
61
+ } catch (e: Exception) {
62
+ BCLogger.e(TAG, "Exception caught in banner view - SDK will not propagate to prevent app crash", e)
63
+ }
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Optional listener for ad events
69
+ */
70
+ var listener: BannerAdListener? = null
71
+ set(value) {
72
+ field = value
73
+ bannerAdListener = value
74
+ }
75
+
76
+ // Keep reference to support both old and new listener interfaces
77
+ private var bannerAdListener: BannerAdListener? = null
78
+
79
+ private var placementId: String? = null
80
+ private var adSize: AdSize? = null
81
+ private var customTargeting: Map<String, String>? = null
82
+ private var isConfigured = false
83
+ private var isDestroyed = false
84
+ private var isPaused = false
85
+ private var adOrchestrator: AdOrchestrator? = null
86
+
87
+ // Refresh properties
88
+ private val refreshHandler = Handler(Looper.getMainLooper())
89
+ private var refreshRunnable: Runnable? = null
90
+ private var refreshCount: Int = 0
91
+ private var effectiveRefreshConfig: RefreshConfig? = null
92
+
93
+ /**
94
+ * Runnable that manually measures and lays out child views.
95
+ * This is needed for React Native compatibility because React Native's UIManagerModule
96
+ * intercepts requestLayout() calls, preventing dynamically added child views
97
+ * (like AdManagerAdView) from being properly measured and laid out.
98
+ * See: https://github.com/facebook/react-native/issues/17968
99
+ */
100
+ private val measureAndLayout = Runnable {
101
+ BCLogger.d(TAG, "=== measureAndLayout START === childCount=$childCount, parent=${width}x${height}, measured=${measuredWidth}x${measuredHeight}")
102
+
103
+ for (i in 0 until childCount) {
104
+ val child = getChildAt(i)
105
+
106
+ // For AdManagerAdView, use its own ad size for measurement
107
+ val childWidth: Int
108
+ val childHeight: Int
109
+
110
+ if (child is com.google.android.gms.ads.admanager.AdManagerAdView) {
111
+ val adSize = child.adSize
112
+ BCLogger.d(TAG, "Child $i is AdManagerAdView, adSize=$adSize")
113
+ if (adSize != null) {
114
+ val adWidthPx = adSize.getWidthInPixels(context)
115
+ val adHeightPx = adSize.getHeightInPixels(context)
116
+ // Constrain width to parent to prevent overflow (important for adaptive banners)
117
+ val parentWidth = if (width > 0) width else measuredWidth
118
+ childWidth = if (parentWidth > 0) minOf(adWidthPx, parentWidth) else adWidthPx
119
+ childHeight = adHeightPx
120
+ BCLogger.d(TAG, "Using adSize pixels: ${adWidthPx}x${adHeightPx}, constrained width: $childWidth (parent: $parentWidth)")
121
+ } else {
122
+ // Fallback to parent dimensions
123
+ childWidth = if (width > 0) width else measuredWidth
124
+ childHeight = if (height > 0) height else measuredHeight
125
+ BCLogger.d(TAG, "adSize is null, using parent: ${childWidth}x${childHeight}")
126
+ }
127
+ } else {
128
+ // For other views, use parent dimensions
129
+ childWidth = if (width > 0) width else measuredWidth
130
+ childHeight = if (height > 0) height else measuredHeight
131
+ BCLogger.d(TAG, "Child $i is ${child.javaClass.simpleName}, using parent: ${childWidth}x${childHeight}")
132
+ }
133
+
134
+ // Skip if we don't have valid dimensions
135
+ if (childWidth <= 0 || childHeight <= 0) {
136
+ BCLogger.w(TAG, "Skipping child $i - invalid dimensions: ${childWidth}x${childHeight}")
137
+ continue
138
+ }
139
+
140
+ BCLogger.d(TAG, "BEFORE measure/layout - child dimensions: ${child.width}x${child.height}, measured: ${child.measuredWidth}x${child.measuredHeight}, visibility: ${child.visibility}")
141
+
142
+ child.measure(
143
+ View.MeasureSpec.makeMeasureSpec(childWidth, View.MeasureSpec.EXACTLY),
144
+ View.MeasureSpec.makeMeasureSpec(childHeight, View.MeasureSpec.EXACTLY)
145
+ )
146
+ child.layout(0, 0, child.measuredWidth, child.measuredHeight)
147
+
148
+ // Force invalidation to ensure the view is redrawn after layout
149
+ child.invalidate()
150
+
151
+ BCLogger.d(TAG, "AFTER measure/layout - child dimensions: ${child.width}x${child.height}, measured: ${child.measuredWidth}x${child.measuredHeight}, visibility: ${child.visibility}")
152
+
153
+ // Log additional view properties for debugging
154
+ if (child is com.google.android.gms.ads.admanager.AdManagerAdView) {
155
+ BCLogger.d(TAG, """
156
+ === AD VIEW DEBUG ===
157
+ isShown: ${child.isShown}
158
+ alpha: ${child.alpha}
159
+ isAttachedToWindow: ${child.isAttachedToWindow}
160
+ hasWindowFocus: ${child.hasWindowFocus()}
161
+ parent: ${child.parent}
162
+ clipChildren (this): $clipChildren
163
+ clipToPadding (this): $clipToPadding
164
+ =====================
165
+ """.trimIndent())
166
+ }
167
+ }
168
+
169
+ // Invalidate this container as well to ensure redraw
170
+ invalidate()
171
+ BCLogger.d(TAG, "=== measureAndLayout END ===")
172
+ }
173
+
174
+ init {
175
+ // Disable clipping to ensure child views can be fully drawn
176
+ // This is important for React Native where clipping can cause issues
177
+ clipChildren = false
178
+ clipToPadding = false
179
+ BCLogger.v(TAG, "BigCrunchBannerView created")
180
+ }
181
+
182
+ /**
183
+ * Override requestLayout to ensure child views are properly measured and laid out.
184
+ * React Native suppresses normal Android layout passes, so we manually trigger them.
185
+ */
186
+ override fun requestLayout() {
187
+ super.requestLayout()
188
+ post(measureAndLayout)
189
+ }
190
+
191
+ /**
192
+ * Configure the banner view with a placement ID
193
+ *
194
+ * @param placementId The placement ID from the BigCrunch dashboard
195
+ */
196
+ fun configure(placementId: String) {
197
+ if (isDestroyed) {
198
+ BCLogger.w(TAG, "Cannot configure destroyed banner view")
199
+ return
200
+ }
201
+
202
+ if (placementId.isBlank()) {
203
+ BCLogger.e(TAG, "Invalid placementId: cannot be blank")
204
+ return
205
+ }
206
+
207
+ this.placementId = placementId
208
+ this.isConfigured = true
209
+ BCLogger.d(TAG, "Banner configured with placement: $placementId")
210
+ }
211
+
212
+ /**
213
+ * Load a banner ad
214
+ *
215
+ * Must call [configure] before calling this method.
216
+ * The listener will be notified of load success or failure.
217
+ */
218
+ fun loadAd() = safeExecute {
219
+ if (isDestroyed) {
220
+ BCLogger.w(TAG, "Cannot load ad on destroyed banner view")
221
+ bannerAdListener?.onAdFailedToLoad("DESTROYED", "Banner view has been destroyed")
222
+ return@safeExecute
223
+ }
224
+
225
+ if (!isConfigured || placementId == null) {
226
+ BCLogger.e(TAG, "Banner not configured. Call configure() first.")
227
+ bannerAdListener?.onAdFailedToLoad("NOT_CONFIGURED", "Banner not configured. Call configure() first.")
228
+ return@safeExecute
229
+ }
230
+
231
+ val pid = placementId ?: return@safeExecute
232
+ BCLogger.d(TAG, "Loading banner ad: $pid with adSize override: $adSize (refreshCount: $refreshCount)")
233
+
234
+ if (!BigCrunchAds.isInitialized()) {
235
+ BCLogger.e(TAG, "SDK not initialized")
236
+ bannerAdListener?.onAdFailedToLoad("SDK_NOT_INITIALIZED", "SDK not initialized. Call BigCrunchAds.initialize() first.")
237
+ return@safeExecute
238
+ }
239
+
240
+ // Resolve effective refresh config on first load
241
+ if (effectiveRefreshConfig == null) {
242
+ val configManager = BigCrunchAds.getConfigManager()
243
+ effectiveRefreshConfig = configManager.getEffectiveRefreshConfig(pid)
244
+ effectiveRefreshConfig?.let { config ->
245
+ BCLogger.d(TAG, "Refresh config resolved - enabled: ${config.enabled}, interval: ${config.intervalMs}ms, max: ${config.maxRefreshes}")
246
+ }
247
+ }
248
+
249
+ // Create AdOrchestrator lazily
250
+ if (adOrchestrator == null) {
251
+ adOrchestrator = createAdOrchestrator()
252
+ }
253
+
254
+ // Create callback wrapper
255
+ val callback = object : BannerCallback {
256
+ override fun onAdLoaded() {
257
+ BCLogger.d(TAG, "Banner ad loaded: $pid")
258
+ bannerAdListener?.onAdLoaded()
259
+ // Schedule next refresh after successful load
260
+ scheduleRefresh()
261
+ }
262
+
263
+ override fun onAdFailedToLoad(error: String) {
264
+ BCLogger.w(TAG, "Banner ad failed to load: $pid - $error")
265
+ bannerAdListener?.onAdFailedToLoad("LOAD_ERROR", error)
266
+ // Note: Do NOT schedule refresh on failure to avoid retry loops
267
+ }
268
+
269
+ override fun onAdClicked() {
270
+ BCLogger.d(TAG, "Banner ad clicked: $pid")
271
+ bannerAdListener?.onAdClicked()
272
+ }
273
+
274
+ override fun onAdImpression() {
275
+ BCLogger.d(TAG, "Banner ad impression: $pid")
276
+ bannerAdListener?.onAdImpression()
277
+ }
278
+ }
279
+
280
+ // Load the ad through the orchestrator, passing the ad size override if set
281
+ adOrchestrator?.loadBannerAd(pid, this, callback, adSize, refreshCount)
282
+ }
283
+
284
+ /**
285
+ * Destroy the banner view and release resources
286
+ *
287
+ * Call this when the banner is no longer needed (e.g., in Activity.onDestroy())
288
+ */
289
+ fun destroy() = safeExecute {
290
+ if (isDestroyed) {
291
+ BCLogger.v(TAG, "Banner already destroyed")
292
+ return@safeExecute
293
+ }
294
+
295
+ BCLogger.d(TAG, "Destroying banner: $placementId")
296
+
297
+ // Stop refresh timer
298
+ cancelRefreshTimer()
299
+
300
+ placementId?.let { pid ->
301
+ adOrchestrator?.destroyBannerAd(pid)
302
+ }
303
+
304
+ removeAllViews()
305
+ listener = null
306
+ adOrchestrator = null
307
+ effectiveRefreshConfig = null
308
+ refreshCount = 0
309
+ isDestroyed = true
310
+ }
311
+
312
+ /**
313
+ * Check if the banner view has been destroyed
314
+ */
315
+ fun isDestroyed(): Boolean = isDestroyed
316
+
317
+ /**
318
+ * Get the configured placement ID
319
+ */
320
+ fun getPlacementId(): String? = placementId
321
+
322
+ /**
323
+ * Get the current refresh count
324
+ */
325
+ fun getRefreshCount(): Int = refreshCount
326
+
327
+ /**
328
+ * Set the placement ID (alternative to configure method)
329
+ *
330
+ * @param placementId The placement ID from the BigCrunch dashboard
331
+ */
332
+ fun setPlacementId(placementId: String) {
333
+ configure(placementId)
334
+ }
335
+
336
+ /**
337
+ * Set the ad size for this banner
338
+ *
339
+ * @param adSize The desired ad size
340
+ */
341
+ fun setAdSize(adSize: AdSize) {
342
+ this.adSize = adSize
343
+ BCLogger.d(TAG, "Ad size set to: $adSize")
344
+ }
345
+
346
+ /**
347
+ * Get the current ad size
348
+ *
349
+ * @return The ad size, or null if not set
350
+ */
351
+ fun getAdSize(): AdSize? = adSize
352
+
353
+ /**
354
+ * Set custom targeting parameters for ad requests
355
+ *
356
+ * @param targeting Map of key-value pairs for custom targeting
357
+ */
358
+ fun setCustomTargeting(targeting: Map<String, String>) {
359
+ this.customTargeting = targeting
360
+ BCLogger.d(TAG, "Custom targeting set: ${targeting.size} parameters")
361
+ }
362
+
363
+ /**
364
+ * Set the refresh interval for auto-refreshing ads
365
+ *
366
+ * This overrides the config-driven refresh interval.
367
+ *
368
+ * @param intervalMs Refresh interval in milliseconds (0 to disable)
369
+ */
370
+ fun setRefreshInterval(intervalMs: Int) {
371
+ if (intervalMs <= 0) {
372
+ effectiveRefreshConfig = RefreshConfig(enabled = false, intervalMs = 0, maxRefreshes = 0)
373
+ cancelRefreshTimer()
374
+ } else {
375
+ effectiveRefreshConfig = RefreshConfig(
376
+ enabled = true,
377
+ intervalMs = intervalMs,
378
+ maxRefreshes = effectiveRefreshConfig?.maxRefreshes ?: 99
379
+ )
380
+ }
381
+ BCLogger.d(TAG, "Refresh interval set to: ${intervalMs}ms")
382
+ }
383
+
384
+ /**
385
+ * Set the banner ad listener
386
+ *
387
+ * @param listener The listener for ad events
388
+ */
389
+ fun setBannerAdListener(listener: BannerAdListener) {
390
+ this.listener = listener
391
+ }
392
+
393
+ /**
394
+ * Pause ad refresh (if auto-refresh is enabled)
395
+ */
396
+ fun pause() {
397
+ isPaused = true
398
+ cancelRefreshTimer()
399
+ BCLogger.d(TAG, "Banner ad refresh paused")
400
+ }
401
+
402
+ /**
403
+ * Resume ad refresh (if auto-refresh is enabled)
404
+ */
405
+ fun resume() {
406
+ isPaused = false
407
+ BCLogger.d(TAG, "Banner ad refresh resumed")
408
+ scheduleRefresh()
409
+ }
410
+
411
+ // MARK: - Refresh Logic
412
+
413
+ private fun scheduleRefresh() {
414
+ // Don't schedule if destroyed or paused
415
+ if (isDestroyed || isPaused) return
416
+
417
+ val config = effectiveRefreshConfig ?: return
418
+ if (!config.enabled) return
419
+
420
+ // Check max refreshes limit
421
+ if (refreshCount >= config.maxRefreshes) {
422
+ BCLogger.d(TAG, "Max refreshes reached (${config.maxRefreshes}) for $placementId")
423
+ return
424
+ }
425
+
426
+ // Enforce minimum interval
427
+ val intervalMs = maxOf(config.intervalMs.toLong(), MIN_REFRESH_INTERVAL_MS)
428
+
429
+ // Cancel any existing timer
430
+ cancelRefreshTimer()
431
+
432
+ BCLogger.d(TAG, "Scheduling refresh in ${intervalMs}ms (count: $refreshCount/${config.maxRefreshes}) for $placementId")
433
+
434
+ val runnable = Runnable {
435
+ performRefresh()
436
+ }
437
+ refreshRunnable = runnable
438
+ refreshHandler.postDelayed(runnable, intervalMs)
439
+ }
440
+
441
+ private fun performRefresh() {
442
+ if (isDestroyed || isPaused) return
443
+
444
+ refreshCount++
445
+ BCLogger.d(TAG, "Refreshing banner ad (refresh #$refreshCount) for $placementId")
446
+
447
+ loadAd()
448
+ }
449
+
450
+ private fun cancelRefreshTimer() {
451
+ refreshRunnable?.let { refreshHandler.removeCallbacks(it) }
452
+ refreshRunnable = null
453
+ }
454
+
455
+ private fun createAdOrchestrator(): AdOrchestrator {
456
+ val configManager = BigCrunchAds.getConfigManager()
457
+ val analyticsClient = BigCrunchAds.getAnalyticsClient()
458
+ val googleAdsAdapter = GoogleAdsAdapter(context, analyticsClient)
459
+
460
+ // BidRequestClient is shared across all views for batching
461
+ val bidRequestClient = BigCrunchAds.bidRequestClient ?: BidRequestClient(
462
+ httpClient = com.bigcrunch.ads.internal.HttpClient(),
463
+ configManager = configManager,
464
+ privacyStore = BigCrunchAds.privacyStore,
465
+ s2sConfig = com.bigcrunch.ads.models.S2SConfig(enabled = false, serverUrl = "", timeoutMs = 0)
466
+ )
467
+
468
+ return AdOrchestrator(
469
+ context,
470
+ configManager,
471
+ analyticsClient,
472
+ bidRequestClient,
473
+ googleAdsAdapter
474
+ )
475
+ }
476
+
477
+ override fun onDetachedFromWindow() {
478
+ super.onDetachedFromWindow()
479
+ // Note: We don't auto-destroy here for React Native compatibility
480
+ // React Native may temporarily detach views during ScrollView recycling
481
+ // The view manager will call destroy() when appropriate
482
+ BCLogger.v(TAG, "Banner detached from window (not auto-destroying)")
483
+ }
484
+ }