@sellwild/react-native-sdk 1.5.0 → 1.7.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.
@@ -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
  }
@@ -154,6 +163,7 @@ class SellwildBannerViewManager : SimpleViewManager<SellwildAdView>() {
154
163
  return MapBuilder.builder<String, Any>()
155
164
  .put("onAdLoaded", MapBuilder.of("registrationName", "onAdLoaded"))
156
165
  .put("onAdImpression", MapBuilder.of("registrationName", "onAdImpression"))
166
+ .put("onHouseAdImpression", MapBuilder.of("registrationName", "onHouseAdImpression"))
157
167
  .put("onAdClicked", MapBuilder.of("registrationName", "onAdClicked"))
158
168
  .put("onAdFailed", MapBuilder.of("registrationName", "onAdFailed"))
159
169
  .put("onAdResize", MapBuilder.of("registrationName", "onAdResize"))
@@ -209,6 +219,46 @@ class SellwildBannerViewManager : SimpleViewManager<SellwildAdView>() {
209
219
  null
210
220
  }
211
221
 
222
+ val growthCode = if (map.hasKey("growthCode") && !map.isNull("growthCode")) {
223
+ val gc = map.getMap("growthCode")!!
224
+ fun bool(k: String): Boolean? = if (gc.hasKey(k) && !gc.isNull(k)) gc.getBoolean(k) else null
225
+ fun str(k: String): String? = if (gc.hasKey(k) && !gc.isNull(k)) gc.getString(k) else null
226
+ fun int(k: String): Int? = if (gc.hasKey(k) && !gc.isNull(k)) gc.getInt(k) else null
227
+ SellwildGrowthCodeConfig(
228
+ enabled = bool("enabled"),
229
+ partnerId = str("partnerId"),
230
+ endpoint = str("endpoint"),
231
+ syncUrl = str("syncUrl"),
232
+ sendMaid = bool("sendMaid"),
233
+ ttlHours = int("ttlHours"),
234
+ )
235
+ } else {
236
+ null
237
+ }
238
+
239
+ val localizedListings = if (map.hasKey("localizedListings") && !map.isNull("localizedListings")) {
240
+ val ll = map.getMap("localizedListings")!!
241
+ fun bool(k: String): Boolean? = if (ll.hasKey(k) && !ll.isNull(k)) ll.getBoolean(k) else null
242
+ fun str(k: String): String? = if (ll.hasKey(k) && !ll.isNull(k)) ll.getString(k) else null
243
+ fun int(k: String): Int? = if (ll.hasKey(k) && !ll.isNull(k)) ll.getInt(k) else null
244
+ SellwildLocalizedListingsConfig(
245
+ enabled = bool("enabled"),
246
+ source = str("source"),
247
+ baseUrl = str("baseUrl"),
248
+ urlTemplate = str("urlTemplate"),
249
+ frequency = int("frequency"),
250
+ forceState = str("forceState"),
251
+ )
252
+ } else {
253
+ null
254
+ }
255
+
256
+ // Custom Prebid Server (S2S) config — mirror the iOS bridge so the
257
+ // same RN app auctions against the partner's PBS on both platforms.
258
+ val prebidServer = RnPrebidServer.fromMap(
259
+ if (map.hasKey("prebidServer") && !map.isNull("prebidServer")) map.getMap("prebidServer") else null
260
+ )
261
+
212
262
  return SellwildConfig(
213
263
  partnerCode = partnerCode ?: "",
214
264
  appBundleId = appBundleId,
@@ -221,6 +271,9 @@ class SellwildBannerViewManager : SimpleViewManager<SellwildAdView>() {
221
271
  adRefreshMaxMobile = adRefreshMaxMobile,
222
272
  adRefreshIntervalMs = adRefreshIntervalMs,
223
273
  remoteJson = remoteJson,
274
+ growthCode = growthCode,
275
+ localizedListings = localizedListings,
276
+ prebidServer = prebidServer,
224
277
  )
225
278
  }
226
279
  }
@@ -12,6 +12,7 @@ import com.facebook.react.uimanager.events.RCTEventEmitter
12
12
  import com.sellwild.sdk.SellwildConfig
13
13
  import com.sellwild.sdk.SellwildFeedView
14
14
  import com.sellwild.sdk.SellwildListing
15
+ import com.sellwild.sdk.SellwildLocalizedListingsConfig
15
16
  import com.sellwild.sdk.SellwildSDK
16
17
  import org.json.JSONObject
17
18
 
@@ -67,6 +68,11 @@ class SellwildFeedViewManager : SimpleViewManager<SellwildFeedView>() {
67
68
  emit(reactContext, view, "onAdImpression", payload)
68
69
  }
69
70
 
71
+ override fun onHouseAdImpression(zoneId: String) {
72
+ val payload = Arguments.createMap().apply { putString("zoneId", zoneId) }
73
+ emit(reactContext, view, "onHouseAdImpression", payload)
74
+ }
75
+
70
76
  override fun onAdClicked(zoneId: String) {
71
77
  val payload = Arguments.createMap().apply { putString("zoneId", zoneId) }
72
78
  emit(reactContext, view, "onAdClicked", payload)
@@ -80,6 +86,11 @@ class SellwildFeedViewManager : SimpleViewManager<SellwildFeedView>() {
80
86
  val payload = Arguments.createMap().apply { putString("message", message) }
81
87
  emit(reactContext, view, "onFeedError", payload)
82
88
  }
89
+
90
+ override fun onContentHeightChanged(feedView: SellwildFeedView, heightDp: Int) {
91
+ val payload = Arguments.createMap().apply { putInt("height", heightDp) }
92
+ emit(reactContext, view, "onContentSizeChange", payload)
93
+ }
83
94
  }
84
95
  return view
85
96
  }
@@ -89,6 +100,11 @@ class SellwildFeedViewManager : SimpleViewManager<SellwildFeedView>() {
89
100
  pendingFor(view).config = value
90
101
  }
91
102
 
103
+ @ReactProp(name = "scrollEnabled", defaultBoolean = true)
104
+ fun setScrollEnabled(view: SellwildFeedView, value: Boolean) {
105
+ view.scrollEnabled = value
106
+ }
107
+
92
108
  override fun onAfterUpdateTransaction(view: SellwildFeedView) {
93
109
  super.onAfterUpdateTransaction(view)
94
110
 
@@ -117,8 +133,10 @@ class SellwildFeedViewManager : SimpleViewManager<SellwildFeedView>() {
117
133
  .put("onFeedLoaded", MapBuilder.of("registrationName", "onFeedLoaded"))
118
134
  .put("onListingTap", MapBuilder.of("registrationName", "onListingTap"))
119
135
  .put("onAdImpression", MapBuilder.of("registrationName", "onAdImpression"))
136
+ .put("onHouseAdImpression", MapBuilder.of("registrationName", "onHouseAdImpression"))
120
137
  .put("onAdClicked", MapBuilder.of("registrationName", "onAdClicked"))
121
138
  .put("onFeedError", MapBuilder.of("registrationName", "onFeedError"))
139
+ .put("onContentSizeChange", MapBuilder.of("registrationName", "onContentSizeChange"))
122
140
  .build()
123
141
  }
124
142
 
@@ -167,6 +185,29 @@ class SellwildFeedViewManager : SimpleViewManager<SellwildFeedView>() {
167
185
  if (map.hasKey("adRefreshMax")) config = config.copy(adRefreshMax = map.getInt("adRefreshMax"))
168
186
  if (map.hasKey("adRefreshMaxMobile")) config = config.copy(adRefreshMaxMobile = map.getInt("adRefreshMaxMobile"))
169
187
  if (map.hasKey("adRefreshIntervalMs")) config = config.copy(adRefreshIntervalMs = map.getDouble("adRefreshIntervalMs").toLong())
188
+ // Custom Prebid Server (S2S) config — mirror the iOS feed bridge.
189
+ if (map.hasKey("prebidServer") && !map.isNull("prebidServer")) {
190
+ config = config.copy(prebidServer = RnPrebidServer.fromMap(map.getMap("prebidServer")))
191
+ }
192
+
193
+ // Local override for the localized (geo-based) secondary-listings
194
+ // integration; the remote LOCALIZED_LISTINGS object rides `remote`.
195
+ if (map.hasKey("localizedListings") && !map.isNull("localizedListings")) {
196
+ val ll = map.getMap("localizedListings")!!
197
+ fun bool(k: String): Boolean? = if (ll.hasKey(k) && !ll.isNull(k)) ll.getBoolean(k) else null
198
+ fun str(k: String): String? = if (ll.hasKey(k) && !ll.isNull(k)) ll.getString(k) else null
199
+ fun int(k: String): Int? = if (ll.hasKey(k) && !ll.isNull(k)) ll.getInt(k) else null
200
+ config = config.copy(
201
+ localizedListings = SellwildLocalizedListingsConfig(
202
+ enabled = bool("enabled"),
203
+ source = str("source"),
204
+ baseUrl = str("baseUrl"),
205
+ urlTemplate = str("urlTemplate"),
206
+ frequency = int("frequency"),
207
+ forceState = str("forceState"),
208
+ ),
209
+ )
210
+ }
170
211
 
171
212
  return config
172
213
  }
@@ -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,19 @@ final class SellwildFeedHostView: UIView, SellwildFeedViewDelegate {
181
207
  )
182
208
  }
183
209
 
210
+ // Local override for the localized (geo-based) secondary-listings
211
+ // integration; remote LOCALIZED_LISTINGS rides `remote` verbatim.
212
+ if let ll = map["localizedListings"] as? NSDictionary {
213
+ cfg.localizedListings = SellwildLocalizedListingsConfig(
214
+ enabled: ll["enabled"] as? Bool,
215
+ source: ll["source"] as? String,
216
+ baseUrl: ll["baseUrl"] as? String,
217
+ urlTemplate: ll["urlTemplate"] as? String,
218
+ frequency: ll["frequency"] as? Int,
219
+ forceState: ll["forceState"] as? String
220
+ )
221
+ }
222
+
184
223
  return cfg
185
224
  }
186
225
 
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.0",
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,8 @@ 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,
128
169
  remote: config.remote,
129
170
  listingsUrl: config.listingsUrl,
130
171
  priceColor: config.priceColor,
@@ -136,8 +177,16 @@ export function SellwildFeed({
136
177
 
137
178
  return (
138
179
  <NativeFeed
139
- style={[styles.fill, style]}
180
+ style={containerStyle}
140
181
  config={nativeConfig}
182
+ scrollEnabled={scrollEnabled}
183
+ onContentSizeChange={(e: NativeSyntheticEvent<{ width?: number; height: number }>) => {
184
+ const { width, height } = e.nativeEvent ?? { height: 0 }
185
+ // Only size our own container when embedded; a scrolling feed fills
186
+ // its parent via flex:1 and doesn't need a measured height.
187
+ if (embedded && height > 0) setContentHeight(height)
188
+ onContentSizeChange?.({ width, height })
189
+ }}
141
190
  onFeedLoaded={() => onLoad?.()}
142
191
  onListingTap={(e: NativeSyntheticEvent<{ listing: SellwildListing }>) => {
143
192
  onListingTap?.(e.nativeEvent.listing)
@@ -145,6 +194,9 @@ export function SellwildFeed({
145
194
  onAdImpression={(e: NativeSyntheticEvent<{ zoneId: string }>) => {
146
195
  onAdImpression?.(e.nativeEvent.zoneId)
147
196
  }}
197
+ onHouseAdImpression={(e: NativeSyntheticEvent<{ zoneId: string }>) => {
198
+ onHouseAdImpression?.(e.nativeEvent.zoneId)
199
+ }}
148
200
  onAdClicked={(e: NativeSyntheticEvent<{ zoneId: string }>) => {
149
201
  onAdClicked?.(e.nativeEvent.zoneId)
150
202
  }}