@brandonknudsen/admob-native-advanced
v1.1.2
Published
Capacitor plugin for Google AdMob Native Advanced Ads
Maintainers
Readme
Capacitor AdMob Native Advanced Plugin
A Capacitor plugin for displaying Google AdMob Native Advanced Ads in Ionic and Angular applications with a hybrid approach: native iOS rendering for proper click tracking and JavaScript rendering for Android/web. Perfect for social media feeds and content walls.
Features
- ✅ Hybrid Rendering Approach
- iOS: Native
GADNativeAdViewoverlays with automatic click/impression tracking - Android: JavaScript rendering with programmatic tracking
- Web: Mock data for development
- iOS: Native
- ✅ Load Native Advanced Ads from Google AdMob
- ✅ Extract ad assets (headline, body, images, call-to-action, etc.)
- ✅ iOS Native Overlays - Seamless integration with webview positioning
- ✅ Custom Styling System - Match your app's design perfectly
- ✅ Automatic Click Handling - iOS SDK handles URL/app store opening
- ✅ Automatic Impression Tracking - No manual reporting needed on iOS
- ✅ Support for both app install and content ads
- ✅ AdChoices compliance with proper positioning
- ✅ Cross-platform (iOS & Android)
- ✅ TypeScript support with comprehensive interfaces
- ✅ Base64 encoded image data URLs for custom rendering
- ✅ Responsive Positioning - Handles scroll, resize, and orientation changes
Why Hybrid Approach?
iOS Issue: Google Mobile Ads SDK v11+ requires native GADNativeAdView components for proper click tracking and URL opening. Extracting assets to JavaScript breaks this functionality.
Our Solution:
- iOS: Load ads natively, render in
GADNativeAdView, overlay on webview - Android: Keep existing JavaScript rendering for simplicity
- Result: Native behavior on iOS, consistent experience across platforms
Installation
1. Install the plugin
npm install @brandonknudsen/admob-native-advanced2. Add to your Capacitor project
npx cap sync3. Platform-specific setup
Android
Add the following to your android/app/src/main/AndroidManifest.xml:
<application>
<!-- Replace with your actual AdMob app ID -->
<meta-data
android:name="com.google.android.gms.ads.APPLICATION_ID"
android:value="ca-app-pub-xxxxxxxxxxxxxxxx~yyyyyyyyyy" />
</application>iOS
Add the following to your ios/App/App/Info.plist:
<key>GADApplicationIdentifier</key>
<string>ca-app-pub-xxxxxxxxxxxxxxxx~yyyyyyyyyy</string>Usage
1. Initialize AdMob
import { AdMobNativeAdvanced } from '@brandonknudsen/admob-native-advanced';
// Initialize with your AdMob app ID
await AdMobNativeAdvanced.initialize({
appId: 'ca-app-pub-xxxxxxxxxxxxxxxx~yyyyyyyyyy'
});2. Load a Native Ad
// Load a native ad
const adData = await AdMobNativeAdvanced.loadAd({
adUnitId: 'ca-app-pub-xxxxxxxxxxxxxxxx/yyyyyyyyyy'
});
console.log('Ad loaded:', adData);
// {
// adId: "unique-ad-id",
// headline: "Amazing Product",
// body: "Discover the amazing features...",
// callToAction: "Learn More",
// advertiser: "Brand Name",
// mediaContentUrl: "data:image/png;base64,iVBORw0KGgoAAAANSU...",
// iconUrl: "data:image/png;base64,iVBORw0KGgoAAAANSU...",
// adChoicesIconUrl: "data:image/png;base64,iVBORw0KGgoAAAANSU...",
// adChoicesText: "AdChoices",
// isAppInstallAd: false,
// isContentAd: true,
// nativeRendered: true // iOS only
// }3. Create a Hybrid Angular Component
// native-ad.component.ts
import { Component, Input, ViewChild, ElementRef, AfterViewInit, OnDestroy, ChangeDetectorRef } from '@angular/core';
import { AdMobNativeAdvanced, NativeAdData } from '@brandonknudsen/admob-native-advanced';
import { Capacitor } from '@capacitor/core';
@Component({
selector: 'app-native-ad',
template: `
<div class="native-ad-container" [class.ios-native]="isIOS" *ngIf="adData">
<!-- iOS: Hidden placeholder for native overlay positioning -->
<div
#adPlaceholder
class="native-ad-placeholder"
[style.visibility]="isIOS && adData ? 'hidden' : 'visible'">
<!-- Android/Web: Rendered in HTML -->
<div class="native-ad" *ngIf="!isIOS" (click)="onAdClick()">
<div class="ad-header">
<img *ngIf="adData.iconUrl" [src]="adData.iconUrl" class="ad-icon" alt="Ad icon">
<div class="ad-info">
<h3 *ngIf="adData.headline" class="ad-headline">{{ adData.headline }}</h3>
<p *ngIf="adData.advertiser" class="ad-advertiser">{{ adData.advertiser }}</p>
</div>
<div class="ad-choices" (click)="onAdChoicesClick($event)">
<img *ngIf="adData.adChoicesIconUrl" [src]="adData.adChoicesIconUrl" alt="AdChoices">
<span>{{ adData.adChoicesText || 'AdChoices' }}</span>
</div>
</div>
<div *ngIf="adData.mediaContentUrl" class="ad-media">
<img [src]="adData.mediaContentUrl" alt="Ad media">
</div>
<div class="ad-content">
<p *ngIf="adData.body" class="ad-body">{{ adData.body }}</p>
<div *ngIf="adData.isAppInstallAd" class="app-install-info">
<span *ngIf="adData.store" class="store">{{ adData.store }}</span>
<span *ngIf="adData.price" class="price">{{ adData.price }}</span>
<div *ngIf="adData.starRating" class="rating">
{{ '★'.repeat(adData.starRating) }}{{ '☆'.repeat(5 - adData.starRating) }}
</div>
</div>
</div>
<button *ngIf="adData.callToAction" class="ad-cta">
{{ adData.callToAction }}
</button>
<div class="ad-label">Ad</div>
</div>
<!-- iOS: Transparent placeholder maintains layout space -->
<div class="native-ad ios-placeholder" *ngIf="isIOS" [style.height.px]="estimatedHeight">
<!-- Native overlay will cover this -->
</div>
</div>
</div>
`,
styles: [`
.native-ad-container {
margin: 16px 0;
}
.native-ad {
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 12px;
background: white;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
position: relative;
cursor: pointer;
}
.native-ad:hover {
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
}
.ad-header {
display: flex;
align-items: center;
margin-bottom: 12px;
}
.ad-icon {
width: 40px;
height: 40px;
border-radius: 4px;
margin-right: 12px;
object-fit: cover;
}
.ad-info {
flex: 1;
}
.ad-headline {
margin: 0;
font-size: 16px;
font-weight: 600;
color: #333;
line-height: 1.3;
}
.ad-advertiser {
margin: 4px 0 0 0;
font-size: 11px;
color: #999;
}
.ad-choices {
display: flex;
align-items: center;
font-size: 10px;
color: #666;
cursor: pointer;
padding: 2px 4px;
border-radius: 3px;
background: rgba(255,255,255,0.9);
}
.ad-choices img {
width: 16px;
height: 16px;
margin-right: 4px;
}
.ad-media img {
width: 100%;
max-height: 200px;
object-fit: contain;
border-radius: 4px;
margin-bottom: 8px;
}
.ad-body {
margin: 0 0 8px 0;
font-size: 14px;
line-height: 1.4;
color: #666;
}
.app-install-info {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
font-size: 12px;
}
.store {
background: #e3f2fd;
color: #1976d2;
padding: 2px 6px;
border-radius: 4px;
}
.price {
color: #4caf50;
font-weight: bold;
}
.rating {
color: #ff9800;
}
.ad-cta {
width: 100%;
padding: 8px 16px;
background: #2196f3;
color: white;
border: none;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
margin: 8px 0;
}
.ad-cta:hover {
background: #1976d2;
}
.ad-label {
position: absolute;
top: 8px;
left: 8px;
background: rgba(0,0,0,0.8);
color: white;
padding: 3px 8px;
border-radius: 3px;
font-size: 11px;
font-weight: bold;
}
.ios-native .native-ad-placeholder {
border: none;
background: transparent;
}
.ios-placeholder {
background: #f5f5f5;
display: flex;
align-items: center;
justify-content: center;
color: #999;
font-style: italic;
}
`]
})
export class NativeAdComponent implements AfterViewInit, OnDestroy {
@ViewChild('adPlaceholder') adPlaceholder!: ElementRef;
@Input() adData!: NativeAdData;
isIOS = Capacitor.getPlatform() === 'ios';
estimatedHeight = 300; // Estimated height for iOS placeholder
private resizeObserver?: ResizeObserver;
private intersectionObserver?: IntersectionObserver;
constructor(private cdr: ChangeDetectorRef) {}
async ngAfterViewInit() {
if (this.isIOS && this.adData) {
await this.configureIOSNativeAd();
this.setupIOSPositioning();
} else if (this.adData) {
// Report impression for Android/web
this.reportImpression();
}
}
ngOnDestroy() {
if (this.isIOS && this.adData) {
AdMobNativeAdvanced.hideNativeAd({ adId: this.adData.adId });
}
this.resizeObserver?.disconnect();
this.intersectionObserver?.disconnect();
}
private async configureIOSNativeAd() {
if (!this.adData) return;
// Configure styling to match your design
await AdMobNativeAdvanced.configureNativeAdStyle({
adId: this.adData.adId,
style: {
backgroundColor: '#ffffff',
cornerRadius: 8,
borderWidth: 1,
borderColor: '#e0e0e0',
headlineColor: '#333333',
headlineFontSize: 16,
bodyColor: '#666666',
bodyFontSize: 14,
advertiserColor: '#999999',
advertiserFontSize: 11,
ctaBackgroundColor: '#2196f3',
ctaTextColor: '#ffffff',
ctaFontSize: 14
}
});
}
private setupIOSPositioning() {
if (!this.adPlaceholder || !this.adData) return;
// Initial positioning
this.positionNativeAd();
// Handle resize
this.resizeObserver = new ResizeObserver(() => {
this.positionNativeAd();
});
this.resizeObserver.observe(document.body);
// Handle visibility
this.intersectionObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (this.adData) {
if (entry.isIntersecting) {
this.positionNativeAd();
} else {
AdMobNativeAdvanced.hideNativeAd({ adId: this.adData.adId });
}
}
});
}, { threshold: 0.1 });
this.intersectionObserver.observe(this.adPlaceholder.nativeElement);
// Handle scroll with throttling
let ticking = false;
const updatePosition = () => {
this.positionNativeAd();
ticking = false;
};
window.addEventListener('scroll', () => {
if (!ticking) {
requestAnimationFrame(updatePosition);
ticking = true;
}
}, { passive: true });
}
private positionNativeAd() {
if (!this.adPlaceholder || !this.adData || !this.isIOS) return;
const rect = this.adPlaceholder.nativeElement.getBoundingClientRect();
const scrollY = window.pageYOffset || document.documentElement.scrollTop;
AdMobNativeAdvanced.positionNativeAd({
adId: this.adData.adId,
x: rect.left,
y: rect.top + scrollY,
width: rect.width,
height: rect.height
});
}
async onAdClick() {
if (!this.adData) return;
// For Android, manually report click (iOS handled automatically)
if (!this.isIOS) {
try {
await AdMobNativeAdvanced.reportClick(this.adData.adId);
} catch (error) {
console.error('Error reporting ad click:', error);
}
}
}
onAdChoicesClick(event: Event) {
event.stopPropagation();
// Open AdChoices info (handled automatically on iOS)
if (!this.isIOS) {
window.open('https://www.google.com/settings/ads', '_blank');
}
}
private async reportImpression() {
try {
await AdMobNativeAdvanced.reportImpression(this.adData.adId);
} catch (error) {
console.error('Error reporting ad impression:', error);
}
}
}4. Use in a Social Media Feed
// feed.component.ts
import { Component, OnInit } from '@angular/core';
import { AdMobNativeAdvanced, NativeAdData } from '@brandonknudsen/admob-native-advanced';
@Component({
selector: 'app-feed',
template: `
<div class="feed">
<div *ngFor="let item of feedItems" class="feed-item">
<ng-container [ngSwitch]="item.type">
<app-post *ngSwitchCase="'post'" [post]="item.data"></app-post>
<app-native-ad *ngSwitchCase="'ad'" [adData]="item.data"></app-native-ad>
</ng-container>
</div>
</div>
`
})
export class FeedComponent implements OnInit {
feedItems: Array<{ type: 'post' | 'ad', data: any }> = [];
async ngOnInit() {
// Initialize AdMob
await AdMobNativeAdvanced.initialize({
appId: 'ca-app-pub-xxxxxxxxxxxxxxxx~yyyyyyyyyy'
});
// Load your feed data
await this.loadFeed();
}
async loadFeed() {
// Load your posts
const posts = await this.getPosts();
// Load an ad
const adData = await AdMobNativeAdvanced.loadAd({
adUnitId: 'ca-app-pub-xxxxxxxxxxxxxxxx/yyyyyyyyyy'
});
// Mix posts and ads (insert ad every 3-4 posts)
this.feedItems = [
{ type: 'post', data: posts[0] },
{ type: 'post', data: posts[1] },
{ type: 'post', data: posts[2] },
{ type: 'ad', data: adData },
{ type: 'post', data: posts[3] },
// ... more items
];
}
private async getPosts() {
// Your post loading logic
return [];
}
}API Reference
Methods
initialize(options: InitializeOptions): Promise<void>
Initialize AdMob with your app ID.
Parameters:
options.appId(string): Your AdMob app ID
loadAd(options: LoadAdOptions): Promise<NativeAdData>
Load a native advanced ad.
Parameters:
options.adUnitId(string): Your AdMob ad unit ID
Returns: Promise with ad data including all available assets
reportClick(adId: string): Promise<void>
Report ad click to AdMob for tracking (Android only - iOS handles automatically).
Parameters:
adId(string): The ID of the ad that was clicked
reportImpression(adId: string): Promise<void>
Report ad impression to AdMob for tracking (Android only - iOS handles automatically).
Parameters:
adId(string): The ID of the ad that was shown
positionNativeAd(options: PositionNativeAdOptions): Promise<void> (iOS only)
Position and show the native ad view overlay on iOS.
Parameters:
options.adId(string): The ID of the ad to positionoptions.x(number): Screen X coordinate in pixelsoptions.y(number): Screen Y coordinate in pixelsoptions.width(number): Width in pixelsoptions.height(number): Height in pixels
hideNativeAd(options: HideNativeAdOptions): Promise<void> (iOS only)
Hide or remove the native ad view overlay on iOS.
Parameters:
options.adId(string): The ID of the ad to hide
configureNativeAdStyle(options: ConfigureNativeAdStyleOptions): Promise<void> (iOS only)
Configure the styling of the native ad view overlay on iOS.
Parameters:
options.adId(string): The ID of the ad to styleoptions.style(NativeAdViewStyle): Style configuration object
Types
NativeAdData
interface NativeAdData {
adId: string; // Unique ad identifier
headline?: string; // Ad headline/title
body?: string; // Ad body text
callToAction?: string; // Call-to-action text
advertiser?: string; // Advertiser name
store?: string; // App store name (app install ads)
price?: string; // App price (app install ads)
starRating?: number; // Star rating (app install ads)
mediaContentUrl?: string; // Main ad image (base64 data URL)
iconUrl?: string; // Ad icon (base64 data URL)
adChoicesIconUrl?: string; // AdChoices icon (base64 data URL)
adChoicesText?: string; // AdChoices text
isAppInstallAd?: boolean; // Whether this is an app install ad
isContentAd?: boolean; // Whether this is a content ad
nativeRendered?: boolean; // Whether ad is rendered natively (iOS only)
}NativeAdViewStyle (iOS only)
interface NativeAdViewStyle {
backgroundColor?: string; // Container background color (hex)
cornerRadius?: number; // Border radius in pixels
borderWidth?: number; // Border width in pixels
borderColor?: string; // Border color (hex)
headlineColor?: string; // Headline text color (hex)
headlineFontSize?: number; // Headline font size in points
bodyColor?: string; // Body text color (hex)
bodyFontSize?: number; // Body font size in points
advertiserColor?: string; // Advertiser text color (hex)
advertiserFontSize?: number; // Advertiser font size in points
ctaBackgroundColor?: string; // CTA button background (hex)
ctaTextColor?: string; // CTA button text color (hex)
ctaFontSize?: number; // CTA button font size in points
}Event Listeners (iOS only)
import { AdMobNativeAdvanced } from '@brandonknudsen/admob-native-advanced';
// Listen for ad events on iOS
AdMobNativeAdvanced.addListener('adClicked', (data) => {
console.log('Ad clicked:', data.adId);
// Track click in analytics
});
AdMobNativeAdvanced.addListener('adImpression', (data) => {
console.log('Ad impression:', data.adId);
// Track impression in analytics
});
AdMobNativeAdvanced.addListener('adWillPresentScreen', (data) => {
console.log('Ad will present full screen:', data.adId);
// Pause video, etc.
});
AdMobNativeAdvanced.addListener('adDidDismissScreen', (data) => {
console.log('Ad dismissed full screen:', data.adId);
// Resume video, etc.
});Test Ad Unit IDs
Use these test ad unit IDs for development:
- Android/iOS Native Advanced:
ca-app-pub-3940256099942544/2247696110 - iOS Native Advanced (legacy):
ca-app-pub-3940256099942544/3985214057
Best Practices
1. Platform-Specific Behavior
- iOS: Let the SDK handle click/impression tracking automatically
- Android: Manually report clicks and impressions
- Styling: Use the styling system on iOS to match your design exactly
2. iOS Native Ad Positioning
- Use
getBoundingClientRect()for accurate positioning - Handle scroll events with throttling (
requestAnimationFrame) - Hide ads when not visible to save resources
- Always call
hideNativeAd()when components unmount
3. Ad Placement
- Place ads naturally within your content feed
- Don't place too many ads close together
- Insert ads every 3-4 posts for optimal user experience
- Follow AdMob's Native Ad Policy
4. Ad Labeling
- Always include "Ad" or "Sponsored" label
- Display AdChoices icon prominently (handled automatically on iOS)
- Make it clear to users that content is promotional
5. Error Handling
try {
const adData = await AdMobNativeAdvanced.loadAd({
adUnitId: 'your-ad-unit-id'
});
// Handle successful ad load
if (Capacitor.getPlatform() === 'ios') {
// Configure iOS native styling
await AdMobNativeAdvanced.configureNativeAdStyle({
adId: adData.adId,
style: { /* your styling */ }
});
}
} catch (error) {
console.error('Failed to load ad:', error);
// Handle ad load failure (e.g., show fallback content)
}6. Performance
- Load ads asynchronously without blocking UI
- Implement proper error handling for failed ad loads
- Use intersection observers for visibility detection
- Clean up native views on component destruction
- Limit concurrent ad requests
Troubleshooting
Common Issues
"AdMob must be initialized" error
- Ensure you call
initialize()beforeloadAd() - Check that your app ID is correct
- Ensure you call
"Ad failed to load" error
- Verify your ad unit ID is correct
- Check your internet connection
- Ensure you're using test ad unit IDs during development
iOS: Ads not showing or clicks not working
- Check that you've added the GADApplicationIdentifier to Info.plist
- Verify your iOS deployment target is 13.0 or higher
- Ensure
positionNativeAd()is called after ad load - Check that positioning coordinates are correct
iOS: Native overlay positioning issues
- Verify
getBoundingClientRect()returns valid dimensions - Check scroll position calculations (
window.pageYOffset) - Ensure the placeholder element has proper dimensions
- Verify
Android: Ads not showing
- Check that you've added the APPLICATION_ID metadata to AndroidManifest.xml
- Verify your minSdkVersion is 21 or higher
- Ensure you're calling
reportClick()andreportImpression()
Debug Mode
Enable debug logging by adding this to your app initialization:
// For development only
if (!environment.production) {
// Add test device IDs to see test ads
console.log('Running in debug mode with test ads');
}Migration from Previous Versions
If you're upgrading from a version without iOS native rendering:
- Update your Angular components to support the hybrid approach
- Add iOS-specific positioning logic
- Configure native ad styling for iOS
- Test on both platforms to ensure consistency
License
MIT License - see LICENSE file for details.
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
Support
For support, please:
- Open an issue on GitHub
- Check the AdMob documentation
- Review the SETUP.md for detailed implementation examples
