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 +38 -0
- package/package.json +48 -0
- package/plugin.xml +102 -0
- package/src/android/ArcadesindoUnityAds.kt +155 -0
- package/src/android/UABanner.kt +87 -0
- package/src/android/UAConsent.kt +30 -0
- package/src/android/UAInterstitial.kt +80 -0
- package/src/android/UARewarded.kt +100 -0
- package/src/ios/ArcadesindoUnityAds.swift +145 -0
- package/src/ios/UAAudioSessionGuard.swift +83 -0
- package/src/ios/UABanner.swift +72 -0
- package/src/ios/UAConsent.swift +7 -0
- package/src/ios/UAInterstitial.swift +75 -0
- package/src/ios/UARewarded.swift +78 -0
- package/www/ArcadesindoUnityAds.js +106 -0
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;
|