@capacitor-community/admob 8.2.1 → 8.3.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 (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
package/README.md CHANGED
@@ -190,7 +190,7 @@ Start with [Installation](#installation) above, then [Configuration](https://doc
190
190
  - [Consent](https://docs.rdlabo.dev/projects/capacitor-admob/docs/consent) — privacy consent and iOS tracking authorization.
191
191
  - [Testing](https://docs.rdlabo.dev/projects/capacitor-admob/docs/testing) — demo ad units, test devices, and consent testing.
192
192
  - [Banner Ads](https://docs.rdlabo.dev/projects/capacitor-admob/docs/banner) — banner options, lifecycle, and events.
193
- - [Native Ads (Preview)](./docs/native-ads.md) — available for testing and production use. Its API may change in a minor release while in Preview.
193
+ - [Native Ads](./docs/native-ads.md) — feed placement, styling, lifecycle, and events.
194
194
  - Full-screen ads:
195
195
  - [Interstitial Ads](https://docs.rdlabo.dev/projects/capacitor-admob/docs/interstitial) — load, show, and multiple prepared ads.
196
196
  - [Rewarded Ads](https://docs.rdlabo.dev/projects/capacitor-admob/docs/rewarded) — rewarded video, rewarded interstitial, and server-side verification.
@@ -53,14 +53,18 @@ android {
53
53
  }
54
54
 
55
55
  repositories {
56
+ if (project.hasProperty('nativeUIEngineRepository')) {
57
+ maven { url = uri(project.property('nativeUIEngineRepository')) }
58
+ }
56
59
  google()
57
- mavenCentral()
60
+ mavenCentral()
58
61
 
59
62
  }
60
63
 
61
64
  def testRunner = "1.7.0"
62
65
 
63
66
  dependencies {
67
+ implementation "io.github.webview-native-ui:engine:${project.findProperty('nativeUIEngineVersion') ?: '1.0.0'}"
64
68
  implementation fileTree(dir: 'libs', include: ['*.jar'])
65
69
  implementation project(':capacitor-android')
66
70
 
@@ -84,6 +84,11 @@ public class AdMob extends Plugin {
84
84
  @Override
85
85
  public void load() {
86
86
  super.load();
87
+ try {
88
+ io.github.webview_native_ui.engine.NativeUIProjection.install(getBridge().getWebView());
89
+ } catch (Exception error) {
90
+ android.util.Log.w(getLogTag(), "Native UI channel unavailable; retaining native tracking", error);
91
+ }
87
92
  if (
88
93
  androidx.webkit.WebViewFeature.isFeatureSupported(androidx.webkit.WebViewFeature.WEB_MESSAGE_LISTENER) &&
89
94
  !getBridge().getConfig().isUsingLegacyBridge()
@@ -276,6 +281,16 @@ public class AdMob extends Plugin {
276
281
  nativeAdExecutor.remove(call);
277
282
  }
278
283
 
284
+ @PluginMethod
285
+ public void captureNativeAdSnapshot(final PluginCall call) {
286
+ nativeAdExecutor.captureSnapshot(call);
287
+ }
288
+
289
+ @PluginMethod
290
+ public void prepareNativeAdSnapshot(final PluginCall call) {
291
+ nativeAdExecutor.prepareSnapshot(call);
292
+ }
293
+
279
294
  // ---------------------------------------------------------
280
295
  // INTERSTITIAL ADS
281
296
  // ---------------------------------------------------------
@@ -2,8 +2,19 @@ package com.getcapacitor.community.admob.nativeads
2
2
 
3
3
  import android.app.Activity
4
4
  import android.content.Context
5
+ import io.github.webview_native_ui.engine.NativeUIProjection
6
+ import android.graphics.Bitmap
7
+ import android.graphics.Canvas
8
+ import android.graphics.Color
9
+ import android.graphics.drawable.GradientDrawable
10
+ import android.os.Debug
11
+ import android.os.Handler
12
+ import android.os.Looper
13
+ import android.os.SystemClock
14
+ import android.util.Base64
5
15
  import android.view.View
6
16
  import android.view.ViewGroup
17
+ import android.webkit.WebView
7
18
  import android.widget.FrameLayout
8
19
  import androidx.core.util.Supplier
9
20
  import com.getcapacitor.JSArray
@@ -23,6 +34,7 @@ import com.google.android.gms.common.util.BiConsumer
23
34
  import org.json.JSONException
24
35
  import org.json.JSONObject
25
36
  import java.util.concurrent.atomic.AtomicBoolean
37
+ import java.io.ByteArrayOutputStream
26
38
  import kotlin.math.roundToInt
27
39
 
28
40
  class NativeAdExecutor(
@@ -39,8 +51,16 @@ class NativeAdExecutor(
39
51
  val nativeAd: NativeAd,
40
52
  val adView: NativeAdView,
41
53
  val clippingContainer: FrameLayout,
54
+ val opaqueBackground: Boolean,
42
55
  var generation: Int = -1,
43
- )
56
+ var projection: NativeUIProjection? = null,
57
+ var projectionIdentity: JSObject? = null,
58
+ var projectionVisible: Boolean = true,
59
+ var deferredPlacement: JSONObject? = null,
60
+
61
+ ) {
62
+ var placementSequence: Long = -1
63
+ }
44
64
 
45
65
  private data class PendingLoad(
46
66
  val call: PluginCall,
@@ -51,6 +71,7 @@ class NativeAdExecutor(
51
71
  private val states = mutableMapOf<String, NativeAdState>()
52
72
  private val pendingLoads = mutableMapOf<String, PendingLoad>()
53
73
  private val feedSessions = NativeAdFeedSessions()
74
+ private val snapshotCapturePending = AtomicBoolean(false)
54
75
 
55
76
  fun startFeed(call: PluginCall) {
56
77
  val feedId = requiredString(call, "feedId") ?: return
@@ -142,26 +163,61 @@ class NativeAdExecutor(
142
163
  requiredString(call, "feedId") ?: return
143
164
  requiredString(call, "sessionId") ?: return
144
165
  activitySupplier.get().runOnUiThread {
145
- applyPlacements(call.data)
146
- call.resolve()
166
+ applyPlacements(call.data) { hidden ->
167
+ if (hidden) call.resolve() else call.reject("Native ad projection did not finish hiding")
168
+ }
147
169
  }
148
170
  }
149
171
 
150
- private fun applyPlacements(batch: JSONObject) {
172
+ private fun applyPlacements(batch: JSONObject, complete: ((Boolean) -> Unit)? = null) {
151
173
  val feedId = batch.optString("feedId")
152
174
  val sessionId = batch.optString("sessionId")
153
- if (!isCurrentSession(feedId, sessionId) || !feedSessions.accepts(feedId, batch.optLong("sequence", -1L))) return
175
+ if (!isCurrentSession(feedId, sessionId) || !feedSessions.accepts(feedId, batch.optLong("sequence", -1L))) { complete?.invoke(true); return }
176
+ val sequence = batch.optLong("sequence", -1L)
177
+ val eligible = states.filter { it.value.feedId == feedId && it.value.placementSequence < sequence }.keys.toSet()
154
178
  val shown = mutableSetOf<String>()
179
+ val retained = mutableSetOf<String>()
155
180
  val placements = batch.optJSONArray("placements") ?: JSArray()
156
181
  repeat(placements.length()) { index ->
157
182
  try {
158
- applyPlacement(feedId, placements.getJSONObject(index))?.let { shown.add(it) }
183
+ val placement = placements.getJSONObject(index)
184
+ val key = stateKey(feedId, placement.optString("slotKey"))
185
+ if (key in eligible) {
186
+ if (placement.optBoolean("retain", false)) {
187
+ val state = states[key]
188
+ if (state != null && placement.optString("feedId") == feedId &&
189
+ placement.optInt("generation", -1) == state.generation && placement.optBoolean("visible", false) &&
190
+ state.projectionVisible && state.clippingContainer.visibility == View.VISIBLE) {
191
+ shown.add(key)
192
+ retained.add(key)
193
+ }
194
+ } else applyPlacement(feedId, placement)?.let { shown.add(it) }
195
+ }
159
196
  } catch (_: JSONException) {
160
197
  // Invalid entries fail closed below.
161
198
  }
162
199
  }
163
- states.filter { it.value.feedId == feedId && it.key !in shown }.values.forEach {
200
+ val hiding = mutableListOf<NativeUIProjection>()
201
+ // Retain carries no new geometry. A delayed retain must not supersede
202
+ // the final position arriving independently through the Engine channel.
203
+ (eligible - retained).forEach { states[it]?.placementSequence = sequence }
204
+ states.filter { it.key in eligible && it.key !in shown }.values.forEach {
205
+ it.deferredPlacement = null
164
206
  it.clippingContainer.visibility = View.GONE
207
+ if (it.projectionVisible || complete != null) it.projection?.let(hiding::add)
208
+ if (it.projectionVisible) {
209
+ it.projectionVisible = false
210
+ it.projectionIdentity?.let { event -> notifyListeners("nativeAdProjectionHidden", event) }
211
+ }
212
+ }
213
+ if (hiding.isEmpty()) complete?.invoke(true)
214
+ else {
215
+ var remaining = hiding.size
216
+ var succeeded = true
217
+ hiding.forEach { projection -> projection.hide { result ->
218
+ succeeded = succeeded && result
219
+ if (--remaining == 0) complete?.invoke(succeeded)
220
+ } }
165
221
  }
166
222
  }
167
223
 
@@ -228,9 +284,65 @@ class NativeAdExecutor(
228
284
  return
229
285
  }
230
286
 
231
- val state = NativeAdState(feedId, template == "small", pendingLoad.token, nativeAd, adView, clippingContainer)
287
+ val backgroundColor = (adView.background as? GradientDrawable)?.color?.defaultColor
288
+ val state = NativeAdState(
289
+ feedId, template == "small", pendingLoad.token, nativeAd, adView, clippingContainer,
290
+ backgroundColor != null && Color.alpha(backgroundColor) == 255,
291
+ )
232
292
  states[stateKey] = state
233
293
  overlayParent.addView(clippingContainer)
294
+ pendingLoad.call.getString("projectionSelector")?.let { selector ->
295
+ val event = identity(feedId, sessionId, slotKey).apply { put("loadId", java.util.UUID.randomUUID().toString()) }
296
+ state.projectionIdentity = event
297
+ val webView = webViewSupplier.get() as? WebView
298
+ if (webView != null) webView.post {
299
+ if (states[stateKey] !== state) return@post
300
+ state.projection = runCatching {
301
+ NativeUIProjection(
302
+ window = activitySupplier.get().window,
303
+ webView = webView,
304
+ plugin = "AdMob",
305
+ projectionSelector = selector,
306
+ receiveListeners = NativeUIProjection.ReceiveListeners(
307
+ update = listOf("nativeAdProjectionUpdated"),
308
+ projection = NativeUIProjection.ReceiveListeners.Projection(
309
+ native = listOf("nativeAdProjectionShown"), none = listOf("nativeAdProjectionHidden")
310
+ ),
311
+ match = mapOf("feedId" to feedId, "sessionId" to sessionId, "slotKey" to slotKey, "loadId" to event.optString("loadId"))
312
+ ),
313
+ failedFallback = NativeUIProjection.FailedFallback.NATIVE,
314
+ captureMode = NativeUIProjection.CaptureMode.VIEW,
315
+ updatePlacement = { placement ->
316
+ check(states[stateKey] === state && isCurrentSession(feedId, sessionId)) { "Native ad owner changed" }
317
+ check(placement.optString("feedId") == feedId && placement.optString("sessionId") == sessionId &&
318
+ placement.optString("slotKey") == slotKey) { "Native ad placement identity changed" }
319
+ check(placement.optInt("generation", -1) >= state.generation &&
320
+ placement.optLong("sequence", -1L) > state.placementSequence && state.projectionVisible) {
321
+ "Native ad placement is stale or hidden"
322
+ }
323
+ if (!placement.optBoolean("visible", false)) {
324
+ state.placementSequence = placement.optLong("sequence")
325
+ state.deferredPlacement = null
326
+ state.clippingContainer.visibility = View.GONE
327
+ state.projectionVisible = false
328
+ state.projection?.hide()
329
+ error("Native ad placement is not visible")
330
+ }
331
+ check(applyPlacement(feedId, placement, defer = false) == stateKey) { "Native ad placement is invalid" }
332
+ state.placementSequence = placement.optLong("sequence")
333
+ },
334
+ beforeNativeReveal = {
335
+ val placement = state.deferredPlacement
336
+ state.deferredPlacement = null
337
+ if (states[stateKey] === state && placement != null) applyPlacement(feedId, placement, defer = false)
338
+ },
339
+ canCapture = { states[stateKey] === state && state.opaqueBackground &&
340
+ nativeAd.mediaContent?.hasVideoContent() != true && isSnapshotSizeSupported(adView.width, adView.height) },
341
+ view = { if (states[stateKey] === state) adView else null }
342
+ )
343
+ }.onFailure { android.util.Log.w("AdMob", "Native ad projection unavailable; retaining native tracking", it) }.getOrNull()
344
+ }
345
+ }
234
346
  nativeAd.setOnPaidEventListener { adValue ->
235
347
  if (states[stateKey] !== state) return@setOnPaidEventListener
236
348
  val responseInfo = nativeAd.responseInfo
@@ -287,7 +399,7 @@ class NativeAdExecutor(
287
399
  }
288
400
  }
289
401
 
290
- private fun applyPlacement(expectedFeedId: String, placement: JSONObject): String? {
402
+ private fun applyPlacement(expectedFeedId: String, placement: JSONObject, defer: Boolean = true): String? {
291
403
  val density = contextSupplier.get().resources.displayMetrics.density
292
404
  val value = NativeAdPlacementValue.parse(expectedFeedId, placement, density) ?: return null
293
405
  val state = states[stateKey(value.feedId, value.slotKey)] ?: return null
@@ -296,6 +408,15 @@ class NativeAdExecutor(
296
408
  val minimumHeight = (if (state.isSmall) 120 else 300) * density
297
409
  if (value.rectWidth < minimumWidth.roundToInt() || value.rectHeight < minimumHeight.roundToInt()) return null
298
410
 
411
+ if (defer && state.projection?.isProjecting == true &&
412
+ state.adView.width == value.rectWidth && state.adView.height == value.rectHeight) {
413
+ state.deferredPlacement = JSONObject(placement.toString())
414
+ state.generation = value.generation
415
+ return stateKey(value.feedId, value.slotKey)
416
+ }
417
+ state.deferredPlacement = null
418
+ val resized = state.adView.width != value.rectWidth || state.adView.height != value.rectHeight
419
+
299
420
  if (!state.isSmall) {
300
421
  val adView = state.adView
301
422
  if (adView.isLayoutRequested || adView.measuredWidth != value.rectWidth || adView.measuredHeight != value.rectHeight) {
@@ -329,13 +450,138 @@ class NativeAdExecutor(
329
450
  state.generation = value.generation
330
451
  state.clippingContainer.visibility = View.VISIBLE
331
452
  if (!wasVisible) state.clippingContainer.bringToFront()
453
+ if (!state.projectionVisible) {
454
+ state.projection?.show()
455
+ state.projectionVisible = true
456
+ state.projectionIdentity?.let { notifyListeners("nativeAdProjectionShown", it) }
457
+ }
458
+ if (resized) state.projectionIdentity?.let { notifyListeners("nativeAdProjectionUpdated", it) }
332
459
  return stateKey(value.feedId, value.slotKey)
333
460
  }
334
461
 
335
462
  private fun overlayParent(): ViewGroup? = webViewSupplier.get()?.parent as? ViewGroup
336
463
 
464
+ // Scroll snapshot proxy. SDK views remain responsible for all ad interactions.
465
+ fun captureSnapshot(call: PluginCall) {
466
+ val feedId = requiredString(call, "feedId") ?: return
467
+ val sessionId = requiredString(call, "sessionId") ?: return
468
+ val slotKey = requiredString(call, "slotKey") ?: return
469
+ activitySupplier.get().runOnUiThread {
470
+ val state = states[stateKey(feedId, slotKey)]
471
+ if (!isCurrentSession(feedId, sessionId) || state == null) {
472
+ call.reject("Snapshots require an active ad")
473
+ return@runOnUiThread
474
+ }
475
+ if (!state.opaqueBackground) {
476
+ call.reject("Snapshot underlays require an opaque native ad background")
477
+ return@runOnUiThread
478
+ }
479
+ if (state.nativeAd.mediaContent?.hasVideoContent() == true) {
480
+ call.reject("Video ads retain native tracking while scrolling")
481
+ return@runOnUiThread
482
+ }
483
+ val view = state.adView
484
+ if (!view.isShown || view.isLayoutRequested || !isSnapshotSizeSupported(view.width, view.height)) {
485
+ call.reject("Native ad is not ready for a bounded snapshot")
486
+ return@runOnUiThread
487
+ }
488
+ // One bitmap/encoder per executor, including concurrent feeds.
489
+ if (!snapshotCapturePending.compareAndSet(false, true)) {
490
+ call.reject("Another native ad snapshot is being encoded")
491
+ return@runOnUiThread
492
+ }
493
+ val started = SystemClock.elapsedRealtimeNanos()
494
+ val drawCpuStart = Debug.threadCpuTimeNanos()
495
+ val bitmap = try {
496
+ Bitmap.createBitmap(view.width, view.height, Bitmap.Config.ARGB_8888)
497
+ } catch (error: Exception) {
498
+ snapshotCapturePending.set(false)
499
+ call.reject("Could not allocate native ad snapshot", error)
500
+ return@runOnUiThread
501
+ }
502
+ try {
503
+ view.draw(Canvas(bitmap))
504
+ val drawn = SystemClock.elapsedRealtimeNanos()
505
+ val drawCpuMs = (Debug.threadCpuTimeNanos() - drawCpuStart) / 1_000_000.0
506
+ // Compression must not block the UI thread when the next gesture begins.
507
+ Thread {
508
+ try {
509
+ val encodeStart = SystemClock.elapsedRealtimeNanos()
510
+ val encodeCpuStart = Debug.threadCpuTimeNanos()
511
+ val bytes = ByteArrayOutputStream()
512
+ bitmap.compress(Bitmap.CompressFormat.PNG, 100, bytes)
513
+ val dataUrl = "data:image/png;base64," + Base64.encodeToString(bytes.toByteArray(), Base64.NO_WRAP)
514
+ val timings = JSObject().apply {
515
+ put("drawMs", (drawn - started) / 1_000_000.0)
516
+ put("drawCpuMs", drawCpuMs)
517
+ put("workerWaitMs", (encodeStart - drawn) / 1_000_000.0)
518
+ put("encodeMs", (SystemClock.elapsedRealtimeNanos() - encodeStart) / 1_000_000.0)
519
+ put("encodeCpuMs", (Debug.threadCpuTimeNanos() - encodeCpuStart) / 1_000_000.0)
520
+ put("bitmapBytes", bitmap.byteCount)
521
+ put("pngBytes", bytes.size())
522
+ }
523
+ call.resolve(JSObject().put("dataUrl", dataUrl).put("timings", timings))
524
+ } catch (error: Exception) {
525
+ call.reject("Could not encode native ad snapshot", error)
526
+ } finally {
527
+ bitmap.recycle()
528
+ snapshotCapturePending.set(false)
529
+ }
530
+ }.start()
531
+ } catch (error: Exception) {
532
+ bitmap.recycle()
533
+ snapshotCapturePending.set(false)
534
+ call.reject("Could not draw native ad snapshot", error)
535
+ }
536
+ }
537
+ }
538
+
539
+ fun prepareSnapshot(call: PluginCall) {
540
+ val feedId = requiredString(call, "feedId") ?: return
541
+ val sessionId = requiredString(call, "sessionId") ?: return
542
+ val slotKey = requiredString(call, "slotKey") ?: return
543
+ activitySupplier.get().runOnUiThread {
544
+ val key = stateKey(feedId, slotKey)
545
+ val state = states[key]
546
+ val webView = webViewSupplier.get() as? WebView
547
+ if (!isCurrentSession(feedId, sessionId) || state == null ||
548
+ webView == null || !webView.isAttachedToWindow || !webView.isShown) {
549
+ call.reject("Native ad snapshot WebView is no longer visible")
550
+ return@runOnUiThread
551
+ }
552
+ val completed = AtomicBoolean(false)
553
+ val handler = Handler(Looper.getMainLooper())
554
+ val timeout = Runnable {
555
+ if (completed.compareAndSet(false, true)) call.reject("Native ad snapshot visual state timed out")
556
+ }
557
+ handler.postDelayed(timeout, 2000)
558
+ try {
559
+ webView.postVisualStateCallback(0, object : WebView.VisualStateCallback() {
560
+ override fun onComplete(requestId: Long) {
561
+ if (!completed.compareAndSet(false, true)) return
562
+ handler.removeCallbacks(timeout)
563
+ if (!isCurrentSession(feedId, sessionId) || states[key] !== state ||
564
+ !webView.isAttachedToWindow || !webView.isShown) {
565
+ call.reject("Native ad snapshot was invalidated")
566
+ return
567
+ }
568
+ // Ready for the next WebView draw, not a claim that pixels have been presented.
569
+ webView.invalidate()
570
+ call.resolve()
571
+ }
572
+ })
573
+ } catch (error: Exception) {
574
+ if (completed.compareAndSet(false, true)) {
575
+ handler.removeCallbacks(timeout)
576
+ call.reject("Could not prepare native ad snapshot", error)
577
+ }
578
+ }
579
+ }
580
+ }
581
+
337
582
  private fun destroyState(stateKey: String) {
338
583
  val state = states.remove(stateKey) ?: return
584
+ state.projection?.close()
339
585
  (state.clippingContainer.parent as? ViewGroup)?.removeView(state.clippingContainer)
340
586
  state.adView.destroy()
341
587
  state.nativeAd.destroy()
@@ -374,5 +620,7 @@ class NativeAdExecutor(
374
620
  companion object {
375
621
  const val NATIVE_TESTER_ID = "ca-app-pub-3940256099942544/2247696110"
376
622
  private const val MAX_ADS_PER_FEED = 3
623
+ internal fun isSnapshotSizeSupported(width: Int, height: Int): Boolean =
624
+ width > 0 && height > 0 && width.toLong() * height <= 2_000_000
377
625
  }
378
626
  }
@@ -10,5 +10,11 @@ export interface NativeAdDefinitions {
10
10
  loadNativeAd(options: NativeAdLoadOptions): Promise<void>;
11
11
  updateNativeAdPlacements(options: NativeAdPlacementBatch): Promise<void>;
12
12
  removeNativeAd(options: NativeAdCommandIdentity): Promise<void>;
13
+ /** Android-only snapshot; does not change native visibility. */
14
+ captureNativeAdSnapshot?(options: NativeAdCommandIdentity): Promise<{
15
+ dataUrl: string;
16
+ }>;
17
+ /** Android-only: confirm that the appended image is ready for the next WebView draw. */
18
+ prepareNativeAdSnapshot?(options: NativeAdCommandIdentity): Promise<void>;
13
19
  addListener(eventName: NativeAdPluginEvents, listenerFunc: (info: (NativeAdEvent | NativeAdErrorEvent | NativeAdRevenueEvent) & NativeAdFeedSession) => void): Promise<PluginListenerHandle>;
14
20
  }
@@ -4,12 +4,10 @@ import type { NativeAdEvent, NativeAdErrorEvent, NativeAdRevenueEvent } from './
4
4
  import type { NativeAdFeedOptions } from './native-ad-options.interface';
5
5
  import type { NativeAdPluginEvents } from './native-ad-plugin-events.enum';
6
6
  /**
7
- * Experimental manager for native-ad integration research.
7
+ * Manages native ad placement and lifecycle in a scrolling feed.
8
8
  *
9
9
  * A stable `slotKey`, rather than the element or array index, owns the native
10
10
  * ad lifecycle. Virtual-scroller compatibility has not yet been validated.
11
- *
12
- * @experimental Native overlay gesture routing is still being validated on devices.
13
11
  */
14
12
  export declare class NativeAdFeed {
15
13
  private readonly options;
@@ -65,6 +63,8 @@ export declare class NativeAdFeed {
65
63
  private removeStateAd;
66
64
  private runOperation;
67
65
  private destroyResources;
66
+ private reuseProjectedPlacement;
67
+ private measurePlacement;
68
68
  private requestPlacementUpdate;
69
69
  private flushPlacementUpdates;
70
70
  private assertActive;