@ezoic/react-native-sdk 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -20,6 +20,6 @@ Pod::Spec.new do |s|
20
20
 
21
21
  # Native Ezoic Ads SDK (vends the `EzoicAdsSDKBinary` module). Brings in
22
22
  # PrebidMobile + Google-Mobile-Ads-SDK transitively.
23
- s.dependency "EzoicAdsSDK", "~> 1.0"
23
+ s.dependency "EzoicAdsSDK", "~> 1.2"
24
24
  s.swift_version = "5.9"
25
25
  end
@@ -68,5 +68,5 @@ dependencies {
68
68
  // Native Ezoic Ads SDK (resolved from Maven Central). Brings in Google Mobile
69
69
  // Ads + Prebid transitively. Requires mavenCentral() + google() in the
70
70
  // consuming app's repositories (the RN template provides both).
71
- implementation "com.ezoic.sdk:ezoic-ads-sdk:1.0.0"
71
+ implementation "com.ezoic.sdk:ezoic-ads-sdk:1.2.0"
72
72
  }
@@ -1,17 +1,56 @@
1
1
  package com.ezoic.reactnative
2
2
 
3
3
  import android.app.Application
4
+ import com.facebook.react.bridge.Arguments
4
5
  import com.facebook.react.bridge.Promise
5
6
  import com.facebook.react.bridge.ReactApplicationContext
6
7
  import com.facebook.react.bridge.ReadableMap
8
+ import com.facebook.react.bridge.WritableMap
9
+ import com.facebook.react.modules.core.DeviceEventManagerModule
10
+ import com.ezoic.ads.sdk.adunits.EzoicInterstitialAd
11
+ import com.ezoic.ads.sdk.adunits.EzoicInterstitialAdListener
12
+ import com.ezoic.ads.sdk.adunits.EzoicInterstitialAdListenerAdapter
13
+ import com.ezoic.ads.sdk.adunits.EzoicReward
14
+ import com.ezoic.ads.sdk.adunits.EzoicRewardedAd
15
+ import com.ezoic.ads.sdk.adunits.EzoicRewardedAdListener
16
+ import com.ezoic.ads.sdk.adunits.EzoicRewardedAdListenerAdapter
7
17
  import com.ezoic.ads.sdk.core.EzoicAds
8
18
  import com.ezoic.ads.sdk.core.EzoicConfiguration
19
+ import com.ezoic.ads.sdk.core.EzoicError
20
+ import java.util.concurrent.ConcurrentHashMap
9
21
 
10
22
  class EzoicAdsModule(reactContext: ReactApplicationContext) :
11
23
  NativeEzoicAdsSpec(reactContext) {
12
24
 
13
25
  override fun getName() = NAME
14
26
 
27
+ /** Loaded rewarded ads awaiting `show`, keyed by ad unit id. */
28
+ private val rewardedAds = ConcurrentHashMap<Int, EzoicRewardedAd>()
29
+
30
+ /** In-flight `show` calls, keyed by ad unit id. */
31
+ private val pendingShows = ConcurrentHashMap<Int, RewardShow>()
32
+
33
+ private class RewardShow(val promise: Promise) {
34
+ @Volatile var settled = false
35
+ @Volatile var reward: EzoicReward? = null
36
+ }
37
+
38
+ /** Loaded interstitial ads awaiting `show`, keyed by ad unit id. */
39
+ private val interstitialAds = ConcurrentHashMap<Int, EzoicInterstitialAd>()
40
+
41
+ /** In-flight interstitial `show` calls, keyed by ad unit id. */
42
+ private val pendingInterstitialShows = ConcurrentHashMap<Int, InterstitialShow>()
43
+
44
+ private class InterstitialShow(val promise: Promise) {
45
+ @Volatile var settled = false
46
+ }
47
+
48
+ /** Ad unit ids with an in-flight rewarded `load`. */
49
+ private val loadingRewarded = ConcurrentHashMap.newKeySet<Int>()
50
+
51
+ /** Ad unit ids with an in-flight interstitial `load`. */
52
+ private val loadingInterstitial = ConcurrentHashMap.newKeySet<Int>()
53
+
15
54
  override fun initialize(config: ReadableMap, promise: Promise) {
16
55
  val domain = if (config.hasKey("domain")) config.getString("domain") else null
17
56
  if (domain.isNullOrEmpty()) {
@@ -53,10 +92,260 @@ class EzoicAdsModule(reactContext: ReactApplicationContext) :
53
92
  EzoicAds.instance.trackPageview { success -> promise.resolve(success) }
54
93
  }
55
94
 
95
+ override fun loadRewardedAd(adUnitIdentifier: String, promise: Promise) {
96
+ val id = adUnitIdentifier.toIntOrNull()
97
+ if (id == null) {
98
+ promise.reject("EzoicAds", "Invalid adUnitIdentifier: $adUnitIdentifier")
99
+ return
100
+ }
101
+ if (rewardedAds.containsKey(id) || !loadingRewarded.add(id)) {
102
+ promise.reject("EzoicAds", "An ad is already loaded/loading for ad unit $adUnitIdentifier")
103
+ return
104
+ }
105
+ EzoicRewardedAd.load(reactApplicationContext, id) { result ->
106
+ loadingRewarded.remove(id)
107
+ result.onSuccess { ad ->
108
+ ad.listener = makeListener(adUnitIdentifier)
109
+ rewardedAds[id] = ad
110
+ promise.resolve(null)
111
+ }.onFailure { e ->
112
+ promise.reject("EzoicAds", e.message ?: "Rewarded ad failed to load", e)
113
+ }
114
+ }
115
+ }
116
+
117
+ override fun showRewardedAd(adUnitIdentifier: String, promise: Promise) {
118
+ val id = adUnitIdentifier.toIntOrNull()
119
+ val ad = if (id != null) rewardedAds[id] else null
120
+ if (id == null || ad == null) {
121
+ promise.reject("EzoicAds", "Rewarded ad not loaded for $adUnitIdentifier")
122
+ return
123
+ }
124
+ if (pendingShows.containsKey(id)) {
125
+ promise.reject("EzoicAds", "A show is already in progress for ad unit $adUnitIdentifier")
126
+ return
127
+ }
128
+ val activity = currentActivity
129
+ if (activity == null) {
130
+ promise.reject("EzoicAds", "No current Activity to present the rewarded ad")
131
+ return
132
+ }
133
+
134
+ val show = RewardShow(promise)
135
+ pendingShows[id] = show
136
+
137
+ // Replace the load-time listener with one that also settles the promise on
138
+ // terminal events (dismiss = resolve, failed-to-show = reject).
139
+ ad.listener = makeListener(
140
+ adUnitIdentifier,
141
+ onDismiss = {
142
+ rewardedAds.remove(id)
143
+ val pending = pendingShows.remove(id)
144
+ if (pending != null && !pending.settled) {
145
+ pending.settled = true
146
+ val reward = pending.reward
147
+ val map = Arguments.createMap()
148
+ map.putBoolean("earned", reward != null)
149
+ map.putString("type", reward?.type ?: "")
150
+ map.putDouble("amount", (reward?.amount ?: 0).toDouble())
151
+ pending.promise.resolve(map)
152
+ }
153
+ },
154
+ onFailedToShow = { message ->
155
+ rewardedAds.remove(id)
156
+ val pending = pendingShows.remove(id)
157
+ if (pending != null && !pending.settled) {
158
+ pending.settled = true
159
+ pending.promise.reject("EzoicAds", message)
160
+ }
161
+ }
162
+ )
163
+
164
+ activity.runOnUiThread {
165
+ ad.show(activity) { reward -> show.reward = reward }
166
+ }
167
+ }
168
+
169
+ override fun loadInterstitialAd(adUnitIdentifier: String, promise: Promise) {
170
+ val id = adUnitIdentifier.toIntOrNull()
171
+ if (id == null) {
172
+ promise.reject("EzoicAds", "Invalid adUnitIdentifier: $adUnitIdentifier")
173
+ return
174
+ }
175
+ if (interstitialAds.containsKey(id) || !loadingInterstitial.add(id)) {
176
+ promise.reject("EzoicAds", "An ad is already loaded/loading for ad unit $adUnitIdentifier")
177
+ return
178
+ }
179
+ EzoicInterstitialAd.load(reactApplicationContext, id) { result ->
180
+ loadingInterstitial.remove(id)
181
+ result.onSuccess { ad ->
182
+ ad.listener = makeInterstitialListener(adUnitIdentifier)
183
+ interstitialAds[id] = ad
184
+ promise.resolve(null)
185
+ }.onFailure { e ->
186
+ promise.reject("EzoicAds", e.message ?: "Interstitial ad failed to load", e)
187
+ }
188
+ }
189
+ }
190
+
191
+ override fun showInterstitialAd(adUnitIdentifier: String, promise: Promise) {
192
+ val id = adUnitIdentifier.toIntOrNull()
193
+ val ad = if (id != null) interstitialAds[id] else null
194
+ if (id == null || ad == null) {
195
+ promise.reject("EzoicAds", "Interstitial ad not loaded for $adUnitIdentifier")
196
+ return
197
+ }
198
+ if (pendingInterstitialShows.containsKey(id)) {
199
+ promise.reject("EzoicAds", "A show is already in progress for ad unit $adUnitIdentifier")
200
+ return
201
+ }
202
+ val activity = currentActivity
203
+ if (activity == null) {
204
+ promise.reject("EzoicAds", "No current Activity to present the interstitial ad")
205
+ return
206
+ }
207
+
208
+ pendingInterstitialShows[id] = InterstitialShow(promise)
209
+
210
+ // Native show(activity) has no completion lambda, so replace the load-time
211
+ // listener with one that settles the promise on terminal events
212
+ // (dismiss = resolve, failed-to-show = reject).
213
+ ad.listener = makeInterstitialListener(
214
+ adUnitIdentifier,
215
+ onDismiss = {
216
+ interstitialAds.remove(id)
217
+ val pending = pendingInterstitialShows.remove(id)
218
+ if (pending != null && !pending.settled) {
219
+ pending.settled = true
220
+ pending.promise.resolve(null)
221
+ }
222
+ },
223
+ onFailedToShow = { message ->
224
+ interstitialAds.remove(id)
225
+ val pending = pendingInterstitialShows.remove(id)
226
+ if (pending != null && !pending.settled) {
227
+ pending.settled = true
228
+ pending.promise.reject("EzoicAds", message)
229
+ }
230
+ }
231
+ )
232
+
233
+ activity.runOnUiThread {
234
+ ad.show(activity)
235
+ }
236
+ }
237
+
238
+ override fun addListener(eventName: String) {
239
+ // No-op: required by the React Native NativeEventEmitter contract.
240
+ }
241
+
242
+ override fun removeListeners(count: Double) {
243
+ // No-op: required by the React Native NativeEventEmitter contract.
244
+ }
245
+
246
+ private fun makeListener(
247
+ adUnitIdentifier: String,
248
+ onDismiss: (() -> Unit)? = null,
249
+ onFailedToShow: ((String) -> Unit)? = null
250
+ ): EzoicRewardedAdListener = object : EzoicRewardedAdListenerAdapter() {
251
+ override fun onRewardedAdShown(rewardedAd: EzoicRewardedAd) {
252
+ emitRewardedEvent(adUnitIdentifier, "shown")
253
+ }
254
+
255
+ override fun onRewardedAdFailedToShow(rewardedAd: EzoicRewardedAd, error: EzoicError) {
256
+ emitRewardedEvent(adUnitIdentifier, "failedToShow") {
257
+ putString("message", error.message)
258
+ putInt("code", error.code)
259
+ }
260
+ onFailedToShow?.invoke(error.message)
261
+ }
262
+
263
+ override fun onRewardedAdImpression(rewardedAd: EzoicRewardedAd) {
264
+ emitRewardedEvent(adUnitIdentifier, "impression")
265
+ }
266
+
267
+ override fun onRewardedAdClicked(rewardedAd: EzoicRewardedAd) {
268
+ emitRewardedEvent(adUnitIdentifier, "clicked")
269
+ }
270
+
271
+ override fun onUserEarnedReward(rewardedAd: EzoicRewardedAd, reward: EzoicReward) {
272
+ emitRewardedEvent(adUnitIdentifier, "reward") {
273
+ putString("rewardType", reward.type)
274
+ putDouble("rewardAmount", reward.amount.toDouble())
275
+ }
276
+ }
277
+
278
+ override fun onRewardedAdDismissed(rewardedAd: EzoicRewardedAd) {
279
+ emitRewardedEvent(adUnitIdentifier, "dismissed")
280
+ onDismiss?.invoke()
281
+ }
282
+ }
283
+
284
+ private fun emitRewardedEvent(
285
+ adUnitIdentifier: String,
286
+ type: String,
287
+ extra: (WritableMap.() -> Unit)? = null
288
+ ) {
289
+ val map = Arguments.createMap()
290
+ map.putString("adUnitIdentifier", adUnitIdentifier)
291
+ map.putString("type", type)
292
+ extra?.invoke(map)
293
+ reactApplicationContext
294
+ .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
295
+ .emit(REWARDED_EVENT, map)
296
+ }
297
+
298
+ private fun makeInterstitialListener(
299
+ adUnitIdentifier: String,
300
+ onDismiss: (() -> Unit)? = null,
301
+ onFailedToShow: ((String) -> Unit)? = null
302
+ ): EzoicInterstitialAdListener = object : EzoicInterstitialAdListenerAdapter() {
303
+ override fun onInterstitialAdShown(interstitialAd: EzoicInterstitialAd) {
304
+ emitInterstitialEvent(adUnitIdentifier, "shown")
305
+ }
306
+
307
+ override fun onInterstitialAdFailedToShow(interstitialAd: EzoicInterstitialAd, error: EzoicError) {
308
+ emitInterstitialEvent(adUnitIdentifier, "failedToShow") {
309
+ putString("message", error.message)
310
+ putInt("code", error.code)
311
+ }
312
+ onFailedToShow?.invoke(error.message)
313
+ }
314
+
315
+ override fun onInterstitialAdImpression(interstitialAd: EzoicInterstitialAd) {
316
+ emitInterstitialEvent(adUnitIdentifier, "impression")
317
+ }
318
+
319
+ override fun onInterstitialAdClicked(interstitialAd: EzoicInterstitialAd) {
320
+ emitInterstitialEvent(adUnitIdentifier, "clicked")
321
+ }
322
+
323
+ override fun onInterstitialAdDismissed(interstitialAd: EzoicInterstitialAd) {
324
+ emitInterstitialEvent(adUnitIdentifier, "dismissed")
325
+ onDismiss?.invoke()
326
+ }
327
+ }
328
+
329
+ private fun emitInterstitialEvent(
330
+ adUnitIdentifier: String,
331
+ type: String,
332
+ extra: (WritableMap.() -> Unit)? = null
333
+ ) {
334
+ val map = Arguments.createMap()
335
+ map.putString("adUnitIdentifier", adUnitIdentifier)
336
+ map.putString("type", type)
337
+ extra?.invoke(map)
338
+ reactApplicationContext
339
+ .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
340
+ .emit(INTERSTITIAL_EVENT, map)
341
+ }
342
+
56
343
  private fun ReadableMap.optBool(key: String, default: Boolean): Boolean =
57
344
  if (hasKey(key) && !isNull(key)) getBoolean(key) else default
58
345
 
59
346
  companion object {
60
347
  const val NAME = NativeEzoicAdsSpec.NAME
348
+ private const val REWARDED_EVENT = "EzoicRewardedAdEvent"
349
+ private const val INTERSTITIAL_EVENT = "EzoicInterstitialAdEvent"
61
350
  }
62
351
  }
@@ -3,46 +3,310 @@ import EzoicAdsSDKBinary
3
3
 
4
4
  @objc public class EzoicAdsImpl: NSObject {
5
5
 
6
+ /// Set by the Obj-C module to forward rewarded lifecycle events to JS.
7
+ @objc public var eventEmitter: ((String, [String: Any]) -> Void)?
8
+
9
+ /// Loaded rewarded ads awaiting `show`, keyed by ad unit id.
10
+ private var rewardedAds: [Int: EzoicRewardedAd] = [:]
11
+
12
+ /// In-flight `show` calls, keyed by ad unit id.
13
+ private var pendingShows: [Int: PendingRewardShow] = [:]
14
+
15
+ private final class PendingRewardShow {
16
+ let resolve: (Any?) -> Void
17
+ let reject: (String, String, NSError?) -> Void
18
+ var reward: EzoicReward?
19
+ init(resolve: @escaping (Any?) -> Void, reject: @escaping (String, String, NSError?) -> Void) {
20
+ self.resolve = resolve
21
+ self.reject = reject
22
+ }
23
+ }
24
+
25
+ /// Loaded interstitial ads awaiting `show`, keyed by ad unit id.
26
+ private var interstitialAds: [Int: EzoicInterstitialAd] = [:]
27
+
28
+ /// In-flight interstitial `show` calls, keyed by ad unit id.
29
+ private var pendingInterstitialShows: [Int: PendingInterstitialShow] = [:]
30
+
31
+ private final class PendingInterstitialShow {
32
+ let resolve: (Any?) -> Void
33
+ let reject: (String, String, NSError?) -> Void
34
+ init(resolve: @escaping (Any?) -> Void, reject: @escaping (String, String, NSError?) -> Void) {
35
+ self.resolve = resolve
36
+ self.reject = reject
37
+ }
38
+ }
39
+
40
+ /// Ad unit ids with an in-flight rewarded `load`.
41
+ private var loadingRewarded: Set<Int> = []
42
+
43
+ /// Ad unit ids with an in-flight interstitial `load`.
44
+ private var loadingInterstitial: Set<Int> = []
45
+
46
+ /// Runs `work` on the main thread. The ad/pending/loading dictionaries and
47
+ /// every native load/show call touch UIKit and this shared state, so they must
48
+ /// only run on main. Delegate callbacks already arrive on main.
49
+ private func onMain(_ work: @escaping () -> Void) {
50
+ if Thread.isMainThread {
51
+ work()
52
+ } else {
53
+ DispatchQueue.main.async(execute: work)
54
+ }
55
+ }
56
+
6
57
  @objc public func initialize(_ config: NSDictionary,
7
58
  resolve: @escaping (Any?) -> Void,
8
59
  reject: @escaping (String, String, NSError?) -> Void) {
9
- guard let domain = config["domain"] as? String, !domain.isEmpty else {
10
- reject("EzoicAds", "initialize requires a non-empty `domain`.", nil)
11
- return
12
- }
13
- let configuration = EzoicConfiguration(
14
- domain: domain,
15
- autoReadConsent: (config["autoReadConsent"] as? Bool) ?? true,
16
- subjectToCOPPA: (config["subjectToCOPPA"] as? Bool) ?? false,
17
- requestATTBeforeAds: (config["requestATTBeforeAds"] as? Bool) ?? true,
18
- debugEnabled: (config["debugEnabled"] as? Bool) ?? false,
19
- testMode: (config["testMode"] as? Bool) ?? false
20
- )
21
- EzoicAds.shared.initialize(with: configuration) { result in
22
- switch result {
23
- case .success:
24
- resolve(nil)
25
- case .failure(let error):
26
- reject("EzoicAds", error.localizedDescription, error as NSError)
60
+ onMain {
61
+ guard let domain = config["domain"] as? String, !domain.isEmpty else {
62
+ reject("EzoicAds", "initialize requires a non-empty `domain`.", nil)
63
+ return
64
+ }
65
+ let configuration = EzoicConfiguration(
66
+ domain: domain,
67
+ autoReadConsent: (config["autoReadConsent"] as? Bool) ?? true,
68
+ subjectToCOPPA: (config["subjectToCOPPA"] as? Bool) ?? false,
69
+ requestATTBeforeAds: (config["requestATTBeforeAds"] as? Bool) ?? true,
70
+ debugEnabled: (config["debugEnabled"] as? Bool) ?? false,
71
+ testMode: (config["testMode"] as? Bool) ?? false
72
+ )
73
+ EzoicAds.shared.initialize(with: configuration) { result in
74
+ switch result {
75
+ case .success:
76
+ resolve(nil)
77
+ case .failure(let error):
78
+ reject("EzoicAds", error.localizedDescription, error as NSError)
79
+ }
27
80
  }
28
81
  }
29
82
  }
30
83
 
31
84
  @objc public func setGDPRConsent(_ applies: Bool, consentString: String?) {
32
- EzoicAds.shared.setGDPRConsent(applies: applies, consentString: consentString)
85
+ onMain {
86
+ EzoicAds.shared.setGDPRConsent(applies: applies, consentString: consentString)
87
+ }
33
88
  }
34
89
 
35
90
  @objc public func setGPPConsent(_ gppString: String?, sectionIds: String?) {
36
- EzoicAds.shared.setGPPConsent(gppString: gppString, sectionIds: sectionIds)
91
+ onMain {
92
+ EzoicAds.shared.setGPPConsent(gppString: gppString, sectionIds: sectionIds)
93
+ }
37
94
  }
38
95
 
39
96
  @objc public func setSubjectToCOPPA(_ value: Bool) {
40
- EzoicAds.shared.setSubjectToCOPPA(value)
97
+ onMain {
98
+ EzoicAds.shared.setSubjectToCOPPA(value)
99
+ }
41
100
  }
42
101
 
43
102
  @objc public func trackPageview(_ resolve: @escaping (Any?) -> Void) {
44
- EzoicAds.shared.trackPageview { success in
45
- resolve(NSNumber(value: success))
103
+ onMain {
104
+ EzoicAds.shared.trackPageview { success in
105
+ resolve(NSNumber(value: success))
106
+ }
107
+ }
108
+ }
109
+
110
+ @objc public func loadRewardedAd(_ adUnitIdentifier: String,
111
+ resolve: @escaping (Any?) -> Void,
112
+ reject: @escaping (String, String, NSError?) -> Void) {
113
+ onMain { [weak self] in
114
+ guard let self = self else { return }
115
+ guard let id = Int(adUnitIdentifier) else {
116
+ reject("EzoicAds", "Invalid adUnitIdentifier: \(adUnitIdentifier)", nil)
117
+ return
118
+ }
119
+ if self.rewardedAds[id] != nil || self.loadingRewarded.contains(id) {
120
+ reject("EzoicAds", "An ad is already loaded/loading for ad unit \(adUnitIdentifier)", nil)
121
+ return
122
+ }
123
+ self.loadingRewarded.insert(id)
124
+ EzoicRewardedAd.load(adUnitIdentifier: id) { [weak self] result in
125
+ guard let self = self else { return }
126
+ self.onMain {
127
+ self.loadingRewarded.remove(id)
128
+ switch result {
129
+ case .success(let ad):
130
+ ad.delegate = self
131
+ self.rewardedAds[id] = ad
132
+ resolve(nil)
133
+ case .failure(let error):
134
+ reject("EzoicAds", error.localizedDescription, error as NSError)
135
+ }
136
+ }
137
+ }
138
+ }
139
+ }
140
+
141
+ @objc public func showRewardedAd(_ adUnitIdentifier: String,
142
+ resolve: @escaping (Any?) -> Void,
143
+ reject: @escaping (String, String, NSError?) -> Void) {
144
+ onMain { [weak self] in
145
+ guard let self = self else { return }
146
+ guard let id = Int(adUnitIdentifier), let ad = self.rewardedAds[id] else {
147
+ reject("EzoicAds", "Rewarded ad not loaded for \(adUnitIdentifier)", nil)
148
+ return
149
+ }
150
+ if self.pendingShows[id] != nil {
151
+ reject("EzoicAds", "A show is already in progress for ad unit \(adUnitIdentifier)", nil)
152
+ return
153
+ }
154
+ self.pendingShows[id] = PendingRewardShow(resolve: resolve, reject: reject)
155
+ // Presenting from nil lets GMA use the application's top view controller.
156
+ ad.show(from: nil) { [weak self] reward in
157
+ self?.onMain { self?.pendingShows[id]?.reward = reward }
158
+ }
159
+ }
160
+ }
161
+
162
+ private func emit(_ ad: EzoicRewardedAd, _ type: String, _ extra: [String: Any] = [:]) {
163
+ var body: [String: Any] = [
164
+ "adUnitIdentifier": String(ad.adUnitIdentifier),
165
+ "type": type
166
+ ]
167
+ for (key, value) in extra { body[key] = value }
168
+ eventEmitter?("EzoicRewardedAdEvent", body)
169
+ }
170
+
171
+ @objc public func loadInterstitialAd(_ adUnitIdentifier: String,
172
+ resolve: @escaping (Any?) -> Void,
173
+ reject: @escaping (String, String, NSError?) -> Void) {
174
+ onMain { [weak self] in
175
+ guard let self = self else { return }
176
+ guard let id = Int(adUnitIdentifier) else {
177
+ reject("EzoicAds", "Invalid adUnitIdentifier: \(adUnitIdentifier)", nil)
178
+ return
179
+ }
180
+ if self.interstitialAds[id] != nil || self.loadingInterstitial.contains(id) {
181
+ reject("EzoicAds", "An ad is already loaded/loading for ad unit \(adUnitIdentifier)", nil)
182
+ return
183
+ }
184
+ self.loadingInterstitial.insert(id)
185
+ EzoicInterstitialAd.load(adUnitIdentifier: id) { [weak self] result in
186
+ guard let self = self else { return }
187
+ self.onMain {
188
+ self.loadingInterstitial.remove(id)
189
+ switch result {
190
+ case .success(let ad):
191
+ ad.delegate = self
192
+ self.interstitialAds[id] = ad
193
+ resolve(nil)
194
+ case .failure(let error):
195
+ reject("EzoicAds", error.localizedDescription, error as NSError)
196
+ }
197
+ }
198
+ }
199
+ }
200
+ }
201
+
202
+ @objc public func showInterstitialAd(_ adUnitIdentifier: String,
203
+ resolve: @escaping (Any?) -> Void,
204
+ reject: @escaping (String, String, NSError?) -> Void) {
205
+ onMain { [weak self] in
206
+ guard let self = self else { return }
207
+ guard let id = Int(adUnitIdentifier), let ad = self.interstitialAds[id] else {
208
+ reject("EzoicAds", "Interstitial ad not loaded for \(adUnitIdentifier)", nil)
209
+ return
210
+ }
211
+ if self.pendingInterstitialShows[id] != nil {
212
+ reject("EzoicAds", "A show is already in progress for ad unit \(adUnitIdentifier)", nil)
213
+ return
214
+ }
215
+ self.pendingInterstitialShows[id] = PendingInterstitialShow(resolve: resolve, reject: reject)
216
+ // Native show(from:) has no completion handler, so the show promise is
217
+ // settled from the delegate (dismiss = resolve, failed-to-present = reject).
218
+ // Presenting from nil lets GMA use the application's top view controller.
219
+ ad.show(from: nil)
220
+ }
221
+ }
222
+
223
+ private func emitInterstitial(_ ad: EzoicInterstitialAd, _ type: String, _ extra: [String: Any] = [:]) {
224
+ var body: [String: Any] = [
225
+ "adUnitIdentifier": String(ad.adUnitIdentifier),
226
+ "type": type
227
+ ]
228
+ for (key, value) in extra { body[key] = value }
229
+ eventEmitter?("EzoicInterstitialAdEvent", body)
230
+ }
231
+ }
232
+
233
+ // MARK: - EzoicRewardedAdDelegate
234
+
235
+ extension EzoicAdsImpl: EzoicRewardedAdDelegate {
236
+
237
+ public func rewardedAdDidPresent(_ rewardedAd: EzoicRewardedAd) {
238
+ emit(rewardedAd, "shown")
239
+ }
240
+
241
+ public func rewardedAd(_ rewardedAd: EzoicRewardedAd, didFailToPresentWithError error: EzoicError) {
242
+ emit(rewardedAd, "failedToShow", ["message": error.localizedDescription, "code": error.code])
243
+ let id = rewardedAd.adUnitIdentifier
244
+ rewardedAds.removeValue(forKey: id)
245
+ if let pending = pendingShows.removeValue(forKey: id) {
246
+ pending.reject("EzoicAds", error.localizedDescription, error as NSError)
247
+ }
248
+ }
249
+
250
+ public func rewardedAdDidRecordImpression(_ rewardedAd: EzoicRewardedAd) {
251
+ emit(rewardedAd, "impression")
252
+ }
253
+
254
+ public func rewardedAdDidRecordClick(_ rewardedAd: EzoicRewardedAd) {
255
+ emit(rewardedAd, "clicked")
256
+ }
257
+
258
+ public func rewardedAd(_ rewardedAd: EzoicRewardedAd, userDidEarn reward: EzoicReward) {
259
+ emit(rewardedAd, "reward", ["rewardType": reward.type, "rewardAmount": reward.amount])
260
+ pendingShows[rewardedAd.adUnitIdentifier]?.reward = reward
261
+ }
262
+
263
+ public func rewardedAdDidDismiss(_ rewardedAd: EzoicRewardedAd) {
264
+ emit(rewardedAd, "dismissed")
265
+ let id = rewardedAd.adUnitIdentifier
266
+ rewardedAds.removeValue(forKey: id)
267
+ if let pending = pendingShows.removeValue(forKey: id) {
268
+ let reward = pending.reward
269
+ let result: [String: Any] = [
270
+ "earned": reward != nil,
271
+ "type": reward?.type ?? "",
272
+ "amount": reward?.amount ?? 0
273
+ ]
274
+ pending.resolve(result)
275
+ }
276
+ }
277
+ }
278
+
279
+ // MARK: - EzoicInterstitialAdDelegate
280
+
281
+ extension EzoicAdsImpl: EzoicInterstitialAdDelegate {
282
+
283
+ public func interstitialAdDidPresent(_ interstitialAd: EzoicInterstitialAd) {
284
+ emitInterstitial(interstitialAd, "shown")
285
+ }
286
+
287
+ public func interstitialAd(_ interstitialAd: EzoicInterstitialAd, didFailToPresentWithError error: EzoicError) {
288
+ emitInterstitial(interstitialAd, "failedToShow", ["message": error.localizedDescription, "code": error.code])
289
+ let id = interstitialAd.adUnitIdentifier
290
+ interstitialAds.removeValue(forKey: id)
291
+ if let pending = pendingInterstitialShows.removeValue(forKey: id) {
292
+ pending.reject("EzoicAds", error.localizedDescription, error as NSError)
293
+ }
294
+ }
295
+
296
+ public func interstitialAdDidRecordImpression(_ interstitialAd: EzoicInterstitialAd) {
297
+ emitInterstitial(interstitialAd, "impression")
298
+ }
299
+
300
+ public func interstitialAdDidRecordClick(_ interstitialAd: EzoicInterstitialAd) {
301
+ emitInterstitial(interstitialAd, "clicked")
302
+ }
303
+
304
+ public func interstitialAdDidDismiss(_ interstitialAd: EzoicInterstitialAd) {
305
+ emitInterstitial(interstitialAd, "dismissed")
306
+ let id = interstitialAd.adUnitIdentifier
307
+ interstitialAds.removeValue(forKey: id)
308
+ if let pending = pendingInterstitialShows.removeValue(forKey: id) {
309
+ pending.resolve(nil)
46
310
  }
47
311
  }
48
312
  }
@@ -1,5 +1,8 @@
1
1
  #import <EzoicReactNativeSdkSpec/EzoicReactNativeSdkSpec.h>
2
+ #import <React/RCTEventEmitter.h>
2
3
 
3
- @interface EzoicReactNativeSdk : NSObject <NativeEzoicAdsSpec>
4
+ // Subclasses RCTEventEmitter so the rewarded ad lifecycle can be surfaced to
5
+ // JS via NativeEventEmitter, while still vending the codegen'd TurboModule.
6
+ @interface EzoicReactNativeSdk : RCTEventEmitter <NativeEzoicAdsSpec>
4
7
 
5
8
  @end