Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name: Build
env:
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1
DOTNET_NOLOGO: true
DOTNET_VERSION: 8.0.x
DOTNET_VERSION: 9.0.x

on:
push:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release-preview.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ on:
env:
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1
DOTNET_NOLOGO: true
DOTNET_VERSION: 8.0.x
DOTNET_VERSION: 9.0.x

jobs:
build:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ on:
env:
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1
DOTNET_NOLOGO: true
DOTNET_VERSION: 8.0.x
DOTNET_VERSION: 9.0.x

jobs:
build:
Expand Down
59 changes: 57 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Avalonia solution derived from [marius-bughiu/Plugin.AdMob](https://github.com/m
- [Usage](#usage)
- [Preparing for Apple Privacy Manifests](#preparing-for-apple-privacy-manifests)
- [Troubleshooting](#troubleshooting)
- [Avalonia 12.x Migration](#avalonia-12x-migration)

## Introduction

Expand All @@ -21,8 +22,8 @@ The library currently supports the following ad units:

| | Consent | Banner | Interstitial | Rewarded interstitial | Rewarded | Native advanced | App open |
| ------- | ------- | ------ | ------------ | --------------------- | -------- | --------------- | -------- |
| Android | ✓ | ✓ | ✓ | ✓ | ✓ | ☓ | |
| iOS | ✓ | ✓ | ✓ | ✓ | ✓ | ☓ | |
| Android | ✓ | ✓ | ✓ | ✓ | ✓ | ☓ | |
| iOS | ✓ | ✓ | ✓ | ✓ | ✓ | ☓ | |

## Usage

Expand Down Expand Up @@ -338,6 +339,56 @@ protected override AppBuilder CustomizeAppBuilder(AppBuilder builder)
| OnAdClosed | |
| OnUserEarnedReward | Contains a `RewardItem` record |

### App Open

| iOS | Android |
| --------------------------------------------------------------- | ------------------------------------------------------------------- |
| <img alt="iOS Banner" src="img/iOS App Open.png" width="250" /> | <img alt="iOS Banner" src="img/Android App Open.jpg" width="250" /> |

App Open ads can be used by calling `AppOpen.Create(unitId)` on the `AdMob` singleton.

This call will load and the return the app load ad with an `OnAdLoaded` event once it's finished loaded. `.Show()` can then be called to display the ad.

```c#
public ICommand ShowAppOpenAdCommand { get; }

public MainViewModel()
{
ShowAppOpenAdCommand = ReactiveCommand.Create(ShowAppOpenAd);
}

private void ShowAppOpenAd()
{
var appOpenAd = AdMob.Current.AppOpen.Create();
appOpenAd.OnAdLoaded += (_, _) => appOpenAd.Show();
}
```

#### Android

When using app open ads on Android, the `Activity` must be passed into the `.AddAdMob()` call:

```c#
protected override AppBuilder CustomizeAppBuilder(AppBuilder builder)
{
return base.CustomizeAppBuilder(builder)
...
.UseAdMob(this, adMobOptions);
}
```

#### Events

| Event | Notes |
| ------------------- | ----- |
| OnAdLoaded | |
| OnAdFailedToLoad | |
| OnAdPresented | |
| OnAdFailedToPresent | |
| OnAdImpression | |
| OnAdClicked | |
| OnAdClosed | |

## Preparing for Apple Privacy Manifests

> I have yet to get around to testing any of this, but now the library is using a different iOS native binding package, the number of frameworks it depends on is significantly lower, and it should just be a case of updating to the latest when I find the time.
Expand Down Expand Up @@ -375,3 +426,7 @@ Try adding the following to your `Info.plist`:
```

> The current iOS bindings are a little out of date and are still using an older version of the Google Mobile Ads SDK. While the version is now no longer a sunset version by Google, this is actively being worked on [here](https://github.com/jcsawyer/Jc.GoogleMobileAds.iOS).

## Avalonia 12.x Migration

Coming soon...
Binary file added img/Android App Open.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/iOS App Open.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 8 additions & 0 deletions samples/Jc.AdMob.Avalonia.Sample/ViewModels/MainViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ public class MainViewModel : ViewModelBase
public ICommand ShowInterstitialAdCommand { get; }
public ICommand ShowRewardedInterstitialAdCommand { get; }
public ICommand ShowRewardedAdCommand { get; }
public ICommand ShowAppOpenAdCommand { get; }

public MainViewModel()
{
Expand All @@ -25,6 +26,7 @@ public MainViewModel()
ShowInterstitialAdCommand = ReactiveCommand.Create(ShowInterstitialAd);
ShowRewardedInterstitialAdCommand = ReactiveCommand.Create(ShowRewardedInterstitialAd);
ShowRewardedAdCommand = ReactiveCommand.Create(ShowRewardedAd);
ShowAppOpenAdCommand = ReactiveCommand.Create(ShowAppOpen);
}

private void ResetConsent()
Expand Down Expand Up @@ -61,4 +63,10 @@ private void ShowRewardedAd()
rewarded.OnAdLoaded += (_, _) => rewarded.Show();
rewarded.OnUserEarnedReward += (_, reward) => Debug.WriteLine($"User earned reward: {reward.Amount} {reward.Type}");
}

private void ShowAppOpen()
{
var appLoadedAd = AdMob.Current.AppOpen.Create();
appLoadedAd.OnAdLoaded += (_, _) => appLoadedAd.Show();
}
}
1 change: 1 addition & 0 deletions samples/Jc.AdMob.Avalonia.Sample/Views/MainView.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
<Grid ColumnDefinitions="*" RowDefinitions="*, Auto, Auto">
<StackPanel Grid.Row="0" Orientation="Vertical" HorizontalAlignment="Center" VerticalAlignment="Center">
<TextBlock Text="{Binding Greeting}" HorizontalAlignment="Center" />
<Button Content="Show App Open Ad" Command="{Binding ShowAppOpenAdCommand}" HorizontalAlignment="Center"></Button>
<Button Content="Show Interstitial Ad" Command="{Binding ShowInterstitialAdCommand}" HorizontalAlignment="Center"></Button>
<Button Content="Show Rewarded Interstitial Ad" Command="{Binding ShowRewardedInterstitialAdCommand}" HorizontalAlignment="Center"></Button>
<Button Content="Show Rewarded Ad" Command="{Binding ShowRewardedAdCommand}" HorizontalAlignment="Center"></Button>
Expand Down
3 changes: 3 additions & 0 deletions src/Jc.AdMob.Avalonia.Android/AppBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,14 @@ public static AppBuilder UseAdMob(this AppBuilder appBuilder, Activity activity,
{
return appBuilder.AfterSetup(_ =>
{
AdMob.Current.AppOpen = new Avalonia.AppOpenAd();
AdMob.Current.Consent = new AdConsentAndroid(activity, options);
AdMob.Current.Interstitial = new Avalonia.InterstitialAd();
AdMob.Current.RewardedInterstitial = new Avalonia.RewardedInterstitialAd();
AdMob.Current.Rewarded = new Avalonia.RewardedAd();

AppOpenAd.Activity = activity;
Avalonia.AppOpenAd.ImplementationFactory = (unitId) => new AppOpenAd(options, unitId);
InterstitialAd.Activity = activity;
Avalonia.InterstitialAd.ImplementationFactory = (unitId) => new InterstitialAd(options, unitId);
RewardedInterstitialAd.Activity = activity;
Expand Down
83 changes: 83 additions & 0 deletions src/Jc.AdMob.Avalonia.Android/AppOpenAd.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
using Android.Gms.Ads;

namespace Jc.AdMob.Avalonia.Android;

public class AppOpenAd : IAppOpenAd
{
private const string TestUnit = "ca-app-pub-3940256099942544/9257395921";

internal static Activity? Activity { get; set; }

private readonly AdMobOptions? _options;
private readonly string? _unitId;
private bool _hasLoaded;
private global::Android.Gms.Ads.AppOpen.AppOpenAd? _ad;

public event EventHandler? OnAdLoaded;
public event EventHandler<AdError>? OnAdFailedToLoad;
public event EventHandler? OnAdPresented;
public event EventHandler<AdError>? OnAdFailedToPresent;
public event EventHandler? OnAdImpression;
public event EventHandler? OnAdClicked;
public event EventHandler? OnAdClosed;

internal AppOpenAd(AdMobOptions options, string? unitId = null)
{
_options = options;
_unitId = unitId;
}

public void Load()
{
var configBuilder = new RequestConfiguration.Builder();
if (_options?.TestDeviceIds is not null)
{
configBuilder.SetTestDeviceIds(_options.TestDeviceIds.ToList());
}
MobileAds.RequestConfiguration = configBuilder.Build();

if (_options is null || AdMob.Current.CanShowAds)
{
var adRequest = _options is null
? new global::Android.Gms.Ads.AdRequest.Builder().Build()
: AdRequest.GetRequest(typeof(AppOpenAd));

var callbacks = new AppOpenAdCallbacks();
callbacks.WhenAdLoaded += AdLoaded;
callbacks.WhenAdFailedToLoaded += (s, e) => OnAdFailedToLoad?.Invoke(this, new AdError(e.Message));

global::Android.Gms.Ads.AppOpen.AppOpenAd.Load(Application.Context,
string.IsNullOrWhiteSpace(_unitId) ? TestUnit : _unitId, adRequest, callbacks);
}
else
{
OnAdFailedToLoad?.Invoke(this, new AdError("Consent has not been granted"));
}
}

public void Show()
{
if (!_hasLoaded || _ad is null)
{
return;
}

var listener = new FullScreenContentCallback();
listener.AdPresented += (s, e) => OnAdPresented?.Invoke(this, EventArgs.Empty);
listener.AdFailedToPresent += (s, e) => OnAdFailedToPresent?.Invoke(this, new AdError(e.Message));
listener.AdImpression += (s, e) => OnAdImpression?.Invoke(this, EventArgs.Empty);
listener.AdClicked += (s, e) => OnAdClicked?.Invoke(this, EventArgs.Empty);
listener.AdClosed += (s, e) => OnAdClosed?.Invoke(this, EventArgs.Empty);

_ad.FullScreenContentCallback = listener;
_ad.Show(Activity);
}

private void AdLoaded(object? sender, global::Android.Gms.Ads.AppOpen.AppOpenAd appOpenAd)
{
_ad = appOpenAd;
_hasLoaded = true;

OnAdLoaded?.Invoke(this, EventArgs.Empty);
}
}
21 changes: 21 additions & 0 deletions src/Jc.AdMob.Avalonia.Android/AppOpenAdCallbacks.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using Android.Gms.Ads;

namespace Jc.AdMob.Avalonia.Android;

internal sealed class AppOpenAdCallbacks : AppOpenAdLoadCallback
{
public event EventHandler<global::Android.Gms.Ads.AppOpen.AppOpenAd> WhenAdLoaded;
public event EventHandler<LoadAdError> WhenAdFailedToLoaded;

public override void OnAdLoaded(global::Android.Gms.Ads.AppOpen.AppOpenAd appOpenAd)
{
base.OnAdLoaded(appOpenAd);
WhenAdLoaded?.Invoke(this, appOpenAd);
}

public override void OnAdFailedToLoad(LoadAdError error)
{
base.OnAdFailedToLoad(error);
WhenAdFailedToLoaded?.Invoke(this, error);
}
}
29 changes: 29 additions & 0 deletions src/Jc.AdMob.Avalonia.Android/AppOpenAdLoadCallback.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using Android.Runtime;

namespace Jc.AdMob.Avalonia.Android;

internal abstract class AppOpenAdLoadCallback : global::Android.Gms.Ads.AppOpen.AppOpenAd.AppOpenAdLoadCallback
{
[Register("onAdLoaded", "(Lcom/google/android/gms/ads/appopen/AppOpenAd;)V", "GetOnAdLoadedHandler")]
public virtual void OnAdLoaded(global::Android.Gms.Ads.AppOpen.AppOpenAd appOpenAd)
{
}

private static Delegate? cb_onAdLoaded;

#pragma warning disable IDE0051 // Remove unused private members
private static Delegate GetOnAdLoadedHandler()
#pragma warning restore IDE0051 // Remove unused private members
{
if (cb_onAdLoaded is null)
cb_onAdLoaded = JNINativeWrapper.CreateDelegate(n_onAdLoaded);
return cb_onAdLoaded;
}

private static void n_onAdLoaded(IntPtr jnienv, IntPtr native__this, IntPtr native_p0)
{
AppOpenAdLoadCallback @this = GetObject<AppOpenAdLoadCallback>(jnienv, native__this, JniHandleOwnership.DoNotTransfer)!;
global::Android.Gms.Ads.AppOpen.AppOpenAd result = GetObject<global::Android.Gms.Ads.AppOpen.AppOpenAd>(native_p0, JniHandleOwnership.DoNotTransfer)!;
@this.OnAdLoaded(result);
}
}
10 changes: 2 additions & 8 deletions src/Jc.AdMob.Avalonia.Android/InterstitialAd.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,6 @@ public sealed class InterstitialAd : IInterstitialAd
public event EventHandler? OnAdClicked;
public event EventHandler? OnAdClosed;

public InterstitialAd(string? unitId, IReadOnlyCollection<string>? testDeviceIds = null)
{
_unitId = unitId;
_testDeviceIds = testDeviceIds;
}

internal InterstitialAd(AdMobOptions options, string? unitId = null)
{
_options = options;
Expand All @@ -37,9 +31,9 @@ internal InterstitialAd(AdMobOptions options, string? unitId = null)
public void Load()
{
var configBuilder = new RequestConfiguration.Builder();
if (_testDeviceIds is not null)
if (_options?.TestDeviceIds is not null)
{
configBuilder.SetTestDeviceIds(_testDeviceIds.ToList());
configBuilder.SetTestDeviceIds(_options.TestDeviceIds.ToList());
}

MobileAds.RequestConfiguration = configBuilder.Build();
Expand Down
2 changes: 2 additions & 0 deletions src/Jc.AdMob.Avalonia.iOS/AppBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@ public static AppBuilder UseAdMob(this AppBuilder appBuilder, AdMobOptions optio
{
return appBuilder.AfterSetup(_ =>
{
AdMob.Current.AppOpen = new Avalonia.AppOpenAd();
AdMob.Current.Consent = new AdConsentiOS(options);
AdMob.Current.Interstitial = new Avalonia.InterstitialAd();
AdMob.Current.RewardedInterstitial = new Avalonia.RewardedInterstitialAd();
AdMob.Current.Rewarded = new Avalonia.RewardedAd();

Avalonia.AppOpenAd.ImplementationFactory = unitId => new AppOpenAd(options, unitId);
Avalonia.InterstitialAd.ImplementationFactory = unitId => new InterstitialAd(options, unitId);
Avalonia.RewardedInterstitialAd.ImplementationFactory = unitId => new RewardedInterstitialAd(options, unitId);
Avalonia.RewardedAd.ImplementationFactory = unitId => new RewardedAd(options, unitId);
Expand Down
Loading
Loading