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.
- package/LICENSE +20 -0
- package/NitroPangleAds.podspec +26 -0
- package/README.md +80 -0
- package/android/CMakeLists.txt +20 -0
- package/android/build.gradle +129 -0
- package/android/consumer-rules.pro +2 -0
- package/android/fix-prefab.gradle +46 -0
- package/android/gradle.properties +5 -0
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/cpp/cpp-adapter.cpp +7 -0
- package/android/src/main/java/com/margelo/nitro/pangleads/HybridPangleAds.kt +123 -0
- package/android/src/main/java/com/margelo/nitro/pangleads/HybridPangleInterstitialAd.kt +106 -0
- package/android/src/main/java/com/margelo/nitro/pangleads/HybridPangleRewardedAd.kt +139 -0
- package/android/src/main/java/com/margelo/nitro/pangleads/NitroPangleAdsPackage.java +27 -0
- package/android/src/main/java/com/margelo/nitro/pangleads/PangleAdEventEmitter.kt +41 -0
- package/android/src/main/java/com/margelo/nitro/pangleads/PangleAndroidUtils.kt +32 -0
- package/ios/HybridPangleAds.swift +120 -0
- package/ios/HybridPangleInterstitialAd.swift +110 -0
- package/ios/HybridPangleRewardedAd.swift +143 -0
- package/ios/PangleSupport.swift +129 -0
- package/lib/module/index.js +172 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/index.test.js +201 -0
- package/lib/module/index.test.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/specs/PangleAds.nitro.js +4 -0
- package/lib/module/specs/PangleAds.nitro.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/index.d.ts +68 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/index.test.d.ts +2 -0
- package/lib/typescript/src/index.test.d.ts.map +1 -0
- package/lib/typescript/src/specs/PangleAds.nitro.d.ts +46 -0
- package/lib/typescript/src/specs/PangleAds.nitro.d.ts.map +1 -0
- package/nitro.json +18 -0
- package/nitrogen/generated/android/NitroPangleAds+autolinking.cmake +85 -0
- package/nitrogen/generated/android/NitroPangleAds+autolinking.gradle +27 -0
- package/nitrogen/generated/android/NitroPangleAdsOnLoad.cpp +50 -0
- package/nitrogen/generated/android/NitroPangleAdsOnLoad.hpp +25 -0
- package/nitrogen/generated/android/c++/JFunc_void_std__optional_PangleAdEventPayload_.hpp +78 -0
- package/nitrogen/generated/android/c++/JHybridPangleAdsSpec.cpp +88 -0
- package/nitrogen/generated/android/c++/JHybridPangleAdsSpec.hpp +69 -0
- package/nitrogen/generated/android/c++/JHybridPangleInterstitialAdSpec.cpp +85 -0
- package/nitrogen/generated/android/c++/JHybridPangleInterstitialAdSpec.hpp +68 -0
- package/nitrogen/generated/android/c++/JHybridPangleRewardedAdSpec.cpp +89 -0
- package/nitrogen/generated/android/c++/JHybridPangleRewardedAdSpec.hpp +68 -0
- package/nitrogen/generated/android/c++/JPangleAdEventPayload.hpp +58 -0
- package/nitrogen/generated/android/c++/JPangleAdEventType.hpp +74 -0
- package/nitrogen/generated/android/c++/JPangleAdVerificationOptions.hpp +58 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/pangleads/Func_void_std__optional_PangleAdEventPayload_.kt +81 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/pangleads/HybridPangleAdsSpec.kt +72 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/pangleads/HybridPangleInterstitialAdSpec.kt +73 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/pangleads/HybridPangleRewardedAdSpec.kt +73 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/pangleads/NitroPangleAdsOnLoad.kt +35 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/pangleads/PangleAdEventPayload.kt +32 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/pangleads/PangleAdEventType.kt +26 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/pangleads/PangleAdVerificationOptions.kt +32 -0
- package/nitrogen/generated/ios/NitroPangleAds+autolinking.rb +60 -0
- package/nitrogen/generated/ios/NitroPangleAds-Swift-Cxx-Bridge.cpp +90 -0
- package/nitrogen/generated/ios/NitroPangleAds-Swift-Cxx-Bridge.hpp +270 -0
- package/nitrogen/generated/ios/NitroPangleAds-Swift-Cxx-Umbrella.hpp +66 -0
- package/nitrogen/generated/ios/NitroPangleAdsAutolinking.mm +33 -0
- package/nitrogen/generated/ios/NitroPangleAdsAutolinking.swift +25 -0
- package/nitrogen/generated/ios/c++/HybridPangleAdsSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridPangleAdsSpecSwift.hpp +114 -0
- package/nitrogen/generated/ios/c++/HybridPangleInterstitialAdSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridPangleInterstitialAdSpecSwift.hpp +107 -0
- package/nitrogen/generated/ios/c++/HybridPangleRewardedAdSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridPangleRewardedAdSpecSwift.hpp +110 -0
- package/nitrogen/generated/ios/swift/Func_void.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_std__optional_PangleAdEventPayload_.swift +54 -0
- package/nitrogen/generated/ios/swift/HybridPangleAdsSpec.swift +54 -0
- package/nitrogen/generated/ios/swift/HybridPangleAdsSpec_cxx.swift +193 -0
- package/nitrogen/generated/ios/swift/HybridPangleInterstitialAdSpec.swift +53 -0
- package/nitrogen/generated/ios/swift/HybridPangleInterstitialAdSpec_cxx.swift +186 -0
- package/nitrogen/generated/ios/swift/HybridPangleRewardedAdSpec.swift +53 -0
- package/nitrogen/generated/ios/swift/HybridPangleRewardedAdSpec_cxx.swift +193 -0
- package/nitrogen/generated/ios/swift/PangleAdEventPayload.swift +77 -0
- package/nitrogen/generated/ios/swift/PangleAdEventType.swift +60 -0
- package/nitrogen/generated/ios/swift/PangleAdVerificationOptions.swift +84 -0
- package/nitrogen/generated/shared/c++/HybridPangleAdsSpec.cpp +26 -0
- package/nitrogen/generated/shared/c++/HybridPangleAdsSpec.hpp +74 -0
- package/nitrogen/generated/shared/c++/HybridPangleInterstitialAdSpec.cpp +25 -0
- package/nitrogen/generated/shared/c++/HybridPangleInterstitialAdSpec.hpp +73 -0
- package/nitrogen/generated/shared/c++/HybridPangleRewardedAdSpec.cpp +25 -0
- package/nitrogen/generated/shared/c++/HybridPangleRewardedAdSpec.hpp +76 -0
- package/nitrogen/generated/shared/c++/PangleAdEventPayload.hpp +72 -0
- package/nitrogen/generated/shared/c++/PangleAdEventType.hpp +96 -0
- package/nitrogen/generated/shared/c++/PangleAdVerificationOptions.hpp +72 -0
- package/package.json +177 -0
- package/react-native.config.js +8 -0
- package/src/index.test.ts +255 -0
- package/src/index.ts +305 -0
- 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
|
+
}
|