feat: support for App Open Ad en Android e iOS#386
feat: support for App Open Ad en Android e iOS#386BETOXL wants to merge 29 commits intocapacitor-community:mainfrom
Conversation
BETOXL
commented
Oct 11, 2025
- Adds native and TypeScript support for the Open Ad App format on Android and iOS.
- Includes usage examples in the Angular demo.
- Documentation and exposed events.
There was a problem hiding this comment.
Pull Request Overview
This PR adds support for App Open Ad format on both Android and iOS platforms, integrating it into the AdMob plugin with complete TypeScript definitions and usage examples.
- Adds App Open Ad functionality with load, show, and status check methods
- Implements native iOS and Android code using Google Mobile Ads SDK
- Includes TypeScript definitions, events, and Angular demo integration
Reviewed Changes
Copilot reviewed 15 out of 16 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| src/web.ts | Adds stub methods for App Open Ad functionality in web implementation |
| src/index.ts | Exports new App Open Ad module |
| src/definitions.ts | Integrates App Open Ad plugin interface into main plugin definitions |
| src/app-open/* | Complete TypeScript definitions for App Open Ad functionality including options, events, and plugin interface |
| ios/Sources/AdMobPlugin/* | Native iOS implementation using GADAppOpenAd with manager pattern |
| android/src/main/java/* | Native Android implementation using Google Mobile Ads AppOpenAd API |
| demo/angular/src/app/app.component.ts | Angular demo showing App Open Ad usage with event listeners |
| README.md | Documentation for App Open Ad API methods and usage examples |
Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.
android/src/main/java/com/getcapacitor/community/admob/AdMob.java
Outdated
Show resolved
Hide resolved
distante
left a comment
There was a problem hiding this comment.
Hello @BETOXL ! thanks for your PR, please check the comments and the CI. You probably need to run the formatter. (I will try to add it to the pre commit lint in the future)
Also can you please update the tests to include your changes?
Thank you!
android/src/main/java/com/getcapacitor/community/admob/appopen/AppOpenAdManager.java
Outdated
Show resolved
Hide resolved
android/src/main/java/com/getcapacitor/community/admob/appopen/AppOpenAdManager.java
Outdated
Show resolved
Hide resolved
android/src/main/java/com/getcapacitor/community/admob/appopen/AppOpenAdManager.java
Outdated
Show resolved
Hide resolved
android/src/main/java/com/getcapacitor/community/admob/AdMob.java
Outdated
Show resolved
Hide resolved
README.md
Outdated
| } from '@capacitor-community/admob'; | ||
|
|
||
| export async function showAppOpenAd(): Promise<void> { | ||
| // Escuchar eventos |
Co-authored-by: Copilot <[email protected]>
Co-authored-by: Copilot <[email protected]>
Co-authored-by: Copilot <[email protected]>
Co-authored-by: Copilot <[email protected]>
|
a lot of comments are not correctly resolved. |
distante
left a comment
There was a problem hiding this comment.
Please do not resolve comments without fixing them.
no single line ifs please
this should be inside the AdMob class.
comments in English.
no single line ifs please
comments in English
Replace any type with proper AppOpenAdOptions type for better type safety and API consistency.
distante
left a comment
There was a problem hiding this comment.
The CI is still failing.
You should check how the other implementation is done so you can fix and add your tests.
android/src/main/java/com/getcapacitor/community/admob/AdMob.java
Outdated
Show resolved
Hide resolved
Moved AppOpenAd Plugin initialization inside the AdMob class.
delete comment
android/src/main/java/com/getcapacitor/community/admob/appopen/AppOpenAdManager.java
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 16 out of 17 changed files in this pull request and generated 17 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
android/src/main/java/com/getcapacitor/community/admob/appopen/AppOpenAdPlugin.java
Outdated
Show resolved
Hide resolved
android/src/test/java/com/getcapacitor/community/admob/models/AdOptionsTest.java
Show resolved
Hide resolved
…/AppOpenAdPlugin.java Co-authored-by: Copilot <[email protected]>
Co-authored-by: Copilot <[email protected]>
Co-authored-by: Copilot <[email protected]>
Co-authored-by: Copilot <[email protected]>
Co-authored-by: Copilot <[email protected]>
Co-authored-by: Copilot <[email protected]>
Co-authored-by: Copilot <[email protected]>
|
Good morning, what became of this feat? |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 16 out of 16 changed files in this pull request and generated 14 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| 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) { |
There was a problem hiding this comment.
AppOpenAdPlugin extends Capacitor Plugin, but AdMob instantiates it directly (new AppOpenAdPlugin()) and forwards calls. Plugin instances need to be created/attached by Capacitor to have a valid bridge/context/activity; this setup risks getContext()/getActivity() being null and listeners not working. Refactor App Open into an executor/helper (like other ad executors) that uses AdMob's getContext/getActivity/notifyListeners, or implement the logic directly in AdMob.
| 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) { | |
| private AppOpenAdPlugin getAppOpenAdPlugin() { | |
| if (getBridge() == null) { | |
| return null; | |
| } | |
| try { | |
| Plugin plugin = getBridge().getPlugin("AppOpenAdPlugin"); | |
| if (plugin instanceof AppOpenAdPlugin) { | |
| return (AppOpenAdPlugin) plugin; | |
| } | |
| } catch (Exception ignored) { | |
| // If the plugin cannot be obtained, return null and let callers handle it. | |
| } | |
| return null; | |
| } | |
| @PluginMethod | |
| public void loadAppOpen(final PluginCall call) { | |
| AppOpenAdPlugin appOpenAdPlugin = getAppOpenAdPlugin(); | |
| if (appOpenAdPlugin == null) { | |
| call.reject("AppOpenAdPlugin is not available"); | |
| return; | |
| } | |
| appOpenAdPlugin.loadAppOpen(call); | |
| } | |
| @PluginMethod | |
| public void showAppOpen(final PluginCall call) { | |
| AppOpenAdPlugin appOpenAdPlugin = getAppOpenAdPlugin(); | |
| if (appOpenAdPlugin == null) { | |
| call.reject("AppOpenAdPlugin is not available"); | |
| return; | |
| } | |
| appOpenAdPlugin.showAppOpen(call); | |
| } | |
| @PluginMethod | |
| public void isAppOpenLoaded(final PluginCall call) { | |
| AppOpenAdPlugin appOpenAdPlugin = getAppOpenAdPlugin(); | |
| if (appOpenAdPlugin == null) { | |
| call.reject("AppOpenAdPlugin is not available"); | |
| return; | |
| } |
| appOpenAdManager.loadAd(context, () -> { | ||
| notifyListeners("appOpenAdLoaded", new JSONObject()); | ||
| call.resolve(); | ||
| }, () -> { | ||
| notifyListeners("appOpenAdFailedToLoad", new JSONObject()); | ||
| call.reject("Failed to load App Open Ad"); | ||
| }); |
There was a problem hiding this comment.
notifyListeners expects a JSObject, but this code passes org.json.JSONObject, which is inconsistent with the rest of the plugin and may not compile. Use com.getcapacitor.JSObject (or pass null/an empty JSObject) for event payloads.
| @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"); | ||
| }); | ||
| } |
There was a problem hiding this comment.
showAppOpen assumes appOpenAdManager and activity are non-null; if loadAppOpen was never called or the plugin isn't attached, this can crash or hang. Add null checks and call.reject(...) when there's no loaded manager/ad or no current activity.
| if (isLoadingAd || appOpenAd != null) { | ||
| return; | ||
| } | ||
|
|
There was a problem hiding this comment.
loadAd returns early when already loading or already loaded without invoking callbacks. Since the plugin call resolves/rejects only inside those callbacks, the JS promise can remain pending forever. Return a status and resolve immediately (already loaded/loading) or invoke onLoaded when appOpenAd != null.
| if (isLoadingAd || appOpenAd != null) { | |
| return; | |
| } | |
| if (appOpenAd != null) { | |
| if (onLoaded != null) { | |
| onLoaded.run(); | |
| } | |
| return; | |
| } | |
| if (isLoadingAd) { | |
| if (onFailed != null) { | |
| onFailed.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(); | ||
| } | ||
| } | ||
| }); |
There was a problem hiding this comment.
AppOpenAdPluginEvents.Opened is defined/used, but Android never notifies an "opened" event. Consider triggering it from FullScreenContentCallback.onAdShowedFullScreenContent() so JS listeners behave consistently with the documented API.
| if isLoadingAd || appOpenAd != nil { | ||
| return | ||
| } | ||
|
|
There was a problem hiding this comment.
loadAd returns early when an ad is already loaded or loading, but it doesn't invoke onLoaded/onFailed. Because the plugin call waits on these callbacks, this can leave the JS promise pending forever. Return a status (already loaded/loading) and resolve/reject accordingly, or invoke onLoaded when appOpenAd != nil.
| if isLoadingAd || appOpenAd != nil { | |
| return | |
| } | |
| // If an ad is already loaded, report success immediately. | |
| if appOpenAd != nil { | |
| onLoaded() | |
| return | |
| } | |
| // If an ad is currently loading, report failure to avoid leaving callers hanging. | |
| if isLoadingAd { | |
| onFailed() | |
| return | |
| } |
| isShowingAd = true | ||
| ad.fullScreenContentDelegate = self | ||
| ad.present(fromRootViewController: rootViewController) | ||
| self.onClosed = onClosed | ||
| self.onFailedToShow = onFailedToShow |
There was a problem hiding this comment.
In showAdIfAvailable, onClosed/onFailedToShow are stored after calling present. Delegate callbacks can fire immediately, so the closures may still be nil and the JS call may never resolve/reject. Assign the closures before presenting the ad.
| isShowingAd = true | |
| ad.fullScreenContentDelegate = self | |
| ad.present(fromRootViewController: rootViewController) | |
| self.onClosed = onClosed | |
| self.onFailedToShow = onFailedToShow | |
| self.onClosed = onClosed | |
| self.onFailedToShow = onFailedToShow | |
| isShowingAd = true | |
| ad.fullScreenContentDelegate = self | |
| ad.present(fromRootViewController: rootViewController) |
| console.log('Failed to load App Open Ad'); | ||
| }); | ||
|
|
||
| const options: AppOpenAdOptions = { | ||
| adUnitId: 'TU_AD_UNIT_ID', | ||
| showOnColdStart: true, // Opcional | ||
| showOnForeground: true, // Opcional |
There was a problem hiding this comment.
The FailedToShow listener logs "Failed to load App Open Ad" and the comments use "Opcional"/TU_AD_UNIT_ID, which is inconsistent with the surrounding English docs. Consider updating to "Failed to show...", "Optional", and an English placeholder like YOUR_AD_UNIT_ID for clarity.
| console.log('Failed to load App Open Ad'); | |
| }); | |
| const options: AppOpenAdOptions = { | |
| adUnitId: 'TU_AD_UNIT_ID', | |
| showOnColdStart: true, // Opcional | |
| showOnForeground: true, // Opcional | |
| console.log('Failed to show App Open Ad'); | |
| }); | |
| const options: AppOpenAdOptions = { | |
| adUnitId: 'YOUR_AD_UNIT_ID', | |
| showOnColdStart: true, // Optional | |
| showOnForeground: true, // Optional |
| showOnColdStart?: boolean; | ||
| showOnForeground?: boolean; |
There was a problem hiding this comment.
showOnColdStart / showOnForeground are defined in the public options but are not used anywhere in the repo (native or TS). This makes the API misleading; either implement the behavior (hook into app lifecycle) or remove these fields until supported.
| showOnColdStart?: boolean; | |
| showOnForeground?: boolean; |
| @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") | ||
| }) |
There was a problem hiding this comment.
showAppOpen never emits the appOpenAdOpened event even though it’s exposed in AppOpenAdPluginEvents and used in the demo/docs. Consider emitting an "Opened" event when presentation begins (e.g., via GADFullScreenContentDelegate.adDidPresentFullScreenContent or a callback from AppOpenAdManager).
distante
left a comment
There was a problem hiding this comment.
Please check the copilot comments.
I will try to check this this week(end) so we can get this merged
| * @see <a href="https://developers.google.com/admob/android/targeting">Target Settings</a> | ||
| */ | ||
| // --------------------------------------------------------- | ||
| // REQUEST CONFIGURATION |
There was a problem hiding this comment.
please keep the documentation link for future developers
|
Also check the CI errors. Note: Closes #167 |