@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.
Files changed (62) hide show
  1. package/README.md +1 -0
  2. package/android/src/main/java/com/getcapacitor/community/admob/AdMob.java +73 -0
  3. package/android/src/main/java/com/getcapacitor/community/admob/nativeads/NativeAdExecutor.kt +378 -0
  4. package/android/src/main/java/com/getcapacitor/community/admob/nativeads/NativeAdFeedSessions.kt +35 -0
  5. package/android/src/main/java/com/getcapacitor/community/admob/nativeads/NativeAdPlacementValue.kt +44 -0
  6. package/android/src/main/java/com/getcapacitor/community/admob/nativeads/NativeAdPluginEvents.kt +11 -0
  7. package/android/src/main/java/com/getcapacitor/community/admob/nativeads/PluginNativeAdView.kt +187 -0
  8. package/dist/esm/index.d.ts +1 -0
  9. package/dist/esm/index.js +5 -1
  10. package/dist/esm/index.js.map +1 -1
  11. package/dist/esm/native-ads/index.d.ts +6 -0
  12. package/dist/esm/native-ads/index.js +4 -0
  13. package/dist/esm/native-ads/index.js.map +1 -0
  14. package/dist/esm/native-ads/native-ad-definitions.interface.d.ts +14 -0
  15. package/dist/esm/native-ads/native-ad-definitions.interface.js +2 -0
  16. package/dist/esm/native-ads/native-ad-definitions.interface.js.map +1 -0
  17. package/dist/esm/native-ads/native-ad-element.d.ts +9 -0
  18. package/dist/esm/native-ads/native-ad-element.js +53 -0
  19. package/dist/esm/native-ads/native-ad-element.js.map +1 -0
  20. package/dist/esm/native-ads/native-ad-event.interface.d.ts +5 -0
  21. package/dist/esm/native-ads/native-ad-event.interface.js +2 -0
  22. package/dist/esm/native-ads/native-ad-event.interface.js.map +1 -0
  23. package/dist/esm/native-ads/native-ad-feed.d.ts +72 -0
  24. package/dist/esm/native-ads/native-ad-feed.js +568 -0
  25. package/dist/esm/native-ads/native-ad-feed.js.map +1 -0
  26. package/dist/esm/native-ads/native-ad-geometry.d.ts +8 -0
  27. package/dist/esm/native-ads/native-ad-geometry.js +91 -0
  28. package/dist/esm/native-ads/native-ad-geometry.js.map +1 -0
  29. package/dist/esm/native-ads/native-ad-options.interface.d.ts +36 -0
  30. package/dist/esm/native-ads/native-ad-options.interface.js +2 -0
  31. package/dist/esm/native-ads/native-ad-options.interface.js.map +1 -0
  32. package/dist/esm/native-ads/native-ad-placement.interface.d.ts +26 -0
  33. package/dist/esm/native-ads/native-ad-placement.interface.js +2 -0
  34. package/dist/esm/native-ads/native-ad-placement.interface.js.map +1 -0
  35. package/dist/esm/native-ads/native-ad-plugin-events.enum.d.ts +9 -0
  36. package/dist/esm/native-ads/native-ad-plugin-events.enum.js +11 -0
  37. package/dist/esm/native-ads/native-ad-plugin-events.enum.js.map +1 -0
  38. package/dist/esm/native-ads/native-ad-style.interface.d.ts +30 -0
  39. package/dist/esm/native-ads/native-ad-style.interface.js +2 -0
  40. package/dist/esm/native-ads/native-ad-style.interface.js.map +1 -0
  41. package/dist/esm/native-ads/native-ad-template.enum.d.ts +11 -0
  42. package/dist/esm/native-ads/native-ad-template.enum.js +13 -0
  43. package/dist/esm/native-ads/native-ad-template.enum.js.map +1 -0
  44. package/dist/esm/web.d.ts +9 -1
  45. package/dist/esm/web.js +15 -0
  46. package/dist/esm/web.js.map +1 -1
  47. package/dist/plugin.cjs.js +751 -1
  48. package/dist/plugin.cjs.js.map +1 -1
  49. package/dist/plugin.js +751 -1
  50. package/dist/plugin.js.map +1 -1
  51. package/docs/native-ads.md +161 -0
  52. package/ios/Sources/AdMobPlugin/AdMobPlugin.swift +76 -44
  53. package/ios/Sources/AdMobPlugin/NativeAds/NativeAdExecutor.swift +354 -0
  54. package/ios/Sources/AdMobPlugin/NativeAds/NativeAdFeedSessions.swift +34 -0
  55. package/ios/Sources/AdMobPlugin/NativeAds/NativeAdPlacementValue.swift +50 -0
  56. package/ios/Sources/AdMobPlugin/NativeAds/NativeAdPluginEvents.swift +11 -0
  57. package/ios/Sources/AdMobPlugin/NativeAds/NativeAdScrollTracker.swift +66 -0
  58. package/ios/Sources/AdMobPlugin/NativeAds/NativeAdValues.swift +27 -0
  59. package/ios/Sources/AdMobPlugin/NativeAds/PluginNativeAdView.swift +187 -0
  60. package/ios/Tests/AdMobPluginTests/AdMobPluginTests.swift +37 -0
  61. package/ios/Tests/AdMobPluginTests/NativeAdScrollTrackerTests.swift +47 -0
  62. 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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@capacitor-community/admob",
3
3
  "private": false,
4
- "version": "8.2.0-beta.pr484.shaa6be63853bbd",
4
+ "version": "8.2.1",
5
5
  "description": "A native plugin for AdMob",
6
6
  "main": "dist/plugin.cjs.js",
7
7
  "module": "dist/esm/index.js",