@capacitor-community/admob 8.2.0 → 8.2.1-beta.pr490.shaf4a3cab095f4
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 +8 -7
- package/android/build.gradle +5 -1
- package/android/src/main/java/com/getcapacitor/community/admob/AdMob.java +88 -0
- package/android/src/main/java/com/getcapacitor/community/admob/nativeads/NativeAdExecutor.kt +626 -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/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/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 +20 -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 +643 -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 +92 -0
- package/dist/esm/native-ads/native-ad-geometry.js.map +1 -0
- package/dist/esm/native-ads/native-ad-options.interface.d.ts +40 -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 +28 -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/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 +883 -1
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +883 -1
- package/dist/plugin.js.map +1 -1
- package/docs/configuration.md +2 -2
- package/docs/consent.md +6 -6
- package/docs/events.md +3 -2
- package/docs/migration.md +22 -2
- package/docs/native-ads.md +187 -0
- package/docs/rewarded.md +13 -0
- package/ios/Sources/AdMobPlugin/AdMobPlugin.swift +103 -41
- package/ios/Sources/AdMobPlugin/AppOpen/AppOpenAdPluginEvents.swift +3 -0
- package/ios/Sources/AdMobPlugin/Banner/BannerAdPluginEvents.swift +3 -0
- package/ios/Sources/AdMobPlugin/Banner/BannerAdSizeEnum.swift +3 -0
- package/ios/Sources/AdMobPlugin/Banner/BannerExecutor.swift +4 -4
- package/ios/Sources/AdMobPlugin/Helper/AuthorizationStatusEnum.swift +3 -0
- package/ios/Sources/AdMobPlugin/Interstitial/AdInterstitialExecutor.swift +3 -0
- package/ios/Sources/AdMobPlugin/Interstitial/InterstitialAdPluginEvents.swift +3 -0
- 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 +3 -0
- package/ios/Sources/AdMobPlugin/Rewarded/RewardAdPluginEvents.swift +3 -0
- package/ios/Sources/AdMobPlugin/RewardedInterstitial/AdRewardInterstitialExecutor.swift +3 -0
- package/ios/Sources/AdMobPlugin/RewardedInterstitial/RewardInterstitialAdPluginEvents.swift +3 -0
- package/ios/Tests/AdMobPluginTests/AdMobPluginTests.swift +82 -10
- package/ios/Tests/AdMobPluginTests/NativeAdScrollTrackerTests.swift +47 -0
- package/package.json +6 -4
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import UIKit
|
|
2
|
+
import WebKit
|
|
3
|
+
|
|
4
|
+
/// Research-preview binding to a single HTML overflow scroll container.
|
|
5
|
+
final class NativeAdScrollTracker {
|
|
6
|
+
private weak var webView: WKWebView?
|
|
7
|
+
private weak var scrollView: UIScrollView?
|
|
8
|
+
private let placements: [NativeAdPlacementValue]
|
|
9
|
+
private let clip: CGRect
|
|
10
|
+
private let render: ([NativeAdPlacementValue]) -> Void
|
|
11
|
+
private var observation: NSKeyValueObservation?
|
|
12
|
+
|
|
13
|
+
init?(
|
|
14
|
+
container: [String: Any],
|
|
15
|
+
webView: WKWebView,
|
|
16
|
+
placements: [NativeAdPlacementValue],
|
|
17
|
+
render: @escaping ([NativeAdPlacementValue]) -> Void
|
|
18
|
+
) {
|
|
19
|
+
guard
|
|
20
|
+
let rectValue = container["rect"] as? [String: Any],
|
|
21
|
+
let clipValue = container["clipRect"] as? [String: Any],
|
|
22
|
+
let rect = NativeAdPlacementValue.rect(rectValue),
|
|
23
|
+
let clip = NativeAdPlacementValue.rect(clipValue),
|
|
24
|
+
let width = container["contentWidth"] as? Double,
|
|
25
|
+
let height = container["contentHeight"] as? Double,
|
|
26
|
+
[rect.minX, rect.minY, rect.width, rect.height,
|
|
27
|
+
clip.minX, clip.minY, clip.width, clip.height, width, height].allSatisfy({ $0.isFinite }),
|
|
28
|
+
rect.width > 0, rect.height > 0
|
|
29
|
+
else { return nil }
|
|
30
|
+
|
|
31
|
+
let candidates = Self.scrollViews(in: webView).filter { scroll in
|
|
32
|
+
let frame = scroll.convert(scroll.bounds, to: webView)
|
|
33
|
+
return scroll !== webView.scrollView && !scroll.isHidden && scroll.window != nil &&
|
|
34
|
+
abs(scroll.zoomScale - 1) < 0.001 &&
|
|
35
|
+
abs(frame.minX - rect.minX) < 2 && abs(frame.minY - rect.minY) < 2 &&
|
|
36
|
+
abs(frame.width - rect.width) < 2 && abs(frame.height - rect.height) < 2 &&
|
|
37
|
+
abs(scroll.contentSize.width - width) < 3 && abs(scroll.contentSize.height - height) < 3
|
|
38
|
+
}
|
|
39
|
+
guard candidates.count == 1, let scroll = candidates.first else { return nil }
|
|
40
|
+
self.webView = webView
|
|
41
|
+
self.scrollView = scroll
|
|
42
|
+
self.placements = placements
|
|
43
|
+
self.clip = clip
|
|
44
|
+
self.render = render
|
|
45
|
+
observation = scroll.observe(\.contentOffset, options: [.new]) { [weak self] _, _ in
|
|
46
|
+
self?.update()
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
func update() {
|
|
51
|
+
guard let webView, let scrollView, scrollView.isDescendant(of: webView) else {
|
|
52
|
+
render([])
|
|
53
|
+
return
|
|
54
|
+
}
|
|
55
|
+
let frame = scrollView.convert(scrollView.bounds, to: webView)
|
|
56
|
+
let viewport = clip.intersection(webView.bounds).intersection(frame)
|
|
57
|
+
let origin = CGPoint(x: frame.minX - scrollView.contentOffset.x, y: frame.minY - scrollView.contentOffset.y)
|
|
58
|
+
render(placements.compactMap { $0.projected(origin: origin, viewport: viewport) })
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
private static func scrollViews(in view: UIView) -> [UIScrollView] {
|
|
62
|
+
view.subviews.flatMap { child in
|
|
63
|
+
((child as? UIScrollView).map { [$0] } ?? []) + scrollViews(in: child)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import Capacitor
|
|
2
|
+
import Foundation
|
|
3
|
+
|
|
4
|
+
enum NativeAdValues {
|
|
5
|
+
static func session(_ call: CAPPluginCall) -> (feedId: String, sessionId: String)? {
|
|
6
|
+
guard let feedId = requiredString(call, "feedId"), let sessionId = requiredString(call, "sessionId") else {
|
|
7
|
+
return nil
|
|
8
|
+
}
|
|
9
|
+
return (feedId, sessionId)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
static func requiredString(_ call: CAPPluginCall, _ name: String) -> String? {
|
|
13
|
+
guard let value = call.getString(name)?.trimmingCharacters(in: .whitespacesAndNewlines), !value.isEmpty else {
|
|
14
|
+
call.reject("\(name) must not be empty")
|
|
15
|
+
return nil
|
|
16
|
+
}
|
|
17
|
+
return value
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
static func key(_ feedId: String, _ slotKey: String) -> String {
|
|
21
|
+
"\(feedId)\u{0}\(slotKey)"
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
static func identity(_ feedId: String, _ sessionId: String, _ slotKey: String) -> [String: Any] {
|
|
25
|
+
["feedId": feedId, "sessionId": sessionId, "slotKey": slotKey]
|
|
26
|
+
}
|
|
27
|
+
}
|