react-native-levelplayads 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/LICENSE +20 -0
  2. package/NitroLevelPlayAds.podspec +28 -0
  3. package/README.md +52 -0
  4. package/android/CMakeLists.txt +20 -0
  5. package/android/build.gradle +130 -0
  6. package/android/consumer-rules.pro +3 -0
  7. package/android/fix-prefab.gradle +46 -0
  8. package/android/gradle.properties +5 -0
  9. package/android/src/main/AndroidManifest.xml +1 -0
  10. package/android/src/main/cpp/cpp-adapter.cpp +7 -0
  11. package/android/src/main/java/com/margelo/nitro/levelplayads/HybridLevelPlayAds.kt +287 -0
  12. package/android/src/main/java/com/margelo/nitro/levelplayads/LevelPlayAndroidUtils.kt +30 -0
  13. package/android/src/main/java/com/margelo/nitro/levelplayads/NitroLevelPlayAdsPackage.java +27 -0
  14. package/ios/HybridLevelPlayAds.swift +274 -0
  15. package/ios/LevelPlayAdsSupport.swift +74 -0
  16. package/lib/module/index.js +31 -0
  17. package/lib/module/index.js.map +1 -0
  18. package/lib/module/index.test.js +78 -0
  19. package/lib/module/index.test.js.map +1 -0
  20. package/lib/module/package.json +1 -0
  21. package/lib/module/specs/LevelPlayAds.nitro.js +4 -0
  22. package/lib/module/specs/LevelPlayAds.nitro.js.map +1 -0
  23. package/lib/typescript/package.json +1 -0
  24. package/lib/typescript/src/index.d.ts +16 -0
  25. package/lib/typescript/src/index.d.ts.map +1 -0
  26. package/lib/typescript/src/index.test.d.ts +2 -0
  27. package/lib/typescript/src/index.test.d.ts.map +1 -0
  28. package/lib/typescript/src/specs/LevelPlayAds.nitro.d.ts +18 -0
  29. package/lib/typescript/src/specs/LevelPlayAds.nitro.d.ts.map +1 -0
  30. package/nitro.json +18 -0
  31. package/nitrogen/generated/android/NitroLevelPlayAds+autolinking.cmake +81 -0
  32. package/nitrogen/generated/android/NitroLevelPlayAds+autolinking.gradle +27 -0
  33. package/nitrogen/generated/android/NitroLevelPlayAdsOnLoad.cpp +44 -0
  34. package/nitrogen/generated/android/NitroLevelPlayAdsOnLoad.hpp +25 -0
  35. package/nitrogen/generated/android/c++/JHybridLevelPlayAdsSpec.cpp +112 -0
  36. package/nitrogen/generated/android/c++/JHybridLevelPlayAdsSpec.hpp +69 -0
  37. package/nitrogen/generated/android/c++/JLevelPlayAdShowResult.hpp +54 -0
  38. package/nitrogen/generated/android/c++/JLevelPlayAdShowState.hpp +59 -0
  39. package/nitrogen/generated/android/c++/JLevelPlayAdType.hpp +59 -0
  40. package/nitrogen/generated/android/kotlin/com/margelo/nitro/levelplayads/HybridLevelPlayAdsSpec.kt +72 -0
  41. package/nitrogen/generated/android/kotlin/com/margelo/nitro/levelplayads/LevelPlayAdShowResult.kt +29 -0
  42. package/nitrogen/generated/android/kotlin/com/margelo/nitro/levelplayads/LevelPlayAdShowState.kt +21 -0
  43. package/nitrogen/generated/android/kotlin/com/margelo/nitro/levelplayads/LevelPlayAdType.kt +21 -0
  44. package/nitrogen/generated/android/kotlin/com/margelo/nitro/levelplayads/NitroLevelPlayAdsOnLoad.kt +35 -0
  45. package/nitrogen/generated/ios/NitroLevelPlayAds+autolinking.rb +60 -0
  46. package/nitrogen/generated/ios/NitroLevelPlayAds-Swift-Cxx-Bridge.cpp +56 -0
  47. package/nitrogen/generated/ios/NitroLevelPlayAds-Swift-Cxx-Bridge.hpp +168 -0
  48. package/nitrogen/generated/ios/NitroLevelPlayAds-Swift-Cxx-Umbrella.hpp +54 -0
  49. package/nitrogen/generated/ios/NitroLevelPlayAdsAutolinking.mm +33 -0
  50. package/nitrogen/generated/ios/NitroLevelPlayAdsAutolinking.swift +25 -0
  51. package/nitrogen/generated/ios/c++/HybridLevelPlayAdsSpecSwift.cpp +11 -0
  52. package/nitrogen/generated/ios/c++/HybridLevelPlayAdsSpecSwift.hpp +116 -0
  53. package/nitrogen/generated/ios/swift/Func_void.swift +47 -0
  54. package/nitrogen/generated/ios/swift/Func_void_LevelPlayAdShowResult.swift +47 -0
  55. package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +47 -0
  56. package/nitrogen/generated/ios/swift/HybridLevelPlayAdsSpec.swift +54 -0
  57. package/nitrogen/generated/ios/swift/HybridLevelPlayAdsSpec_cxx.swift +201 -0
  58. package/nitrogen/generated/ios/swift/LevelPlayAdShowResult.swift +35 -0
  59. package/nitrogen/generated/ios/swift/LevelPlayAdShowState.swift +40 -0
  60. package/nitrogen/generated/ios/swift/LevelPlayAdType.swift +40 -0
  61. package/nitrogen/generated/shared/c++/HybridLevelPlayAdsSpec.cpp +26 -0
  62. package/nitrogen/generated/shared/c++/HybridLevelPlayAdsSpec.hpp +73 -0
  63. package/nitrogen/generated/shared/c++/LevelPlayAdShowResult.hpp +68 -0
  64. package/nitrogen/generated/shared/c++/LevelPlayAdShowState.hpp +76 -0
  65. package/nitrogen/generated/shared/c++/LevelPlayAdType.hpp +76 -0
  66. package/package.json +170 -0
  67. package/react-native.config.js +8 -0
  68. package/src/index.test.ts +119 -0
  69. package/src/index.ts +71 -0
  70. package/src/specs/LevelPlayAds.nitro.ts +26 -0
package/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 exzos
4
+ Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ of this software and associated documentation files (the "Software"), to deal
6
+ in the Software without restriction, including without limitation the rights
7
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the Software is
9
+ furnished to do so, subject to the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be included in all
12
+ copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
+ SOFTWARE.
@@ -0,0 +1,28 @@
1
+ require "json"
2
+
3
+ package = JSON.parse(File.read(File.join(__dir__, "package.json")))
4
+
5
+ Pod::Spec.new do |s|
6
+ s.name = "NitroLevelPlayAds"
7
+ s.version = package["version"]
8
+ s.summary = package["description"]
9
+ s.homepage = "https://github.com/exzos28/react-native-levelplayads"
10
+ s.license = { :type => "MIT" }
11
+ s.authors = { "exzos" => "oleksandr.kurinnyi.work@gmail.com" }
12
+ s.platforms = { :ios => "15.1" }
13
+ s.source = { :path => "." }
14
+
15
+ s.source_files = ["ios/**/*.{swift,m,mm}"]
16
+
17
+ load "nitrogen/generated/ios/NitroLevelPlayAds+autolinking.rb"
18
+ add_nitrogen_files(s)
19
+
20
+ s.dependency "React-Core"
21
+ s.dependency "React-jsi"
22
+ s.dependency "React-callinvoker"
23
+ # Verified against CocoaPods trunk (https://trunk.cocoapods.org/api/v1/pods/IronSourceSDK) —
24
+ # the real pod name is "IronSourceSDK" (module name `IronSource`), not "IronSourceMediation".
25
+ s.dependency "IronSourceSDK", "~> 9.5.0.0"
26
+
27
+ install_modules_dependencies(s)
28
+ end
package/README.md ADDED
@@ -0,0 +1,52 @@
1
+ # react-native-levelplayads
2
+
3
+ [Nitro Modules](https://nitro.margelo.com/) bridge for the LevelPlay
4
+ (ironSource) mediation SDK — interstitial and rewarded ads.
5
+
6
+ ## Install
7
+
8
+ ```sh
9
+ npm install react-native-levelplayads react-native-nitro-modules
10
+ ```
11
+
12
+ ## Usage
13
+
14
+ There's no `InterstitialAd`/`RewardedAd` class or event listeners here —
15
+ just `load`/`show`, keyed by ad type and ad unit ID, resolving through
16
+ promises.
17
+
18
+ ```ts
19
+ import LevelPlayAds from 'react-native-levelplayads';
20
+
21
+ await LevelPlayAds().initialize('YOUR_LEVELPLAY_APP_KEY', {testMode: true});
22
+ LevelPlayAds().setGDPRConsent(true);
23
+ LevelPlayAds().setCCPAConsent(true);
24
+ LevelPlayAds().setCOPPA(false);
25
+
26
+ await LevelPlayAds().load('interstitial', 'YOUR_AD_UNIT_ID');
27
+ const result = await LevelPlayAds().show('interstitial', 'YOUR_AD_UNIT_ID');
28
+ // result.state: 'completed' | 'skipped'
29
+
30
+ await LevelPlayAds().load('rewarded', 'YOUR_REWARDED_AD_UNIT_ID');
31
+ const rewardResult = await LevelPlayAds().show(
32
+ 'rewarded',
33
+ 'YOUR_REWARDED_AD_UNIT_ID',
34
+ );
35
+ if (rewardResult.state === 'completed') {
36
+ // grant the reward
37
+ }
38
+ ```
39
+
40
+ ## Notes
41
+
42
+ - `load()`/`show()` reject on failure — no separate `ERROR` event.
43
+ - `result.state === 'completed'` is also how you know a rewarded ad earned
44
+ its reward; LevelPlay doesn't send a distinct "reward earned" signal.
45
+ - Call `load()` again after each `show()` to prepare the next impression.
46
+
47
+ See [`src/specs/LevelPlayAds.nitro.ts`](src/specs/LevelPlayAds.nitro.ts) for
48
+ the full native interface and [`example`](example) for a runnable demo.
49
+
50
+ ## License
51
+
52
+ MIT
@@ -0,0 +1,20 @@
1
+ project(NitroLevelPlayAds)
2
+ cmake_minimum_required(VERSION 3.9.0)
3
+
4
+ set(PACKAGE_NAME NitroLevelPlayAds)
5
+ set(CMAKE_VERBOSE_MAKEFILE ON)
6
+ set(CMAKE_CXX_STANDARD 20)
7
+
8
+ add_library(${PACKAGE_NAME} SHARED
9
+ src/main/cpp/cpp-adapter.cpp
10
+ )
11
+
12
+ include(${CMAKE_SOURCE_DIR}/../nitrogen/generated/android/NitroLevelPlayAds+autolinking.cmake)
13
+
14
+ find_library(LOG_LIB log)
15
+
16
+ target_link_libraries(
17
+ ${PACKAGE_NAME}
18
+ ${LOG_LIB}
19
+ android
20
+ )
@@ -0,0 +1,130 @@
1
+ buildscript {
2
+ repositories {
3
+ google()
4
+ mavenCentral()
5
+ }
6
+
7
+ dependencies {
8
+ classpath "com.android.tools.build:gradle:8.12.1"
9
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:2.1.20"
10
+ }
11
+ }
12
+
13
+ def reactNativeArchitectures() {
14
+ def value = rootProject.getProperties().get("reactNativeArchitectures")
15
+ return value ? value.split(",") : ["armeabi-v7a", "x86", "x86_64", "arm64-v8a"]
16
+ }
17
+
18
+ def isNewArchitectureEnabled() {
19
+ return rootProject.hasProperty("newArchEnabled") && rootProject.getProperty("newArchEnabled") == "true"
20
+ }
21
+
22
+ apply plugin: "com.android.library"
23
+ apply plugin: "org.jetbrains.kotlin.android"
24
+
25
+ def nitroAutolinkingFile = file("../nitrogen/generated/android/NitroLevelPlayAds+autolinking.gradle")
26
+ if (nitroAutolinkingFile.exists()) {
27
+ apply from: nitroAutolinkingFile
28
+ }
29
+ apply from: "./fix-prefab.gradle"
30
+
31
+ def getExtOrDefault(name) {
32
+ return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties["NitroLevelPlayAds_" + name]
33
+ }
34
+
35
+ def getExtOrIntegerDefault(name) {
36
+ return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties["NitroLevelPlayAds_" + name]).toInteger()
37
+ }
38
+
39
+ android {
40
+ namespace "com.margelo.nitro.levelplayads"
41
+
42
+ ndkVersion getExtOrDefault("ndkVersion")
43
+ compileSdkVersion getExtOrIntegerDefault("compileSdkVersion")
44
+
45
+ defaultConfig {
46
+ minSdkVersion getExtOrIntegerDefault("minSdkVersion")
47
+ targetSdkVersion getExtOrIntegerDefault("targetSdkVersion")
48
+ consumerProguardFiles "consumer-rules.pro"
49
+ buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString()
50
+
51
+ externalNativeBuild {
52
+ cmake {
53
+ cppFlags "-frtti -fexceptions -Wall -Wextra -fstack-protector-all"
54
+ arguments "-DANDROID_STL=c++_shared", "-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON"
55
+ abiFilters (*reactNativeArchitectures())
56
+
57
+ buildTypes {
58
+ debug {
59
+ cppFlags "-O1 -g"
60
+ }
61
+ release {
62
+ cppFlags "-O2"
63
+ }
64
+ }
65
+ }
66
+ }
67
+ }
68
+
69
+ externalNativeBuild {
70
+ cmake {
71
+ path "CMakeLists.txt"
72
+ }
73
+ }
74
+
75
+ packagingOptions {
76
+ excludes = [
77
+ "META-INF",
78
+ "META-INF/**",
79
+ "**/libc++_shared.so",
80
+ "**/libfbjni.so",
81
+ "**/libjsi.so",
82
+ "**/libfolly_json.so",
83
+ "**/libfolly_runtime.so",
84
+ "**/libglog.so",
85
+ "**/libhermes.so",
86
+ "**/libreactnative.so",
87
+ "**/libreactnativejni.so"
88
+ ]
89
+ }
90
+
91
+ buildFeatures {
92
+ buildConfig true
93
+ prefab true
94
+ }
95
+
96
+ buildTypes {
97
+ release {
98
+ minifyEnabled false
99
+ }
100
+ }
101
+
102
+ lintOptions {
103
+ disable "GradleCompatible"
104
+ }
105
+
106
+ compileOptions {
107
+ sourceCompatibility JavaVersion.VERSION_17
108
+ targetCompatibility JavaVersion.VERSION_17
109
+ }
110
+
111
+ kotlinOptions {
112
+ jvmTarget = "17"
113
+ }
114
+
115
+ }
116
+
117
+ repositories {
118
+ google()
119
+ mavenCentral()
120
+ }
121
+
122
+ dependencies {
123
+ implementation "com.facebook.react:react-android"
124
+ implementation project(":react-native-nitro-modules")
125
+
126
+ implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0"
127
+ // android-sdk.is.com was deprecated 2025-06-30 and frozen at 9.2.0, which drops
128
+ // onAdClosed/onAdRewarded on Android; LevelPlay now ships on Maven Central.
129
+ implementation "com.unity3d.ads-mediation:mediation-sdk:9.5.0"
130
+ }
@@ -0,0 +1,3 @@
1
+ -keep class com.margelo.nitro.levelplayads.** { *; }
2
+ -keep class com.ironsource.** { *; }
3
+ -keep class com.unity3d.mediation.** { *; }
@@ -0,0 +1,46 @@
1
+ tasks.configureEach { task ->
2
+ def prefabConfigurePattern = ~/^prefab(.+)ConfigurePackage$/
3
+ def matcher = task.name =~ prefabConfigurePattern
4
+ if (matcher.matches()) {
5
+ def variantName = matcher[0][1]
6
+ task.outputs.upToDateWhen { false }
7
+ task.dependsOn("externalNativeBuild${variantName}")
8
+ }
9
+ }
10
+
11
+ afterEvaluate {
12
+ def abis = reactNativeArchitectures()
13
+ rootProject.allprojects.each { proj ->
14
+ if (proj === rootProject) return
15
+
16
+ def dependsOnThisLib = proj.configurations.findAll { it.canBeResolved }.any { config ->
17
+ config.dependencies.any { dep ->
18
+ dep.group == project.group && dep.name == project.name
19
+ }
20
+ }
21
+ if (!dependsOnThisLib && proj != project) return
22
+
23
+ if (!proj.plugins.hasPlugin("com.android.application") &&
24
+ !proj.plugins.hasPlugin("com.android.library")) {
25
+ return
26
+ }
27
+
28
+ def variants = proj.android.hasProperty("applicationVariants") ?
29
+ proj.android.applicationVariants : proj.android.libraryVariants
30
+ variants.all { variant ->
31
+ def variantName = variant.name
32
+ abis.each { abi ->
33
+ def searchDir = new File(proj.projectDir, ".cxx/${variantName}")
34
+ if (!searchDir.exists()) return
35
+ def matches = []
36
+ searchDir.eachDir { randomDir ->
37
+ def prefabFile = new File(randomDir, "${abi}/prefab_config.json")
38
+ if (prefabFile.exists()) matches << prefabFile
39
+ }
40
+ matches.each { prefabConfig ->
41
+ prefabConfig.setLastModified(System.currentTimeMillis())
42
+ }
43
+ }
44
+ }
45
+ }
46
+ }
@@ -0,0 +1,5 @@
1
+ NitroLevelPlayAds_kotlinVersion=2.1.20
2
+ NitroLevelPlayAds_minSdkVersion=24
3
+ NitroLevelPlayAds_targetSdkVersion=36
4
+ NitroLevelPlayAds_compileSdkVersion=36
5
+ NitroLevelPlayAds_ndkVersion=27.1.12297006
@@ -0,0 +1 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android" />
@@ -0,0 +1,7 @@
1
+ #include <jni.h>
2
+
3
+ #include "NitroLevelPlayAdsOnLoad.hpp"
4
+
5
+ JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void*) {
6
+ return margelo::nitro::levelplayads::initialize(vm);
7
+ }
@@ -0,0 +1,287 @@
1
+ package com.margelo.nitro.levelplayads
2
+
3
+ import android.util.Log
4
+ import androidx.annotation.Keep
5
+ import com.facebook.proguard.annotations.DoNotStrip
6
+ import com.facebook.react.bridge.ReactApplicationContext
7
+ import com.margelo.nitro.NitroModules
8
+ import com.margelo.nitro.core.Promise
9
+ import com.unity3d.mediation.LevelPlay
10
+ import com.unity3d.mediation.LevelPlayAdError
11
+ import com.unity3d.mediation.LevelPlayAdInfo
12
+ import com.unity3d.mediation.LevelPlayInitError
13
+ import com.unity3d.mediation.LevelPlayInitRequest
14
+ import com.unity3d.mediation.interstitial.LevelPlayInterstitialAd
15
+ import com.unity3d.mediation.interstitial.LevelPlayInterstitialAdListener
16
+ import com.unity3d.mediation.rewarded.LevelPlayReward
17
+ import com.unity3d.mediation.rewarded.LevelPlayRewardedAd
18
+ import com.unity3d.mediation.rewarded.LevelPlayRewardedAdListener
19
+ import kotlinx.coroutines.CompletableDeferred
20
+ import kotlinx.coroutines.Dispatchers
21
+ import kotlinx.coroutines.withContext
22
+
23
+ // Verified against the real com.ironsource.sdk:mediationsdk:9.2.0 AAR (javap on
24
+ // the downloaded classes.jar, https://android-sdk.is.com/) — the unified LevelPlay
25
+ // API lives under `com.unity3d.mediation`. LevelPlayInitRequest.Builder has no
26
+ // withLegacyAdFormats() in 9.2.0 (removed since the previous 8.6.0 generation),
27
+ // and setConsent/setMetaData/setAdaptersDebug now live on `LevelPlay` itself
28
+ // rather than the legacy `com.ironsource.mediationsdk.IronSource` facade, whose
29
+ // surface was stripped down in this release.
30
+ @DoNotStrip
31
+ @Keep
32
+ class HybridLevelPlayAds : HybridLevelPlayAdsSpec() {
33
+ private companion object {
34
+ const val TAG = "LevelPlayAds"
35
+ }
36
+
37
+ private val context: ReactApplicationContext by lazy {
38
+ NitroModules.applicationContext as? ReactApplicationContext
39
+ ?: error("LevelPlay requires a ReactApplicationContext")
40
+ }
41
+
42
+ private val stateLock = Any()
43
+ private var isInitialized = false
44
+ private var initializedAppKey: String? = null
45
+ private var initializingAppKey: String? = null
46
+ private var initialization: CompletableDeferred<Unit>? = null
47
+
48
+ // LevelPlay requires the SDK listener to be set exactly once, before the first
49
+ // loadAd() call, and reused for the ad's whole lifetime. Re-registering a
50
+ // different listener object right before showAd() (as this module used to do)
51
+ // silently drops onAdClosed on Android: the internal IronSource event fires
52
+ // (confirmed via logcat, tag "IronSource"), but the unified
53
+ // LevelPlayInterstitialAdListener/LevelPlayRewardedAdListener never sees it
54
+ // once its listener has been swapped. So the SDK listener is installed once
55
+ // in the holder's init{} block, and load()/show() only swap out our own
56
+ // plain-Kotlin callback fields, never the SDK listener.
57
+ private abstract class AdHolder {
58
+ var onLoaded: (() -> Unit)? = null
59
+ var onLoadFailed: ((LevelPlayAdError) -> Unit)? = null
60
+ var onDisplayed: (() -> Unit)? = null
61
+ var onDisplayFailed: ((LevelPlayAdError) -> Unit)? = null
62
+ var onClosed: (() -> Unit)? = null
63
+ }
64
+
65
+ private class InterstitialHolder(adUnitId: String) : AdHolder() {
66
+ val ad = LevelPlayInterstitialAd(adUnitId)
67
+
68
+ init {
69
+ ad.setListener(object : LevelPlayInterstitialAdListener {
70
+ override fun onAdLoaded(adInfo: LevelPlayAdInfo) = onLoaded?.invoke() ?: Unit
71
+ override fun onAdLoadFailed(error: LevelPlayAdError) = onLoadFailed?.invoke(error) ?: Unit
72
+ override fun onAdDisplayed(adInfo: LevelPlayAdInfo) = onDisplayed?.invoke() ?: Unit
73
+ override fun onAdDisplayFailed(error: LevelPlayAdError, adInfo: LevelPlayAdInfo) =
74
+ onDisplayFailed?.invoke(error) ?: Unit
75
+ override fun onAdClosed(adInfo: LevelPlayAdInfo) = onClosed?.invoke() ?: Unit
76
+ })
77
+ }
78
+ }
79
+
80
+ private class RewardedHolder(adUnitId: String) : AdHolder() {
81
+ val ad = LevelPlayRewardedAd(adUnitId)
82
+ var onRewarded: (() -> Unit)? = null
83
+
84
+ init {
85
+ ad.setListener(object : LevelPlayRewardedAdListener {
86
+ override fun onAdLoaded(adInfo: LevelPlayAdInfo) = onLoaded?.invoke() ?: Unit
87
+ override fun onAdLoadFailed(error: LevelPlayAdError) = onLoadFailed?.invoke(error) ?: Unit
88
+ override fun onAdDisplayed(adInfo: LevelPlayAdInfo) = onDisplayed?.invoke() ?: Unit
89
+ override fun onAdDisplayFailed(error: LevelPlayAdError, adInfo: LevelPlayAdInfo) =
90
+ onDisplayFailed?.invoke(error) ?: Unit
91
+ override fun onAdRewarded(reward: LevelPlayReward, adInfo: LevelPlayAdInfo) = onRewarded?.invoke() ?: Unit
92
+ override fun onAdClosed(adInfo: LevelPlayAdInfo) = onClosed?.invoke() ?: Unit
93
+ })
94
+ }
95
+ }
96
+
97
+ // Keyed by adUnitId: load() and show() are separate calls in this module's
98
+ // spec, so the loaded ad object has to be held onto between the two calls.
99
+ private val loadedInterstitials = mutableMapOf<String, InterstitialHolder>()
100
+ private val loadedRewarded = mutableMapOf<String, RewardedHolder>()
101
+
102
+ override fun initialize(appKey: String, testMode: Boolean): Promise<Unit> = Promise.async {
103
+ require(appKey.isNotBlank()) { "LevelPlay app key is empty" }
104
+
105
+ val (deferred, shouldStart) = synchronized(stateLock) {
106
+ if (isInitialized) {
107
+ require(initializedAppKey == appKey) {
108
+ "LevelPlay SDK was already initialized with another app key"
109
+ }
110
+ return@async
111
+ }
112
+
113
+ val current = initialization
114
+ if (current != null) {
115
+ require(initializingAppKey == appKey) {
116
+ "LevelPlay SDK is being initialized with another app key"
117
+ }
118
+ current to false
119
+ } else {
120
+ CompletableDeferred<Unit>().also {
121
+ initialization = it
122
+ initializingAppKey = appKey
123
+ } to true
124
+ }
125
+ }
126
+
127
+ if (shouldStart) {
128
+ withContext(Dispatchers.Main) {
129
+ if (testMode) {
130
+ LevelPlay.setAdaptersDebug(true)
131
+ }
132
+
133
+ val initRequest = LevelPlayInitRequest.Builder(appKey).build()
134
+
135
+ LevelPlay.init(
136
+ context,
137
+ initRequest,
138
+ object : com.unity3d.mediation.LevelPlayInitListener {
139
+ override fun onInitSuccess(configuration: com.unity3d.mediation.LevelPlayConfiguration) {
140
+ synchronized(stateLock) {
141
+ isInitialized = true
142
+ initializedAppKey = appKey
143
+ initialization = null
144
+ initializingAppKey = null
145
+ }
146
+ deferred.complete(Unit)
147
+ }
148
+
149
+ override fun onInitFailed(error: LevelPlayInitError) {
150
+ synchronized(stateLock) {
151
+ initialization = null
152
+ initializingAppKey = null
153
+ }
154
+ deferred.completeExceptionally(
155
+ levelPlayAdsException("initialize", error.errorCode.toString(), error.errorMessage),
156
+ )
157
+ }
158
+ },
159
+ )
160
+ }
161
+ }
162
+
163
+ deferred.await()
164
+ }
165
+
166
+ override fun load(adType: LevelPlayAdType, adUnitId: String): Promise<Unit> = Promise.async {
167
+ require(adUnitId.isNotBlank()) { "LevelPlay ad unit ID is empty" }
168
+ val deferred = CompletableDeferred<Unit>()
169
+
170
+ withContext(Dispatchers.Main) {
171
+ when (adType) {
172
+ LevelPlayAdType.INTERSTITIAL -> {
173
+ val holder = InterstitialHolder(adUnitId)
174
+ holder.onLoaded = {
175
+ synchronized(stateLock) { loadedInterstitials[adUnitId] = holder }
176
+ deferred.complete(Unit)
177
+ }
178
+ holder.onLoadFailed = { error ->
179
+ deferred.completeExceptionally(
180
+ levelPlayAdsException("load", error.getErrorCode().toString(), error.getErrorMessage()),
181
+ )
182
+ }
183
+ holder.ad.loadAd()
184
+ }
185
+ LevelPlayAdType.REWARDED -> {
186
+ val holder = RewardedHolder(adUnitId)
187
+ holder.onLoaded = {
188
+ synchronized(stateLock) { loadedRewarded[adUnitId] = holder }
189
+ deferred.complete(Unit)
190
+ }
191
+ holder.onLoadFailed = { error ->
192
+ deferred.completeExceptionally(
193
+ levelPlayAdsException("load", error.getErrorCode().toString(), error.getErrorMessage()),
194
+ )
195
+ }
196
+ holder.ad.loadAd()
197
+ }
198
+ }
199
+ }
200
+
201
+ deferred.await()
202
+ }
203
+
204
+ private fun onShowDisplayFailed(
205
+ kind: String,
206
+ adUnitId: String,
207
+ error: LevelPlayAdError,
208
+ deferred: CompletableDeferred<*>,
209
+ ) {
210
+ Log.d(TAG, "show($kind, $adUnitId): onAdDisplayFailed (${error.getErrorCode()}) ${error.getErrorMessage()}")
211
+ deferred.completeExceptionally(
212
+ levelPlayAdsException("show", error.getErrorCode().toString(), error.getErrorMessage()),
213
+ )
214
+ }
215
+
216
+ override fun show(adType: LevelPlayAdType, adUnitId: String): Promise<LevelPlayAdShowResult> = Promise.async {
217
+ require(adUnitId.isNotBlank()) { "LevelPlay ad unit ID is empty" }
218
+ val activity = context.requireCurrentActivity()
219
+ val deferred = CompletableDeferred<LevelPlayAdShowResult>()
220
+
221
+ when (adType) {
222
+ LevelPlayAdType.INTERSTITIAL -> {
223
+ val holder = synchronized(stateLock) { loadedInterstitials.remove(adUnitId) }
224
+ ?: error("LevelPlay interstitial ad is not ready")
225
+ // Reuse the same SDK listener that was registered in load() — swapping
226
+ // in a different listener object here is what used to drop onAdClosed.
227
+ holder.onLoaded = null
228
+ holder.onLoadFailed = null
229
+ holder.onDisplayed = {
230
+ Log.d(TAG, "show(interstitial, $adUnitId): onAdDisplayed")
231
+ }
232
+ holder.onDisplayFailed = { error -> onShowDisplayFailed("interstitial", adUnitId, error, deferred) }
233
+ holder.onClosed = {
234
+ Log.d(TAG, "show(interstitial, $adUnitId): onAdClosed")
235
+ deferred.complete(LevelPlayAdShowResult(LevelPlayAdShowState.COMPLETED))
236
+ }
237
+ Log.d(TAG, "show(interstitial, $adUnitId): calling showAd")
238
+ withContext(Dispatchers.Main) { holder.ad.showAd(activity) }
239
+ }
240
+ LevelPlayAdType.REWARDED -> {
241
+ val holder = synchronized(stateLock) { loadedRewarded.remove(adUnitId) }
242
+ ?: error("LevelPlay rewarded ad is not ready")
243
+ // Rewarded ads don't carry a Unity-style "finish state" — LevelPlay fires a
244
+ // separate onAdRewarded event before onAdClosed, so completion is derived
245
+ // from whether that event happened at all before the ad closed.
246
+ var didReceiveReward = false
247
+ holder.onLoaded = null
248
+ holder.onLoadFailed = null
249
+ holder.onDisplayed = {
250
+ Log.d(TAG, "show(rewarded, $adUnitId): onAdDisplayed")
251
+ }
252
+ holder.onRewarded = {
253
+ Log.d(TAG, "show(rewarded, $adUnitId): onAdRewarded")
254
+ didReceiveReward = true
255
+ }
256
+ holder.onDisplayFailed = { error -> onShowDisplayFailed("rewarded", adUnitId, error, deferred) }
257
+ holder.onClosed = {
258
+ Log.d(TAG, "show(rewarded, $adUnitId): onAdClosed (didReceiveReward=$didReceiveReward)")
259
+ val state = if (didReceiveReward) {
260
+ LevelPlayAdShowState.COMPLETED
261
+ } else {
262
+ LevelPlayAdShowState.SKIPPED
263
+ }
264
+ deferred.complete(LevelPlayAdShowResult(state))
265
+ }
266
+ Log.d(TAG, "show(rewarded, $adUnitId): calling showAd")
267
+ withContext(Dispatchers.Main) { holder.ad.showAd(activity) }
268
+ }
269
+ }
270
+
271
+ deferred.await()
272
+ }
273
+
274
+ override fun setGDPRConsent(optIn: Boolean) {
275
+ LevelPlay.setConsent(optIn)
276
+ }
277
+
278
+ // CCPA flag semantics are "opted out of sale", the inverse of our `optIn`
279
+ // (personalized-ads-allowed) flag.
280
+ override fun setCCPAConsent(optIn: Boolean) {
281
+ LevelPlay.setMetaData("do_not_sell", if (optIn) "false" else "true")
282
+ }
283
+
284
+ override fun setCOPPA(isCoppa: Boolean) {
285
+ LevelPlay.setMetaData("is_child_directed", if (isCoppa) "true" else "false")
286
+ }
287
+ }
@@ -0,0 +1,30 @@
1
+ package com.margelo.nitro.levelplayads
2
+
3
+ import android.app.Activity
4
+ import android.os.Handler
5
+ import android.os.Looper
6
+ import com.facebook.react.bridge.ReactApplicationContext
7
+ import java.util.concurrent.CountDownLatch
8
+
9
+ internal fun ReactApplicationContext.requireCurrentActivity(): Activity =
10
+ currentActivity ?: error("Cannot show LevelPlay ad without an active Activity")
11
+
12
+ internal fun levelPlayAdsException(stage: String, errorCode: String, message: String) =
13
+ IllegalStateException("LevelPlay $stage failed ($errorCode): $message")
14
+
15
+ /** Mirrors HybridLevelPlayAds.swift's `runOnMain` so consent metadata writes happen on the same thread on both platforms. */
16
+ internal fun runOnMain(body: () -> Unit) {
17
+ if (Looper.myLooper() == Looper.getMainLooper()) {
18
+ body()
19
+ return
20
+ }
21
+ val latch = CountDownLatch(1)
22
+ Handler(Looper.getMainLooper()).post {
23
+ try {
24
+ body()
25
+ } finally {
26
+ latch.countDown()
27
+ }
28
+ }
29
+ latch.await()
30
+ }
@@ -0,0 +1,27 @@
1
+ package com.margelo.nitro.levelplayads;
2
+
3
+ import androidx.annotation.Nullable;
4
+
5
+ import com.facebook.react.TurboReactPackage;
6
+ import com.facebook.react.bridge.NativeModule;
7
+ import com.facebook.react.bridge.ReactApplicationContext;
8
+ import com.facebook.react.module.model.ReactModuleInfoProvider;
9
+
10
+ import java.util.HashMap;
11
+
12
+ public class NitroLevelPlayAdsPackage extends TurboReactPackage {
13
+ @Nullable
14
+ @Override
15
+ public NativeModule getModule(String name, ReactApplicationContext reactContext) {
16
+ return null;
17
+ }
18
+
19
+ @Override
20
+ public ReactModuleInfoProvider getReactModuleInfoProvider() {
21
+ return HashMap::new;
22
+ }
23
+
24
+ static {
25
+ NitroLevelPlayAdsOnLoad.initializeNative();
26
+ }
27
+ }