console.log('Native shown')}
+ onAdFailedToLoad={() => console.log('Native failed')}
+ onAdClicked={() => console.log('Native clicked')}
+ style={{ width: '100%', height: 300 }}
+/>
+```
+
+Optional:
+
+```javascript
+Appodeal.setPreferredNativeContentType('auto'); // 'auto' | 'noVideo' | 'video'
+Appodeal.getAvailableNativeAdsCount();
+Appodeal.destroyNativeAd(adId);
+```
+
## Privacy Policy and Consent
> Note: Keep in mind that it's best to contact qualified legal professionals, if you haven't done so already, to get
diff --git a/android/src/main/java/com/appodeal/rnappodeal/NativeAdViewAssetBinder.java b/android/src/main/java/com/appodeal/rnappodeal/NativeAdViewAssetBinder.java
new file mode 100644
index 0000000..014786f
--- /dev/null
+++ b/android/src/main/java/com/appodeal/rnappodeal/NativeAdViewAssetBinder.java
@@ -0,0 +1,47 @@
+package com.appodeal.rnappodeal;
+
+import android.view.View;
+import android.widget.TextView;
+
+import com.appodeal.ads.nativead.NativeAdView;
+import com.appodeal.ads.nativead.NativeIconView;
+import com.appodeal.ads.nativead.NativeMediaView;
+
+/**
+ * Binds custom native-ad assets onto {@link NativeAdView}.
+ *
+ * Appodeal's {@code NativeAdView} keeps JavaBean setters for Java callers, but the Kotlin
+ * metadata is obfuscated — so a Kotlin subclass cannot resolve {@code setMediaView} /
+ * {@code mediaView = ...}. Call the setters from Java instead.
+ */
+public final class NativeAdViewAssetBinder {
+ private NativeAdViewAssetBinder() {}
+
+ public static void bind(
+ NativeAdView view,
+ NativeMediaView media,
+ NativeIconView icon,
+ View title,
+ View description,
+ View callToAction,
+ TextView attribution) {
+ if (media != null) {
+ view.setMediaView(media);
+ }
+ if (icon != null) {
+ view.setIconView(icon);
+ }
+ if (title != null) {
+ view.setTitleView(title);
+ }
+ if (description != null) {
+ view.setDescriptionView(description);
+ }
+ if (callToAction != null) {
+ view.setCallToActionView(callToAction);
+ }
+ if (attribution != null) {
+ view.setAdAttributionView(attribution);
+ }
+ }
+}
diff --git a/android/src/main/java/com/appodeal/rnappodeal/RCTAppodealNativeAdView.kt b/android/src/main/java/com/appodeal/rnappodeal/RCTAppodealNativeAdView.kt
new file mode 100644
index 0000000..160e59d
--- /dev/null
+++ b/android/src/main/java/com/appodeal/rnappodeal/RCTAppodealNativeAdView.kt
@@ -0,0 +1,375 @@
+package com.appodeal.rnappodeal
+
+import android.content.Context
+import android.os.Handler
+import android.os.Looper
+import android.util.Log
+import android.view.View
+import android.view.ViewGroup
+import android.widget.TextView
+import com.appodeal.ads.NativeAd
+import com.appodeal.ads.nativead.NativeAdView
+import com.appodeal.ads.nativead.NativeIconView
+import com.appodeal.ads.nativead.NativeMediaView
+import com.appodeal.ads.nativead.Position
+import com.appodeal.rnappodeal.callbacks.RNAppodealEventHandler
+import com.appodeal.rnappodeal.constants.NativeEvents
+import com.facebook.react.bridge.Arguments
+import com.facebook.react.bridge.ReactContext
+import com.facebook.react.bridge.WritableMap
+import com.facebook.react.uimanager.UIManagerHelper
+import com.facebook.react.uimanager.events.Event
+import java.lang.ref.WeakReference
+import java.util.concurrent.CopyOnWriteArrayList
+
+/**
+ * Composable native ad root — matches Appodeal Android demo custom [NativeAdView]
+ * (`native_ad_view_custom.xml`): asset children + [registerView].
+ */
+class RCTAppodealNativeAdView(context: Context) :
+ NativeAdView(context),
+ RNAppodealEventHandler {
+
+ private val reactContext: ReactContext = context as ReactContext
+ private val surfaceId: Int by lazy { UIManagerHelper.getSurfaceId(reactContext) }
+ private val uiHandler: Handler by lazy { Handler(Looper.getMainLooper()) }
+
+ var adId: String? = null
+ set(value) {
+ if (field == value) return
+ field = value
+ boundAdId = null
+ bindGeneration++
+ scheduleBind(BIND_DELAY_MS)
+ }
+
+ var placement: String = "default"
+ set(value) {
+ if (field == value) return
+ field = value
+ if (boundAdId != null) {
+ boundAdId = null
+ bindGeneration++
+ scheduleBind(BIND_DELAY_MS)
+ }
+ }
+
+ private var boundAdId: String? = null
+ private var bindRunnable: Runnable? = null
+ private var bindGeneration: Int = 0
+ private var bindAttempts: Int = 0
+
+ init {
+ liveViews.add(WeakReference(this))
+ visibility = VISIBLE
+ setAdChoicesPosition(Position.END_TOP)
+ clipChildren = false
+ clipToPadding = false
+ contentDescription = "appodeal-native-ad-view"
+ }
+
+ fun onAssetChanged() {
+ if (boundAdId != null) {
+ boundAdId = null
+ bindGeneration++
+ }
+ if (!adId.isNullOrEmpty()) {
+ scheduleBind(BIND_DELAY_MS)
+ }
+ }
+
+ override fun onViewAdded(child: View?) {
+ super.onViewAdded(child)
+ if (!adId.isNullOrEmpty() && boundAdId == null) {
+ scheduleBind(BIND_DELAY_MS)
+ }
+ }
+
+ override fun onAttachedToWindow() {
+ super.onAttachedToWindow()
+ if (boundAdId == null && !adId.isNullOrEmpty()) {
+ scheduleBind(0)
+ }
+ }
+
+ override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
+ super.onLayout(changed, left, top, right, bottom)
+ if (right - left > 0 && bottom - top > 0 && boundAdId == null && !adId.isNullOrEmpty()) {
+ scheduleBind(0)
+ }
+ }
+
+ fun cleanup() {
+ bindRunnable?.let { uiHandler.removeCallbacks(it) }
+ bindRunnable = null
+ bindGeneration++
+ try {
+ unregisterView()
+ } catch (_: Exception) {
+ }
+ boundAdId = null
+ bindAttempts = 0
+ val self = this
+ liveViews.removeAll { it.get() == null || it.get() === self }
+ }
+
+ fun onActivityReady() {
+ if (boundAdId == null && !adId.isNullOrEmpty()) {
+ scheduleBind(0)
+ }
+ }
+
+ private fun scheduleBind(delayMs: Long) {
+ bindRunnable?.let { uiHandler.removeCallbacks(it) }
+ val gen = bindGeneration
+ val runnable = Runnable {
+ if (gen != bindGeneration) return@Runnable
+ bindAd(gen)
+ }.also { bindRunnable = it }
+ uiHandler.postDelayed(runnable, delayMs.coerceAtLeast(0L))
+ }
+
+ private fun bindAd(gen: Int) {
+ if (gen != bindGeneration) return
+ val id = adId
+ if (id.isNullOrEmpty()) return
+ if (boundAdId == id) return
+
+ val ad: NativeAd = RNAppodealNativeAdStore.get(id) ?: run {
+ if (!retry(gen, id, "native ad not in store")) {
+ dispatchFailed(id, "native ad not in store")
+ }
+ return
+ }
+
+ if (measuredWidth <= 0 || measuredHeight <= 0) {
+ if (!retry(gen, id, "view has zero size")) {
+ dispatchFailed(id, "view has zero size")
+ }
+ return
+ }
+
+ if (!isAttachedToWindow) {
+ if (!retry(gen, id, "not attached to window")) {
+ dispatchFailed(id, "not attached to window")
+ }
+ return
+ }
+
+ val activity = RNAppodealActivityHolder.get() ?: reactContext.currentActivity
+ if (activity == null) {
+ if (!retry(gen, id, "activity unavailable")) {
+ dispatchFailed(id, "activity unavailable")
+ }
+ return
+ }
+
+ val canShow = try {
+ ad.canShow(activity, placement)
+ } catch (e: Exception) {
+ Log.e(TAG, "canShow threw", e)
+ false
+ }
+ if (!canShow) {
+ if (!retry(gen, id, "canShow=false")) {
+ dispatchFailed(id, "canShow=false for placement=$placement")
+ }
+ return
+ }
+
+ val assets = collectAssets(this)
+ if (assets.media == null && assets.icon == null) {
+ if (!retry(gen, id, "missing media/icon asset")) {
+ dispatchFailed(id, "composable native ad requires media or icon asset")
+ }
+ return
+ }
+
+ try {
+ unregisterView()
+ } catch (_: Exception) {
+ }
+
+ // Bind via Java helper — Appodeal's Kotlin metadata is obfuscated, so
+ // setMediaView / mediaView= are unresolved from a Kotlin subclass.
+ NativeAdViewAssetBinder.bind(
+ this,
+ assets.media,
+ assets.icon,
+ assets.title,
+ assets.description,
+ assets.callToAction,
+ assets.attribution
+ )
+
+ val registered = try {
+ registerView(ad, placement)
+ } catch (e: Exception) {
+ Log.e(TAG, "registerView threw", e)
+ false
+ }
+
+ Log.d(
+ TAG,
+ "composable registerView id=$id result=$registered " +
+ "size=${measuredWidth}x${measuredHeight} " +
+ "media=${assets.media != null} icon=${assets.icon != null} " +
+ "title=${assets.title != null} cta=${assets.callToAction != null}"
+ )
+
+ if (gen != bindGeneration) return
+
+ if (registered) {
+ boundAdId = id
+ bindAttempts = 0
+ dispatchLoaded(id)
+ } else if (!retry(gen, id, "registerView returned false")) {
+ dispatchFailed(id, "registerView returned false")
+ }
+ }
+
+ private fun retry(gen: Int, id: String, reason: String): Boolean {
+ if (gen != bindGeneration) return true
+ bindAttempts += 1
+ if (bindAttempts > MAX_BIND_ATTEMPTS) return false
+ Log.d(TAG, "retry #$bindAttempts id=$id reason=$reason")
+ scheduleBind(BIND_DELAY_MS * bindAttempts)
+ return true
+ }
+
+ private fun dispatchLoaded(adIdValue: String) {
+ val params = Arguments.createMap().apply { putString("adId", adIdValue) }
+ dispatchFabricEvent(id, NativeEvents.ON_AD_LOADED, params)
+ }
+
+ private fun dispatchFailed(adIdValue: String, message: String) {
+ Log.w(TAG, "bind failed id=$adIdValue message=$message")
+ val params = Arguments.createMap().apply {
+ putString("adId", adIdValue)
+ putString("message", message)
+ }
+ dispatchFabricEvent(id, NativeEvents.ON_AD_FAILED_TO_LOAD, params)
+ }
+
+ override fun handleEvent(event: String, params: WritableMap?) {
+ when (event) {
+ NativeEvents.ON_NATIVE_SHOWN -> {
+ if (boundAdId == null) return
+ dispatchFabricEvent(id, NativeEvents.ON_AD_SHOWN, withAdId(params))
+ }
+ NativeEvents.ON_NATIVE_CLICKED -> {
+ if (boundAdId == null) return
+ dispatchFabricEvent(id, NativeEvents.ON_AD_CLICKED, withAdId(params))
+ }
+ NativeEvents.ON_NATIVE_EXPIRED -> {
+ if (boundAdId == null) return
+ try {
+ unregisterView()
+ } catch (_: Exception) {
+ }
+ boundAdId = null
+ dispatchFabricEvent(id, NativeEvents.ON_AD_EXPIRED, withAdId(params))
+ }
+ else -> Unit
+ }
+ }
+
+ private fun withAdId(params: WritableMap?): WritableMap {
+ val copy = Arguments.createMap()
+ if (params != null) copy.merge(params)
+ if (!copy.hasKey("adId")) copy.putString("adId", boundAdId ?: adId)
+ return copy
+ }
+
+ private fun dispatchFabricEvent(viewId: Int, eventName: String, params: WritableMap?) {
+ val dispatcher =
+ UIManagerHelper.getEventDispatcherForReactTag(reactContext, viewId)
+ dispatcher?.dispatchEvent(OnViewEvent(surfaceId, viewId, eventName, params))
+ }
+
+ private class OnViewEvent(
+ surfaceId: Int,
+ viewId: Int,
+ private val eventNameParam: String,
+ private val payload: WritableMap?
+ ) : Event(surfaceId, viewId) {
+ override fun getEventName(): String = eventNameParam
+ override fun getEventData(): WritableMap? {
+ if (payload == null) return null
+ val copy = Arguments.createMap()
+ copy.merge(payload)
+ return copy
+ }
+ }
+
+ private data class Assets(
+ val media: NativeMediaView?,
+ val icon: NativeIconView?,
+ val title: TextView?,
+ val description: TextView?,
+ val callToAction: View?,
+ val attribution: TextView?
+ )
+
+ companion object {
+ private const val TAG = "RNAppodealNativeAd"
+ private const val MAX_BIND_ATTEMPTS = 6
+ private const val BIND_DELAY_MS = 200L
+
+ private val liveViews = CopyOnWriteArrayList>()
+
+ fun notifyActivityReady() {
+ prune()
+ for (ref in liveViews) ref.get()?.onActivityReady()
+ }
+
+ fun unbindAdId(adId: String) {
+ prune()
+ for (ref in liveViews) {
+ val view = ref.get() ?: continue
+ if (view.adId == adId || view.boundAdId == adId) {
+ view.bindGeneration++
+ try {
+ view.unregisterView()
+ } catch (_: Exception) {
+ }
+ view.boundAdId = null
+ }
+ }
+ }
+
+ private fun prune() {
+ liveViews.removeAll { it.get() == null }
+ }
+
+ private fun collectAssets(root: ViewGroup): Assets {
+ var media: NativeMediaView? = null
+ var icon: NativeIconView? = null
+ var title: TextView? = null
+ var description: TextView? = null
+ var callToAction: View? = null
+ var attribution: TextView? = null
+
+ fun walk(view: View) {
+ if (view is RCTAppodealNativeAssetView) {
+ when (view.assetType) {
+ "media" -> media = view.mediaView() ?: media
+ "icon" -> icon = view.iconView() ?: icon
+ "title" -> title = view.textView() ?: title
+ "description" -> description = view.textView() ?: description
+ "callToAction" -> callToAction = view.textView() ?: callToAction
+ "attribution" -> attribution = view.textView() ?: attribution
+ }
+ }
+ if (view is ViewGroup) {
+ for (i in 0 until view.childCount) {
+ walk(view.getChildAt(i))
+ }
+ }
+ }
+
+ walk(root)
+ return Assets(media, icon, title, description, callToAction, attribution)
+ }
+ }
+}
diff --git a/android/src/main/java/com/appodeal/rnappodeal/RCTAppodealNativeAssetView.kt b/android/src/main/java/com/appodeal/rnappodeal/RCTAppodealNativeAssetView.kt
new file mode 100644
index 0000000..657dcde
--- /dev/null
+++ b/android/src/main/java/com/appodeal/rnappodeal/RCTAppodealNativeAssetView.kt
@@ -0,0 +1,113 @@
+package com.appodeal.rnappodeal
+
+import android.content.Context
+import android.view.View
+import android.view.ViewGroup
+import android.widget.FrameLayout
+import android.widget.TextView
+import com.appodeal.ads.nativead.NativeIconView
+import com.appodeal.ads.nativead.NativeMediaView
+import com.facebook.react.views.text.ReactTextView
+
+/**
+ * Marker view for a single native-ad asset inside [RCTAppodealNativeAdView].
+ *
+ * Mirrors Appodeal Android demo custom layout children:
+ * [NativeMediaView], [NativeIconView], and TextViews for title / body / CTA.
+ */
+class RCTAppodealNativeAssetView(context: Context) : FrameLayout(context) {
+
+ var assetType: String = "title"
+ set(value) {
+ val normalized = value.ifBlank { "title" }
+ if (field == normalized) return
+ field = normalized
+ rebuild()
+ }
+
+ private var content: View? = null
+
+ private val measureAndLayout = Runnable {
+ val w = measuredWidth
+ val h = measuredHeight
+ if (w <= 0 || h <= 0) return@Runnable
+ val child = content ?: return@Runnable
+ child.visibility = VISIBLE
+ child.measure(
+ MeasureSpec.makeMeasureSpec(w, MeasureSpec.EXACTLY),
+ MeasureSpec.makeMeasureSpec(h, MeasureSpec.EXACTLY)
+ )
+ child.layout(0, 0, child.measuredWidth, child.measuredHeight)
+ }
+
+ init {
+ clipChildren = false
+ clipToPadding = false
+ rebuild()
+ }
+
+ override fun requestLayout() {
+ super.requestLayout()
+ post(measureAndLayout)
+ }
+
+ override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
+ super.onLayout(changed, left, top, right, bottom)
+ post(measureAndLayout)
+ }
+
+ fun mediaView(): NativeMediaView? = content as? NativeMediaView
+
+ fun iconView(): NativeIconView? = content as? NativeIconView
+
+ fun textView(): TextView? = content as? TextView
+
+ private fun rebuild() {
+ removeAllViews()
+ val view: View = when (assetType) {
+ "media" -> NativeMediaView(context)
+ "icon" -> NativeIconView(context)
+ else -> ReactTextView(context).apply {
+ when (assetType) {
+ "callToAction" -> {
+ setTextColor(0xFFFFFFFF.toInt())
+ textSize = 12f
+ gravity = android.view.Gravity.CENTER
+ maxLines = 1
+ }
+ "attribution" -> {
+ setTextColor(0xFFFFFFFF.toInt())
+ textSize = 9f
+ gravity = android.view.Gravity.CENTER
+ text = "Ad"
+ maxLines = 1
+ }
+ "description" -> {
+ setTextColor(0xFF7F6D5F.toInt())
+ textSize = 11f
+ maxLines = 1
+ }
+ else -> {
+ setTextColor(0xFF2D2424.toInt())
+ textSize = 13f
+ maxLines = 2
+ }
+ }
+ }
+ }
+ content = view
+ addView(
+ view,
+ LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT
+ )
+ )
+ post(measureAndLayout)
+ (parent as? RCTAppodealNativeAdView)?.onAssetChanged()
+ }
+
+ companion object {
+ const val NAME = "RNAppodealNativeAssetView"
+ }
+}
diff --git a/android/src/main/java/com/appodeal/rnappodeal/RCTAppodealNativeView.kt b/android/src/main/java/com/appodeal/rnappodeal/RCTAppodealNativeView.kt
new file mode 100644
index 0000000..2e87579
--- /dev/null
+++ b/android/src/main/java/com/appodeal/rnappodeal/RCTAppodealNativeView.kt
@@ -0,0 +1,421 @@
+package com.appodeal.rnappodeal
+
+import android.content.Context
+import android.graphics.Color
+import android.os.Handler
+import android.os.Looper
+import android.util.Log
+import android.view.ViewGroup
+import android.widget.FrameLayout
+import com.appodeal.ads.NativeAd
+import com.appodeal.ads.nativead.NativeAdView
+import com.appodeal.ads.nativead.NativeAdViewAppWall
+import com.appodeal.ads.nativead.NativeAdViewContentStream
+import com.appodeal.ads.nativead.NativeAdViewNewsFeed
+import com.appodeal.ads.nativead.Position
+import com.appodeal.rnappodeal.callbacks.RNAppodealEventHandler
+import com.appodeal.rnappodeal.constants.NativeEvents
+import com.facebook.react.bridge.Arguments
+import com.facebook.react.bridge.ReactContext
+import com.facebook.react.bridge.WritableMap
+import com.facebook.react.uimanager.UIManagerHelper
+import com.facebook.react.uimanager.events.Event
+import com.facebook.react.views.view.ReactViewGroup
+import java.lang.ref.WeakReference
+import java.util.concurrent.CopyOnWriteArrayList
+
+/**
+ * RN host for Appodeal **stock** native templates
+ * (`newsFeed` / `appWall` / `contentStream`).
+ *
+ * Custom layouts use [RCTAppodealNativeAdView] + asset children instead.
+ */
+class RCTAppodealNativeView(context: Context) : ReactViewGroup(context), RNAppodealEventHandler {
+
+ private val reactContext: ReactContext = context as ReactContext
+ private val surfaceId: Int by lazy { UIManagerHelper.getSurfaceId(reactContext) }
+ private val uiHandler: Handler by lazy { Handler(Looper.getMainLooper()) }
+
+ var adId: String? = null
+ set(value) {
+ if (field == value) return
+ field = value
+ boundAdId = null
+ bindGeneration++
+ scheduleBind(BIND_DELAY_MS)
+ }
+
+ var placement: String = "default"
+ set(value) {
+ if (field == value) return
+ field = value
+ if (boundAdId != null) {
+ boundAdId = null
+ bindGeneration++
+ scheduleBind(BIND_DELAY_MS)
+ }
+ }
+
+ var adTemplate: String = "contentStream"
+ set(value) {
+ val normalized = value.ifBlank { "contentStream" }
+ if (field == normalized) return
+ field = normalized
+ tearDownAdView()
+ boundAdId = null
+ bindGeneration++
+ scheduleBind(BIND_DELAY_MS)
+ }
+
+ private var adView: NativeAdView? = null
+ private var boundAdId: String? = null
+ private var bindRunnable: Runnable? = null
+ private var bindGeneration: Int = 0
+ private var bindAttempts: Int = 0
+
+ private val measureAndLayout = Runnable {
+ val w = measuredWidth
+ val h = measuredHeight
+ if (w <= 0 || h <= 0) return@Runnable
+ for (i in 0 until childCount) {
+ val child = getChildAt(i)
+ child.visibility = VISIBLE
+ child.measure(
+ MeasureSpec.makeMeasureSpec(w, MeasureSpec.EXACTLY),
+ MeasureSpec.makeMeasureSpec(h, MeasureSpec.EXACTLY)
+ )
+ child.layout(0, 0, child.measuredWidth, child.measuredHeight)
+ }
+ }
+
+ init {
+ liveViews.add(WeakReference(this))
+ visibility = VISIBLE
+ setBackgroundColor(Color.TRANSPARENT)
+ contentDescription = "appodeal-native-host"
+ clipChildren = false
+ clipToPadding = false
+ }
+
+ override fun requestLayout() {
+ super.requestLayout()
+ post(measureAndLayout)
+ }
+
+ override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
+ super.onLayout(changed, left, top, right, bottom)
+ post(measureAndLayout)
+ if (right - left > 0 && bottom - top > 0 && boundAdId == null && !adId.isNullOrEmpty()) {
+ scheduleBind(0)
+ }
+ }
+
+ override fun onAttachedToWindow() {
+ super.onAttachedToWindow()
+ if (boundAdId == null && !adId.isNullOrEmpty()) {
+ scheduleBind(0)
+ }
+ }
+
+ fun cleanup() {
+ bindRunnable?.let { uiHandler.removeCallbacks(it) }
+ bindRunnable = null
+ bindGeneration++
+ tearDownAdView()
+ boundAdId = null
+ bindAttempts = 0
+ val self = this
+ liveViews.removeAll { it.get() == null || it.get() === self }
+ }
+
+ fun onActivityReady() {
+ if (boundAdId == null && !adId.isNullOrEmpty()) {
+ scheduleBind(0)
+ }
+ }
+
+ private fun tearDownAdView() {
+ val view = adView
+ adView = null
+ if (view != null) {
+ try {
+ view.unregisterView()
+ } catch (_: Exception) {
+ }
+ try {
+ view.destroy()
+ } catch (_: Exception) {
+ }
+ (view.parent as? ViewGroup)?.removeView(view)
+ }
+ removeAllViews()
+ }
+
+ private fun ensureAdView(): NativeAdView {
+ adView?.let { existing ->
+ existing.visibility = VISIBLE
+ post(measureAndLayout)
+ return existing
+ }
+
+ val view = createTemplate(adTemplate).apply {
+ visibility = VISIBLE
+ descendantFocusability = FOCUS_BLOCK_DESCENDANTS
+ setAdChoicesPosition(Position.END_TOP)
+ contentDescription = "appodeal-native-ad"
+ layoutParams = FrameLayout.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT
+ )
+ }
+
+ removeAllViews()
+ this.visibility = VISIBLE
+ addView(view)
+ view.bringToFront()
+ try {
+ (parent as? ViewGroup)?.bringChildToFront(this)
+ } catch (_: Exception) {
+ }
+ post(measureAndLayout)
+
+ adView = view
+ return view
+ }
+
+ private fun createTemplate(template: String): NativeAdView {
+ return when (template) {
+ "newsFeed" -> NativeAdViewNewsFeed(context)
+ "appWall" -> NativeAdViewAppWall(context)
+ else -> NativeAdViewContentStream(context)
+ }
+ }
+
+ private fun scheduleBind(delayMs: Long) {
+ bindRunnable?.let { uiHandler.removeCallbacks(it) }
+ val gen = bindGeneration
+ val runnable = Runnable {
+ if (gen != bindGeneration) return@Runnable
+ bindAd(gen)
+ }.also { bindRunnable = it }
+ uiHandler.postDelayed(runnable, delayMs.coerceAtLeast(0L))
+ }
+
+ private fun bindAd(gen: Int) {
+ if (gen != bindGeneration) return
+ val id = adId
+ if (id.isNullOrEmpty()) return
+ if (boundAdId == id) return
+
+ val ad: NativeAd = RNAppodealNativeAdStore.get(id) ?: run {
+ if (!retry(gen, id, "native ad not in store")) {
+ dispatchFailed(id, "native ad not in store")
+ }
+ return
+ }
+
+ if (measuredWidth <= 0 || measuredHeight <= 0) {
+ if (!retry(gen, id, "view has zero size")) {
+ dispatchFailed(id, "view has zero size")
+ }
+ return
+ }
+
+ if (!isAttachedToWindow) {
+ if (!retry(gen, id, "not attached to window")) {
+ dispatchFailed(id, "not attached to window")
+ }
+ return
+ }
+
+ val activity = RNAppodealActivityHolder.get() ?: reactContext.currentActivity
+ if (activity == null) {
+ if (!retry(gen, id, "activity unavailable")) {
+ dispatchFailed(id, "activity unavailable")
+ }
+ return
+ }
+
+ val canShow = try {
+ ad.canShow(activity, placement)
+ } catch (e: Exception) {
+ Log.e(TAG, "canShow threw", e)
+ false
+ }
+ if (!canShow) {
+ if (!retry(gen, id, "canShow=false")) {
+ dispatchFailed(id, "canShow=false for placement=$placement")
+ }
+ return
+ }
+
+ val view = ensureAdView()
+ view.visibility = VISIBLE
+ runMeasureAndLayoutNow()
+
+ if (view.width <= 0 || view.height <= 0) {
+ if (!retry(gen, id, "child still 0x0 after layout")) {
+ dispatchFailed(id, "child still 0x0 after layout")
+ }
+ return
+ }
+
+ try {
+ view.unregisterView()
+ } catch (_: Exception) {
+ }
+
+ val registered = try {
+ view.registerView(ad, placement)
+ } catch (e: Exception) {
+ Log.e(TAG, "registerView threw", e)
+ false
+ }
+
+ view.visibility = VISIBLE
+ this.visibility = VISIBLE
+ runMeasureAndLayoutNow()
+
+ Log.d(
+ TAG,
+ "registerView id=$id result=$registered host=${measuredWidth}x${measuredHeight} " +
+ "childSize=${view.width}x${view.height} template=$adTemplate"
+ )
+
+ if (gen != bindGeneration) return
+
+ if (registered) {
+ boundAdId = id
+ bindAttempts = 0
+ uiHandler.postDelayed({
+ if (gen != bindGeneration) return@postDelayed
+ view.visibility = VISIBLE
+ runMeasureAndLayoutNow()
+ }, 100L)
+ dispatchLoaded(id)
+ } else if (!retry(gen, id, "registerView returned false")) {
+ dispatchFailed(id, "registerView returned false")
+ }
+ }
+
+ private fun runMeasureAndLayoutNow() {
+ val w = measuredWidth
+ val h = measuredHeight
+ if (w <= 0 || h <= 0) return
+ for (i in 0 until childCount) {
+ val child = getChildAt(i)
+ child.visibility = VISIBLE
+ child.measure(
+ MeasureSpec.makeMeasureSpec(w, MeasureSpec.EXACTLY),
+ MeasureSpec.makeMeasureSpec(h, MeasureSpec.EXACTLY)
+ )
+ child.layout(0, 0, child.measuredWidth, child.measuredHeight)
+ }
+ }
+
+ private fun retry(gen: Int, id: String, reason: String): Boolean {
+ if (gen != bindGeneration) return true
+ bindAttempts += 1
+ if (bindAttempts > MAX_BIND_ATTEMPTS) return false
+ Log.d(TAG, "retry #$bindAttempts id=$id reason=$reason")
+ scheduleBind(BIND_DELAY_MS * bindAttempts)
+ return true
+ }
+
+ private fun dispatchLoaded(adIdValue: String) {
+ val params = Arguments.createMap().apply { putString("adId", adIdValue) }
+ dispatchFabricEvent(id, NativeEvents.ON_AD_LOADED, params)
+ }
+
+ private fun dispatchFailed(adIdValue: String, message: String) {
+ Log.w(TAG, "bind failed id=$adIdValue message=$message")
+ val params = Arguments.createMap().apply {
+ putString("adId", adIdValue)
+ putString("message", message)
+ }
+ dispatchFabricEvent(id, NativeEvents.ON_AD_FAILED_TO_LOAD, params)
+ }
+
+ override fun handleEvent(event: String, params: WritableMap?) {
+ when (event) {
+ NativeEvents.ON_NATIVE_SHOWN -> {
+ if (boundAdId == null) return
+ dispatchFabricEvent(id, NativeEvents.ON_AD_SHOWN, withAdId(params))
+ }
+ NativeEvents.ON_NATIVE_CLICKED -> {
+ if (boundAdId == null) return
+ dispatchFabricEvent(id, NativeEvents.ON_AD_CLICKED, withAdId(params))
+ }
+ NativeEvents.ON_NATIVE_EXPIRED -> {
+ if (boundAdId == null) return
+ try {
+ adView?.unregisterView()
+ } catch (_: Exception) {
+ }
+ boundAdId = null
+ dispatchFabricEvent(id, NativeEvents.ON_AD_EXPIRED, withAdId(params))
+ }
+ else -> Unit
+ }
+ }
+
+ private fun withAdId(params: WritableMap?): WritableMap {
+ val copy = Arguments.createMap()
+ if (params != null) copy.merge(params)
+ if (!copy.hasKey("adId")) copy.putString("adId", boundAdId ?: adId)
+ return copy
+ }
+
+ private fun dispatchFabricEvent(viewId: Int, eventName: String, params: WritableMap?) {
+ val dispatcher =
+ UIManagerHelper.getEventDispatcherForReactTag(reactContext, viewId)
+ dispatcher?.dispatchEvent(OnViewEvent(surfaceId, viewId, eventName, params))
+ }
+
+ private class OnViewEvent(
+ surfaceId: Int,
+ viewId: Int,
+ private val eventNameParam: String,
+ private val payload: WritableMap?
+ ) : Event(surfaceId, viewId) {
+ override fun getEventName(): String = eventNameParam
+ override fun getEventData(): WritableMap? {
+ if (payload == null) return null
+ val copy = Arguments.createMap()
+ copy.merge(payload)
+ return copy
+ }
+ }
+
+ companion object {
+ private const val TAG = "RNAppodealNative"
+ private const val MAX_BIND_ATTEMPTS = 6
+ private const val BIND_DELAY_MS = 200L
+
+ private val liveViews = CopyOnWriteArrayList>()
+
+ fun notifyActivityReady() {
+ prune()
+ for (ref in liveViews) ref.get()?.onActivityReady()
+ }
+
+ fun unbindAdId(adId: String) {
+ prune()
+ for (ref in liveViews) {
+ val view = ref.get() ?: continue
+ if (view.adId == adId || view.boundAdId == adId) {
+ view.bindGeneration++
+ try {
+ view.adView?.unregisterView()
+ } catch (_: Exception) {
+ }
+ view.boundAdId = null
+ }
+ }
+ }
+
+ private fun prune() {
+ liveViews.removeAll { it.get() == null }
+ }
+ }
+}
diff --git a/android/src/main/java/com/appodeal/rnappodeal/RNAppodealActivityHolder.kt b/android/src/main/java/com/appodeal/rnappodeal/RNAppodealActivityHolder.kt
new file mode 100644
index 0000000..84d9147
--- /dev/null
+++ b/android/src/main/java/com/appodeal/rnappodeal/RNAppodealActivityHolder.kt
@@ -0,0 +1,19 @@
+package com.appodeal.rnappodeal
+
+import android.app.Activity
+import java.lang.ref.WeakReference
+
+/**
+ * Shared Activity ref for native ad views.
+ * Mirrors [RNAppodealModuleImpl]'s workaround for RN's null getCurrentActivity bug.
+ */
+internal object RNAppodealActivityHolder {
+ @Volatile
+ private var activityRef: WeakReference? = null
+
+ fun set(activity: Activity?) {
+ activityRef = if (activity != null) WeakReference(activity) else null
+ }
+
+ fun get(): Activity? = activityRef?.get()
+}
diff --git a/android/src/main/java/com/appodeal/rnappodeal/RNAppodealModuleImpl.kt b/android/src/main/java/com/appodeal/rnappodeal/RNAppodealModuleImpl.kt
index 827e2d2..2fea0bb 100644
--- a/android/src/main/java/com/appodeal/rnappodeal/RNAppodealModuleImpl.kt
+++ b/android/src/main/java/com/appodeal/rnappodeal/RNAppodealModuleImpl.kt
@@ -4,6 +4,7 @@ import android.app.Activity
import android.content.Context
import android.util.Log
import com.appodeal.ads.Appodeal
+import com.appodeal.ads.NativeMediaViewContentType
import com.appodeal.ads.inapp.InAppPurchase
import com.appodeal.ads.inapp.InAppPurchaseValidateCallback
import com.appodeal.ads.service.ServiceError
@@ -11,6 +12,7 @@ import com.appodeal.rnappodeal.callbacks.RNAppodealAdRevenueCallbacks
import com.appodeal.rnappodeal.callbacks.RNAppodealBannerCallbacks
import com.appodeal.rnappodeal.callbacks.RNAppodealInterstitialCallbacks
import com.appodeal.rnappodeal.callbacks.RNAppodealMrecCallbacks
+import com.appodeal.rnappodeal.callbacks.RNAppodealNativeCallbacks
import com.appodeal.rnappodeal.callbacks.RNAppodealRewardedVideoCallbacks
import com.appodeal.rnappodeal.ext.AdTypeExtensions.toAppodealTypes
import com.appodeal.rnappodeal.ext.LogLevelExtensions.toLogLevel
@@ -42,6 +44,7 @@ internal class RNAppodealModuleImpl(
private val mrecCallbacks by lazy { RNAppodealMrecCallbacks(eventDispatcher) }
private val rewardedVideoCallbacks by lazy { RNAppodealRewardedVideoCallbacks(eventDispatcher) }
private val adRevenueCallbacks by lazy { RNAppodealAdRevenueCallbacks(eventDispatcher) }
+ private val nativeCallbacks by lazy { RNAppodealNativeCallbacks(eventDispatcher) }
// Consent handler
private val consentHandler by lazy { RNAppodealConsent() }
@@ -52,6 +55,7 @@ internal class RNAppodealModuleImpl(
init {
this.currentActivity = WeakReference(reactContext.currentActivity)
+ RNAppodealActivityHolder.set(reactContext.currentActivity)
this.reactContext.addLifecycleEventListener(this)
}
@@ -63,8 +67,9 @@ internal class RNAppodealModuleImpl(
private fun getActivity(): Activity? {
if (reactContext.hasCurrentActivity()) {
currentActivity = WeakReference(reactContext.currentActivity)
+ RNAppodealActivityHolder.set(reactContext.currentActivity)
}
- return currentActivity?.get()
+ return currentActivity?.get() ?: RNAppodealActivityHolder.get()
}
/**
@@ -114,6 +119,7 @@ internal class RNAppodealModuleImpl(
Appodeal.setBannerCallbacks(bannerCallbacks)
Appodeal.setMrecCallbacks(mrecCallbacks)
Appodeal.setRewardedVideoCallbacks(rewardedVideoCallbacks)
+ Appodeal.setNativeCallbacks(nativeCallbacks)
Appodeal.setAdRevenueCallbacks(adRevenueCallbacks)
// Set up Appodeal options
@@ -388,6 +394,46 @@ internal class RNAppodealModuleImpl(
Appodeal.logEvent(name, parameters.toMap())
}
+ /**
+ * TurboModule codegen maps Spec `UnsafeObject` → WritableMap.
+ * Shape: `{ ads: Array }`.
+ */
+ fun getNativeAds(count: Double): WritableMap {
+ val ads = Appodeal.getNativeAds(count.toInt())
+ val storedAds = RNAppodealNativeAdStore.putAds(ads)
+ val array = Arguments.createArray().apply {
+ storedAds.forEach { pushMap(it) }
+ }
+ return Arguments.createMap().apply {
+ putArray("ads", array)
+ }
+ }
+
+ fun getAvailableNativeAdsCount(): Double {
+ return Appodeal.getAvailableNativeAdsCount().toDouble()
+ }
+
+ fun destroyNativeAd(adId: String) {
+ RCTAppodealNativeView.unbindAdId(adId)
+ RCTAppodealNativeAdView.unbindAdId(adId)
+ RNAppodealNativeAdStore.remove(adId)
+ }
+
+ fun cacheNativeAds(count: Double) {
+ withActivity("cacheNativeAds") { activity ->
+ Appodeal.cache(activity, Appodeal.NATIVE, count.toInt().coerceIn(1, 5))
+ }
+ }
+
+ fun setPreferredNativeContentType(type: String) {
+ val contentType = when (type) {
+ "noVideo" -> NativeMediaViewContentType.NoVideo
+ "video" -> NativeMediaViewContentType.Video
+ else -> NativeMediaViewContentType.Auto
+ }
+ Appodeal.setPreferredNativeContentType(contentType)
+ }
+
// MARK: - Event Management
fun eventsNotifyReady(ready: Boolean) {
@@ -418,7 +464,13 @@ internal class RNAppodealModuleImpl(
}
override fun onHostResume() {
- // Not implemented
+ val activity = reactContext.currentActivity
+ if (activity != null) {
+ currentActivity = WeakReference(activity)
+ RNAppodealActivityHolder.set(activity)
+ }
+ RCTAppodealNativeView.notifyActivityReady()
+ RCTAppodealNativeAdView.notifyActivityReady()
}
companion object Companion {
diff --git a/android/src/main/java/com/appodeal/rnappodeal/RNAppodealNativeAdStore.kt b/android/src/main/java/com/appodeal/rnappodeal/RNAppodealNativeAdStore.kt
new file mode 100644
index 0000000..2cfcd4a
--- /dev/null
+++ b/android/src/main/java/com/appodeal/rnappodeal/RNAppodealNativeAdStore.kt
@@ -0,0 +1,38 @@
+package com.appodeal.rnappodeal
+
+import com.appodeal.ads.NativeAd
+import com.facebook.react.bridge.Arguments
+import com.facebook.react.bridge.WritableMap
+import java.util.UUID
+import java.util.concurrent.ConcurrentHashMap
+
+internal object RNAppodealNativeAdStore {
+
+ private val ads = ConcurrentHashMap()
+
+ fun putAds(ads: List): List {
+ return ads.map { ad ->
+ val id = UUID.randomUUID().toString()
+ this.ads[id] = ad
+ ad.toWritableMap(id)
+ }
+ }
+
+ fun get(id: String): NativeAd? = ads[id]
+
+ fun remove(id: String) {
+ ads.remove(id)
+ }
+
+ private fun NativeAd.toWritableMap(id: String): WritableMap {
+ return Arguments.createMap().apply {
+ putString("id", id)
+ putString("title", title.orEmpty())
+ putString("description", description.orEmpty())
+ putString("callToAction", callToAction.orEmpty())
+ putDouble("rating", rating.toDouble())
+ putBoolean("containsVideo", containsVideo())
+ putDouble("predictedEcpm", predictedEcpm)
+ }
+ }
+}
diff --git a/android/src/main/java/com/appodeal/rnappodeal/RNAppodealNativeAdViewManagerImpl.kt b/android/src/main/java/com/appodeal/rnappodeal/RNAppodealNativeAdViewManagerImpl.kt
new file mode 100644
index 0000000..510b43c
--- /dev/null
+++ b/android/src/main/java/com/appodeal/rnappodeal/RNAppodealNativeAdViewManagerImpl.kt
@@ -0,0 +1,34 @@
+package com.appodeal.rnappodeal
+
+import com.appodeal.rnappodeal.callbacks.RNAppodealEventHandlerManager
+import com.appodeal.rnappodeal.constants.NativeEvents
+import com.facebook.react.uimanager.ThemedReactContext
+
+internal class RNAppodealNativeAdViewManagerImpl(
+ private val handlerManager: RNAppodealEventHandlerManager = RNAppodealEventHandlerManager
+) {
+ fun createViewInstance(reactContext: ThemedReactContext): RCTAppodealNativeAdView {
+ val view = RCTAppodealNativeAdView(reactContext)
+ handlerManager.registerHandler(view)
+ return view
+ }
+
+ fun onDropViewInstance(view: RCTAppodealNativeAdView) {
+ view.cleanup()
+ handlerManager.unregisterHandler(view)
+ }
+
+ fun getExportedCustomDirectEventTypeConstants(): Map? {
+ return mapOf(
+ NativeEvents.ON_AD_LOADED to mapOf("registrationName" to NativeEvents.ON_AD_LOADED),
+ NativeEvents.ON_AD_FAILED_TO_LOAD to mapOf("registrationName" to NativeEvents.ON_AD_FAILED_TO_LOAD),
+ NativeEvents.ON_AD_SHOWN to mapOf("registrationName" to NativeEvents.ON_AD_SHOWN),
+ NativeEvents.ON_AD_CLICKED to mapOf("registrationName" to NativeEvents.ON_AD_CLICKED),
+ NativeEvents.ON_AD_EXPIRED to mapOf("registrationName" to NativeEvents.ON_AD_EXPIRED)
+ )
+ }
+
+ companion object {
+ const val NAME = "RNAppodealNativeAdView"
+ }
+}
diff --git a/android/src/main/java/com/appodeal/rnappodeal/RNAppodealNativeAssetViewManagerImpl.kt b/android/src/main/java/com/appodeal/rnappodeal/RNAppodealNativeAssetViewManagerImpl.kt
new file mode 100644
index 0000000..8f7fa0b
--- /dev/null
+++ b/android/src/main/java/com/appodeal/rnappodeal/RNAppodealNativeAssetViewManagerImpl.kt
@@ -0,0 +1,17 @@
+package com.appodeal.rnappodeal
+
+import com.facebook.react.uimanager.ThemedReactContext
+
+internal class RNAppodealNativeAssetViewManagerImpl {
+ fun createViewInstance(reactContext: ThemedReactContext): RCTAppodealNativeAssetView {
+ return RCTAppodealNativeAssetView(reactContext)
+ }
+
+ fun onDropViewInstance(view: RCTAppodealNativeAssetView) {
+ // no-op
+ }
+
+ companion object {
+ const val NAME = RCTAppodealNativeAssetView.NAME
+ }
+}
diff --git a/android/src/main/java/com/appodeal/rnappodeal/RNAppodealNativeViewManagerImpl.kt b/android/src/main/java/com/appodeal/rnappodeal/RNAppodealNativeViewManagerImpl.kt
new file mode 100644
index 0000000..7b6eeb5
--- /dev/null
+++ b/android/src/main/java/com/appodeal/rnappodeal/RNAppodealNativeViewManagerImpl.kt
@@ -0,0 +1,35 @@
+package com.appodeal.rnappodeal
+
+import com.appodeal.rnappodeal.callbacks.RNAppodealEventHandlerManager
+import com.appodeal.rnappodeal.constants.NativeEvents
+import com.facebook.react.uimanager.ThemedReactContext
+
+internal class RNAppodealNativeViewManagerImpl(
+ private val handlerManager: RNAppodealEventHandlerManager = RNAppodealEventHandlerManager
+) {
+
+ fun createViewInstance(reactContext: ThemedReactContext): RCTAppodealNativeView {
+ val nativeView = RCTAppodealNativeView(reactContext)
+ handlerManager.registerHandler(nativeView)
+ return nativeView
+ }
+
+ fun onDropViewInstance(view: RCTAppodealNativeView) {
+ view.cleanup()
+ handlerManager.unregisterHandler(view)
+ }
+
+ fun getExportedCustomDirectEventTypeConstants(): Map? {
+ return mapOf(
+ NativeEvents.ON_AD_LOADED to mapOf("registrationName" to NativeEvents.ON_AD_LOADED),
+ NativeEvents.ON_AD_FAILED_TO_LOAD to mapOf("registrationName" to NativeEvents.ON_AD_FAILED_TO_LOAD),
+ NativeEvents.ON_AD_SHOWN to mapOf("registrationName" to NativeEvents.ON_AD_SHOWN),
+ NativeEvents.ON_AD_CLICKED to mapOf("registrationName" to NativeEvents.ON_AD_CLICKED),
+ NativeEvents.ON_AD_EXPIRED to mapOf("registrationName" to NativeEvents.ON_AD_EXPIRED)
+ )
+ }
+
+ companion object Companion {
+ const val NAME = "RNAppodealNativeView"
+ }
+}
diff --git a/android/src/main/java/com/appodeal/rnappodeal/RNAppodealPackage.kt b/android/src/main/java/com/appodeal/rnappodeal/RNAppodealPackage.kt
index 2611291..b9bccd4 100644
--- a/android/src/main/java/com/appodeal/rnappodeal/RNAppodealPackage.kt
+++ b/android/src/main/java/com/appodeal/rnappodeal/RNAppodealPackage.kt
@@ -20,7 +20,10 @@ class RNAppodealPackage : BaseReactPackage() {
reactContext: ReactApplicationContext
): List> = listOf(
RNAppodealBannerViewManager(),
- RNAppodealMrecViewManager()
+ RNAppodealMrecViewManager(),
+ RNAppodealNativeViewManager(),
+ RNAppodealNativeAdViewManager(),
+ RNAppodealNativeAssetViewManager()
)
override fun getReactModuleInfoProvider(): ReactModuleInfoProvider {
diff --git a/android/src/main/java/com/appodeal/rnappodeal/callbacks/RNAppodealNativeCallbacks.kt b/android/src/main/java/com/appodeal/rnappodeal/callbacks/RNAppodealNativeCallbacks.kt
new file mode 100644
index 0000000..c33fba1
--- /dev/null
+++ b/android/src/main/java/com/appodeal/rnappodeal/callbacks/RNAppodealNativeCallbacks.kt
@@ -0,0 +1,62 @@
+package com.appodeal.rnappodeal.callbacks
+
+import com.appodeal.ads.NativeAd
+import com.appodeal.ads.NativeCallbacks
+import com.appodeal.rnappodeal.RNAEventDispatcher
+import com.appodeal.rnappodeal.constants.NativeEvents
+import com.facebook.react.bridge.Arguments
+
+/**
+ * Callback handler for Appodeal native ad events.
+ *
+ * Appodeal 4.2.0 exposes these Java callbacks as platform types; Kotlin sees
+ * the NativeAd parameters as nullable (`NativeAd?`).
+ */
+internal class RNAppodealNativeCallbacks(
+ private val eventDispatcher: RNAEventDispatcher,
+ private val handlers: RNAppodealEventHandler = RNAppodealEventHandlerManager
+) : NativeCallbacks {
+
+ override fun onNativeLoaded() {
+ eventDispatcher.dispatchEvent(NativeEvents.ON_NATIVE_LOADED, null)
+ handlers.handleEvent(NativeEvents.ON_NATIVE_LOADED, null)
+ }
+
+ override fun onNativeFailedToLoad() {
+ eventDispatcher.dispatchEvent(NativeEvents.ON_NATIVE_FAILED_TO_LOAD, null)
+ handlers.handleEvent(NativeEvents.ON_NATIVE_FAILED_TO_LOAD, null)
+ }
+
+ override fun onNativeShown(nativeAd: NativeAd?) {
+ val params = createNativeAdParams(nativeAd)
+ eventDispatcher.dispatchEvent(NativeEvents.ON_NATIVE_SHOWN, params)
+ handlers.handleEvent(NativeEvents.ON_NATIVE_SHOWN, params)
+ }
+
+ override fun onNativeShowFailed(nativeAd: NativeAd?) {
+ val params = createNativeAdParams(nativeAd)
+ eventDispatcher.dispatchEvent(NativeEvents.ON_NATIVE_SHOW_FAILED, params)
+ handlers.handleEvent(NativeEvents.ON_NATIVE_SHOW_FAILED, params)
+ }
+
+ override fun onNativeClicked(nativeAd: NativeAd?) {
+ val params = createNativeAdParams(nativeAd)
+ eventDispatcher.dispatchEvent(NativeEvents.ON_NATIVE_CLICKED, params)
+ handlers.handleEvent(NativeEvents.ON_NATIVE_CLICKED, params)
+ }
+
+ override fun onNativeExpired() {
+ eventDispatcher.dispatchEvent(NativeEvents.ON_NATIVE_EXPIRED, null)
+ handlers.handleEvent(NativeEvents.ON_NATIVE_EXPIRED, null)
+ }
+
+ private fun createNativeAdParams(nativeAd: NativeAd?) = Arguments.createMap().apply {
+ if (nativeAd == null) return@apply
+ putString("title", nativeAd.title ?: "")
+ putString("description", nativeAd.description ?: "")
+ putString("callToAction", nativeAd.callToAction ?: "")
+ putDouble("rating", nativeAd.rating.toDouble())
+ putBoolean("containsVideo", nativeAd.containsVideo())
+ putDouble("predictedEcpm", nativeAd.predictedEcpm)
+ }
+}
diff --git a/android/src/main/java/com/appodeal/rnappodeal/constants/NativeEvents.kt b/android/src/main/java/com/appodeal/rnappodeal/constants/NativeEvents.kt
new file mode 100644
index 0000000..4f74238
--- /dev/null
+++ b/android/src/main/java/com/appodeal/rnappodeal/constants/NativeEvents.kt
@@ -0,0 +1,21 @@
+package com.appodeal.rnappodeal.constants
+
+/**
+ * Constants for native ad events.
+ * Centralized event names that can be reused across the codebase.
+ */
+internal object NativeEvents {
+ const val ON_NATIVE_LOADED = "onNativeLoaded"
+ const val ON_NATIVE_FAILED_TO_LOAD = "onNativeFailedToLoad"
+ const val ON_NATIVE_SHOWN = "onNativeShown"
+ const val ON_NATIVE_SHOW_FAILED = "onNativeShowFailed"
+ const val ON_NATIVE_CLICKED = "onNativeClicked"
+ const val ON_NATIVE_EXPIRED = "onNativeExpired"
+
+ // React Native event registration names
+ const val ON_AD_LOADED = "onAdLoaded"
+ const val ON_AD_FAILED_TO_LOAD = "onAdFailedToLoad"
+ const val ON_AD_CLICKED = "onAdClicked"
+ const val ON_AD_EXPIRED = "onAdExpired"
+ const val ON_AD_SHOWN = "onAdShown"
+}
diff --git a/android/src/main/java/com/appodeal/rnappodeal/ext/AdTypeExtensions.kt b/android/src/main/java/com/appodeal/rnappodeal/ext/AdTypeExtensions.kt
index 11d81db..8a0e3d2 100644
--- a/android/src/main/java/com/appodeal/rnappodeal/ext/AdTypeExtensions.kt
+++ b/android/src/main/java/com/appodeal/rnappodeal/ext/AdTypeExtensions.kt
@@ -24,6 +24,7 @@ internal object AdTypeExtensions {
if (types.hasFlag(BANNER_TOP_FLAG)) result = result or Appodeal.BANNER_TOP
if (types.hasFlag(REWARDED_VIDEO_FLAG)) result = result or Appodeal.REWARDED_VIDEO
if (types.hasFlag(MREC_FLAG)) result = result or Appodeal.MREC
+ if (types.hasFlag(NATIVE_FLAG)) result = result or Appodeal.NATIVE
return result
}
@@ -42,6 +43,7 @@ internal object AdTypeExtensions {
if (this.hasFlag(Appodeal.BANNER_TOP)) result = result or BANNER_TOP_FLAG
if (this.hasFlag(Appodeal.REWARDED_VIDEO)) result = result or REWARDED_VIDEO_FLAG
if (this.hasFlag(Appodeal.MREC)) result = result or MREC_FLAG
+ if (this.hasFlag(Appodeal.NATIVE)) result = result or NATIVE_FLAG
return result
}
@@ -61,4 +63,5 @@ internal object AdTypeExtensions {
private const val BANNER_TOP_FLAG = 1 shl 4
private const val REWARDED_VIDEO_FLAG = 1 shl 5
private const val MREC_FLAG = 1 shl 8
+ private const val NATIVE_FLAG = 1 shl 7
}
\ No newline at end of file
diff --git a/android/src/newarch/com/appodeal/rnappodeal/RNAppodealModule.kt b/android/src/newarch/com/appodeal/rnappodeal/RNAppodealModule.kt
index 0995c4b..38b59cd 100644
--- a/android/src/newarch/com/appodeal/rnappodeal/RNAppodealModule.kt
+++ b/android/src/newarch/com/appodeal/rnappodeal/RNAppodealModule.kt
@@ -197,6 +197,26 @@ class RNAppodealModule(
moduleImplementation.trackEvent(name, parameters)
}
+ override fun getNativeAds(count: Double): WritableMap {
+ return moduleImplementation.getNativeAds(count)
+ }
+
+ override fun getAvailableNativeAdsCount(): Double {
+ return moduleImplementation.getAvailableNativeAdsCount()
+ }
+
+ override fun destroyNativeAd(adId: String) {
+ moduleImplementation.destroyNativeAd(adId)
+ }
+
+ override fun cacheNativeAds(count: Double) {
+ moduleImplementation.cacheNativeAds(count)
+ }
+
+ override fun setPreferredNativeContentType(type: String) {
+ moduleImplementation.setPreferredNativeContentType(type)
+ }
+
override fun eventsNotifyReady(ready: Boolean) {
moduleImplementation.eventsNotifyReady(ready)
}
diff --git a/android/src/newarch/com/appodeal/rnappodeal/RNAppodealNativeAdViewManager.kt b/android/src/newarch/com/appodeal/rnappodeal/RNAppodealNativeAdViewManager.kt
new file mode 100644
index 0000000..dd2d352
--- /dev/null
+++ b/android/src/newarch/com/appodeal/rnappodeal/RNAppodealNativeAdViewManager.kt
@@ -0,0 +1,38 @@
+package com.appodeal.rnappodeal
+
+import com.facebook.react.module.annotations.ReactModule
+import com.facebook.react.uimanager.ThemedReactContext
+import com.facebook.react.uimanager.ViewGroupManager
+import com.facebook.react.uimanager.annotations.ReactProp
+
+@ReactModule(name = RNAppodealNativeAdViewManagerImpl.NAME)
+class RNAppodealNativeAdViewManager :
+ ViewGroupManager() {
+
+ private val impl = RNAppodealNativeAdViewManagerImpl()
+
+ override fun getName(): String = RNAppodealNativeAdViewManagerImpl.NAME
+
+ override fun createViewInstance(reactContext: ThemedReactContext): RCTAppodealNativeAdView {
+ return impl.createViewInstance(reactContext)
+ }
+
+ override fun onDropViewInstance(view: RCTAppodealNativeAdView) {
+ super.onDropViewInstance(view)
+ impl.onDropViewInstance(view)
+ }
+
+ override fun getExportedCustomDirectEventTypeConstants(): Map? {
+ return impl.getExportedCustomDirectEventTypeConstants()
+ }
+
+ @ReactProp(name = "adId")
+ fun setAdId(view: RCTAppodealNativeAdView, value: String?) {
+ view.adId = value
+ }
+
+ @ReactProp(name = "placement")
+ fun setPlacement(view: RCTAppodealNativeAdView, value: String?) {
+ value?.let { view.placement = it }
+ }
+}
diff --git a/android/src/newarch/com/appodeal/rnappodeal/RNAppodealNativeAssetViewManager.kt b/android/src/newarch/com/appodeal/rnappodeal/RNAppodealNativeAssetViewManager.kt
new file mode 100644
index 0000000..1d302c7
--- /dev/null
+++ b/android/src/newarch/com/appodeal/rnappodeal/RNAppodealNativeAssetViewManager.kt
@@ -0,0 +1,29 @@
+package com.appodeal.rnappodeal
+
+import com.facebook.react.module.annotations.ReactModule
+import com.facebook.react.uimanager.SimpleViewManager
+import com.facebook.react.uimanager.ThemedReactContext
+import com.facebook.react.uimanager.annotations.ReactProp
+
+@ReactModule(name = RNAppodealNativeAssetViewManagerImpl.NAME)
+class RNAppodealNativeAssetViewManager :
+ SimpleViewManager() {
+
+ private val impl = RNAppodealNativeAssetViewManagerImpl()
+
+ override fun getName(): String = RNAppodealNativeAssetViewManagerImpl.NAME
+
+ override fun createViewInstance(reactContext: ThemedReactContext): RCTAppodealNativeAssetView {
+ return impl.createViewInstance(reactContext)
+ }
+
+ override fun onDropViewInstance(view: RCTAppodealNativeAssetView) {
+ super.onDropViewInstance(view)
+ impl.onDropViewInstance(view)
+ }
+
+ @ReactProp(name = "asset")
+ fun setAsset(view: RCTAppodealNativeAssetView, value: String?) {
+ value?.let { view.assetType = it }
+ }
+}
diff --git a/android/src/newarch/com/appodeal/rnappodeal/RNAppodealNativeViewManager.kt b/android/src/newarch/com/appodeal/rnappodeal/RNAppodealNativeViewManager.kt
new file mode 100644
index 0000000..983be89
--- /dev/null
+++ b/android/src/newarch/com/appodeal/rnappodeal/RNAppodealNativeViewManager.kt
@@ -0,0 +1,43 @@
+package com.appodeal.rnappodeal
+
+import com.facebook.react.module.annotations.ReactModule
+import com.facebook.react.uimanager.SimpleViewManager
+import com.facebook.react.uimanager.ThemedReactContext
+import com.facebook.react.uimanager.annotations.ReactProp
+
+@ReactModule(name = RNAppodealNativeViewManagerImpl.NAME)
+class RNAppodealNativeViewManager :
+ SimpleViewManager() {
+
+ private val nativeViewManagerImpl = RNAppodealNativeViewManagerImpl()
+
+ override fun getName(): String = RNAppodealNativeViewManagerImpl.NAME
+
+ override fun createViewInstance(reactContext: ThemedReactContext): RCTAppodealNativeView {
+ return nativeViewManagerImpl.createViewInstance(reactContext)
+ }
+
+ override fun onDropViewInstance(view: RCTAppodealNativeView) {
+ super.onDropViewInstance(view)
+ nativeViewManagerImpl.onDropViewInstance(view)
+ }
+
+ override fun getExportedCustomDirectEventTypeConstants(): Map? {
+ return nativeViewManagerImpl.getExportedCustomDirectEventTypeConstants()
+ }
+
+ @ReactProp(name = "adId")
+ fun setAdId(view: RCTAppodealNativeView, value: String?) {
+ view.adId = value
+ }
+
+ @ReactProp(name = "placement")
+ fun setPlacement(view: RCTAppodealNativeView, value: String?) {
+ value?.let { view.placement = it }
+ }
+
+ @ReactProp(name = "adTemplate")
+ fun setAdTemplate(view: RCTAppodealNativeView, value: String?) {
+ value?.let { view.adTemplate = it }
+ }
+}
diff --git a/android/src/oldarch/com/appodeal/rnappodeal/RNAppodealModule.java b/android/src/oldarch/com/appodeal/rnappodeal/RNAppodealModule.java
index d0a9f6b..cca18db 100644
--- a/android/src/oldarch/com/appodeal/rnappodeal/RNAppodealModule.java
+++ b/android/src/oldarch/com/appodeal/rnappodeal/RNAppodealModule.java
@@ -239,6 +239,31 @@ public void trackEvent(String name, ReadableMap parameters) {
moduleImplementation.trackEvent(name, parameters);
}
+ @ReactMethod(isBlockingSynchronousMethod = true)
+ public WritableMap getNativeAds(double count) {
+ return moduleImplementation.getNativeAds(count);
+ }
+
+ @ReactMethod(isBlockingSynchronousMethod = true)
+ public double getAvailableNativeAdsCount() {
+ return moduleImplementation.getAvailableNativeAdsCount();
+ }
+
+ @ReactMethod
+ public void destroyNativeAd(String adId) {
+ moduleImplementation.destroyNativeAd(adId);
+ }
+
+ @ReactMethod
+ public void cacheNativeAds(double count) {
+ moduleImplementation.cacheNativeAds(count);
+ }
+
+ @ReactMethod
+ public void setPreferredNativeContentType(String type) {
+ moduleImplementation.setPreferredNativeContentType(type);
+ }
+
@ReactMethod
public void setBidonEndpoint(String endpoint) {
moduleImplementation.setBidonEndpoint(endpoint);
diff --git a/android/src/oldarch/com/appodeal/rnappodeal/RNAppodealNativeAdViewManager.java b/android/src/oldarch/com/appodeal/rnappodeal/RNAppodealNativeAdViewManager.java
new file mode 100644
index 0000000..416cfc4
--- /dev/null
+++ b/android/src/oldarch/com/appodeal/rnappodeal/RNAppodealNativeAdViewManager.java
@@ -0,0 +1,51 @@
+package com.appodeal.rnappodeal;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.facebook.react.uimanager.ThemedReactContext;
+import com.facebook.react.uimanager.ViewGroupManager;
+import com.facebook.react.uimanager.annotations.ReactProp;
+
+import java.util.Map;
+
+public class RNAppodealNativeAdViewManager extends ViewGroupManager {
+
+ private final RNAppodealNativeAdViewManagerImpl impl = new RNAppodealNativeAdViewManagerImpl();
+
+ @NonNull
+ @Override
+ public String getName() {
+ return RNAppodealNativeAdViewManagerImpl.NAME;
+ }
+
+ @NonNull
+ @Override
+ protected RCTAppodealNativeAdView createViewInstance(@NonNull ThemedReactContext reactContext) {
+ return impl.createViewInstance(reactContext);
+ }
+
+ @Override
+ public void onDropViewInstance(@NonNull RCTAppodealNativeAdView view) {
+ super.onDropViewInstance(view);
+ impl.onDropViewInstance(view);
+ }
+
+ @Nullable
+ @Override
+ public Map getExportedCustomDirectEventTypeConstants() {
+ return impl.getExportedCustomDirectEventTypeConstants();
+ }
+
+ @ReactProp(name = "adId")
+ public void setAdId(RCTAppodealNativeAdView view, @Nullable String value) {
+ view.setAdId(value);
+ }
+
+ @ReactProp(name = "placement")
+ public void setPlacement(RCTAppodealNativeAdView view, @Nullable String value) {
+ if (value != null) {
+ view.setPlacement(value);
+ }
+ }
+}
diff --git a/android/src/oldarch/com/appodeal/rnappodeal/RNAppodealNativeAssetViewManager.java b/android/src/oldarch/com/appodeal/rnappodeal/RNAppodealNativeAssetViewManager.java
new file mode 100644
index 0000000..ab9a594
--- /dev/null
+++ b/android/src/oldarch/com/appodeal/rnappodeal/RNAppodealNativeAssetViewManager.java
@@ -0,0 +1,38 @@
+package com.appodeal.rnappodeal;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.facebook.react.uimanager.SimpleViewManager;
+import com.facebook.react.uimanager.ThemedReactContext;
+import com.facebook.react.uimanager.annotations.ReactProp;
+
+public class RNAppodealNativeAssetViewManager extends SimpleViewManager {
+
+ private final RNAppodealNativeAssetViewManagerImpl impl = new RNAppodealNativeAssetViewManagerImpl();
+
+ @NonNull
+ @Override
+ public String getName() {
+ return RNAppodealNativeAssetViewManagerImpl.NAME;
+ }
+
+ @NonNull
+ @Override
+ protected RCTAppodealNativeAssetView createViewInstance(@NonNull ThemedReactContext reactContext) {
+ return impl.createViewInstance(reactContext);
+ }
+
+ @Override
+ public void onDropViewInstance(@NonNull RCTAppodealNativeAssetView view) {
+ super.onDropViewInstance(view);
+ impl.onDropViewInstance(view);
+ }
+
+ @ReactProp(name = "asset")
+ public void setAsset(RCTAppodealNativeAssetView view, @Nullable String value) {
+ if (value != null) {
+ view.setAssetType(value);
+ }
+ }
+}
diff --git a/android/src/oldarch/com/appodeal/rnappodeal/RNAppodealNativeViewManager.java b/android/src/oldarch/com/appodeal/rnappodeal/RNAppodealNativeViewManager.java
new file mode 100644
index 0000000..a13ac32
--- /dev/null
+++ b/android/src/oldarch/com/appodeal/rnappodeal/RNAppodealNativeViewManager.java
@@ -0,0 +1,62 @@
+package com.appodeal.rnappodeal;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.facebook.react.uimanager.SimpleViewManager;
+import com.facebook.react.uimanager.ThemedReactContext;
+import com.facebook.react.uimanager.annotations.ReactProp;
+
+import java.util.Map;
+
+public class RNAppodealNativeViewManager extends SimpleViewManager {
+
+ private final RNAppodealNativeViewManagerImpl nativeViewManagerImpl;
+
+ public RNAppodealNativeViewManager() {
+ this.nativeViewManagerImpl = new RNAppodealNativeViewManagerImpl();
+ }
+
+ @NonNull
+ @Override
+ public String getName() {
+ return RNAppodealNativeViewManagerImpl.NAME;
+ }
+
+ @NonNull
+ @Override
+ protected RCTAppodealNativeView createViewInstance(@NonNull ThemedReactContext reactContext) {
+ return nativeViewManagerImpl.createViewInstance(reactContext);
+ }
+
+ @Override
+ public void onDropViewInstance(@NonNull RCTAppodealNativeView view) {
+ super.onDropViewInstance(view);
+ nativeViewManagerImpl.onDropViewInstance(view);
+ }
+
+ @Nullable
+ @Override
+ public Map getExportedCustomDirectEventTypeConstants() {
+ return nativeViewManagerImpl.getExportedCustomDirectEventTypeConstants();
+ }
+
+ @ReactProp(name = "adId")
+ public void setAdId(RCTAppodealNativeView view, @Nullable String value) {
+ view.setAdId(value);
+ }
+
+ @ReactProp(name = "placement")
+ public void setPlacement(RCTAppodealNativeView view, @Nullable String value) {
+ if (value != null) {
+ view.setPlacement(value);
+ }
+ }
+
+ @ReactProp(name = "adTemplate")
+ public void setAdTemplate(RCTAppodealNativeView view, @Nullable String value) {
+ if (value != null) {
+ view.setAdTemplate(value);
+ }
+ }
+}
diff --git a/ios/RNADefines.h b/ios/RNADefines.h
index d8da48d..b992c37 100644
--- a/ios/RNADefines.h
+++ b/ios/RNADefines.h
@@ -37,6 +37,16 @@ FOUNDATION_EXPORT NSString *const kEventInterstitialShown;
FOUNDATION_EXPORT NSString *const kEventInterstitialClosed;
FOUNDATION_EXPORT NSString *const kEventInterstitialClicked;
+/**
+ * Native ad events
+ */
+FOUNDATION_EXPORT NSString *const kEventNativeLoaded;
+FOUNDATION_EXPORT NSString *const kEventNativeFailedToLoad;
+FOUNDATION_EXPORT NSString *const kEventNativeShown;
+FOUNDATION_EXPORT NSString *const kEventNativeShowFailed;
+FOUNDATION_EXPORT NSString *const kEventNativeClicked;
+FOUNDATION_EXPORT NSString *const kEventNativeExpired;
+
/**
* Rewarded video ad events
*/
diff --git a/ios/RNADefines.m b/ios/RNADefines.m
index 7e0e180..6640494 100644
--- a/ios/RNADefines.m
+++ b/ios/RNADefines.m
@@ -27,6 +27,14 @@
NSString *const kEventInterstitialClosed = @"onInterstitialClosed";
NSString *const kEventInterstitialClicked = @"onInterstitialClicked";
+// Native ad events
+NSString *const kEventNativeLoaded = @"onNativeLoaded";
+NSString *const kEventNativeFailedToLoad = @"onNativeFailedToLoad";
+NSString *const kEventNativeShown = @"onNativeShown";
+NSString *const kEventNativeShowFailed = @"onNativeShowFailed";
+NSString *const kEventNativeClicked = @"onNativeClicked";
+NSString *const kEventNativeExpired = @"onNativeExpired";
+
// Rewarded video ad events
NSString *const kEventRewardedVideoLoaded = @"onRewardedVideoLoaded";
NSString *const kEventRewardedVideoFailedToLoad = @"onRewardedVideoFailedToLoad";
@@ -101,6 +109,14 @@ + (RNAAdType)RNAAdType:(id)json RCT_DYNAMIC {
kEventInterstitialExpired,
kEventInterstitialClosed,
kEventInterstitialClicked,
+
+ // Native events
+ kEventNativeLoaded,
+ kEventNativeFailedToLoad,
+ kEventNativeShown,
+ kEventNativeShowFailed,
+ kEventNativeClicked,
+ kEventNativeExpired,
// Rewarded video events
kEventRewardedVideoLoaded,
diff --git a/ios/RNAppodeal.mm b/ios/RNAppodeal.mm
index 9281296..9b95338 100644
--- a/ios/RNAppodeal.mm
+++ b/ios/RNAppodeal.mm
@@ -2,6 +2,7 @@
#import "RNADefines.h"
#import "RNAEventDispatcher.h"
#import "RNAppodealBannerView.h"
+#import "RNAppodealNativeAdStore.h"
#import
#import
@@ -58,6 +59,10 @@ - (void)initializeWithAppKey:(NSString *)appKey
[Appodeal initializeWithApiKey:appKey
types:AppodealAdTypeFromRNAAdType(adTypes)];
+
+ if (((NSInteger)adTypes & RNAAdTypeNative) > 0) {
+ [[RNAppodealNativeAdStore shared] ensureQueue];
+ }
}
#ifndef RCT_NEW_ARCH_ENABLED
@@ -86,6 +91,9 @@ - (void)initializeWithAppKey:(NSString *)appKey
if ((NSInteger)showType == RNAAdTypeMREC) {
return @([RNAppodealMrecView isActiveMrecReady]);
}
+ if ((NSInteger)showType == RNAAdTypeNative) {
+ return @([[RNAppodealNativeAdStore shared] availableCount] > 0);
+ }
BOOL isLoaded = [Appodeal isReadyForShowWithStyle:AppodealShowStyleFromRNAAdType(showType)];
return @(isLoaded);
}
@@ -119,6 +127,28 @@ - (void)initializeWithAppKey:(NSString *)appKey
return @(canShow);
}
+#pragma mark - Native Ads
+
+RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(getNativeAds:(double)count) {
+ return [[RNAppodealNativeAdStore shared] getNativeAds:(NSInteger)count];
+}
+
+RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(getAvailableNativeAdsCount) {
+ return @([[RNAppodealNativeAdStore shared] availableCount]);
+}
+
+RCT_EXPORT_METHOD(destroyNativeAd:(NSString *)adId) {
+ [[RNAppodealNativeAdStore shared] destroyAdId:adId];
+}
+
+RCT_EXPORT_METHOD(cacheNativeAds:(double)count) {
+ [[RNAppodealNativeAdStore shared] cacheNativeAds:(NSInteger)count];
+}
+
+RCT_EXPORT_METHOD(setPreferredNativeContentType:(NSString *)type) {
+ [[RNAppodealNativeAdStore shared] setPreferredType:type];
+}
+
#pragma mark - Banner Settings
RCT_EXPORT_METHOD(setTabletBanners:(BOOL)value) {
@@ -440,6 +470,9 @@ - (NSNumber *)isLoaded:(double)showType {
if ((NSInteger)showType == RNAAdTypeMREC) {
return @([RNAppodealMrecView isActiveMrecReady]);
}
+ if ((NSInteger)showType == RNAAdTypeNative) {
+ return @([[RNAppodealNativeAdStore shared] availableCount] > 0);
+ }
BOOL isLoaded = [Appodeal isReadyForShowWithStyle:AppodealShowStyleFromRNAAdType(showType)];
return @(isLoaded);
}
@@ -472,6 +505,28 @@ - (NSNumber *)isPrecache:(double)adTypes {
return @([Appodeal isPrecacheAd:AppodealAdTypeFromRNAAdType(adTypes)]);
}
+#pragma mark - Native Ads
+
+- (NSArray *)getNativeAds:(double)count {
+ return [[RNAppodealNativeAdStore shared] getNativeAds:(NSInteger)count];
+}
+
+- (NSNumber *)getAvailableNativeAdsCount {
+ return @([[RNAppodealNativeAdStore shared] availableCount]);
+}
+
+- (void)destroyNativeAd:(NSString *)adId {
+ [[RNAppodealNativeAdStore shared] destroyAdId:adId];
+}
+
+- (void)cacheNativeAds:(double)count {
+ [[RNAppodealNativeAdStore shared] cacheNativeAds:(NSInteger)count];
+}
+
+- (void)setPreferredNativeContentType:(NSString *)type {
+ [[RNAppodealNativeAdStore shared] setPreferredType:type];
+}
+
#pragma mark - Banner Settings
- (void)setTabletBanners:(BOOL)value {
diff --git a/ios/RNAppodealNativeAdStore.h b/ios/RNAppodealNativeAdStore.h
new file mode 100644
index 0000000..7d9eab5
--- /dev/null
+++ b/ios/RNAppodealNativeAdStore.h
@@ -0,0 +1,22 @@
+#import
+#import
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface RNAppodealNativeAdStore : NSObject
+
++ (instancetype)shared;
+
+- (void)ensureQueue;
+- (NSArray *)getNativeAds:(NSInteger)count;
+- (NSInteger)availableCount;
+- (nullable APDNativeAd *)nativeAdForId:(NSString *)adId;
+- (void)destroyAdId:(NSString *)adId;
+- (void)setPreferredType:(NSString *)type;
+- (void)setAdViewTemplateClass:(Class)templateClass;
+- (void)cacheNativeAds:(NSInteger)count;
+- (void)emitNativeEvent:(NSString *)eventName payload:(nullable NSDictionary *)payload;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/ios/RNAppodealNativeAdStore.m b/ios/RNAppodealNativeAdStore.m
new file mode 100644
index 0000000..1ef6532
--- /dev/null
+++ b/ios/RNAppodealNativeAdStore.m
@@ -0,0 +1,174 @@
+#import "RNAppodealNativeAdStore.h"
+#import "RNADefines.h"
+#import "RNAEventDispatcher.h"
+
+@interface RNAppodealNativeAdStore ()
+
+@property (nonatomic, strong, nullable) APDNativeAdQueue *queue;
+@property (nonatomic, strong) NSMutableDictionary *adsById;
+@property (nonatomic, copy) NSString *preferredContentType;
+
+@end
+
+@implementation RNAppodealNativeAdStore
+
++ (instancetype)shared {
+ static RNAppodealNativeAdStore *sharedInstance = nil;
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ sharedInstance = [[self alloc] init];
+ });
+ return sharedInstance;
+}
+
+- (instancetype)init {
+ self = [super init];
+ if (self) {
+ _adsById = [NSMutableDictionary dictionary];
+ _preferredContentType = @"auto";
+ }
+ return self;
+}
+
+#pragma mark - Queue lifecycle
+
+- (void)ensureQueue {
+ if (self.queue != nil) {
+ return;
+ }
+
+ APDNativeAdQueue *queue = [[APDNativeAdQueue alloc] init];
+ // Appodeal iOS docs: APDNativeAdSettings.default()
+ queue.settings = [APDNativeAdSettings default];
+ queue.settings.adViewClass = APDDefaultNativeAdView.class;
+ queue.settings.type = [self nativeAdTypeFromPreferredContentType:self.preferredContentType];
+ queue.delegate = self;
+
+ self.queue = queue;
+ [self.queue loadAd];
+}
+
+- (APDNativeAdType)nativeAdTypeFromPreferredContentType:(NSString *)type {
+ if ([type isEqualToString:@"noVideo"]) {
+ return APDNativeAdTypeNoVideo;
+ }
+ if ([type isEqualToString:@"video"]) {
+ return APDNativeAdTypeVideo;
+ }
+ return APDNativeAdTypeAuto;
+}
+
+- (void)setPreferredType:(NSString *)type {
+ self.preferredContentType = type ?: @"auto";
+
+ if (self.queue != nil) {
+ self.queue.settings.type = [self nativeAdTypeFromPreferredContentType:self.preferredContentType];
+ }
+}
+
+- (void)setAdViewTemplateClass:(Class)templateClass {
+ [self ensureQueue];
+
+ if (templateClass != Nil) {
+ self.queue.settings.adViewClass = templateClass;
+ } else {
+ self.queue.settings.adViewClass = APDDefaultNativeAdView.class;
+ }
+}
+
+- (void)cacheNativeAds:(NSInteger)count {
+ [self ensureQueue];
+
+ if ([self.queue respondsToSelector:@selector(setMaxAdSize:)]) {
+ [self.queue setMaxAdSize:count];
+ }
+
+ [self.queue loadAd];
+}
+
+#pragma mark - Ad access
+
+- (NSInteger)availableCount {
+ if (self.queue == nil) {
+ return 0;
+ }
+
+ // APDNativeAdQueue exposes currentAdCount; Appodeal also has availableNativeAdsCount.
+ if ([self.queue respondsToSelector:@selector(currentAdCount)]) {
+ return self.queue.currentAdCount;
+ }
+
+ return (NSInteger)[Appodeal availableNativeAdsCount];
+}
+
+- (NSArray *)getNativeAds:(NSInteger)count {
+ [self ensureQueue];
+
+ if (count <= 0) {
+ return @[];
+ }
+
+ NSArray *nativeAds = [self.queue getNativeAdsOfCount:count];
+ NSMutableArray *result = [NSMutableArray arrayWithCapacity:nativeAds.count];
+
+ for (APDNativeAd *nativeAd in nativeAds) {
+ NSString *adId = [[NSUUID UUID] UUIDString];
+ self.adsById[adId] = nativeAd;
+
+ NSMutableDictionary *payload = [NSMutableDictionary dictionary];
+ payload[@"id"] = adId;
+ payload[@"title"] = nativeAd.title ?: @"";
+ payload[@"description"] = nativeAd.descriptionText ?: @"";
+ payload[@"callToAction"] = nativeAd.callToActionText ?: @"";
+ payload[@"rating"] = nativeAd.starRating ?: @0;
+ payload[@"containsVideo"] = @(nativeAd.containsVideo);
+ payload[@"predictedEcpm"] = @([Appodeal predictedEcpmForAdType:AppodealAdTypeNativeAd]);
+
+ [result addObject:payload];
+ }
+
+ return result;
+}
+
+- (APDNativeAd *)nativeAdForId:(NSString *)adId {
+ if (adId.length == 0) {
+ return nil;
+ }
+ return self.adsById[adId];
+}
+
+- (void)destroyAdId:(NSString *)adId {
+ if (adId.length == 0) {
+ return;
+ }
+
+ APDNativeAd *nativeAd = self.adsById[adId];
+ if (nativeAd != nil) {
+ [nativeAd detachFromView];
+ nativeAd.delegate = nil;
+ }
+
+ [self.adsById removeObjectForKey:adId];
+}
+
+#pragma mark - Events
+
+- (void)emitNativeEvent:(NSString *)eventName payload:(nullable NSDictionary *)payload {
+ [[RNAEventDispatcher sharedDispatcher] dispatchEvent:eventName payload:payload];
+}
+
+#pragma mark - APDNativeAdQueueDelegate
+
+- (void)adQueueAdIsAvailable:(APDNativeAdQueue *)adQueue ofCount:(NSInteger)count {
+ [self emitNativeEvent:kEventNativeLoaded payload:@{@"count": @(count)}];
+}
+
+- (void)adQueue:(APDNativeAdQueue *)adQueue failedWithError:(NSError *)error {
+ NSMutableDictionary *payload = [NSMutableDictionary dictionary];
+ if (error.localizedDescription.length > 0) {
+ payload[@"message"] = error.localizedDescription;
+ }
+ [self emitNativeEvent:kEventNativeFailedToLoad payload:payload.count > 0 ? payload : nil];
+}
+
+@end
diff --git a/ios/RNAppodealNativeAdView.h b/ios/RNAppodealNativeAdView.h
new file mode 100644
index 0000000..087a9fc
--- /dev/null
+++ b/ios/RNAppodealNativeAdView.h
@@ -0,0 +1,24 @@
+#import
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * Composable native ad root (AdMob-style).
+ * Asset children are RNAppodealNativeAssetView instances.
+ */
+@interface RNAppodealNativeAdView : RCTView
+
+@property (nonatomic, copy, nullable) NSString *adId;
+@property (nonatomic, copy) NSString *placement;
+
+@property (nonatomic, copy) RCTBubblingEventBlock onAdLoaded;
+@property (nonatomic, copy) RCTBubblingEventBlock onAdFailedToLoad;
+@property (nonatomic, copy) RCTBubblingEventBlock onAdClicked;
+@property (nonatomic, copy) RCTBubblingEventBlock onAdExpired;
+@property (nonatomic, copy) RCTBubblingEventBlock onAdShown;
+
+- (void)onAssetChanged;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/ios/RNAppodealNativeAdView.m b/ios/RNAppodealNativeAdView.m
new file mode 100644
index 0000000..334268c
--- /dev/null
+++ b/ios/RNAppodealNativeAdView.m
@@ -0,0 +1,237 @@
+#import "RNAppodealNativeAdView.h"
+#import "RNAppodealNativeAssetView.h"
+#import "RNAppodealNativeAdStore.h"
+#import "RNADefines.h"
+
+#import
+#import
+#import
+
+@interface RNAppodealNativeAdView ()
+@property (nonatomic, strong, nullable) APDNativeAd *nativeAd;
+@property (nonatomic, strong, nullable) UIView *mediaView;
+@property (nonatomic, copy, nullable) NSString *boundAdId;
+@end
+
+@implementation RNAppodealNativeAdView
+
+- (instancetype)initWithFrame:(CGRect)frame {
+ if (self = [super initWithFrame:frame]) {
+ self.backgroundColor = UIColor.clearColor;
+ _placement = @"default";
+ self.clipsToBounds = NO;
+ }
+ return self;
+}
+
+- (void)setAdId:(NSString *)adId {
+ if ([_adId isEqualToString:adId]) {
+ return;
+ }
+ _adId = [adId copy];
+ self.boundAdId = nil;
+ [self bindNativeAd];
+}
+
+- (void)setPlacement:(NSString *)placement {
+ _placement = placement.length > 0 ? [placement copy] : @"default";
+ if (self.boundAdId != nil) {
+ self.boundAdId = nil;
+ [self bindNativeAd];
+ }
+}
+
+- (void)onAssetChanged {
+ self.boundAdId = nil;
+ [self bindNativeAd];
+}
+
+- (void)didMoveToWindow {
+ [super didMoveToWindow];
+ if (self.window != nil && self.boundAdId == nil && self.adId.length > 0) {
+ [self bindNativeAd];
+ }
+}
+
+- (void)layoutSubviews {
+ [super layoutSubviews];
+ self.mediaView.frame = self.mediaAsset.mediaContainer.bounds;
+ if (self.boundAdId == nil && self.adId.length > 0 && CGRectGetWidth(self.bounds) > 0) {
+ [self bindNativeAd];
+ }
+}
+
+- (void)clearBinding {
+ if (self.nativeAd != nil) {
+ [self.nativeAd detachFromView];
+ self.nativeAd.delegate = nil;
+ self.nativeAd = nil;
+ }
+ [self.mediaView removeFromSuperview];
+ self.mediaView = nil;
+ self.boundAdId = nil;
+}
+
+- (void)bindNativeAd {
+ if (self.adId.length == 0) {
+ return;
+ }
+ if ([self.boundAdId isEqualToString:self.adId]) {
+ return;
+ }
+ if (self.window == nil || CGRectGetWidth(self.bounds) <= 0 || CGRectGetHeight(self.bounds) <= 0) {
+ return;
+ }
+
+ [self clearBinding];
+
+ APDNativeAd *nativeAd = [[RNAppodealNativeAdStore shared] nativeAdForId:self.adId];
+ if (nativeAd == nil) {
+ if (self.onAdFailedToLoad) {
+ self.onAdFailedToLoad(@{@"adId": self.adId ?: @"", @"message": @"native ad not in store"});
+ }
+ return;
+ }
+
+ RNAppodealNativeAssetView *titleAsset = [self assetWithType:@"title"];
+ RNAppodealNativeAssetView *descriptionAsset = [self assetWithType:@"description"];
+ RNAppodealNativeAssetView *ctaAsset = [self assetWithType:@"callToAction"];
+ RNAppodealNativeAssetView *attributionAsset = [self assetWithType:@"attribution"];
+ RNAppodealNativeAssetView *iconAsset = [self assetWithType:@"icon"];
+ RNAppodealNativeAssetView *mediaAsset = [self assetWithType:@"media"];
+
+ if (mediaAsset == nil && iconAsset == nil) {
+ if (self.onAdFailedToLoad) {
+ self.onAdFailedToLoad(@{
+ @"adId": self.adId ?: @"",
+ @"message": @"composable native ad requires media or icon asset"
+ });
+ }
+ return;
+ }
+
+ // Fill text/image assets from the ad object (iOS custom path fills via
+ // APDNativeAdView outlets; here RN owns the views so we copy fields).
+ titleAsset.textLabel.text = nativeAd.title ?: @"";
+ descriptionAsset.textLabel.text = nativeAd.descriptionText ?: @"";
+ ctaAsset.textLabel.text = nativeAd.callToActionText ?: @"";
+ if (attributionAsset.textLabel.text.length == 0) {
+ attributionAsset.textLabel.text = @"Ad";
+ }
+
+ if (iconAsset.iconImageView != nil && nativeAd.iconImage != nil) {
+ if ([nativeAd.iconImage respondsToSelector:@selector(image)]) {
+ iconAsset.iconImageView.image = [nativeAd.iconImage performSelector:@selector(image)];
+ }
+ }
+
+ UIViewController *rootViewController = RCTPresentedViewController();
+ if (rootViewController == nil) {
+ if (self.onAdFailedToLoad) {
+ self.onAdFailedToLoad(@{@"adId": self.adId ?: @"", @"message": @"root view controller unavailable"});
+ }
+ return;
+ }
+
+ // Media: prefer APDMediaView when available (Appodeal iOS media container).
+ if (mediaAsset.mediaContainer != nil) {
+ Class mediaClass = NSClassFromString(@"APDMediaView");
+ if (mediaClass != Nil) {
+ UIView *media = [[mediaClass alloc] initWithFrame:mediaAsset.mediaContainer.bounds];
+ media.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
+ [mediaAsset.mediaContainer addSubview:media];
+ self.mediaView = media;
+ if ([media respondsToSelector:@selector(setNativeAd:rootViewController:)]) {
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
+ [media performSelector:@selector(setNativeAd:rootViewController:)
+ withObject:nativeAd
+ withObject:rootViewController];
+#pragma clang diagnostic pop
+ } else if ([media respondsToSelector:@selector(setNativeAd:)]) {
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
+ [media performSelector:@selector(setNativeAd:) withObject:nativeAd];
+#pragma clang diagnostic pop
+ }
+ } else if (nativeAd.mainImage != nil && [nativeAd.mainImage respondsToSelector:@selector(image)]) {
+ UIImageView *imageView = [[UIImageView alloc] initWithFrame:mediaAsset.mediaContainer.bounds];
+ imageView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
+ imageView.contentMode = UIViewContentModeScaleAspectFit;
+ imageView.image = [nativeAd.mainImage performSelector:@selector(image)];
+ [mediaAsset.mediaContainer addSubview:imageView];
+ self.mediaView = imageView;
+ }
+ }
+
+ self.nativeAd = nativeAd;
+ self.nativeAd.delegate = self;
+
+ // Register the RN-composed hierarchy for impressions / clicks.
+ // Pair of detachFromView used in RNAppodealNativeAdStore.
+ if ([nativeAd respondsToSelector:@selector(attachToView:withRootViewController:)]) {
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
+ [nativeAd performSelector:@selector(attachToView:withRootViewController:)
+ withObject:self
+ withObject:rootViewController];
+#pragma clang diagnostic pop
+ } else if ([nativeAd respondsToSelector:@selector(attachToView:)]) {
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
+ [nativeAd performSelector:@selector(attachToView:) withObject:self];
+#pragma clang diagnostic pop
+ }
+
+ self.boundAdId = self.adId;
+ if (self.onAdLoaded) {
+ self.onAdLoaded(@{@"adId": self.adId ?: @""});
+ }
+}
+
+- (RNAppodealNativeAssetView *)mediaAsset {
+ return [self assetWithType:@"media"];
+}
+
+- (RNAppodealNativeAssetView *)assetWithType:(NSString *)type {
+ return [self findAsset:type inView:self];
+}
+
+- (RNAppodealNativeAssetView *)findAsset:(NSString *)type inView:(UIView *)view {
+ if ([view isKindOfClass:[RNAppodealNativeAssetView class]]) {
+ RNAppodealNativeAssetView *assetView = (RNAppodealNativeAssetView *)view;
+ if ([assetView.asset isEqualToString:type]) {
+ return assetView;
+ }
+ }
+ for (UIView *child in view.subviews) {
+ RNAppodealNativeAssetView *found = [self findAsset:type inView:child];
+ if (found != nil) {
+ return found;
+ }
+ }
+ return nil;
+}
+
+- (void)removeFromSuperview {
+ [self clearBinding];
+ [super removeFromSuperview];
+}
+
+#pragma mark - APDNativeAdPresentationDelegate
+
+- (void)nativeAdWillLogImpression:(APDNativeAd *)nativeAd {
+ if (self.onAdShown) {
+ self.onAdShown(@{@"adId": self.adId ?: @""});
+ }
+ [[RNAppodealNativeAdStore shared] emitNativeEvent:kEventNativeShown payload:@{@"adId": self.adId ?: @""}];
+}
+
+- (void)nativeAdWillLogUserInteraction:(APDNativeAd *)nativeAd {
+ if (self.onAdClicked) {
+ self.onAdClicked(@{@"adId": self.adId ?: @""});
+ }
+ [[RNAppodealNativeAdStore shared] emitNativeEvent:kEventNativeClicked payload:@{@"adId": self.adId ?: @""}];
+}
+
+@end
diff --git a/ios/RNAppodealNativeAdViewManager.mm b/ios/RNAppodealNativeAdViewManager.mm
new file mode 100644
index 0000000..db64ff0
--- /dev/null
+++ b/ios/RNAppodealNativeAdViewManager.mm
@@ -0,0 +1,23 @@
+#import
+#import "RNAppodealNativeAdView.h"
+
+@interface RNAppodealNativeAdViewManager : RCTViewManager
+@end
+
+@implementation RNAppodealNativeAdViewManager
+
+RCT_EXPORT_MODULE(RNAppodealNativeAdView)
+
+- (UIView *)view {
+ return [[RNAppodealNativeAdView alloc] initWithFrame:CGRectZero];
+}
+
+RCT_EXPORT_VIEW_PROPERTY(adId, NSString)
+RCT_EXPORT_VIEW_PROPERTY(placement, NSString)
+RCT_EXPORT_VIEW_PROPERTY(onAdLoaded, RCTBubblingEventBlock)
+RCT_EXPORT_VIEW_PROPERTY(onAdFailedToLoad, RCTBubblingEventBlock)
+RCT_EXPORT_VIEW_PROPERTY(onAdClicked, RCTBubblingEventBlock)
+RCT_EXPORT_VIEW_PROPERTY(onAdExpired, RCTBubblingEventBlock)
+RCT_EXPORT_VIEW_PROPERTY(onAdShown, RCTBubblingEventBlock)
+
+@end
diff --git a/ios/RNAppodealNativeAssetView.h b/ios/RNAppodealNativeAssetView.h
new file mode 100644
index 0000000..4dd3dbf
--- /dev/null
+++ b/ios/RNAppodealNativeAssetView.h
@@ -0,0 +1,19 @@
+#import
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * Single native-ad asset marker.
+ * asset: media | icon | title | description | callToAction | attribution
+ */
+@interface RNAppodealNativeAssetView : RCTView
+
+@property (nonatomic, copy) NSString *asset;
+
+@property (nonatomic, strong, readonly) UILabel *textLabel;
+@property (nonatomic, strong, readonly) UIImageView *iconImageView;
+@property (nonatomic, strong, readonly) UIView *mediaContainer;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/ios/RNAppodealNativeAssetView.m b/ios/RNAppodealNativeAssetView.m
new file mode 100644
index 0000000..66d3a04
--- /dev/null
+++ b/ios/RNAppodealNativeAssetView.m
@@ -0,0 +1,83 @@
+#import "RNAppodealNativeAssetView.h"
+
+@interface RNAppodealNativeAssetView ()
+@property (nonatomic, strong, readwrite) UILabel *textLabel;
+@property (nonatomic, strong, readwrite) UIImageView *iconImageView;
+@property (nonatomic, strong, readwrite) UIView *mediaContainer;
+@end
+
+@implementation RNAppodealNativeAssetView
+
+- (instancetype)initWithFrame:(CGRect)frame {
+ if (self = [super initWithFrame:frame]) {
+ _asset = @"title";
+ self.clipsToBounds = NO;
+ [self rebuildContent];
+ }
+ return self;
+}
+
+- (void)setAsset:(NSString *)asset {
+ NSString *normalized = asset.length > 0 ? asset : @"title";
+ if ([_asset isEqualToString:normalized]) {
+ return;
+ }
+ _asset = [normalized copy];
+ [self rebuildContent];
+
+ UIView *parent = self.superview;
+ while (parent != nil) {
+ if ([parent respondsToSelector:@selector(onAssetChanged)]) {
+ [parent performSelector:@selector(onAssetChanged)];
+ break;
+ }
+ parent = parent.superview;
+ }
+}
+
+- (void)rebuildContent {
+ [self.textLabel removeFromSuperview];
+ [self.iconImageView removeFromSuperview];
+ [self.mediaContainer removeFromSuperview];
+ self.textLabel = nil;
+ self.iconImageView = nil;
+ self.mediaContainer = nil;
+
+ if ([self.asset isEqualToString:@"media"]) {
+ UIView *container = [[UIView alloc] initWithFrame:self.bounds];
+ container.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
+ container.clipsToBounds = YES;
+ [self addSubview:container];
+ self.mediaContainer = container;
+ return;
+ }
+
+ if ([self.asset isEqualToString:@"icon"]) {
+ UIImageView *imageView = [[UIImageView alloc] initWithFrame:self.bounds];
+ imageView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
+ imageView.contentMode = UIViewContentModeScaleAspectFit;
+ imageView.clipsToBounds = YES;
+ [self addSubview:imageView];
+ self.iconImageView = imageView;
+ return;
+ }
+
+ UILabel *label = [[UILabel alloc] initWithFrame:self.bounds];
+ label.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
+ label.numberOfLines = [self.asset isEqualToString:@"title"] ? 2 : 1;
+ label.lineBreakMode = NSLineBreakByTruncatingTail;
+ if ([self.asset isEqualToString:@"callToAction"] || [self.asset isEqualToString:@"attribution"]) {
+ label.textAlignment = NSTextAlignmentCenter;
+ }
+ [self addSubview:label];
+ self.textLabel = label;
+}
+
+- (void)layoutSubviews {
+ [super layoutSubviews];
+ self.textLabel.frame = self.bounds;
+ self.iconImageView.frame = self.bounds;
+ self.mediaContainer.frame = self.bounds;
+}
+
+@end
diff --git a/ios/RNAppodealNativeAssetViewManager.mm b/ios/RNAppodealNativeAssetViewManager.mm
new file mode 100644
index 0000000..3882ed8
--- /dev/null
+++ b/ios/RNAppodealNativeAssetViewManager.mm
@@ -0,0 +1,17 @@
+#import
+#import "RNAppodealNativeAssetView.h"
+
+@interface RNAppodealNativeAssetViewManager : RCTViewManager
+@end
+
+@implementation RNAppodealNativeAssetViewManager
+
+RCT_EXPORT_MODULE(RNAppodealNativeAssetView)
+
+- (UIView *)view {
+ return [[RNAppodealNativeAssetView alloc] initWithFrame:CGRectZero];
+}
+
+RCT_EXPORT_VIEW_PROPERTY(asset, NSString)
+
+@end
diff --git a/ios/RNAppodealNativeView.h b/ios/RNAppodealNativeView.h
new file mode 100644
index 0000000..67ca964
--- /dev/null
+++ b/ios/RNAppodealNativeView.h
@@ -0,0 +1,19 @@
+#import
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface RNAppodealNativeView : RCTView
+
+@property (nonatomic, copy, nullable) NSString *adId;
+@property (nonatomic, copy) NSString *placement;
+@property (nonatomic, copy, nullable) NSString *templateName;
+
+@property (nonatomic, copy) RCTBubblingEventBlock onAdLoaded;
+@property (nonatomic, copy) RCTBubblingEventBlock onAdFailedToLoad;
+@property (nonatomic, copy) RCTBubblingEventBlock onAdClicked;
+@property (nonatomic, copy) RCTBubblingEventBlock onAdExpired;
+@property (nonatomic, copy) RCTBubblingEventBlock onAdShown;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/ios/RNAppodealNativeView.m b/ios/RNAppodealNativeView.m
new file mode 100644
index 0000000..ef128b2
--- /dev/null
+++ b/ios/RNAppodealNativeView.m
@@ -0,0 +1,157 @@
+#import "RNAppodealNativeView.h"
+#import "RNAppodealNativeAdStore.h"
+#import "RNADefines.h"
+
+#import
+#import
+#import
+
+@interface RNAppodealNativeView ()
+
+@property (nonatomic, strong, nullable) APDNativeAd *nativeAd;
+@property (nonatomic, strong, nullable) UIView *adContentView;
+
+@end
+
+@implementation RNAppodealNativeView
+
+- (instancetype)initWithFrame:(CGRect)frame {
+ if (self = [super initWithFrame:frame]) {
+ self.backgroundColor = UIColor.clearColor;
+ _placement = @"default";
+ }
+ return self;
+}
+
+- (void)setAdId:(NSString *)adId {
+ if ([_adId isEqualToString:adId]) {
+ return;
+ }
+
+ _adId = [adId copy];
+ [self reloadNativeAdView];
+}
+
+- (void)setPlacement:(NSString *)placement {
+ _placement = placement.length > 0 ? [placement copy] : @"default";
+
+ if (self.adContentView != nil) {
+ [self reloadNativeAdView];
+ }
+}
+
+- (void)setTemplateName:(NSString *)templateName {
+ _templateName = [templateName copy];
+
+ if (self.adContentView != nil) {
+ [self reloadNativeAdView];
+ }
+}
+
+- (void)reloadNativeAdView {
+ [self clearNativeAdView];
+
+ if (self.adId.length == 0) {
+ return;
+ }
+
+ NSAssert([Appodeal isInitializedForAdType:AppodealAdTypeNativeAd],
+ @"Appodeal should be initialised with AppodealAdTypeNativeAd before trying to add RNAppodealNativeView in hierarchy");
+
+ APDNativeAd *nativeAd = [[RNAppodealNativeAdStore shared] nativeAdForId:self.adId];
+ if (nativeAd == nil) {
+ if (self.onAdFailedToLoad) {
+ self.onAdFailedToLoad(@{});
+ }
+ [[RNAppodealNativeAdStore shared] emitNativeEvent:kEventNativeShowFailed payload:@{@"adId": self.adId}];
+ return;
+ }
+
+ [self applyTemplateIfNeeded];
+
+ self.nativeAd = nativeAd;
+ self.nativeAd.delegate = self;
+
+ UIViewController *rootViewController = RCTPresentedViewController();
+ NSError *error = nil;
+ UIView *adView = [nativeAd getViewForPlacement:self.placement
+ withRootViewController:rootViewController
+ error:&error];
+
+ if (adView == nil || error != nil) {
+ if (self.onAdFailedToLoad) {
+ self.onAdFailedToLoad(@{});
+ }
+
+ NSMutableDictionary *payload = [NSMutableDictionary dictionaryWithObject:self.adId forKey:@"adId"];
+ if (error.localizedDescription.length > 0) {
+ payload[@"message"] = error.localizedDescription;
+ }
+ [[RNAppodealNativeAdStore shared] emitNativeEvent:kEventNativeShowFailed payload:payload];
+ return;
+ }
+
+ self.adContentView = adView;
+ self.adContentView.frame = self.bounds;
+ self.adContentView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
+ [self addSubview:self.adContentView];
+
+ if (self.onAdLoaded) {
+ self.onAdLoaded(@{@"adId": self.adId});
+ }
+}
+
+- (void)applyTemplateIfNeeded {
+ [[RNAppodealNativeAdStore shared] ensureQueue];
+
+ if (self.templateName.length == 0 || [self.templateName isEqualToString:@"default"]) {
+ [[RNAppodealNativeAdStore shared] setAdViewTemplateClass:APDDefaultNativeAdView.class];
+ return;
+ }
+
+ Class templateClass = NSClassFromString(self.templateName);
+ [[RNAppodealNativeAdStore shared] setAdViewTemplateClass:templateClass != Nil ? templateClass : APDDefaultNativeAdView.class];
+}
+
+- (void)clearNativeAdView {
+ if (self.nativeAd != nil) {
+ [self.nativeAd detachFromView];
+ self.nativeAd.delegate = nil;
+ self.nativeAd = nil;
+ }
+
+ [self.adContentView removeFromSuperview];
+ self.adContentView = nil;
+}
+
+- (void)layoutSubviews {
+ [super layoutSubviews];
+ self.adContentView.frame = self.bounds;
+}
+
+- (void)removeFromSuperview {
+ [self clearNativeAdView];
+ [super removeFromSuperview];
+}
+
+- (void)insertReactSubview:(UIView *)subview atIndex:(NSInteger)atIndex {
+ RCTLogError(@"RNAppodealNativeView cannot have subviews");
+}
+
+#pragma mark - APDNativeAdPresentationDelegate
+
+- (void)nativeAdWillLogImpression:(APDNativeAd *)nativeAd {
+ if (self.onAdShown) {
+ self.onAdShown(@{@"adId": self.adId ?: @""});
+ }
+ [[RNAppodealNativeAdStore shared] emitNativeEvent:kEventNativeShown payload:@{@"adId": self.adId ?: @""}];
+}
+
+- (void)nativeAdWillLogUserInteraction:(APDNativeAd *)nativeAd {
+ if (self.onAdClicked) {
+ self.onAdClicked(@{@"adId": self.adId ?: @""});
+ }
+ [[RNAppodealNativeAdStore shared] emitNativeEvent:kEventNativeClicked payload:@{@"adId": self.adId ?: @""}];
+}
+
+@end
diff --git a/ios/RNAppodealNativeViewManager.h b/ios/RNAppodealNativeViewManager.h
new file mode 100644
index 0000000..3e490ff
--- /dev/null
+++ b/ios/RNAppodealNativeViewManager.h
@@ -0,0 +1,5 @@
+#import
+
+@interface RNAppodealNativeViewManager : RCTViewManager
+
+@end
diff --git a/ios/RNAppodealNativeViewManager.mm b/ios/RNAppodealNativeViewManager.mm
new file mode 100644
index 0000000..61c11b4
--- /dev/null
+++ b/ios/RNAppodealNativeViewManager.mm
@@ -0,0 +1,22 @@
+#import "RNAppodealNativeViewManager.h"
+#import "RNAppodealNativeView.h"
+
+@implementation RNAppodealNativeViewManager
+
+RCT_EXPORT_MODULE(RNAppodealNativeView);
+
+- (UIView *)view {
+ return [[RNAppodealNativeView alloc] initWithFrame:CGRectZero];
+}
+
+RCT_EXPORT_VIEW_PROPERTY(adId, NSString)
+RCT_EXPORT_VIEW_PROPERTY(placement, NSString)
+RCT_REMAP_VIEW_PROPERTY(adTemplate, templateName, NSString)
+
+RCT_EXPORT_VIEW_PROPERTY(onAdLoaded, RCTBubblingEventBlock)
+RCT_EXPORT_VIEW_PROPERTY(onAdFailedToLoad, RCTBubblingEventBlock)
+RCT_EXPORT_VIEW_PROPERTY(onAdClicked, RCTBubblingEventBlock)
+RCT_EXPORT_VIEW_PROPERTY(onAdExpired, RCTBubblingEventBlock)
+RCT_EXPORT_VIEW_PROPERTY(onAdShown, RCTBubblingEventBlock)
+
+@end
diff --git a/package.json b/package.json
index fd13116..79993ae 100644
--- a/package.json
+++ b/package.json
@@ -1,14 +1,16 @@
{
"name": "react-native-appodeal",
- "version": "4.2.0",
+ "version": "4.3.1",
"description": "React Native Module created to support Appodeal SDK for iOS and Android platforms",
- "main": "./lib/module/index.js",
- "types": "./lib/typescript/src/index.d.ts",
+ "main": "./src/index.tsx",
+ "react-native": "./src/index.tsx",
+ "types": "./src/index.tsx",
"exports": {
".": {
+ "react-native": "./src/index.tsx",
+ "types": "./src/index.tsx",
"source": "./src/index.tsx",
- "types": "./lib/typescript/src/index.d.ts",
- "default": "./lib/module/index.js"
+ "default": "./src/index.tsx"
},
"./package.json": "./package.json"
},
diff --git a/src/RNAppodeal.ts b/src/RNAppodeal.ts
index 9230f3e..85f908f 100644
--- a/src/RNAppodeal.ts
+++ b/src/RNAppodeal.ts
@@ -3,7 +3,7 @@
*
* This module provides a complete interface to the Appodeal SDK for React Native applications.
* It includes functionality for:
- * - Ad management (banner, interstitial, rewarded video, MREC)
+ * - Ad management (banner, interstitial, rewarded video, MREC, native)
* - Analytics and revenue tracking
* - In-app purchase validation
* - Consent management (GDPR compliance)
@@ -19,6 +19,8 @@ import type {
AppodealConsentStatus,
AppodealPrivacyOptionsStatus,
AppodealIOSPurchase,
+ AppodealNativeAdInfo,
+ AppodealNativeContentType,
AppodealReward,
Map,
} from './types';
@@ -93,6 +95,31 @@ export interface Appodeal {
* @param adTypes Ad types mask
*/
cache(adTypes: AppodealAdType): void;
+ /**
+ * Pull cached native ads from the SDK (removes them from the SDK cache).
+ * Returns metadata + opaque ids for use with AppodealNative.
+ * @param count Number of ads to pull (Android max 5)
+ */
+ getNativeAds(count?: number): AppodealNativeAdInfo[];
+ /**
+ * Number of native ads currently available in the SDK cache / queue
+ */
+ getAvailableNativeAdsCount(): number;
+ /**
+ * Destroy a native ad previously returned by getNativeAds
+ * @param adId Opaque ad id
+ */
+ destroyNativeAd(adId: string): void;
+ /**
+ * Manually cache native ads
+ * @param count Number of ads to request (clamped 1–5 on Android)
+ */
+ cacheNativeAds(count?: number): void;
+ /**
+ * Preferred native media content type
+ * @param type auto | noVideo | video
+ */
+ setPreferredNativeContentType(type: AppodealNativeContentType): void;
/**
* Enables or disables autocache for specific ad type
* @param adTypes Ad types mask
@@ -253,7 +280,7 @@ export interface Appodeal {
/**
* Plugin version constant
*/
-const PLUGIN_VERSION = '4.2.0';
+const PLUGIN_VERSION = '4.3.1';
/**
* Appodeal SDK implementation
@@ -306,6 +333,38 @@ const appodeal: Appodeal = {
NativeAppodeal.cache(adTypes);
},
+ getNativeAds: (count: number = 1): AppodealNativeAdInfo[] => {
+ // Android TurboModule Spec uses UnsafeObject → { ads: [...] }
+ const result = NativeAppodeal.getNativeAds(count) as unknown;
+ if (Array.isArray(result)) {
+ return result as AppodealNativeAdInfo[];
+ }
+ if (
+ result &&
+ typeof result === 'object' &&
+ Array.isArray((result as { ads?: unknown }).ads)
+ ) {
+ return (result as { ads: AppodealNativeAdInfo[] }).ads;
+ }
+ return [];
+ },
+
+ getAvailableNativeAdsCount: (): number => {
+ return NativeAppodeal.getAvailableNativeAdsCount();
+ },
+
+ destroyNativeAd: (adId: string): void => {
+ NativeAppodeal.destroyNativeAd(adId);
+ },
+
+ cacheNativeAds: (count: number = 2): void => {
+ NativeAppodeal.cacheNativeAds(count);
+ },
+
+ setPreferredNativeContentType: (type: AppodealNativeContentType): void => {
+ NativeAppodeal.setPreferredNativeContentType(type);
+ },
+
setAutoCache: (adTypes: AppodealAdType, value: boolean): void => {
NativeAppodeal.setAutoCache(adTypes, value);
},
diff --git a/src/RNAppodealEvents.ts b/src/RNAppodealEvents.ts
index 10b6087..17636f6 100644
--- a/src/RNAppodealEvents.ts
+++ b/src/RNAppodealEvents.ts
@@ -51,6 +51,24 @@ export namespace AppodealInterstitialEvents {
export const CLOSED = 'onInterstitialClosed';
}
+/**
+ * Native ad events
+ */
+export namespace AppodealNativeEvents {
+ /** Fired when native ads are loaded into the queue */
+ export const LOADED = 'onNativeLoaded';
+ /** Fired when native ads fail to load */
+ export const FAILED_TO_LOAD = 'onNativeFailedToLoad';
+ /** Fired when a native ad is shown */
+ export const SHOWN = 'onNativeShown';
+ /** Fired when a native ad fails to show */
+ export const SHOW_FAILED = 'onNativeShowFailed';
+ /** Fired when a native ad is clicked */
+ export const CLICKED = 'onNativeClicked';
+ /** Fired when a native ad expires */
+ export const EXPIRED = 'onNativeExpired';
+}
+
/**
* Rewarded video ad events
*/
diff --git a/src/RNAppodealNative.tsx b/src/RNAppodealNative.tsx
new file mode 100644
index 0000000..e0fd3ea
--- /dev/null
+++ b/src/RNAppodealNative.tsx
@@ -0,0 +1,27 @@
+/**
+ * Appodeal Native Ad Component — stock templates only.
+ *
+ * Templates: newsFeed / appWall / contentStream.
+ * For custom JSX layouts use AppodealNativeAdView + asset views.
+ */
+import AppodealNativeView, {
+ type NativeProps,
+} from './specs/AppodealNativeViewNativeComponent';
+
+const AppodealNative = ({
+ adTemplate = 'contentStream',
+ placement = 'default',
+ style,
+ ...rest
+}: NativeProps) => {
+ return (
+
+ );
+};
+
+export default AppodealNative;
diff --git a/src/RNAppodealNativeAdView.tsx b/src/RNAppodealNativeAdView.tsx
new file mode 100644
index 0000000..e6f763e
--- /dev/null
+++ b/src/RNAppodealNativeAdView.tsx
@@ -0,0 +1,66 @@
+/**
+ * Composable Appodeal native ad — AdMob-style layout in JSX.
+ *
+ * Stock templates: use .
+ * Custom layout: compose assets under AppodealNativeAdView (both platforms).
+ */
+import React from 'react';
+import type { StyleProp, TextStyle, ViewStyle } from 'react-native';
+import AppodealNativeAdViewNative, {
+ type NativeProps as AdViewProps,
+} from './specs/AppodealNativeAdViewNativeComponent';
+import AppodealNativeAssetViewNative from './specs/AppodealNativeAssetViewNativeComponent';
+
+export type AppodealNativeAssetType =
+ | 'media'
+ | 'icon'
+ | 'title'
+ | 'description'
+ | 'callToAction'
+ | 'attribution';
+
+export type AppodealNativeAdViewProps = AdViewProps;
+
+export type AppodealNativeAssetProps = {
+ asset: AppodealNativeAssetType;
+ style?: StyleProp;
+ children?: React.ReactNode;
+};
+
+export const AppodealNativeAdView = (props: AppodealNativeAdViewProps) => {
+ return ;
+};
+
+export const AppodealNativeAsset = ({
+ asset,
+ style,
+ ...rest
+}: AppodealNativeAssetProps) => {
+ return (
+
+ );
+};
+
+export const AppodealNativeMediaView = (
+ props: Omit
+) => ;
+
+export const AppodealNativeIconView = (
+ props: Omit
+) => ;
+
+export const AppodealNativeTitleView = (
+ props: Omit
+) => ;
+
+export const AppodealNativeDescriptionView = (
+ props: Omit
+) => ;
+
+export const AppodealNativeCallToActionView = (
+ props: Omit
+) => ;
+
+export const AppodealNativeAttributionView = (
+ props: Omit
+) => ;
diff --git a/src/__tests__/types.test.ts b/src/__tests__/types.test.ts
index f9e7c97..2e36e43 100644
--- a/src/__tests__/types.test.ts
+++ b/src/__tests__/types.test.ts
@@ -21,7 +21,12 @@ describe('Appodeal Type Definitions', () => {
expect(typeof AppodealAdType.INTERSTITIAL).toBe('number');
expect(typeof AppodealAdType.BANNER).toBe('number');
expect(typeof AppodealAdType.REWARDED_VIDEO).toBe('number');
+ expect(typeof AppodealAdType.NATIVE).toBe('number');
+ expect(AppodealAdType.NATIVE).toBe(1 << 7);
expect(typeof AppodealAdType.MREC).toBe('number');
+ expect(AppodealAdType.ALL & AppodealAdType.NATIVE).toBe(
+ AppodealAdType.NATIVE
+ );
});
it('should have correct AppodealLogLevel values', () => {
diff --git a/src/index.tsx b/src/index.tsx
index 1d23484..dcd885f 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -9,6 +9,22 @@ import Appodeal from './RNAppodeal';
// React Native components
export { default as AppodealBanner } from './RNAppodealBanner';
export { default as AppodealMrec } from './RNAppodealMrec';
+export { default as AppodealNative } from './RNAppodealNative';
+export {
+ AppodealNativeAdView,
+ AppodealNativeAsset,
+ AppodealNativeMediaView,
+ AppodealNativeIconView,
+ AppodealNativeTitleView,
+ AppodealNativeDescriptionView,
+ AppodealNativeCallToActionView,
+ AppodealNativeAttributionView,
+} from './RNAppodealNativeAdView';
+export type {
+ AppodealNativeAdViewProps,
+ AppodealNativeAssetProps,
+ AppodealNativeAssetType,
+} from './RNAppodealNativeAdView';
// Export the main Appodeal interface
export default Appodeal;
@@ -29,6 +45,9 @@ export type {
AppodealIOSPurchase,
AppodealAndroidPurchase,
AppodealAdRevenue,
+ AppodealNativeAdInfo,
+ AppodealNativeContentType,
+ AppodealNativeTemplate,
AppodealPurchaseValidationResult,
Map,
} from './types';
@@ -39,4 +58,5 @@ export {
AppodealBannerEvents,
AppodealInterstitialEvents,
AppodealRewardedEvents,
+ AppodealNativeEvents,
} from './RNAppodealEvents';
diff --git a/src/specs/AppodealNativeAdViewNativeComponent.ts b/src/specs/AppodealNativeAdViewNativeComponent.ts
new file mode 100644
index 0000000..1c88f76
--- /dev/null
+++ b/src/specs/AppodealNativeAdViewNativeComponent.ts
@@ -0,0 +1,26 @@
+import type { HostComponent, ViewProps } from 'react-native';
+import type { DirectEventHandler } from 'react-native/Libraries/Types/CodegenTypes';
+import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent';
+
+export type NativeAdInfoEvent = Readonly<{
+ adId?: string;
+}>;
+
+export type NativeAdLoadFailedEvent = Readonly<{
+ adId?: string;
+ message?: string;
+}>;
+
+export interface NativeProps extends ViewProps {
+ adId?: string;
+ placement?: string;
+ onAdLoaded?: DirectEventHandler;
+ onAdFailedToLoad?: DirectEventHandler;
+ onAdShown?: DirectEventHandler;
+ onAdClicked?: DirectEventHandler;
+ onAdExpired?: DirectEventHandler;
+}
+
+export default codegenNativeComponent(
+ 'RNAppodealNativeAdView'
+) as HostComponent;
diff --git a/src/specs/AppodealNativeAssetViewNativeComponent.ts b/src/specs/AppodealNativeAssetViewNativeComponent.ts
new file mode 100644
index 0000000..b234461
--- /dev/null
+++ b/src/specs/AppodealNativeAssetViewNativeComponent.ts
@@ -0,0 +1,14 @@
+import type { HostComponent, ViewProps } from 'react-native';
+import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent';
+
+export interface NativeProps extends ViewProps {
+ /**
+ * Asset role inside AppodealNativeAdView:
+ * media | icon | title | description | callToAction | attribution
+ */
+ asset?: string;
+}
+
+export default codegenNativeComponent(
+ 'RNAppodealNativeAssetView'
+) as HostComponent;
diff --git a/src/specs/AppodealNativeViewNativeComponent.ts b/src/specs/AppodealNativeViewNativeComponent.ts
new file mode 100644
index 0000000..6ea5b92
--- /dev/null
+++ b/src/specs/AppodealNativeViewNativeComponent.ts
@@ -0,0 +1,28 @@
+import type { HostComponent, ViewProps } from 'react-native';
+import type { DirectEventHandler } from 'react-native/Libraries/Types/CodegenTypes';
+import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent';
+
+export type NativeAdInfoEvent = Readonly<{
+ adId?: string;
+}>;
+
+export type NativeAdLoadFailedEvent = Readonly<{
+ adId?: string;
+ message?: string;
+}>;
+
+export interface NativeProps extends ViewProps {
+ adId?: string;
+ placement?: string;
+ /** Layout style: newsFeed | appWall | contentStream. Named adTemplate — `template` is a C++ keyword and breaks Fabric codegen. */
+ adTemplate?: string;
+ onAdLoaded?: DirectEventHandler;
+ onAdFailedToLoad?: DirectEventHandler;
+ onAdShown?: DirectEventHandler;
+ onAdClicked?: DirectEventHandler;
+ onAdExpired?: DirectEventHandler;
+}
+
+export default codegenNativeComponent(
+ 'RNAppodealNativeView'
+) as HostComponent;
diff --git a/src/specs/NativeAppodealModule.ts b/src/specs/NativeAppodealModule.ts
index 2aa6b67..39bfd16 100644
--- a/src/specs/NativeAppodealModule.ts
+++ b/src/specs/NativeAppodealModule.ts
@@ -74,6 +74,13 @@ export interface Spec extends TurboModule {
): Promise;
trackEvent(name: string, parameters: UnsafeObject): void;
+ // Native ads
+ getNativeAds(count: number): UnsafeObject;
+ getAvailableNativeAdsCount(): number;
+ destroyNativeAd(adId: string): void;
+ cacheNativeAds(count: number): void;
+ setPreferredNativeContentType(type: string): void;
+
// Bidon
setBidonEndpoint(endpoint: string): void;
getBidonEndpoint(): string | null;
diff --git a/src/types/AppodealAdTypes.ts b/src/types/AppodealAdTypes.ts
index dab445c..77f7bf1 100644
--- a/src/types/AppodealAdTypes.ts
+++ b/src/types/AppodealAdTypes.ts
@@ -16,15 +16,41 @@ export enum AppodealAdType {
BANNER_BOTTOM = 1 << 3, // 8
BANNER_TOP = 1 << 4, // 16
REWARDED_VIDEO = 1 << 5, // 32
+ NATIVE = 1 << 7, // 128
MREC = 1 << 8, // 256
ALL = INTERSTITIAL |
BANNER |
BANNER_BOTTOM |
BANNER_TOP |
REWARDED_VIDEO |
+ NATIVE |
MREC,
}
+/**
+ * Preferred media content type for native ads
+ */
+export type AppodealNativeContentType = 'auto' | 'noVideo' | 'video';
+
+/**
+ * Native ad template used by AppodealNative view (stock SDK templates).
+ * Custom layouts use AppodealNativeAdView + asset children instead.
+ */
+export type AppodealNativeTemplate = 'newsFeed' | 'appWall' | 'contentStream';
+
+/**
+ * Metadata for a cached native ad pulled from the SDK
+ */
+export interface AppodealNativeAdInfo {
+ id: string;
+ title: string;
+ description: string;
+ callToAction: string;
+ rating: number;
+ containsVideo: boolean;
+ predictedEcpm: number;
+}
+
/**
* Ad revenue information for analytics
*/
diff --git a/src/types/index.ts b/src/types/index.ts
index 2f52762..e7ca576 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -8,7 +8,12 @@
// Ad-related types
export { AppodealAdType } from './AppodealAdTypes';
-export type { AppodealAdRevenue } from './AppodealAdTypes';
+export type {
+ AppodealAdRevenue,
+ AppodealNativeAdInfo,
+ AppodealNativeContentType,
+ AppodealNativeTemplate,
+} from './AppodealAdTypes';
// Consent-related types
export {