@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.
- package/EzoicReactNativeSdk.podspec +4 -1
- package/README.md +26 -2
- package/android/build.gradle +7 -1
- package/android/src/main/java/com/ezoic/reactnative/EzoicAdsModule.kt +144 -0
- package/android/src/main/java/com/ezoic/reactnative/EzoicNativeAdViewManager.kt +314 -0
- package/android/src/main/java/com/ezoic/reactnative/EzoicReactNativeSdkPackage.kt +4 -1
- package/ios/EzoicAdsImpl.swift +200 -44
- package/ios/EzoicNativeAdHostView.swift +181 -0
- package/ios/EzoicNativeAdViewComponentView.mm +99 -0
- package/ios/EzoicReactNativeSdk.mm +18 -1
- package/lib/module/EzoicInterstitialAd.js +108 -0
- package/lib/module/EzoicInterstitialAd.js.map +1 -0
- package/lib/module/EzoicNativeAdViewNativeComponent.ts +22 -0
- package/lib/module/EzoicRewardedAd.js +4 -1
- package/lib/module/EzoicRewardedAd.js.map +1 -1
- package/lib/module/NativeEzoicAds.js.map +1 -1
- package/lib/module/helpers.js.map +1 -1
- package/lib/module/index.js +29 -0
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/src/EzoicInterstitialAd.d.ts +51 -0
- package/lib/typescript/src/EzoicInterstitialAd.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/EzoicRewardedAd.d.ts +1 -0
- package/lib/typescript/src/EzoicRewardedAd.d.ts.map +1 -1
- package/lib/typescript/src/NativeEzoicAds.d.ts +2 -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 +21 -0
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/EzoicInterstitialAd.ts +126 -0
- package/src/EzoicNativeAdViewNativeComponent.ts +22 -0
- package/src/EzoicRewardedAd.ts +8 -2
- package/src/NativeEzoicAds.ts +3 -1
- package/src/helpers.ts +1 -1
- package/src/index.tsx +51 -0
package/ios/EzoicAdsImpl.swift
CHANGED
|
@@ -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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
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
|
-
|
|
91
|
+
onMain {
|
|
92
|
+
EzoicAds.shared.setGPPConsent(gppString: gppString, sectionIds: sectionIds)
|
|
93
|
+
}
|
|
56
94
|
}
|
|
57
95
|
|
|
58
96
|
@objc public func setSubjectToCOPPA(_ value: Bool) {
|
|
59
|
-
|
|
97
|
+
onMain {
|
|
98
|
+
EzoicAds.shared.setSubjectToCOPPA(value)
|
|
99
|
+
}
|
|
60
100
|
}
|
|
61
101
|
|
|
62
102
|
@objc public func trackPageview(_ resolve: @escaping (Any?) -> Void) {
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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);
|