@capacitor-community/admob 8.1.0 → 8.2.0-beta.pr454.sha49386d40323a
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CapacitorCommunityAdmob.podspec +1 -1
- package/Package.swift +1 -1
- package/README.md +129 -323
- package/android/build.gradle +1 -1
- package/android/src/main/java/com/getcapacitor/community/admob/AdMob.java +86 -3
- package/android/src/main/java/com/getcapacitor/community/admob/banner/BannerExecutor.java +261 -141
- package/android/src/main/java/com/getcapacitor/community/admob/helpers/FullscreenPluginCallback.kt +5 -1
- package/android/src/main/java/com/getcapacitor/community/admob/interstitial/AdInterstitialExecutor.java +27 -31
- package/android/src/main/java/com/getcapacitor/community/admob/interstitial/InterstitialAdCallbackAndListeners.kt +1 -1
- package/android/src/main/java/com/getcapacitor/community/admob/models/LoadPluginEventNames.kt +2 -1
- package/android/src/main/java/com/getcapacitor/community/admob/nativeads/NativeAdExecutor.kt +378 -0
- package/android/src/main/java/com/getcapacitor/community/admob/nativeads/NativeAdFeedSessions.kt +35 -0
- package/android/src/main/java/com/getcapacitor/community/admob/nativeads/NativeAdPlacementValue.kt +44 -0
- package/android/src/main/java/com/getcapacitor/community/admob/nativeads/NativeAdPluginEvents.kt +11 -0
- package/android/src/main/java/com/getcapacitor/community/admob/nativeads/PluginNativeAdView.kt +187 -0
- package/android/src/main/java/com/getcapacitor/community/admob/rewarded/AdRewardExecutor.java +29 -33
- package/android/src/main/java/com/getcapacitor/community/admob/rewarded/RewardAdPluginEvents.kt +2 -1
- package/android/src/main/java/com/getcapacitor/community/admob/rewarded/RewardedAdCallbackAndListeners.kt +1 -1
- package/android/src/main/java/com/getcapacitor/community/admob/rewardedinterstitial/AdRewardInterstitialExecutor.java +29 -37
- package/android/src/main/java/com/getcapacitor/community/admob/rewardedinterstitial/RewardedInterstitialAdCallbackAndListeners.kt +1 -1
- package/dist/docs.json +33 -3
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/index.js +5 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/interstitial/interstitial-definitions.interface.d.ts +3 -0
- package/dist/esm/native-ads/index.d.ts +6 -0
- package/dist/esm/native-ads/index.js +4 -0
- package/dist/esm/native-ads/index.js.map +1 -0
- package/dist/esm/native-ads/native-ad-definitions.interface.d.ts +14 -0
- package/dist/esm/native-ads/native-ad-definitions.interface.js +2 -0
- package/dist/esm/native-ads/native-ad-definitions.interface.js.map +1 -0
- package/dist/esm/native-ads/native-ad-element.d.ts +9 -0
- package/dist/esm/native-ads/native-ad-element.js +53 -0
- package/dist/esm/native-ads/native-ad-element.js.map +1 -0
- package/dist/esm/native-ads/native-ad-event.interface.d.ts +5 -0
- package/dist/esm/native-ads/native-ad-event.interface.js +2 -0
- package/dist/esm/native-ads/native-ad-event.interface.js.map +1 -0
- package/dist/esm/native-ads/native-ad-feed.d.ts +72 -0
- package/dist/esm/native-ads/native-ad-feed.js +568 -0
- package/dist/esm/native-ads/native-ad-feed.js.map +1 -0
- package/dist/esm/native-ads/native-ad-geometry.d.ts +8 -0
- package/dist/esm/native-ads/native-ad-geometry.js +91 -0
- package/dist/esm/native-ads/native-ad-geometry.js.map +1 -0
- package/dist/esm/native-ads/native-ad-options.interface.d.ts +36 -0
- package/dist/esm/native-ads/native-ad-options.interface.js +2 -0
- package/dist/esm/native-ads/native-ad-options.interface.js.map +1 -0
- package/dist/esm/native-ads/native-ad-placement.interface.d.ts +26 -0
- package/dist/esm/native-ads/native-ad-placement.interface.js +2 -0
- package/dist/esm/native-ads/native-ad-placement.interface.js.map +1 -0
- package/dist/esm/native-ads/native-ad-plugin-events.enum.d.ts +9 -0
- package/dist/esm/native-ads/native-ad-plugin-events.enum.js +11 -0
- package/dist/esm/native-ads/native-ad-plugin-events.enum.js.map +1 -0
- package/dist/esm/native-ads/native-ad-style.interface.d.ts +30 -0
- package/dist/esm/native-ads/native-ad-style.interface.js +2 -0
- package/dist/esm/native-ads/native-ad-style.interface.js.map +1 -0
- package/dist/esm/native-ads/native-ad-template.enum.d.ts +11 -0
- package/dist/esm/native-ads/native-ad-template.enum.js +13 -0
- package/dist/esm/native-ads/native-ad-template.enum.js.map +1 -0
- package/dist/esm/reward/reward-ad-plugin-events.enum.d.ts +4 -0
- package/dist/esm/reward/reward-ad-plugin-events.enum.js +4 -0
- package/dist/esm/reward/reward-ad-plugin-events.enum.js.map +1 -1
- package/dist/esm/reward/reward-definitions.interface.d.ts +8 -0
- package/dist/esm/reward-interstitial/reward-interstitial-definitions.interface.d.ts +3 -0
- package/dist/esm/web.d.ts +9 -1
- package/dist/esm/web.js +15 -0
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +755 -1
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +755 -1
- package/dist/plugin.js.map +1 -1
- package/docs/app-open.md +53 -0
- package/docs/banner.md +100 -0
- package/docs/configuration.md +30 -0
- package/docs/consent.md +80 -0
- package/docs/events.md +76 -0
- package/docs/interstitial.md +47 -0
- package/docs/migration.md +146 -0
- package/docs/native-ads.md +161 -0
- package/docs/rewarded.md +148 -0
- package/docs/testing.md +67 -0
- package/ios/Sources/AdMobPlugin/AdMobPlugin.swift +106 -41
- package/ios/Sources/AdMobPlugin/AppOpen/AppOpenAdManager.swift +1 -1
- package/ios/Sources/AdMobPlugin/Banner/BannerExecutor.swift +1 -1
- package/ios/Sources/AdMobPlugin/Interstitial/AdInterstitialExecutor.swift +6 -3
- package/ios/Sources/AdMobPlugin/NativeAds/NativeAdExecutor.swift +354 -0
- package/ios/Sources/AdMobPlugin/NativeAds/NativeAdFeedSessions.swift +34 -0
- package/ios/Sources/AdMobPlugin/NativeAds/NativeAdPlacementValue.swift +50 -0
- package/ios/Sources/AdMobPlugin/NativeAds/NativeAdPluginEvents.swift +11 -0
- package/ios/Sources/AdMobPlugin/NativeAds/NativeAdScrollTracker.swift +66 -0
- package/ios/Sources/AdMobPlugin/NativeAds/NativeAdValues.swift +27 -0
- package/ios/Sources/AdMobPlugin/NativeAds/PluginNativeAdView.swift +187 -0
- package/ios/Sources/AdMobPlugin/Rewarded/AdRewardExecutor.swift +10 -3
- package/ios/Sources/AdMobPlugin/Rewarded/RewardAdPluginEvents.swift +1 -0
- package/ios/Sources/AdMobPlugin/RewardedInterstitial/AdRewardInterstitialExecutor.swift +6 -3
- package/ios/Tests/AdMobPluginTests/AdMobPluginTests.swift +82 -10
- package/ios/Tests/AdMobPluginTests/NativeAdScrollTrackerTests.swift +47 -0
- package/package.json +4 -1
|
@@ -23,6 +23,11 @@ public class AdMobPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
23
23
|
CAPPluginMethod(name: "resumeBanner", returnType: CAPPluginReturnPromise),
|
|
24
24
|
CAPPluginMethod(name: "hideBanner", returnType: CAPPluginReturnPromise),
|
|
25
25
|
CAPPluginMethod(name: "removeBanner", returnType: CAPPluginReturnPromise),
|
|
26
|
+
CAPPluginMethod(name: "startNativeAdFeed", returnType: CAPPluginReturnPromise),
|
|
27
|
+
CAPPluginMethod(name: "destroyNativeAdFeed", returnType: CAPPluginReturnPromise),
|
|
28
|
+
CAPPluginMethod(name: "loadNativeAd", returnType: CAPPluginReturnPromise),
|
|
29
|
+
CAPPluginMethod(name: "updateNativeAdPlacements", returnType: CAPPluginReturnPromise),
|
|
30
|
+
CAPPluginMethod(name: "removeNativeAd", returnType: CAPPluginReturnPromise),
|
|
26
31
|
CAPPluginMethod(name: "prepareInterstitial", returnType: CAPPluginReturnPromise),
|
|
27
32
|
CAPPluginMethod(name: "showInterstitial", returnType: CAPPluginReturnPromise),
|
|
28
33
|
CAPPluginMethod(name: "prepareRewardVideoAd", returnType: CAPPluginReturnPromise),
|
|
@@ -64,22 +69,40 @@ public class AdMobPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
64
69
|
private let adRewardExecutor = AdRewardExecutor()
|
|
65
70
|
private let adRewardInterstitialExecutor = AdRewardInterstitialExecutor()
|
|
66
71
|
private let consentExecutor = ConsentExecutor()
|
|
72
|
+
private let nativeAdExecutor = NativeAdExecutor()
|
|
67
73
|
|
|
68
|
-
|
|
69
|
-
* Enable SKAdNetwork to track conversions
|
|
70
|
-
* https://developers.google.com/admob/ios/ios14
|
|
71
|
-
*/
|
|
72
|
-
@objc func initialize(_ call: CAPPluginCall) {
|
|
73
|
-
self.bannerExecutor.plugin = self
|
|
74
|
-
self.adInterstitialExecutor.plugin = self
|
|
75
|
-
self.adRewardExecutor.plugin = self
|
|
76
|
-
self.adRewardInterstitialExecutor.plugin = self
|
|
77
|
-
self.adInterstitialExecutor.plugin = self
|
|
74
|
+
public override func load() {
|
|
78
75
|
self.consentExecutor.plugin = self
|
|
79
|
-
|
|
76
|
+
}
|
|
80
77
|
|
|
81
|
-
|
|
82
|
-
|
|
78
|
+
/// Full-screen ads that currently have web media suspended.
|
|
79
|
+
private var webMediaSuspenders = Set<ObjectIdentifier>()
|
|
80
|
+
|
|
81
|
+
/// Pauses all web media while a full-screen ad is up, so WebKit releases its audio before the ad plays
|
|
82
|
+
/// and sets it up again afterwards, instead of leaving web audio silent until the app restarts.
|
|
83
|
+
func suspendWebMedia(for owner: AnyObject) {
|
|
84
|
+
Task { @MainActor [weak self] in
|
|
85
|
+
guard let self, self.webMediaSuspenders.insert(ObjectIdentifier(owner)).inserted,
|
|
86
|
+
self.webMediaSuspenders.count == 1
|
|
87
|
+
else { return }
|
|
88
|
+
self.setWebMediaPlaybackSuspended(true)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/// Only resumes once every ad that suspended has resumed, so a failed present (which can arrive
|
|
93
|
+
/// without a preceding adWillPresentFullScreenContent) can't release another ad's suspension.
|
|
94
|
+
func resumeWebMedia(for owner: AnyObject) {
|
|
95
|
+
Task { @MainActor [weak self] in
|
|
96
|
+
guard let self, self.webMediaSuspenders.remove(ObjectIdentifier(owner)) != nil,
|
|
97
|
+
self.webMediaSuspenders.isEmpty
|
|
98
|
+
else { return }
|
|
99
|
+
self.setWebMediaPlaybackSuspended(false)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
@MainActor
|
|
104
|
+
func setWebMediaPlaybackSuspended(_ suspended: Bool) {
|
|
105
|
+
self.bridge?.webView?.setAllMediaPlaybackSuspended(suspended, completionHandler: nil)
|
|
83
106
|
}
|
|
84
107
|
|
|
85
108
|
/**
|
|
@@ -123,34 +146,12 @@ public class AdMobPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
123
146
|
}
|
|
124
147
|
}
|
|
125
148
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
let request = self.GADRequestWithOption(call.getBool("npa") ?? false)
|
|
133
|
-
|
|
134
|
-
DispatchQueue.main.async {
|
|
135
|
-
self.bannerExecutor.showBanner(call, request, adUnitID)
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
@objc func hideBanner(_ call: CAPPluginCall) {
|
|
140
|
-
DispatchQueue.main.async {
|
|
141
|
-
self.bannerExecutor.hideBanner(call)
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
@objc func resumeBanner(_ call: CAPPluginCall) {
|
|
146
|
-
DispatchQueue.main.async {
|
|
147
|
-
self.bannerExecutor.resumeBanner(call)
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
@objc func removeBanner(_ call: CAPPluginCall) {
|
|
152
|
-
DispatchQueue.main.async {
|
|
153
|
-
self.bannerExecutor.removeBanner(call)
|
|
149
|
+
deinit {
|
|
150
|
+
let executor = nativeAdExecutor
|
|
151
|
+
if Thread.isMainThread {
|
|
152
|
+
executor.destroyAll()
|
|
153
|
+
} else {
|
|
154
|
+
DispatchQueue.main.async { executor.destroyAll() }
|
|
154
155
|
}
|
|
155
156
|
}
|
|
156
157
|
|
|
@@ -346,3 +347,67 @@ public class AdMobPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
346
347
|
return window?.rootViewController
|
|
347
348
|
}
|
|
348
349
|
}
|
|
350
|
+
|
|
351
|
+
extension AdMobPlugin {
|
|
352
|
+
// MARK: SDK initialization
|
|
353
|
+
|
|
354
|
+
@objc func initialize(_ call: CAPPluginCall) {
|
|
355
|
+
bannerExecutor.plugin = self
|
|
356
|
+
adInterstitialExecutor.plugin = self
|
|
357
|
+
adRewardExecutor.plugin = self
|
|
358
|
+
adRewardInterstitialExecutor.plugin = self
|
|
359
|
+
adInterstitialExecutor.plugin = self
|
|
360
|
+
nativeAdExecutor.plugin = self
|
|
361
|
+
setRequestConfiguration(call)
|
|
362
|
+
|
|
363
|
+
MobileAds.shared.start(completionHandler: nil)
|
|
364
|
+
call.resolve([:])
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// MARK: Banner ads
|
|
368
|
+
|
|
369
|
+
@objc func showBanner(_ call: CAPPluginCall) {
|
|
370
|
+
let adUnitID = getAdId(call, "ca-app-pub-3940256099942544/6300978111")
|
|
371
|
+
let request = GADRequestWithOption(call.getBool("npa") ?? false)
|
|
372
|
+
DispatchQueue.main.async {
|
|
373
|
+
self.bannerExecutor.showBanner(call, request, adUnitID)
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
@objc func hideBanner(_ call: CAPPluginCall) {
|
|
378
|
+
DispatchQueue.main.async { self.bannerExecutor.hideBanner(call) }
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
@objc func resumeBanner(_ call: CAPPluginCall) {
|
|
382
|
+
DispatchQueue.main.async { self.bannerExecutor.resumeBanner(call) }
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
@objc func removeBanner(_ call: CAPPluginCall) {
|
|
386
|
+
DispatchQueue.main.async { self.bannerExecutor.removeBanner(call) }
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// MARK: Native ads research preview
|
|
390
|
+
|
|
391
|
+
@objc func startNativeAdFeed(_ call: CAPPluginCall) {
|
|
392
|
+
DispatchQueue.main.async { self.nativeAdExecutor.startFeed(call) }
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
@objc func destroyNativeAdFeed(_ call: CAPPluginCall) {
|
|
396
|
+
DispatchQueue.main.async { self.nativeAdExecutor.destroyFeed(call) }
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
@objc func loadNativeAd(_ call: CAPPluginCall) {
|
|
400
|
+
let request = GADRequestWithOption(call.getBool("npa") ?? false)
|
|
401
|
+
DispatchQueue.main.async {
|
|
402
|
+
self.nativeAdExecutor.load(call, request: request)
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
@objc func updateNativeAdPlacements(_ call: CAPPluginCall) {
|
|
407
|
+
DispatchQueue.main.async { self.nativeAdExecutor.updatePlacements(call) }
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
@objc func removeNativeAd(_ call: CAPPluginCall) {
|
|
411
|
+
DispatchQueue.main.async { self.nativeAdExecutor.remove(call) }
|
|
412
|
+
}
|
|
413
|
+
}
|
|
@@ -38,7 +38,7 @@ import UIKit
|
|
|
38
38
|
let networkName = ad.responseInfo.loadedAdNetworkResponseInfo?.adNetworkClassName ?? ""
|
|
39
39
|
let impressionId = ad.responseInfo.responseIdentifier ?? ""
|
|
40
40
|
onPaidEvent(
|
|
41
|
-
adValue.value.int64Value,
|
|
41
|
+
adValue.value.multiplying(byPowerOf10: 6).int64Value,
|
|
42
42
|
adValue.currencyCode,
|
|
43
43
|
adValue.precision.rawValue,
|
|
44
44
|
networkName,
|
|
@@ -55,7 +55,7 @@ class BannerExecutor: NSObject, BannerViewDelegate {
|
|
|
55
55
|
let impressionId = self.bannerView.responseInfo?.responseIdentifier ?? ""
|
|
56
56
|
self.plugin?.notifyListeners(BannerAdPluginEvents.AdPaid.rawValue, data: [
|
|
57
57
|
"adUnitId": self.bannerView.adUnitID ?? "",
|
|
58
|
-
"valueMicros": adValue.value.int64Value,
|
|
58
|
+
"valueMicros": adValue.value.multiplying(byPowerOf10: 6).int64Value,
|
|
59
59
|
"currencyCode": adValue.currencyCode,
|
|
60
60
|
"precision": adValue.precision.rawValue,
|
|
61
61
|
"networkName": networkName,
|
|
@@ -16,10 +16,10 @@ class AdInterstitialExecutor: NSObject, FullScreenContentDelegate {
|
|
|
16
16
|
if let error = error {
|
|
17
17
|
NSLog("Interstitial ad failed to load with error: \(error.localizedDescription)")
|
|
18
18
|
self.plugin?.notifyListeners(InterstitialAdPluginEvents.FailedToLoad.rawValue, data: [
|
|
19
|
-
"code":
|
|
19
|
+
"code": (error as NSError).code,
|
|
20
20
|
"message": error.localizedDescription
|
|
21
21
|
])
|
|
22
|
-
call.reject(
|
|
22
|
+
call.reject(error.localizedDescription, String((error as NSError).code), error)
|
|
23
23
|
return
|
|
24
24
|
}
|
|
25
25
|
|
|
@@ -30,7 +30,7 @@ class AdInterstitialExecutor: NSObject, FullScreenContentDelegate {
|
|
|
30
30
|
let impressionId = ad.responseInfo.responseIdentifier ?? ""
|
|
31
31
|
self.plugin?.notifyListeners(InterstitialAdPluginEvents.AdImpression.rawValue, data: [
|
|
32
32
|
"adUnitId": adUnitID,
|
|
33
|
-
"valueMicros": adValue.value.int64Value,
|
|
33
|
+
"valueMicros": adValue.value.multiplying(byPowerOf10: 6).int64Value,
|
|
34
34
|
"currencyCode": adValue.currencyCode,
|
|
35
35
|
"precision": adValue.precision.rawValue,
|
|
36
36
|
"networkName": networkName,
|
|
@@ -73,6 +73,7 @@ class AdInterstitialExecutor: NSObject, FullScreenContentDelegate {
|
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
func ad(_ ad: FullScreenPresentingAd, didFailToPresentFullScreenContentWithError error: Error) {
|
|
76
|
+
self.plugin?.resumeWebMedia(for: ad)
|
|
76
77
|
removeCurrentlyShowingAd()
|
|
77
78
|
NSLog("InterstitialFullScreenDelegate Ad failed to present full screen content with error \(error.localizedDescription).")
|
|
78
79
|
self.plugin?.notifyListeners(InterstitialAdPluginEvents.FailedToShow.rawValue, data: [
|
|
@@ -82,11 +83,13 @@ class AdInterstitialExecutor: NSObject, FullScreenContentDelegate {
|
|
|
82
83
|
}
|
|
83
84
|
|
|
84
85
|
func adWillPresentFullScreenContent(_ ad: FullScreenPresentingAd) {
|
|
86
|
+
self.plugin?.suspendWebMedia(for: ad)
|
|
85
87
|
NSLog("InterstitialFullScreenDelegate Ad did present full screen content.")
|
|
86
88
|
self.plugin?.notifyListeners(InterstitialAdPluginEvents.Showed.rawValue, data: [:])
|
|
87
89
|
}
|
|
88
90
|
|
|
89
91
|
func adDidDismissFullScreenContent(_ ad: FullScreenPresentingAd) {
|
|
92
|
+
self.plugin?.resumeWebMedia(for: ad)
|
|
90
93
|
removeCurrentlyShowingAd()
|
|
91
94
|
NSLog("InterstitialFullScreenDelegate Ad did dismiss full screen content.")
|
|
92
95
|
self.plugin?.notifyListeners(InterstitialAdPluginEvents.Dismissed.rawValue, data: [:])
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
import Capacitor
|
|
2
|
+
import Foundation
|
|
3
|
+
import GoogleMobileAds
|
|
4
|
+
import UIKit
|
|
5
|
+
|
|
6
|
+
final class NativeAdExecutor: NSObject, NativeAdLoaderDelegate, NativeAdDelegate {
|
|
7
|
+
static let testAdUnitID = "ca-app-pub-3940256099942544/3986624511"
|
|
8
|
+
private let maxAdsPerFeed = 3
|
|
9
|
+
|
|
10
|
+
weak var plugin: AdMobPlugin?
|
|
11
|
+
|
|
12
|
+
private final class State {
|
|
13
|
+
let feedId: String
|
|
14
|
+
let isSmall: Bool
|
|
15
|
+
let sessionId: String
|
|
16
|
+
let slotKey: String
|
|
17
|
+
let nativeAd: NativeAd
|
|
18
|
+
let adView: PluginNativeAdView
|
|
19
|
+
let clippingView: UIView
|
|
20
|
+
var generation = -1
|
|
21
|
+
|
|
22
|
+
init(
|
|
23
|
+
feedId: String,
|
|
24
|
+
isSmall: Bool,
|
|
25
|
+
sessionId: String,
|
|
26
|
+
slotKey: String,
|
|
27
|
+
nativeAd: NativeAd,
|
|
28
|
+
adView: PluginNativeAdView,
|
|
29
|
+
clippingView: UIView
|
|
30
|
+
) {
|
|
31
|
+
self.feedId = feedId
|
|
32
|
+
self.isSmall = isSmall
|
|
33
|
+
self.sessionId = sessionId
|
|
34
|
+
self.slotKey = slotKey
|
|
35
|
+
self.nativeAd = nativeAd
|
|
36
|
+
self.adView = adView
|
|
37
|
+
self.clippingView = clippingView
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
private struct Pending {
|
|
42
|
+
let loader: AdLoader
|
|
43
|
+
let call: CAPPluginCall
|
|
44
|
+
let feedId: String
|
|
45
|
+
let sessionId: String
|
|
46
|
+
let slotKey: String
|
|
47
|
+
let template: String
|
|
48
|
+
let style: [String: Any]
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
private var states: [String: State] = [:]
|
|
52
|
+
private var pending: [ObjectIdentifier: Pending] = [:]
|
|
53
|
+
private let feedSessions = NativeAdFeedSessions()
|
|
54
|
+
private var scrollTrackers: [String: NativeAdScrollTracker] = [:]
|
|
55
|
+
|
|
56
|
+
func startFeed(_ call: CAPPluginCall) {
|
|
57
|
+
guard let session = NativeAdValues.session(call) else { return }
|
|
58
|
+
let (feedId, sessionId) = session
|
|
59
|
+
if !feedSessions.isCurrent(feedId: feedId, sessionId: sessionId) { clearFeed(feedId) }
|
|
60
|
+
guard feedSessions.start(feedId: feedId, sessionId: sessionId) else {
|
|
61
|
+
call.reject("At most two native ad feeds can be active")
|
|
62
|
+
return
|
|
63
|
+
}
|
|
64
|
+
call.resolve()
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
func destroyFeed(_ call: CAPPluginCall) {
|
|
68
|
+
guard let session = NativeAdValues.session(call) else { return }
|
|
69
|
+
let (feedId, sessionId) = session
|
|
70
|
+
if feedSessions.isCurrent(feedId: feedId, sessionId: sessionId) {
|
|
71
|
+
clearFeed(feedId)
|
|
72
|
+
feedSessions.remove(feedId: feedId)
|
|
73
|
+
}
|
|
74
|
+
call.resolve()
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
func load(_ call: CAPPluginCall, request: Request) {
|
|
78
|
+
guard
|
|
79
|
+
let feedId = NativeAdValues.requiredString(call, "feedId"),
|
|
80
|
+
let sessionId = NativeAdValues.requiredString(call, "sessionId"),
|
|
81
|
+
let slotKey = NativeAdValues.requiredString(call, "slotKey")
|
|
82
|
+
else { return }
|
|
83
|
+
guard feedSessions.isCurrent(feedId: feedId, sessionId: sessionId) else {
|
|
84
|
+
call.reject("Native ad feed session is no longer active")
|
|
85
|
+
return
|
|
86
|
+
}
|
|
87
|
+
let adUnitID: String
|
|
88
|
+
if call.getBool("isTesting") == true {
|
|
89
|
+
adUnitID = Self.testAdUnitID
|
|
90
|
+
} else {
|
|
91
|
+
guard let requestedID = NativeAdValues.requiredString(call, "adId") else { return }
|
|
92
|
+
adUnitID = requestedID
|
|
93
|
+
}
|
|
94
|
+
let loader = AdLoader(
|
|
95
|
+
adUnitID: adUnitID,
|
|
96
|
+
rootViewController: plugin?.getRootVC(),
|
|
97
|
+
adTypes: [.native],
|
|
98
|
+
options: nil
|
|
99
|
+
)
|
|
100
|
+
let identifier = ObjectIdentifier(loader)
|
|
101
|
+
destroyState(NativeAdValues.key(feedId, slotKey))
|
|
102
|
+
cancelPending(feedId: feedId, slotKey: slotKey)
|
|
103
|
+
let feedAdCount = states.values.filter { $0.feedId == feedId }.count
|
|
104
|
+
+ pending.values.filter { $0.feedId == feedId }.count
|
|
105
|
+
guard feedAdCount < maxAdsPerFeed else {
|
|
106
|
+
call.reject("At most three native ads can be active in a feed")
|
|
107
|
+
return
|
|
108
|
+
}
|
|
109
|
+
pending[identifier] = Pending(
|
|
110
|
+
loader: loader,
|
|
111
|
+
call: call,
|
|
112
|
+
feedId: feedId,
|
|
113
|
+
sessionId: sessionId,
|
|
114
|
+
slotKey: slotKey,
|
|
115
|
+
template: call.getString("template") ?? "medium",
|
|
116
|
+
style: call.getObject("style") ?? [:]
|
|
117
|
+
)
|
|
118
|
+
loader.delegate = self
|
|
119
|
+
loader.load(request)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
func updatePlacements(_ call: CAPPluginCall) {
|
|
123
|
+
guard
|
|
124
|
+
let feedId = NativeAdValues.requiredString(call, "feedId"),
|
|
125
|
+
let sessionId = NativeAdValues.requiredString(call, "sessionId")
|
|
126
|
+
else { return }
|
|
127
|
+
guard feedSessions.isCurrent(feedId: feedId, sessionId: sessionId) else {
|
|
128
|
+
call.resolve()
|
|
129
|
+
return
|
|
130
|
+
}
|
|
131
|
+
let sequence = call.getInt("sequence") ?? -1
|
|
132
|
+
guard feedSessions.accepts(feedId: feedId, sequence: sequence) else {
|
|
133
|
+
call.resolve()
|
|
134
|
+
return
|
|
135
|
+
}
|
|
136
|
+
scrollTrackers.removeValue(forKey: feedId)
|
|
137
|
+
states.values.filter { $0.feedId == feedId }.forEach { $0.clippingView.isHidden = true }
|
|
138
|
+
|
|
139
|
+
if updateTrackedPlacements(call, feedId: feedId) { return }
|
|
140
|
+
|
|
141
|
+
for case let placement as [String: Any] in call.getArray("placements") ?? [] {
|
|
142
|
+
applyPlacement(placement, expectedFeedId: feedId)
|
|
143
|
+
}
|
|
144
|
+
call.resolve()
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
func remove(_ call: CAPPluginCall) {
|
|
148
|
+
guard
|
|
149
|
+
let feedId = NativeAdValues.requiredString(call, "feedId"),
|
|
150
|
+
let sessionId = NativeAdValues.requiredString(call, "sessionId"),
|
|
151
|
+
let slotKey = NativeAdValues.requiredString(call, "slotKey")
|
|
152
|
+
else { return }
|
|
153
|
+
guard feedSessions.isCurrent(feedId: feedId, sessionId: sessionId) else {
|
|
154
|
+
call.resolve()
|
|
155
|
+
return
|
|
156
|
+
}
|
|
157
|
+
cancelPending(feedId: feedId, slotKey: slotKey)
|
|
158
|
+
destroyState(NativeAdValues.key(feedId, slotKey))
|
|
159
|
+
call.resolve()
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
func destroyAll() {
|
|
163
|
+
scrollTrackers.removeAll()
|
|
164
|
+
for request in pending.values { request.call.reject("Native ad load was cancelled") }
|
|
165
|
+
for stateKey in Array(states.keys) { destroyState(stateKey) }
|
|
166
|
+
pending.removeAll()
|
|
167
|
+
feedSessions.removeAll()
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
func adLoader(_ adLoader: AdLoader, didReceive nativeAd: NativeAd) {
|
|
171
|
+
let identifier = ObjectIdentifier(adLoader)
|
|
172
|
+
guard let request = pending.removeValue(forKey: identifier) else { return }
|
|
173
|
+
|
|
174
|
+
if request.template == "small", nativeAd.mediaContent.hasVideoContent {
|
|
175
|
+
let message = "Small native ad template does not support video ads; use Medium"
|
|
176
|
+
var event = NativeAdValues.identity(request.feedId, request.sessionId, request.slotKey)
|
|
177
|
+
event["code"] = -1
|
|
178
|
+
event["message"] = message
|
|
179
|
+
plugin?.notifyListeners(NativeAdPluginEvents.failedToLoad.rawValue, data: event)
|
|
180
|
+
request.call.reject(message, "-1")
|
|
181
|
+
return
|
|
182
|
+
}
|
|
183
|
+
nativeAd.delegate = self
|
|
184
|
+
let adView = PluginNativeAdView(nativeAd: nativeAd, template: request.template, style: request.style)
|
|
185
|
+
let clippingView = UIView(frame: .zero)
|
|
186
|
+
clippingView.clipsToBounds = true
|
|
187
|
+
clippingView.isHidden = true
|
|
188
|
+
clippingView.addSubview(adView)
|
|
189
|
+
|
|
190
|
+
guard let webView = plugin?.bridge?.webView, let parent = webView.superview else {
|
|
191
|
+
nativeAd.delegate = nil
|
|
192
|
+
request.call.reject("AdMob WebView is unavailable")
|
|
193
|
+
return
|
|
194
|
+
}
|
|
195
|
+
parent.addSubview(clippingView)
|
|
196
|
+
let state = State(
|
|
197
|
+
feedId: request.feedId,
|
|
198
|
+
isSmall: request.template == "small",
|
|
199
|
+
sessionId: request.sessionId,
|
|
200
|
+
slotKey: request.slotKey,
|
|
201
|
+
nativeAd: nativeAd,
|
|
202
|
+
adView: adView,
|
|
203
|
+
clippingView: clippingView
|
|
204
|
+
)
|
|
205
|
+
states[NativeAdValues.key(request.feedId, request.slotKey)] = state
|
|
206
|
+
|
|
207
|
+
let feedId = request.feedId
|
|
208
|
+
let sessionId = request.sessionId
|
|
209
|
+
let slotKey = request.slotKey
|
|
210
|
+
let adUnitID = adLoader.adUnitID
|
|
211
|
+
nativeAd.paidEventHandler = { [weak self, weak nativeAd, weak state] adValue in
|
|
212
|
+
guard let self, let nativeAd, let state,
|
|
213
|
+
self.states[NativeAdValues.key(feedId, slotKey)] === state else { return }
|
|
214
|
+
self.plugin?.notifyListeners(NativeAdPluginEvents.adPaid.rawValue, data: [
|
|
215
|
+
"feedId": feedId,
|
|
216
|
+
"sessionId": sessionId,
|
|
217
|
+
"slotKey": slotKey,
|
|
218
|
+
"adUnitId": adUnitID,
|
|
219
|
+
"valueMicros": adValue.value.multiplying(byPowerOf10: 6).int64Value,
|
|
220
|
+
"currencyCode": adValue.currencyCode,
|
|
221
|
+
"precision": adValue.precision.rawValue,
|
|
222
|
+
"networkName": nativeAd.responseInfo.loadedAdNetworkResponseInfo?.adNetworkClassName ?? "",
|
|
223
|
+
"impressionId": nativeAd.responseInfo.responseIdentifier ?? ""
|
|
224
|
+
])
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
plugin?.notifyListeners(
|
|
228
|
+
NativeAdPluginEvents.loaded.rawValue,
|
|
229
|
+
data: NativeAdValues.identity(request.feedId, request.sessionId, request.slotKey)
|
|
230
|
+
)
|
|
231
|
+
request.call.resolve()
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
private func applyPlacement(_ placement: [String: Any], expectedFeedId: String) {
|
|
235
|
+
guard let value = NativeAdPlacementValue(placement, expectedFeedId: expectedFeedId) else { return }
|
|
236
|
+
applyPlacement(value)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
private func applyPlacement(_ placement: NativeAdPlacementValue) {
|
|
240
|
+
guard
|
|
241
|
+
let state = states[NativeAdValues.key(placement.feedId, placement.slotKey)],
|
|
242
|
+
placement.generation >= state.generation,
|
|
243
|
+
placement.rect.width >= (state.isSmall ? 120 : 144),
|
|
244
|
+
placement.rect.height >= (state.isSmall ? 120 : 300),
|
|
245
|
+
let webView = plugin?.bridge?.webView
|
|
246
|
+
else { return }
|
|
247
|
+
|
|
248
|
+
let webOrigin = webView.frame.origin
|
|
249
|
+
state.clippingView.frame = placement.clip.offsetBy(dx: webOrigin.x, dy: webOrigin.y)
|
|
250
|
+
state.adView.frame = CGRect(
|
|
251
|
+
x: placement.rect.minX - placement.clip.minX,
|
|
252
|
+
y: placement.rect.minY - placement.clip.minY,
|
|
253
|
+
width: placement.rect.width,
|
|
254
|
+
height: placement.rect.height
|
|
255
|
+
)
|
|
256
|
+
state.generation = placement.generation
|
|
257
|
+
state.clippingView.isHidden = false
|
|
258
|
+
state.clippingView.superview?.bringSubviewToFront(state.clippingView)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
private func notify(_ event: NativeAdPluginEvents, nativeAd: NativeAd) {
|
|
262
|
+
guard let state = states.values.first(where: { $0.nativeAd === nativeAd }) else { return }
|
|
263
|
+
plugin?.notifyListeners(
|
|
264
|
+
event.rawValue,
|
|
265
|
+
data: NativeAdValues.identity(state.feedId, state.sessionId, state.slotKey)
|
|
266
|
+
)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
private func destroyState(_ stateKey: String) {
|
|
270
|
+
guard let state = states.removeValue(forKey: stateKey) else { return }
|
|
271
|
+
state.nativeAd.delegate = nil
|
|
272
|
+
state.nativeAd.paidEventHandler = nil
|
|
273
|
+
state.adView.nativeAd = nil
|
|
274
|
+
state.clippingView.removeFromSuperview()
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
private func cancelPending(feedId: String, slotKey: String) {
|
|
278
|
+
let identifiers = pending.compactMap { identifier, request in
|
|
279
|
+
request.feedId == feedId && request.slotKey == slotKey ? identifier : nil
|
|
280
|
+
}
|
|
281
|
+
for identifier in identifiers {
|
|
282
|
+
pending.removeValue(forKey: identifier)?.call.reject("Native ad load was cancelled")
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
private func clearFeed(_ feedId: String) {
|
|
287
|
+
scrollTrackers.removeValue(forKey: feedId)
|
|
288
|
+
let requests = pending.filter { $0.value.feedId == feedId }
|
|
289
|
+
for (identifier, request) in requests {
|
|
290
|
+
pending.removeValue(forKey: identifier)
|
|
291
|
+
request.call.reject("Native ad load was cancelled")
|
|
292
|
+
}
|
|
293
|
+
for stateKey in states.filter({ $0.value.feedId == feedId }).keys {
|
|
294
|
+
destroyState(stateKey)
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
extension NativeAdExecutor {
|
|
301
|
+
private func updateTrackedPlacements(_ call: CAPPluginCall, feedId: String) -> Bool {
|
|
302
|
+
guard let container = call.getObject("scrollContainer") else { return false }
|
|
303
|
+
let placements = (call.getArray("placements") ?? []).compactMap { value -> NativeAdPlacementValue? in
|
|
304
|
+
guard let value = value as? [String: Any] else { return nil }
|
|
305
|
+
return NativeAdPlacementValue(value, expectedFeedId: feedId)
|
|
306
|
+
}
|
|
307
|
+
if placements.isEmpty {
|
|
308
|
+
call.resolve()
|
|
309
|
+
return true
|
|
310
|
+
}
|
|
311
|
+
guard let webView = plugin?.bridge?.webView,
|
|
312
|
+
let tracker = NativeAdScrollTracker(container: container, webView: webView, placements: placements, render: { [weak self] values in
|
|
313
|
+
guard let self else { return }
|
|
314
|
+
self.states.values.filter { $0.feedId == feedId }.forEach { $0.clippingView.isHidden = true }
|
|
315
|
+
values.forEach { self.applyPlacement($0) }
|
|
316
|
+
}) else {
|
|
317
|
+
call.reject("Cannot uniquely bind native ad scroll container")
|
|
318
|
+
return true
|
|
319
|
+
}
|
|
320
|
+
scrollTrackers[feedId] = tracker
|
|
321
|
+
tracker.update()
|
|
322
|
+
call.resolve()
|
|
323
|
+
return true
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
func adLoader(_ adLoader: AdLoader, didFailToReceiveAdWithError error: Error) {
|
|
327
|
+
let identifier = ObjectIdentifier(adLoader)
|
|
328
|
+
guard let request = pending.removeValue(forKey: identifier) else { return }
|
|
329
|
+
plugin?.notifyListeners(NativeAdPluginEvents.failedToLoad.rawValue, data: [
|
|
330
|
+
"feedId": request.feedId,
|
|
331
|
+
"sessionId": request.sessionId,
|
|
332
|
+
"slotKey": request.slotKey,
|
|
333
|
+
"code": (error as NSError).code,
|
|
334
|
+
"message": error.localizedDescription
|
|
335
|
+
])
|
|
336
|
+
request.call.reject(error.localizedDescription, String((error as NSError).code), error)
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
func nativeAdDidRecordClick(_ nativeAd: NativeAd) {
|
|
340
|
+
notify(.clicked, nativeAd: nativeAd)
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
func nativeAdDidRecordImpression(_ nativeAd: NativeAd) {
|
|
344
|
+
notify(.impression, nativeAd: nativeAd)
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
func nativeAdWillPresentScreen(_ nativeAd: NativeAd) {
|
|
348
|
+
notify(.opened, nativeAd: nativeAd)
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
func nativeAdDidDismissScreen(_ nativeAd: NativeAd) {
|
|
352
|
+
notify(.closed, nativeAd: nativeAd)
|
|
353
|
+
}
|
|
354
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
final class NativeAdFeedSessions {
|
|
4
|
+
private let maxFeeds = 2
|
|
5
|
+
private var sessions: [String: String] = [:]
|
|
6
|
+
private var placementSequences: [String: Int] = [:]
|
|
7
|
+
|
|
8
|
+
func start(feedId: String, sessionId: String) -> Bool {
|
|
9
|
+
guard sessions[feedId] != nil || sessions.count < maxFeeds else { return false }
|
|
10
|
+
sessions[feedId] = sessionId
|
|
11
|
+
placementSequences[feedId] = -1
|
|
12
|
+
return true
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
func isCurrent(feedId: String, sessionId: String) -> Bool {
|
|
16
|
+
sessions[feedId] == sessionId
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
func accepts(feedId: String, sequence: Int) -> Bool {
|
|
20
|
+
guard sequence > (placementSequences[feedId] ?? -1) else { return false }
|
|
21
|
+
placementSequences[feedId] = sequence
|
|
22
|
+
return true
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
func remove(feedId: String) {
|
|
26
|
+
sessions.removeValue(forKey: feedId)
|
|
27
|
+
placementSequences.removeValue(forKey: feedId)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
func removeAll() {
|
|
31
|
+
sessions.removeAll()
|
|
32
|
+
placementSequences.removeAll()
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
struct NativeAdPlacementValue {
|
|
4
|
+
let feedId: String
|
|
5
|
+
let slotKey: String
|
|
6
|
+
let generation: Int
|
|
7
|
+
var rect: CGRect
|
|
8
|
+
var clip: CGRect
|
|
9
|
+
|
|
10
|
+
init?(_ value: [String: Any], expectedFeedId: String) {
|
|
11
|
+
guard
|
|
12
|
+
value["visible"] as? Bool == true,
|
|
13
|
+
let feedId = value["feedId"] as? String,
|
|
14
|
+
feedId == expectedFeedId,
|
|
15
|
+
let slotKey = value["slotKey"] as? String,
|
|
16
|
+
let generation = (value["generation"] as? NSNumber)?.intValue,
|
|
17
|
+
let rectValue = value["rect"] as? [String: Any],
|
|
18
|
+
let clipValue = value["clipRect"] as? [String: Any],
|
|
19
|
+
let rect = Self.rect(rectValue),
|
|
20
|
+
let clip = Self.rect(clipValue),
|
|
21
|
+
rect.width > 0,
|
|
22
|
+
rect.height > 0,
|
|
23
|
+
clip.width > 0,
|
|
24
|
+
clip.height > 0
|
|
25
|
+
else { return nil }
|
|
26
|
+
|
|
27
|
+
self.feedId = feedId
|
|
28
|
+
self.slotKey = slotKey
|
|
29
|
+
self.generation = generation
|
|
30
|
+
self.rect = rect
|
|
31
|
+
self.clip = clip
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
func projected(origin: CGPoint, viewport: CGRect) -> Self? {
|
|
35
|
+
var result = self
|
|
36
|
+
result.rect = rect.offsetBy(dx: origin.x, dy: origin.y)
|
|
37
|
+
result.clip = clip.offsetBy(dx: origin.x, dy: origin.y).intersection(viewport)
|
|
38
|
+
return result.clip.isNull || result.clip.isEmpty ? nil : result
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
static func rect(_ value: [String: Any]) -> CGRect? {
|
|
42
|
+
guard
|
|
43
|
+
let originX = (value["x"] as? NSNumber)?.doubleValue,
|
|
44
|
+
let originY = (value["y"] as? NSNumber)?.doubleValue,
|
|
45
|
+
let width = (value["width"] as? NSNumber)?.doubleValue,
|
|
46
|
+
let height = (value["height"] as? NSNumber)?.doubleValue
|
|
47
|
+
else { return nil }
|
|
48
|
+
return CGRect(x: originX, y: originY, width: width, height: height)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
enum NativeAdPluginEvents: String {
|
|
4
|
+
case loaded = "nativeAdLoaded"
|
|
5
|
+
case failedToLoad = "nativeAdFailedToLoad"
|
|
6
|
+
case clicked = "nativeAdClicked"
|
|
7
|
+
case impression = "nativeAdImpression"
|
|
8
|
+
case opened = "nativeAdOpened"
|
|
9
|
+
case closed = "nativeAdClosed"
|
|
10
|
+
case adPaid = "nativeAdPaid"
|
|
11
|
+
}
|