@capacitor-community/admob 8.2.1 → 8.3.0-beta.pr491.shab64a7cd68709

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 (26) hide show
  1. package/README.md +1 -1
  2. package/android/build.gradle +5 -1
  3. package/android/src/main/java/com/getcapacitor/community/admob/AdMob.java +15 -0
  4. package/android/src/main/java/com/getcapacitor/community/admob/nativeads/NativeAdExecutor.kt +257 -9
  5. package/dist/esm/native-ads/native-ad-definitions.interface.d.ts +6 -0
  6. package/dist/esm/native-ads/native-ad-feed.d.ts +3 -3
  7. package/dist/esm/native-ads/native-ad-feed.js +107 -32
  8. package/dist/esm/native-ads/native-ad-feed.js.map +1 -1
  9. package/dist/esm/native-ads/native-ad-geometry.js +2 -1
  10. package/dist/esm/native-ads/native-ad-geometry.js.map +1 -1
  11. package/dist/esm/native-ads/native-ad-options.interface.d.ts +6 -2
  12. package/dist/esm/native-ads/native-ad-placement.interface.d.ts +2 -0
  13. package/dist/plugin.cjs.js +165 -33
  14. package/dist/plugin.cjs.js.map +1 -1
  15. package/dist/plugin.js +165 -33
  16. package/dist/plugin.js.map +1 -1
  17. package/docs/native-ads.md +32 -6
  18. package/ios/Sources/AdMobPlugin/AppOpen/AppOpenAdPluginEvents.swift +3 -0
  19. package/ios/Sources/AdMobPlugin/Banner/BannerAdPluginEvents.swift +3 -0
  20. package/ios/Sources/AdMobPlugin/Banner/BannerAdSizeEnum.swift +3 -0
  21. package/ios/Sources/AdMobPlugin/Banner/BannerExecutor.swift +4 -4
  22. package/ios/Sources/AdMobPlugin/Helper/AuthorizationStatusEnum.swift +3 -0
  23. package/ios/Sources/AdMobPlugin/Interstitial/InterstitialAdPluginEvents.swift +3 -0
  24. package/ios/Sources/AdMobPlugin/Rewarded/RewardAdPluginEvents.swift +3 -0
  25. package/ios/Sources/AdMobPlugin/RewardedInterstitial/RewardInterstitialAdPluginEvents.swift +3 -0
  26. package/package.json +5 -4
@@ -2,6 +2,64 @@
2
2
 
3
3
  var core = require('@capacitor/core');
4
4
 
5
+ function* projectionAncestors(element) {
6
+ let current = element;
7
+ while (current) {
8
+ const root = current.getRootNode();
9
+ current = current.assignedSlot ?? current.parentElement ?? (root instanceof ShadowRoot ? root.host : null);
10
+ if (current)
11
+ yield current;
12
+ }
13
+ }
14
+
15
+ /** Current CSS visibility without the Engine's projection hiding rules.
16
+ * Use when an existing renderer decides whether its DOM source is authored hidden.
17
+ * Projection attributes are restored synchronously before returning; this performs
18
+ * a style read and may notify MutationObservers, so it is not a cached measurement.
19
+ */
20
+ const getUnprojectedVisibility = (element) => {
21
+ const win = element.ownerDocument.defaultView;
22
+ if (!win)
23
+ return 'hidden';
24
+ const markers = [];
25
+ for (const node of [element, ...projectionAncestors(element)]) {
26
+ for (const attribute of Array.from(node.attributes)) {
27
+ if (attribute.name === 'data-native-ui-snapshot-source' ||
28
+ attribute.name === 'data-native-ui-snapshot-prepared' ||
29
+ attribute.name === 'data-native-ui-snapshot-content' ||
30
+ attribute.name.startsWith('data-native-ui-session-'))
31
+ markers.push({ node, name: attribute.name, value: attribute.value });
32
+ }
33
+ }
34
+ try {
35
+ for (const { node, name } of markers)
36
+ node.removeAttribute(name);
37
+ return win.getComputedStyle(element).visibility;
38
+ }
39
+ finally {
40
+ for (const { node, name, value } of markers)
41
+ node.setAttribute(name, value);
42
+ }
43
+ };
44
+
45
+ // The injected runtime and a plugin's bundled helper share the binding.
46
+ const placementKey = Symbol.for('webview-native-ui-engine.projection-placement');
47
+ const bindProjectionPlacement = (element, measure, invalidate = () => { }) => {
48
+ const host = element;
49
+ const state = { projected: false, measure, invalidate };
50
+ host[placementKey] = state;
51
+ return {
52
+ get projected() {
53
+ return host[placementKey] === state && state.projected;
54
+ },
55
+ dispose() {
56
+ if (host[placementKey] === state)
57
+ delete host[placementKey];
58
+ state.projected = false;
59
+ },
60
+ };
61
+ };
62
+
5
63
  const NATIVE_AD_ELEMENT_NAME = 'capacitor-admob-native';
6
64
  const defineNativeAdElement = (findFeed) => {
7
65
  const runtime = globalThis;
@@ -94,7 +152,7 @@ const measureNativeAdSlot = (element, scrollElement) => {
94
152
  const domRect = element.getBoundingClientRect();
95
153
  if (!element.isConnected ||
96
154
  style.display === 'none' ||
97
- style.visibility === 'hidden' ||
155
+ (style.visibility === 'hidden' && getUnprojectedVisibility(element) === 'hidden') ||
98
156
  Number.parseFloat(style.opacity || '1') === 0 ||
99
157
  domRect.width <= 0 ||
100
158
  domRect.height <= 0) {
@@ -189,12 +247,10 @@ let sessionSequence = 0;
189
247
  const feeds = new Map();
190
248
  const createSessionId = () => { var _a, _b, _c; return (_c = (_b = (_a = globalThis.crypto) === null || _a === void 0 ? void 0 : _a.randomUUID) === null || _b === void 0 ? void 0 : _b.call(_a)) !== null && _c !== void 0 ? _c : `${Date.now()}-${++sessionSequence}-${Math.random().toString(36).slice(2)}`; };
191
249
  /**
192
- * Experimental manager for native-ad integration research.
250
+ * Manages native ad placement and lifecycle in a scrolling feed.
193
251
  *
194
252
  * A stable `slotKey`, rather than the element or array index, owns the native
195
253
  * ad lifecycle. Virtual-scroller compatibility has not yet been validated.
196
- *
197
- * @experimental Native overlay gesture routing is still being validated on devices.
198
254
  */
199
255
  class NativeAdFeed {
200
256
  constructor(options) {
@@ -217,6 +273,15 @@ class NativeAdFeed {
217
273
  if (this.destroyed) {
218
274
  return;
219
275
  }
276
+ if (event instanceof Event &&
277
+ event.type === 'load' &&
278
+ event.target instanceof HTMLImageElement &&
279
+ event.target.dataset['admobSnapshot'] !== undefined)
280
+ return;
281
+ if (!(event instanceof Event) || !['scroll', 'touchmove', 'wheel'].includes(event.type)) {
282
+ for (const state of this.slots.values())
283
+ state.lastPlacement = undefined;
284
+ }
220
285
  if (!(this.nativeScrollElement &&
221
286
  event instanceof Event &&
222
287
  ['scroll', 'touchmove', 'wheel'].includes(event.type))) {
@@ -245,6 +310,9 @@ class NativeAdFeed {
245
310
  if (feeds.size >= MAX_ACTIVE_FEEDS) {
246
311
  throw new Error(`At most ${MAX_ACTIVE_FEEDS} native ad feeds can be active`);
247
312
  }
313
+ if (options.androidScrollSnapshot && core.Capacitor.getPlatform() !== 'android') {
314
+ throw new Error('androidScrollSnapshot requires Android');
315
+ }
248
316
  this.options = Object.assign(Object.assign({}, options), { feedId: id });
249
317
  this.nativeScrollElement = core.Capacitor.getPlatform() === 'ios' ? options.scrollElement : undefined;
250
318
  if (typeof IntersectionObserver !== 'undefined') {
@@ -331,7 +399,7 @@ class NativeAdFeed {
331
399
  return handle;
332
400
  }
333
401
  attach(slotKey, element) {
334
- var _a, _b, _c;
402
+ var _a, _b, _c, _d, _e;
335
403
  this.assertActive();
336
404
  const normalizedKey = slotKey.trim();
337
405
  if (!normalizedKey) {
@@ -358,30 +426,58 @@ class NativeAdFeed {
358
426
  };
359
427
  applySlotDefaults(element, (_a = this.options.template) !== null && _a !== void 0 ? _a : exports.NativeAdTemplate.Medium);
360
428
  state.element = element;
429
+ if (this.options.androidScrollSnapshot) {
430
+ const owner = ((_b = state.projectionOwner) !== null && _b !== void 0 ? _b : (state.projectionOwner = `admob-${crypto.randomUUID()}`));
431
+ element.dataset['admobProjection'] = owner;
432
+ state.projectionSelector = `[data-admob-projection="${owner}"]`;
433
+ (_c = state.placementBinding) === null || _c === void 0 ? void 0 : _c.dispose();
434
+ state.placementBinding = bindProjectionPlacement(element, () => {
435
+ const placement = this.measurePlacement(state);
436
+ state.lastPlacement = placement;
437
+ // The reveal request applies this exact placement; keep batch deduplication in sync.
438
+ if (this.lastPlacements) {
439
+ const previous = JSON.parse(this.lastPlacements);
440
+ previous.placements = previous.placements.map((item) => item.slotKey === state.slotKey ? placement : item);
441
+ this.lastPlacements = JSON.stringify(previous);
442
+ }
443
+ return Object.assign(Object.assign({}, placement), { sessionId: this.sessionId, sequence: ++this.placementSequence });
444
+ }, () => {
445
+ if (state.element !== element || this.destroyed)
446
+ return;
447
+ state.lastPlacement = undefined;
448
+ this.lastPlacements = undefined;
449
+ this.schedulePlacementUpdate();
450
+ });
451
+ }
361
452
  state.generation += 1;
362
453
  state.lastUsed = Date.now();
363
454
  this.slots.set(normalizedKey, state);
364
455
  this.elementSlots.set(element, normalizedKey);
365
456
  this.updateScrollRoots();
366
- (_b = this.intersectionObserver) === null || _b === void 0 ? void 0 : _b.observe(element);
367
- (_c = this.resizeObserver) === null || _c === void 0 ? void 0 : _c.observe(element);
457
+ (_d = this.intersectionObserver) === null || _d === void 0 ? void 0 : _d.observe(element);
458
+ (_e = this.resizeObserver) === null || _e === void 0 ? void 0 : _e.observe(element);
368
459
  this.scheduleEvaluation();
369
460
  }
370
461
  detach(element) {
371
- var _a, _b;
462
+ var _a, _b, _c;
372
463
  const slotKey = this.elementSlots.get(element);
373
464
  if (!slotKey) {
374
465
  return;
375
466
  }
376
467
  const state = this.slots.get(slotKey);
377
468
  if ((state === null || state === void 0 ? void 0 : state.element) === element) {
469
+ (_a = state.placementBinding) === null || _a === void 0 ? void 0 : _a.dispose();
470
+ state.placementBinding = undefined;
471
+ state.lastPlacement = undefined;
472
+ if (state.projectionOwner)
473
+ delete element.dataset['admobProjection'];
378
474
  state.element = undefined;
379
475
  state.intersecting = false;
380
476
  state.generation += 1;
381
477
  state.lastUsed = Date.now();
382
478
  }
383
- (_a = this.intersectionObserver) === null || _a === void 0 ? void 0 : _a.unobserve(element);
384
- (_b = this.resizeObserver) === null || _b === void 0 ? void 0 : _b.unobserve(element);
479
+ (_b = this.intersectionObserver) === null || _b === void 0 ? void 0 : _b.unobserve(element);
480
+ (_c = this.resizeObserver) === null || _c === void 0 ? void 0 : _c.unobserve(element);
385
481
  this.elementSlots.delete(element);
386
482
  this.updateScrollRoots();
387
483
  void this.requestPlacementUpdate();
@@ -403,6 +499,7 @@ class NativeAdFeed {
403
499
  throw new Error('Native ad capacity is currently in use by visible slots');
404
500
  }
405
501
  await this.loadState(state);
502
+ await this.requestPlacementUpdate();
406
503
  });
407
504
  }
408
505
  /** Immediately hides this feed's overlays until {@link resume} is called. */
@@ -423,6 +520,8 @@ class NativeAdFeed {
423
520
  /** Hides stale overlays and remeasures after an application-driven layout change. */
424
521
  async invalidateLayout() {
425
522
  this.assertActive();
523
+ for (const state of this.slots.values())
524
+ state.lastPlacement = undefined;
426
525
  this.layoutInvalidated = true;
427
526
  this.scheduleEvaluation();
428
527
  if (!(await this.requestPlacementUpdate())) {
@@ -507,7 +606,9 @@ class NativeAdFeed {
507
606
  }
508
607
  this.settleTimer = setTimeout(() => {
509
608
  this.layoutInvalidated = false;
510
- void this.runOperation(() => this.evaluate());
609
+ void this.runOperation(async () => {
610
+ await this.evaluate();
611
+ });
511
612
  }, SCROLL_SETTLE_MS);
512
613
  }
513
614
  async evaluate() {
@@ -524,7 +625,7 @@ class NativeAdFeed {
524
625
  .filter((state) => state.status === 'idle')
525
626
  .slice(0, Math.max(0, MAX_NATIVE_ADS - occupied));
526
627
  await Promise.allSettled(loadable.map((state) => this.loadState(state)));
527
- void this.requestPlacementUpdate();
628
+ await this.requestPlacementUpdate();
528
629
  }
529
630
  async loadState(state) {
530
631
  var _a;
@@ -534,16 +635,7 @@ class NativeAdFeed {
534
635
  state.status = 'loading';
535
636
  const loadVersion = ++state.loadVersion;
536
637
  try {
537
- await bridge.loadNativeAd({
538
- feedId: this.options.feedId,
539
- sessionId: this.sessionId,
540
- slotKey: state.slotKey,
541
- template: (_a = this.options.template) !== null && _a !== void 0 ? _a : exports.NativeAdTemplate.Medium,
542
- style: this.options.style,
543
- adId: this.options.adId,
544
- isTesting: this.options.isTesting,
545
- npa: this.options.npa,
546
- });
638
+ await bridge.loadNativeAd(Object.assign({ feedId: this.options.feedId, sessionId: this.sessionId, slotKey: state.slotKey, template: (_a = this.options.template) !== null && _a !== void 0 ? _a : exports.NativeAdTemplate.Medium, style: this.options.style, adId: this.options.adId, isTesting: this.options.isTesting, npa: this.options.npa }, (state.projectionSelector ? { projectionSelector: state.projectionSelector } : {})));
547
639
  if (state.loadVersion === loadVersion) {
548
640
  state.status = 'loaded';
549
641
  state.lastUsed = Date.now();
@@ -577,6 +669,9 @@ class NativeAdFeed {
577
669
  }
578
670
  getVisibleStates() {
579
671
  return [...this.slots.values()].filter((state) => {
672
+ var _a;
673
+ if (this.reuseProjectedPlacement(state))
674
+ return state.intersecting && !!((_a = state.lastPlacement) === null || _a === void 0 ? void 0 : _a.visible);
580
675
  const measured = state.element && state.intersecting ? measureNativeAdSlot(state.element) : undefined;
581
676
  return Boolean(measured && this.isSupportedSize(measured));
582
677
  });
@@ -625,6 +720,7 @@ class NativeAdFeed {
625
720
  return result;
626
721
  }
627
722
  async destroyResources() {
723
+ var _a;
628
724
  await Promise.all([
629
725
  ...[...this.listenerHandles].map((handle) => handle.remove()),
630
726
  ...(this.nativeStarted && bridge
@@ -633,9 +729,39 @@ class NativeAdFeed {
633
729
  ]);
634
730
  this.nativeStarted = false;
635
731
  this.listenerHandles.clear();
732
+ for (const state of this.slots.values())
733
+ (_a = state.placementBinding) === null || _a === void 0 ? void 0 : _a.dispose();
636
734
  this.slots.clear();
637
735
  this.elementSlots.clear();
638
736
  }
737
+ reuseProjectedPlacement(state) {
738
+ var _a, _b, _c;
739
+ return Boolean(((_a = state.placementBinding) === null || _a === void 0 ? void 0 : _a.projected) &&
740
+ ((_b = state.lastPlacement) === null || _b === void 0 ? void 0 : _b.visible) &&
741
+ ((_c = state.element) === null || _c === void 0 ? void 0 : _c.isConnected) &&
742
+ !this.paused &&
743
+ !this.destroyed &&
744
+ !document.hidden &&
745
+ state.status === 'loaded' &&
746
+ state.lastPlacement.generation === state.generation);
747
+ }
748
+ measurePlacement(state) {
749
+ const measured = state.element ? measureNativeAdSlot(state.element, this.nativeScrollElement) : undefined;
750
+ return {
751
+ feedId: this.options.feedId,
752
+ slotKey: state.slotKey,
753
+ generation: state.generation,
754
+ visible: Boolean(!this.paused &&
755
+ !this.destroyed &&
756
+ state.status === 'loaded' &&
757
+ (!this.layoutInvalidated || this.options.androidScrollSnapshot) &&
758
+ measured &&
759
+ this.isSupportedSize(measured) &&
760
+ !document.hidden),
761
+ rect: measured === null || measured === void 0 ? void 0 : measured.rect,
762
+ clipRect: measured === null || measured === void 0 ? void 0 : measured.clipRect,
763
+ };
764
+ }
639
765
  requestPlacementUpdate(allowDirect = false) {
640
766
  this.updateDirty = true;
641
767
  this.placementAcknowledgementRequired || (this.placementAcknowledgementRequired = !allowDirect);
@@ -653,19 +779,15 @@ class NativeAdFeed {
653
779
  this.updateDirty = false;
654
780
  const allowDirect = !this.placementAcknowledgementRequired;
655
781
  this.placementAcknowledgementRequired = false;
782
+ const retainedSlots = new Set();
656
783
  const placements = [...this.slots.values()]
657
784
  .filter((state) => state.status === 'loaded')
658
785
  .map((state) => {
659
- const measured = state.element ? measureNativeAdSlot(state.element, this.nativeScrollElement) : undefined;
660
- const visible = Boolean(!this.paused && !this.layoutInvalidated && measured && this.isSupportedSize(measured) && !document.hidden);
661
- return {
662
- feedId: this.options.feedId,
663
- slotKey: state.slotKey,
664
- generation: state.generation,
665
- visible,
666
- rect: measured === null || measured === void 0 ? void 0 : measured.rect,
667
- clipRect: measured === null || measured === void 0 ? void 0 : measured.clipRect,
668
- };
786
+ if (this.reuseProjectedPlacement(state)) {
787
+ retainedSlots.add(state.slotKey);
788
+ return state.lastPlacement;
789
+ }
790
+ return (state.lastPlacement = this.measurePlacement(state));
669
791
  });
670
792
  const scrollContainer = this.nativeScrollElement
671
793
  ? measureNativeAdScrollContainer(this.nativeScrollElement)
@@ -675,7 +797,17 @@ class NativeAdFeed {
675
797
  continue;
676
798
  }
677
799
  try {
678
- const batch = Object.assign({ feedId: this.options.feedId, sessionId: this.sessionId, sequence: ++this.placementSequence, placements }, (scrollContainer ? { scrollContainer } : {}));
800
+ const batch = Object.assign({ feedId: this.options.feedId, sessionId: this.sessionId, sequence: ++this.placementSequence, placements: placements.map((placement) => {
801
+ return retainedSlots.has(placement.slotKey)
802
+ ? {
803
+ feedId: placement.feedId,
804
+ slotKey: placement.slotKey,
805
+ generation: placement.generation,
806
+ visible: true,
807
+ retain: true,
808
+ }
809
+ : placement;
810
+ }) }, (scrollContainer ? { scrollContainer } : {}));
679
811
  const channel = core.Capacitor.getPlatform() === 'android'
680
812
  ? window
681
813
  .capacitorAdMobPlacements