@sellwild/react-native-sdk 1.5.0 → 1.7.1
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/android/src/main/java/com/sellwild/rnsdk/RnPrebidServer.kt +36 -0
- package/android/src/main/java/com/sellwild/rnsdk/SellwildBannerViewManager.kt +57 -0
- package/android/src/main/java/com/sellwild/rnsdk/SellwildFeedViewManager.kt +83 -0
- package/ios/SellwildBannerViewManager.m +1 -0
- package/ios/SellwildBannerViewManager.swift +28 -0
- package/ios/SellwildFeedViewManager.m +3 -0
- package/ios/SellwildFeedViewManager.swift +56 -2
- package/package.json +1 -1
- package/src/SellwildBanner.tsx +12 -0
- package/src/SellwildFeed.tsx +57 -2
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
package com.sellwild.rnsdk
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.bridge.ReadableMap
|
|
4
|
+
import com.sellwild.sdk.PrebidServerConfig
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Marshals a bridged JS `prebidServer` object (React `ReadableMap`) into the
|
|
8
|
+
* native [PrebidServerConfig]. Lives in the RN module because `ReadableMap` is a
|
|
9
|
+
* React type. Mirrors the iOS bridge (`configFromMap`) so an RN partner's custom
|
|
10
|
+
* Prebid Server (accountId / endpoint / bidders / timeout) routes to the same
|
|
11
|
+
* auction endpoint on both platforms. Returns null when `accountId`/`endpoint`
|
|
12
|
+
* are absent, so `bootstrap()` falls back to `S2S_CONFIG` / the hosted default.
|
|
13
|
+
*/
|
|
14
|
+
internal object RnPrebidServer {
|
|
15
|
+
fun fromMap(map: ReadableMap?): PrebidServerConfig? {
|
|
16
|
+
if (map == null) return null
|
|
17
|
+
fun str(k: String) = if (map.hasKey(k) && !map.isNull(k)) map.getString(k) else null
|
|
18
|
+
val accountId = str("accountId") ?: return null
|
|
19
|
+
val endpoint = str("endpoint") ?: return null
|
|
20
|
+
val bidders = if (map.hasKey("bidders") && !map.isNull("bidders")) {
|
|
21
|
+
map.getArray("bidders")?.let { arr ->
|
|
22
|
+
(0 until arr.size()).mapNotNull { arr.getString(it) }
|
|
23
|
+
} ?: emptyList()
|
|
24
|
+
} else {
|
|
25
|
+
emptyList()
|
|
26
|
+
}
|
|
27
|
+
val timeout = if (map.hasKey("timeout") && !map.isNull("timeout")) map.getInt("timeout") else 1500
|
|
28
|
+
return PrebidServerConfig(
|
|
29
|
+
accountId = accountId,
|
|
30
|
+
endpoint = endpoint,
|
|
31
|
+
bidders = bidders,
|
|
32
|
+
timeout = timeout,
|
|
33
|
+
syncEndpoint = str("syncEndpoint"),
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -11,6 +11,8 @@ import com.sellwild.sdk.AdSize
|
|
|
11
11
|
import com.sellwild.sdk.SellwildAdStack
|
|
12
12
|
import com.sellwild.sdk.SellwildAdView
|
|
13
13
|
import com.sellwild.sdk.SellwildConfig
|
|
14
|
+
import com.sellwild.sdk.SellwildGrowthCodeConfig
|
|
15
|
+
import com.sellwild.sdk.SellwildLocalizedListingsConfig
|
|
14
16
|
import org.json.JSONObject
|
|
15
17
|
|
|
16
18
|
/**
|
|
@@ -77,6 +79,13 @@ class SellwildBannerViewManager : SimpleViewManager<SellwildAdView>() {
|
|
|
77
79
|
emit(reactContext, adView, "onAdImpression", payload)
|
|
78
80
|
}
|
|
79
81
|
|
|
82
|
+
override fun onHouseAdImpression(adView: SellwildAdView, zoneId: String) {
|
|
83
|
+
val payload = com.facebook.react.bridge.Arguments.createMap().apply {
|
|
84
|
+
putString("zoneId", zoneId)
|
|
85
|
+
}
|
|
86
|
+
emit(reactContext, adView, "onHouseAdImpression", payload)
|
|
87
|
+
}
|
|
88
|
+
|
|
80
89
|
override fun onAdClicked(adView: SellwildAdView) {
|
|
81
90
|
emit(reactContext, adView, "onAdClicked", null)
|
|
82
91
|
}
|
|
@@ -143,6 +152,10 @@ class SellwildBannerViewManager : SimpleViewManager<SellwildAdView>() {
|
|
|
143
152
|
}
|
|
144
153
|
|
|
145
154
|
override fun onDropViewInstance(view: SellwildAdView) {
|
|
155
|
+
// Tear down the native ad view on unmount so its GMA / Prebid banners are
|
|
156
|
+
// released deterministically. Without this the views leak until GC (iOS
|
|
157
|
+
// RN gets this for free via ARC deinit). Mirrors SellwildFeedView cleanup.
|
|
158
|
+
view.destroy()
|
|
146
159
|
pending.remove(view)
|
|
147
160
|
super.onDropViewInstance(view)
|
|
148
161
|
}
|
|
@@ -154,6 +167,7 @@ class SellwildBannerViewManager : SimpleViewManager<SellwildAdView>() {
|
|
|
154
167
|
return MapBuilder.builder<String, Any>()
|
|
155
168
|
.put("onAdLoaded", MapBuilder.of("registrationName", "onAdLoaded"))
|
|
156
169
|
.put("onAdImpression", MapBuilder.of("registrationName", "onAdImpression"))
|
|
170
|
+
.put("onHouseAdImpression", MapBuilder.of("registrationName", "onHouseAdImpression"))
|
|
157
171
|
.put("onAdClicked", MapBuilder.of("registrationName", "onAdClicked"))
|
|
158
172
|
.put("onAdFailed", MapBuilder.of("registrationName", "onAdFailed"))
|
|
159
173
|
.put("onAdResize", MapBuilder.of("registrationName", "onAdResize"))
|
|
@@ -209,6 +223,46 @@ class SellwildBannerViewManager : SimpleViewManager<SellwildAdView>() {
|
|
|
209
223
|
null
|
|
210
224
|
}
|
|
211
225
|
|
|
226
|
+
val growthCode = if (map.hasKey("growthCode") && !map.isNull("growthCode")) {
|
|
227
|
+
val gc = map.getMap("growthCode")!!
|
|
228
|
+
fun bool(k: String): Boolean? = if (gc.hasKey(k) && !gc.isNull(k)) gc.getBoolean(k) else null
|
|
229
|
+
fun str(k: String): String? = if (gc.hasKey(k) && !gc.isNull(k)) gc.getString(k) else null
|
|
230
|
+
fun int(k: String): Int? = if (gc.hasKey(k) && !gc.isNull(k)) gc.getInt(k) else null
|
|
231
|
+
SellwildGrowthCodeConfig(
|
|
232
|
+
enabled = bool("enabled"),
|
|
233
|
+
partnerId = str("partnerId"),
|
|
234
|
+
endpoint = str("endpoint"),
|
|
235
|
+
syncUrl = str("syncUrl"),
|
|
236
|
+
sendMaid = bool("sendMaid"),
|
|
237
|
+
ttlHours = int("ttlHours"),
|
|
238
|
+
)
|
|
239
|
+
} else {
|
|
240
|
+
null
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
val localizedListings = if (map.hasKey("localizedListings") && !map.isNull("localizedListings")) {
|
|
244
|
+
val ll = map.getMap("localizedListings")!!
|
|
245
|
+
fun bool(k: String): Boolean? = if (ll.hasKey(k) && !ll.isNull(k)) ll.getBoolean(k) else null
|
|
246
|
+
fun str(k: String): String? = if (ll.hasKey(k) && !ll.isNull(k)) ll.getString(k) else null
|
|
247
|
+
fun int(k: String): Int? = if (ll.hasKey(k) && !ll.isNull(k)) ll.getInt(k) else null
|
|
248
|
+
SellwildLocalizedListingsConfig(
|
|
249
|
+
enabled = bool("enabled"),
|
|
250
|
+
source = str("source"),
|
|
251
|
+
baseUrl = str("baseUrl"),
|
|
252
|
+
urlTemplate = str("urlTemplate"),
|
|
253
|
+
frequency = int("frequency"),
|
|
254
|
+
forceState = str("forceState"),
|
|
255
|
+
)
|
|
256
|
+
} else {
|
|
257
|
+
null
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Custom Prebid Server (S2S) config — mirror the iOS bridge so the
|
|
261
|
+
// same RN app auctions against the partner's PBS on both platforms.
|
|
262
|
+
val prebidServer = RnPrebidServer.fromMap(
|
|
263
|
+
if (map.hasKey("prebidServer") && !map.isNull("prebidServer")) map.getMap("prebidServer") else null
|
|
264
|
+
)
|
|
265
|
+
|
|
212
266
|
return SellwildConfig(
|
|
213
267
|
partnerCode = partnerCode ?: "",
|
|
214
268
|
appBundleId = appBundleId,
|
|
@@ -221,6 +275,9 @@ class SellwildBannerViewManager : SimpleViewManager<SellwildAdView>() {
|
|
|
221
275
|
adRefreshMaxMobile = adRefreshMaxMobile,
|
|
222
276
|
adRefreshIntervalMs = adRefreshIntervalMs,
|
|
223
277
|
remoteJson = remoteJson,
|
|
278
|
+
growthCode = growthCode,
|
|
279
|
+
localizedListings = localizedListings,
|
|
280
|
+
prebidServer = prebidServer,
|
|
224
281
|
)
|
|
225
282
|
}
|
|
226
283
|
}
|
|
@@ -11,7 +11,9 @@ import com.facebook.react.uimanager.annotations.ReactProp
|
|
|
11
11
|
import com.facebook.react.uimanager.events.RCTEventEmitter
|
|
12
12
|
import com.sellwild.sdk.SellwildConfig
|
|
13
13
|
import com.sellwild.sdk.SellwildFeedView
|
|
14
|
+
import com.sellwild.sdk.SellwildGrowthCodeConfig
|
|
14
15
|
import com.sellwild.sdk.SellwildListing
|
|
16
|
+
import com.sellwild.sdk.SellwildLocalizedListingsConfig
|
|
15
17
|
import com.sellwild.sdk.SellwildSDK
|
|
16
18
|
import org.json.JSONObject
|
|
17
19
|
|
|
@@ -67,6 +69,11 @@ class SellwildFeedViewManager : SimpleViewManager<SellwildFeedView>() {
|
|
|
67
69
|
emit(reactContext, view, "onAdImpression", payload)
|
|
68
70
|
}
|
|
69
71
|
|
|
72
|
+
override fun onHouseAdImpression(zoneId: String) {
|
|
73
|
+
val payload = Arguments.createMap().apply { putString("zoneId", zoneId) }
|
|
74
|
+
emit(reactContext, view, "onHouseAdImpression", payload)
|
|
75
|
+
}
|
|
76
|
+
|
|
70
77
|
override fun onAdClicked(zoneId: String) {
|
|
71
78
|
val payload = Arguments.createMap().apply { putString("zoneId", zoneId) }
|
|
72
79
|
emit(reactContext, view, "onAdClicked", payload)
|
|
@@ -80,6 +87,11 @@ class SellwildFeedViewManager : SimpleViewManager<SellwildFeedView>() {
|
|
|
80
87
|
val payload = Arguments.createMap().apply { putString("message", message) }
|
|
81
88
|
emit(reactContext, view, "onFeedError", payload)
|
|
82
89
|
}
|
|
90
|
+
|
|
91
|
+
override fun onContentHeightChanged(feedView: SellwildFeedView, heightDp: Int) {
|
|
92
|
+
val payload = Arguments.createMap().apply { putInt("height", heightDp) }
|
|
93
|
+
emit(reactContext, view, "onContentSizeChange", payload)
|
|
94
|
+
}
|
|
83
95
|
}
|
|
84
96
|
return view
|
|
85
97
|
}
|
|
@@ -89,6 +101,11 @@ class SellwildFeedViewManager : SimpleViewManager<SellwildFeedView>() {
|
|
|
89
101
|
pendingFor(view).config = value
|
|
90
102
|
}
|
|
91
103
|
|
|
104
|
+
@ReactProp(name = "scrollEnabled", defaultBoolean = true)
|
|
105
|
+
fun setScrollEnabled(view: SellwildFeedView, value: Boolean) {
|
|
106
|
+
view.scrollEnabled = value
|
|
107
|
+
}
|
|
108
|
+
|
|
92
109
|
override fun onAfterUpdateTransaction(view: SellwildFeedView) {
|
|
93
110
|
super.onAfterUpdateTransaction(view)
|
|
94
111
|
|
|
@@ -117,8 +134,10 @@ class SellwildFeedViewManager : SimpleViewManager<SellwildFeedView>() {
|
|
|
117
134
|
.put("onFeedLoaded", MapBuilder.of("registrationName", "onFeedLoaded"))
|
|
118
135
|
.put("onListingTap", MapBuilder.of("registrationName", "onListingTap"))
|
|
119
136
|
.put("onAdImpression", MapBuilder.of("registrationName", "onAdImpression"))
|
|
137
|
+
.put("onHouseAdImpression", MapBuilder.of("registrationName", "onHouseAdImpression"))
|
|
120
138
|
.put("onAdClicked", MapBuilder.of("registrationName", "onAdClicked"))
|
|
121
139
|
.put("onFeedError", MapBuilder.of("registrationName", "onFeedError"))
|
|
140
|
+
.put("onContentSizeChange", MapBuilder.of("registrationName", "onContentSizeChange"))
|
|
122
141
|
.build()
|
|
123
142
|
}
|
|
124
143
|
|
|
@@ -168,6 +187,70 @@ class SellwildFeedViewManager : SimpleViewManager<SellwildFeedView>() {
|
|
|
168
187
|
if (map.hasKey("adRefreshMaxMobile")) config = config.copy(adRefreshMaxMobile = map.getInt("adRefreshMaxMobile"))
|
|
169
188
|
if (map.hasKey("adRefreshIntervalMs")) config = config.copy(adRefreshIntervalMs = map.getDouble("adRefreshIntervalMs").toLong())
|
|
170
189
|
|
|
190
|
+
// Flat identity/display/zone fields — mirror the iOS feed bridge so
|
|
191
|
+
// static buildConfig() and overrides land identically. SellwildSDK.apply
|
|
192
|
+
// (above) resolves these from `remote` when present; these overlay on top.
|
|
193
|
+
if (map.hasKey("slug")) config = config.copy(slug = map.getString("slug") ?: config.slug)
|
|
194
|
+
if (map.hasKey("listingsUrl")) config = config.copy(listingsUrl = map.getString("listingsUrl"))
|
|
195
|
+
if (map.hasKey("priceColor")) config = config.copy(priceColor = map.getString("priceColor") ?: config.priceColor)
|
|
196
|
+
if (map.hasKey("bannerZid")) config = config.copy(bannerZid = map.getString("bannerZid"))
|
|
197
|
+
if (map.hasKey("bottomBannerZid")) config = config.copy(bottomBannerZid = map.getString("bottomBannerZid"))
|
|
198
|
+
// mobileZids / mobileBannerZid are OS-suffix-resolved by SellwildSDK.apply
|
|
199
|
+
// when `remote` is present; only fall back to the flat (OS-agnostic) JS
|
|
200
|
+
// values when there was no remote payload to resolve from (parity w/ iOS).
|
|
201
|
+
if (!map.hasKey("remote") || map.isNull("remote")) {
|
|
202
|
+
if (map.hasKey("mobileBannerZid")) config = config.copy(mobileBannerZid = map.getString("mobileBannerZid"))
|
|
203
|
+
if (map.hasKey("mobileZids") && !map.isNull("mobileZids")) {
|
|
204
|
+
val arr = map.getArray("mobileZids")
|
|
205
|
+
if (arr != null) config = config.copy(mobileZids = (0 until arr.size()).mapNotNull { arr.getString(it) })
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Custom Prebid Server (S2S) config — mirror the iOS feed bridge.
|
|
210
|
+
if (map.hasKey("prebidServer") && !map.isNull("prebidServer")) {
|
|
211
|
+
config = config.copy(prebidServer = RnPrebidServer.fromMap(map.getMap("prebidServer")))
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Local GrowthCode override — parity with the banner bridge and the
|
|
215
|
+
// iOS feed bridge. Without it the feed's ad rows only see remote
|
|
216
|
+
// GROWTHCODE_* keys; a code-supplied partnerId never reaches the
|
|
217
|
+
// auction, so the identity sync never fires for feed ad rows.
|
|
218
|
+
if (map.hasKey("growthCode") && !map.isNull("growthCode")) {
|
|
219
|
+
val gc = map.getMap("growthCode")!!
|
|
220
|
+
fun bool(k: String): Boolean? = if (gc.hasKey(k) && !gc.isNull(k)) gc.getBoolean(k) else null
|
|
221
|
+
fun str(k: String): String? = if (gc.hasKey(k) && !gc.isNull(k)) gc.getString(k) else null
|
|
222
|
+
fun int(k: String): Int? = if (gc.hasKey(k) && !gc.isNull(k)) gc.getInt(k) else null
|
|
223
|
+
config = config.copy(
|
|
224
|
+
growthCode = SellwildGrowthCodeConfig(
|
|
225
|
+
enabled = bool("enabled"),
|
|
226
|
+
partnerId = str("partnerId"),
|
|
227
|
+
endpoint = str("endpoint"),
|
|
228
|
+
syncUrl = str("syncUrl"),
|
|
229
|
+
sendMaid = bool("sendMaid"),
|
|
230
|
+
ttlHours = int("ttlHours"),
|
|
231
|
+
),
|
|
232
|
+
)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Local override for the localized (geo-based) secondary-listings
|
|
236
|
+
// integration; the remote LOCALIZED_LISTINGS object rides `remote`.
|
|
237
|
+
if (map.hasKey("localizedListings") && !map.isNull("localizedListings")) {
|
|
238
|
+
val ll = map.getMap("localizedListings")!!
|
|
239
|
+
fun bool(k: String): Boolean? = if (ll.hasKey(k) && !ll.isNull(k)) ll.getBoolean(k) else null
|
|
240
|
+
fun str(k: String): String? = if (ll.hasKey(k) && !ll.isNull(k)) ll.getString(k) else null
|
|
241
|
+
fun int(k: String): Int? = if (ll.hasKey(k) && !ll.isNull(k)) ll.getInt(k) else null
|
|
242
|
+
config = config.copy(
|
|
243
|
+
localizedListings = SellwildLocalizedListingsConfig(
|
|
244
|
+
enabled = bool("enabled"),
|
|
245
|
+
source = str("source"),
|
|
246
|
+
baseUrl = str("baseUrl"),
|
|
247
|
+
urlTemplate = str("urlTemplate"),
|
|
248
|
+
frequency = int("frequency"),
|
|
249
|
+
forceState = str("forceState"),
|
|
250
|
+
),
|
|
251
|
+
)
|
|
252
|
+
}
|
|
253
|
+
|
|
171
254
|
return config
|
|
172
255
|
}
|
|
173
256
|
|
|
@@ -19,6 +19,7 @@ RCT_EXPORT_VIEW_PROPERTY(adStack, NSString)
|
|
|
19
19
|
// Direct events emitted to JS callbacks.
|
|
20
20
|
RCT_EXPORT_VIEW_PROPERTY(onAdLoaded, RCTDirectEventBlock)
|
|
21
21
|
RCT_EXPORT_VIEW_PROPERTY(onAdImpression, RCTDirectEventBlock)
|
|
22
|
+
RCT_EXPORT_VIEW_PROPERTY(onHouseAdImpression, RCTDirectEventBlock)
|
|
22
23
|
RCT_EXPORT_VIEW_PROPERTY(onAdClicked, RCTDirectEventBlock)
|
|
23
24
|
RCT_EXPORT_VIEW_PROPERTY(onAdFailed, RCTDirectEventBlock)
|
|
24
25
|
RCT_EXPORT_VIEW_PROPERTY(onAdResize, RCTDirectEventBlock)
|
|
@@ -47,6 +47,8 @@ final class SellwildBannerHostView: UIView, SellwildAdViewDelegate {
|
|
|
47
47
|
|
|
48
48
|
@objc var onAdLoaded: RCTDirectEventBlock?
|
|
49
49
|
@objc var onAdImpression: RCTDirectEventBlock?
|
|
50
|
+
/// A house ad backfilled an empty slot (no-fill). Not a paid impression.
|
|
51
|
+
@objc var onHouseAdImpression: RCTDirectEventBlock?
|
|
50
52
|
@objc var onAdClicked: RCTDirectEventBlock?
|
|
51
53
|
@objc var onAdFailed: RCTDirectEventBlock?
|
|
52
54
|
/// Emitted with the rendered creative size so JS can resize the slot.
|
|
@@ -124,6 +126,10 @@ final class SellwildBannerHostView: UIView, SellwildAdViewDelegate {
|
|
|
124
126
|
onAdImpression?(["zoneId": zoneId])
|
|
125
127
|
}
|
|
126
128
|
|
|
129
|
+
func sellwildAdView(_ adView: SellwildAdView, didRecordHouseImpressionForZoneId zoneId: String) {
|
|
130
|
+
onHouseAdImpression?(["zoneId": zoneId])
|
|
131
|
+
}
|
|
132
|
+
|
|
127
133
|
func sellwildAdViewDidRecordClick(_ adView: SellwildAdView) {
|
|
128
134
|
onAdClicked?([:])
|
|
129
135
|
}
|
|
@@ -175,6 +181,28 @@ final class SellwildBannerHostView: UIView, SellwildAdViewDelegate {
|
|
|
175
181
|
)
|
|
176
182
|
}
|
|
177
183
|
|
|
184
|
+
if let gc = map["growthCode"] as? NSDictionary {
|
|
185
|
+
cfg.growthCode = SellwildGrowthCodeConfig(
|
|
186
|
+
enabled: gc["enabled"] as? Bool,
|
|
187
|
+
partnerId: gc["partnerId"] as? String,
|
|
188
|
+
endpoint: gc["endpoint"] as? String,
|
|
189
|
+
syncUrl: gc["syncUrl"] as? String,
|
|
190
|
+
sendMaid: gc["sendMaid"] as? Bool,
|
|
191
|
+
ttlHours: gc["ttlHours"] as? Int
|
|
192
|
+
)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if let ll = map["localizedListings"] as? NSDictionary {
|
|
196
|
+
cfg.localizedListings = SellwildLocalizedListingsConfig(
|
|
197
|
+
enabled: ll["enabled"] as? Bool,
|
|
198
|
+
source: ll["source"] as? String,
|
|
199
|
+
baseUrl: ll["baseUrl"] as? String,
|
|
200
|
+
urlTemplate: ll["urlTemplate"] as? String,
|
|
201
|
+
frequency: ll["frequency"] as? Int,
|
|
202
|
+
forceState: ll["forceState"] as? String
|
|
203
|
+
)
|
|
204
|
+
}
|
|
205
|
+
|
|
178
206
|
if let remote = map["remote"] as? NSDictionary,
|
|
179
207
|
let data = try? JSONSerialization.data(withJSONObject: remote, options: []) {
|
|
180
208
|
cfg.remoteJSON = data
|
|
@@ -12,12 +12,15 @@
|
|
|
12
12
|
// Props set from JS — these forward to @objc properties on
|
|
13
13
|
// SellwildFeedHostView.
|
|
14
14
|
RCT_EXPORT_VIEW_PROPERTY(config, NSDictionary)
|
|
15
|
+
RCT_EXPORT_VIEW_PROPERTY(scrollEnabled, BOOL)
|
|
15
16
|
|
|
16
17
|
// Direct events emitted to JS callbacks.
|
|
17
18
|
RCT_EXPORT_VIEW_PROPERTY(onFeedLoaded, RCTDirectEventBlock)
|
|
18
19
|
RCT_EXPORT_VIEW_PROPERTY(onListingTap, RCTDirectEventBlock)
|
|
19
20
|
RCT_EXPORT_VIEW_PROPERTY(onAdImpression, RCTDirectEventBlock)
|
|
21
|
+
RCT_EXPORT_VIEW_PROPERTY(onHouseAdImpression, RCTDirectEventBlock)
|
|
20
22
|
RCT_EXPORT_VIEW_PROPERTY(onAdClicked, RCTDirectEventBlock)
|
|
21
23
|
RCT_EXPORT_VIEW_PROPERTY(onFeedError, RCTDirectEventBlock)
|
|
24
|
+
RCT_EXPORT_VIEW_PROPERTY(onContentSizeChange, RCTDirectEventBlock)
|
|
22
25
|
|
|
23
26
|
@end
|
|
@@ -38,14 +38,22 @@ final class SellwildFeedHostView: UIView, SellwildFeedViewDelegate {
|
|
|
38
38
|
// MARK: RN-set props
|
|
39
39
|
|
|
40
40
|
@objc var config: NSDictionary? { didSet { needsApply = true } }
|
|
41
|
+
/// Disable the feed's internal scrolling so it can be embedded inside a
|
|
42
|
+
/// parent scroll view. Applied live and on feed creation.
|
|
43
|
+
@objc var scrollEnabled: Bool = true { didSet { feedView?.scrollEnabled = scrollEnabled } }
|
|
41
44
|
|
|
42
45
|
// MARK: RN events
|
|
43
46
|
|
|
44
47
|
@objc var onFeedLoaded: RCTDirectEventBlock?
|
|
45
48
|
@objc var onListingTap: RCTDirectEventBlock?
|
|
46
49
|
@objc var onAdImpression: RCTDirectEventBlock?
|
|
50
|
+
/// A house ad backfilled an empty feed slot (no-fill). Not a paid impression.
|
|
51
|
+
@objc var onHouseAdImpression: RCTDirectEventBlock?
|
|
47
52
|
@objc var onAdClicked: RCTDirectEventBlock?
|
|
48
53
|
@objc var onFeedError: RCTDirectEventBlock?
|
|
54
|
+
/// Emitted whenever the feed's content height changes so JS can size the
|
|
55
|
+
/// host container when embedding with `scrollEnabled={false}`.
|
|
56
|
+
@objc var onContentSizeChange: RCTDirectEventBlock?
|
|
49
57
|
|
|
50
58
|
// MARK: Internals
|
|
51
59
|
|
|
@@ -83,6 +91,7 @@ final class SellwildFeedHostView: UIView, SellwildFeedViewDelegate {
|
|
|
83
91
|
|
|
84
92
|
let view = SellwildFeedView(config: sellwildConfig)
|
|
85
93
|
view.delegate = self
|
|
94
|
+
view.scrollEnabled = scrollEnabled
|
|
86
95
|
view.translatesAutoresizingMaskIntoConstraints = false
|
|
87
96
|
addSubview(view)
|
|
88
97
|
NSLayoutConstraint.activate([
|
|
@@ -118,6 +127,10 @@ final class SellwildFeedHostView: UIView, SellwildFeedViewDelegate {
|
|
|
118
127
|
onAdImpression?(["zoneId": zoneId])
|
|
119
128
|
}
|
|
120
129
|
|
|
130
|
+
func sellwildFeed(_ feed: SellwildFeedView, didRecordHouseAdImpressionForZoneId zoneId: String) {
|
|
131
|
+
onHouseAdImpression?(["zoneId": zoneId])
|
|
132
|
+
}
|
|
133
|
+
|
|
121
134
|
func sellwildFeed(_ feed: SellwildFeedView, didRecordAdClickForZoneId zoneId: String) {
|
|
122
135
|
onAdClicked?(["zoneId": zoneId])
|
|
123
136
|
}
|
|
@@ -126,6 +139,10 @@ final class SellwildFeedHostView: UIView, SellwildFeedViewDelegate {
|
|
|
126
139
|
onFeedError?(["message": message])
|
|
127
140
|
}
|
|
128
141
|
|
|
142
|
+
func sellwildFeed(_ feed: SellwildFeedView, didChangeContentHeight height: CGFloat) {
|
|
143
|
+
onContentSizeChange?(["height": height])
|
|
144
|
+
}
|
|
145
|
+
|
|
129
146
|
// MARK: Config marshalling
|
|
130
147
|
|
|
131
148
|
/// Build a full SellwildConfig from a JS dictionary. The JS side
|
|
@@ -159,8 +176,17 @@ final class SellwildFeedHostView: UIView, SellwildFeedViewDelegate {
|
|
|
159
176
|
if let v = map["priceColor"] as? String { cfg.priceColor = v }
|
|
160
177
|
if let v = map["bannerZid"] as? String { cfg.bannerZid = v }
|
|
161
178
|
if let v = map["bottomBannerZid"] as? String { cfg.bottomBannerZid = v }
|
|
162
|
-
|
|
163
|
-
|
|
179
|
+
|
|
180
|
+
// mobileZids / mobileBannerZid are resolved per-platform (iOS here) by
|
|
181
|
+
// SellwildSDK.apply(_:to:) above, which reads the OS-suffixed CDN keys
|
|
182
|
+
// (MOBILE_ZID_IOS / MOBILE_BANNER_ZID_IOS) out of `remote`. Only fall
|
|
183
|
+
// back to the flat JS-passed values when there was no `remote` payload
|
|
184
|
+
// to resolve from — otherwise the iOS-resolved zones would be clobbered
|
|
185
|
+
// by the unsuffixed values the JS core mapped (which are OS-agnostic).
|
|
186
|
+
if map["remote"] == nil {
|
|
187
|
+
if let v = map["mobileBannerZid"] as? String { cfg.mobileBannerZid = v }
|
|
188
|
+
if let v = map["mobileZids"] as? [String] { cfg.mobileZids = v }
|
|
189
|
+
}
|
|
164
190
|
|
|
165
191
|
// JS bridge passes ms; iOS API is seconds (TimeInterval).
|
|
166
192
|
if let v = map["adRefreshIntervalMs"] as? NSNumber {
|
|
@@ -181,6 +207,34 @@ final class SellwildFeedHostView: UIView, SellwildFeedViewDelegate {
|
|
|
181
207
|
)
|
|
182
208
|
}
|
|
183
209
|
|
|
210
|
+
// Local GrowthCode override — parity with the banner bridge. Without
|
|
211
|
+
// this the feed's ad rows only see remote GROWTHCODE_* keys (via
|
|
212
|
+
// remoteJSON); a code-supplied partnerId never reaches the auction, so
|
|
213
|
+
// the identity sync never fires and feed ad rows lose eids enrichment.
|
|
214
|
+
if let gc = map["growthCode"] as? NSDictionary {
|
|
215
|
+
cfg.growthCode = SellwildGrowthCodeConfig(
|
|
216
|
+
enabled: gc["enabled"] as? Bool,
|
|
217
|
+
partnerId: gc["partnerId"] as? String,
|
|
218
|
+
endpoint: gc["endpoint"] as? String,
|
|
219
|
+
syncUrl: gc["syncUrl"] as? String,
|
|
220
|
+
sendMaid: gc["sendMaid"] as? Bool,
|
|
221
|
+
ttlHours: gc["ttlHours"] as? Int
|
|
222
|
+
)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Local override for the localized (geo-based) secondary-listings
|
|
226
|
+
// integration; remote LOCALIZED_LISTINGS rides `remote` verbatim.
|
|
227
|
+
if let ll = map["localizedListings"] as? NSDictionary {
|
|
228
|
+
cfg.localizedListings = SellwildLocalizedListingsConfig(
|
|
229
|
+
enabled: ll["enabled"] as? Bool,
|
|
230
|
+
source: ll["source"] as? String,
|
|
231
|
+
baseUrl: ll["baseUrl"] as? String,
|
|
232
|
+
urlTemplate: ll["urlTemplate"] as? String,
|
|
233
|
+
frequency: ll["frequency"] as? Int,
|
|
234
|
+
forceState: ll["forceState"] as? String
|
|
235
|
+
)
|
|
236
|
+
}
|
|
237
|
+
|
|
184
238
|
return cfg
|
|
185
239
|
}
|
|
186
240
|
|
package/package.json
CHANGED
package/src/SellwildBanner.tsx
CHANGED
|
@@ -62,6 +62,7 @@ interface NativeBannerProps {
|
|
|
62
62
|
style?: ViewStyle
|
|
63
63
|
onAdLoaded?: (e: NativeSyntheticEvent<{}>) => void
|
|
64
64
|
onAdImpression?: (e: NativeSyntheticEvent<{ zoneId: string }>) => void
|
|
65
|
+
onHouseAdImpression?: (e: NativeSyntheticEvent<{ zoneId: string }>) => void
|
|
65
66
|
onAdClicked?: (e: NativeSyntheticEvent<{}>) => void
|
|
66
67
|
onAdFailed?: (e: NativeSyntheticEvent<{ message: string }>) => void
|
|
67
68
|
onAdResize?: (e: NativeSyntheticEvent<{ width: number; height: number }>) => void
|
|
@@ -86,6 +87,11 @@ export interface SellwildBannerProps {
|
|
|
86
87
|
zoneId: number | string
|
|
87
88
|
style?: ViewStyle
|
|
88
89
|
onImpression?: () => void
|
|
90
|
+
/**
|
|
91
|
+
* Fired when a house ad backfilled an empty slot (a no-fill). NOT a paid
|
|
92
|
+
* impression — track it separately. See the `MOBILE_HOUSE_AD_*` config keys.
|
|
93
|
+
*/
|
|
94
|
+
onHouseImpression?: (zoneId: string) => void
|
|
89
95
|
onClick?: () => void
|
|
90
96
|
onError?: (error: Error) => void
|
|
91
97
|
}
|
|
@@ -96,6 +102,7 @@ export function SellwildBanner({
|
|
|
96
102
|
zoneId,
|
|
97
103
|
style,
|
|
98
104
|
onImpression,
|
|
105
|
+
onHouseImpression,
|
|
99
106
|
onClick,
|
|
100
107
|
onError,
|
|
101
108
|
}: SellwildBannerProps) {
|
|
@@ -167,6 +174,10 @@ export function SellwildBanner({
|
|
|
167
174
|
adRefreshMaxMobile: config.adRefreshMaxMobile,
|
|
168
175
|
adRefreshIntervalMs: config.adRefreshInterval,
|
|
169
176
|
prebidServer: config.prebidServer,
|
|
177
|
+
// Local GrowthCode overrides (remote GROWTHCODE_* keys ride `remote`).
|
|
178
|
+
growthCode: config.growthCode,
|
|
179
|
+
// Local localized-listings overrides (remote LOCALIZED_LISTINGS rides `remote`).
|
|
180
|
+
localizedListings: config.localizedListings,
|
|
170
181
|
remote: config.remote,
|
|
171
182
|
}
|
|
172
183
|
|
|
@@ -181,6 +192,7 @@ export function SellwildBanner({
|
|
|
181
192
|
// No-op event hook today; surfaced for future fill metrics.
|
|
182
193
|
}}
|
|
183
194
|
onAdImpression={() => onImpression?.()}
|
|
195
|
+
onHouseAdImpression={(e: NativeSyntheticEvent<{ zoneId: string }>) => onHouseImpression?.(e.nativeEvent?.zoneId)}
|
|
184
196
|
onAdClicked={() => onClick?.()}
|
|
185
197
|
onAdFailed={(e: NativeSyntheticEvent<{ message: string }>) => {
|
|
186
198
|
const msg = e.nativeEvent?.message ?? 'Ad failed'
|
package/src/SellwildFeed.tsx
CHANGED
|
@@ -29,12 +29,15 @@ const NATIVE_NAME = 'SellwildFeedView'
|
|
|
29
29
|
|
|
30
30
|
interface NativeFeedProps {
|
|
31
31
|
config: object
|
|
32
|
+
scrollEnabled?: boolean
|
|
32
33
|
style?: StyleProp<ViewStyle>
|
|
33
34
|
onFeedLoaded?: (e: NativeSyntheticEvent<{}>) => void
|
|
34
35
|
onListingTap?: (e: NativeSyntheticEvent<{ listing: SellwildListing }>) => void
|
|
35
36
|
onAdImpression?: (e: NativeSyntheticEvent<{ zoneId: string }>) => void
|
|
37
|
+
onHouseAdImpression?: (e: NativeSyntheticEvent<{ zoneId: string }>) => void
|
|
36
38
|
onAdClicked?: (e: NativeSyntheticEvent<{ zoneId: string }>) => void
|
|
37
39
|
onFeedError?: (e: NativeSyntheticEvent<{ message: string }>) => void
|
|
40
|
+
onContentSizeChange?: (e: NativeSyntheticEvent<{ width?: number; height: number }>) => void
|
|
38
41
|
}
|
|
39
42
|
|
|
40
43
|
const NativeFeed = (() => {
|
|
@@ -60,6 +63,24 @@ export interface SellwildFeedProps {
|
|
|
60
63
|
/** Optional style override. The feed expands to its container by default. */
|
|
61
64
|
style?: ViewStyle
|
|
62
65
|
|
|
66
|
+
/**
|
|
67
|
+
* When `false`, the feed's own scrolling is disabled and the component
|
|
68
|
+
* sizes its own container from the reported content height, so it can be
|
|
69
|
+
* embedded inside a parent `ScrollView` (single-scroll pages, e.g.
|
|
70
|
+
* alongside a Taboola feed). The feed then renders every row (no
|
|
71
|
+
* virtualization) and pull-to-refresh is disabled — the host drives
|
|
72
|
+
* refresh. Defaults to `true`; existing full-screen integrations are
|
|
73
|
+
* unaffected.
|
|
74
|
+
*/
|
|
75
|
+
scrollEnabled?: boolean
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Fired whenever the feed's content height changes. Use it to size the
|
|
79
|
+
* feed's container when embedding with `scrollEnabled={false}`; when
|
|
80
|
+
* scroll is disabled the component also applies the height itself.
|
|
81
|
+
*/
|
|
82
|
+
onContentSizeChange?: (e: { width?: number; height: number }) => void
|
|
83
|
+
|
|
63
84
|
/** Fired once after the initial listings fetch resolves successfully. */
|
|
64
85
|
onLoad?: () => void
|
|
65
86
|
|
|
@@ -73,6 +94,12 @@ export interface SellwildFeedProps {
|
|
|
73
94
|
/** Fired when a native ad row records an impression. */
|
|
74
95
|
onAdImpression?: (zoneId: string) => void
|
|
75
96
|
|
|
97
|
+
/**
|
|
98
|
+
* Fired when a house ad backfilled an empty ad row (a no-fill). NOT a paid
|
|
99
|
+
* impression — track it separately. See the `MOBILE_HOUSE_AD_*` config keys.
|
|
100
|
+
*/
|
|
101
|
+
onHouseAdImpression?: (zoneId: string) => void
|
|
102
|
+
|
|
76
103
|
/** Fired when a native ad row is clicked. */
|
|
77
104
|
onAdClicked?: (zoneId: string) => void
|
|
78
105
|
|
|
@@ -83,18 +110,30 @@ export interface SellwildFeedProps {
|
|
|
83
110
|
export function SellwildFeed({
|
|
84
111
|
config,
|
|
85
112
|
style,
|
|
113
|
+
scrollEnabled = true,
|
|
114
|
+
onContentSizeChange,
|
|
86
115
|
onLoad,
|
|
87
116
|
onListingTap,
|
|
88
117
|
onAdImpression,
|
|
118
|
+
onHouseAdImpression,
|
|
89
119
|
onAdClicked,
|
|
90
120
|
onError,
|
|
91
121
|
}: SellwildFeedProps) {
|
|
122
|
+
// When embedded (scrollEnabled === false) the parent ScrollView owns
|
|
123
|
+
// scrolling, so we size our own container from the native-reported content
|
|
124
|
+
// height rather than filling with flex:1.
|
|
125
|
+
const [contentHeight, setContentHeight] = React.useState<number | null>(null)
|
|
126
|
+
const embedded = scrollEnabled === false
|
|
127
|
+
const containerStyle: StyleProp<ViewStyle> = embedded
|
|
128
|
+
? [contentHeight != null ? { height: contentHeight } : undefined, style]
|
|
129
|
+
: [styles.fill, style]
|
|
130
|
+
|
|
92
131
|
if (!NativeFeed) {
|
|
93
132
|
// Native module not registered. Most common cause: the host app was
|
|
94
133
|
// built before the @sellwild/react-native-sdk autolink ran, or this
|
|
95
134
|
// is being rendered in a JS-only test environment.
|
|
96
135
|
return (
|
|
97
|
-
<View style={[
|
|
136
|
+
<View style={[containerStyle, __DEV__ ? styles.devPlaceholder : undefined]}>
|
|
98
137
|
{__DEV__ ? (
|
|
99
138
|
<Text style={styles.devText}>
|
|
100
139
|
Sellwild native feed not available on {Platform.OS} (yet)
|
|
@@ -125,6 +164,11 @@ export function SellwildFeed({
|
|
|
125
164
|
adRefreshMaxMobile: config.adRefreshMaxMobile,
|
|
126
165
|
adRefreshIntervalMs: config.adRefreshInterval,
|
|
127
166
|
prebidServer: config.prebidServer,
|
|
167
|
+
// Local localized-listings overrides (remote LOCALIZED_LISTINGS rides `remote`).
|
|
168
|
+
localizedListings: config.localizedListings,
|
|
169
|
+
// Local GrowthCode overrides (remote GROWTHCODE_* rides `remote`) — parity
|
|
170
|
+
// with SellwildBanner so the feed's native auctions honor local overrides too.
|
|
171
|
+
growthCode: config.growthCode,
|
|
128
172
|
remote: config.remote,
|
|
129
173
|
listingsUrl: config.listingsUrl,
|
|
130
174
|
priceColor: config.priceColor,
|
|
@@ -136,8 +180,16 @@ export function SellwildFeed({
|
|
|
136
180
|
|
|
137
181
|
return (
|
|
138
182
|
<NativeFeed
|
|
139
|
-
style={
|
|
183
|
+
style={containerStyle}
|
|
140
184
|
config={nativeConfig}
|
|
185
|
+
scrollEnabled={scrollEnabled}
|
|
186
|
+
onContentSizeChange={(e: NativeSyntheticEvent<{ width?: number; height: number }>) => {
|
|
187
|
+
const { width, height } = e.nativeEvent ?? { height: 0 }
|
|
188
|
+
// Only size our own container when embedded; a scrolling feed fills
|
|
189
|
+
// its parent via flex:1 and doesn't need a measured height.
|
|
190
|
+
if (embedded && height > 0) setContentHeight(height)
|
|
191
|
+
onContentSizeChange?.({ width, height })
|
|
192
|
+
}}
|
|
141
193
|
onFeedLoaded={() => onLoad?.()}
|
|
142
194
|
onListingTap={(e: NativeSyntheticEvent<{ listing: SellwildListing }>) => {
|
|
143
195
|
onListingTap?.(e.nativeEvent.listing)
|
|
@@ -145,6 +197,9 @@ export function SellwildFeed({
|
|
|
145
197
|
onAdImpression={(e: NativeSyntheticEvent<{ zoneId: string }>) => {
|
|
146
198
|
onAdImpression?.(e.nativeEvent.zoneId)
|
|
147
199
|
}}
|
|
200
|
+
onHouseAdImpression={(e: NativeSyntheticEvent<{ zoneId: string }>) => {
|
|
201
|
+
onHouseAdImpression?.(e.nativeEvent.zoneId)
|
|
202
|
+
}}
|
|
148
203
|
onAdClicked={(e: NativeSyntheticEvent<{ zoneId: string }>) => {
|
|
149
204
|
onAdClicked?.(e.nativeEvent.zoneId)
|
|
150
205
|
}}
|