react-native-pangleads 0.1.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 (95) hide show
  1. package/LICENSE +20 -0
  2. package/NitroPangleAds.podspec +26 -0
  3. package/README.md +80 -0
  4. package/android/CMakeLists.txt +20 -0
  5. package/android/build.gradle +129 -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/pangleads/HybridPangleAds.kt +123 -0
  12. package/android/src/main/java/com/margelo/nitro/pangleads/HybridPangleInterstitialAd.kt +106 -0
  13. package/android/src/main/java/com/margelo/nitro/pangleads/HybridPangleRewardedAd.kt +139 -0
  14. package/android/src/main/java/com/margelo/nitro/pangleads/NitroPangleAdsPackage.java +27 -0
  15. package/android/src/main/java/com/margelo/nitro/pangleads/PangleAdEventEmitter.kt +41 -0
  16. package/android/src/main/java/com/margelo/nitro/pangleads/PangleAndroidUtils.kt +32 -0
  17. package/ios/HybridPangleAds.swift +120 -0
  18. package/ios/HybridPangleInterstitialAd.swift +110 -0
  19. package/ios/HybridPangleRewardedAd.swift +143 -0
  20. package/ios/PangleSupport.swift +129 -0
  21. package/lib/module/index.js +172 -0
  22. package/lib/module/index.js.map +1 -0
  23. package/lib/module/index.test.js +201 -0
  24. package/lib/module/index.test.js.map +1 -0
  25. package/lib/module/package.json +1 -0
  26. package/lib/module/specs/PangleAds.nitro.js +4 -0
  27. package/lib/module/specs/PangleAds.nitro.js.map +1 -0
  28. package/lib/typescript/package.json +1 -0
  29. package/lib/typescript/src/index.d.ts +68 -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/PangleAds.nitro.d.ts +46 -0
  34. package/lib/typescript/src/specs/PangleAds.nitro.d.ts.map +1 -0
  35. package/nitro.json +18 -0
  36. package/nitrogen/generated/android/NitroPangleAds+autolinking.cmake +85 -0
  37. package/nitrogen/generated/android/NitroPangleAds+autolinking.gradle +27 -0
  38. package/nitrogen/generated/android/NitroPangleAdsOnLoad.cpp +50 -0
  39. package/nitrogen/generated/android/NitroPangleAdsOnLoad.hpp +25 -0
  40. package/nitrogen/generated/android/c++/JFunc_void_std__optional_PangleAdEventPayload_.hpp +78 -0
  41. package/nitrogen/generated/android/c++/JHybridPangleAdsSpec.cpp +88 -0
  42. package/nitrogen/generated/android/c++/JHybridPangleAdsSpec.hpp +69 -0
  43. package/nitrogen/generated/android/c++/JHybridPangleInterstitialAdSpec.cpp +85 -0
  44. package/nitrogen/generated/android/c++/JHybridPangleInterstitialAdSpec.hpp +68 -0
  45. package/nitrogen/generated/android/c++/JHybridPangleRewardedAdSpec.cpp +89 -0
  46. package/nitrogen/generated/android/c++/JHybridPangleRewardedAdSpec.hpp +68 -0
  47. package/nitrogen/generated/android/c++/JPangleAdEventPayload.hpp +58 -0
  48. package/nitrogen/generated/android/c++/JPangleAdEventType.hpp +74 -0
  49. package/nitrogen/generated/android/c++/JPangleAdVerificationOptions.hpp +58 -0
  50. package/nitrogen/generated/android/kotlin/com/margelo/nitro/pangleads/Func_void_std__optional_PangleAdEventPayload_.kt +81 -0
  51. package/nitrogen/generated/android/kotlin/com/margelo/nitro/pangleads/HybridPangleAdsSpec.kt +72 -0
  52. package/nitrogen/generated/android/kotlin/com/margelo/nitro/pangleads/HybridPangleInterstitialAdSpec.kt +73 -0
  53. package/nitrogen/generated/android/kotlin/com/margelo/nitro/pangleads/HybridPangleRewardedAdSpec.kt +73 -0
  54. package/nitrogen/generated/android/kotlin/com/margelo/nitro/pangleads/NitroPangleAdsOnLoad.kt +35 -0
  55. package/nitrogen/generated/android/kotlin/com/margelo/nitro/pangleads/PangleAdEventPayload.kt +32 -0
  56. package/nitrogen/generated/android/kotlin/com/margelo/nitro/pangleads/PangleAdEventType.kt +26 -0
  57. package/nitrogen/generated/android/kotlin/com/margelo/nitro/pangleads/PangleAdVerificationOptions.kt +32 -0
  58. package/nitrogen/generated/ios/NitroPangleAds+autolinking.rb +60 -0
  59. package/nitrogen/generated/ios/NitroPangleAds-Swift-Cxx-Bridge.cpp +90 -0
  60. package/nitrogen/generated/ios/NitroPangleAds-Swift-Cxx-Bridge.hpp +270 -0
  61. package/nitrogen/generated/ios/NitroPangleAds-Swift-Cxx-Umbrella.hpp +66 -0
  62. package/nitrogen/generated/ios/NitroPangleAdsAutolinking.mm +33 -0
  63. package/nitrogen/generated/ios/NitroPangleAdsAutolinking.swift +25 -0
  64. package/nitrogen/generated/ios/c++/HybridPangleAdsSpecSwift.cpp +11 -0
  65. package/nitrogen/generated/ios/c++/HybridPangleAdsSpecSwift.hpp +114 -0
  66. package/nitrogen/generated/ios/c++/HybridPangleInterstitialAdSpecSwift.cpp +11 -0
  67. package/nitrogen/generated/ios/c++/HybridPangleInterstitialAdSpecSwift.hpp +107 -0
  68. package/nitrogen/generated/ios/c++/HybridPangleRewardedAdSpecSwift.cpp +11 -0
  69. package/nitrogen/generated/ios/c++/HybridPangleRewardedAdSpecSwift.hpp +110 -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_PangleAdEventPayload_.swift +54 -0
  73. package/nitrogen/generated/ios/swift/HybridPangleAdsSpec.swift +54 -0
  74. package/nitrogen/generated/ios/swift/HybridPangleAdsSpec_cxx.swift +193 -0
  75. package/nitrogen/generated/ios/swift/HybridPangleInterstitialAdSpec.swift +53 -0
  76. package/nitrogen/generated/ios/swift/HybridPangleInterstitialAdSpec_cxx.swift +186 -0
  77. package/nitrogen/generated/ios/swift/HybridPangleRewardedAdSpec.swift +53 -0
  78. package/nitrogen/generated/ios/swift/HybridPangleRewardedAdSpec_cxx.swift +193 -0
  79. package/nitrogen/generated/ios/swift/PangleAdEventPayload.swift +77 -0
  80. package/nitrogen/generated/ios/swift/PangleAdEventType.swift +60 -0
  81. package/nitrogen/generated/ios/swift/PangleAdVerificationOptions.swift +84 -0
  82. package/nitrogen/generated/shared/c++/HybridPangleAdsSpec.cpp +26 -0
  83. package/nitrogen/generated/shared/c++/HybridPangleAdsSpec.hpp +74 -0
  84. package/nitrogen/generated/shared/c++/HybridPangleInterstitialAdSpec.cpp +25 -0
  85. package/nitrogen/generated/shared/c++/HybridPangleInterstitialAdSpec.hpp +73 -0
  86. package/nitrogen/generated/shared/c++/HybridPangleRewardedAdSpec.cpp +25 -0
  87. package/nitrogen/generated/shared/c++/HybridPangleRewardedAdSpec.hpp +76 -0
  88. package/nitrogen/generated/shared/c++/PangleAdEventPayload.hpp +72 -0
  89. package/nitrogen/generated/shared/c++/PangleAdEventType.hpp +96 -0
  90. package/nitrogen/generated/shared/c++/PangleAdVerificationOptions.hpp +72 -0
  91. package/package.json +177 -0
  92. package/react-native.config.js +8 -0
  93. package/src/index.test.ts +255 -0
  94. package/src/index.ts +305 -0
  95. package/src/specs/PangleAds.nitro.ts +66 -0
@@ -0,0 +1,139 @@
1
+ package com.margelo.nitro.pangleads
2
+
3
+ import android.os.Handler
4
+ import android.os.Looper
5
+ import androidx.annotation.Keep
6
+ import com.bytedance.sdk.openadsdk.api.init.PAGConfig
7
+ import com.bytedance.sdk.openadsdk.api.reward.PAGRewardItem
8
+ import com.bytedance.sdk.openadsdk.api.reward.PAGRewardedAd
9
+ import com.bytedance.sdk.openadsdk.api.reward.PAGRewardedAdInteractionListener
10
+ import com.bytedance.sdk.openadsdk.api.reward.PAGRewardedAdLoadListener
11
+ import com.bytedance.sdk.openadsdk.api.reward.PAGRewardedRequest
12
+ import com.facebook.proguard.annotations.DoNotStrip
13
+ import com.facebook.react.bridge.ReactApplicationContext
14
+ import com.margelo.nitro.core.Promise
15
+ import kotlinx.coroutines.Dispatchers
16
+ import kotlinx.coroutines.withContext
17
+
18
+ @DoNotStrip
19
+ @Keep
20
+ class HybridPangleRewardedAd(
21
+ private val context: ReactApplicationContext,
22
+ private val placementId: String,
23
+ ) : HybridPangleRewardedAdSpec(), PAGRewardedAdInteractionListener {
24
+ private val events = PangleAdEventEmitter()
25
+ private val mainHandler = Handler(Looper.getMainLooper())
26
+ private var ad: PAGRewardedAd? = null
27
+ private var showTimeout: Runnable? = null
28
+
29
+ override fun load(verification: PangleAdVerificationOptions?) {
30
+ context.runOnUiQueueThread {
31
+ // Pangle's S2S reward callback fills `user_id` from this SDK-wide
32
+ // setting, not from PAGRewardedRequest.extraInfo — without it Pangle
33
+ // reports the callback with its own placeholder user id.
34
+ verification?.userId?.takeIf { it.isNotEmpty() }?.let(PAGConfig::setUserData)
35
+ val request = buildRewardedRequest(verification)
36
+ PAGRewardedAd.loadAd(
37
+ placementId,
38
+ request,
39
+ object : PAGRewardedAdLoadListener {
40
+ override fun onError(code: Int, message: String) {
41
+ mainHandler.post {
42
+ events.emit(
43
+ PangleAdEventType.ERROR,
44
+ pangleException("load", code, message).toPangleEventPayload(),
45
+ )
46
+ }
47
+ }
48
+
49
+ override fun onAdLoaded(rewardedAd: PAGRewardedAd) {
50
+ mainHandler.post {
51
+ rewardedAd.setAdInteractionListener(this@HybridPangleRewardedAd)
52
+ ad = rewardedAd
53
+ events.emit(PangleAdEventType.REWARDED_LOADED)
54
+ }
55
+ }
56
+ },
57
+ )
58
+ }
59
+ }
60
+
61
+ private fun buildRewardedRequest(verification: PangleAdVerificationOptions?): PAGRewardedRequest {
62
+ val request = PAGRewardedRequest()
63
+ val extraInfo = extraInfoFrom(verification)
64
+ if (extraInfo.isNotEmpty()) {
65
+ request.extraInfo = extraInfo
66
+ }
67
+ return request
68
+ }
69
+
70
+ private fun extraInfoFrom(verification: PangleAdVerificationOptions?): Map<String, String> = buildMap {
71
+ verification?.userId?.takeIf { it.isNotEmpty() }?.let { put("userId", it) }
72
+ verification?.customData?.takeIf { it.isNotEmpty() }?.let { put("customData", it) }
73
+ }
74
+
75
+ override fun show(): Promise<Unit> = Promise.async {
76
+ withContext(Dispatchers.Main) {
77
+ val activity = context.requireCurrentActivity()
78
+ val current = ad ?: error("Pangle rewarded ad is not ready")
79
+ armShowTimeout()
80
+ current.show(activity)
81
+ }
82
+ }
83
+
84
+ override fun addAdEventListener(
85
+ eventType: PangleAdEventType,
86
+ listener: (payload: PangleAdEventPayload?) -> Unit,
87
+ ) = events.add(eventType, listener)
88
+
89
+ override fun removeAdEventListener(subscriptionId: Double) {
90
+ events.remove(subscriptionId)
91
+ }
92
+
93
+ override fun removeAllListeners() {
94
+ events.removeAll()
95
+ }
96
+
97
+ override fun onAdShowed() {
98
+ disarmShowTimeout()
99
+ events.emit(PangleAdEventType.OPENED)
100
+ }
101
+
102
+ override fun onAdClicked() {
103
+ events.emit(PangleAdEventType.CLICKED)
104
+ }
105
+
106
+ override fun onAdDismissed() {
107
+ disarmShowTimeout()
108
+ ad = null
109
+ events.emit(PangleAdEventType.CLOSED)
110
+ }
111
+
112
+ override fun onUserEarnedReward(rewardItem: PAGRewardItem) {
113
+ disarmShowTimeout()
114
+ events.emit(PangleAdEventType.REWARDED_EARNED_REWARD)
115
+ }
116
+
117
+ override fun onUserEarnedRewardFail(errorCode: Int, errorMsg: String) {
118
+ events.emit(
119
+ PangleAdEventType.ERROR,
120
+ pangleException("reward", errorCode, errorMsg).toPangleEventPayload(),
121
+ )
122
+ }
123
+
124
+ private fun armShowTimeout() {
125
+ disarmShowTimeout()
126
+ val runnable = Runnable {
127
+ showTimeout = null
128
+ ad = null
129
+ events.emit(PangleAdEventType.ERROR, pangleShowTimeoutPayload())
130
+ }
131
+ showTimeout = runnable
132
+ mainHandler.postDelayed(runnable, PANGLE_SHOW_TIMEOUT_MS)
133
+ }
134
+
135
+ private fun disarmShowTimeout() {
136
+ showTimeout?.let(mainHandler::removeCallbacks)
137
+ showTimeout = null
138
+ }
139
+ }
@@ -0,0 +1,27 @@
1
+ package com.margelo.nitro.pangleads;
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 NitroPangleAdsPackage 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
+ NitroPangleAdsOnLoad.initializeNative();
26
+ }
27
+ }
@@ -0,0 +1,41 @@
1
+ package com.margelo.nitro.pangleads
2
+
3
+ internal class PangleAdEventEmitter {
4
+ private val lock = Any()
5
+ private val listeners = mutableMapOf<Double, Listener>()
6
+ private var nextSubscriptionId = 1.0
7
+
8
+ fun add(
9
+ eventType: PangleAdEventType,
10
+ listener: (payload: PangleAdEventPayload?) -> 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: PangleAdEventType,
27
+ payload: PangleAdEventPayload? = 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: PangleAdEventType,
39
+ val callback: (payload: PangleAdEventPayload?) -> Unit,
40
+ )
41
+ }
@@ -0,0 +1,32 @@
1
+ package com.margelo.nitro.pangleads
2
+
3
+ import com.facebook.react.bridge.ReactApplicationContext
4
+
5
+ // PAGInterstitialAdInteractionListener/PAGRewardedAdInteractionListener have no
6
+ // show-failure callback, so a show() that the SDK silently drops would
7
+ // otherwise never emit CLOSED/ERROR and hang the caller forever. show() call
8
+ // sites arm a timeout via this value and clear it once a real callback fires.
9
+ internal const val PANGLE_SHOW_TIMEOUT_MS = 10_000L
10
+
11
+ internal class PangleLoadException(
12
+ val code: Int,
13
+ message: String,
14
+ ) : IllegalStateException(message)
15
+
16
+ internal fun pangleException(stage: String, code: Int, message: String) =
17
+ PangleLoadException(code, "Pangle $stage failed ($code): $message")
18
+
19
+ internal fun pangleShowTimeoutPayload(): PangleAdEventPayload =
20
+ PangleAdEventPayload(
21
+ errorCode = null,
22
+ errorMessage = "Pangle ad show() timed out without a response from the SDK",
23
+ )
24
+
25
+ internal fun ReactApplicationContext.requireCurrentActivity() =
26
+ currentActivity ?: error("Cannot show Pangle ad without an active Activity")
27
+
28
+ internal fun Throwable.toPangleEventPayload(): PangleAdEventPayload =
29
+ PangleAdEventPayload(
30
+ errorCode = (this as? PangleLoadException)?.code?.toDouble(),
31
+ errorMessage = message ?: localizedMessage,
32
+ )
@@ -0,0 +1,120 @@
1
+ import Foundation
2
+ import NitroModules
3
+ import PAGAdSDK
4
+
5
+ final class HybridPangleAds: HybridPangleAdsSpec, @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 pangleError("Pangle 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 let existing = self.initializedAppId {
22
+ self.stateLock.unlock()
23
+ if existing != appId {
24
+ continuation.resume(
25
+ throwing: pangleError("Pangle SDK was initialized with another app ID")
26
+ )
27
+ } else {
28
+ continuation.resume()
29
+ }
30
+ return
31
+ }
32
+
33
+ if self.isInitializing, self.initializingAppId != appId {
34
+ self.stateLock.unlock()
35
+ continuation.resume(
36
+ throwing: pangleError("Pangle SDK is being initialized with another app ID")
37
+ )
38
+ return
39
+ }
40
+
41
+ self.initializationContinuations.append(continuation)
42
+ if self.isInitializing {
43
+ self.stateLock.unlock()
44
+ return
45
+ }
46
+
47
+ self.isInitializing = true
48
+ self.initializingAppId = appId
49
+ self.stateLock.unlock()
50
+
51
+ let config = PAGConfig.share()
52
+ config.appID = appId
53
+
54
+ PAGSdk.start(with: config, completionHandler: { success, 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 success { self.initializedAppId = appId }
61
+ self.stateLock.unlock()
62
+
63
+ continuations.forEach { item in
64
+ if !success {
65
+ item.resume(throwing: error ?? pangleError("Pangle SDK initialization failed"))
66
+ } else {
67
+ item.resume()
68
+ }
69
+ }
70
+ })
71
+ }
72
+ }
73
+ }
74
+ }
75
+
76
+ // PAGAdSDK 8.x collapsed the separate GDPRConsent/doNotSell properties into a single
77
+ // paConsent flag, so GDPR and CCPA consent are no longer distinguishable on iOS.
78
+ func setGDPRConsent(optIn: Bool) throws {
79
+ runOnMain {
80
+ PAGConfig.share().paConsent = optIn ? .consent : .noConsent
81
+ }
82
+ }
83
+
84
+ func setCCPAConsent(optIn: Bool) throws {
85
+ runOnMain {
86
+ PAGConfig.share().paConsent = optIn ? .consent : .noConsent
87
+ }
88
+ }
89
+
90
+ // PAGAdSDK 8.x removed the dedicated COPPA/child-directed API (PAGChildDirectedType);
91
+ // there is no longer a public way to flag child-directed traffic to Pangle on iOS.
92
+ func setCOPPA(isUserCoppa: Bool) throws {}
93
+
94
+ func createInterstitialAd(
95
+ placementId: String
96
+ ) throws -> any HybridPangleInterstitialAdSpec {
97
+ try requireInitialized(placementId: placementId)
98
+ return HybridPangleInterstitialAd(placementId: placementId)
99
+ }
100
+
101
+ func createRewardedAd(
102
+ placementId: String
103
+ ) throws -> any HybridPangleRewardedAdSpec {
104
+ try requireInitialized(placementId: placementId)
105
+ return HybridPangleRewardedAd(placementId: placementId)
106
+ }
107
+
108
+ private func requireInitialized(placementId: String) throws {
109
+ stateLock.lock()
110
+ let initialized = initializedAppId != nil
111
+ stateLock.unlock()
112
+
113
+ guard initialized else {
114
+ throw pangleError("Pangle SDK is not initialized")
115
+ }
116
+ guard !placementId.isEmpty else {
117
+ throw pangleError("Pangle placement ID is empty")
118
+ }
119
+ }
120
+ }
@@ -0,0 +1,110 @@
1
+ import Foundation
2
+ import NitroModules
3
+ import PAGAdSDK
4
+
5
+ final class HybridPangleInterstitialAd:
6
+ HybridPangleInterstitialAdSpec, @unchecked Sendable {
7
+ fileprivate let events = PangleAdEventEmitter()
8
+ private let placementId: String
9
+ fileprivate var ad: PAGLInterstitialAd?
10
+ fileprivate let showTimeout = PangleShowTimeoutController()
11
+ private lazy var delegate = PangleInterstitialDelegate(owner: self)
12
+
13
+ init(placementId: String) {
14
+ self.placementId = placementId
15
+ }
16
+
17
+ func load() throws {
18
+ DispatchQueue.main.async {
19
+ let request = PAGInterstitialRequest()
20
+ PAGLInterstitialAd.load(withSlotID: self.placementId, request: request) {
21
+ interstitialAd, error in
22
+ DispatchQueue.main.async {
23
+ guard let interstitialAd else {
24
+ self.events.emit(.error, error: error ?? pangleError("Pangle interstitial ad failed to load"))
25
+ return
26
+ }
27
+ interstitialAd.delegate = self.delegate
28
+ self.ad = interstitialAd
29
+ self.events.emit(.loaded)
30
+ }
31
+ }
32
+ }
33
+ }
34
+
35
+ func show() throws -> Promise<Void> {
36
+ Promise.async {
37
+ do {
38
+ let viewController = try await currentViewController()
39
+ try await withCheckedThrowingContinuation {
40
+ (continuation: CheckedContinuation<Void, Error>) in
41
+ DispatchQueue.main.async {
42
+ guard let ad = self.ad else {
43
+ continuation.resume(
44
+ throwing: pangleError("Pangle interstitial ad is not ready")
45
+ )
46
+ return
47
+ }
48
+ self.showTimeout.arm { [weak self] in
49
+ guard let self else { return }
50
+ self.ad = nil
51
+ self.events.emit(.error, payload: PangleAdEventPayload(
52
+ errorCode: nil,
53
+ errorMessage: "Pangle ad show() timed out without a response from the SDK"
54
+ ))
55
+ }
56
+ ad.present(fromRootViewController: viewController)
57
+ continuation.resume()
58
+ }
59
+ }
60
+ } catch {
61
+ self.events.emit(.error, error: error)
62
+ throw error
63
+ }
64
+ }
65
+ }
66
+
67
+ func addAdEventListener(
68
+ eventType: PangleAdEventType,
69
+ listener: @escaping (PangleAdEventPayload?) -> Void
70
+ ) throws -> Double {
71
+ events.add(eventType: eventType, listener: listener)
72
+ }
73
+
74
+ func removeAdEventListener(subscriptionId: Double) throws {
75
+ events.remove(subscriptionId: subscriptionId)
76
+ }
77
+
78
+ func removeAllListeners() throws {
79
+ events.removeAll()
80
+ }
81
+ }
82
+
83
+ private final class PangleInterstitialDelegate: NSObject, PAGLInterstitialAdDelegate {
84
+ private weak var owner: HybridPangleInterstitialAd?
85
+
86
+ init(owner: HybridPangleInterstitialAd) {
87
+ self.owner = owner
88
+ }
89
+
90
+ func adDidShow(_ ad: any PAGAdProtocol) {
91
+ owner?.showTimeout.disarm()
92
+ owner?.events.emit(.opened)
93
+ }
94
+
95
+ func adDidClick(_ ad: any PAGAdProtocol) {
96
+ owner?.events.emit(.clicked)
97
+ }
98
+
99
+ func adDidDismiss(_ ad: any PAGAdProtocol) {
100
+ owner?.showTimeout.disarm()
101
+ owner?.ad = nil
102
+ owner?.events.emit(.closed)
103
+ }
104
+
105
+ func adDidShowFail(_ ad: any PAGAdProtocol, error: Error) {
106
+ owner?.showTimeout.disarm()
107
+ owner?.ad = nil
108
+ owner?.events.emit(.error, error: error)
109
+ }
110
+ }
@@ -0,0 +1,143 @@
1
+ import Foundation
2
+ import NitroModules
3
+ import PAGAdSDK
4
+
5
+ final class HybridPangleRewardedAd:
6
+ HybridPangleRewardedAdSpec, @unchecked Sendable {
7
+ fileprivate let events = PangleAdEventEmitter()
8
+ private let placementId: String
9
+ fileprivate var ad: PAGRewardedAd?
10
+ fileprivate let showTimeout = PangleShowTimeoutController()
11
+ private lazy var delegate = PangleRewardedDelegate(owner: self)
12
+
13
+ init(placementId: String) {
14
+ self.placementId = placementId
15
+ }
16
+
17
+ func load(verification: PangleAdVerificationOptions?) throws {
18
+ DispatchQueue.main.async {
19
+ // Pangle's S2S reward callback fills `user_id` from this SDK-wide
20
+ // setting, not from PAGRewardedRequest.extraInfo — without it Pangle
21
+ // reports the callback with its own placeholder user id.
22
+ if let userId = verification?.userId, !userId.isEmpty {
23
+ PAGConfig.share().userDataString = userId
24
+ }
25
+ let request = Self.makeRewardedRequest(verification: verification)
26
+ PAGRewardedAd.load(withSlotID: self.placementId, request: request) {
27
+ rewardedAd, error in
28
+ DispatchQueue.main.async {
29
+ guard let rewardedAd else {
30
+ self.events.emit(.error, error: error ?? pangleError("Pangle rewarded ad failed to load"))
31
+ return
32
+ }
33
+ rewardedAd.delegate = self.delegate
34
+ self.ad = rewardedAd
35
+ self.events.emit(.rewardedLoaded)
36
+ }
37
+ }
38
+ }
39
+ }
40
+
41
+ private static func makeRewardedRequest(
42
+ verification: PangleAdVerificationOptions?
43
+ ) -> PAGRewardedRequest {
44
+ let request = PAGRewardedRequest()
45
+ let extraInfo = extraInfo(from: verification)
46
+ if !extraInfo.isEmpty {
47
+ request.extraInfo = extraInfo
48
+ }
49
+ return request
50
+ }
51
+
52
+ private static func extraInfo(from verification: PangleAdVerificationOptions?) -> [String: String] {
53
+ var extraInfo: [String: String] = [:]
54
+ if let userId = verification?.userId, !userId.isEmpty {
55
+ extraInfo["userId"] = userId
56
+ }
57
+ if let customData = verification?.customData, !customData.isEmpty {
58
+ extraInfo["customData"] = customData
59
+ }
60
+ return extraInfo
61
+ }
62
+
63
+ func show() throws -> Promise<Void> {
64
+ Promise.async {
65
+ do {
66
+ let viewController = try await currentViewController()
67
+ try await withCheckedThrowingContinuation {
68
+ (continuation: CheckedContinuation<Void, Error>) in
69
+ DispatchQueue.main.async {
70
+ guard let ad = self.ad else {
71
+ continuation.resume(
72
+ throwing: pangleError("Pangle rewarded ad is not ready")
73
+ )
74
+ return
75
+ }
76
+ self.showTimeout.arm { [weak self] in
77
+ guard let self else { return }
78
+ self.ad = nil
79
+ self.events.emit(.error, payload: PangleAdEventPayload(
80
+ errorCode: nil,
81
+ errorMessage: "Pangle ad show() timed out without a response from the SDK"
82
+ ))
83
+ }
84
+ ad.present(fromRootViewController: viewController)
85
+ continuation.resume()
86
+ }
87
+ }
88
+ } catch {
89
+ self.events.emit(.error, error: error)
90
+ throw error
91
+ }
92
+ }
93
+ }
94
+
95
+ func addAdEventListener(
96
+ eventType: PangleAdEventType,
97
+ listener: @escaping (PangleAdEventPayload?) -> Void
98
+ ) throws -> Double {
99
+ events.add(eventType: eventType, listener: listener)
100
+ }
101
+
102
+ func removeAdEventListener(subscriptionId: Double) throws {
103
+ events.remove(subscriptionId: subscriptionId)
104
+ }
105
+
106
+ func removeAllListeners() throws {
107
+ events.removeAll()
108
+ }
109
+ }
110
+
111
+ private final class PangleRewardedDelegate: NSObject, PAGRewardedAdDelegate {
112
+ private weak var owner: HybridPangleRewardedAd?
113
+
114
+ init(owner: HybridPangleRewardedAd) {
115
+ self.owner = owner
116
+ }
117
+
118
+ func adDidShow(_ ad: any PAGAdProtocol) {
119
+ owner?.showTimeout.disarm()
120
+ owner?.events.emit(.opened)
121
+ }
122
+
123
+ func adDidClick(_ ad: any PAGAdProtocol) {
124
+ owner?.events.emit(.clicked)
125
+ }
126
+
127
+ func adDidDismiss(_ ad: any PAGAdProtocol) {
128
+ owner?.showTimeout.disarm()
129
+ owner?.ad = nil
130
+ owner?.events.emit(.closed)
131
+ }
132
+
133
+ func rewardedAd(_ rewardedAd: PAGRewardedAd, userDidEarnReward rewardModel: PAGRewardModel) {
134
+ owner?.showTimeout.disarm()
135
+ owner?.events.emit(.rewardedEarnedReward)
136
+ }
137
+
138
+ func adDidShowFail(_ ad: any PAGAdProtocol, error: Error) {
139
+ owner?.showTimeout.disarm()
140
+ owner?.ad = nil
141
+ owner?.events.emit(.error, error: error)
142
+ }
143
+ }