@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.
@@ -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
- if let v = map["mobileBannerZid"] as? String { cfg.mobileBannerZid = v }
163
- if let v = map["mobileZids"] as? [String] { cfg.mobileZids = v }
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sellwild/react-native-sdk",
3
- "version": "1.5.0",
3
+ "version": "1.7.1",
4
4
  "description": "Sellwild mobile advertising SDK for React Native",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -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'
@@ -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={[styles.fill, style, __DEV__ ? styles.devPlaceholder : undefined]}>
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={[styles.fill, 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
  }}