@capacitor-community/admob 8.2.0-beta.pr484.shaa6be63853bbd → 8.2.1
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/README.md +1 -0
- package/android/src/main/java/com/getcapacitor/community/admob/AdMob.java +73 -0
- 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/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 +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/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 +751 -1
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +751 -1
- package/dist/plugin.js.map +1 -1
- package/docs/native-ads.md +161 -0
- package/ios/Sources/AdMobPlugin/AdMobPlugin.swift +76 -44
- 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/Tests/AdMobPluginTests/AdMobPluginTests.swift +37 -0
- package/ios/Tests/AdMobPluginTests/NativeAdScrollTrackerTests.swift +47 -0
- package/package.json +1 -1
|
@@ -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
|
+
}
|
|
@@ -38,6 +38,43 @@ final class AdMobPluginTests: XCTestCase {
|
|
|
38
38
|
}
|
|
39
39
|
plugin.onSuspension = nil
|
|
40
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
|
+
}
|
|
41
78
|
}
|
|
42
79
|
|
|
43
80
|
private final class MediaTestPlugin: AdMobPlugin {
|
|
@@ -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