react-native-liftoffads 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/LICENSE +20 -0
  2. package/NitroLiftoffAds.podspec +26 -0
  3. package/README.md +61 -0
  4. package/android/CMakeLists.txt +20 -0
  5. package/android/build.gradle +132 -0
  6. package/android/consumer-rules.pro +2 -0
  7. package/android/fix-prefab.gradle +46 -0
  8. package/android/gradle.properties +5 -0
  9. package/android/src/main/AndroidManifest.xml +1 -0
  10. package/android/src/main/cpp/cpp-adapter.cpp +7 -0
  11. package/android/src/main/java/com/margelo/nitro/liftoffads/HybridLiftoffAds.kt +121 -0
  12. package/android/src/main/java/com/margelo/nitro/liftoffads/HybridLiftoffInterstitialAd.kt +96 -0
  13. package/android/src/main/java/com/margelo/nitro/liftoffads/HybridLiftoffRewardedAd.kt +104 -0
  14. package/android/src/main/java/com/margelo/nitro/liftoffads/LiftoffAdEventEmitter.kt +51 -0
  15. package/android/src/main/java/com/margelo/nitro/liftoffads/LiftoffAndroidUtils.kt +54 -0
  16. package/android/src/main/java/com/margelo/nitro/liftoffads/NitroLiftoffAdsPackage.java +27 -0
  17. package/ios/HybridLiftoffAds.swift +113 -0
  18. package/ios/HybridLiftoffInterstitialAd.swift +98 -0
  19. package/ios/HybridLiftoffRewardedAd.swift +100 -0
  20. package/ios/LiftoffSupport.swift +106 -0
  21. package/lib/module/index.js +169 -0
  22. package/lib/module/index.js.map +1 -0
  23. package/lib/module/index.test.js +205 -0
  24. package/lib/module/index.test.js.map +1 -0
  25. package/lib/module/package.json +1 -0
  26. package/lib/module/specs/LiftoffAds.nitro.js +4 -0
  27. package/lib/module/specs/LiftoffAds.nitro.js.map +1 -0
  28. package/lib/typescript/package.json +1 -0
  29. package/lib/typescript/src/index.d.ts +72 -0
  30. package/lib/typescript/src/index.d.ts.map +1 -0
  31. package/lib/typescript/src/index.test.d.ts +2 -0
  32. package/lib/typescript/src/index.test.d.ts.map +1 -0
  33. package/lib/typescript/src/specs/LiftoffAds.nitro.d.ts +46 -0
  34. package/lib/typescript/src/specs/LiftoffAds.nitro.d.ts.map +1 -0
  35. package/nitro.json +18 -0
  36. package/nitrogen/generated/android/NitroLiftoffAds+autolinking.cmake +85 -0
  37. package/nitrogen/generated/android/NitroLiftoffAds+autolinking.gradle +27 -0
  38. package/nitrogen/generated/android/NitroLiftoffAdsOnLoad.cpp +50 -0
  39. package/nitrogen/generated/android/NitroLiftoffAdsOnLoad.hpp +25 -0
  40. package/nitrogen/generated/android/c++/JFunc_void_std__optional_LiftoffAdEventPayload_.hpp +78 -0
  41. package/nitrogen/generated/android/c++/JHybridLiftoffAdsSpec.cpp +88 -0
  42. package/nitrogen/generated/android/c++/JHybridLiftoffAdsSpec.hpp +69 -0
  43. package/nitrogen/generated/android/c++/JHybridLiftoffInterstitialAdSpec.cpp +89 -0
  44. package/nitrogen/generated/android/c++/JHybridLiftoffInterstitialAdSpec.hpp +68 -0
  45. package/nitrogen/generated/android/c++/JHybridLiftoffRewardedAdSpec.cpp +93 -0
  46. package/nitrogen/generated/android/c++/JHybridLiftoffRewardedAdSpec.hpp +69 -0
  47. package/nitrogen/generated/android/c++/JLiftoffAdEventPayload.hpp +58 -0
  48. package/nitrogen/generated/android/c++/JLiftoffAdEventType.hpp +77 -0
  49. package/nitrogen/generated/android/c++/JLiftoffAdShowOptions.hpp +53 -0
  50. package/nitrogen/generated/android/kotlin/com/margelo/nitro/liftoffads/Func_void_std__optional_LiftoffAdEventPayload_.kt +81 -0
  51. package/nitrogen/generated/android/kotlin/com/margelo/nitro/liftoffads/HybridLiftoffAdsSpec.kt +72 -0
  52. package/nitrogen/generated/android/kotlin/com/margelo/nitro/liftoffads/HybridLiftoffInterstitialAdSpec.kt +73 -0
  53. package/nitrogen/generated/android/kotlin/com/margelo/nitro/liftoffads/HybridLiftoffRewardedAdSpec.kt +77 -0
  54. package/nitrogen/generated/android/kotlin/com/margelo/nitro/liftoffads/LiftoffAdEventPayload.kt +32 -0
  55. package/nitrogen/generated/android/kotlin/com/margelo/nitro/liftoffads/LiftoffAdEventType.kt +27 -0
  56. package/nitrogen/generated/android/kotlin/com/margelo/nitro/liftoffads/LiftoffAdShowOptions.kt +29 -0
  57. package/nitrogen/generated/android/kotlin/com/margelo/nitro/liftoffads/NitroLiftoffAdsOnLoad.kt +35 -0
  58. package/nitrogen/generated/ios/NitroLiftoffAds+autolinking.rb +60 -0
  59. package/nitrogen/generated/ios/NitroLiftoffAds-Swift-Cxx-Bridge.cpp +90 -0
  60. package/nitrogen/generated/ios/NitroLiftoffAds-Swift-Cxx-Bridge.hpp +285 -0
  61. package/nitrogen/generated/ios/NitroLiftoffAds-Swift-Cxx-Umbrella.hpp +66 -0
  62. package/nitrogen/generated/ios/NitroLiftoffAdsAutolinking.mm +33 -0
  63. package/nitrogen/generated/ios/NitroLiftoffAdsAutolinking.swift +25 -0
  64. package/nitrogen/generated/ios/c++/HybridLiftoffAdsSpecSwift.cpp +11 -0
  65. package/nitrogen/generated/ios/c++/HybridLiftoffAdsSpecSwift.hpp +114 -0
  66. package/nitrogen/generated/ios/c++/HybridLiftoffInterstitialAdSpecSwift.cpp +11 -0
  67. package/nitrogen/generated/ios/c++/HybridLiftoffInterstitialAdSpecSwift.hpp +110 -0
  68. package/nitrogen/generated/ios/c++/HybridLiftoffRewardedAdSpecSwift.cpp +11 -0
  69. package/nitrogen/generated/ios/c++/HybridLiftoffRewardedAdSpecSwift.hpp +116 -0
  70. package/nitrogen/generated/ios/swift/Func_void.swift +47 -0
  71. package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +47 -0
  72. package/nitrogen/generated/ios/swift/Func_void_std__optional_LiftoffAdEventPayload_.swift +54 -0
  73. package/nitrogen/generated/ios/swift/HybridLiftoffAdsSpec.swift +54 -0
  74. package/nitrogen/generated/ios/swift/HybridLiftoffAdsSpec_cxx.swift +193 -0
  75. package/nitrogen/generated/ios/swift/HybridLiftoffInterstitialAdSpec.swift +53 -0
  76. package/nitrogen/generated/ios/swift/HybridLiftoffInterstitialAdSpec_cxx.swift +193 -0
  77. package/nitrogen/generated/ios/swift/HybridLiftoffRewardedAdSpec.swift +54 -0
  78. package/nitrogen/generated/ios/swift/HybridLiftoffRewardedAdSpec_cxx.swift +204 -0
  79. package/nitrogen/generated/ios/swift/LiftoffAdEventPayload.swift +77 -0
  80. package/nitrogen/generated/ios/swift/LiftoffAdEventType.swift +64 -0
  81. package/nitrogen/generated/ios/swift/LiftoffAdShowOptions.swift +47 -0
  82. package/nitrogen/generated/shared/c++/HybridLiftoffAdsSpec.cpp +26 -0
  83. package/nitrogen/generated/shared/c++/HybridLiftoffAdsSpec.hpp +74 -0
  84. package/nitrogen/generated/shared/c++/HybridLiftoffInterstitialAdSpec.cpp +25 -0
  85. package/nitrogen/generated/shared/c++/HybridLiftoffInterstitialAdSpec.hpp +76 -0
  86. package/nitrogen/generated/shared/c++/HybridLiftoffRewardedAdSpec.cpp +26 -0
  87. package/nitrogen/generated/shared/c++/HybridLiftoffRewardedAdSpec.hpp +78 -0
  88. package/nitrogen/generated/shared/c++/LiftoffAdEventPayload.hpp +72 -0
  89. package/nitrogen/generated/shared/c++/LiftoffAdEventType.hpp +100 -0
  90. package/nitrogen/generated/shared/c++/LiftoffAdShowOptions.hpp +67 -0
  91. package/package.json +173 -0
  92. package/react-native.config.js +8 -0
  93. package/src/index.test.ts +257 -0
  94. package/src/index.ts +307 -0
  95. package/src/specs/LiftoffAds.nitro.ts +67 -0
@@ -0,0 +1,51 @@
1
+ package com.margelo.nitro.liftoffads
2
+
3
+ internal class LiftoffAdEventEmitter {
4
+ private val lock = Any()
5
+ private val listeners = mutableMapOf<Double, Listener>()
6
+ private var nextSubscriptionId = 1.0
7
+
8
+ fun add(
9
+ eventType: LiftoffAdEventType,
10
+ listener: (payload: LiftoffAdEventPayload?) -> Unit,
11
+ ): Double = synchronized(lock) {
12
+ val subscriptionId = nextSubscriptionId++
13
+ listeners[subscriptionId] = Listener(eventType, listener)
14
+ subscriptionId
15
+ }
16
+
17
+ fun remove(subscriptionId: Double) {
18
+ synchronized(lock) { listeners.remove(subscriptionId) }
19
+ }
20
+
21
+ fun removeAll() {
22
+ synchronized(lock) { listeners.clear() }
23
+ }
24
+
25
+ fun emit(
26
+ eventType: LiftoffAdEventType,
27
+ payload: LiftoffAdEventPayload? = null,
28
+ ) {
29
+ val callbacks = synchronized(lock) {
30
+ listeners.values
31
+ .filter { it.eventType == eventType }
32
+ .map { it.callback }
33
+ }
34
+ callbacks.forEach { callback -> runCatching { callback(payload) } }
35
+ }
36
+
37
+ private data class Listener(
38
+ val eventType: LiftoffAdEventType,
39
+ val callback: (payload: LiftoffAdEventPayload?) -> Unit,
40
+ )
41
+ }
42
+
43
+ internal fun Throwable.toLiftoffEventPayload(): LiftoffAdEventPayload {
44
+ val vungleError =
45
+ this as? com.vungle.ads.VungleError
46
+ ?: cause as? com.vungle.ads.VungleError
47
+ return LiftoffAdEventPayload(
48
+ errorCode = vungleError?.code?.toDouble(),
49
+ errorMessage = message ?: localizedMessage,
50
+ )
51
+ }
@@ -0,0 +1,54 @@
1
+ package com.margelo.nitro.liftoffads
2
+
3
+ import android.app.Activity
4
+ import android.view.View
5
+ import com.facebook.react.bridge.ReactApplicationContext
6
+ import com.vungle.ads.VungleError
7
+ import java.lang.ref.WeakReference
8
+
9
+ internal fun ReactApplicationContext.requireCurrentActivity(): Activity =
10
+ currentActivity ?: error("Cannot show Liftoff ad without an active Activity")
11
+
12
+ internal class ImmersiveModeController {
13
+ private var activityReference: WeakReference<Activity>? = null
14
+ private var previousSystemUiVisibility: Int? = null
15
+
16
+ fun enable(activity: Activity) {
17
+ restore()
18
+
19
+ @Suppress("DEPRECATION")
20
+ val decorView = activity.window.decorView
21
+ @Suppress("DEPRECATION")
22
+ previousSystemUiVisibility = decorView.systemUiVisibility
23
+ activityReference = WeakReference(activity)
24
+
25
+ @Suppress("DEPRECATION")
26
+ decorView.systemUiVisibility =
27
+ View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY or
28
+ View.SYSTEM_UI_FLAG_FULLSCREEN or
29
+ View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or
30
+ View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or
31
+ View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
32
+ View.SYSTEM_UI_FLAG_LAYOUT_STABLE
33
+ }
34
+
35
+ fun restore() {
36
+ val activity = activityReference?.get()
37
+ val visibility = previousSystemUiVisibility
38
+ activityReference = null
39
+ previousSystemUiVisibility = null
40
+
41
+ if (activity != null && visibility != null) {
42
+ activity.runOnUiThread {
43
+ @Suppress("DEPRECATION")
44
+ activity.window.decorView.systemUiVisibility = visibility
45
+ }
46
+ }
47
+ }
48
+ }
49
+
50
+ internal fun liftoffException(stage: String, error: VungleError) =
51
+ IllegalStateException(
52
+ "Liftoff $stage failed (${error.code}): ${error.localizedMessage}",
53
+ error,
54
+ )
@@ -0,0 +1,27 @@
1
+ package com.margelo.nitro.liftoffads;
2
+
3
+ import androidx.annotation.Nullable;
4
+
5
+ import com.facebook.react.TurboReactPackage;
6
+ import com.facebook.react.bridge.NativeModule;
7
+ import com.facebook.react.bridge.ReactApplicationContext;
8
+ import com.facebook.react.module.model.ReactModuleInfoProvider;
9
+
10
+ import java.util.HashMap;
11
+
12
+ public class NitroLiftoffAdsPackage extends TurboReactPackage {
13
+ @Nullable
14
+ @Override
15
+ public NativeModule getModule(String name, ReactApplicationContext reactContext) {
16
+ return null;
17
+ }
18
+
19
+ @Override
20
+ public ReactModuleInfoProvider getReactModuleInfoProvider() {
21
+ return HashMap::new;
22
+ }
23
+
24
+ static {
25
+ NitroLiftoffAdsOnLoad.initializeNative();
26
+ }
27
+ }
@@ -0,0 +1,113 @@
1
+ import Foundation
2
+ import NitroModules
3
+ import VungleAdsSDK
4
+
5
+ final class HybridLiftoffAds: HybridLiftoffAdsSpec, @unchecked Sendable {
6
+ private let stateLock = NSLock()
7
+ private var initializedAppId: String?
8
+ private var initializingAppId: String?
9
+ private var initializationContinuations: [CheckedContinuation<Void, Error>] = []
10
+ private var isInitializing = false
11
+
12
+ func initialize(appId: String) throws -> Promise<Void> {
13
+ guard !appId.isEmpty else {
14
+ throw liftoffError("Liftoff app ID is empty")
15
+ }
16
+
17
+ return Promise.async {
18
+ try await withCheckedThrowingContinuation { continuation in
19
+ DispatchQueue.main.async {
20
+ self.stateLock.lock()
21
+ if VungleAds.isInitialized() {
22
+ let existing = self.initializedAppId
23
+ if existing == nil { self.initializedAppId = appId }
24
+ self.stateLock.unlock()
25
+
26
+ if let existing, existing != appId {
27
+ continuation.resume(
28
+ throwing: liftoffError("Liftoff SDK was initialized with another app ID")
29
+ )
30
+ } else {
31
+ continuation.resume()
32
+ }
33
+ return
34
+ }
35
+
36
+ if self.isInitializing, self.initializingAppId != appId {
37
+ self.stateLock.unlock()
38
+ continuation.resume(
39
+ throwing: liftoffError("Liftoff SDK is being initialized with another app ID")
40
+ )
41
+ return
42
+ }
43
+
44
+ self.initializationContinuations.append(continuation)
45
+ if self.isInitializing {
46
+ self.stateLock.unlock()
47
+ return
48
+ }
49
+
50
+ self.isInitializing = true
51
+ self.initializingAppId = appId
52
+ self.stateLock.unlock()
53
+
54
+ VungleAds.initWithAppId(appId) { error in
55
+ self.stateLock.lock()
56
+ let continuations = self.initializationContinuations
57
+ self.initializationContinuations.removeAll()
58
+ self.isInitializing = false
59
+ self.initializingAppId = nil
60
+ if error == nil { self.initializedAppId = appId }
61
+ self.stateLock.unlock()
62
+
63
+ continuations.forEach { item in
64
+ if let error {
65
+ item.resume(throwing: error)
66
+ } else {
67
+ item.resume()
68
+ }
69
+ }
70
+ }
71
+ }
72
+ }
73
+ }
74
+ }
75
+
76
+ func setGDPRConsent(optIn: Bool, consentMessageVersion: String) throws {
77
+ runOnMain {
78
+ VunglePrivacySettings.setGDPRStatus(optIn)
79
+ VunglePrivacySettings.setGDPRMessageVersion(consentMessageVersion)
80
+ }
81
+ }
82
+
83
+ func setCCPAConsent(optIn: Bool) throws {
84
+ runOnMain { VunglePrivacySettings.setCCPAStatus(optIn) }
85
+ }
86
+
87
+ func setCOPPA(isUserCoppa: Bool) throws {
88
+ runOnMain { VunglePrivacySettings.setCOPPAStatus(isUserCoppa) }
89
+ }
90
+
91
+ func createInterstitialAd(
92
+ placementId: String
93
+ ) throws -> any HybridLiftoffInterstitialAdSpec {
94
+ try requireInitialized(placementId: placementId)
95
+ return HybridLiftoffInterstitialAd(placementId: placementId)
96
+ }
97
+
98
+ func createRewardedAd(
99
+ placementId: String
100
+ ) throws -> any HybridLiftoffRewardedAdSpec {
101
+ try requireInitialized(placementId: placementId)
102
+ return HybridLiftoffRewardedAd(placementId: placementId)
103
+ }
104
+
105
+ private func requireInitialized(placementId: String) throws {
106
+ guard VungleAds.isInitialized() else {
107
+ throw liftoffError("Liftoff SDK is not initialized")
108
+ }
109
+ guard !placementId.isEmpty else {
110
+ throw liftoffError("Liftoff placement ID is empty")
111
+ }
112
+ }
113
+ }
@@ -0,0 +1,98 @@
1
+ import Foundation
2
+ import NitroModules
3
+ import VungleAdsSDK
4
+
5
+ final class HybridLiftoffInterstitialAd:
6
+ HybridLiftoffInterstitialAdSpec, @unchecked Sendable {
7
+ fileprivate let events = LiftoffAdEventEmitter()
8
+ private let ad: VungleInterstitial
9
+ private lazy var delegate = LiftoffInterstitialDelegate(owner: self)
10
+
11
+ init(placementId: String) {
12
+ self.ad = VungleInterstitial(placementId: placementId)
13
+ super.init()
14
+ self.ad.delegate = delegate
15
+ }
16
+
17
+ func load() throws {
18
+ DispatchQueue.main.async { self.ad.load() }
19
+ }
20
+
21
+ func show(options: LiftoffAdShowOptions?) throws -> Promise<Void> {
22
+ Promise.async {
23
+ do {
24
+ let viewController = try await currentViewController()
25
+ try await withCheckedThrowingContinuation {
26
+ (continuation: CheckedContinuation<Void, Error>) in
27
+ DispatchQueue.main.async {
28
+ guard self.ad.canPlayAd() else {
29
+ continuation.resume(
30
+ throwing: liftoffError("Liftoff interstitial ad is not ready")
31
+ )
32
+ return
33
+ }
34
+ self.ad.present(with: viewController)
35
+ continuation.resume()
36
+ }
37
+ }
38
+ } catch {
39
+ self.events.emit(.error, error: error)
40
+ throw error
41
+ }
42
+ }
43
+ }
44
+
45
+ func addAdEventListener(
46
+ eventType: LiftoffAdEventType,
47
+ listener: @escaping (LiftoffAdEventPayload?) -> Void
48
+ ) throws -> Double {
49
+ events.add(eventType: eventType, listener: listener)
50
+ }
51
+
52
+ func removeAdEventListener(subscriptionId: Double) throws {
53
+ events.remove(subscriptionId: subscriptionId)
54
+ }
55
+
56
+ func removeAllListeners() throws {
57
+ events.removeAll()
58
+ }
59
+
60
+ }
61
+
62
+ private final class LiftoffInterstitialDelegate: NSObject, VungleInterstitialDelegate {
63
+ private weak var owner: HybridLiftoffInterstitialAd?
64
+
65
+ init(owner: HybridLiftoffInterstitialAd) {
66
+ self.owner = owner
67
+ }
68
+
69
+ func interstitialAdDidLoad(_ interstitial: VungleInterstitial) {
70
+ owner?.events.emit(.loaded)
71
+ }
72
+
73
+ func interstitialAdDidFailToLoad(
74
+ _ interstitial: VungleInterstitial,
75
+ withError error: NSError
76
+ ) {
77
+ owner?.events.emit(.error, error: error)
78
+ }
79
+
80
+ func interstitialAdDidFailToPresent(
81
+ _ interstitial: VungleInterstitial,
82
+ withError error: NSError
83
+ ) {
84
+ owner?.events.emit(.error, error: error)
85
+ }
86
+
87
+ func interstitialAdDidPresent(_ interstitial: VungleInterstitial) {
88
+ owner?.events.emit(.opened)
89
+ }
90
+
91
+ func interstitialAdDidClick(_ interstitial: VungleInterstitial) {
92
+ owner?.events.emit(.clicked)
93
+ }
94
+
95
+ func interstitialAdDidClose(_ interstitial: VungleInterstitial) {
96
+ owner?.events.emit(.closed)
97
+ }
98
+ }
@@ -0,0 +1,100 @@
1
+ import Foundation
2
+ import NitroModules
3
+ import VungleAdsSDK
4
+
5
+ final class HybridLiftoffRewardedAd:
6
+ HybridLiftoffRewardedAdSpec, @unchecked Sendable {
7
+ fileprivate let events = LiftoffAdEventEmitter()
8
+ private let ad: VungleRewarded
9
+ private lazy var delegate = LiftoffRewardedDelegate(owner: self)
10
+
11
+ init(placementId: String) {
12
+ self.ad = VungleRewarded(placementId: placementId)
13
+ super.init()
14
+ self.ad.delegate = delegate
15
+ }
16
+
17
+ func load() throws {
18
+ DispatchQueue.main.async { self.ad.load() }
19
+ }
20
+
21
+ func setUserId(userId: String) throws {
22
+ DispatchQueue.main.async { self.ad.setUserId(userId: userId) }
23
+ }
24
+
25
+ func show(options: LiftoffAdShowOptions?) throws -> Promise<Void> {
26
+ Promise.async {
27
+ do {
28
+ let viewController = try await currentViewController()
29
+ try await withCheckedThrowingContinuation {
30
+ (continuation: CheckedContinuation<Void, Error>) in
31
+ DispatchQueue.main.async {
32
+ guard self.ad.canPlayAd() else {
33
+ continuation.resume(
34
+ throwing: liftoffError("Liftoff rewarded ad is not ready")
35
+ )
36
+ return
37
+ }
38
+ self.ad.present(with: viewController)
39
+ continuation.resume()
40
+ }
41
+ }
42
+ } catch {
43
+ self.events.emit(.error, error: error)
44
+ throw error
45
+ }
46
+ }
47
+ }
48
+
49
+ func addAdEventListener(
50
+ eventType: LiftoffAdEventType,
51
+ listener: @escaping (LiftoffAdEventPayload?) -> Void
52
+ ) throws -> Double {
53
+ events.add(eventType: eventType, listener: listener)
54
+ }
55
+
56
+ func removeAdEventListener(subscriptionId: Double) throws {
57
+ events.remove(subscriptionId: subscriptionId)
58
+ }
59
+
60
+ func removeAllListeners() throws {
61
+ events.removeAll()
62
+ }
63
+
64
+ }
65
+
66
+ private final class LiftoffRewardedDelegate: NSObject, VungleRewardedDelegate {
67
+ private weak var owner: HybridLiftoffRewardedAd?
68
+
69
+ init(owner: HybridLiftoffRewardedAd) {
70
+ self.owner = owner
71
+ }
72
+
73
+ func rewardedAdDidLoad(_ rewarded: VungleRewarded) {
74
+ owner?.events.emit(.rewardedLoaded)
75
+ }
76
+
77
+ func rewardedAdDidFailToLoad(_ rewarded: VungleRewarded, withError error: NSError) {
78
+ owner?.events.emit(.error, error: error)
79
+ }
80
+
81
+ func rewardedAdDidFailToPresent(_ rewarded: VungleRewarded, withError error: NSError) {
82
+ owner?.events.emit(.error, error: error)
83
+ }
84
+
85
+ func rewardedAdDidPresent(_ rewarded: VungleRewarded) {
86
+ owner?.events.emit(.opened)
87
+ }
88
+
89
+ func rewardedAdDidClick(_ rewarded: VungleRewarded) {
90
+ owner?.events.emit(.clicked)
91
+ }
92
+
93
+ func rewardedAdDidRewardUser(_ rewarded: VungleRewarded) {
94
+ owner?.events.emit(.rewardedEarnedReward)
95
+ }
96
+
97
+ func rewardedAdDidClose(_ rewarded: VungleRewarded) {
98
+ owner?.events.emit(.closed)
99
+ }
100
+ }
@@ -0,0 +1,106 @@
1
+ import Foundation
2
+ import UIKit
3
+
4
+ let liftoffErrorDomain = "NitroLiftoffAds"
5
+
6
+ func liftoffError(_ message: String, code: Int = 1) -> NSError {
7
+ NSError(
8
+ domain: liftoffErrorDomain,
9
+ code: code,
10
+ userInfo: [NSLocalizedDescriptionKey: message]
11
+ )
12
+ }
13
+
14
+ func runOnMain(_ body: @escaping () -> Void) {
15
+ if Thread.isMainThread {
16
+ body()
17
+ } else {
18
+ DispatchQueue.main.sync(execute: body)
19
+ }
20
+ }
21
+
22
+ func currentViewController() async throws -> UIViewController {
23
+ try await withCheckedThrowingContinuation { continuation in
24
+ DispatchQueue.main.async {
25
+ let root = UIApplication.shared.connectedScenes
26
+ .compactMap { $0 as? UIWindowScene }
27
+ .flatMap(\.windows)
28
+ .first(where: \.isKeyWindow)?
29
+ .rootViewController
30
+
31
+ guard let root else {
32
+ continuation.resume(
33
+ throwing: liftoffError("Cannot show Liftoff ad without a view controller")
34
+ )
35
+ return
36
+ }
37
+ continuation.resume(returning: topViewController(root))
38
+ }
39
+ }
40
+ }
41
+
42
+ private func topViewController(_ viewController: UIViewController) -> UIViewController {
43
+ if let presented = viewController.presentedViewController {
44
+ return topViewController(presented)
45
+ }
46
+ if let navigation = viewController as? UINavigationController,
47
+ let visible = navigation.visibleViewController {
48
+ return topViewController(visible)
49
+ }
50
+ if let tab = viewController as? UITabBarController,
51
+ let selected = tab.selectedViewController {
52
+ return topViewController(selected)
53
+ }
54
+ return viewController
55
+ }
56
+
57
+ final class LiftoffAdEventEmitter {
58
+ typealias Listener = (LiftoffAdEventPayload?) -> Void
59
+
60
+ private let lock = NSLock()
61
+ private var listeners: [Double: (eventType: String, listener: Listener)] = [:]
62
+ private var nextSubscriptionId = 1.0
63
+
64
+ func add(eventType: LiftoffAdEventType, listener: @escaping Listener) -> Double {
65
+ lock.lock()
66
+ defer { lock.unlock() }
67
+
68
+ let subscriptionId = nextSubscriptionId
69
+ nextSubscriptionId += 1
70
+ listeners[subscriptionId] = (eventType.stringValue, listener)
71
+ return subscriptionId
72
+ }
73
+
74
+ func remove(subscriptionId: Double) {
75
+ lock.lock()
76
+ listeners.removeValue(forKey: subscriptionId)
77
+ lock.unlock()
78
+ }
79
+
80
+ func removeAll() {
81
+ lock.lock()
82
+ listeners.removeAll()
83
+ lock.unlock()
84
+ }
85
+
86
+ func emit(
87
+ _ eventType: LiftoffAdEventType,
88
+ error: Error? = nil,
89
+ payload: LiftoffAdEventPayload? = nil
90
+ ) {
91
+ lock.lock()
92
+ let callbacks = listeners.values
93
+ .filter { $0.eventType == eventType.stringValue }
94
+ .map(\.listener)
95
+ lock.unlock()
96
+
97
+ let resolvedPayload = payload ?? error.map { error in
98
+ let nsError = error as NSError
99
+ return LiftoffAdEventPayload(
100
+ errorCode: Double(nsError.code),
101
+ errorMessage: nsError.localizedDescription
102
+ )
103
+ }
104
+ callbacks.forEach { $0(resolvedPayload) }
105
+ }
106
+ }