@bigcrunch/react-native-ads 0.3.1 → 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 (64) 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/android/src/main/java/com/bigcrunch/ads/react/BigCrunchAdsModule.kt +0 -23
  30. package/ios/BigCrunchAds/Sources/Adapters/GoogleAdsAdapter.swift +512 -0
  31. package/ios/BigCrunchAds/Sources/BigCrunchAds.swift +387 -0
  32. package/ios/BigCrunchAds/Sources/BigCrunchBannerView.swift +448 -0
  33. package/ios/BigCrunchAds/Sources/BigCrunchInterstitial.swift +412 -0
  34. package/ios/BigCrunchAds/Sources/BigCrunchRewarded.swift +523 -0
  35. package/ios/BigCrunchAds/Sources/Core/AdOrchestrator.swift +514 -0
  36. package/ios/BigCrunchAds/Sources/Core/AnalyticsClient.swift +874 -0
  37. package/ios/BigCrunchAds/Sources/Core/BidRequestClient.swift +344 -0
  38. package/ios/BigCrunchAds/Sources/Core/ConfigManager.swift +306 -0
  39. package/ios/BigCrunchAds/Sources/Core/DeviceContext.swift +284 -0
  40. package/ios/BigCrunchAds/Sources/Core/SessionManager.swift +392 -0
  41. package/ios/BigCrunchAds/Sources/Internal/HTTPClient.swift +146 -0
  42. package/ios/BigCrunchAds/Sources/Internal/Logger.swift +62 -0
  43. package/ios/BigCrunchAds/Sources/Internal/PrivacyStore.swift +129 -0
  44. package/ios/BigCrunchAds/Sources/Internal/Storage.swift +73 -0
  45. package/ios/BigCrunchAds/Sources/Models/AdEvent.swift +784 -0
  46. package/ios/BigCrunchAds/Sources/Models/AppConfig.swift +100 -0
  47. package/ios/BigCrunchAds/Sources/Models/DeviceData.swift +68 -0
  48. package/ios/BigCrunchAds/Sources/Models/PlacementConfig.swift +137 -0
  49. package/ios/BigCrunchAds/Sources/Models/SessionInfo.swift +48 -0
  50. package/ios/BigCrunchAdsModule.swift +5 -14
  51. package/ios/BigCrunchBannerViewManager.swift +0 -1
  52. package/lib/index.d.ts +1 -1
  53. package/lib/index.d.ts.map +1 -1
  54. package/lib/index.js +3 -2
  55. package/lib/types/config.d.ts +22 -9
  56. package/lib/types/config.d.ts.map +1 -1
  57. package/lib/types/events.d.ts +4 -4
  58. package/lib/types/events.d.ts.map +1 -1
  59. package/package.json +11 -4
  60. package/react-native-bigcrunch-ads.podspec +1 -3
  61. package/scripts/inject-version.js +55 -0
  62. package/src/index.ts +3 -2
  63. package/src/types/config.ts +23 -9
  64. package/src/types/events.ts +4 -4
@@ -0,0 +1,448 @@
1
+ import UIKit
2
+ import GoogleMobileAds
3
+
4
+ /**
5
+ * BigCrunch Banner View - UI component for displaying banner ads
6
+ *
7
+ * Usage:
8
+ * ```swift
9
+ * let bannerView = BigCrunchBannerView()
10
+ * bannerView.configure(placementId: "home_banner")
11
+ * bannerView.delegate = self
12
+ * bannerView.loadAd(rootViewController: self)
13
+ *
14
+ * // Add to view hierarchy
15
+ * view.addSubview(bannerView)
16
+ *
17
+ * // When done:
18
+ * bannerView.destroy()
19
+ * ```
20
+ *
21
+ * Implement BigCrunchBannerViewDelegate for ad events:
22
+ * ```swift
23
+ * extension ViewController: BigCrunchBannerViewDelegate {
24
+ * func bannerViewDidLoadAd(_ bannerView: BigCrunchBannerView) { }
25
+ * func bannerView(_ bannerView: BigCrunchBannerView, didFailToLoadWithError error: String) { }
26
+ * func bannerViewDidRecordClick(_ bannerView: BigCrunchBannerView) { }
27
+ * func bannerViewDidRecordImpression(_ bannerView: BigCrunchBannerView) { }
28
+ * }
29
+ * ```
30
+ */
31
+ public class BigCrunchBannerView: UIView {
32
+
33
+ // MARK: - Public Properties
34
+
35
+ /**
36
+ * Delegate for receiving banner ad events
37
+ */
38
+ public weak var delegate: BigCrunchBannerViewDelegate?
39
+
40
+ /**
41
+ * The configured placement ID
42
+ */
43
+ public private(set) var placementId: String?
44
+
45
+ /**
46
+ * Override ad size (optional)
47
+ *
48
+ * If set, this size will be used instead of the size from backend config.
49
+ * Set this before calling loadAd().
50
+ * Use `AdSize.adaptive()` for adaptive banners that fill the screen width.
51
+ */
52
+ public var adSizeOverride: AdSize?
53
+
54
+ // MARK: - Private Properties
55
+
56
+ private static let TAG = "BigCrunchBannerView"
57
+ private static let MIN_REFRESH_INTERVAL_MS = 10000 // 10 seconds minimum
58
+ private var isConfigured = false
59
+ private var isDestroyed = false
60
+ private var isPaused = false
61
+ private var adOrchestrator: AdOrchestrator?
62
+ private var internalBannerView: GoogleMobileAds.BannerView?
63
+
64
+ // Refresh properties
65
+ private var refreshTimer: Timer?
66
+ private var refreshCount: Int = 0
67
+ private var effectiveRefreshConfig: RefreshConfig?
68
+ private weak var rootViewControllerForRefresh: UIViewController?
69
+
70
+ // MARK: - Initialization
71
+
72
+ public override init(frame: CGRect) {
73
+ super.init(frame: frame)
74
+ BCLogger.verbose("\(BigCrunchBannerView.TAG): BigCrunchBannerView created")
75
+ }
76
+
77
+ public required init?(coder: NSCoder) {
78
+ super.init(coder: coder)
79
+ BCLogger.verbose("\(BigCrunchBannerView.TAG): BigCrunchBannerView created from coder")
80
+ }
81
+
82
+ deinit {
83
+ if !isDestroyed {
84
+ BCLogger.verbose("\(BigCrunchBannerView.TAG): Auto-destroying banner in deinit")
85
+ destroy()
86
+ }
87
+ }
88
+
89
+ // MARK: - Public Methods
90
+
91
+ /**
92
+ * Configure the banner view with a placement ID
93
+ *
94
+ * - Parameter placementId: The placement ID from the BigCrunch dashboard
95
+ */
96
+ public func configure(placementId: String) {
97
+ if isDestroyed {
98
+ BCLogger.warning("\(BigCrunchBannerView.TAG): Cannot configure destroyed banner view")
99
+ return
100
+ }
101
+
102
+ guard !placementId.isEmpty else {
103
+ BCLogger.error("\(BigCrunchBannerView.TAG): Invalid placementId: cannot be empty")
104
+ return
105
+ }
106
+
107
+ self.placementId = placementId
108
+ self.isConfigured = true
109
+ BCLogger.debug("\(BigCrunchBannerView.TAG): Banner configured with placement: \(placementId)")
110
+ }
111
+
112
+ /**
113
+ * Load a banner ad
114
+ *
115
+ * Must call `configure(placementId:)` before calling this method.
116
+ * The delegate will be notified of load success or failure.
117
+ *
118
+ * - Parameter rootViewController: The view controller to use for presenting ad click actions
119
+ */
120
+ public func loadAd(rootViewController: UIViewController) {
121
+ if isDestroyed {
122
+ BCLogger.warning("\(BigCrunchBannerView.TAG): Cannot load ad on destroyed banner view")
123
+ delegate?.bannerView(self, didFailToLoadWithError: "Banner view has been destroyed")
124
+ return
125
+ }
126
+
127
+ guard isConfigured, let pid = placementId else {
128
+ BCLogger.error("\(BigCrunchBannerView.TAG): Banner not configured. Call configure() first.")
129
+ delegate?.bannerView(self, didFailToLoadWithError: "Banner not configured. Call configure() first.")
130
+ return
131
+ }
132
+
133
+ BCLogger.debug("\(BigCrunchBannerView.TAG): Loading banner ad: \(pid) (refreshCount: \(refreshCount))")
134
+
135
+ guard BigCrunchAds.isInitialized() else {
136
+ BCLogger.error("\(BigCrunchBannerView.TAG): SDK not initialized")
137
+ delegate?.bannerView(self, didFailToLoadWithError: "SDK not initialized. Call BigCrunchAds.initialize() first.")
138
+ return
139
+ }
140
+
141
+ // Store root view controller for refresh
142
+ rootViewControllerForRefresh = rootViewController
143
+
144
+ // Resolve effective refresh config on first load
145
+ if effectiveRefreshConfig == nil {
146
+ let configManager = BigCrunchAds.getConfigManager()
147
+ effectiveRefreshConfig = configManager.getEffectiveRefreshConfig(placementId: pid)
148
+ if let config = effectiveRefreshConfig {
149
+ BCLogger.debug("\(BigCrunchBannerView.TAG): Refresh config resolved - enabled: \(config.enabled), interval: \(config.intervalMs)ms, max: \(config.maxRefreshes)")
150
+ }
151
+ }
152
+
153
+ // Create AdOrchestrator lazily
154
+ if adOrchestrator == nil {
155
+ adOrchestrator = createAdOrchestrator()
156
+ }
157
+
158
+ // Create callback wrapper
159
+ let callback = BannerCallbackImpl(bannerView: self)
160
+
161
+ // Load the ad through the orchestrator
162
+ _ = adOrchestrator?.loadBannerAd(
163
+ placementId: pid,
164
+ rootViewController: rootViewController,
165
+ callback: callback,
166
+ refreshCount: refreshCount,
167
+ adSizeOverride: adSizeOverride,
168
+ bannerViewSetter: { [weak self] bannerView in
169
+ self?.setInternalBannerView(bannerView)
170
+ }
171
+ )
172
+ }
173
+
174
+ /**
175
+ * Load and display the banner ad (legacy method without root view controller)
176
+ *
177
+ * Note: This method attempts to find the root view controller automatically.
178
+ * For better control, use loadAd(rootViewController:) instead.
179
+ */
180
+ public func loadAd() {
181
+ guard BigCrunchAds.isInitialized() else {
182
+ BCLogger.error("\(BigCrunchBannerView.TAG): SDK not initialized")
183
+ delegate?.bannerView(self, didFailToLoadWithError: "SDK not initialized. Call BigCrunchAds.initialize() first.")
184
+ return
185
+ }
186
+
187
+ // Try to find the root view controller
188
+ guard let rootVC = findRootViewController() else {
189
+ BCLogger.error("\(BigCrunchBannerView.TAG): Could not find root view controller")
190
+ delegate?.bannerView(self, didFailToLoadWithError: "Could not find root view controller. Use loadAd(rootViewController:) instead.")
191
+ return
192
+ }
193
+
194
+ loadAd(rootViewController: rootVC)
195
+ }
196
+
197
+ /**
198
+ * Pause ad refresh
199
+ *
200
+ * Stops the refresh timer. Call `resume()` to restart.
201
+ */
202
+ public func pause() {
203
+ isPaused = true
204
+ cancelRefreshTimer()
205
+ BCLogger.debug("\(BigCrunchBannerView.TAG): Banner ad refresh paused")
206
+ }
207
+
208
+ /**
209
+ * Resume ad refresh
210
+ *
211
+ * Restarts the refresh timer if refresh is enabled and max refreshes not reached.
212
+ */
213
+ public func resume() {
214
+ isPaused = false
215
+ BCLogger.debug("\(BigCrunchBannerView.TAG): Banner ad refresh resumed")
216
+ scheduleRefresh()
217
+ }
218
+
219
+ /**
220
+ * Destroy the banner view and release resources
221
+ *
222
+ * Call this when the banner is no longer needed.
223
+ */
224
+ public func destroy() {
225
+ if isDestroyed {
226
+ BCLogger.verbose("\(BigCrunchBannerView.TAG): Banner already destroyed")
227
+ return
228
+ }
229
+
230
+ BCLogger.debug("\(BigCrunchBannerView.TAG): Destroying banner: \(placementId ?? "nil")")
231
+
232
+ // Stop refresh timer
233
+ cancelRefreshTimer()
234
+
235
+ if let pid = placementId {
236
+ adOrchestrator?.destroyBannerAd(placementId: pid)
237
+ }
238
+
239
+ // Remove internal banner view
240
+ internalBannerView?.removeFromSuperview()
241
+ internalBannerView = nil
242
+
243
+ // Clear references
244
+ delegate = nil
245
+ adOrchestrator = nil
246
+ rootViewControllerForRefresh = nil
247
+ effectiveRefreshConfig = nil
248
+ refreshCount = 0
249
+ isDestroyed = true
250
+ }
251
+
252
+ /**
253
+ * Check if the banner view has been destroyed
254
+ */
255
+ public func isDestroyedState() -> Bool {
256
+ return isDestroyed
257
+ }
258
+
259
+ /**
260
+ * Get the current refresh count
261
+ */
262
+ public func getRefreshCount() -> Int {
263
+ return refreshCount
264
+ }
265
+
266
+ // MARK: - Internal Methods
267
+
268
+ internal func setInternalBannerView(_ bannerView: GoogleMobileAds.BannerView) {
269
+ internalBannerView?.removeFromSuperview()
270
+ internalBannerView = bannerView
271
+
272
+ // Add to view hierarchy
273
+ bannerView.translatesAutoresizingMaskIntoConstraints = false
274
+ addSubview(bannerView)
275
+
276
+ NSLayoutConstraint.activate([
277
+ bannerView.topAnchor.constraint(equalTo: topAnchor),
278
+ bannerView.bottomAnchor.constraint(equalTo: bottomAnchor),
279
+ bannerView.leadingAnchor.constraint(equalTo: leadingAnchor),
280
+ bannerView.trailingAnchor.constraint(equalTo: trailingAnchor)
281
+ ])
282
+ }
283
+
284
+ /// Called by the callback when an ad loads successfully. Schedules the next refresh.
285
+ internal func onAdLoadedForRefresh() {
286
+ scheduleRefresh()
287
+ }
288
+
289
+ // MARK: - Refresh Logic
290
+
291
+ private func scheduleRefresh() {
292
+ // Don't schedule if destroyed or paused
293
+ guard !isDestroyed, !isPaused else { return }
294
+
295
+ guard let config = effectiveRefreshConfig, config.enabled else { return }
296
+
297
+ // Check max refreshes limit
298
+ guard refreshCount < config.maxRefreshes else {
299
+ BCLogger.debug("\(BigCrunchBannerView.TAG): Max refreshes reached (\(config.maxRefreshes)) for \(placementId ?? "")")
300
+ return
301
+ }
302
+
303
+ // Enforce minimum interval
304
+ let intervalMs = max(config.intervalMs, BigCrunchBannerView.MIN_REFRESH_INTERVAL_MS)
305
+ let intervalSeconds = Double(intervalMs) / 1000.0
306
+
307
+ // Cancel any existing timer
308
+ cancelRefreshTimer()
309
+
310
+ BCLogger.debug("\(BigCrunchBannerView.TAG): Scheduling refresh in \(intervalSeconds)s (count: \(refreshCount)/\(config.maxRefreshes)) for \(placementId ?? "")")
311
+
312
+ refreshTimer = Timer.scheduledTimer(withTimeInterval: intervalSeconds, repeats: false) { [weak self] _ in
313
+ self?.performRefresh()
314
+ }
315
+ }
316
+
317
+ private func performRefresh() {
318
+ guard !isDestroyed, !isPaused else { return }
319
+
320
+ guard let rootVC = rootViewControllerForRefresh else {
321
+ BCLogger.warning("\(BigCrunchBannerView.TAG): Cannot refresh - root view controller is nil")
322
+ return
323
+ }
324
+
325
+ refreshCount += 1
326
+ BCLogger.debug("\(BigCrunchBannerView.TAG): Refreshing banner ad (refresh #\(refreshCount)) for \(placementId ?? "")")
327
+
328
+ loadAd(rootViewController: rootVC)
329
+ }
330
+
331
+ private func cancelRefreshTimer() {
332
+ refreshTimer?.invalidate()
333
+ refreshTimer = nil
334
+ }
335
+
336
+ // MARK: - Private Methods
337
+
338
+ private func createAdOrchestrator() -> AdOrchestrator {
339
+ let configManager = BigCrunchAds.getConfigManager()
340
+ let analyticsClient = BigCrunchAds.getAnalyticsClient()
341
+ let googleAdsAdapter = GoogleAdsAdapter(analyticsClient: analyticsClient)
342
+
343
+ // BidRequestClient is shared across all views for batching
344
+ // If S2S is disabled, create a no-op client
345
+ let bidRequestClient = BigCrunchAds.getBidRequestClient() ?? BidRequestClient(
346
+ httpClient: HTTPClient(),
347
+ configManager: configManager,
348
+ privacyStore: BigCrunchAds.privacyStore,
349
+ s2sConfig: S2SConfig(enabled: false, serverUrl: "", timeoutMs: 0)
350
+ )
351
+
352
+ return AdOrchestrator(
353
+ configManager: configManager,
354
+ analyticsClient: analyticsClient,
355
+ bidRequestClient: bidRequestClient,
356
+ googleAdsAdapter: googleAdsAdapter
357
+ )
358
+ }
359
+
360
+ private func findRootViewController() -> UIViewController? {
361
+ // Try to find key window and its root view controller
362
+ if #available(iOS 15.0, *) {
363
+ return UIApplication.shared.connectedScenes
364
+ .compactMap { $0 as? UIWindowScene }
365
+ .flatMap { $0.windows }
366
+ .first { $0.isKeyWindow }?
367
+ .rootViewController
368
+ } else {
369
+ return UIApplication.shared.windows
370
+ .first { $0.isKeyWindow }?
371
+ .rootViewController
372
+ }
373
+ }
374
+ }
375
+
376
+ // MARK: - Delegate Protocol
377
+
378
+ /**
379
+ * Delegate protocol for BigCrunchBannerView events
380
+ */
381
+ public protocol BigCrunchBannerViewDelegate: AnyObject {
382
+ /**
383
+ * Called when the ad has been successfully loaded
384
+ */
385
+ func bannerViewDidLoadAd(_ bannerView: BigCrunchBannerView)
386
+
387
+ /**
388
+ * Called when the ad fails to load
389
+ */
390
+ func bannerView(_ bannerView: BigCrunchBannerView, didFailToLoadWithError error: String)
391
+
392
+ /**
393
+ * Called when the user clicks on the ad
394
+ */
395
+ func bannerViewDidRecordClick(_ bannerView: BigCrunchBannerView)
396
+
397
+ /**
398
+ * Called when an impression is recorded for the ad
399
+ */
400
+ func bannerViewDidRecordImpression(_ bannerView: BigCrunchBannerView)
401
+ }
402
+
403
+ // MARK: - Default Implementations
404
+
405
+ public extension BigCrunchBannerViewDelegate {
406
+ func bannerViewDidLoadAd(_ bannerView: BigCrunchBannerView) {}
407
+ func bannerView(_ bannerView: BigCrunchBannerView, didFailToLoadWithError error: String) {}
408
+ func bannerViewDidRecordClick(_ bannerView: BigCrunchBannerView) {}
409
+ func bannerViewDidRecordImpression(_ bannerView: BigCrunchBannerView) {}
410
+ }
411
+
412
+ // MARK: - Internal Callback Implementation
413
+
414
+ private class BannerCallbackImpl: BannerCallback {
415
+ private static let TAG = "BigCrunchBannerView"
416
+ weak var bannerView: BigCrunchBannerView?
417
+
418
+ init(bannerView: BigCrunchBannerView) {
419
+ self.bannerView = bannerView
420
+ }
421
+
422
+ func onAdLoaded() {
423
+ guard let bannerView = bannerView else { return }
424
+ BCLogger.debug("\(BannerCallbackImpl.TAG): Banner ad loaded: \(bannerView.placementId ?? "")")
425
+ bannerView.delegate?.bannerViewDidLoadAd(bannerView)
426
+ // Schedule next refresh after successful load
427
+ bannerView.onAdLoadedForRefresh()
428
+ }
429
+
430
+ func onAdFailedToLoad(error: String) {
431
+ guard let bannerView = bannerView else { return }
432
+ BCLogger.warning("\(BannerCallbackImpl.TAG): Banner ad failed to load: \(bannerView.placementId ?? "") - \(error)")
433
+ bannerView.delegate?.bannerView(bannerView, didFailToLoadWithError: error)
434
+ // Note: Do NOT schedule refresh on failure to avoid retry loops
435
+ }
436
+
437
+ func onAdClicked() {
438
+ guard let bannerView = bannerView else { return }
439
+ BCLogger.debug("\(BannerCallbackImpl.TAG): Banner ad clicked: \(bannerView.placementId ?? "")")
440
+ bannerView.delegate?.bannerViewDidRecordClick(bannerView)
441
+ }
442
+
443
+ func onAdImpression() {
444
+ guard let bannerView = bannerView else { return }
445
+ BCLogger.debug("\(BannerCallbackImpl.TAG): Banner ad impression: \(bannerView.placementId ?? "")")
446
+ bannerView.delegate?.bannerViewDidRecordImpression(bannerView)
447
+ }
448
+ }