@ezoic/react-native-sdk 1.2.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/EzoicReactNativeSdk.podspec +4 -1
- package/README.md +86 -2
- package/android/build.gradle +7 -1
- package/android/src/main/java/com/ezoic/reactnative/EzoicAdsModule.kt +129 -0
- package/android/src/main/java/com/ezoic/reactnative/EzoicNativeAdViewManager.kt +314 -0
- package/android/src/main/java/com/ezoic/reactnative/EzoicOutstreamAdViewManager.kt +191 -0
- package/android/src/main/java/com/ezoic/reactnative/EzoicReactNativeSdkPackage.kt +5 -1
- package/ios/EzoicAdsImpl.swift +160 -0
- package/ios/EzoicNativeAdHostView.swift +181 -0
- package/ios/EzoicNativeAdViewComponentView.mm +99 -0
- package/ios/EzoicOutstreamAdHostView.swift +86 -0
- package/ios/EzoicOutstreamAdViewComponentView.mm +105 -0
- package/ios/EzoicReactNativeSdk.mm +41 -0
- package/lib/module/EzoicInstreamAd.js +91 -0
- package/lib/module/EzoicInstreamAd.js.map +1 -0
- package/lib/module/EzoicNativeAdViewNativeComponent.ts +22 -0
- package/lib/module/EzoicOutstreamAdViewNativeComponent.ts +23 -0
- package/lib/module/NativeEzoicAds.js.map +1 -1
- package/lib/module/helpers.js.map +1 -1
- package/lib/module/index.js +58 -0
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/src/EzoicInstreamAd.d.ts +86 -0
- package/lib/typescript/src/EzoicInstreamAd.d.ts.map +1 -0
- package/lib/typescript/src/EzoicNativeAdViewNativeComponent.d.ts +18 -0
- package/lib/typescript/src/EzoicNativeAdViewNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/EzoicOutstreamAdViewNativeComponent.d.ts +18 -0
- package/lib/typescript/src/EzoicOutstreamAdViewNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/NativeEzoicAds.d.ts +4 -0
- package/lib/typescript/src/NativeEzoicAds.d.ts.map +1 -1
- package/lib/typescript/src/helpers.d.ts +1 -1
- package/lib/typescript/src/helpers.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +42 -0
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/EzoicInstreamAd.ts +110 -0
- package/src/EzoicNativeAdViewNativeComponent.ts +22 -0
- package/src/EzoicOutstreamAdViewNativeComponent.ts +23 -0
- package/src/NativeEzoicAds.ts +18 -0
- package/src/helpers.ts +1 -1
- package/src/index.tsx +100 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
package com.ezoic.reactnative
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.view.View
|
|
5
|
+
import android.view.ViewGroup
|
|
6
|
+
import android.widget.FrameLayout
|
|
7
|
+
import com.facebook.react.bridge.Arguments
|
|
8
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
9
|
+
import com.facebook.react.bridge.WritableMap
|
|
10
|
+
import com.facebook.react.module.annotations.ReactModule
|
|
11
|
+
import com.facebook.react.uimanager.SimpleViewManager
|
|
12
|
+
import com.facebook.react.uimanager.ThemedReactContext
|
|
13
|
+
import com.facebook.react.uimanager.annotations.ReactProp
|
|
14
|
+
import com.facebook.react.uimanager.events.RCTEventEmitter
|
|
15
|
+
import com.ezoic.ads.sdk.adunits.EzoicOutstreamAdView
|
|
16
|
+
import com.ezoic.ads.sdk.adunits.EzoicOutstreamAdViewListener
|
|
17
|
+
import com.ezoic.ads.sdk.core.EzoicError
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Fabric view manager for the outstream video component. Mirrors
|
|
21
|
+
* [EzoicNativeAdViewManager] exactly (deferred load, load guards, teardown,
|
|
22
|
+
* manual measure/layout fix, bubbling-event registry) and differs only in the
|
|
23
|
+
* body of [maybeLoad]: the native [EzoicOutstreamAdView] is itself the rendered
|
|
24
|
+
* view (a `FrameLayout` that attaches its own GAM `AdManagerAdView`), so this
|
|
25
|
+
* manager adds that native view as the container's child and lets it render —
|
|
26
|
+
* there is no template to build as the native-ad manager does.
|
|
27
|
+
*/
|
|
28
|
+
@ReactModule(name = EzoicOutstreamAdViewManager.NAME)
|
|
29
|
+
class EzoicOutstreamAdViewManager(private val ctx: ReactApplicationContext) :
|
|
30
|
+
SimpleViewManager<EzoicOutstreamAdViewManager.OutstreamAdContainer>() {
|
|
31
|
+
|
|
32
|
+
override fun getName() = NAME
|
|
33
|
+
|
|
34
|
+
override fun createViewInstance(reactContext: ThemedReactContext): OutstreamAdContainer {
|
|
35
|
+
val container = OutstreamAdContainer(reactContext)
|
|
36
|
+
container.layoutParams = ViewGroup.LayoutParams(
|
|
37
|
+
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT
|
|
38
|
+
)
|
|
39
|
+
return container
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
@ReactProp(name = "adUnitIdentifier")
|
|
43
|
+
fun setAdUnitIdentifier(view: OutstreamAdContainer, value: String?) {
|
|
44
|
+
val newAdUnitId = value?.toIntOrNull() ?: 0
|
|
45
|
+
// Ad unit changed after a load already started: tear down the loaded/loading
|
|
46
|
+
// ad and clear the started flag so onAfterUpdateTransaction's maybeLoad
|
|
47
|
+
// starts a fresh load for the new id.
|
|
48
|
+
if (newAdUnitId != view.adUnitId && view.loadStarted) {
|
|
49
|
+
view.ezoicOutstreamAd?.destroy()
|
|
50
|
+
view.ezoicOutstreamAd = null
|
|
51
|
+
view.removeAllViews()
|
|
52
|
+
view.loadStarted = false
|
|
53
|
+
view.loadGeneration++
|
|
54
|
+
}
|
|
55
|
+
view.adUnitId = newAdUnitId
|
|
56
|
+
view.rawAdUnitId = value ?: ""
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Fabric sets every prop for the mount transaction before this runs, so the
|
|
60
|
+
// ad unit id is final here. `view.post` escapes the mount transaction so the
|
|
61
|
+
// synchronous native-SDK failure path (uninitialized) can't reenter the
|
|
62
|
+
// mounting layer; the started flag survives repeated transactions.
|
|
63
|
+
override fun onAfterUpdateTransaction(view: OutstreamAdContainer) {
|
|
64
|
+
super.onAfterUpdateTransaction(view)
|
|
65
|
+
view.post { maybeLoad(view) }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
private fun maybeLoad(view: OutstreamAdContainer) {
|
|
69
|
+
if (view.loadStarted || view.disposed) return
|
|
70
|
+
if (view.adUnitId <= 0) {
|
|
71
|
+
// Non-numeric or missing id coerces to 0 in setAdUnitIdentifier. Only
|
|
72
|
+
// emit an error when a prop was actually supplied (non-empty raw
|
|
73
|
+
// string); an unset prop should stay silent.
|
|
74
|
+
if (view.rawAdUnitId.isNotEmpty()) {
|
|
75
|
+
view.loadStarted = true
|
|
76
|
+
emit(view, "topError", errorMap("Invalid ad unit identifier", 0))
|
|
77
|
+
}
|
|
78
|
+
return
|
|
79
|
+
}
|
|
80
|
+
view.loadStarted = true
|
|
81
|
+
val generation = view.loadGeneration
|
|
82
|
+
// The native outstream view renders itself. Attach the listener BEFORE
|
|
83
|
+
// loadAd() so no early lifecycle callback is missed, add it to the
|
|
84
|
+
// container, then load.
|
|
85
|
+
val outstreamAd = EzoicOutstreamAdView(view.context, view.adUnitId)
|
|
86
|
+
outstreamAd.listener = object : EzoicOutstreamAdViewListener {
|
|
87
|
+
override fun onOutstreamLoaded(adView: EzoicOutstreamAdView) {
|
|
88
|
+
// dispose() and this callback both arrive on the main thread, so the
|
|
89
|
+
// check is race-free. A generation mismatch means the ad unit changed
|
|
90
|
+
// mid-load (mirrors the SDK's isCurrentLoad token pattern): emit nothing.
|
|
91
|
+
if (view.disposed || view.loadGeneration != generation) return
|
|
92
|
+
emit(view, "topLoad", Arguments.createMap())
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
override fun onOutstreamLoadFailed(adView: EzoicOutstreamAdView, error: EzoicError) {
|
|
96
|
+
if (view.disposed || view.loadGeneration != generation) return
|
|
97
|
+
emit(view, "topError", errorMap(error.message, error.code))
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
override fun onOutstreamImpression(adView: EzoicOutstreamAdView) {
|
|
101
|
+
if (view.disposed || view.loadGeneration != generation) return
|
|
102
|
+
emit(view, "topImpression", Arguments.createMap())
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
override fun onOutstreamClicked(adView: EzoicOutstreamAdView) {
|
|
106
|
+
if (view.disposed || view.loadGeneration != generation) return
|
|
107
|
+
emit(view, "topAdClick", Arguments.createMap())
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
override fun onOutstreamOpened(adView: EzoicOutstreamAdView) {
|
|
111
|
+
if (view.disposed || view.loadGeneration != generation) return
|
|
112
|
+
emit(view, "topOpen", Arguments.createMap())
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
override fun onOutstreamClosed(adView: EzoicOutstreamAdView) {
|
|
116
|
+
if (view.disposed || view.loadGeneration != generation) return
|
|
117
|
+
emit(view, "topClose", Arguments.createMap())
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
view.ezoicOutstreamAd = outstreamAd
|
|
121
|
+
view.removeAllViews()
|
|
122
|
+
view.addView(outstreamAd)
|
|
123
|
+
outstreamAd.loadAd()
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
override fun onDropViewInstance(view: OutstreamAdContainer) {
|
|
127
|
+
super.onDropViewInstance(view)
|
|
128
|
+
view.disposed = true
|
|
129
|
+
view.loadGeneration++
|
|
130
|
+
view.ezoicOutstreamAd?.destroy()
|
|
131
|
+
view.ezoicOutstreamAd = null
|
|
132
|
+
view.removeAllViews()
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
private fun emit(view: OutstreamAdContainer, event: String, payload: WritableMap) {
|
|
136
|
+
if (view.disposed) return
|
|
137
|
+
ctx.getJSModule(RCTEventEmitter::class.java).receiveEvent(view.id, event, payload)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private fun errorMap(message: String?, code: Int): WritableMap {
|
|
141
|
+
val map = Arguments.createMap()
|
|
142
|
+
map.putString("message", message ?: "")
|
|
143
|
+
map.putInt("code", code)
|
|
144
|
+
return map
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
override fun getExportedCustomBubblingEventTypeConstants(): Map<String, Any> {
|
|
148
|
+
fun reg(on: String) = mapOf("phasedRegistrationNames" to mapOf("bubbled" to on))
|
|
149
|
+
return mapOf(
|
|
150
|
+
"topLoad" to reg("onLoad"),
|
|
151
|
+
"topError" to reg("onError"),
|
|
152
|
+
"topImpression" to reg("onImpression"),
|
|
153
|
+
"topAdClick" to reg("onAdClick"),
|
|
154
|
+
"topOpen" to reg("onOpen"),
|
|
155
|
+
"topClose" to reg("onClose")
|
|
156
|
+
)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Container for the native outstream view. RN lays out only Yoga-managed
|
|
161
|
+
* views; the native [EzoicOutstreamAdView] is added from native code and stays
|
|
162
|
+
* unmeasured (blank) unless we force a measure/layout pass. Overriding
|
|
163
|
+
* [requestLayout] to post a manual measure(EXACTLY)+layout of the current
|
|
164
|
+
* bounds is the proven fix (identical to [EzoicNativeAdViewManager]).
|
|
165
|
+
*/
|
|
166
|
+
class OutstreamAdContainer(context: Context) : FrameLayout(context) {
|
|
167
|
+
var adUnitId: Int = 0
|
|
168
|
+
var rawAdUnitId: String = ""
|
|
169
|
+
var loadStarted: Boolean = false
|
|
170
|
+
var disposed: Boolean = false
|
|
171
|
+
var loadGeneration: Int = 0
|
|
172
|
+
var ezoicOutstreamAd: EzoicOutstreamAdView? = null
|
|
173
|
+
|
|
174
|
+
private val measureAndLayout = Runnable {
|
|
175
|
+
measure(
|
|
176
|
+
View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
|
|
177
|
+
View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY)
|
|
178
|
+
)
|
|
179
|
+
layout(left, top, right, bottom)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
override fun requestLayout() {
|
|
183
|
+
super.requestLayout()
|
|
184
|
+
post(measureAndLayout)
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
companion object {
|
|
189
|
+
const val NAME = "EzoicOutstreamAdView"
|
|
190
|
+
}
|
|
191
|
+
}
|
|
@@ -19,7 +19,11 @@ class EzoicReactNativeSdkPackage : BaseReactPackage() {
|
|
|
19
19
|
override fun createViewManagers(
|
|
20
20
|
reactContext: ReactApplicationContext
|
|
21
21
|
): List<ViewManager<*, *>> {
|
|
22
|
-
return listOf(
|
|
22
|
+
return listOf(
|
|
23
|
+
EzoicBannerViewManager(reactContext),
|
|
24
|
+
EzoicNativeAdViewManager(reactContext),
|
|
25
|
+
EzoicOutstreamAdViewManager(reactContext)
|
|
26
|
+
)
|
|
23
27
|
}
|
|
24
28
|
|
|
25
29
|
override fun getReactModuleInfoProvider() = ReactModuleInfoProvider {
|
package/ios/EzoicAdsImpl.swift
CHANGED
|
@@ -43,6 +43,28 @@ import EzoicAdsSDKBinary
|
|
|
43
43
|
/// Ad unit ids with an in-flight interstitial `load`.
|
|
44
44
|
private var loadingInterstitial: Set<Int> = []
|
|
45
45
|
|
|
46
|
+
/// Active instream controllers, keyed by ad unit id. Instream is multi-use and
|
|
47
|
+
/// NOT auto-destroying, so this impl retains each controller across load
|
|
48
|
+
/// cycles (and is its `weak` delegate) until `destroyInstreamAd`.
|
|
49
|
+
private var instreamAds: [Int: EzoicInstreamAd] = [:]
|
|
50
|
+
|
|
51
|
+
/// In-flight instream `load` calls, keyed by ad unit id. Doubles as the
|
|
52
|
+
/// duplicate-load guard: the native `load` is a silent no-op while already
|
|
53
|
+
/// loading, so an unguarded second promise would hang forever.
|
|
54
|
+
private var pendingInstreamLoads: [Int: PendingInstreamLoad] = [:]
|
|
55
|
+
|
|
56
|
+
private final class PendingInstreamLoad {
|
|
57
|
+
let resolve: (Any?) -> Void
|
|
58
|
+
let reject: (String, String, NSError?) -> Void
|
|
59
|
+
/// Set once the promise is fulfilled so a late delegate callback (or a
|
|
60
|
+
/// destroy that already settled it) cannot double-settle it.
|
|
61
|
+
var settled: Bool = false
|
|
62
|
+
init(resolve: @escaping (Any?) -> Void, reject: @escaping (String, String, NSError?) -> Void) {
|
|
63
|
+
self.resolve = resolve
|
|
64
|
+
self.reject = reject
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
46
68
|
/// Runs `work` on the main thread. The ad/pending/loading dictionaries and
|
|
47
69
|
/// every native load/show call touch UIKit and this shared state, so they must
|
|
48
70
|
/// only run on main. Delegate callbacks already arrive on main.
|
|
@@ -228,6 +250,113 @@ import EzoicAdsSDKBinary
|
|
|
228
250
|
for (key, value) in extra { body[key] = value }
|
|
229
251
|
eventEmitter?("EzoicInterstitialAdEvent", body)
|
|
230
252
|
}
|
|
253
|
+
|
|
254
|
+
// MARK: - Instream video
|
|
255
|
+
|
|
256
|
+
/// Converts a bridge `Double` ad unit id to a native `Int`, rejecting NaN,
|
|
257
|
+
/// infinite, and out-of-`Int` ids. `Number("abc")` on the JS side arrives as
|
|
258
|
+
/// NaN, and `Int(Double.nan)` traps; likewise `Int(1e20)` traps even though
|
|
259
|
+
/// 1e20 is finite, so the upper bound must be checked before the `Int(...)`
|
|
260
|
+
/// conversion. Bounded to `Int32.max` and requires >= 1 to match Android's
|
|
261
|
+
/// rejection of ids <= 0.
|
|
262
|
+
private func instreamId(_ adUnitIdentifier: Double,
|
|
263
|
+
_ reject: (String, String, NSError?) -> Void) -> Int? {
|
|
264
|
+
guard adUnitIdentifier.isFinite,
|
|
265
|
+
adUnitIdentifier >= 1,
|
|
266
|
+
adUnitIdentifier <= Double(Int32.max) else {
|
|
267
|
+
reject("EzoicAds", "Invalid adUnitIdentifier: \(adUnitIdentifier)", nil)
|
|
268
|
+
return nil
|
|
269
|
+
}
|
|
270
|
+
return Int(adUnitIdentifier)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
@objc public func loadInstreamAd(_ adUnitIdentifier: Double,
|
|
274
|
+
contentUrl: String?,
|
|
275
|
+
resolve: @escaping (Any?) -> Void,
|
|
276
|
+
reject: @escaping (String, String, NSError?) -> Void) {
|
|
277
|
+
onMain { [weak self] in
|
|
278
|
+
guard let self = self else { return }
|
|
279
|
+
guard let id = self.instreamId(adUnitIdentifier, reject) else { return }
|
|
280
|
+
// Reject overlapping loads: the native load silently no-ops while an
|
|
281
|
+
// earlier one is in flight, which would hang this promise forever.
|
|
282
|
+
if self.pendingInstreamLoads[id] != nil {
|
|
283
|
+
reject("EzoicAds", "An instream ad is already loading for ad unit \(id)", nil)
|
|
284
|
+
return
|
|
285
|
+
}
|
|
286
|
+
// Create-or-reuse: instream is multi-use, so a repeat load on the same id
|
|
287
|
+
// reuses the existing native controller (preserving its tag state).
|
|
288
|
+
let ad: EzoicInstreamAd
|
|
289
|
+
if let existing = self.instreamAds[id] {
|
|
290
|
+
ad = existing
|
|
291
|
+
} else {
|
|
292
|
+
ad = EzoicInstreamAd(adUnitId: id)
|
|
293
|
+
self.instreamAds[id] = ad
|
|
294
|
+
}
|
|
295
|
+
// Register the pending holder BEFORE calling load: early validation
|
|
296
|
+
// failures deliver the delegate callback synchronously.
|
|
297
|
+
self.pendingInstreamLoads[id] = PendingInstreamLoad(resolve: resolve, reject: reject)
|
|
298
|
+
ad.load(contentUrl: contentUrl, delegate: self)
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
@objc public func getInstreamNextAdTagUrl(_ adUnitIdentifier: Double,
|
|
303
|
+
resolve: @escaping (Any?) -> Void,
|
|
304
|
+
reject: @escaping (String, String, NSError?) -> Void) {
|
|
305
|
+
onMain { [weak self] in
|
|
306
|
+
guard let self = self else { return }
|
|
307
|
+
guard let id = self.instreamId(adUnitIdentifier, reject) else { return }
|
|
308
|
+
// Native getNextAdTagUrl returns nil once the waterfall is exhausted,
|
|
309
|
+
// before a successful load, or after destroy — surface that as JS null.
|
|
310
|
+
resolve(self.instreamAds[id]?.getNextAdTagUrl())
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
@objc public func reportInstreamImpression(_ adUnitIdentifier: Double,
|
|
315
|
+
revenueUsd: NSNumber?,
|
|
316
|
+
resolve: @escaping (Any?) -> Void,
|
|
317
|
+
reject: @escaping (String, String, NSError?) -> Void) {
|
|
318
|
+
onMain { [weak self] in
|
|
319
|
+
guard let self = self else { return }
|
|
320
|
+
guard let id = self.instreamId(adUnitIdentifier, reject) else { return }
|
|
321
|
+
self.instreamAds[id]?.reportImpression(revenueUsd: revenueUsd?.doubleValue)
|
|
322
|
+
resolve(nil)
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
@objc public func destroyInstreamAd(_ adUnitIdentifier: Double,
|
|
327
|
+
resolve: @escaping (Any?) -> Void,
|
|
328
|
+
reject: @escaping (String, String, NSError?) -> Void) {
|
|
329
|
+
onMain { [weak self] in
|
|
330
|
+
guard let self = self else { return }
|
|
331
|
+
guard let id = self.instreamId(adUnitIdentifier, reject) else { return }
|
|
332
|
+
// Native suppresses load callbacks once destroyed, so settle any pending
|
|
333
|
+
// load's promise here first or it hangs forever.
|
|
334
|
+
if let pending = self.pendingInstreamLoads.removeValue(forKey: id), !pending.settled {
|
|
335
|
+
pending.settled = true
|
|
336
|
+
pending.reject("EzoicAds", "Instream ad was destroyed while loading", nil)
|
|
337
|
+
}
|
|
338
|
+
self.instreamAds.removeValue(forKey: id)?.destroy()
|
|
339
|
+
resolve(nil)
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/// Module teardown parity with Android's `EzoicAdsModule.invalidate`: settle
|
|
344
|
+
/// every pending instream load with an error and destroy every controller so
|
|
345
|
+
/// no promise hangs and no native resource leaks.
|
|
346
|
+
@objc public func invalidate() {
|
|
347
|
+
onMain { [weak self] in
|
|
348
|
+
guard let self = self else { return }
|
|
349
|
+
for (_, pending) in self.pendingInstreamLoads where !pending.settled {
|
|
350
|
+
pending.settled = true
|
|
351
|
+
pending.reject("EzoicAds", "Module was destroyed while loading", nil)
|
|
352
|
+
}
|
|
353
|
+
self.pendingInstreamLoads.removeAll()
|
|
354
|
+
for (_, ad) in self.instreamAds {
|
|
355
|
+
ad.destroy()
|
|
356
|
+
}
|
|
357
|
+
self.instreamAds.removeAll()
|
|
358
|
+
}
|
|
359
|
+
}
|
|
231
360
|
}
|
|
232
361
|
|
|
233
362
|
// MARK: - EzoicRewardedAdDelegate
|
|
@@ -310,3 +439,34 @@ extension EzoicAdsImpl: EzoicInterstitialAdDelegate {
|
|
|
310
439
|
}
|
|
311
440
|
}
|
|
312
441
|
}
|
|
442
|
+
|
|
443
|
+
// MARK: - EzoicInstreamAdDelegate
|
|
444
|
+
|
|
445
|
+
extension EzoicAdsImpl: EzoicInstreamAdDelegate {
|
|
446
|
+
|
|
447
|
+
// Delegate callbacks arrive on main (see `onMain`), matching the rewarded /
|
|
448
|
+
// interstitial extensions which touch shared state directly. Removal happens
|
|
449
|
+
// only inside the not-yet-settled branch (settled-conditional removal) so a
|
|
450
|
+
// stale callback can't evict a newer load's holder after destroy→reload.
|
|
451
|
+
public func instreamAd(_ instreamAd: EzoicInstreamAd, didReceiveAdTag adTagUrl: String) {
|
|
452
|
+
let id = instreamAd.adUnitId
|
|
453
|
+
// Identity check: after destroy->reload for this id, a late callback from
|
|
454
|
+
// the destroyed controller must not settle the newer load's promise.
|
|
455
|
+
guard self.instreamAds[id] === instreamAd else { return }
|
|
456
|
+
guard let pending = pendingInstreamLoads[id], !pending.settled else { return }
|
|
457
|
+
pending.settled = true
|
|
458
|
+
pendingInstreamLoads.removeValue(forKey: id)
|
|
459
|
+
pending.resolve(adTagUrl)
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
public func instreamAd(_ instreamAd: EzoicInstreamAd, didFailToLoadWithError error: EzoicError) {
|
|
463
|
+
let id = instreamAd.adUnitId
|
|
464
|
+
// Identity check: after destroy->reload for this id, a late callback from
|
|
465
|
+
// the destroyed controller must not settle the newer load's promise.
|
|
466
|
+
guard self.instreamAds[id] === instreamAd else { return }
|
|
467
|
+
guard let pending = pendingInstreamLoads[id], !pending.settled else { return }
|
|
468
|
+
pending.settled = true
|
|
469
|
+
pendingInstreamLoads.removeValue(forKey: id)
|
|
470
|
+
pending.reject("EzoicAds", error.localizedDescription, error as NSError)
|
|
471
|
+
}
|
|
472
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import UIKit
|
|
2
|
+
import EzoicAdsSDKBinary
|
|
3
|
+
import GoogleMobileAds
|
|
4
|
+
|
|
5
|
+
@objc public protocol EzoicNativeAdHostViewDelegate: AnyObject {
|
|
6
|
+
func nativeAdDidLoad()
|
|
7
|
+
func nativeAdDidFail(_ message: String, code: Int)
|
|
8
|
+
func nativeAdDidRecordImpression()
|
|
9
|
+
func nativeAdDidRecordClick()
|
|
10
|
+
func nativeAdWillPresentScreen()
|
|
11
|
+
func nativeAdDidDismissScreen()
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
@objc public class EzoicNativeAdHostView: UIView, EzoicNativeAdDelegate {
|
|
15
|
+
|
|
16
|
+
@objc public weak var hostDelegate: EzoicNativeAdHostViewDelegate?
|
|
17
|
+
|
|
18
|
+
private var adUnitId: Int = 0
|
|
19
|
+
private var ezoicNativeAd: EzoicNativeAd?
|
|
20
|
+
private var adView: NativeAdView?
|
|
21
|
+
private var loadStarted = false
|
|
22
|
+
|
|
23
|
+
/// Stores the ad unit id. The load is NOT started here — the Fabric
|
|
24
|
+
/// component view calls `startLoad()` from `finalizeUpdates`, after the
|
|
25
|
+
/// event emitter is attached, so a synchronous SDK failure (uninitialized)
|
|
26
|
+
/// can deliver `onError` instead of being dropped.
|
|
27
|
+
@objc public func configure(adUnitIdentifier: String) {
|
|
28
|
+
self.adUnitId = Int(adUnitIdentifier) ?? 0
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/// Starts the native-ad load once. A second call is a no-op; the guard
|
|
32
|
+
/// survives repeated `finalizeUpdates` calls.
|
|
33
|
+
@objc public func startLoad() {
|
|
34
|
+
if loadStarted { return }
|
|
35
|
+
loadStarted = true
|
|
36
|
+
EzoicNativeAd.load(adUnitIdentifier: adUnitId) { [weak self] result in
|
|
37
|
+
guard let self = self else { return }
|
|
38
|
+
switch result {
|
|
39
|
+
case .success(let ad):
|
|
40
|
+
guard let gmaAd = ad.nativeAd else {
|
|
41
|
+
// Empty-content ad: destroy it and do not retain it instead of
|
|
42
|
+
// keeping an unrenderable, errored ad alive.
|
|
43
|
+
ad.destroy()
|
|
44
|
+
self.hostDelegate?.nativeAdDidFail("Native ad loaded without content", code: 0)
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
self.ezoicNativeAd = ad
|
|
48
|
+
// Attach the delegate before rendering so the impression, which fires
|
|
49
|
+
// as soon as the NativeAdView is displayed, is delivered.
|
|
50
|
+
ad.delegate = self
|
|
51
|
+
self.render(gmaAd)
|
|
52
|
+
self.hostDelegate?.nativeAdDidLoad()
|
|
53
|
+
case .failure(let error):
|
|
54
|
+
self.hostDelegate?.nativeAdDidFail(error.localizedDescription, code: error.code)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/// Builds a template `NativeAdView` in code (mirrors the Android template):
|
|
60
|
+
/// a header row (icon + headline/advertiser), a `MediaView`, the body text
|
|
61
|
+
/// and a call-to-action button. Optional text/image assets are created and
|
|
62
|
+
/// registered only when present, but the `MediaView` is always built: on
|
|
63
|
+
/// GMA 12 `NativeAd.mediaContent` is non-optional and the media view is a
|
|
64
|
+
/// required asset. `adView.nativeAd` is assigned last.
|
|
65
|
+
private func render(_ gmaAd: GoogleMobileAds.NativeAd) {
|
|
66
|
+
let adView = NativeAdView()
|
|
67
|
+
adView.translatesAutoresizingMaskIntoConstraints = false
|
|
68
|
+
|
|
69
|
+
let mainStack = UIStackView()
|
|
70
|
+
mainStack.axis = .vertical
|
|
71
|
+
mainStack.spacing = 8
|
|
72
|
+
mainStack.translatesAutoresizingMaskIntoConstraints = false
|
|
73
|
+
|
|
74
|
+
let headerRow = UIStackView()
|
|
75
|
+
headerRow.axis = .horizontal
|
|
76
|
+
headerRow.spacing = 8
|
|
77
|
+
headerRow.alignment = .center
|
|
78
|
+
|
|
79
|
+
if let image = gmaAd.icon?.image {
|
|
80
|
+
let iconView = UIImageView(image: image)
|
|
81
|
+
iconView.translatesAutoresizingMaskIntoConstraints = false
|
|
82
|
+
NSLayoutConstraint.activate([
|
|
83
|
+
iconView.widthAnchor.constraint(equalToConstant: 40),
|
|
84
|
+
iconView.heightAnchor.constraint(equalToConstant: 40),
|
|
85
|
+
])
|
|
86
|
+
headerRow.addArrangedSubview(iconView)
|
|
87
|
+
adView.iconView = iconView
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
let textColumn = UIStackView()
|
|
91
|
+
textColumn.axis = .vertical
|
|
92
|
+
|
|
93
|
+
if let headline = gmaAd.headline {
|
|
94
|
+
let label = UILabel()
|
|
95
|
+
label.text = headline
|
|
96
|
+
label.font = .boldSystemFont(ofSize: 16)
|
|
97
|
+
label.numberOfLines = 0
|
|
98
|
+
textColumn.addArrangedSubview(label)
|
|
99
|
+
adView.headlineView = label
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if let advertiser = gmaAd.advertiser {
|
|
103
|
+
let label = UILabel()
|
|
104
|
+
label.text = advertiser
|
|
105
|
+
label.font = .systemFont(ofSize: 12)
|
|
106
|
+
textColumn.addArrangedSubview(label)
|
|
107
|
+
adView.advertiserView = label
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
headerRow.addArrangedSubview(textColumn)
|
|
111
|
+
mainStack.addArrangedSubview(headerRow)
|
|
112
|
+
|
|
113
|
+
let mediaView = MediaView()
|
|
114
|
+
mediaView.mediaContent = gmaAd.mediaContent
|
|
115
|
+
mediaView.translatesAutoresizingMaskIntoConstraints = false
|
|
116
|
+
// Priority 999 so a caller-supplied style shorter than the template's
|
|
117
|
+
// natural height breaks this constraint instead of spamming
|
|
118
|
+
// unsatisfiable-constraint logs.
|
|
119
|
+
let mediaHeight = mediaView.heightAnchor.constraint(equalToConstant: 175)
|
|
120
|
+
mediaHeight.priority = UILayoutPriority(999)
|
|
121
|
+
mediaHeight.isActive = true
|
|
122
|
+
mainStack.addArrangedSubview(mediaView)
|
|
123
|
+
adView.mediaView = mediaView
|
|
124
|
+
|
|
125
|
+
if let body = gmaAd.body {
|
|
126
|
+
let label = UILabel()
|
|
127
|
+
label.text = body
|
|
128
|
+
label.font = .systemFont(ofSize: 14)
|
|
129
|
+
label.numberOfLines = 0
|
|
130
|
+
mainStack.addArrangedSubview(label)
|
|
131
|
+
adView.bodyView = label
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if let cta = gmaAd.callToAction {
|
|
135
|
+
let button = UIButton(type: .system)
|
|
136
|
+
button.setTitle(cta, for: .normal)
|
|
137
|
+
// The NativeAdView handles the tap; the button must not intercept it.
|
|
138
|
+
button.isUserInteractionEnabled = false
|
|
139
|
+
mainStack.addArrangedSubview(button)
|
|
140
|
+
adView.callToActionView = button
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
adView.addSubview(mainStack)
|
|
144
|
+
NSLayoutConstraint.activate([
|
|
145
|
+
mainStack.topAnchor.constraint(equalTo: adView.topAnchor, constant: 8),
|
|
146
|
+
mainStack.leadingAnchor.constraint(equalTo: adView.leadingAnchor, constant: 8),
|
|
147
|
+
mainStack.trailingAnchor.constraint(equalTo: adView.trailingAnchor, constant: -8),
|
|
148
|
+
mainStack.bottomAnchor.constraint(equalTo: adView.bottomAnchor, constant: -8),
|
|
149
|
+
])
|
|
150
|
+
|
|
151
|
+
self.adView?.removeFromSuperview()
|
|
152
|
+
addSubview(adView)
|
|
153
|
+
NSLayoutConstraint.activate([
|
|
154
|
+
adView.topAnchor.constraint(equalTo: topAnchor),
|
|
155
|
+
adView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
|
156
|
+
adView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
|
157
|
+
adView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
|
158
|
+
])
|
|
159
|
+
|
|
160
|
+
adView.nativeAd = gmaAd
|
|
161
|
+
self.adView = adView
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// MARK: - EzoicNativeAdDelegate
|
|
165
|
+
public func nativeAdDidRecordImpression(_ nativeAd: EzoicNativeAd) {
|
|
166
|
+
hostDelegate?.nativeAdDidRecordImpression()
|
|
167
|
+
}
|
|
168
|
+
public func nativeAdDidRecordClick(_ nativeAd: EzoicNativeAd) {
|
|
169
|
+
hostDelegate?.nativeAdDidRecordClick()
|
|
170
|
+
}
|
|
171
|
+
public func nativeAdWillPresentScreen(_ nativeAd: EzoicNativeAd) {
|
|
172
|
+
hostDelegate?.nativeAdWillPresentScreen()
|
|
173
|
+
}
|
|
174
|
+
public func nativeAdDidDismissScreen(_ nativeAd: EzoicNativeAd) {
|
|
175
|
+
hostDelegate?.nativeAdDidDismissScreen()
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
deinit {
|
|
179
|
+
ezoicNativeAd?.destroy()
|
|
180
|
+
}
|
|
181
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
#import <React/RCTViewComponentView.h>
|
|
2
|
+
#import <react/renderer/components/EzoicReactNativeSdkSpec/ComponentDescriptors.h>
|
|
3
|
+
#import <react/renderer/components/EzoicReactNativeSdkSpec/EventEmitters.h>
|
|
4
|
+
#import <react/renderer/components/EzoicReactNativeSdkSpec/Props.h>
|
|
5
|
+
#import <react/renderer/components/EzoicReactNativeSdkSpec/RCTComponentViewHelpers.h>
|
|
6
|
+
#import <EzoicReactNativeSdk/EzoicReactNativeSdk-Swift.h>
|
|
7
|
+
|
|
8
|
+
using namespace facebook::react;
|
|
9
|
+
|
|
10
|
+
@interface EzoicNativeAdView : RCTViewComponentView <EzoicNativeAdHostViewDelegate>
|
|
11
|
+
@end
|
|
12
|
+
|
|
13
|
+
@implementation EzoicNativeAdView {
|
|
14
|
+
EzoicNativeAdHostView *_host;
|
|
15
|
+
NSString *_lastAdUnit;
|
|
16
|
+
BOOL _loadStarted;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
+ (ComponentDescriptorProvider)componentDescriptorProvider {
|
|
20
|
+
return concreteComponentDescriptorProvider<EzoicNativeAdViewComponentDescriptor>();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
- (instancetype)initWithFrame:(CGRect)frame {
|
|
24
|
+
if (self = [super initWithFrame:frame]) {
|
|
25
|
+
_host = [EzoicNativeAdHostView new];
|
|
26
|
+
_host.hostDelegate = self;
|
|
27
|
+
self.contentView = _host;
|
|
28
|
+
}
|
|
29
|
+
return self;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
- (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps {
|
|
33
|
+
const auto &newProps = *std::static_pointer_cast<EzoicNativeAdViewProps const>(props);
|
|
34
|
+
NSString *adUnit = [NSString stringWithUTF8String:newProps.adUnitIdentifier.c_str()];
|
|
35
|
+
if (![adUnit isEqualToString:_lastAdUnit]) {
|
|
36
|
+
// Ad unit changed after a load already started: swap in a fresh host
|
|
37
|
+
// (mirrors prepareForRecycle) and reset the load guard so
|
|
38
|
+
// finalizeUpdates starts a new load for the new id.
|
|
39
|
+
if (_loadStarted) {
|
|
40
|
+
[_host removeFromSuperview];
|
|
41
|
+
_host = [EzoicNativeAdHostView new];
|
|
42
|
+
_host.hostDelegate = self;
|
|
43
|
+
self.contentView = _host;
|
|
44
|
+
_loadStarted = NO;
|
|
45
|
+
}
|
|
46
|
+
_lastAdUnit = adUnit;
|
|
47
|
+
[_host configureWithAdUnitIdentifier:adUnit];
|
|
48
|
+
}
|
|
49
|
+
[super updateProps:props oldProps:oldProps];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// RCTMountingManager mounts on Insert as updateProps → updateEventEmitter →
|
|
53
|
+
// finalizeUpdates. Starting the load here (not in updateProps) guarantees the
|
|
54
|
+
// event emitter is attached before the native SDK can fail synchronously and
|
|
55
|
+
// emit onError, which would otherwise be dropped while _eventEmitter is nil.
|
|
56
|
+
- (void)finalizeUpdates:(RNComponentViewUpdateMask)updateMask {
|
|
57
|
+
[super finalizeUpdates:updateMask];
|
|
58
|
+
if (!_loadStarted && _lastAdUnit.length > 0) {
|
|
59
|
+
_loadStarted = YES;
|
|
60
|
+
[_host startLoad];
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
- (void)nativeAdDidLoad {
|
|
65
|
+
if (_eventEmitter) std::static_pointer_cast<EzoicNativeAdViewEventEmitter const>(_eventEmitter)->onLoad({});
|
|
66
|
+
}
|
|
67
|
+
- (void)nativeAdDidFail:(NSString *)message code:(NSInteger)code {
|
|
68
|
+
if (_eventEmitter)
|
|
69
|
+
std::static_pointer_cast<EzoicNativeAdViewEventEmitter const>(_eventEmitter)
|
|
70
|
+
->onError({.message = std::string([message UTF8String]), .code = (int)code});
|
|
71
|
+
}
|
|
72
|
+
- (void)nativeAdDidRecordImpression {
|
|
73
|
+
if (_eventEmitter) std::static_pointer_cast<EzoicNativeAdViewEventEmitter const>(_eventEmitter)->onImpression({});
|
|
74
|
+
}
|
|
75
|
+
- (void)nativeAdDidRecordClick {
|
|
76
|
+
if (_eventEmitter) std::static_pointer_cast<EzoicNativeAdViewEventEmitter const>(_eventEmitter)->onAdClick({});
|
|
77
|
+
}
|
|
78
|
+
- (void)nativeAdWillPresentScreen {
|
|
79
|
+
if (_eventEmitter) std::static_pointer_cast<EzoicNativeAdViewEventEmitter const>(_eventEmitter)->onOpen({});
|
|
80
|
+
}
|
|
81
|
+
- (void)nativeAdDidDismissScreen {
|
|
82
|
+
if (_eventEmitter) std::static_pointer_cast<EzoicNativeAdViewEventEmitter const>(_eventEmitter)->onClose({});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
- (void)prepareForRecycle {
|
|
86
|
+
// Recycled views are reused for a new ad unit; rebuild the host (which owns
|
|
87
|
+
// the loaded EzoicNativeAd, destroyed on deinit) and reset the load guards.
|
|
88
|
+
[_host removeFromSuperview];
|
|
89
|
+
_host = [EzoicNativeAdHostView new];
|
|
90
|
+
_host.hostDelegate = self;
|
|
91
|
+
self.contentView = _host;
|
|
92
|
+
_lastAdUnit = nil;
|
|
93
|
+
_loadStarted = NO;
|
|
94
|
+
[super prepareForRecycle];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
Class<RCTComponentViewProtocol> EzoicNativeAdViewCls(void) { return EzoicNativeAdView.class; }
|
|
98
|
+
|
|
99
|
+
@end
|