diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index 95cf3d2..9c037bc 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -27,6 +27,11 @@ jobs: - name: Setup gradle uses: gradle/gradle-build-action@v2 + - name: Check for Invisible Characters + uses: hakz/hidden-characters@v1.0-beta04 + with: + path: "./basic-ads" + - name: Check API run: ./gradlew apiCheck diff --git a/VERSIONS.md b/VERSIONS.md index 93867c5..07e52b8 100644 --- a/VERSIONS.md +++ b/VERSIONS.md @@ -32,4 +32,5 @@ Here's a list of the Basic-Ads dependency versions for each release after 0.2.0: | 1.1.0-beta02 | 2.3.0 | 1.9.3 | 1.9.1 | 24.9.0 / 12.14.0 | 4.0.0 / 3.1.0 | | 1.1.0-beta03 | 2.3.0 | 1.9.3 | 1.9.1 | 24.9.0 / 12.14.0 | 4.0.0 / 3.1.0 | | 1.1.0 | 2.3.0 | 1.9.3 | 1.9.1 | 24.9.0 / 12.14.0 | 4.0.0 / 3.1.0 | -| 1.1.1 | 2.3.0 | 1.10.0 | 1.9.1 | 24.9.0 / 12.14.0 | 4.0.0 / 3.1.0 | +| 1.1.1 | 2.3.0 | 1.10.0 | 1.9.1 | 24.9.0 / 12.14.0 | 4.0.0 / 3.1.0 | +| 1.2.0-beta01 | 2.3.21 | 1.10.3 | 1.9.1 | 25.2.0 / 13.3.0 | 4.0.0 / 3.1.0 | \ No newline at end of file diff --git a/basic-ads/api/basic-ads.api b/basic-ads/api/basic-ads.api index 4c03f35..08b52cb 100644 --- a/basic-ads/api/basic-ads.api +++ b/basic-ads/api/basic-ads.api @@ -286,7 +286,9 @@ public final class app/lexilabs/basic/ads/RewardedInterstitialAdHandler { public static final field $stable I public fun (Ljava/lang/Object;)V public final fun getState ()Lapp/lexilabs/basic/ads/AdState; + public final fun load (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;)V public final fun load (Ljava/lang/String;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;)V + public static synthetic fun load$default (Lapp/lexilabs/basic/ads/RewardedInterstitialAdHandler;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V public static synthetic fun load$default (Lapp/lexilabs/basic/ads/RewardedInterstitialAdHandler;Ljava/lang/String;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V public final fun setListeners (Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;)V public static synthetic fun setListeners$default (Lapp/lexilabs/basic/ads/RewardedInterstitialAdHandler;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)V @@ -347,6 +349,7 @@ public final class app/lexilabs/basic/ads/composable/RememberRewardedAdKt { public final class app/lexilabs/basic/ads/composable/RememberRewardedInterstitialAdKt { public static final fun rememberRewardedInterstitialAd (Ljava/lang/Object;Ljava/lang/String;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)Landroidx/compose/runtime/MutableState; + public static final fun rememberRewardedInterstitialAd (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)Landroidx/compose/runtime/MutableState; public static final fun rememberRewardedInterstitialAd (Ljava/lang/String;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)Landroidx/compose/runtime/MutableState; } diff --git a/basic-ads/build.gradle.kts b/basic-ads/build.gradle.kts index e63b6fc..8b42196 100644 --- a/basic-ads/build.gradle.kts +++ b/basic-ads/build.gradle.kts @@ -1,4 +1,3 @@ -import com.android.build.api.dsl.androidLibrary import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget @@ -13,43 +12,43 @@ plugins { } /** REMEDIATION **/ -dependencies { - /** This patches all transitive dependencies that have vulnerabilities **/ - constraints { - androidMainImplementation(libs.remediate.okhttp) { - version { strictly(libs.versions.remediate.okhttp.get()) } - because("CVE-2021-0341") - } - androidMainImplementation(libs.remediate.bitbucket) { - version { strictly(libs.versions.remediate.bitbucket.get())} - because("CVE-2024-29371") - } - androidMainImplementation(libs.remediate.netty.codec.http){ - version { strictly(libs.versions.remediate.netty.get())} - because("CVE-2025-67735") - } - androidMainImplementation(libs.remediate.netty.codec.http2){ - version { strictly(libs.versions.remediate.netty.get())} - because("CVE-2025-55163") - } - androidMainImplementation(libs.remediate.google.protobuf.kotlin){ - version { strictly(libs.versions.remediate.google.protobuf.get())} - because("CVE-2024-7254") - } - androidMainImplementation(libs.remediate.google.protobuf.java){ - version { strictly(libs.versions.remediate.google.protobuf.get())} - because("CVE-2024-7254") - } - androidMainImplementation(libs.remediate.jdom){ - version { strictly(libs.versions.remediate.jdom.get())} - because("CVE-2021-33813") - } - androidMainImplementation(libs.remediate.apache.compress){ - version { strictly(libs.versions.remediate.apache.compress.get())} - because("CVE-2024-26308") - } - } -} +//dependencies { +// /** This patches all transitive dependencies that have vulnerabilities **/ +// constraints { +// androidMainImplementation(libs.remediate.okhttp) { +// version { strictly(libs.versions.remediate.okhttp.get()) } +// because("CVE-2021-0341") +// } +// androidMainImplementation(libs.remediate.bitbucket) { +// version { strictly(libs.versions.remediate.bitbucket.get())} +// because("CVE-2024-29371") +// } +// androidMainImplementation(libs.remediate.netty.codec.http){ +// version { strictly(libs.versions.remediate.netty.get())} +// because("CVE-2025-67735") +// } +// androidMainImplementation(libs.remediate.netty.codec.http2){ +// version { strictly(libs.versions.remediate.netty.get())} +// because("CVE-2025-55163") +// } +// androidMainImplementation(libs.remediate.google.protobuf.kotlin){ +// version { strictly(libs.versions.remediate.google.protobuf.get())} +// because("CVE-2024-7254") +// } +// androidMainImplementation(libs.remediate.google.protobuf.java){ +// version { strictly(libs.versions.remediate.google.protobuf.get())} +// because("CVE-2024-7254") +// } +// androidMainImplementation(libs.remediate.jdom){ +// version { strictly(libs.versions.remediate.jdom.get())} +// because("CVE-2021-33813") +// } +// androidMainImplementation(libs.remediate.apache.compress){ +// version { strictly(libs.versions.remediate.apache.compress.get())} +// because("CVE-2024-26308") +// } +// } +//} kotlin { @@ -112,8 +111,7 @@ kotlin { freeCompilerArgs.add("-Xexpect-actual-classes") } - @Suppress("UnstableApiUsage") - androidLibrary{ + android { namespace = "app.lexilabs.basic.ads" compileSdk = libs.versions.build.sdk.compile.get().toInt() minSdk = libs.versions.build.sdk.min.get().toInt() diff --git a/basic-ads/src/androidMain/kotlin/app/lexilabs/basic/ads/InterstitialAdHandler.kt b/basic-ads/src/androidMain/kotlin/app/lexilabs/basic/ads/InterstitialAdHandler.kt index db9d6f2..6f3fa17 100644 --- a/basic-ads/src/androidMain/kotlin/app/lexilabs/basic/ads/InterstitialAdHandler.kt +++ b/basic-ads/src/androidMain/kotlin/app/lexilabs/basic/ads/InterstitialAdHandler.kt @@ -94,7 +94,7 @@ public actual class InterstitialAdHandler actual constructor( Log.d(tag, "setListeners: Loading") require(interstitialAd != null) { _state.value = AdState.FAILING - "InterstitialAd not loaded yet. `InterstitialAd.load()` must be called first" + "The provided InterstitialAdHandler is not loaded yet. You must call .load() on your handler instance (e.g., interstitialAdHandler.load()) before displaying the InterstitialAd composable." } interstitialAd?.let { interstitialAd?.fullScreenContentCallback = FullscreenContentDelegate( @@ -133,7 +133,7 @@ public actual class InterstitialAdHandler actual constructor( } require(interstitialAd != null) { _state.value = AdState.FAILING - "InterstitialAd not loaded yet. `InterstitialAd.load()` must be called first" + "The provided InterstitialAdHandler is not loaded yet. You must call .load() on your handler instance (e.g., interstitialAdHandler.load()) before displaying the InterstitialAd composable." } interstitialAd?.show(activity) } diff --git a/basic-ads/src/androidMain/kotlin/app/lexilabs/basic/ads/RewardedAdHandler.kt b/basic-ads/src/androidMain/kotlin/app/lexilabs/basic/ads/RewardedAdHandler.kt index 858b5d6..f3b22eb 100644 --- a/basic-ads/src/androidMain/kotlin/app/lexilabs/basic/ads/RewardedAdHandler.kt +++ b/basic-ads/src/androidMain/kotlin/app/lexilabs/basic/ads/RewardedAdHandler.kt @@ -145,7 +145,7 @@ public actual class RewardedAdHandler actual constructor(private val activity: A Log.d(tag, "setListeners: Loading") require(rewardedAd != null) { _state.value = AdState.FAILING - "RewardedAd not loaded yet. `RewardedAd.load()` must be called first" + "The provided RewardedAdHandler is not loaded yet. You must call .load() on your handler instance (e.g., rewardedAdHandler.load()) before displaying the RewardedAd composable." } rewardedAd?.let { rewardedAd?.fullScreenContentCallback = FullscreenContentDelegate( @@ -187,7 +187,7 @@ public actual class RewardedAdHandler actual constructor(private val activity: A } require(rewardedAd != null) { _state.value = AdState.FAILING - "RewardedAd not loaded yet. `RewardedAd.load()` must be called first" + "The provided RewardedAdHandler is not loaded yet. You must call .load() on your handler instance (e.g., rewardedAdHandler.load()) before displaying the RewardedAd composable." } rewardedAd?.show(activity) { reward -> Log.d(tag, "A reward was earned") diff --git a/basic-ads/src/androidMain/kotlin/app/lexilabs/basic/ads/RewardedInterstitialAdHandler.kt b/basic-ads/src/androidMain/kotlin/app/lexilabs/basic/ads/RewardedInterstitialAdHandler.kt index 4cbb981..c5268b3 100644 --- a/basic-ads/src/androidMain/kotlin/app/lexilabs/basic/ads/RewardedInterstitialAdHandler.kt +++ b/basic-ads/src/androidMain/kotlin/app/lexilabs/basic/ads/RewardedInterstitialAdHandler.kt @@ -7,6 +7,7 @@ import androidx.compose.runtime.mutableStateOf import app.lexilabs.basic.logging.Log import com.google.android.gms.ads.AdRequest import com.google.android.gms.ads.LoadAdError +import com.google.android.gms.ads.rewarded.ServerSideVerificationOptions import com.google.android.gms.ads.rewardedinterstitial.RewardedInterstitialAdLoadCallback import com.google.android.gms.ads.rewardedinterstitial.RewardedInterstitialAd as AndroidRewardedInterstitialAd @@ -70,6 +71,61 @@ public actual class RewardedInterstitialAdHandler actual constructor( ) } + /** + * Loads a rewarded interstitial ad. + * + * @param adUnitId The ad unit ID. + * @param userId Used for Server-Side Verification + * @param customData Used for Server-Side Verification + * @param onLoad A callback invoked when the ad is loaded. + * @param onFailure A callback invoked when the ad fails to load. + */ + public actual fun load( + adUnitId: String, + userId: String, + customData: String, + onLoad: () -> Unit, + onFailure: (Exception) -> Unit + ) { + _state.value = AdState.LOADING + Log.d(tag, "loadRewardedAd: Loading") + require(activity != null) { + _state.value = AdState.FAILING + "Activity Context must be set to non-null value in Android" + } + require(activity is Activity) { + _state.value = AdState.FAILING + "activity variable must be of the Android `Activity` type" + } + AndroidRewardedInterstitialAd.load( + activity, + adUnitId, + AdRequest.Builder().build(), + object : RewardedInterstitialAdLoadCallback() { + override fun onAdFailedToLoad(adError: LoadAdError) { + super.onAdFailedToLoad(adError) + Log.d(tag, "loadRewardedAd:failure:$adError") + _state.value = AdState.FAILING + onFailure(AdException(adError.message)) + } + + override fun onAdLoaded(ad: AndroidRewardedInterstitialAd) { + super.onAdLoaded(ad) + Log.d(tag, "loadRewardedAd:success") + rewardedInterstitialAd = ad + val options = + ServerSideVerificationOptions.Builder().apply { + setUserId(userId) + setCustomData(customData) + }.build() + rewardedInterstitialAd?.setServerSideVerificationOptions(options) + _state.value = AdState.READY + onLoad() + } + } + ) + } + /** * Sets the listeners for the rewarded interstitial ad. * @@ -89,7 +145,7 @@ public actual class RewardedInterstitialAdHandler actual constructor( Log.d(tag, "setListeners: Loading") require(rewardedInterstitialAd != null) { _state.value = AdState.FAILING - "RewardedAd not loaded yet. `RewardedAd.load()` must be called first" + "The provided RewardedInterstitialAdHandler is not loaded yet. You must call .load() on your handler instance (e.g., rewardedInterstitialAdHandler.load()) before displaying the RewardedInterstitialAd composable." } rewardedInterstitialAd?.let { rewardedInterstitialAd?.fullScreenContentCallback = FullscreenContentDelegate( @@ -129,7 +185,7 @@ public actual class RewardedInterstitialAdHandler actual constructor( } require(rewardedInterstitialAd != null) { _state.value = AdState.FAILING - "RewardedAd not loaded yet. `RewardedAd.load()` must be called first" + "The provided RewardedInterstitialAdHandler is not loaded yet. You must call .load() on your handler instance (e.g., rewardedInterstitialAdHandler.load()) before displaying the RewardedInterstitialAd composable." } rewardedInterstitialAd?.show(activity) { Log.d(tag, "A reward was earned") diff --git a/basic-ads/src/androidMain/kotlin/app/lexilabs/basic/ads/composable/RememberRewardedInterstitialAd.kt b/basic-ads/src/androidMain/kotlin/app/lexilabs/basic/ads/composable/RememberRewardedInterstitialAd.kt index 7f5de97..097a8c8 100644 --- a/basic-ads/src/androidMain/kotlin/app/lexilabs/basic/ads/composable/RememberRewardedInterstitialAd.kt +++ b/basic-ads/src/androidMain/kotlin/app/lexilabs/basic/ads/composable/RememberRewardedInterstitialAd.kt @@ -8,6 +8,7 @@ import androidx.compose.ui.platform.LocalContext import app.lexilabs.basic.ads.AdState import app.lexilabs.basic.ads.AdUnitId import app.lexilabs.basic.ads.DependsOnGoogleMobileAds +import app.lexilabs.basic.ads.RewardedAdHandler import app.lexilabs.basic.ads.RewardedInterstitialAdHandler import app.lexilabs.basic.ads.getActivity @@ -51,6 +52,46 @@ public actual fun rememberRewardedInterstitialAd( return ad } +/** + * Remembers a [RewardedAdHandler], which is used to load and show rewarded ads. + * + * This function will automatically attempt to load an ad when the [RewardedAdHandler.state] + * is [AdState.NONE] or [AdState.DISMISSED]. + * + * @param userId Used for Server-Side Verification + * @param customData Used for Server-Side Verification + * @param adUnitId The ad unit ID to use for loading the ad. Defaults to [AdUnitId.REWARDED_DEFAULT]. + * @param onLoad A callback that will be invoked when the ad has successfully loaded. + * @param onFailure A callback that will be invoked if the ad fails to load, providing an [Exception] with details of the failure. + * @return A [MutableState] holding the [RewardedAdHandler]. You can observe this state to react to changes in the ad's lifecycle. + */ +@DependsOnGoogleMobileAds +@Composable +public actual fun rememberRewardedInterstitialAd( + userId: String, + customData: String, + adUnitId: String, + onLoad: () -> Unit, + onFailure: (Exception) -> Unit +): MutableState { + val activity = LocalContext.current.getActivity() + val ad = remember(activity) { mutableStateOf(RewardedAdHandler(activity)) } + when(ad.value.state){ + AdState.DISMISSED, + AdState.NONE -> { + ad.value.load( + adUnitId = adUnitId, + userId = userId, + customData = customData, + onLoad = onLoad, + onFailure = onFailure + ) + } + else -> { /** DO NOTHING **/ } + } + return ad +} + /** * A composable function that remembers and manages a RewardedInterstitialAdHandler. * diff --git a/basic-ads/src/androidMain/kotlin/app/lexilabs/basic/ads/nativead/NativeAdHandler.kt b/basic-ads/src/androidMain/kotlin/app/lexilabs/basic/ads/nativead/NativeAdHandler.kt index a7a6a05..713da37 100644 --- a/basic-ads/src/androidMain/kotlin/app/lexilabs/basic/ads/nativead/NativeAdHandler.kt +++ b/basic-ads/src/androidMain/kotlin/app/lexilabs/basic/ads/nativead/NativeAdHandler.kt @@ -96,7 +96,7 @@ public actual class NativeAdHandler actual constructor(private val activity: Any @MainThread public actual fun render(): NativeAdData { require(ad?.android != null){ - "NativeAd has not loaded" + "The provided NativeAdHandler is not loaded yet. You must call .load() on your handler instance (e.g., nativeAdHandler.load()) before displaying the NativeAd composable." } return ad!! } diff --git a/basic-ads/src/commonMain/kotlin/app/lexilabs/basic/ads/RewardedAdHandler.kt b/basic-ads/src/commonMain/kotlin/app/lexilabs/basic/ads/RewardedAdHandler.kt index db9523f..1d158e9 100644 --- a/basic-ads/src/commonMain/kotlin/app/lexilabs/basic/ads/RewardedAdHandler.kt +++ b/basic-ads/src/commonMain/kotlin/app/lexilabs/basic/ads/RewardedAdHandler.kt @@ -47,10 +47,10 @@ public expect class RewardedAdHandler(activity: Any?) { public val state: AdState /** - * Loads an Rewarded Ad. + * Loads a Rewarded Ad. * Note: Make all calls to the Mobile Ads SDK on the main thread. * - * To load an Rewarded ad, call [RewardedAdHandler.load] method + * To load a Rewarded ad, call [RewardedAdHandler.load] method * and pass in an [AdUnitId] as a [String] to receive the loaded ad, the [onLoad] * callback, and any possible [Exception] from the [onFailure] callback. * @param adUnitId Your Rewarded Ad AdUnitId [String] from AdMob @@ -66,10 +66,10 @@ public expect class RewardedAdHandler(activity: Any?) { ) /** - * Loads an Rewarded Ad. + * Loads a Rewarded Ad. * Note: Make all calls to the Mobile Ads SDK on the main thread. * - * To load an Rewarded ad, call [RewardedAdHandler.load] method + * To load a Rewarded ad, call [RewardedAdHandler.load] method * and pass in an [AdUnitId] as a [String] to receive the loaded ad, the [onLoad] * callback, and any possible [Exception] from the [onFailure] callback. * @param adUnitId Your Rewarded Ad AdUnitId [String] from AdMob diff --git a/basic-ads/src/commonMain/kotlin/app/lexilabs/basic/ads/RewardedInterstitialAdHandler.kt b/basic-ads/src/commonMain/kotlin/app/lexilabs/basic/ads/RewardedInterstitialAdHandler.kt index 2b409c4..a6a01cd 100644 --- a/basic-ads/src/commonMain/kotlin/app/lexilabs/basic/ads/RewardedInterstitialAdHandler.kt +++ b/basic-ads/src/commonMain/kotlin/app/lexilabs/basic/ads/RewardedInterstitialAdHandler.kt @@ -46,10 +46,10 @@ public expect class RewardedInterstitialAdHandler(activity: Any?) { public val state: AdState /** - * Loads an RewardedInterstitial Ad. + * Loads a RewardedInterstitial Ad. * Note: Make all calls to the Mobile Ads SDK on the main thread. * - * To load an RewardedInterstitial ad, call [RewardedInterstitialAdHandler.load] method + * To load a RewardedInterstitial ad, call [RewardedInterstitialAdHandler.load] method * and pass in an [AdUnitId] as a [String] to receive the loaded ad, the [onLoad] * callback, and any possible [Exception] from the [onFailure] callback. * @param adUnitId Your RewardedInterstitial Ad AdUnitId [String] from AdMob @@ -64,6 +64,29 @@ public expect class RewardedInterstitialAdHandler(activity: Any?) { onFailure: (Exception) -> Unit ) + /** + * Loads a RewardedInterstitial Ad. + * Note: Make all calls to the Mobile Ads SDK on the main thread. + * + * To load a Rewarded ad, call [RewardedAdHandler.load] method + * and pass in an [AdUnitId] as a [String] to receive the loaded ad, the [onLoad] + * callback, and any possible [Exception] from the [onFailure] callback. + * @param adUnitId Your Rewarded Ad AdUnitId [String] from AdMob + * @param userId Used for Server-Side Verification + * @param customData Used for Server-Side Verification + * @param onLoad Callback after the ad loads + * @param onFailure Callback sharing the [Exception] when the ad fail to load + * @see [AdUnitId.autoSelect] + * @see [AdUnitId.REWARDED_INTERSTITIAL_DEFAULT] + */ + public fun load( + adUnitId: String = AdUnitId.REWARDED_INTERSTITIAL_DEFAULT, + userId: String, + customData: String, + onLoad: () -> Unit, + onFailure: (Exception) -> Unit + ) + /** * Sets the FullScreenContentCallback for the RewardedInterstitial Ad. * diff --git a/basic-ads/src/commonMain/kotlin/app/lexilabs/basic/ads/composable/RememberRewardedInterstitialAd.kt b/basic-ads/src/commonMain/kotlin/app/lexilabs/basic/ads/composable/RememberRewardedInterstitialAd.kt index 6b2ea3d..1c2dc64 100644 --- a/basic-ads/src/commonMain/kotlin/app/lexilabs/basic/ads/composable/RememberRewardedInterstitialAd.kt +++ b/basic-ads/src/commonMain/kotlin/app/lexilabs/basic/ads/composable/RememberRewardedInterstitialAd.kt @@ -2,8 +2,12 @@ package app.lexilabs.basic.ads.composable import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState +import app.lexilabs.basic.ads.AdState +import app.lexilabs.basic.ads.AdState.DISMISSED +import app.lexilabs.basic.ads.AdState.NONE import app.lexilabs.basic.ads.AdUnitId import app.lexilabs.basic.ads.DependsOnGoogleMobileAds +import app.lexilabs.basic.ads.RewardedAdHandler import app.lexilabs.basic.ads.RewardedInterstitialAdHandler /** @@ -31,6 +35,29 @@ public expect fun rememberRewardedInterstitialAd( onFailure: (Exception) -> Unit = {} ): MutableState +/** + * Remembers a [RewardedAdHandler], which is used to load and show rewarded ads. + * + * This function will automatically attempt to load an ad when the [RewardedAdHandler.state] + * is [AdState.NONE] or [AdState.DISMISSED]. + * + * @param userId Used for Server-Side Verification + * @param customData Used for Server-Side Verification + * @param adUnitId The ad unit ID to use for loading the ad. Defaults to [AdUnitId.REWARDED_DEFAULT]. + * @param onLoad A callback that will be invoked when the ad has successfully loaded. + * @param onFailure A callback that will be invoked if the ad fails to load, providing an [Exception] with details of the failure. + * @return A [MutableState] holding the [RewardedAdHandler]. You can observe this state to react to changes in the ad's lifecycle. + */ +@DependsOnGoogleMobileAds +@Composable +public expect fun rememberRewardedInterstitialAd( + userId: String, + customData: String, + adUnitId: String = AdUnitId.REWARDED_DEFAULT, + onLoad: () -> Unit = {}, + onFailure: (Exception) -> Unit = {} +): MutableState + /** * A composable function that remembers and manages a RewardedInterstitialAdHandler. * diff --git a/basic-ads/src/iosMain/kotlin/app/lexilabs/basic/ads/InterstitialAdHandler.kt b/basic-ads/src/iosMain/kotlin/app/lexilabs/basic/ads/InterstitialAdHandler.kt index 22aad63..1c4db7c 100644 --- a/basic-ads/src/iosMain/kotlin/app/lexilabs/basic/ads/InterstitialAdHandler.kt +++ b/basic-ads/src/iosMain/kotlin/app/lexilabs/basic/ads/InterstitialAdHandler.kt @@ -60,7 +60,7 @@ public actual class InterstitialAdHandler actual constructor(activity: Any?) { Log.d(tag, "setListeners:starting") require(interstitialAd != null) { _state.value = AdState.FAILING - "InterstitialAd not loaded yet. `InterstitialAd.load()` must be called first" + "The provided InterstitialAdHandler is not loaded yet. You must call .load() on your handler instance (e.g., interstitialAdHandler.load()) before displaying the InterstitialAd composable." } delegate = FullScreenContentDelegate( onClick = onClick, @@ -87,11 +87,11 @@ public actual class InterstitialAdHandler actual constructor(activity: Any?) { Log.d(tag, "show:starting") require(interstitialAd != null) { _state.value = AdState.FAILING - "InterstitialAd not loaded yet. `InterstitialAd.load()` must be called first" + "The provided InterstitialAdHandler is not loaded yet. You must call .load() on your handler instance (e.g., interstitialAdHandler.load()) before displaying the InterstitialAd composable." } require(delegate != null) { _state.value = AdState.FAILING - "InterstitialAd listeners not set yet. `InterstitialAd.setListeners()` must be called first" + "The provided InterstitialAdHandler listeners are not set yet. You must call .setListeners() on your handler instance (e.g., interstitialAdHandler.setListeners()) before displaying the InterstitialAd composable." } interstitialAd?.presentFromRootViewController(null) } @@ -102,11 +102,11 @@ public actual class InterstitialAdHandler actual constructor(activity: Any?) { Log.d(tag, "show:starting") require(interstitialAd != null) { _state.value = AdState.FAILING - "InterstitialAd not loaded yet. `InterstitialAd.load()` must be called first" + "The provided InterstitialAdHandler is not loaded yet. You must call .load() on your handler instance (e.g., interstitialAdHandler.load()) before displaying the InterstitialAd composable." } require(delegate != null) { _state.value = AdState.FAILING - "InterstitialAd listeners not set yet. `InterstitialAd.setListeners()` must be called first" + "The provided InterstitialAdHandler listeners are not set yet. You must call .setListeners() on your handler instance (e.g., interstitialAdHandler.setListeners()) before displaying the InterstitialAd composable." } interstitialAd?.presentFromRootViewController(viewController) } diff --git a/basic-ads/src/iosMain/kotlin/app/lexilabs/basic/ads/RewardedAdHandler.kt b/basic-ads/src/iosMain/kotlin/app/lexilabs/basic/ads/RewardedAdHandler.kt index 4602ad9..794db1e 100644 --- a/basic-ads/src/iosMain/kotlin/app/lexilabs/basic/ads/RewardedAdHandler.kt +++ b/basic-ads/src/iosMain/kotlin/app/lexilabs/basic/ads/RewardedAdHandler.kt @@ -88,7 +88,7 @@ public actual class RewardedAdHandler actual constructor(activity: Any?) { Log.d(tag, "setListeners:starting") require(rewardedAd != null) { _state.value = AdState.FAILING - "RewardedAd not loaded yet. `RewardedAd.load()` must be called first" + "The provided RewardedAdHandler is not loaded yet. You must call .load() on your handler instance (e.g., rewardedAdHandler.load()) before displaying the RewardedAd composable." } delegate = FullScreenContentDelegate( onClick = onClick, @@ -116,11 +116,11 @@ public actual class RewardedAdHandler actual constructor(activity: Any?) { Log.d(tag, "show:starting") require(rewardedAd != null) { _state.value = AdState.FAILING - "RewardedAd not loaded yet. `RewardedAd.load()` must be called first" + "The provided RewardedAdHandler is not loaded yet. You must call .load() on your handler instance (e.g., rewardedAdHandler.load()) before displaying the RewardedAd composable." } require(delegate != null) { _state.value = AdState.FAILING - "RewardedAd listeners not set yet. `RewardedAd.setListeners()` must be called first" + "The provided RewardedAdHandler listeners are not set yet. You must call .setListeners() on your handler instance (e.g., rewardedAdHandler.setListeners()) before displaying the RewardedAd composable." } rewardedAd?.let { ad -> ad.presentFromRootViewController( diff --git a/basic-ads/src/iosMain/kotlin/app/lexilabs/basic/ads/RewardedInterstitialAdHandler.kt b/basic-ads/src/iosMain/kotlin/app/lexilabs/basic/ads/RewardedInterstitialAdHandler.kt index f1d19aa..22f6043 100644 --- a/basic-ads/src/iosMain/kotlin/app/lexilabs/basic/ads/RewardedInterstitialAdHandler.kt +++ b/basic-ads/src/iosMain/kotlin/app/lexilabs/basic/ads/RewardedInterstitialAdHandler.kt @@ -6,6 +6,7 @@ import androidx.compose.runtime.mutableStateOf import app.lexilabs.basic.logging.Log import cocoapods.Google_Mobile_Ads_SDK.GADRequest import cocoapods.Google_Mobile_Ads_SDK.GADRewardedInterstitialAd +import cocoapods.Google_Mobile_Ads_SDK.GADServerSideVerificationOptions import kotlinx.cinterop.ExperimentalForeignApi import platform.Foundation.NSError @@ -45,6 +46,38 @@ public actual class RewardedInterstitialAdHandler actual constructor(activity: A ) } + public actual fun load( + adUnitId: String, + userId: String, + customData: String, + onLoad: () -> Unit, + onFailure: (Exception) -> Unit + ) { + _state.value = AdState.LOADING + Log.d(tag, "load:starting") + GADRewardedInterstitialAd.loadWithAdUnitID( + adUnitID = adUnitId, + request = GADRequest(), + completionHandler = { ad: GADRewardedInterstitialAd?, error: NSError? -> + ad?.let { + Log.d(tag, "load:success") + rewardedInterstitialAd = it + val options = GADServerSideVerificationOptions() + options.userIdentifier = userId + options.customRewardString = customData + it.serverSideVerificationOptions = options + _state.value = AdState.READY + onLoad() + } + error?.let { + Log.e(tag, "load:failure:$it") + _state.value = AdState.FAILING + onFailure(AdException()) + } + } + ) + } + public actual fun setListeners( onFailure: (Exception) -> Unit, onDismissed: () -> Unit, @@ -55,7 +88,7 @@ public actual class RewardedInterstitialAdHandler actual constructor(activity: A Log.d(tag, "setListeners:starting") require(rewardedInterstitialAd != null) { _state.value = AdState.FAILING - "RewardedAd not loaded yet. `RewardedAd.load()` must be called first" + "The provided RewardedInterstitialAdHandler is not loaded yet. You must call .load() on your handler instance (e.g., rewardedInterstitialAdHandler.load()) before displaying the RewardedInterstitialAd composable." } delegate = FullScreenContentDelegate( onClick = onClick, @@ -81,11 +114,11 @@ public actual class RewardedInterstitialAdHandler actual constructor(activity: A Log.d(tag, "show:starting") require(rewardedInterstitialAd != null) { _state.value = AdState.FAILING - "RewardedAd not loaded yet. `RewardedAd.load()` must be called first" + "The provided RewardedInterstitialAdHandler is not loaded yet. You must call .load() on your handler instance (e.g., rewardedInterstitialAdHandler.load()) before displaying the RewardedInterstitialAd composable." } require(delegate != null) { _state.value = AdState.FAILING - "RewardedAd listeners not set yet. `RewardedAd.setListeners()` must be called first" + "The provided RewardedInterstitialAdHandler listeners are not set yet. You must call .setListeners() on your handler instance (e.g., rewardedInterstitialAdHandler.setListeners()) before displaying the RewardedInterstitialAd composable." } rewardedInterstitialAd?.presentFromRootViewController( viewController = null, diff --git a/basic-ads/src/iosMain/kotlin/app/lexilabs/basic/ads/composable/RememberRewardedInterstitialAd.kt b/basic-ads/src/iosMain/kotlin/app/lexilabs/basic/ads/composable/RememberRewardedInterstitialAd.kt index f00e6ea..407623a 100644 --- a/basic-ads/src/iosMain/kotlin/app/lexilabs/basic/ads/composable/RememberRewardedInterstitialAd.kt +++ b/basic-ads/src/iosMain/kotlin/app/lexilabs/basic/ads/composable/RememberRewardedInterstitialAd.kt @@ -7,6 +7,7 @@ import androidx.compose.runtime.remember import app.lexilabs.basic.ads.AdState import app.lexilabs.basic.ads.AdUnitId import app.lexilabs.basic.ads.DependsOnGoogleMobileAds +import app.lexilabs.basic.ads.RewardedAdHandler import app.lexilabs.basic.ads.RewardedInterstitialAdHandler /** @@ -48,6 +49,45 @@ public actual fun rememberRewardedInterstitialAd( return ad } +/** + * Remembers a [RewardedAdHandler], which is used to load and show rewarded ads. + * + * This function will automatically attempt to load an ad when the [RewardedAdHandler.state] + * is [AdState.NONE] or [AdState.DISMISSED]. + * + * @param userId Used for Server-Side Verification + * @param customData Used for Server-Side Verification + * @param adUnitId The ad unit ID to use for loading the ad. Defaults to [AdUnitId.REWARDED_DEFAULT]. + * @param onLoad A callback that will be invoked when the ad has successfully loaded. + * @param onFailure A callback that will be invoked if the ad fails to load, providing an [Exception] with details of the failure. + * @return A [MutableState] holding the [RewardedAdHandler]. You can observe this state to react to changes in the ad's lifecycle. + */ +@DependsOnGoogleMobileAds +@Composable +public actual fun rememberRewardedInterstitialAd( + userId: String, + customData: String, + adUnitId: String, + onLoad: () -> Unit, + onFailure: (Exception) -> Unit +): MutableState { + val ad = remember(null) { mutableStateOf(RewardedAdHandler(null)) } + when(ad.value.state){ + AdState.DISMISSED, + AdState.NONE -> { + ad.value.load( + adUnitId = adUnitId, + userId = userId, + customData = customData, + onLoad = onLoad, + onFailure = onFailure + ) + } + else -> { /** DO NOTHING **/ } + } + return ad +} + /** * A composable function that remembers and manages a RewardedInterstitialAdHandler. * diff --git a/basic-ads/src/iosMain/kotlin/app/lexilabs/basic/ads/nativead/NativeAdHandler.kt b/basic-ads/src/iosMain/kotlin/app/lexilabs/basic/ads/nativead/NativeAdHandler.kt index a81101c..4b178f6 100644 --- a/basic-ads/src/iosMain/kotlin/app/lexilabs/basic/ads/nativead/NativeAdHandler.kt +++ b/basic-ads/src/iosMain/kotlin/app/lexilabs/basic/ads/nativead/NativeAdHandler.kt @@ -74,7 +74,7 @@ public actual class NativeAdHandler actual constructor(activity: Any?) { @MainThread public actual fun render(): NativeAdData { require(adLoader?.nativeAd != null) { - "NativeAd is null" + "The provided NativeAdHandler is not loaded yet. You must call .load() on your handler instance (e.g., nativeAdHandler.load()) before displaying the NativeAd composable." } return NativeAdData(adLoader!!.nativeAd!!) } diff --git a/gradle.properties b/gradle.properties index 9fe1239..4583525 100644 --- a/gradle.properties +++ b/gradle.properties @@ -8,8 +8,6 @@ org.gradle.parallel=true #Kotlin kotlin.code.style=official -kotlin.native.cacheKind.iosArm64=none -kotlin.native.cacheKind.iosSimulatorArm64=none #Android android.useAndroidX=true diff --git a/gradle/gradle-daemon-jvm.properties b/gradle/gradle-daemon-jvm.properties new file mode 100644 index 0000000..5c34300 --- /dev/null +++ b/gradle/gradle-daemon-jvm.properties @@ -0,0 +1,13 @@ +#This file is generated by updateDaemonJvm +toolchainUrl.FREE_BSD.AARCH64=https\://api.foojay.io/disco/v3.0/ids/56a19bc915b9ba2eb62ba7554c61b919/redirect +toolchainUrl.FREE_BSD.X86_64=https\://api.foojay.io/disco/v3.0/ids/398ffe3949748bfb1d5636f023d228fd/redirect +toolchainUrl.LINUX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/56a19bc915b9ba2eb62ba7554c61b919/redirect +toolchainUrl.LINUX.X86_64=https\://api.foojay.io/disco/v3.0/ids/398ffe3949748bfb1d5636f023d228fd/redirect +toolchainUrl.MAC_OS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/e99bae143b75f9a10ead10248f02055e/redirect +toolchainUrl.MAC_OS.X86_64=https\://api.foojay.io/disco/v3.0/ids/04e088f8677de3b384108493cc9481d0/redirect +toolchainUrl.UNIX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/56a19bc915b9ba2eb62ba7554c61b919/redirect +toolchainUrl.UNIX.X86_64=https\://api.foojay.io/disco/v3.0/ids/398ffe3949748bfb1d5636f023d228fd/redirect +toolchainUrl.WINDOWS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/e55dccbfe27cb97945148c61a39c89c5/redirect +toolchainUrl.WINDOWS.X86_64=https\://api.foojay.io/disco/v3.0/ids/dbd05c4936d573642f94cd149e1356c8/redirect +toolchainVendor=JETBRAINS +toolchainVersion=21 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 31a5590..790636c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,23 +1,23 @@ [versions] # BUILD INFO -ads = "1.1.1" -build-sdk-compile = "36" +ads = "1.2.0-beta01" +build-sdk-compile = "37" build-sdk-min = "24" -build-sdk-target = "36" +build-sdk-target = "37" build-ios-target-deployment = "13.0" -kotlin = "2.3.0" -agp = "8.13.2" +kotlin = "2.3.21" +agp = "9.2.1" # COCOAPODS DEPENDENCIES -cocoapods-admob = "12.14.0" +cocoapods-admob = "13.3.0" cocoapods-ump = "3.1.0" # DEPENDENCIES bcv = "0.18.1" -dokka = "2.1.0" #"2.0.0" -compose = "1.10.0" -google-play-services-ads = "24.9.0" -android-core = "1.17.0" -annotations = "1.9.1" -kover = "0.9.4" +dokka = "2.2.0" +compose = "1.10.3" +google-play-services-ads = "25.2.0" +android-core = "1.18.0" +annotations = "1.9.1" # DO NOT UPDATE. Version 1.10.0 removes iosX64 build +kover = "0.9.8" logging = "0.2.6" android-ump = "4.0.0" maven-publish = "0.36.0" @@ -54,6 +54,6 @@ kotlinx-binary-compatibility-validator = { id = "org.jetbrains.kotlinx.binary-co kotlinx-serialization-plugin = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin"} native-cocoapods = { id = "org.jetbrains.kotlin.native.cocoapods", version.ref = "kotlin" } dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" } -composeCompiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } +composeCompiler = { id = "org.jetbrains.kotlin.plugin.compose", version = "2.3.21" } kover = { id = "org.jetbrains.kotlinx.kover", version.ref = "kover" } maven-publish = { id = "com.vanniktech.maven.publish", version.ref = "maven-publish"} \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 9bbc975..b1b8ef5 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 37f853b..b52fb7e 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,9 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.0-bin.zip networkTimeout=10000 +retries=0 +retryBackOffMs=500 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index faf9300..b9bb139 100755 --- a/gradlew +++ b/gradlew @@ -1,7 +1,7 @@ #!/bin/sh # -# Copyright © 2015-2021 the original authors. +# Copyright © 2015 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -57,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -114,7 +114,6 @@ case "$( uname )" in #( NONSTOP* ) nonstop=true ;; esac -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. @@ -172,7 +171,6 @@ fi # For Cygwin or MSYS, switch paths to Windows format before running java if "$cygwin" || "$msys" ; then APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) - CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) JAVACMD=$( cygpath --unix "$JAVACMD" ) @@ -212,8 +210,7 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ "$@" # Stop when "xargs" is not available. diff --git a/gradlew.bat b/gradlew.bat index 9d21a21..24c62d5 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -23,8 +23,8 @@ @rem @rem ########################################################################## -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal +@rem Set local scope for the variables, and ensure extensions are enabled +setlocal EnableExtensions set DIRNAME=%~dp0 if "%DIRNAME%"=="" set DIRNAME=. @@ -51,7 +51,7 @@ echo. 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo location of your Java installation. 1>&2 -goto fail +"%COMSPEC%" /c exit 1 :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% @@ -65,30 +65,18 @@ echo. 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo location of your Java installation. 1>&2 -goto fail +"%COMSPEC%" /c exit 1 :execute @rem Setup the command line -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* +@rem endlocal doesn't take effect until after the line is parsed and variables are expanded +@rem which allows us to clear the local environment before executing the java command +endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel -:end -@rem End local scope for the variables with windows NT shell -if %ERRORLEVEL% equ 0 goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -set EXIT_CODE=%ERRORLEVEL% -if %EXIT_CODE% equ 0 set EXIT_CODE=1 -if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% -exit /b %EXIT_CODE% - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +:exitWithErrorLevel +@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts +"%COMSPEC%" /c exit %ERRORLEVEL% diff --git a/settings.gradle.kts b/settings.gradle.kts index bd243c7..77738d8 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -9,6 +9,10 @@ pluginManagement { } } +plugins { + id("org.gradle.toolchains.foojay-resolver-convention") version("1.0.0") +} + dependencyResolutionManagement { @Suppress("UnstableApiUsage") repositories { @@ -16,4 +20,3 @@ dependencyResolutionManagement { mavenCentral() } } -