@ezoic/react-native-sdk 1.1.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/EzoicReactNativeSdk.podspec +4 -1
  2. package/README.md +26 -2
  3. package/android/build.gradle +7 -1
  4. package/android/src/main/java/com/ezoic/reactnative/EzoicAdsModule.kt +144 -0
  5. package/android/src/main/java/com/ezoic/reactnative/EzoicNativeAdViewManager.kt +314 -0
  6. package/android/src/main/java/com/ezoic/reactnative/EzoicReactNativeSdkPackage.kt +4 -1
  7. package/ios/EzoicAdsImpl.swift +200 -44
  8. package/ios/EzoicNativeAdHostView.swift +181 -0
  9. package/ios/EzoicNativeAdViewComponentView.mm +99 -0
  10. package/ios/EzoicReactNativeSdk.mm +18 -1
  11. package/lib/module/EzoicInterstitialAd.js +108 -0
  12. package/lib/module/EzoicInterstitialAd.js.map +1 -0
  13. package/lib/module/EzoicNativeAdViewNativeComponent.ts +22 -0
  14. package/lib/module/EzoicRewardedAd.js +4 -1
  15. package/lib/module/EzoicRewardedAd.js.map +1 -1
  16. package/lib/module/NativeEzoicAds.js.map +1 -1
  17. package/lib/module/helpers.js.map +1 -1
  18. package/lib/module/index.js +29 -0
  19. package/lib/module/index.js.map +1 -1
  20. package/lib/typescript/src/EzoicInterstitialAd.d.ts +51 -0
  21. package/lib/typescript/src/EzoicInterstitialAd.d.ts.map +1 -0
  22. package/lib/typescript/src/EzoicNativeAdViewNativeComponent.d.ts +18 -0
  23. package/lib/typescript/src/EzoicNativeAdViewNativeComponent.d.ts.map +1 -0
  24. package/lib/typescript/src/EzoicRewardedAd.d.ts +1 -0
  25. package/lib/typescript/src/EzoicRewardedAd.d.ts.map +1 -1
  26. package/lib/typescript/src/NativeEzoicAds.d.ts +2 -0
  27. package/lib/typescript/src/NativeEzoicAds.d.ts.map +1 -1
  28. package/lib/typescript/src/helpers.d.ts +1 -1
  29. package/lib/typescript/src/helpers.d.ts.map +1 -1
  30. package/lib/typescript/src/index.d.ts +21 -0
  31. package/lib/typescript/src/index.d.ts.map +1 -1
  32. package/package.json +2 -2
  33. package/src/EzoicInterstitialAd.ts +126 -0
  34. package/src/EzoicNativeAdViewNativeComponent.ts +22 -0
  35. package/src/EzoicRewardedAd.ts +8 -2
  36. package/src/NativeEzoicAds.ts +3 -1
  37. package/src/helpers.ts +1 -1
  38. package/src/index.tsx +51 -0
@@ -22,65 +22,118 @@ import EzoicAdsSDKBinary
22
22
  }
23
23
  }
24
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
+
25
57
  @objc public func initialize(_ config: NSDictionary,
26
58
  resolve: @escaping (Any?) -> Void,
27
59
  reject: @escaping (String, String, NSError?) -> Void) {
28
- guard let domain = config["domain"] as? String, !domain.isEmpty else {
29
- reject("EzoicAds", "initialize requires a non-empty `domain`.", nil)
30
- return
31
- }
32
- let configuration = EzoicConfiguration(
33
- domain: domain,
34
- autoReadConsent: (config["autoReadConsent"] as? Bool) ?? true,
35
- subjectToCOPPA: (config["subjectToCOPPA"] as? Bool) ?? false,
36
- requestATTBeforeAds: (config["requestATTBeforeAds"] as? Bool) ?? true,
37
- debugEnabled: (config["debugEnabled"] as? Bool) ?? false,
38
- testMode: (config["testMode"] as? Bool) ?? false
39
- )
40
- EzoicAds.shared.initialize(with: configuration) { result in
41
- switch result {
42
- case .success:
43
- resolve(nil)
44
- case .failure(let error):
45
- 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
+ }
46
80
  }
47
81
  }
48
82
  }
49
83
 
50
84
  @objc public func setGDPRConsent(_ applies: Bool, consentString: String?) {
51
- EzoicAds.shared.setGDPRConsent(applies: applies, consentString: consentString)
85
+ onMain {
86
+ EzoicAds.shared.setGDPRConsent(applies: applies, consentString: consentString)
87
+ }
52
88
  }
53
89
 
54
90
  @objc public func setGPPConsent(_ gppString: String?, sectionIds: String?) {
55
- EzoicAds.shared.setGPPConsent(gppString: gppString, sectionIds: sectionIds)
91
+ onMain {
92
+ EzoicAds.shared.setGPPConsent(gppString: gppString, sectionIds: sectionIds)
93
+ }
56
94
  }
57
95
 
58
96
  @objc public func setSubjectToCOPPA(_ value: Bool) {
59
- EzoicAds.shared.setSubjectToCOPPA(value)
97
+ onMain {
98
+ EzoicAds.shared.setSubjectToCOPPA(value)
99
+ }
60
100
  }
61
101
 
62
102
  @objc public func trackPageview(_ resolve: @escaping (Any?) -> Void) {
63
- EzoicAds.shared.trackPageview { success in
64
- resolve(NSNumber(value: success))
103
+ onMain {
104
+ EzoicAds.shared.trackPageview { success in
105
+ resolve(NSNumber(value: success))
106
+ }
65
107
  }
66
108
  }
67
109
 
68
110
  @objc public func loadRewardedAd(_ adUnitIdentifier: String,
69
111
  resolve: @escaping (Any?) -> Void,
70
112
  reject: @escaping (String, String, NSError?) -> Void) {
71
- guard let id = Int(adUnitIdentifier) else {
72
- reject("EzoicAds", "Invalid adUnitIdentifier: \(adUnitIdentifier)", nil)
73
- return
74
- }
75
- EzoicRewardedAd.load(adUnitIdentifier: id) { [weak self] result in
113
+ onMain { [weak self] in
76
114
  guard let self = self else { return }
77
- switch result {
78
- case .success(let ad):
79
- ad.delegate = self
80
- self.rewardedAds[id] = ad
81
- resolve(nil)
82
- case .failure(let error):
83
- reject("EzoicAds", error.localizedDescription, error as NSError)
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
+ }
84
137
  }
85
138
  }
86
139
  }
@@ -88,14 +141,21 @@ import EzoicAdsSDKBinary
88
141
  @objc public func showRewardedAd(_ adUnitIdentifier: String,
89
142
  resolve: @escaping (Any?) -> Void,
90
143
  reject: @escaping (String, String, NSError?) -> Void) {
91
- guard let id = Int(adUnitIdentifier), let ad = rewardedAds[id] else {
92
- reject("EzoicAds", "Rewarded ad not loaded for \(adUnitIdentifier)", nil)
93
- return
94
- }
95
- pendingShows[id] = PendingRewardShow(resolve: resolve, reject: reject)
96
- // Presenting from nil lets GMA use the application's top view controller.
97
- ad.show(from: nil) { [weak self] reward in
98
- self?.pendingShows[id]?.reward = reward
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
+ }
99
159
  }
100
160
  }
101
161
 
@@ -107,6 +167,67 @@ import EzoicAdsSDKBinary
107
167
  for (key, value) in extra { body[key] = value }
108
168
  eventEmitter?("EzoicRewardedAdEvent", body)
109
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
+ }
110
231
  }
111
232
 
112
233
  // MARK: - EzoicRewardedAdDelegate
@@ -118,7 +239,7 @@ extension EzoicAdsImpl: EzoicRewardedAdDelegate {
118
239
  }
119
240
 
120
241
  public func rewardedAd(_ rewardedAd: EzoicRewardedAd, didFailToPresentWithError error: EzoicError) {
121
- emit(rewardedAd, "failedToShow", ["message": error.localizedDescription])
242
+ emit(rewardedAd, "failedToShow", ["message": error.localizedDescription, "code": error.code])
122
243
  let id = rewardedAd.adUnitIdentifier
123
244
  rewardedAds.removeValue(forKey: id)
124
245
  if let pending = pendingShows.removeValue(forKey: id) {
@@ -154,3 +275,38 @@ extension EzoicAdsImpl: EzoicRewardedAdDelegate {
154
275
  }
155
276
  }
156
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)
310
+ }
311
+ }
312
+ }
@@ -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
@@ -2,6 +2,7 @@
2
2
  #import <EzoicReactNativeSdk/EzoicReactNativeSdk-Swift.h>
3
3
 
4
4
  static NSString *const kEzoicRewardedEvent = @"EzoicRewardedAdEvent";
5
+ static NSString *const kEzoicInterstitialEvent = @"EzoicInterstitialAdEvent";
5
6
 
6
7
  @implementation EzoicReactNativeSdk {
7
8
  EzoicAdsImpl *_impl;
@@ -30,7 +31,7 @@ static NSString *const kEzoicRewardedEvent = @"EzoicRewardedAdEvent";
30
31
  }
31
32
 
32
33
  - (NSArray<NSString *> *)supportedEvents {
33
- return @[ kEzoicRewardedEvent ];
34
+ return @[ kEzoicRewardedEvent, kEzoicInterstitialEvent ];
34
35
  }
35
36
 
36
37
  - (void)startObserving {
@@ -88,6 +89,22 @@ static NSString *const kEzoicRewardedEvent = @"EzoicRewardedAdEvent";
88
89
  reject:^(NSString *code, NSString *msg, NSError *_Nullable e) { reject(code, msg, e); }];
89
90
  }
90
91
 
92
+ - (void)loadInterstitialAd:(NSString *)adUnitIdentifier
93
+ resolve:(RCTPromiseResolveBlock)resolve
94
+ reject:(RCTPromiseRejectBlock)reject {
95
+ [_impl loadInterstitialAd:adUnitIdentifier
96
+ resolve:^(id _Nullable v) { resolve(v); }
97
+ reject:^(NSString *code, NSString *msg, NSError *_Nullable e) { reject(code, msg, e); }];
98
+ }
99
+
100
+ - (void)showInterstitialAd:(NSString *)adUnitIdentifier
101
+ resolve:(RCTPromiseResolveBlock)resolve
102
+ reject:(RCTPromiseRejectBlock)reject {
103
+ [_impl showInterstitialAd:adUnitIdentifier
104
+ resolve:^(id _Nullable v) { resolve(v); }
105
+ reject:^(NSString *code, NSString *msg, NSError *_Nullable e) { reject(code, msg, e); }];
106
+ }
107
+
91
108
  - (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
92
109
  (const facebook::react::ObjCTurboModule::InitParams &)params {
93
110
  return std::make_shared<facebook::react::NativeEzoicAdsSpecJSI>(params);