@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
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import GoogleMobileAds
|
|
3
|
+
import UIKit
|
|
4
|
+
|
|
5
|
+
final class PluginNativeAdView: NativeAdView {
|
|
6
|
+
private let isSmall: Bool
|
|
7
|
+
private let attributionLabel = UILabel()
|
|
8
|
+
private let headlineLabel = UILabel()
|
|
9
|
+
private let bodyLabel = UILabel()
|
|
10
|
+
private let advertiserLabel = UILabel()
|
|
11
|
+
private let iconImageView = UIImageView()
|
|
12
|
+
private let actionButton = UIButton(type: .system)
|
|
13
|
+
private let nativeMediaView = MediaView()
|
|
14
|
+
|
|
15
|
+
init(nativeAd: NativeAd, template: String, style: [String: Any]) {
|
|
16
|
+
self.isSmall = template == "small"
|
|
17
|
+
super.init(frame: .zero)
|
|
18
|
+
clipsToBounds = true
|
|
19
|
+
configure(nativeAd: nativeAd, style: style)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
required init?(coder: NSCoder) {
|
|
23
|
+
fatalError("init(coder:) has not been implemented")
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
override func layoutSubviews() {
|
|
27
|
+
super.layoutSubviews()
|
|
28
|
+
let padding: CGFloat = 12
|
|
29
|
+
let available = bounds.insetBy(dx: padding, dy: padding)
|
|
30
|
+
|
|
31
|
+
attributionLabel.frame = CGRect(x: padding, y: padding, width: 28, height: 20)
|
|
32
|
+
|
|
33
|
+
if isSmall {
|
|
34
|
+
layoutDetails(
|
|
35
|
+
in: CGRect(
|
|
36
|
+
x: available.minX,
|
|
37
|
+
y: attributionLabel.frame.maxY + 4,
|
|
38
|
+
width: available.width,
|
|
39
|
+
height: max(0, available.maxY - attributionLabel.frame.maxY - 4)
|
|
40
|
+
)
|
|
41
|
+
)
|
|
42
|
+
} else {
|
|
43
|
+
let mediaHeight = max(120, available.height * 0.58)
|
|
44
|
+
nativeMediaView.frame = CGRect(
|
|
45
|
+
x: available.minX,
|
|
46
|
+
y: available.minY,
|
|
47
|
+
width: available.width,
|
|
48
|
+
height: min(mediaHeight, available.height)
|
|
49
|
+
)
|
|
50
|
+
layoutDetails(
|
|
51
|
+
in: CGRect(
|
|
52
|
+
x: available.minX,
|
|
53
|
+
y: nativeMediaView.frame.maxY + 8,
|
|
54
|
+
width: available.width,
|
|
55
|
+
height: max(0, available.maxY - nativeMediaView.frame.maxY - 8)
|
|
56
|
+
)
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
bringSubviewToFront(attributionLabel)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private func layoutDetails(in rect: CGRect) {
|
|
63
|
+
let headlineHeight = min(42, max(22, rect.height * 0.27))
|
|
64
|
+
headlineLabel.frame = CGRect(x: rect.minX, y: rect.minY, width: rect.width, height: headlineHeight)
|
|
65
|
+
let bodyHeight = bodyLabel.isHidden ? 0 : min(38, max(18, rect.height * 0.22))
|
|
66
|
+
bodyLabel.frame = CGRect(x: rect.minX, y: headlineLabel.frame.maxY, width: rect.width, height: bodyHeight)
|
|
67
|
+
|
|
68
|
+
let footerY = bodyLabel.frame.maxY + 4
|
|
69
|
+
let footerHeight = max(0, rect.maxY - footerY)
|
|
70
|
+
let iconSize = min(32, footerHeight)
|
|
71
|
+
iconImageView.frame = CGRect(x: rect.minX, y: footerY, width: iconSize, height: iconSize)
|
|
72
|
+
let actionWidth = actionButton.isHidden ? 0 : min(128, rect.width * 0.4)
|
|
73
|
+
actionButton.frame = CGRect(x: rect.maxX - actionWidth, y: footerY, width: actionWidth, height: min(40, footerHeight))
|
|
74
|
+
advertiserLabel.frame = CGRect(
|
|
75
|
+
x: iconImageView.isHidden ? rect.minX : iconImageView.frame.maxX + 6,
|
|
76
|
+
y: footerY,
|
|
77
|
+
width: max(0, actionButton.frame.minX - (iconImageView.isHidden ? rect.minX : iconImageView.frame.maxX + 6) - 6),
|
|
78
|
+
height: min(32, footerHeight)
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private func configure(nativeAd: NativeAd, style: [String: Any]) {
|
|
83
|
+
backgroundColor = color(style["backgroundColor"] as? String) ?? .white
|
|
84
|
+
layer.cornerRadius = number(style["cornerRadius"], fallback: 0)
|
|
85
|
+
layer.borderWidth = number(style["borderWidth"], fallback: 0)
|
|
86
|
+
layer.borderColor = (color(style["borderColor"] as? String) ?? .clear).cgColor
|
|
87
|
+
|
|
88
|
+
configureAttribution()
|
|
89
|
+
|
|
90
|
+
headlineLabel.text = nativeAd.headline
|
|
91
|
+
headlineLabel.numberOfLines = 2
|
|
92
|
+
headlineLabel.font = .boldSystemFont(
|
|
93
|
+
ofSize: fontSize(style["headlineFontSize"], fallback: 16, range: 12 ... 24)
|
|
94
|
+
)
|
|
95
|
+
headlineLabel.textColor = color(style["headlineColor"] as? String) ?? .black
|
|
96
|
+
addSubview(headlineLabel)
|
|
97
|
+
headlineView = headlineLabel
|
|
98
|
+
|
|
99
|
+
bodyLabel.text = nativeAd.body
|
|
100
|
+
bodyLabel.numberOfLines = isSmall ? 1 : 2
|
|
101
|
+
bodyLabel.font = .systemFont(ofSize: fontSize(style["bodyFontSize"], fallback: 13, range: 10 ... 18))
|
|
102
|
+
bodyLabel.textColor = color(style["bodyColor"] as? String) ?? .darkGray
|
|
103
|
+
bodyLabel.isHidden = isSmall || nativeAd.body == nil
|
|
104
|
+
addSubview(bodyLabel)
|
|
105
|
+
bodyView = bodyLabel
|
|
106
|
+
|
|
107
|
+
advertiserLabel.text = nativeAd.advertiser
|
|
108
|
+
advertiserLabel.font = .systemFont(ofSize: 11)
|
|
109
|
+
advertiserLabel.numberOfLines = 1
|
|
110
|
+
advertiserLabel.isHidden = nativeAd.advertiser == nil
|
|
111
|
+
addSubview(advertiserLabel)
|
|
112
|
+
advertiserView = advertiserLabel
|
|
113
|
+
|
|
114
|
+
iconImageView.image = nativeAd.icon?.image
|
|
115
|
+
iconImageView.contentMode = .scaleAspectFill
|
|
116
|
+
iconImageView.clipsToBounds = true
|
|
117
|
+
iconImageView.isHidden = nativeAd.icon == nil
|
|
118
|
+
addSubview(iconImageView)
|
|
119
|
+
iconView = iconImageView
|
|
120
|
+
|
|
121
|
+
actionButton.setTitle(nativeAd.callToAction, for: .normal)
|
|
122
|
+
actionButton.titleLabel?.font = .systemFont(
|
|
123
|
+
ofSize: fontSize(style["callToActionFontSize"], fallback: 13, range: 12 ... 18)
|
|
124
|
+
)
|
|
125
|
+
actionButton.setTitleColor(color(style["callToActionTextColor"] as? String) ?? .white, for: .normal)
|
|
126
|
+
actionButton.backgroundColor = color(style["callToActionBackgroundColor"] as? String) ?? UIColor(
|
|
127
|
+
red: 33 / 255,
|
|
128
|
+
green: 150 / 255,
|
|
129
|
+
blue: 243 / 255,
|
|
130
|
+
alpha: 1
|
|
131
|
+
)
|
|
132
|
+
actionButton.isUserInteractionEnabled = false
|
|
133
|
+
actionButton.isHidden = nativeAd.callToAction == nil
|
|
134
|
+
addSubview(actionButton)
|
|
135
|
+
callToActionView = actionButton
|
|
136
|
+
|
|
137
|
+
configureMedia(nativeAd)
|
|
138
|
+
|
|
139
|
+
self.nativeAd = nativeAd
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
private func configureAttribution() {
|
|
143
|
+
attributionLabel.text = "Ad"
|
|
144
|
+
attributionLabel.textAlignment = .center
|
|
145
|
+
attributionLabel.font = .boldSystemFont(ofSize: 10)
|
|
146
|
+
attributionLabel.textColor = .white
|
|
147
|
+
attributionLabel.backgroundColor = .darkGray
|
|
148
|
+
addSubview(attributionLabel)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
private func configureMedia(_ nativeAd: NativeAd) {
|
|
152
|
+
guard !isSmall else { return }
|
|
153
|
+
nativeMediaView.mediaContent = nativeAd.mediaContent
|
|
154
|
+
nativeMediaView.contentMode = .scaleAspectFit
|
|
155
|
+
nativeMediaView.clipsToBounds = true
|
|
156
|
+
addSubview(nativeMediaView)
|
|
157
|
+
mediaView = nativeMediaView
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
private func color(_ value: String?) -> UIColor? {
|
|
161
|
+
guard var value = value?.trimmingCharacters(in: .whitespacesAndNewlines) else { return nil }
|
|
162
|
+
if value.hasPrefix("#") { value.removeFirst() }
|
|
163
|
+
guard value.count == 6 || value.count == 8, let number = UInt64(value, radix: 16) else { return nil }
|
|
164
|
+
if value.count == 8 {
|
|
165
|
+
return UIColor(
|
|
166
|
+
red: CGFloat((number >> 24) & 0xff) / 255,
|
|
167
|
+
green: CGFloat((number >> 16) & 0xff) / 255,
|
|
168
|
+
blue: CGFloat((number >> 8) & 0xff) / 255,
|
|
169
|
+
alpha: CGFloat(number & 0xff) / 255
|
|
170
|
+
)
|
|
171
|
+
}
|
|
172
|
+
return UIColor(
|
|
173
|
+
red: CGFloat((number >> 16) & 0xff) / 255,
|
|
174
|
+
green: CGFloat((number >> 8) & 0xff) / 255,
|
|
175
|
+
blue: CGFloat(number & 0xff) / 255,
|
|
176
|
+
alpha: 1
|
|
177
|
+
)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private func number(_ value: Any?, fallback: CGFloat) -> CGFloat {
|
|
181
|
+
max(0, CGFloat((value as? NSNumber)?.doubleValue ?? Double(fallback)))
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
private func fontSize(_ value: Any?, fallback: CGFloat, range: ClosedRange<CGFloat>) -> CGFloat {
|
|
185
|
+
min(range.upperBound, max(range.lowerBound, number(value, fallback: fallback)))
|
|
186
|
+
}
|
|
187
|
+
}
|
|
@@ -16,10 +16,10 @@ class AdRewardExecutor: NSObject, FullScreenContentDelegate {
|
|
|
16
16
|
if let error = error {
|
|
17
17
|
NSLog("Rewarded ad failed to load with error: \(error.localizedDescription)")
|
|
18
18
|
self.plugin?.notifyListeners(RewardAdPluginEvents.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
|
|
|
@@ -50,7 +50,7 @@ class AdRewardExecutor: NSObject, FullScreenContentDelegate {
|
|
|
50
50
|
let impressionId = ad.responseInfo.responseIdentifier ?? ""
|
|
51
51
|
self.plugin?.notifyListeners(RewardAdPluginEvents.AdImpression.rawValue, data: [
|
|
52
52
|
"adUnitId": adUnitID,
|
|
53
|
-
"valueMicros": adValue.value.int64Value,
|
|
53
|
+
"valueMicros": adValue.value.multiplying(byPowerOf10: 6).int64Value,
|
|
54
54
|
"currencyCode": adValue.currencyCode,
|
|
55
55
|
"precision": adValue.precision.rawValue,
|
|
56
56
|
"networkName": networkName,
|
|
@@ -97,6 +97,7 @@ class AdRewardExecutor: NSObject, FullScreenContentDelegate {
|
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
func ad(_ ad: FullScreenPresentingAd, didFailToPresentFullScreenContentWithError error: Error) {
|
|
100
|
+
self.plugin?.resumeWebMedia(for: ad)
|
|
100
101
|
removeCurrentlyShowingAd()
|
|
101
102
|
NSLog("RewardFullScreenDelegate Ad failed to present full screen content with error \(error.localizedDescription).")
|
|
102
103
|
self.plugin?.notifyListeners(RewardAdPluginEvents.FailedToShow.rawValue, data: [
|
|
@@ -106,11 +107,17 @@ class AdRewardExecutor: NSObject, FullScreenContentDelegate {
|
|
|
106
107
|
}
|
|
107
108
|
|
|
108
109
|
func adWillPresentFullScreenContent(_ ad: FullScreenPresentingAd) {
|
|
110
|
+
self.plugin?.suspendWebMedia(for: ad)
|
|
109
111
|
NSLog("RewardFullScreenDelegate Ad did present full screen content.")
|
|
110
112
|
self.plugin?.notifyListeners(RewardAdPluginEvents.Showed.rawValue, data: [:])
|
|
111
113
|
}
|
|
112
114
|
|
|
115
|
+
func adDidRecordClick(_ advertisement: FullScreenPresentingAd) {
|
|
116
|
+
self.plugin?.notifyListeners(RewardAdPluginEvents.adClicked.rawValue, data: [:])
|
|
117
|
+
}
|
|
118
|
+
|
|
113
119
|
func adDidDismissFullScreenContent(_ ad: FullScreenPresentingAd) {
|
|
120
|
+
self.plugin?.resumeWebMedia(for: ad)
|
|
114
121
|
removeCurrentlyShowingAd()
|
|
115
122
|
NSLog("RewardFullScreenDelegate Ad did dismiss full screen content.")
|
|
116
123
|
self.plugin?.notifyListeners(RewardAdPluginEvents.Dismissed.rawValue, data: [:])
|
|
@@ -16,10 +16,10 @@ class AdRewardInterstitialExecutor: NSObject, FullScreenContentDelegate {
|
|
|
16
16
|
if let error = error {
|
|
17
17
|
NSLog("Rewarded interstitial ad failed to load with error: \(error.localizedDescription)")
|
|
18
18
|
self.plugin?.notifyListeners(RewardInterstitialAdPluginEvents.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
|
|
|
@@ -50,7 +50,7 @@ class AdRewardInterstitialExecutor: NSObject, FullScreenContentDelegate {
|
|
|
50
50
|
let impressionId = ad.responseInfo.responseIdentifier ?? ""
|
|
51
51
|
self.plugin?.notifyListeners(RewardInterstitialAdPluginEvents.AdImpression.rawValue, data: [
|
|
52
52
|
"adUnitId": adUnitID,
|
|
53
|
-
"valueMicros": adValue.value.int64Value,
|
|
53
|
+
"valueMicros": adValue.value.multiplying(byPowerOf10: 6).int64Value,
|
|
54
54
|
"currencyCode": adValue.currencyCode,
|
|
55
55
|
"precision": adValue.precision.rawValue,
|
|
56
56
|
"networkName": networkName,
|
|
@@ -97,6 +97,7 @@ class AdRewardInterstitialExecutor: NSObject, FullScreenContentDelegate {
|
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
func ad(_ ad: FullScreenPresentingAd, didFailToPresentFullScreenContentWithError error: Error) {
|
|
100
|
+
self.plugin?.resumeWebMedia(for: ad)
|
|
100
101
|
removeCurrentlyShowingAd()
|
|
101
102
|
NSLog("RewardFullScreenDelegate Ad failed to present full screen content with error \(error.localizedDescription).")
|
|
102
103
|
self.plugin?.notifyListeners(RewardInterstitialAdPluginEvents.FailedToShow.rawValue, data: [
|
|
@@ -106,11 +107,13 @@ class AdRewardInterstitialExecutor: NSObject, FullScreenContentDelegate {
|
|
|
106
107
|
}
|
|
107
108
|
|
|
108
109
|
func adWillPresentFullScreenContent(_ ad: FullScreenPresentingAd) {
|
|
110
|
+
self.plugin?.suspendWebMedia(for: ad)
|
|
109
111
|
NSLog("RewardFullScreenDelegate Ad did present full screen content.")
|
|
110
112
|
self.plugin?.notifyListeners(RewardInterstitialAdPluginEvents.Showed.rawValue, data: [:])
|
|
111
113
|
}
|
|
112
114
|
|
|
113
115
|
func adDidDismissFullScreenContent(_ ad: FullScreenPresentingAd) {
|
|
116
|
+
self.plugin?.resumeWebMedia(for: ad)
|
|
114
117
|
removeCurrentlyShowingAd()
|
|
115
118
|
NSLog("RewardFullScreenDelegate Ad did dismiss full screen content.")
|
|
116
119
|
self.plugin?.notifyListeners(RewardInterstitialAdPluginEvents.Dismissed.rawValue, data: [:])
|
|
@@ -1,15 +1,87 @@
|
|
|
1
1
|
import XCTest
|
|
2
2
|
@testable import AdMobPlugin
|
|
3
3
|
|
|
4
|
-
class
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
4
|
+
final class AdMobPluginTests: XCTestCase {
|
|
5
|
+
@MainActor
|
|
6
|
+
func testNormalSuspensionAndResume() async {
|
|
7
|
+
await checkSuspensions([(0, true, true), (0, false, false)])
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
@MainActor
|
|
11
|
+
func testFailedAdCannotReleaseAnotherAdsSuspension() async {
|
|
12
|
+
await checkSuspensions([(0, true, true), (1, false, nil), (0, false, false)])
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
@MainActor
|
|
16
|
+
func testOnlyLastAdResumesMedia() async {
|
|
17
|
+
await checkSuspensions([(0, true, true), (1, true, nil), (0, false, nil), (1, false, false)])
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
@MainActor
|
|
21
|
+
private func checkSuspensions(_ steps: [(Int, Bool, Bool?)]) async {
|
|
22
|
+
let plugin = MediaTestPlugin()
|
|
23
|
+
let owners = [NSObject(), NSObject()]
|
|
24
|
+
|
|
25
|
+
for (owner, suspend, expected) in steps {
|
|
26
|
+
let called = expectation(description: "WebKit suspension: \(String(describing: expected))")
|
|
27
|
+
called.isInverted = expected == nil
|
|
28
|
+
plugin.onSuspension = { actual in
|
|
29
|
+
XCTAssertEqual(actual, expected)
|
|
30
|
+
called.fulfill()
|
|
31
|
+
}
|
|
32
|
+
if suspend {
|
|
33
|
+
plugin.suspendWebMedia(for: owners[owner])
|
|
34
|
+
} else {
|
|
35
|
+
plugin.resumeWebMedia(for: owners[owner])
|
|
36
|
+
}
|
|
37
|
+
await fulfillment(of: [called], timeout: expected == nil ? 0.1 : 2)
|
|
38
|
+
}
|
|
39
|
+
plugin.onSuspension = nil
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
func testNativeFeedSessionsEnforceCapacityAndAllowReplacement() {
|
|
43
|
+
let sessions = NativeAdFeedSessions()
|
|
44
|
+
|
|
45
|
+
XCTAssertTrue(sessions.start(feedId: "first", sessionId: "session-1"))
|
|
46
|
+
XCTAssertTrue(sessions.start(feedId: "second", sessionId: "session-2"))
|
|
47
|
+
XCTAssertFalse(sessions.start(feedId: "third", sessionId: "session-3"))
|
|
48
|
+
XCTAssertTrue(sessions.start(feedId: "first", sessionId: "replacement"))
|
|
49
|
+
XCTAssertTrue(sessions.isCurrent(feedId: "first", sessionId: "replacement"))
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
func testNativeFeedSessionsRejectStalePlacementSequences() {
|
|
53
|
+
let sessions = NativeAdFeedSessions()
|
|
54
|
+
XCTAssertTrue(sessions.start(feedId: "feed", sessionId: "session"))
|
|
55
|
+
|
|
56
|
+
XCTAssertTrue(sessions.accepts(feedId: "feed", sequence: 1))
|
|
57
|
+
XCTAssertFalse(sessions.accepts(feedId: "feed", sequence: 1))
|
|
58
|
+
XCTAssertFalse(sessions.accepts(feedId: "feed", sequence: 0))
|
|
59
|
+
XCTAssertTrue(sessions.accepts(feedId: "feed", sequence: 2))
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
func testNativePlacementRejectsWrongFeedAndMalformedRect() {
|
|
63
|
+
let valid: [String: Any] = [
|
|
64
|
+
"visible": true,
|
|
65
|
+
"feedId": "feed",
|
|
66
|
+
"slotKey": "slot",
|
|
67
|
+
"generation": 1,
|
|
68
|
+
"rect": ["x": 0, "y": 0, "width": 144, "height": 300],
|
|
69
|
+
"clipRect": ["x": 0, "y": 0, "width": 144, "height": 300]
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
XCTAssertNotNil(NativeAdPlacementValue(valid, expectedFeedId: "feed"))
|
|
73
|
+
XCTAssertNil(NativeAdPlacementValue(valid, expectedFeedId: "another-feed"))
|
|
74
|
+
var malformed = valid
|
|
75
|
+
malformed["rect"] = ["x": 0, "y": 0, "width": 0, "height": 300]
|
|
76
|
+
XCTAssertNil(NativeAdPlacementValue(malformed, expectedFeedId: "feed"))
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private final class MediaTestPlugin: AdMobPlugin {
|
|
81
|
+
var onSuspension: ((Bool) -> Void)?
|
|
82
|
+
|
|
83
|
+
@MainActor
|
|
84
|
+
override func setWebMediaPlaybackSuspended(_ suspended: Bool) {
|
|
85
|
+
onSuspension?(suspended)
|
|
14
86
|
}
|
|
15
87
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import XCTest
|
|
2
|
+
import WebKit
|
|
3
|
+
@testable import AdMobPlugin
|
|
4
|
+
|
|
5
|
+
final class NativeAdScrollTrackerTests: XCTestCase {
|
|
6
|
+
@MainActor
|
|
7
|
+
func testScrollClipsPlacementsAndReleasingTrackerStopsUpdates() throws {
|
|
8
|
+
let window = UIWindow(frame: CGRect(x: 0, y: 0, width: 320, height: 640))
|
|
9
|
+
let web = WKWebView(frame: window.bounds)
|
|
10
|
+
window.addSubview(web)
|
|
11
|
+
let scroll = UIScrollView(frame: CGRect(x: 0, y: 80, width: 320, height: 500))
|
|
12
|
+
scroll.contentSize = CGSize(width: 320, height: 1200)
|
|
13
|
+
web.addSubview(scroll)
|
|
14
|
+
let placement = try XCTUnwrap(NativeAdPlacementValue([
|
|
15
|
+
"feedId": "feed", "slotKey": "ad", "generation": 1, "visible": true,
|
|
16
|
+
"rect": ["x": 10, "y": 400, "width": 300, "height": 320],
|
|
17
|
+
"clipRect": ["x": 10, "y": 400, "width": 300, "height": 320]
|
|
18
|
+
], expectedFeedId: "feed"))
|
|
19
|
+
let container: [String: Any] = [
|
|
20
|
+
"rect": ["x": 0, "y": 80, "width": 320, "height": 500],
|
|
21
|
+
"clipRect": ["x": 0, "y": 100, "width": 320, "height": 480],
|
|
22
|
+
"contentWidth": 320.0, "contentHeight": 1200.0
|
|
23
|
+
]
|
|
24
|
+
var rendered: [NativeAdPlacementValue] = []
|
|
25
|
+
var updates = 0
|
|
26
|
+
var tracker: NativeAdScrollTracker? = try XCTUnwrap(NativeAdScrollTracker(
|
|
27
|
+
container: container, webView: web, placements: [placement],
|
|
28
|
+
render: { rendered = $0; updates += 1 }
|
|
29
|
+
))
|
|
30
|
+
tracker?.update()
|
|
31
|
+
XCTAssertEqual(rendered.first?.clip, CGRect(x: 10, y: 480, width: 300, height: 100))
|
|
32
|
+
scroll.contentOffset.y = 450
|
|
33
|
+
XCTAssertEqual(rendered.first?.rect, CGRect(x: 10, y: 30, width: 300, height: 320))
|
|
34
|
+
XCTAssertEqual(rendered.first?.clip, CGRect(x: 10, y: 100, width: 300, height: 250))
|
|
35
|
+
scroll.contentOffset.y = 800
|
|
36
|
+
XCTAssertTrue(rendered.isEmpty)
|
|
37
|
+
tracker = nil
|
|
38
|
+
let previousUpdates = updates
|
|
39
|
+
scroll.contentOffset.y = 0
|
|
40
|
+
XCTAssertEqual(updates, previousUpdates)
|
|
41
|
+
|
|
42
|
+
let duplicate = UIScrollView(frame: scroll.frame)
|
|
43
|
+
duplicate.contentSize = scroll.contentSize
|
|
44
|
+
web.addSubview(duplicate)
|
|
45
|
+
XCTAssertNil(NativeAdScrollTracker(container: container, webView: web, placements: [placement], render: { _ in }))
|
|
46
|
+
}
|
|
47
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@capacitor-community/admob",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "8.
|
|
4
|
+
"version": "8.2.0-beta.pr454.sha49386d40323a",
|
|
5
5
|
"description": "A native plugin for AdMob",
|
|
6
6
|
"main": "dist/plugin.cjs.js",
|
|
7
7
|
"module": "dist/esm/index.js",
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
"android/src/main/",
|
|
15
15
|
"android/build.gradle",
|
|
16
16
|
"dist/",
|
|
17
|
+
"docs/",
|
|
17
18
|
"ios/Sources",
|
|
18
19
|
"ios/Tests",
|
|
19
20
|
"Package.swift",
|
|
@@ -28,9 +29,11 @@
|
|
|
28
29
|
"bugs": {
|
|
29
30
|
"url": "git@github.com:capacitor-community/admob.git/issues"
|
|
30
31
|
},
|
|
32
|
+
"homepage": "https://docs.rdlabo.dev/projects/capacitor-admob",
|
|
31
33
|
"scripts": {
|
|
32
34
|
"verify": "npm run verify:ios && npm run verify:android && npm run verify:web",
|
|
33
35
|
"verify:ios": "xcodebuild -scheme CapacitorCommunityAdmob -destination generic/platform=iOS",
|
|
36
|
+
"test:ios": "xcodebuild test -scheme CapacitorCommunityAdmob",
|
|
34
37
|
"verify:android": "cd android && ./gradlew clean build test && cd ..",
|
|
35
38
|
"verify:web": "npm run build",
|
|
36
39
|
"lint": "npm run eslint && npm run prettier -- --check && npm run swiftlint -- lint",
|