diff --git a/README.md b/README.md index dea1fb0e..94876434 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,4 @@ +


AdMob

@capacitor-community/admob

@@ -188,7 +189,46 @@ const consentInfo = await AdMob.requestConsentInfo({ 2. AdMob.requestConsentInfo 3. AdMob.showConsentForm (If consent form required ) 3/ AdMob.showBanner + +### Show App Open Ad + +```ts +import { + AdMob, + AppOpenAdPluginEvents, + AppOpenAdOptions, +} from '@capacitor-community/admob'; + +export async function showAppOpenAd(): Promise { + // listen to events + AdMob.addListener(AppOpenAdPluginEvents.Loaded, () => { + console.log('App Open Ad loaded'); + }); + AdMob.addListener(AppOpenAdPluginEvents.FailedToLoad, () => { + console.log('Failed to load App Open Ad'); + }); + AdMob.addListener(AppOpenAdPluginEvents.Opened, () => { + console.log('App Open Ad open'); + }); + AdMob.addListener(AppOpenAdPluginEvents.Closed, () => { + console.log('App Open Ad close'); + }); + AdMob.addListener(AppOpenAdPluginEvents.FailedToShow, () => { + console.log('Failed to load App Open Ad'); + }); + const options: AppOpenAdOptions = { + adUnitId: 'TU_AD_UNIT_ID', + showOnColdStart: true, // Opcional + showOnForeground: true, // Opcional + }; + await AdMob.loadAppOpen(options); + const { value } = await AdMob.isAppOpenLoaded(); + if (value) { + await AdMob.showAppOpen(); + } +} +``` ### Show Banner ```ts @@ -371,6 +411,10 @@ AdMob.addListener(RewardAdPluginEvents.Rewarded, async () => { * [`addListener(RewardInterstitialAdPluginEvents.Dismissed, ...)`](#addlistenerrewardinterstitialadplugineventsdismissed-) * [`addListener(RewardInterstitialAdPluginEvents.FailedToShow, ...)`](#addlistenerrewardinterstitialadplugineventsfailedtoshow-) * [`addListener(RewardInterstitialAdPluginEvents.Showed, ...)`](#addlistenerrewardinterstitialadplugineventsshowed-) +* [`loadAppOpen(...)`](#loadappopen) +* [`showAppOpen()`](#showappopen) +* [`isAppOpenLoaded()`](#isappopenloaded) +* [`addListener(AppOpenAdPluginEvents, ...)`](#addlistenerappopenadpluginevents-) * [Interfaces](#interfaces) * [Type Aliases](#type-aliases) * [Enums](#enums) @@ -1067,6 +1111,63 @@ addListener(eventName: RewardInterstitialAdPluginEvents.Showed, listenerFunc: () -------------------- +### loadAppOpen(...) + +```typescript +loadAppOpen(options: AppOpenAdOptions) => Promise +``` + +Carga un anuncio App Open + +| Param | Type | +| ------------- | ------------------------------------------------------------- | +| **`options`** | AppOpenAdOptions | + +-------------------- + + +### showAppOpen() + +```typescript +showAppOpen() => Promise +``` + +Shows the App Open ad if loaded + +-------------------- + + +### isAppOpenLoaded() + +```typescript +isAppOpenLoaded() => Promise<{ value: boolean; }> +``` + +Check if the App Open ad is loaded + +**Returns:** Promise<{ value: boolean; }> + +-------------------- + + +### addListener(AppOpenAdPluginEvents, ...) + +```typescript +addListener(eventName: AppOpenAdPluginEvents, listenerFunc: (...args: any[]) => void) => Promise +``` + +Add listeners for App Open events + +| Param | Type | +| ------------------ | ----------------------------------------------------------------------- | +| **`eventName`** | AppOpenAdPluginEvents | +| **`listenerFunc`** | (...args: any[]) => void | + +**Returns:** Promise<PluginListenerHandle> + +-------------------- + + ### Interfaces @@ -1136,7 +1237,7 @@ When notice listener of OnAdLoaded, you can get banner size. #### AdMobError -For more information +For more information https://developers.google.com/android/reference/com/google/android/gms/ads/AdError | Prop | Type | Description | @@ -1196,7 +1297,7 @@ https://developers.google.com/android/reference/com/google/android/gms/ads/AdErr #### AdMobRewardItem -For more information +For more information https://developers.google.com/admob/android/rewarded-video-adapters?hl=en | Prop | Type | Description | @@ -1228,6 +1329,15 @@ https://developers.google.com/admob/android/rewarded-video-adapters?hl=en | **`amount`** | number | Rewarded amount user got | +#### AppOpenAdOptions + +| Prop | Type | +| ---------------------- | -------------------- | +| **`adUnitId`** | string | +| **`showOnColdStart`** | boolean | +| **`showOnForeground`** | boolean | + + ### Type Aliases @@ -1240,7 +1350,7 @@ https://developers.google.com/admob/android/rewarded-video-adapters?hl=en From T, pick a set of properties whose keys are in the union K -{ [P in K]: T[P]; } +{ [P in K]: T[P] } ### Enums @@ -1354,6 +1464,17 @@ From T, pick a set of properties whose keys are in the union K | **`Dismissed`** | 'onRewardedInterstitialAdDismissed' | Emits when the AdReward video is not visible to the user anymore. **Important**: This has nothing to do with the reward it self. This event will emits in this two cases: 1. The user starts the video ad but close it before the reward emit. 2. The user start the video and see it until end, then gets the reward and after that the ad is closed. | | **`Rewarded`** | 'onRewardedInterstitialAdReward' | Emits when user get rewarded from AdReward | + +#### AppOpenAdPluginEvents + +| Members | Value | +| ------------------ | ------------------------------------ | +| **`Loaded`** | 'appOpenAdLoaded' | +| **`FailedToLoad`** | 'appOpenAdFailedToLoad' | +| **`Opened`** | 'appOpenAdOpened' | +| **`Closed`** | 'appOpenAdClosed' | +| **`FailedToShow`** | 'appOpenAdFailedToShow' | + ## TROUBLE SHOOTING diff --git a/android/src/main/java/com/getcapacitor/community/admob/AdMob.java b/android/src/main/java/com/getcapacitor/community/admob/AdMob.java index b8b36c79..1ade459e 100644 --- a/android/src/main/java/com/getcapacitor/community/admob/AdMob.java +++ b/android/src/main/java/com/getcapacitor/community/admob/AdMob.java @@ -8,6 +8,7 @@ import com.getcapacitor.PluginMethod; import com.getcapacitor.annotation.CapacitorPlugin; import com.getcapacitor.annotation.Permission; + import com.getcapacitor.community.admob.banner.BannerExecutor; import com.getcapacitor.community.admob.consent.AdConsentExecutor; import com.getcapacitor.community.admob.helpers.AuthorizationStatusEnum; @@ -15,14 +16,19 @@ import com.getcapacitor.community.admob.interstitial.InterstitialAdCallbackAndListeners; import com.getcapacitor.community.admob.rewarded.AdRewardExecutor; import com.getcapacitor.community.admob.rewardedinterstitial.AdRewardInterstitialExecutor; +import com.getcapacitor.community.admob.appopen.AppOpenAdPlugin; + import com.google.android.gms.ads.MobileAds; import com.google.android.gms.ads.RequestConfiguration; import com.google.android.gms.ads.initialization.InitializationStatus; import com.google.android.gms.ads.initialization.OnInitializationCompleteListener; + import org.json.JSONException; @CapacitorPlugin( - permissions = { @Permission(alias = "network", strings = { Manifest.permission.ACCESS_NETWORK_STATE, Manifest.permission.INTERNET }) } + permissions = { + @Permission(alias = "network", strings = { Manifest.permission.ACCESS_NETWORK_STATE, Manifest.permission.INTERNET }) + } ) public class AdMob extends Plugin { @@ -34,18 +40,21 @@ public class AdMob extends Plugin { this::notifyListeners, getLogTag() ); + private final AdRewardExecutor adRewardExecutor = new AdRewardExecutor( this::getContext, this::getActivity, this::notifyListeners, getLogTag() ); + private final AdRewardInterstitialExecutor adRewardInterstitialExecutor = new AdRewardInterstitialExecutor( this::getContext, this::getActivity, this::notifyListeners, getLogTag() ); + private final AdInterstitialExecutor adInterstitialExecutor = new AdInterstitialExecutor( this::getContext, this::getActivity, @@ -61,7 +70,27 @@ public class AdMob extends Plugin { getLogTag() ); - // Initialize AdMob with appId + private final AppOpenAdPlugin appOpenAdPlugin = new AppOpenAdPlugin(); + + @PluginMethod + public void loadAppOpen(final PluginCall call) { + appOpenAdPlugin.loadAppOpen(call); + } + + @PluginMethod + public void showAppOpen(final PluginCall call) { + appOpenAdPlugin.showAppOpen(call); + } + + @PluginMethod + public void isAppOpenLoaded(final PluginCall call) { + appOpenAdPlugin.isAppOpenLoaded(call); + } + + // --------------------------------------------------------- + // MAIN METHODS + // --------------------------------------------------------- + @PluginMethod public void initialize(final PluginCall call) { this.setRequestConfiguration(call); @@ -93,7 +122,10 @@ public void trackingAuthorizationStatus(final PluginCall call) { call.resolve(response); } - // User Consent + // --------------------------------------------------------- + // USER CONSENT + // --------------------------------------------------------- + @PluginMethod public void requestConsentInfo(final PluginCall call) { adConsentExecutor.requestConsentInfo(call, this::notifyListeners); @@ -114,6 +146,10 @@ public void resetConsentInfo(final PluginCall call) { adConsentExecutor.resetConsentInfo(call, this::notifyListeners); } + // --------------------------------------------------------- + // APP SETTINGS + // --------------------------------------------------------- + @PluginMethod public void setApplicationMuted(final PluginCall call) { Boolean muted = call.getBoolean("muted"); @@ -136,41 +172,48 @@ public void setApplicationVolume(final PluginCall call) { call.resolve(); } - // Show a banner Ad + // --------------------------------------------------------- + // BANNER ADS + // --------------------------------------------------------- + @PluginMethod public void showBanner(final PluginCall call) { bannerExecutor.showBanner(call); } - // Hide the banner, remove it from screen, but can show it later @PluginMethod public void hideBanner(final PluginCall call) { bannerExecutor.hideBanner(call); } - // Resume the banner, show it after hide @PluginMethod public void resumeBanner(final PluginCall call) { bannerExecutor.resumeBanner(call); } - // Destroy the banner, remove it from screen. @PluginMethod public void removeBanner(final PluginCall call) { bannerExecutor.removeBanner(call); } + // --------------------------------------------------------- + // INTERSTITIAL ADS + // --------------------------------------------------------- + @PluginMethod public void prepareInterstitial(final PluginCall call) { adInterstitialExecutor.prepareInterstitial(call, this::notifyListeners); } - // Show interstitial Ad @PluginMethod public void showInterstitial(final PluginCall call) { adInterstitialExecutor.showInterstitial(call, this::notifyListeners); } + // --------------------------------------------------------- + // REWARDED ADS + // --------------------------------------------------------- + @PluginMethod public void prepareRewardVideoAd(final PluginCall call) { adRewardExecutor.prepareRewardVideoAd(call, this::notifyListeners); @@ -191,10 +234,10 @@ public void showRewardInterstitialAd(final PluginCall call) { adRewardInterstitialExecutor.showRewardInterstitialAd(call, this::notifyListeners); } - /** - * @see Test Devices - * @see Target Settings - */ + // --------------------------------------------------------- + // REQUEST CONFIGURATION + // --------------------------------------------------------- + private void setRequestConfiguration(final PluginCall call) { // Testing Devices final boolean initializeForTesting = call.getBoolean("initializeForTesting", false); diff --git a/android/src/main/java/com/getcapacitor/community/admob/appopen/AppOpenAdManager.java b/android/src/main/java/com/getcapacitor/community/admob/appopen/AppOpenAdManager.java new file mode 100644 index 00000000..d897292d --- /dev/null +++ b/android/src/main/java/com/getcapacitor/community/admob/appopen/AppOpenAdManager.java @@ -0,0 +1,94 @@ +package com.getcapacitor.community.admob.appopen; + +import android.app.Activity; +import android.content.Context; +import androidx.annotation.NonNull; +import com.google.android.gms.ads.appopen.AppOpenAd; +import com.google.android.gms.ads.AdRequest; +import com.google.android.gms.ads.LoadAdError; +import com.google.android.gms.ads.FullScreenContentCallback; + +public class AppOpenAdManager { + private AppOpenAd appOpenAd = null; + private boolean isLoadingAd = false; + private boolean isShowingAd = false; + private String adUnitId; + + public AppOpenAdManager(String adUnitId) { + this.adUnitId = adUnitId; + } + + public void loadAd(Context context, final Runnable onLoaded, final Runnable onFailed) { + if (isLoadingAd || appOpenAd != null) { + return; + } + + isLoadingAd = true; + AdRequest request = new AdRequest.Builder().build(); + + AppOpenAd.load( + context, + adUnitId, + request, + AppOpenAd.APP_OPEN_AD_ORIENTATION_PORTRAIT, + new AppOpenAd.AppOpenAdLoadCallback() { + @Override + public void onAdLoaded(@NonNull AppOpenAd ad) { + appOpenAd = ad; + isLoadingAd = false; + + if (onLoaded != null) { + onLoaded.run(); + } + } + + @Override + public void onAdFailedToLoad(@NonNull LoadAdError loadAdError) { + isLoadingAd = false; + + if (onFailed != null) { + onFailed.run(); + } + } + } + ); + } + + public void showAdIfAvailable(Activity activity, final Runnable onClosed, final Runnable onFailedToShow) { + if (appOpenAd == null || isShowingAd) { + if (onFailedToShow != null) { + onFailedToShow.run(); + } + return; + } + + isShowingAd = true; + appOpenAd.setFullScreenContentCallback(new FullScreenContentCallback() { + @Override + public void onAdDismissedFullScreenContent() { + appOpenAd = null; + isShowingAd = false; + + if (onClosed != null) { + onClosed.run(); + } + } + + @Override + public void onAdFailedToShowFullScreenContent(com.google.android.gms.ads.AdError adError) { + appOpenAd = null; + isShowingAd = false; + + if (onFailedToShow != null) { + onFailedToShow.run(); + } + } + }); + + appOpenAd.show(activity); + } + + public boolean isAdLoaded() { + return appOpenAd != null; + } +} diff --git a/android/src/main/java/com/getcapacitor/community/admob/appopen/AppOpenAdPlugin.java b/android/src/main/java/com/getcapacitor/community/admob/appopen/AppOpenAdPlugin.java new file mode 100644 index 00000000..551aaf38 --- /dev/null +++ b/android/src/main/java/com/getcapacitor/community/admob/appopen/AppOpenAdPlugin.java @@ -0,0 +1,59 @@ +package com.getcapacitor.community.admob.appopen; + +import android.app.Activity; +import android.content.Context; +import com.getcapacitor.PluginCall; +import com.getcapacitor.PluginMethod; +import com.getcapacitor.annotation.CapacitorPlugin; +import com.getcapacitor.Plugin; +import org.json.JSONException; +import org.json.JSONObject; + +@CapacitorPlugin(name = "AppOpenAd") +public class AppOpenAdPlugin extends Plugin { + private AppOpenAdManager appOpenAdManager; + + @PluginMethod + public void loadAppOpen(PluginCall call) { + String adUnitId = call.getString("adUnitId"); + if (adUnitId == null) { + call.reject("adUnitId is required"); + return; + } + if (appOpenAdManager == null) { + appOpenAdManager = new AppOpenAdManager(adUnitId); + } + Context context = getContext(); + appOpenAdManager.loadAd(context, () -> { + notifyListeners("appOpenAdLoaded", new JSONObject()); + call.resolve(); + }, () -> { + notifyListeners("appOpenAdFailedToLoad", new JSONObject()); + call.reject("Failed to load App Open Ad"); + }); + } + + @PluginMethod + public void showAppOpen(PluginCall call) { + Activity activity = getActivity(); + appOpenAdManager.showAdIfAvailable(activity, () -> { + notifyListeners("appOpenAdClosed", new JSONObject()); + call.resolve(); + }, () -> { + notifyListeners("appOpenAdFailedToShow", new JSONObject()); + call.reject("Failed to show App Open Ad"); + }); + } + + @PluginMethod + public void isAppOpenLoaded(PluginCall call) { + boolean loaded = appOpenAdManager != null && appOpenAdManager.isAdLoaded(); + try { + JSONObject result = new JSONObject(); + result.put("value", loaded); + call.resolve(result); + } catch (JSONException e) { + call.reject("JSON error"); + } + } +} diff --git a/android/src/test/java/com/getcapacitor/community/admob/models/AdOptionsTest.java b/android/src/test/java/com/getcapacitor/community/admob/models/AdOptionsTest.java index 197ba426..e6858647 100644 --- a/android/src/test/java/com/getcapacitor/community/admob/models/AdOptionsTest.java +++ b/android/src/test/java/com/getcapacitor/community/admob/models/AdOptionsTest.java @@ -114,6 +114,84 @@ public void ssv() { final AdOptions adOptions = AdOptions.getFactory().createGenericOptions(pluginCallMock, ""); + verify(pluginCallMock, atLeastOnce()).getObject(wantedProperty); + assertEquals(userId, adOptions.ssvInfo.getUserId()); + assertEquals(customData, adOptions.ssvInfo.getCustomData()); + } + @Test + public void appOpen_ad_Id() { + final String expected = "Some Given AppOpen Test Id"; + when(pluginCallMock.getString(eq("adId"), anyString())).thenReturn(expected); + + final AdOptions adOptions = AdOptions.getFactory().createAppOpenOptions(pluginCallMock); + + assertEquals(expected, adOptions.adId); + } + + @Test + public void appOpen_position() { + final String wantedProperty = "position"; + final String expected = "TOP_CENTER"; + final String defaultValue = "BOTTOM_CENTER"; + when(pluginCallMock.getString(eq(wantedProperty), anyString())).thenReturn(expected); + + final AdOptions adOptions = AdOptions.getFactory().createAppOpenOptions(pluginCallMock); + + verify(pluginCallMock).getString(wantedProperty, defaultValue); + assertEquals(expected, adOptions.position); + } + + @Test + public void appOpen_margin() { + final String wantedProperty = "margin"; + final int expected = 10; + final int defaultValue = 0; + when(pluginCallMock.getInt(eq(wantedProperty), anyInt())).thenReturn(expected); + + final AdOptions adOptions = AdOptions.getFactory().createAppOpenOptions(pluginCallMock); + + verify(pluginCallMock).getInt(wantedProperty, defaultValue); + assertEquals(expected, adOptions.margin); + } + + @Test + public void appOpen_isTesting() { + final String wantedProperty = "isTesting"; + final boolean expected = true; + final boolean defaultValue = false; + when(pluginCallMock.getBoolean(eq(wantedProperty), anyBoolean())).thenReturn(expected); + + final AdOptions adOptions = AdOptions.getFactory().createAppOpenOptions(pluginCallMock); + + verify(pluginCallMock).getBoolean(wantedProperty, defaultValue); + assertEquals(expected, adOptions.isTesting); + } + + @Test + public void appOpen_npa() { + final String wantedProperty = "npa"; + final boolean expected = true; + final boolean defaultValue = false; + lenient().when(pluginCallMock.getBoolean(eq(wantedProperty), anyBoolean())).thenReturn(expected); + + final AdOptions adOptions = AdOptions.getFactory().createAppOpenOptions(pluginCallMock); + + verify(pluginCallMock).getBoolean(wantedProperty, defaultValue); + assertEquals(expected, adOptions.npa); + } + + @Test + public void appOpen_ssv() { + final String customData = "customData"; + final String userId = "userId"; + final String wantedProperty = "ssv"; + final JSObject expected = new JSObject(); + expected.put(customData, customData); + expected.put(userId, userId); + lenient().when(pluginCallMock.getObject(eq(wantedProperty))).thenReturn(expected); + + final AdOptions adOptions = AdOptions.getFactory().createAppOpenOptions(pluginCallMock); + verify(pluginCallMock, atLeastOnce()).getObject(wantedProperty); assertEquals(userId, adOptions.ssvInfo.getUserId()); assertEquals(customData, adOptions.ssvInfo.getCustomData()); diff --git a/demo/angular/src/app/app.component.ts b/demo/angular/src/app/app.component.ts index 2980fb71..df84bf0a 100644 --- a/demo/angular/src/app/app.component.ts +++ b/demo/angular/src/app/app.component.ts @@ -2,7 +2,7 @@ import { Component } from '@angular/core'; import { IonApp, IonRouterOutlet, Platform } from '@ionic/angular/standalone'; -import { AdMob } from '@capacitor-community/admob'; +import { AdMob, AppOpenAdPluginEvents, AppOpenAdOptions } from '@capacitor-community/admob'; @Component({ selector: 'app-root', @@ -32,6 +32,39 @@ export class AppComponent { AdMob.setApplicationVolume({ volume: 0.5, }); + + // example of App Open Ad + this.showAppOpenAd(); + }); + } + + async showAppOpenAd() { + // Listen to events + AdMob.addListener(AppOpenAdPluginEvents.Loaded, () => { + console.log('App Open Ad loaded'); + }); + AdMob.addListener(AppOpenAdPluginEvents.FailedToLoad, () => { + console.log('Failed to load App Open Ad'); }); + AdMob.addListener(AppOpenAdPluginEvents.Opened, () => { + console.log('App Open Ad open'); + }); + AdMob.addListener(AppOpenAdPluginEvents.Closed, () => { + console.log('App Open Ad close'); + }); + AdMob.addListener(AppOpenAdPluginEvents.FailedToShow, () => { + console.log('Failed to load App Open Ad'); + }); + + const options: AppOpenAdOptions = { + adUnitId: 'TU_AD_UNIT_ID', // Replace with your real ID + showOnColdStart: true, + showOnForeground: true, + }; + await AdMob.loadAppOpen(options); + const { value } = await AdMob.isAppOpenLoaded(); + if (value) { + await AdMob.showAppOpen(); + } } } diff --git a/ios/Sources/AdMobPlugin/AdMobPlugin.swift b/ios/Sources/AdMobPlugin/AdMobPlugin.swift index 529fbaac..a71a362d 100644 --- a/ios/Sources/AdMobPlugin/AdMobPlugin.swift +++ b/ios/Sources/AdMobPlugin/AdMobPlugin.swift @@ -28,8 +28,23 @@ public class AdMobPlugin: CAPPlugin, CAPBridgedPlugin { CAPPluginMethod(name: "prepareRewardVideoAd", returnType: CAPPluginReturnPromise), CAPPluginMethod(name: "showRewardVideoAd", returnType: CAPPluginReturnPromise), CAPPluginMethod(name: "prepareRewardInterstitialAd", returnType: CAPPluginReturnPromise), - CAPPluginMethod(name: "showRewardInterstitialAd", returnType: CAPPluginReturnPromise) + CAPPluginMethod(name: "showRewardInterstitialAd", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "loadAppOpen", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "showAppOpen", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "isAppOpenLoaded", returnType: CAPPluginReturnPromise) ] + private let appOpenAdPlugin = AppOpenAdPlugin() + @objc func loadAppOpen(_ call: CAPPluginCall) { + appOpenAdPlugin.loadAppOpen(call) + } + + @objc func showAppOpen(_ call: CAPPluginCall) { + appOpenAdPlugin.showAppOpen(call) + } + + @objc func isAppOpenLoaded(_ call: CAPPluginCall) { + appOpenAdPlugin.isAppOpenLoaded(call) + } var testingDevices: [String] = [] diff --git a/ios/Sources/AdMobPlugin/AppOpen/AppOpenAdManager.swift b/ios/Sources/AdMobPlugin/AppOpen/AppOpenAdManager.swift new file mode 100644 index 00000000..9c2302e7 --- /dev/null +++ b/ios/Sources/AdMobPlugin/AppOpen/AppOpenAdManager.swift @@ -0,0 +1,66 @@ +import Foundation +import GoogleMobileAds +import UIKit + +@objc public class AppOpenAdManager: NSObject { + private var appOpenAd: GADAppOpenAd? + private var isLoadingAd = false + private var isShowingAd = false + private var adUnitId: String + + public init(adUnitId: String) { + self.adUnitId = adUnitId + } + + public func loadAd(rootViewController: UIViewController, onLoaded: @escaping () -> Void, onFailed: @escaping () -> Void) { + if isLoadingAd || appOpenAd != nil { + return + } + + isLoadingAd = true + GADAppOpenAd.load(withAdUnitID: adUnitId, request: GADRequest(), orientation: .portrait) { [weak self] ad, error in + self?.isLoadingAd = false + + if let ad = ad { + self?.appOpenAd = ad + onLoaded() + } else { + onFailed() + } + } + } + + public func showAdIfAvailable(rootViewController: UIViewController, onClosed: @escaping () -> Void, onFailedToShow: @escaping () -> Void) { + guard let ad = appOpenAd, !isShowingAd else { + onFailedToShow() + return + } + + isShowingAd = true + ad.fullScreenContentDelegate = self + ad.present(fromRootViewController: rootViewController) + self.onClosed = onClosed + self.onFailedToShow = onFailedToShow + } + + public func isAdLoaded() -> Bool { + return appOpenAd != nil + } + + private var onClosed: (() -> Void)? + private var onFailedToShow: (() -> Void)? +} + +extension AppOpenAdManager: GADFullScreenContentDelegate { + public func adDidDismissFullScreenContent(_ ad: GADFullScreenPresentingAd) { + appOpenAd = nil + isShowingAd = false + onClosed?() + } + + public func ad(_ ad: GADFullScreenPresentingAd, didFailToPresentFullScreenContentWithError error: Error) { + appOpenAd = nil + isShowingAd = false + onFailedToShow?() + } +} diff --git a/ios/Sources/AdMobPlugin/AppOpen/AppOpenAdPlugin.swift b/ios/Sources/AdMobPlugin/AppOpen/AppOpenAdPlugin.swift new file mode 100644 index 00000000..05bb2fd4 --- /dev/null +++ b/ios/Sources/AdMobPlugin/AppOpen/AppOpenAdPlugin.swift @@ -0,0 +1,52 @@ +import Foundation +import Capacitor +import UIKit + +@objc(AppOpenAdPlugin) +public class AppOpenAdPlugin: CAPPlugin { + private var appOpenAdManager: AppOpenAdManager? + + @objc func loadAppOpen(_ call: CAPPluginCall) { + guard let adUnitId = call.getString("adUnitId") else { + call.reject("adUnitId is required") + return + } + if appOpenAdManager == nil { + appOpenAdManager = AppOpenAdManager(adUnitId: adUnitId) + } + DispatchQueue.main.async { + if let rootVC = UIApplication.shared.keyWindow?.rootViewController { + self.appOpenAdManager?.loadAd(rootViewController: rootVC, onLoaded: { + self.notifyListeners("appOpenAdLoaded", data: [:]) + call.resolve() + }, onFailed: { + self.notifyListeners("appOpenAdFailedToLoad", data: [:]) + call.reject("Failed to load App Open Ad") + }) + } else { + call.reject("No rootViewController") + } + } + } + + @objc func showAppOpen(_ call: CAPPluginCall) { + DispatchQueue.main.async { + if let rootVC = UIApplication.shared.keyWindow?.rootViewController { + self.appOpenAdManager?.showAdIfAvailable(rootViewController: rootVC, onClosed: { + self.notifyListeners("appOpenAdClosed", data: [:]) + call.resolve() + }, onFailedToShow: { + self.notifyListeners("appOpenAdFailedToShow", data: [:]) + call.reject("Failed to show App Open Ad") + }) + } else { + call.reject("No rootViewController") + } + } + } + + @objc func isAppOpenLoaded(_ call: CAPPluginCall) { + let loaded = appOpenAdManager?.isAdLoaded() ?? false + call.resolve(["value": loaded]) + } +} diff --git a/package-lock.json b/package-lock.json index f11084e8..4ca9a3ac 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8277,8 +8277,9 @@ }, "node_modules/rimraf": { "version": "6.0.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.0.1.tgz", + "integrity": "sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==", "dev": true, - "license": "ISC", "dependencies": { "glob": "^11.0.0", "package-json-from-dist": "^1.0.0" diff --git a/src/app-open/app-open-ad-options.interface.ts b/src/app-open/app-open-ad-options.interface.ts new file mode 100644 index 00000000..01cd3835 --- /dev/null +++ b/src/app-open/app-open-ad-options.interface.ts @@ -0,0 +1,5 @@ +export interface AppOpenAdOptions { + adUnitId: string; + showOnColdStart?: boolean; + showOnForeground?: boolean; +} diff --git a/src/app-open/app-open-ad-plugin-events.enum.ts b/src/app-open/app-open-ad-plugin-events.enum.ts new file mode 100644 index 00000000..327b1e7c --- /dev/null +++ b/src/app-open/app-open-ad-plugin-events.enum.ts @@ -0,0 +1,7 @@ +export enum AppOpenAdPluginEvents { + Loaded = 'appOpenAdLoaded', + FailedToLoad = 'appOpenAdFailedToLoad', + Opened = 'appOpenAdOpened', + Closed = 'appOpenAdClosed', + FailedToShow = 'appOpenAdFailedToShow', +} diff --git a/src/app-open/app-open-definitions.interface.ts b/src/app-open/app-open-definitions.interface.ts new file mode 100644 index 00000000..acb1a663 --- /dev/null +++ b/src/app-open/app-open-definitions.interface.ts @@ -0,0 +1,34 @@ +import type { PluginListenerHandle } from '@capacitor/core'; +import type { ValidateAllEventsEnumAreImplemented } from '../private/validate-all-events-implemented.type'; +import type { AppOpenAdPluginEvents } from './app-open-ad-plugin-events.enum'; +import type { AppOpenAdOptions } from './app-open-ad-options.interface'; + +export type AppOpenDefinitionsHasAllEvents = ValidateAllEventsEnumAreImplemented< + AppOpenAdPluginEvents, + AppOpenAdPlugin +>; + +export interface AppOpenAdPlugin { + /** + * Load an ad App Open + */ + loadAppOpen(options: AppOpenAdOptions): Promise; + + /** + * Shows the App Open ad if loaded + */ + showAppOpen(): Promise; + + /** + * Check if the App Open ad is loaded + */ + isAppOpenLoaded(): Promise<{ value: boolean }>; + + /** + * Add listeners for App Open events + */ + addListener( + eventName: AppOpenAdPluginEvents, + listenerFunc: (...args: any[]) => void, + ): Promise; +} diff --git a/src/app-open/index.ts b/src/app-open/index.ts new file mode 100644 index 00000000..9b1c9536 --- /dev/null +++ b/src/app-open/index.ts @@ -0,0 +1,3 @@ +export * from './app-open-ad-options.interface'; +export * from './app-open-ad-plugin-events.enum'; +export * from './app-open-definitions.interface'; diff --git a/src/definitions.ts b/src/definitions.ts index 08361cf4..89090a9c 100644 --- a/src/definitions.ts +++ b/src/definitions.ts @@ -4,12 +4,14 @@ import type { InterstitialDefinitions } from './interstitial'; import type { RewardDefinitions } from './reward'; import type { RewardInterstitialDefinitions } from './reward-interstitial'; import type { TrackingAuthorizationStatusInterface } from './shared/tracking-authorization-status.interface'; +import type { AppOpenAdPlugin } from './app-open'; type AdMobDefinitions = BannerDefinitions & RewardDefinitions & RewardInterstitialDefinitions & InterstitialDefinitions & - AdmobConsentDefinitions; + AdmobConsentDefinitions & + AppOpenAdPlugin; export interface AdMobPlugin extends AdMobDefinitions { /** diff --git a/src/index.ts b/src/index.ts index fef4da63..3ac3e079 100644 --- a/src/index.ts +++ b/src/index.ts @@ -13,4 +13,5 @@ export * from './reward-interstitial/index'; export * from './reward/index'; export * from './consent/index'; export * from './shared/index'; +export * from './app-open/index'; export { AdMob }; diff --git a/src/web.ts b/src/web.ts index 938e0fb4..4b59eaca 100644 --- a/src/web.ts +++ b/src/web.ts @@ -12,6 +12,7 @@ import { PrivacyOptionsRequirementStatus } from './consent/privacy-options-requi import type { AdMobRewardItem } from './reward'; import type { AdOptions, AdLoadInfo } from './shared'; import type { TrackingAuthorizationStatusInterface } from './shared/tracking-authorization-status.interface'; +import type { AppOpenAdOptions } from './app-open/app-open-ad-options.interface'; export class AdMobWeb extends WebPlugin implements AdMobPlugin { async initialize(): Promise { @@ -67,17 +68,14 @@ export class AdMobWeb extends WebPlugin implements AdMobPlugin { console.log('showBanner', options); } - // Hide the banner, remove it from screen, but can show it later async hideBanner(): Promise { console.log('hideBanner'); } - // Resume the banner, show it after hide async resumeBanner(): Promise { console.log('resumeBanner'); } - // Destroy the banner, remove it from screen. async removeBanner(): Promise { console.log('removeBanner'); } @@ -94,7 +92,7 @@ export class AdMobWeb extends WebPlugin implements AdMobPlugin { } async prepareRewardVideoAd(options: AdOptions): Promise { - console.log(options); + console.log('prepareRewardVideoAd', options); return { adUnitId: options.adId, }; @@ -108,7 +106,7 @@ export class AdMobWeb extends WebPlugin implements AdMobPlugin { } async prepareRewardInterstitialAd(options: AdOptions): Promise { - console.log(options); + console.log('prepareRewardInterstitialAd', options); return { adUnitId: options.adId, }; @@ -120,4 +118,21 @@ export class AdMobWeb extends WebPlugin implements AdMobPlugin { amount: 0, }; } + + async loadAppOpen(options: AppOpenAdOptions): Promise { + console.log('loadAppOpen', options); + } + + async showAppOpen(): Promise { + console.log('showAppOpen'); + } + + async isAppOpenLoaded(): Promise<{ value: boolean }> { + return { value: false }; + } + + addListener(eventName: string, _listenerFunc: (...args: any[]) => void): any { + console.log('addListener', eventName); + return { remove: async () => {} }; + } }