arcadesindo-unityads-cordova 1.0.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/README.md ADDED
@@ -0,0 +1,38 @@
1
+ # arcadesindo-unityads-cordova
2
+
3
+ Standalone Unity Ads plugin for Cordova / Construct 3, built directly on the Unity LevelPlay SDK
4
+ with **only the Unity Ads network adapter installed** -- no other mediated demand sources, and
5
+ no dependency on any AdMob SDK or account. Ad requests never touch an AdMob pipeline, so this
6
+ plugin's revenue is fully isolated from AdMob account-level enforcement.
7
+
8
+ Companion Construct 3 addon: `Claude_UnityAds` (`.c3addon`).
9
+
10
+ ## Install
11
+
12
+ ```
13
+ cordova plugin add arcadesindo-unityads-cordova
14
+ ```
15
+
16
+ ## Formats supported
17
+
18
+ - Interstitial
19
+ - Rewarded (with `rewardFailed` synthesized when the ad closes without a reward being earned)
20
+ - Banner
21
+
22
+ ## Platforms
23
+
24
+ - Android (Kotlin, `com.ironsource.mediationsdk` / LevelPlay Android SDK)
25
+ - iOS (Swift, `IronSourceSDK` CocoaPod / LevelPlay iOS SDK)
26
+
27
+ ## Notes
28
+
29
+ - Requires a LevelPlay App Key per platform, from the LevelPlay dashboard (platform.ironsrc.com)
30
+ -- not the same as a plain Unity Ads Monetize Game ID.
31
+ - iOS build includes defensive `AVAudioSession` mitigations for the "game audio lost after
32
+ fullscreen ad" pattern (see `UAAudioSessionGuard.swift`). Best-effort, not a guarantee.
33
+ - Some native class/method names should be re-verified against the exact LevelPlay SDK version
34
+ pinned in `plugin.xml` before your first production build.
35
+
36
+ ## License
37
+
38
+ MIT
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "arcadesindo-unityads-cordova",
3
+ "version": "1.0.0",
4
+ "description": "Standalone Unity Ads plugin for Cordova, built on the Unity LevelPlay SDK with the Unity Ads network adapter only (no other mediated demand sources, fully isolated from any AdMob SDK/account in the same app).",
5
+ "cordova": {
6
+ "id": "arcadesindo-unityads-cordova",
7
+ "platforms": [
8
+ "android",
9
+ "ios"
10
+ ]
11
+ },
12
+ "keywords": [
13
+ "cordova",
14
+ "ecosystem:cordova",
15
+ "cordova-android",
16
+ "cordova-ios",
17
+ "unity",
18
+ "unityads",
19
+ "levelplay",
20
+ "ironsource",
21
+ "ads",
22
+ "monetization",
23
+ "interstitial",
24
+ "rewarded",
25
+ "banner"
26
+ ],
27
+ "author": "Arcadesindo Games",
28
+ "license": "MIT",
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "git+https://github.com/arcadesindo/arcadesindo-unityads-cordova.git"
32
+ },
33
+ "bugs": {
34
+ "url": "https://github.com/arcadesindo/arcadesindo-unityads-cordova/issues"
35
+ },
36
+ "engines": [
37
+ {
38
+ "name": "cordova",
39
+ "version": ">=9.0.0"
40
+ }
41
+ ],
42
+ "files": [
43
+ "src",
44
+ "www",
45
+ "plugin.xml",
46
+ "README.md"
47
+ ]
48
+ }
package/plugin.xml ADDED
@@ -0,0 +1,102 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <plugin xmlns="http://apache.org/cordova/ns/plugins/1.0"
3
+ id="arcadesindo-unityads-cordova"
4
+ version="1.0.0">
5
+
6
+ <name>ArcadesindoUnityAds</name>
7
+ <description>
8
+ Standalone Unity Ads plugin for Cordova/Construct 3, built on the Unity LevelPlay SDK
9
+ with the Unity Ads network adapter only (no other mediated demand sources, no AdMob
10
+ pipeline). Fully isolated from any AdMob SDK/account in the same app.
11
+ </description>
12
+ <license>MIT</license>
13
+ <keywords>cordova,unity,levelplay,ads,interstitial,rewarded,banner</keywords>
14
+
15
+ <js-module src="www/ArcadesindoUnityAds.js" name="ArcadesindoUnityAds">
16
+ <clobbers target="cordova.plugins.ArcadesindoUnityAds" />
17
+ </js-module>
18
+
19
+ <!-- ============================== ANDROID ============================== -->
20
+ <platform name="android">
21
+ <config-file target="config.xml" parent="/*">
22
+ <feature name="ArcadesindoUnityAds">
23
+ <param name="android-package" value="com.arcadesindo.unityads.ArcadesindoUnityAds" />
24
+ </feature>
25
+ </config-file>
26
+
27
+ <config-file target="AndroidManifest.xml" parent="/manifest">
28
+ <!-- Required since LevelPlay SDK 4.1+ auto-declares AD_ID, kept explicit for older adapters -->
29
+ <uses-permission android:name="com.google.android.gms.permission.AD_ID" />
30
+ </config-file>
31
+
32
+ <source-file src="src/android/ArcadesindoUnityAds.kt" target-dir="src/com/arcadesindo/unityads" />
33
+ <source-file src="src/android/UAInterstitial.kt" target-dir="src/com/arcadesindo/unityads" />
34
+ <source-file src="src/android/UARewarded.kt" target-dir="src/com/arcadesindo/unityads" />
35
+ <source-file src="src/android/UABanner.kt" target-dir="src/com/arcadesindo/unityads" />
36
+ <source-file src="src/android/UAConsent.kt" target-dir="src/com/arcadesindo/unityads" />
37
+
38
+ <!-- LevelPlay core SDK -->
39
+ <framework src="com.unity3d.ads-mediation:mediation-sdk:9.5.0" />
40
+ <!-- Unity Ads network adapter for LevelPlay -- ONLY adapter installed, by design -->
41
+ <framework src="com.unity3d.ads-mediation:unityads-adapter:5.8.1" />
42
+ <!-- Kotlin stdlib required by both LevelPlay core and Unity Ads network SDK -->
43
+ <framework src="org.jetbrains.kotlin:kotlin-stdlib:2.1.0" />
44
+
45
+ <config-file target="AndroidManifest.xml" parent="/manifest/application">
46
+ <!-- placeholder: android:networkSecurityConfig etc. handled by app itself if needed -->
47
+ </config-file>
48
+ </platform>
49
+
50
+ <!-- ================================ iOS ================================ -->
51
+ <platform name="ios">
52
+ <config-file target="config.xml" parent="/*">
53
+ <feature name="ArcadesindoUnityAds">
54
+ <param name="ios-package" value="ArcadesindoUnityAds" />
55
+ </feature>
56
+ </config-file>
57
+
58
+ <source-file src="src/ios/ArcadesindoUnityAds.swift" />
59
+ <source-file src="src/ios/UAInterstitial.swift" />
60
+ <source-file src="src/ios/UARewarded.swift" />
61
+ <source-file src="src/ios/UABanner.swift" />
62
+ <source-file src="src/ios/UAConsent.swift" />
63
+ <source-file src="src/ios/UAAudioSessionGuard.swift" />
64
+
65
+ <!-- LevelPlay core SDK (still literally "IronSourceSDK" upstream) -->
66
+ <podspec>
67
+ <config>
68
+ <source url="https://cdn.cocoapods.org/" />
69
+ </config>
70
+ <pods use-frameworks="true">
71
+ <pod name="IronSourceSDK" spec="9.5.0.0" />
72
+ <!-- Unity Ads network adapter for LevelPlay -- ONLY adapter installed, by design -->
73
+ <pod name="IronSourceUnityAdsAdapter" spec="~> 5.8.1" />
74
+ </pods>
75
+ </podspec>
76
+
77
+ <config-file target="*-Info.plist" parent="NSAppTransportSecurity">
78
+ <dict>
79
+ <key>NSAllowsArbitraryLoads</key>
80
+ <true/>
81
+ </dict>
82
+ </config-file>
83
+
84
+ <config-file target="*-Info.plist" parent="SKAdNetworkItems">
85
+ <array>
86
+ <dict>
87
+ <key>SKAdNetworkIdentifier</key>
88
+ <string>su67r6k2v3.skadnetwork</string>
89
+ </dict>
90
+ </array>
91
+ </config-file>
92
+
93
+ <!-- Required so background/foreground audio-session reset notifications are observed
94
+ from plugin init, per brief section 4 mitigation -->
95
+ <config-file target="*-Info.plist" parent="UIBackgroundModes">
96
+ <array>
97
+ <string>audio</string>
98
+ </array>
99
+ </config-file>
100
+ </platform>
101
+
102
+ </plugin>
@@ -0,0 +1,155 @@
1
+ package com.arcadesindo.unityads
2
+
3
+ import android.media.AudioManager
4
+ import android.os.SystemClock
5
+ import org.apache.cordova.CallbackContext
6
+ import org.apache.cordova.CordovaPlugin
7
+ import org.apache.cordova.PluginResult
8
+ import org.json.JSONObject
9
+ import com.ironsource.mediationsdk.IronSource
10
+ import com.ironsource.mediationsdk.IronSource.AD_UNIT
11
+ import com.ironsource.mediationsdk.logger.IronSourceError
12
+
13
+ /**
14
+ * Main Cordova bridge. Delegates per-format logic to UAInterstitial / UARewarded / UABanner
15
+ * so each ad format lives in its own file (mirrors the Claude_AdMob native plugin pattern).
16
+ *
17
+ * NOTE ON SDK SURFACE: this file targets the LevelPlay Android SDK (package com.ironsource.*,
18
+ * still literally "IronSource" under the hood -- see project notes). The exact class/method
19
+ * names below (IronSource.init, IronSource.loadInterstitial, listener interfaces, etc.) follow
20
+ * the long-documented IronSource Android API and should be re-verified against whichever
21
+ * mediation-sdk version is pinned in plugin.xml before first build, since LevelPlay has been
22
+ * migrating some APIs to newer LevelPlayInterstitialAd-style classes.
23
+ */
24
+ class ArcadesindoUnityAds : CordovaPlugin() {
25
+
26
+ companion object {
27
+ const val TAG = "ArcadesindoUnityAds"
28
+ }
29
+
30
+ /** Single persistent listener callback -- events are pushed to it with keepCallback=true. */
31
+ private var listenerCallback: CallbackContext? = null
32
+
33
+ override fun execute(
34
+ action: String,
35
+ args: org.json.JSONArray,
36
+ callbackContext: CallbackContext
37
+ ): Boolean {
38
+ when (action) {
39
+ "initialize" -> initialize(args.optJSONObject(0) ?: JSONObject(), callbackContext)
40
+ "setAdListener" -> {
41
+ listenerCallback = callbackContext
42
+ // Keep the JS promise pending forever; events pushed via emitEvent().
43
+ val result = PluginResult(PluginResult.Status.NO_RESULT)
44
+ result.keepCallback = true
45
+ callbackContext.sendPluginResult(result)
46
+ }
47
+
48
+ "loadInterstitial" -> UAInterstitial.load(callbackContext, ::emitEvent)
49
+ "showInterstitial" -> UAInterstitial.show(cordova.activity, callbackContext)
50
+ "isInterstitialReady" -> UAInterstitial.isReady(callbackContext)
51
+
52
+ "loadRewarded" -> UARewarded.load(callbackContext, ::emitEvent)
53
+ "showRewarded" -> UARewarded.show(cordova.activity, callbackContext)
54
+ "isRewardedReady" -> UARewarded.isReady(callbackContext)
55
+
56
+ "loadBanner" -> UABanner.load(
57
+ cordova.activity, args.optJSONObject(0) ?: JSONObject(), callbackContext, ::emitEvent
58
+ )
59
+ "showBanner" -> UABanner.show(callbackContext)
60
+ "hideBanner" -> UABanner.hide(callbackContext)
61
+ "destroyBanner" -> UABanner.destroy(callbackContext)
62
+
63
+ "setConsent" -> UAConsent.setConsent(args.optBoolean(0), callbackContext)
64
+ "setDoNotSell" -> UAConsent.setDoNotSell(args.optBoolean(0), callbackContext)
65
+ "setChildDirected" -> UAConsent.setChildDirected(args.optBoolean(0), callbackContext)
66
+
67
+ "getAudioDiagnostics" -> getAudioDiagnostics(callbackContext)
68
+
69
+ else -> return false
70
+ }
71
+ return true
72
+ }
73
+
74
+ // ---- init --------------------------------------------------------------------------
75
+
76
+ private fun initialize(options: JSONObject, callbackContext: CallbackContext) {
77
+ val appKey = options.optString("appKey", "")
78
+ if (appKey.isEmpty()) {
79
+ callbackContext.error("appKey is required")
80
+ return
81
+ }
82
+
83
+ val activity = cordova.activity
84
+
85
+ // Register per-format listeners once, before init, per LevelPlay docs guidance.
86
+ UAInterstitial.registerListener(::emitEvent)
87
+ UARewarded.registerListener(::emitEvent)
88
+ UABanner.registerListener(::emitEvent)
89
+
90
+ // NOTE: init signature/ad-unit selection to verify against pinned SDK version --
91
+ // some LevelPlay versions accept AD_UNIT varargs to scope which formats initialize.
92
+ IronSource.setISDemandOnlyInterstitialListener(null) // ensure demand-only mode is off
93
+ IronSource.init(
94
+ activity,
95
+ appKey,
96
+ AD_UNIT.INTERSTITIAL,
97
+ AD_UNIT.REWARDED_VIDEO,
98
+ AD_UNIT.BANNER
99
+ )
100
+
101
+ if (options.optBoolean("isTesting", false)) {
102
+ // Test-ad mode: verify current flag name against SDK version at build time.
103
+ IronSource.setMetaData("is_test_suite", "enable")
104
+ }
105
+
106
+ val userId = options.optString("userId", "")
107
+ if (userId.isNotEmpty()) {
108
+ IronSource.setDynamicUserId(userId)
109
+ }
110
+
111
+ callbackContext.success()
112
+ }
113
+
114
+ // ---- event fan-out -------------------------------------------------------------------
115
+
116
+ /**
117
+ * Pushes a single ad lifecycle event to the JS listener registered via setAdListener().
118
+ * format: "interstitial" | "rewarded" | "banner"
119
+ * event: "load" | "loadFailed" | "show" | "showFailed" | "click" | "dismiss"
120
+ * | "rewarded" | "rewardFailed"
121
+ */
122
+ fun emitEvent(format: String, event: String, data: JSONObject = JSONObject()) {
123
+ val payload = JSONObject()
124
+ payload.put("format", format)
125
+ payload.put("event", event)
126
+ payload.put("data", data)
127
+
128
+ val cb = listenerCallback ?: return
129
+ val result = PluginResult(PluginResult.Status.OK, payload)
130
+ result.keepCallback = true
131
+ cb.sendPluginResult(result)
132
+ }
133
+
134
+ // ---- lifecycle passthrough (LevelPlay recommends forwarding these) ---------------------
135
+
136
+ override fun onResume(multitasking: Boolean) {
137
+ super.onResume(multitasking)
138
+ IronSource.onResume(cordova.activity)
139
+ }
140
+
141
+ override fun onPause(multitasking: Boolean) {
142
+ super.onPause(multitasking)
143
+ IronSource.onPause(cordova.activity)
144
+ }
145
+
146
+ // ---- diagnostics (brief section 4: instrumentation for the AdMob audio-loss bug) ------
147
+
148
+ private fun getAudioDiagnostics(callbackContext: CallbackContext) {
149
+ val payload = JSONObject()
150
+ payload.put("elapsedRealtimeMs", SystemClock.elapsedRealtime())
151
+ val am = cordova.activity.getSystemService(android.content.Context.AUDIO_SERVICE) as AudioManager
152
+ payload.put("isMusicActive", am.isMusicActive)
153
+ callbackContext.success(payload)
154
+ }
155
+ }
@@ -0,0 +1,87 @@
1
+ package com.arcadesindo.unityads
2
+
3
+ import android.app.Activity
4
+ import android.view.Gravity
5
+ import android.widget.FrameLayout
6
+ import org.apache.cordova.CallbackContext
7
+ import org.json.JSONObject
8
+ import com.ironsource.mediationsdk.IronSource
9
+ import com.ironsource.mediationsdk.logger.IronSourceError
10
+ import com.ironsource.mediationsdk.sdk.BannerListener
11
+ import com.ironsource.mediationsdk.model.IronSourceBannerLayout
12
+ import com.ironsource.mediationsdk.ISBannerSize
13
+
14
+ object UABanner {
15
+
16
+ private var emit: ((String, String, JSONObject) -> Unit)? = null
17
+ private var bannerView: IronSourceBannerLayout? = null
18
+
19
+ fun registerListener(emitter: (String, String, JSONObject) -> Unit) {
20
+ emit = emitter
21
+ }
22
+
23
+ fun load(
24
+ activity: Activity,
25
+ options: JSONObject,
26
+ callbackContext: CallbackContext,
27
+ emitter: (String, String, JSONObject) -> Unit
28
+ ) {
29
+ emit = emitter
30
+ destroyInternal()
31
+
32
+ val banner = IronSource.createBanner(activity, ISBannerSize.BANNER)
33
+ bannerView = banner
34
+
35
+ banner.setBannerListener(object : BannerListener {
36
+ override fun onBannerAdLoaded() = emit?.invoke("banner", "load", JSONObject()) ?: Unit
37
+ override fun onBannerAdLoadFailed(error: IronSourceError) =
38
+ emit?.invoke("banner", "loadFailed", errorPayload(error)) ?: Unit
39
+ override fun onBannerAdClicked() = emit?.invoke("banner", "click", JSONObject()) ?: Unit
40
+ override fun onBannerAdScreenPresented() = Unit
41
+ override fun onBannerAdScreenDismissed() = Unit
42
+ override fun onBannerAdLeftApplication() = Unit
43
+ })
44
+
45
+ val position = options.optString("position", "bottom")
46
+ val gravity = if (position == "top") Gravity.TOP or Gravity.CENTER_HORIZONTAL
47
+ else Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL
48
+
49
+ val params = FrameLayout.LayoutParams(
50
+ FrameLayout.LayoutParams.WRAP_CONTENT,
51
+ FrameLayout.LayoutParams.WRAP_CONTENT,
52
+ gravity
53
+ )
54
+ activity.addContentView(banner, params)
55
+ banner.visibility = android.view.View.GONE
56
+
57
+ IronSource.loadBanner(banner)
58
+ callbackContext.success()
59
+ }
60
+
61
+ fun show(callbackContext: CallbackContext) {
62
+ bannerView?.visibility = android.view.View.VISIBLE
63
+ callbackContext.success()
64
+ }
65
+
66
+ fun hide(callbackContext: CallbackContext) {
67
+ bannerView?.visibility = android.view.View.GONE
68
+ callbackContext.success()
69
+ }
70
+
71
+ fun destroy(callbackContext: CallbackContext) {
72
+ destroyInternal()
73
+ callbackContext.success()
74
+ }
75
+
76
+ private fun destroyInternal() {
77
+ bannerView?.let { IronSource.destroyBanner(it) }
78
+ bannerView = null
79
+ }
80
+
81
+ private fun errorPayload(error: IronSourceError): JSONObject {
82
+ val o = JSONObject()
83
+ o.put("code", error.errorCode)
84
+ o.put("message", error.errorMessage)
85
+ return o
86
+ }
87
+ }
@@ -0,0 +1,30 @@
1
+ package com.arcadesindo.unityads
2
+
3
+ import org.apache.cordova.CallbackContext
4
+ import com.ironsource.mediationsdk.IronSource
5
+
6
+ /**
7
+ * GDPR / CCPA / COPPA regulation settings. LevelPlay's setConsent() fans the choice out to
8
+ * every mediated network that supports a consent API -- for us that's just the Unity Ads
9
+ * adapter, since it's the only adapter installed (see plugin.xml).
10
+ */
11
+ object UAConsent {
12
+
13
+ fun setConsent(consent: Boolean, callbackContext: CallbackContext) {
14
+ IronSource.setConsent(consent)
15
+ callbackContext.success()
16
+ }
17
+
18
+ fun setDoNotSell(value: Boolean, callbackContext: CallbackContext) {
19
+ IronSource.setMetaData("do_not_sell", if (value) "YES" else "NO")
20
+ callbackContext.success()
21
+ }
22
+
23
+ fun setChildDirected(value: Boolean, callbackContext: CallbackContext) {
24
+ // NOTE: verify exact flag/value semantics against SDK version + brief's earlier finding
25
+ // that "mixed"/"targeted" values were reported unreliable by some developers -- test
26
+ // thoroughly before relying on this for compliance.
27
+ IronSource.setMetaData("is_child_directed", if (value) "true" else "false")
28
+ callbackContext.success()
29
+ }
30
+ }
@@ -0,0 +1,80 @@
1
+ package com.arcadesindo.unityads
2
+
3
+ import android.app.Activity
4
+ import org.apache.cordova.CallbackContext
5
+ import org.json.JSONObject
6
+ import com.ironsource.mediationsdk.IronSource
7
+ import com.ironsource.mediationsdk.sdk.ISDemandOnlyInterstitialListener
8
+ import com.ironsource.mediationsdk.logger.IronSourceError
9
+
10
+ /** Interstitial ad format -- split into its own file per brief 3B convention. */
11
+ object UAInterstitial {
12
+
13
+ private var emit: ((String, String, JSONObject) -> Unit)? = null
14
+ private var ready = false
15
+
16
+ fun registerListener(emitter: (String, String, JSONObject) -> Unit) {
17
+ emit = emitter
18
+ // NOTE: verify listener interface name against pinned SDK version --
19
+ // newer LevelPlay versions favor per-instance LevelPlayInterstitialAdListener.
20
+ IronSource.setInterstitialListener(object : com.ironsource.mediationsdk.sdk.InterstitialListener {
21
+ override fun onInterstitialAdReady() {
22
+ ready = true
23
+ emit?.invoke("interstitial", "load", JSONObject())
24
+ }
25
+
26
+ override fun onInterstitialAdLoadFailed(error: IronSourceError) {
27
+ ready = false
28
+ emit?.invoke("interstitial", "loadFailed", errorPayload(error))
29
+ }
30
+
31
+ override fun onInterstitialAdShowSucceeded() {
32
+ emit?.invoke("interstitial", "show", JSONObject())
33
+ }
34
+
35
+ override fun onInterstitialAdShowFailed(error: IronSourceError) {
36
+ ready = false
37
+ emit?.invoke("interstitial", "showFailed", errorPayload(error))
38
+ }
39
+
40
+ override fun onInterstitialAdClicked() {
41
+ emit?.invoke("interstitial", "click", JSONObject())
42
+ }
43
+
44
+ override fun onInterstitialAdOpened() { /* no-op, mirrors "show" */ }
45
+
46
+ override fun onInterstitialAdClosed() {
47
+ ready = false
48
+ emit?.invoke("interstitial", "dismiss", JSONObject())
49
+ // Proactively load the next one so isReady stays useful.
50
+ IronSource.loadInterstitial()
51
+ }
52
+ })
53
+ }
54
+
55
+ fun load(callbackContext: CallbackContext, emitter: (String, String, JSONObject) -> Unit) {
56
+ emit = emitter
57
+ IronSource.loadInterstitial()
58
+ callbackContext.success()
59
+ }
60
+
61
+ fun show(activity: Activity, callbackContext: CallbackContext) {
62
+ if (!IronSource.isInterstitialReady()) {
63
+ callbackContext.error("Interstitial not ready")
64
+ return
65
+ }
66
+ IronSource.showInterstitial()
67
+ callbackContext.success()
68
+ }
69
+
70
+ fun isReady(callbackContext: CallbackContext) {
71
+ callbackContext.success(if (IronSource.isInterstitialReady()) 1 else 0)
72
+ }
73
+
74
+ private fun errorPayload(error: IronSourceError): JSONObject {
75
+ val o = JSONObject()
76
+ o.put("code", error.errorCode)
77
+ o.put("message", error.errorMessage)
78
+ return o
79
+ }
80
+ }
@@ -0,0 +1,100 @@
1
+ package com.arcadesindo.unityads
2
+
3
+ import android.app.Activity
4
+ import org.apache.cordova.CallbackContext
5
+ import org.json.JSONObject
6
+ import com.ironsource.mediationsdk.IronSource
7
+ import com.ironsource.mediationsdk.logger.IronSourceError
8
+ import com.ironsource.mediationsdk.model.Placement
9
+
10
+ /**
11
+ * Rewarded ad format. Synthesizes a "rewardFailed" event when the ad is dismissed WITHOUT a
12
+ * prior "rewarded" event having fired -- exactly the pattern called out in brief 3B so we don't
13
+ * have to retrofit it later like Claude_AdMob did.
14
+ */
15
+ object UARewarded {
16
+
17
+ private var emit: ((String, String, JSONObject) -> Unit)? = null
18
+ private var earnedThisSession = false
19
+
20
+ fun registerListener(emitter: (String, String, JSONObject) -> Unit) {
21
+ emit = emitter
22
+ // NOTE: verify against pinned SDK version -- newer LevelPlay favors
23
+ // LevelPlayRewardedAdListener with an AdInfo-based per-instance API.
24
+ IronSource.setLevelPlayRewardedVideoListener(object : com.ironsource.mediationsdk.sdk.LevelPlayRewardedVideoListener {
25
+ override fun onAdAvailable(adInfo: com.ironsource.mediationsdk.adunit.adapter.utility.AdInfo) {
26
+ emit?.invoke("rewarded", "load", JSONObject())
27
+ }
28
+
29
+ override fun onAdUnavailable() {
30
+ emit?.invoke("rewarded", "loadFailed", JSONObject())
31
+ }
32
+
33
+ override fun onAdOpened(adInfo: com.ironsource.mediationsdk.adunit.adapter.utility.AdInfo) {
34
+ earnedThisSession = false
35
+ emit?.invoke("rewarded", "show", JSONObject())
36
+ }
37
+
38
+ override fun onAdShowFailed(
39
+ error: IronSourceError,
40
+ adInfo: com.ironsource.mediationsdk.adunit.adapter.utility.AdInfo
41
+ ) {
42
+ emit?.invoke("rewarded", "showFailed", errorPayload(error))
43
+ }
44
+
45
+ override fun onAdRewarded(
46
+ placement: Placement,
47
+ adInfo: com.ironsource.mediationsdk.adunit.adapter.utility.AdInfo
48
+ ) {
49
+ earnedThisSession = true
50
+ val o = JSONObject()
51
+ o.put("placementName", placement.placementName)
52
+ o.put("rewardAmount", placement.rewardAmount)
53
+ o.put("rewardName", placement.rewardName)
54
+ emit?.invoke("rewarded", "rewarded", o)
55
+ }
56
+
57
+ override fun onAdClicked(
58
+ placement: Placement,
59
+ adInfo: com.ironsource.mediationsdk.adunit.adapter.utility.AdInfo
60
+ ) {
61
+ emit?.invoke("rewarded", "click", JSONObject())
62
+ }
63
+
64
+ override fun onAdClosed(adInfo: com.ironsource.mediationsdk.adunit.adapter.utility.AdInfo) {
65
+ if (!earnedThisSession) {
66
+ // Synthesized -- player closed the ad before earning a reward.
67
+ emit?.invoke("rewarded", "rewardFailed", JSONObject())
68
+ }
69
+ emit?.invoke("rewarded", "dismiss", JSONObject())
70
+ }
71
+ })
72
+ }
73
+
74
+ fun load(callbackContext: CallbackContext, emitter: (String, String, JSONObject) -> Unit) {
75
+ emit = emitter
76
+ // Rewarded video in classic IronSource API auto-loads; nothing to trigger explicitly,
77
+ // but we still ack the JS call so the promise resolves.
78
+ callbackContext.success()
79
+ }
80
+
81
+ fun show(activity: Activity, callbackContext: CallbackContext) {
82
+ if (!IronSource.isRewardedVideoAvailable()) {
83
+ callbackContext.error("Rewarded not ready")
84
+ return
85
+ }
86
+ IronSource.showRewardedVideo()
87
+ callbackContext.success()
88
+ }
89
+
90
+ fun isReady(callbackContext: CallbackContext) {
91
+ callbackContext.success(if (IronSource.isRewardedVideoAvailable()) 1 else 0)
92
+ }
93
+
94
+ private fun errorPayload(error: IronSourceError): JSONObject {
95
+ val o = JSONObject()
96
+ o.put("code", error.errorCode)
97
+ o.put("message", error.errorMessage)
98
+ return o
99
+ }
100
+ }
@@ -0,0 +1,145 @@
1
+ import Foundation
2
+
3
+ /**
4
+ Main Cordova bridge (iOS). Delegates per-format logic to UAInterstitial / UARewarded / UABanner,
5
+ mirroring the Android split and the Claude_AdMob native plugin convention (brief 3B).
6
+
7
+ NOTE ON SDK SURFACE: targets the LevelPlay iOS SDK (CocoaPod `IronSourceSDK`, header
8
+ `IronSource/IronSource.h` -- still literally IronSource under the hood, see project notes).
9
+ The exact class/method names below follow the long-documented classic `IronSource` static API
10
+ (IronSource.loadInterstitial(), IronSource.showInterstitial(with:), delegate protocols) for
11
+ consistency with the Android side. Re-verify against the exact IronSourceSDK version pinned in
12
+ plugin.xml before first build -- newer LevelPlay releases are migrating some formats to
13
+ `LPMInterstitialAd`-style per-instance classes.
14
+ */
15
+ @objc(ArcadesindoUnityAds)
16
+ class ArcadesindoUnityAds: CDVPlugin {
17
+
18
+ /// Command callback ID for the single persistent event listener.
19
+ var listenerCallbackId: String?
20
+
21
+ override func pluginInitialize() {
22
+ super.pluginInitialize()
23
+ // Audio-session mitigations must be wired up at plugin init, not lazily on first ad --
24
+ // see brief section 4.
25
+ UAAudioSessionGuard.shared.install()
26
+ }
27
+
28
+ // MARK: - lifecycle
29
+
30
+ @objc(initialize:)
31
+ func initialize(_ command: CDVInvokedUrlCommand) {
32
+ guard let options = command.argument(at: 0) as? [String: Any],
33
+ let appKey = options["appKey"] as? String, !appKey.isEmpty else {
34
+ send(command, error: "appKey is required")
35
+ return
36
+ }
37
+
38
+ UAInterstitial.shared.registerListener(emit: emitEvent)
39
+ UARewarded.shared.registerListener(emit: emitEvent)
40
+ UABanner.shared.registerListener(emit: emitEvent)
41
+
42
+ // NOTE: verify init entry point against pinned SDK version -- some releases expose
43
+ // `IronSource.initWithAppKey(_:adUnits:)`, others `LevelPlay.initWith(_:completion:)`
44
+ // via LPMInitRequestBuilder. Using the classic entry point here for parity with Android.
45
+ IronSource.initWithAppKey(appKey, adUnits: [IronSourceAdUnitsAdapter.interstitial, .rewardedVideo, .banner])
46
+
47
+ if (options["isTesting"] as? Bool) == true {
48
+ IronSource.setMetaDataWithKey("is_test_suite", value: "enable")
49
+ }
50
+ if let userId = options["userId"] as? String, !userId.isEmpty {
51
+ IronSource.setDynamicUserId(userId)
52
+ }
53
+
54
+ send(command, success: true)
55
+ }
56
+
57
+ @objc(setAdListener:)
58
+ func setAdListener(_ command: CDVInvokedUrlCommand) {
59
+ listenerCallbackId = command.callbackId
60
+ let result = CDVPluginResult(status: CDVCommandStatus_NO_RESULT)
61
+ result?.setKeepCallbackAs(true)
62
+ commandDelegate.send(result, callbackId: command.callbackId)
63
+ }
64
+
65
+ // MARK: - dispatch helpers used by format files
66
+
67
+ func emitEvent(format: String, event: String, data: [String: Any] = [:]) {
68
+ guard let callbackId = listenerCallbackId else { return }
69
+ let payload: [String: Any] = ["format": format, "event": event, "data": data]
70
+ let result = CDVPluginResult(status: CDVCommandStatus_OK, messageAs: payload)
71
+ result?.setKeepCallbackAs(true)
72
+ commandDelegate.send(result, callbackId: callbackId)
73
+ }
74
+
75
+ func send(_ command: CDVInvokedUrlCommand, success: Bool = true, payload: Any? = nil, error: String? = nil) {
76
+ let result: CDVPluginResult
77
+ if let error = error {
78
+ result = CDVPluginResult(status: CDVCommandStatus_ERROR, messageAs: error)
79
+ } else if let payload = payload {
80
+ result = CDVPluginResult(status: CDVCommandStatus_OK, messageAs: payload)
81
+ } else {
82
+ result = CDVPluginResult(status: success ? CDVCommandStatus_OK : CDVCommandStatus_ERROR)
83
+ }
84
+ commandDelegate.send(result, callbackId: command.callbackId)
85
+ }
86
+
87
+ // MARK: - format entry points (thin forwards, actual logic lives in UAInterstitial etc.)
88
+
89
+ @objc(loadInterstitial:)
90
+ func loadInterstitial(_ command: CDVInvokedUrlCommand) { UAInterstitial.shared.load(command, plugin: self) }
91
+
92
+ @objc(showInterstitial:)
93
+ func showInterstitial(_ command: CDVInvokedUrlCommand) { UAInterstitial.shared.show(command, plugin: self) }
94
+
95
+ @objc(isInterstitialReady:)
96
+ func isInterstitialReady(_ command: CDVInvokedUrlCommand) { UAInterstitial.shared.isReady(command, plugin: self) }
97
+
98
+ @objc(loadRewarded:)
99
+ func loadRewarded(_ command: CDVInvokedUrlCommand) { UARewarded.shared.load(command, plugin: self) }
100
+
101
+ @objc(showRewarded:)
102
+ func showRewarded(_ command: CDVInvokedUrlCommand) { UARewarded.shared.show(command, plugin: self) }
103
+
104
+ @objc(isRewardedReady:)
105
+ func isRewardedReady(_ command: CDVInvokedUrlCommand) { UARewarded.shared.isReady(command, plugin: self) }
106
+
107
+ @objc(loadBanner:)
108
+ func loadBanner(_ command: CDVInvokedUrlCommand) { UABanner.shared.load(command, plugin: self) }
109
+
110
+ @objc(showBanner:)
111
+ func showBanner(_ command: CDVInvokedUrlCommand) { UABanner.shared.show(command, plugin: self) }
112
+
113
+ @objc(hideBanner:)
114
+ func hideBanner(_ command: CDVInvokedUrlCommand) { UABanner.shared.hide(command, plugin: self) }
115
+
116
+ @objc(destroyBanner:)
117
+ func destroyBanner(_ command: CDVInvokedUrlCommand) { UABanner.shared.destroy(command, plugin: self) }
118
+
119
+ @objc(setConsent:)
120
+ func setConsent(_ command: CDVInvokedUrlCommand) {
121
+ let consent = (command.argument(at: 0) as? Bool) ?? false
122
+ IronSource.setConsent(consent)
123
+ send(command, success: true)
124
+ }
125
+
126
+ @objc(setDoNotSell:)
127
+ func setDoNotSell(_ command: CDVInvokedUrlCommand) {
128
+ let value = (command.argument(at: 0) as? Bool) ?? false
129
+ IronSource.setMetaDataWithKey("do_not_sell", value: value ? "YES" : "NO")
130
+ send(command, success: true)
131
+ }
132
+
133
+ @objc(setChildDirected:)
134
+ func setChildDirected(_ command: CDVInvokedUrlCommand) {
135
+ let value = (command.argument(at: 0) as? Bool) ?? false
136
+ // NOTE: same reliability caveat as Android -- verify against current SDK docs/testing.
137
+ IronSource.setMetaDataWithKey("is_child_directed", value: value ? "true" : "false")
138
+ send(command, success: true)
139
+ }
140
+
141
+ @objc(getAudioDiagnostics:)
142
+ func getAudioDiagnostics(_ command: CDVInvokedUrlCommand) {
143
+ send(command, payload: UAAudioSessionGuard.shared.diagnosticsSnapshot())
144
+ }
145
+ }
@@ -0,0 +1,83 @@
1
+ import Foundation
2
+ import AVFoundation
3
+
4
+ /**
5
+ Defensive mitigations for the "game audio lost after fullscreen ad on iOS" pattern that hit
6
+ Claude_AdMob (root cause still unconfirmed at the time this was written). Unity Ads/LevelPlay
7
+ also presents a native fullscreen ad view over the WKWebView, so the same failure mode is
8
+ plausible here too. These are best-effort preventions, NOT a guaranteed fix -- see brief
9
+ section 4, point 5.
10
+
11
+ Applied from plugin init (not lazily on first ad), per brief instruction.
12
+ */
13
+ final class UAAudioSessionGuard {
14
+
15
+ static let shared = UAAudioSessionGuard()
16
+ private init() {}
17
+
18
+ private var installed = false
19
+ private var lastKnownCurrentTime: TimeInterval = 0
20
+
21
+ func install() {
22
+ guard !installed else { return }
23
+ installed = true
24
+
25
+ configureSessionForAppManagedPlayback()
26
+
27
+ // 1. Observe media-services-reset -- iOS can silently reset the whole audio subsystem;
28
+ // without this, session category/activation state is lost with no other signal.
29
+ NotificationCenter.default.addObserver(
30
+ self,
31
+ selector: #selector(handleMediaServicesReset),
32
+ name: AVAudioSession.mediaServicesWereResetNotification,
33
+ object: nil
34
+ )
35
+
36
+ // NOTE: if the Unity Ads / LevelPlay SDK exposes an equivalent of AdMob's
37
+ // `isAudioSessionApplicationManaged`, set it here so the SDK does not fight the app
38
+ // for control of AVAudioSession. As of this writing no such flag was confirmed in
39
+ // LevelPlay's public iOS docs -- verify against the current SDK reference and, if
40
+ // found, wire it in before first release.
41
+ }
42
+
43
+ private func configureSessionForAppManagedPlayback() {
44
+ let session = AVAudioSession.sharedInstance()
45
+ do {
46
+ // .playback (not .ambient) -- .ambient was a contributing factor in the
47
+ // Claude_AdMob investigation.
48
+ try session.setCategory(.playback, options: [.mixWithOthers])
49
+ try session.setActive(true)
50
+ } catch {
51
+ NSLog("[ArcadesindoUnityAds] AVAudioSession configure failed: \(error)")
52
+ }
53
+ }
54
+
55
+ @objc private func handleMediaServicesReset() {
56
+ NSLog("[ArcadesindoUnityAds] AVAudioSession media services were reset -- reconfiguring")
57
+ configureSessionForAppManagedPlayback()
58
+ }
59
+
60
+ /// Call from the ad's dismiss delegate callback (mirrors AdMob's
61
+ /// `adDidDismissFullScreenContent` pattern) to reassert session state after the SDK's own
62
+ /// fullscreen ad view tears down.
63
+ func reactivateAfterAdDismiss() {
64
+ configureSessionForAppManagedPlayback()
65
+ }
66
+
67
+ // MARK: - diagnostics (brief 4.5: instrumentation so a recurrence is fast to investigate)
68
+
69
+ /// currentTime delta polling snapshot, same technique used to diagnose the Claude_AdMob bug.
70
+ func diagnosticsSnapshot() -> [String: Any] {
71
+ let session = AVAudioSession.sharedInstance()
72
+ let now = ProcessInfo.processInfo.systemUptime
73
+ let delta = now - lastKnownCurrentTime
74
+ lastKnownCurrentTime = now
75
+ return [
76
+ "systemUptime": now,
77
+ "deltaSinceLastSnapshot": delta,
78
+ "sessionCategory": session.category.rawValue,
79
+ "isOtherAudioPlaying": session.isOtherAudioPlaying,
80
+ "outputVolume": session.outputVolume
81
+ ]
82
+ }
83
+ }
@@ -0,0 +1,72 @@
1
+ import Foundation
2
+
3
+ final class UABanner: NSObject {
4
+
5
+ static let shared = UABanner()
6
+ private override init() {}
7
+
8
+ private var emit: ((String, String, [String: Any]) -> Void)?
9
+ private var bannerView: ISBannerView?
10
+
11
+ func registerListener(emit: @escaping (String, String, [String: Any]) -> Void) {
12
+ self.emit = emit
13
+ }
14
+
15
+ func load(_ command: CDVInvokedUrlCommand, plugin: ArcadesindoUnityAds) {
16
+ let options = command.argument(at: 0) as? [String: Any] ?? [:]
17
+ let position = options["position"] as? String ?? "bottom"
18
+
19
+ destroyInternal()
20
+
21
+ let banner = IronSource.createBanner(with: .BANNER, viewController: plugin.viewController)
22
+ bannerView = banner
23
+ banner?.delegate = self
24
+ banner?.isHidden = true
25
+
26
+ if let banner = banner, let view = plugin.viewController.view {
27
+ view.addSubview(banner)
28
+ banner.translatesAutoresizingMaskIntoConstraints = false
29
+ NSLayoutConstraint.activate([
30
+ banner.centerXAnchor.constraint(equalTo: view.centerXAnchor),
31
+ position == "top"
32
+ ? banner.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor)
33
+ : banner.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor)
34
+ ])
35
+ }
36
+
37
+ IronSource.loadBanner(with: banner)
38
+ plugin.send(command, success: true)
39
+ }
40
+
41
+ func show(_ command: CDVInvokedUrlCommand, plugin: ArcadesindoUnityAds) {
42
+ bannerView?.isHidden = false
43
+ plugin.send(command, success: true)
44
+ }
45
+
46
+ func hide(_ command: CDVInvokedUrlCommand, plugin: ArcadesindoUnityAds) {
47
+ bannerView?.isHidden = true
48
+ plugin.send(command, success: true)
49
+ }
50
+
51
+ func destroy(_ command: CDVInvokedUrlCommand, plugin: ArcadesindoUnityAds) {
52
+ destroyInternal()
53
+ plugin.send(command, success: true)
54
+ }
55
+
56
+ private func destroyInternal() {
57
+ bannerView?.removeFromSuperview()
58
+ bannerView = nil
59
+ }
60
+ }
61
+
62
+ extension UABanner: ISBannerDelegate {
63
+ func bannerDidLoad(_ bannerView: ISBannerView!) { emit?("banner", "load", [:]) }
64
+ func bannerDidFailToLoadWithError(_ error: Error!) {
65
+ let nsError = error as NSError
66
+ emit?("banner", "loadFailed", ["code": nsError.code, "message": nsError.localizedDescription])
67
+ }
68
+ func didClickBanner() { emit?("banner", "click", [:]) }
69
+ func bannerWillPresentScreen() {}
70
+ func bannerDidDismissScreen() {}
71
+ func bannerWillLeaveApplication() {}
72
+ }
@@ -0,0 +1,7 @@
1
+ import Foundation
2
+
3
+ /// Placeholder file kept in sync with UAConsent.kt on Android. All three consent methods
4
+ /// (setConsent / setDoNotSell / setChildDirected) are currently implemented directly in
5
+ /// ArcadesindoUnityAds.swift for brevity -- moved here if the consent logic grows
6
+ /// (e.g. per-network granular consent map, TCF string passthrough).
7
+ enum UAConsent {}
@@ -0,0 +1,75 @@
1
+ import Foundation
2
+
3
+ /// Interstitial ad format -- split into its own file per brief 3B convention.
4
+ final class UAInterstitial: NSObject {
5
+
6
+ static let shared = UAInterstitial()
7
+ private override init() {}
8
+
9
+ private var emit: ((String, String, [String: Any]) -> Void)?
10
+ private var ready = false
11
+
12
+ func registerListener(emit: @escaping (String, String, [String: Any]) -> Void) {
13
+ self.emit = emit
14
+ // NOTE: verify delegate protocol name against pinned SDK version -- classic API is
15
+ // `IronSourceInterstitialDelegate` set via `IronSource.setInterstitialDelegate(self)`.
16
+ IronSource.setInterstitialDelegate(self)
17
+ }
18
+
19
+ func load(_ command: CDVInvokedUrlCommand, plugin: ArcadesindoUnityAds) {
20
+ IronSource.loadInterstitial()
21
+ plugin.send(command, success: true)
22
+ }
23
+
24
+ func show(_ command: CDVInvokedUrlCommand, plugin: ArcadesindoUnityAds) {
25
+ guard IronSource.hasInterstitial() else {
26
+ plugin.send(command, error: "Interstitial not ready")
27
+ return
28
+ }
29
+ IronSource.showInterstitial(with: plugin.viewController)
30
+ plugin.send(command, success: true)
31
+ }
32
+
33
+ func isReady(_ command: CDVInvokedUrlCommand, plugin: ArcadesindoUnityAds) {
34
+ plugin.send(command, payload: IronSource.hasInterstitial())
35
+ }
36
+ }
37
+
38
+ extension UAInterstitial: IronSourceInterstitialDelegate {
39
+
40
+ func interstitialDidLoad() {
41
+ ready = true
42
+ emit?("interstitial", "load", [:])
43
+ }
44
+
45
+ func interstitialDidFailToLoadWithError(_ error: Error) {
46
+ ready = false
47
+ emit?("interstitial", "loadFailed", errorPayload(error))
48
+ }
49
+
50
+ func interstitialDidOpen() {
51
+ emit?("interstitial", "show", [:])
52
+ }
53
+
54
+ func interstitialDidFailToShowWithError(_ error: Error) {
55
+ ready = false
56
+ emit?("interstitial", "showFailed", errorPayload(error))
57
+ }
58
+
59
+ func interstitialDidClick() {
60
+ emit?("interstitial", "click", [:])
61
+ }
62
+
63
+ func interstitialDidClose() {
64
+ ready = false
65
+ // Mirror AdMob's adDidDismissFullScreenContent pattern -- brief section 4, point 4.
66
+ UAAudioSessionGuard.shared.reactivateAfterAdDismiss()
67
+ emit?("interstitial", "dismiss", [:])
68
+ IronSource.loadInterstitial()
69
+ }
70
+
71
+ private func errorPayload(_ error: Error) -> [String: Any] {
72
+ let nsError = error as NSError
73
+ return ["code": nsError.code, "message": nsError.localizedDescription]
74
+ }
75
+ }
@@ -0,0 +1,78 @@
1
+ import Foundation
2
+
3
+ /**
4
+ Rewarded ad format. Synthesizes a "rewardFailed" event when the ad closes WITHOUT a prior
5
+ "rewarded" event -- built in from the start per brief 3B, rather than retrofitted later like
6
+ Claude_AdMob's OnRewardedFailed had to be.
7
+ */
8
+ final class UARewarded: NSObject {
9
+
10
+ static let shared = UARewarded()
11
+ private override init() {}
12
+
13
+ private var emit: ((String, String, [String: Any]) -> Void)?
14
+ private var earnedThisSession = false
15
+
16
+ func registerListener(emit: @escaping (String, String, [String: Any]) -> Void) {
17
+ self.emit = emit
18
+ // NOTE: verify against pinned SDK version -- classic API is
19
+ // `IronSourceRewardedVideoDelegate` via `IronSource.setRewardedVideoDelegate(self)`.
20
+ IronSource.setRewardedVideoDelegate(self)
21
+ }
22
+
23
+ func load(_ command: CDVInvokedUrlCommand, plugin: ArcadesindoUnityAds) {
24
+ // Classic rewarded-video API auto-loads in the background; just ack the JS call.
25
+ plugin.send(command, success: true)
26
+ }
27
+
28
+ func show(_ command: CDVInvokedUrlCommand, plugin: ArcadesindoUnityAds) {
29
+ guard IronSource.hasRewardedVideo() else {
30
+ plugin.send(command, error: "Rewarded not ready")
31
+ return
32
+ }
33
+ IronSource.showRewardedVideo(with: plugin.viewController)
34
+ plugin.send(command, success: true)
35
+ }
36
+
37
+ func isReady(_ command: CDVInvokedUrlCommand, plugin: ArcadesindoUnityAds) {
38
+ plugin.send(command, payload: IronSource.hasRewardedVideo())
39
+ }
40
+ }
41
+
42
+ extension UARewarded: IronSourceRewardedVideoDelegate {
43
+
44
+ func rewardedVideoHasChangedAvailability(_ available: Bool) {
45
+ emit?("rewarded", available ? "load" : "loadFailed", [:])
46
+ }
47
+
48
+ func rewardedVideoDidOpen() {
49
+ earnedThisSession = false
50
+ emit?("rewarded", "show", [:])
51
+ }
52
+
53
+ func rewardedVideoDidFailToShowWithError(_ error: Error) {
54
+ let nsError = error as NSError
55
+ emit?("rewarded", "showFailed", ["code": nsError.code, "message": nsError.localizedDescription])
56
+ }
57
+
58
+ func rewardedVideoDidClick() {
59
+ emit?("rewarded", "click", [:])
60
+ }
61
+
62
+ func rewardedVideoDidClose() {
63
+ UAAudioSessionGuard.shared.reactivateAfterAdDismiss()
64
+ if !earnedThisSession {
65
+ emit?("rewarded", "rewardFailed", [:])
66
+ }
67
+ emit?("rewarded", "dismiss", [:])
68
+ }
69
+
70
+ func didReceiveReward(forPlacement placementInfo: ISPlacementInfo) {
71
+ earnedThisSession = true
72
+ emit?("rewarded", "rewarded", [
73
+ "placementName": placementInfo.placementName ?? "",
74
+ "rewardName": placementInfo.rewardName ?? "",
75
+ "rewardAmount": placementInfo.rewardAmount
76
+ ])
77
+ }
78
+ }
@@ -0,0 +1,106 @@
1
+ var exec = require('cordova/exec');
2
+
3
+ var PLUGIN_NAME = 'ArcadesindoUnityAds';
4
+
5
+ /**
6
+ * Thin JS bridge over the native LevelPlay (Unity Ads adapter only) wrapper.
7
+ * Every method returns nothing synchronously; results/events arrive via the
8
+ * registered listener callback (see `setAdListener`), matching the async
9
+ * load/show/dismiss/reward lifecycle of ad SDKs.
10
+ */
11
+ var ArcadesindoUnityAds = {
12
+
13
+ // ---- lifecycle -------------------------------------------------------
14
+
15
+ /**
16
+ * @param {Object} options
17
+ * @param {string} options.appKey LevelPlay App Key (platform.ironsrc.com)
18
+ * @param {boolean} [options.isTesting] Show test ads only
19
+ * @param {string} [options.userId] For server-to-server rewarded callbacks
20
+ */
21
+ initialize: function (options, successCb, errorCb) {
22
+ exec(successCb, errorCb, PLUGIN_NAME, 'initialize', [options || {}]);
23
+ },
24
+
25
+ /**
26
+ * Registers a single persistent listener that receives every ad lifecycle
27
+ * event as { format, event, data }. format: 'interstitial'|'rewarded'|'banner'.
28
+ * event: 'load'|'loadFailed'|'show'|'showFailed'|'click'|'dismiss'|'rewarded'|'rewardFailed'.
29
+ */
30
+ setAdListener: function (eventCb) {
31
+ exec(eventCb, null, PLUGIN_NAME, 'setAdListener', []);
32
+ },
33
+
34
+ // ---- interstitial ------------------------------------------------------
35
+
36
+ loadInterstitial: function (successCb, errorCb) {
37
+ exec(successCb, errorCb, PLUGIN_NAME, 'loadInterstitial', []);
38
+ },
39
+
40
+ showInterstitial: function (successCb, errorCb) {
41
+ exec(successCb, errorCb, PLUGIN_NAME, 'showInterstitial', []);
42
+ },
43
+
44
+ isInterstitialReady: function (successCb, errorCb) {
45
+ exec(successCb, errorCb, PLUGIN_NAME, 'isInterstitialReady', []);
46
+ },
47
+
48
+ // ---- rewarded ------------------------------------------------------
49
+
50
+ loadRewarded: function (successCb, errorCb) {
51
+ exec(successCb, errorCb, PLUGIN_NAME, 'loadRewarded', []);
52
+ },
53
+
54
+ showRewarded: function (successCb, errorCb) {
55
+ exec(successCb, errorCb, PLUGIN_NAME, 'showRewarded', []);
56
+ },
57
+
58
+ isRewardedReady: function (successCb, errorCb) {
59
+ exec(successCb, errorCb, PLUGIN_NAME, 'isRewardedReady', []);
60
+ },
61
+
62
+ // ---- banner ------------------------------------------------------
63
+
64
+ /** @param {Object} options { position: 'top'|'bottom' } */
65
+ loadBanner: function (options, successCb, errorCb) {
66
+ exec(successCb, errorCb, PLUGIN_NAME, 'loadBanner', [options || {}]);
67
+ },
68
+
69
+ showBanner: function (successCb, errorCb) {
70
+ exec(successCb, errorCb, PLUGIN_NAME, 'showBanner', []);
71
+ },
72
+
73
+ hideBanner: function (successCb, errorCb) {
74
+ exec(successCb, errorCb, PLUGIN_NAME, 'hideBanner', []);
75
+ },
76
+
77
+ destroyBanner: function (successCb, errorCb) {
78
+ exec(successCb, errorCb, PLUGIN_NAME, 'destroyBanner', []);
79
+ },
80
+
81
+ // ---- consent / privacy ------------------------------------------------
82
+
83
+ /** @param {boolean} consent GDPR consent */
84
+ setConsent: function (consent, successCb, errorCb) {
85
+ exec(successCb, errorCb, PLUGIN_NAME, 'setConsent', [!!consent]);
86
+ },
87
+
88
+ /** @param {boolean} value CCPA "do not sell" flag */
89
+ setDoNotSell: function (value, successCb, errorCb) {
90
+ exec(successCb, errorCb, PLUGIN_NAME, 'setDoNotSell', [!!value]);
91
+ },
92
+
93
+ /** @param {boolean} value COPPA child-directed flag */
94
+ setChildDirected: function (value, successCb, errorCb) {
95
+ exec(successCb, errorCb, PLUGIN_NAME, 'setChildDirected', [!!value]);
96
+ },
97
+
98
+ // ---- diagnostics (audio bug investigation, see brief section 4) ------
99
+
100
+ /** Returns { currentTime } for delta-polling diagnostics around fullscreen ads. */
101
+ getAudioDiagnostics: function (successCb, errorCb) {
102
+ exec(successCb, errorCb, PLUGIN_NAME, 'getAudioDiagnostics', []);
103
+ }
104
+ };
105
+
106
+ module.exports = ArcadesindoUnityAds;