@ququplay/admob 8.0.9 → 8.2.0

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 (36) hide show
  1. package/README.md +179 -0
  2. package/android/src/main/java/com/getcapacitor/community/admob/AdMob.java +38 -0
  3. package/android/src/main/java/com/getcapacitor/community/admob/nativead/NativeAdExecutor.java +391 -0
  4. package/android/src/main/java/com/getcapacitor/community/admob/nativead/NativeAdPluginEvents.kt +9 -0
  5. package/android/src/main/res/layout/admob_native_ad_strip.xml +112 -0
  6. package/android/src/main/res/values/strings.xml +4 -0
  7. package/android/src/main/res/values-pl/strings.xml +4 -0
  8. package/dist/docs.json +495 -0
  9. package/dist/esm/definitions.d.ts +2 -1
  10. package/dist/esm/definitions.js.map +1 -1
  11. package/dist/esm/index.d.ts +1 -0
  12. package/dist/esm/index.js +1 -0
  13. package/dist/esm/index.js.map +1 -1
  14. package/dist/esm/native-ad/index.d.ts +3 -0
  15. package/dist/esm/native-ad/index.js +4 -0
  16. package/dist/esm/native-ad/index.js.map +1 -0
  17. package/dist/esm/native-ad/native-ad-definitions.interface.d.ts +68 -0
  18. package/dist/esm/native-ad/native-ad-definitions.interface.js +2 -0
  19. package/dist/esm/native-ad/native-ad-definitions.interface.js.map +1 -0
  20. package/dist/esm/native-ad/native-ad-options.interface.d.ts +43 -0
  21. package/dist/esm/native-ad/native-ad-options.interface.js +2 -0
  22. package/dist/esm/native-ad/native-ad-options.interface.js.map +1 -0
  23. package/dist/esm/native-ad/native-ad-plugin-events.enum.d.ts +10 -0
  24. package/dist/esm/native-ad/native-ad-plugin-events.enum.js +13 -0
  25. package/dist/esm/native-ad/native-ad-plugin-events.enum.js.map +1 -0
  26. package/dist/esm/web.d.ts +4 -0
  27. package/dist/esm/web.js +9 -0
  28. package/dist/esm/web.js.map +1 -1
  29. package/dist/plugin.cjs.js +22 -0
  30. package/dist/plugin.cjs.js.map +1 -1
  31. package/dist/plugin.js +22 -0
  32. package/dist/plugin.js.map +1 -1
  33. package/ios/Sources/AdMobPlugin/AdMobPlugin.swift +30 -0
  34. package/ios/Sources/AdMobPlugin/NativeAd/NativeAdExecutor.swift +371 -0
  35. package/ios/Sources/AdMobPlugin/NativeAd/NativeAdPluginEvents.swift +7 -0
  36. package/package.json +1 -1
@@ -0,0 +1,371 @@
1
+ import Foundation
2
+ import Capacitor
3
+ import GoogleMobileAds
4
+
5
+ /**
6
+ * A native ad drawn as a strip across the top of the screen, below the status bar, over the WebView like a top banner.
7
+ * Hiding keeps the loaded ad, so showing it again brings back the same ad without a new request.
8
+ * With a refresh interval, a new ad replaces it once it has been on screen that long in all, hidden time and time in
9
+ * the background not counted; the old one stays up until the new one comes.
10
+ */
11
+ class NativeAdExecutor: NSObject, NativeAdLoaderDelegate, NativeAdDelegate {
12
+ weak var plugin: AdMobPlugin?
13
+
14
+ // A loaded native ad may be shown for an hour; a new one is loaded a little before that.
15
+ private static let maxAdAge: TimeInterval = 55 * 60
16
+
17
+ private var adView: NativeAdView?
18
+ private var nativeAd: NativeAd?
19
+ private var adLoader: AdLoader?
20
+ private var loadedAdUnitID: String?
21
+ private var loadedAt = Date.distantPast
22
+ private var wantsVisible = false
23
+ private var inBackground = false
24
+ // What the next refresh asks for: the last show's ad unit and request.
25
+ private var adUnitID: String?
26
+ private var request: Request?
27
+ private var refreshInterval: TimeInterval = 0
28
+ // How long the current ad has been on screen: the time before it was last hidden, and since when it shows.
29
+ private var shownTime: TimeInterval = 0
30
+ private var shownSince: Date?
31
+ private var refreshTimer: Timer?
32
+ private var observers: [NSObjectProtocol] = []
33
+
34
+ private let iconView = UIImageView()
35
+ private let badgeLabel = PaddedLabel()
36
+ private let headlineLabel = UILabel()
37
+ private let bodyLabel = UILabel()
38
+ private let callToActionLabel = PaddedLabel()
39
+ private let divider = UIView()
40
+ private let adChoicesView = AdChoicesView()
41
+
42
+ func showNativeAd(_ call: CAPPluginCall, _ request: Request, _ adUnitID: String) {
43
+ guard let rootViewController = plugin?.getRootVC() else {
44
+ call.reject("AdMob: no view to show the native ad in")
45
+ return
46
+ }
47
+
48
+ ensureAdView(in: rootViewController.view)
49
+ applyStyle(call.getObject("style") ?? [:])
50
+ observeApp()
51
+ wantsVisible = true
52
+ self.adUnitID = adUnitID
53
+ self.request = request
54
+ refreshInterval = TimeInterval(max(0, call.getInt("refreshInterval") ?? 0))
55
+
56
+ if nativeAd != nil && loadedAdUnitID == adUnitID && !isExpired() {
57
+ setVisible(true)
58
+ startCounting()
59
+ } else {
60
+ load(adUnitID, request, rootViewController)
61
+ }
62
+
63
+ call.resolve([:])
64
+ }
65
+
66
+ func hideNativeAd(_ call: CAPPluginCall) {
67
+ wantsVisible = false
68
+ stopCounting()
69
+ if adView != nil {
70
+ setVisible(false)
71
+ }
72
+ call.resolve([:])
73
+ }
74
+
75
+ func removeNativeAd(_ call: CAPPluginCall) {
76
+ wantsVisible = false
77
+ stopCounting()
78
+ nativeAd = nil
79
+ loadedAdUnitID = nil
80
+
81
+ if let adView = adView {
82
+ adView.removeFromSuperview()
83
+ self.adView = nil
84
+ notifySize(0, 0)
85
+ }
86
+
87
+ call.resolve([:])
88
+ }
89
+
90
+ private func load(_ adUnitID: String, _ request: Request, _ rootViewController: UIViewController) {
91
+ if adLoader != nil { return }
92
+
93
+ NSLog("AdMob: Native Ad ID: \(adUnitID)")
94
+ let options = NativeAdViewAdOptions()
95
+ options.preferredAdChoicesPosition = .topRightCorner
96
+
97
+ let loader = AdLoader(adUnitID: adUnitID, rootViewController: rootViewController, adTypes: [.native], options: [options])
98
+ loader.delegate = self
99
+ adLoader = loader
100
+ loader.load(request)
101
+ }
102
+
103
+ func adLoader(_ adLoader: AdLoader, didReceive nativeAd: NativeAd) {
104
+ self.adLoader = nil
105
+
106
+ // Removed while it loaded.
107
+ guard let adView = adView else { return }
108
+
109
+ stopCounting()
110
+ nativeAd.delegate = self
111
+ self.nativeAd = nativeAd
112
+ loadedAdUnitID = adLoader.adUnitID
113
+ loadedAt = Date()
114
+ shownTime = 0
115
+ populate(adView, nativeAd)
116
+ plugin?.notifyListeners(NativeAdPluginEvents.Loaded.rawValue, data: [:])
117
+
118
+ if wantsVisible {
119
+ setVisible(true)
120
+ startCounting()
121
+ }
122
+ }
123
+
124
+ func adLoader(_ adLoader: AdLoader, didFailToReceiveAdWithError error: Error) {
125
+ self.adLoader = nil
126
+ NSLog("AdMob: native ad failed to load: \(error.localizedDescription)")
127
+ stopCounting()
128
+
129
+ // An expired ad may not be shown again; a fresh one keeps showing, and the next refresh is tried after another
130
+ // interval on screen.
131
+ if nativeAd != nil && isExpired() {
132
+ nativeAd = nil
133
+ loadedAdUnitID = nil
134
+ }
135
+ if nativeAd == nil && adView != nil {
136
+ setVisible(false)
137
+ }
138
+ shownTime = 0
139
+ startCounting()
140
+
141
+ plugin?.notifyListeners(NativeAdPluginEvents.FailedToLoad.rawValue, data: [
142
+ "code": (error as NSError).code,
143
+ "message": error.localizedDescription
144
+ ])
145
+ }
146
+
147
+ func nativeAdDidRecordClick(_ nativeAd: NativeAd) {
148
+ plugin?.notifyListeners(NativeAdPluginEvents.Clicked.rawValue, data: [:])
149
+ }
150
+
151
+ func nativeAdDidRecordImpression(_ nativeAd: NativeAd) {
152
+ plugin?.notifyListeners(NativeAdPluginEvents.AdImpression.rawValue, data: [:])
153
+ }
154
+
155
+ // Counts the time the ad is on screen and asks for a new one when it has been there for the refresh interval.
156
+ private func startCounting() {
157
+ if shownSince != nil || refreshInterval <= 0 || nativeAd == nil || !wantsVisible || inBackground { return }
158
+
159
+ shownSince = Date()
160
+ refreshTimer = Timer.scheduledTimer(withTimeInterval: max(0, refreshInterval - shownTime), repeats: false) { [weak self] _ in
161
+ guard let self = self, let adUnitID = self.adUnitID, let request = self.request,
162
+ let rootViewController = self.plugin?.getRootVC() else { return }
163
+
164
+ self.load(adUnitID, request, rootViewController)
165
+ }
166
+ }
167
+
168
+ private func stopCounting() {
169
+ refreshTimer?.invalidate()
170
+ refreshTimer = nil
171
+ guard let shownSince = shownSince else { return }
172
+
173
+ shownTime += Date().timeIntervalSince(shownSince)
174
+ self.shownSince = nil
175
+ }
176
+
177
+ // The app went to the background, or came back: the ad is not on screen meanwhile.
178
+ private func observeApp() {
179
+ if !observers.isEmpty { return }
180
+
181
+ let center = NotificationCenter.default
182
+ observers = [
183
+ center.addObserver(forName: UIApplication.didEnterBackgroundNotification, object: nil, queue: .main) { [weak self] _ in
184
+ self?.inBackground = true
185
+ self?.stopCounting()
186
+ },
187
+ center.addObserver(forName: UIApplication.willEnterForegroundNotification, object: nil, queue: .main) { [weak self] _ in
188
+ self?.inBackground = false
189
+ self?.startCounting()
190
+ }
191
+ ]
192
+ }
193
+
194
+ // Icon, "Ad" badge, headline and body, the call to action and AdChoices in the top right corner; a hairline along
195
+ // the bottom separates it from the screen below. The wider trailing padding keeps the button clear of AdChoices.
196
+ private func ensureAdView(in parent: UIView) {
197
+ if adView != nil { return }
198
+
199
+ let adView = NativeAdView()
200
+ adView.translatesAutoresizingMaskIntoConstraints = false
201
+ adView.isHidden = true
202
+
203
+ iconView.contentMode = .scaleAspectFit
204
+ badgeLabel.text = Locale.preferredLanguages.first?.hasPrefix("pl") == true ? "Reklama" : "Ad"
205
+ badgeLabel.font = .boldSystemFont(ofSize: 10)
206
+ badgeLabel.insets = UIEdgeInsets(top: 1, left: 4, bottom: 1, right: 4)
207
+ badgeLabel.layer.cornerRadius = 3
208
+ badgeLabel.layer.masksToBounds = true
209
+ headlineLabel.font = .boldSystemFont(ofSize: 14)
210
+ bodyLabel.font = .systemFont(ofSize: 12)
211
+ callToActionLabel.font = .boldSystemFont(ofSize: 13)
212
+ callToActionLabel.insets = UIEdgeInsets(top: 0, left: 12, bottom: 0, right: 12)
213
+ callToActionLabel.layer.cornerRadius = 18
214
+ callToActionLabel.layer.masksToBounds = true
215
+ callToActionLabel.textAlignment = .center
216
+ // The SDK handles taps on the ad's views.
217
+ callToActionLabel.isUserInteractionEnabled = false
218
+
219
+ let headlineRow = UIStackView(arrangedSubviews: [badgeLabel, headlineLabel])
220
+ headlineRow.spacing = 6
221
+ headlineRow.alignment = .center
222
+ let texts = UIStackView(arrangedSubviews: [headlineRow, bodyLabel])
223
+ texts.axis = .vertical
224
+ texts.spacing = 2
225
+
226
+ badgeLabel.setContentHuggingPriority(.required, for: .horizontal)
227
+ badgeLabel.setContentCompressionResistancePriority(.required, for: .horizontal)
228
+ callToActionLabel.setContentHuggingPriority(.required, for: .horizontal)
229
+ callToActionLabel.setContentCompressionResistancePriority(.required, for: .horizontal)
230
+
231
+ for view in [iconView, texts, callToActionLabel, divider, adChoicesView] as [UIView] {
232
+ view.translatesAutoresizingMaskIntoConstraints = false
233
+ adView.addSubview(view)
234
+ }
235
+
236
+ let content = UILayoutGuide()
237
+ adView.addLayoutGuide(content)
238
+
239
+ NSLayoutConstraint.activate([
240
+ content.topAnchor.constraint(equalTo: adView.topAnchor, constant: 6),
241
+ content.leadingAnchor.constraint(equalTo: adView.leadingAnchor, constant: 12),
242
+ content.trailingAnchor.constraint(equalTo: adView.trailingAnchor, constant: -20),
243
+ content.bottomAnchor.constraint(equalTo: divider.topAnchor, constant: -7),
244
+ content.heightAnchor.constraint(greaterThanOrEqualToConstant: 41),
245
+
246
+ iconView.leadingAnchor.constraint(equalTo: content.leadingAnchor),
247
+ iconView.centerYAnchor.constraint(equalTo: content.centerYAnchor),
248
+ iconView.widthAnchor.constraint(equalToConstant: 40),
249
+ iconView.heightAnchor.constraint(equalToConstant: 40),
250
+
251
+ texts.leadingAnchor.constraint(equalTo: iconView.trailingAnchor, constant: 10),
252
+ texts.centerYAnchor.constraint(equalTo: content.centerYAnchor),
253
+ texts.topAnchor.constraint(greaterThanOrEqualTo: content.topAnchor),
254
+ texts.bottomAnchor.constraint(lessThanOrEqualTo: content.bottomAnchor),
255
+
256
+ callToActionLabel.leadingAnchor.constraint(equalTo: texts.trailingAnchor, constant: 10),
257
+ callToActionLabel.trailingAnchor.constraint(equalTo: content.trailingAnchor),
258
+ callToActionLabel.centerYAnchor.constraint(equalTo: content.centerYAnchor),
259
+ callToActionLabel.heightAnchor.constraint(equalToConstant: 36),
260
+ callToActionLabel.widthAnchor.constraint(greaterThanOrEqualToConstant: 64),
261
+ callToActionLabel.widthAnchor.constraint(lessThanOrEqualToConstant: 120),
262
+
263
+ divider.leadingAnchor.constraint(equalTo: adView.leadingAnchor),
264
+ divider.trailingAnchor.constraint(equalTo: adView.trailingAnchor),
265
+ divider.bottomAnchor.constraint(equalTo: adView.bottomAnchor),
266
+ divider.heightAnchor.constraint(equalToConstant: 1 / UIScreen.main.scale),
267
+
268
+ adChoicesView.topAnchor.constraint(equalTo: adView.topAnchor),
269
+ adChoicesView.trailingAnchor.constraint(equalTo: adView.trailingAnchor),
270
+ adChoicesView.widthAnchor.constraint(equalToConstant: 15),
271
+ adChoicesView.heightAnchor.constraint(equalToConstant: 15)
272
+ ])
273
+
274
+ adView.iconView = iconView
275
+ adView.headlineView = headlineLabel
276
+ adView.bodyView = bodyLabel
277
+ adView.callToActionView = callToActionLabel
278
+ adView.adChoicesView = adChoicesView
279
+
280
+ // Below the status bar, like a banner at the top; it follows the safe area when that changes (rotation).
281
+ parent.addSubview(adView)
282
+ NSLayoutConstraint.activate([
283
+ adView.topAnchor.constraint(equalTo: parent.safeAreaLayoutGuide.topAnchor),
284
+ adView.leadingAnchor.constraint(equalTo: parent.leadingAnchor),
285
+ adView.trailingAnchor.constraint(equalTo: parent.trailingAnchor)
286
+ ])
287
+
288
+ self.adView = adView
289
+ }
290
+
291
+ private func populate(_ adView: NativeAdView, _ nativeAd: NativeAd) {
292
+ headlineLabel.text = nativeAd.headline
293
+ bodyLabel.text = nativeAd.body
294
+ bodyLabel.isHidden = nativeAd.body == nil
295
+ iconView.image = nativeAd.icon?.image
296
+ iconView.isHidden = nativeAd.icon == nil
297
+ callToActionLabel.text = nativeAd.callToAction
298
+ callToActionLabel.isHidden = nativeAd.callToAction == nil
299
+
300
+ adView.nativeAd = nativeAd
301
+ }
302
+
303
+ private func applyStyle(_ style: JSObject) {
304
+ let textColor = color(style["textColor"], .black)
305
+
306
+ adView?.backgroundColor = color(style["backgroundColor"], .white)
307
+ headlineLabel.textColor = textColor
308
+ bodyLabel.textColor = color(style["secondaryTextColor"], textColor)
309
+ badgeLabel.backgroundColor = color(style["buttonColor"], UIColor(red: 26 / 255, green: 115 / 255, blue: 232 / 255, alpha: 1))
310
+ badgeLabel.textColor = color(style["buttonTextColor"], .white)
311
+ callToActionLabel.backgroundColor = badgeLabel.backgroundColor
312
+ callToActionLabel.textColor = badgeLabel.textColor
313
+ divider.backgroundColor = color(style["dividerColor"], .clear)
314
+ }
315
+
316
+ private func setVisible(_ visible: Bool) {
317
+ guard let adView = adView else { return }
318
+
319
+ adView.isHidden = !visible
320
+
321
+ if !visible {
322
+ notifySize(0, 0)
323
+ return
324
+ }
325
+
326
+ adView.superview?.layoutIfNeeded()
327
+ notifySize(adView.frame.width, adView.frame.height)
328
+ }
329
+
330
+ private func notifySize(_ width: CGFloat, _ height: CGFloat) {
331
+ plugin?.notifyListeners(NativeAdPluginEvents.SizeChanged.rawValue, data: [
332
+ "width": Int(width.rounded()),
333
+ "height": Int(height.rounded())
334
+ ])
335
+ }
336
+
337
+ private func isExpired() -> Bool {
338
+ return Date().timeIntervalSince(loadedAt) > NativeAdExecutor.maxAdAge
339
+ }
340
+
341
+ // "#RRGGBB" or "#AARRGGBB", as on Android.
342
+ private func color(_ value: JSValue?, _ fallback: UIColor) -> UIColor {
343
+ guard let string = value as? String, string.hasPrefix("#"),
344
+ let number = UInt64(string.dropFirst(), radix: 16) else { return fallback }
345
+
346
+ switch string.count {
347
+ case 7:
348
+ return UIColor(red: CGFloat((number >> 16) & 0xFF) / 255, green: CGFloat((number >> 8) & 0xFF) / 255,
349
+ blue: CGFloat(number & 0xFF) / 255, alpha: 1)
350
+ case 9:
351
+ return UIColor(red: CGFloat((number >> 16) & 0xFF) / 255, green: CGFloat((number >> 8) & 0xFF) / 255,
352
+ blue: CGFloat(number & 0xFF) / 255, alpha: CGFloat((number >> 24) & 0xFF) / 255)
353
+ default:
354
+ return fallback
355
+ }
356
+ }
357
+ }
358
+
359
+ // A label with padding around its text, for the badge and the call to action.
360
+ class PaddedLabel: UILabel {
361
+ var insets = UIEdgeInsets.zero
362
+
363
+ override func drawText(in rect: CGRect) {
364
+ super.drawText(in: rect.inset(by: insets))
365
+ }
366
+
367
+ override var intrinsicContentSize: CGSize {
368
+ let size = super.intrinsicContentSize
369
+ return CGSize(width: size.width + insets.left + insets.right, height: size.height + insets.top + insets.bottom)
370
+ }
371
+ }
@@ -0,0 +1,7 @@
1
+ public enum NativeAdPluginEvents: String {
2
+ case SizeChanged = "nativeAdSizeChanged"
3
+ case Loaded = "nativeAdLoaded"
4
+ case FailedToLoad = "nativeAdFailedToLoad"
5
+ case Clicked = "nativeAdClicked"
6
+ case AdImpression = "nativeAdImpression"
7
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ququplay/admob",
3
- "version": "8.0.9",
3
+ "version": "8.2.0",
4
4
  "description": "A native plugin for AdMob",
5
5
  "main": "dist/plugin.cjs.js",
6
6
  "module": "dist/esm/index.js",