@sellwild/react-native-sdk 1.4.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.
@@ -76,7 +76,7 @@ dependencies {
76
76
  // The native SDK that owns SellwildAdView, SellwildPrebidMobile, etc.
77
77
  // Resolves to the published AAR off https://maven.sellwild.com/releases
78
78
  // for npm consumers, or to mavenLocal in the SDK monorepo dev workflow.
79
- implementation "com.sellwild:sdk:1.4.0"
79
+ implementation "com.sellwild:sdk:1.4.1"
80
80
  }
81
81
 
82
82
  // Force kotlin-stdlib down to the bridge's declared Kotlin version. The
@@ -0,0 +1,31 @@
1
+ package com.sellwild.rnsdk
2
+
3
+ import com.facebook.react.bridge.ReadableMap
4
+ import com.sellwild.sdk.SellwildGeo
5
+
6
+ /**
7
+ * Marshals a bridged JS geo object (React `ReadableMap`) into the native
8
+ * [SellwildGeo]. Lives in the RN module (not core) because `ReadableMap` is a
9
+ * React type — the core SDK must not depend on React. Returns null when nothing
10
+ * usable is present, so callers can clear geo by passing an empty object.
11
+ */
12
+ internal object RnGeo {
13
+ fun readableMapToGeo(map: ReadableMap?): SellwildGeo? {
14
+ if (map == null) return null
15
+ fun str(k: String) = if (map.hasKey(k) && !map.isNull(k)) map.getString(k) else null
16
+ fun dbl(k: String) = if (map.hasKey(k) && !map.isNull(k)) map.getDouble(k) else null
17
+ fun int(k: String) = if (map.hasKey(k) && !map.isNull(k)) map.getInt(k) else null
18
+ val geo = SellwildGeo(
19
+ country = str("country"),
20
+ state = str("state"),
21
+ city = str("city"),
22
+ zip = str("zip"),
23
+ metro = str("metro"),
24
+ lat = dbl("lat"),
25
+ lon = dbl("lon"),
26
+ type = int("type"),
27
+ )
28
+ // toOrtbGeo() returns null when every field is empty — treat that as "no geo".
29
+ return if (geo.toOrtbGeo() != null) geo else null
30
+ }
31
+ }
@@ -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
  }
@@ -87,6 +96,14 @@ class SellwildBannerViewManager : SimpleViewManager<SellwildAdView>() {
87
96
  }
88
97
  emit(reactContext, adView, "onAdFailed", payload)
89
98
  }
99
+
100
+ override fun onAdResize(adView: SellwildAdView, width: Int, height: Int) {
101
+ val payload = com.facebook.react.bridge.Arguments.createMap().apply {
102
+ putInt("width", width)
103
+ putInt("height", height)
104
+ }
105
+ emit(reactContext, adView, "onAdResize", payload)
106
+ }
90
107
  }
91
108
  return view
92
109
  }
@@ -146,8 +163,10 @@ class SellwildBannerViewManager : SimpleViewManager<SellwildAdView>() {
146
163
  return MapBuilder.builder<String, Any>()
147
164
  .put("onAdLoaded", MapBuilder.of("registrationName", "onAdLoaded"))
148
165
  .put("onAdImpression", MapBuilder.of("registrationName", "onAdImpression"))
166
+ .put("onHouseAdImpression", MapBuilder.of("registrationName", "onHouseAdImpression"))
149
167
  .put("onAdClicked", MapBuilder.of("registrationName", "onAdClicked"))
150
168
  .put("onAdFailed", MapBuilder.of("registrationName", "onAdFailed"))
169
+ .put("onAdResize", MapBuilder.of("registrationName", "onAdResize"))
151
170
  .build()
152
171
  }
153
172
 
@@ -186,6 +205,10 @@ class SellwildBannerViewManager : SimpleViewManager<SellwildAdView>() {
186
205
  val appStoreUrl = if (map.hasKey("appStoreUrl")) map.getString("appStoreUrl") else null
187
206
  val gamTag = if (map.hasKey("gamTag")) map.getString("gamTag") else null
188
207
  val debug = map.hasKey("debug") && map.getBoolean("debug")
208
+ val pbsDebug = map.hasKey("pbsDebug") && map.getBoolean("pbsDebug")
209
+ val geo = RnGeo.readableMapToGeo(
210
+ if (map.hasKey("geo") && !map.isNull("geo")) map.getMap("geo") else null
211
+ )
189
212
  val adRefreshMax = if (map.hasKey("adRefreshMax")) map.getInt("adRefreshMax") else 0
190
213
  val adRefreshMaxMobile = if (map.hasKey("adRefreshMaxMobile")) map.getInt("adRefreshMaxMobile") else 0
191
214
  val adRefreshIntervalMs = if (map.hasKey("adRefreshIntervalMs")) map.getDouble("adRefreshIntervalMs").toLong() else 30_000L
@@ -196,16 +219,61 @@ class SellwildBannerViewManager : SimpleViewManager<SellwildAdView>() {
196
219
  null
197
220
  }
198
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
+
199
262
  return SellwildConfig(
200
263
  partnerCode = partnerCode ?: "",
201
264
  appBundleId = appBundleId,
202
265
  appStoreUrl = appStoreUrl,
203
266
  gamTag = gamTag,
204
267
  debug = debug,
268
+ pbsDebug = pbsDebug,
269
+ geo = geo,
205
270
  adRefreshMax = adRefreshMax,
206
271
  adRefreshMaxMobile = adRefreshMaxMobile,
207
272
  adRefreshIntervalMs = adRefreshIntervalMs,
208
273
  remoteJson = remoteJson,
274
+ growthCode = growthCode,
275
+ localizedListings = localizedListings,
276
+ prebidServer = prebidServer,
209
277
  )
210
278
  }
211
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
 
@@ -162,9 +180,34 @@ class SellwildFeedViewManager : SimpleViewManager<SellwildFeedView>() {
162
180
  if (map.hasKey("appStoreUrl")) config = config.copy(appStoreUrl = map.getString("appStoreUrl"))
163
181
  if (map.hasKey("gamTag")) config = config.copy(gamTag = map.getString("gamTag"))
164
182
  if (map.hasKey("debug")) config = config.copy(debug = map.getBoolean("debug"))
183
+ if (map.hasKey("pbsDebug")) config = config.copy(pbsDebug = map.getBoolean("pbsDebug"))
184
+ if (map.hasKey("geo") && !map.isNull("geo")) config = config.copy(geo = RnGeo.readableMapToGeo(map.getMap("geo")))
165
185
  if (map.hasKey("adRefreshMax")) config = config.copy(adRefreshMax = map.getInt("adRefreshMax"))
166
186
  if (map.hasKey("adRefreshMaxMobile")) config = config.copy(adRefreshMaxMobile = map.getInt("adRefreshMaxMobile"))
167
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
+ }
168
211
 
169
212
  return config
170
213
  }
@@ -0,0 +1,67 @@
1
+ package com.sellwild.rnsdk
2
+
3
+ import com.facebook.react.bridge.ReactApplicationContext
4
+ import com.facebook.react.bridge.ReactContextBaseJavaModule
5
+ import com.facebook.react.bridge.ReactMethod
6
+ import com.facebook.react.bridge.ReadableArray
7
+ import com.facebook.react.bridge.ReadableMap
8
+ import com.sellwild.sdk.SellwildEid
9
+ import com.sellwild.sdk.SellwildEidUid
10
+ import com.sellwild.sdk.SellwildPrebidMobile
11
+
12
+ /**
13
+ * React Native method module for the native Sellwild SDK's runtime setters.
14
+ *
15
+ * The RN ad surface is otherwise view-manager-only (config flows as a prop);
16
+ * this module is the one callable bridge for imperative, session-scoped calls
17
+ * like [setGeo]. Registered in [SellwildSdkPackage.createNativeModules]. The
18
+ * module name ("SellwildRNModule") matches the iOS `@objc(SellwildRNModule)` so
19
+ * JS resolves one `NativeModules.SellwildRNModule` on both platforms.
20
+ */
21
+ class SellwildModule(reactContext: ReactApplicationContext) :
22
+ ReactContextBaseJavaModule(reactContext) {
23
+
24
+ override fun getName(): String = "SellwildRNModule"
25
+
26
+ /**
27
+ * JS: `SellwildRNModule.setGeo({ state: "NY", zip: "10001", ... })`.
28
+ * Pass an empty object to clear. Mirrors [SellwildPrebidMobile.setGeo] —
29
+ * updates the Prebid auction geo AND the shared SellwildGeoStore.
30
+ */
31
+ @ReactMethod
32
+ fun setGeo(geo: ReadableMap?) {
33
+ SellwildPrebidMobile.setGeo(RnGeo.readableMapToGeo(geo))
34
+ }
35
+
36
+ /**
37
+ * JS: `SellwildRNModule.setExternalUserIds([{ source, uids: [{ id, atype, ext? }] }])`.
38
+ * Pass `[]` to clear. Mirrors [SellwildPrebidMobile.setExternalUserIds].
39
+ */
40
+ @ReactMethod
41
+ fun setExternalUserIds(eids: ReadableArray?) {
42
+ SellwildPrebidMobile.setExternalUserIds(toEids(eids))
43
+ }
44
+
45
+ private fun toEids(arr: ReadableArray?): List<SellwildEid> {
46
+ if (arr == null) return emptyList()
47
+ val out = ArrayList<SellwildEid>()
48
+ for (i in 0 until arr.size()) {
49
+ val eid = arr.getMap(i) ?: continue
50
+ val source = if (eid.hasKey("source")) eid.getString("source") else null
51
+ val uidsArr = if (eid.hasKey("uids") && !eid.isNull("uids")) eid.getArray("uids") else null
52
+ if (source == null || uidsArr == null) continue
53
+ val uids = ArrayList<SellwildEidUid>()
54
+ for (j in 0 until uidsArr.size()) {
55
+ val u = uidsArr.getMap(j) ?: continue
56
+ val id = (if (u.hasKey("id")) u.getString("id") else null) ?: continue
57
+ val atype = if (u.hasKey("atype")) u.getInt("atype") else 0
58
+ @Suppress("UNCHECKED_CAST")
59
+ val ext = if (u.hasKey("ext") && !u.isNull("ext"))
60
+ u.getMap("ext")?.toHashMap() as? Map<String, Any> else null
61
+ uids.add(SellwildEidUid(id, atype, ext))
62
+ }
63
+ out.add(SellwildEid(source, uids))
64
+ }
65
+ return out
66
+ }
67
+ }
@@ -8,7 +8,8 @@ import com.facebook.react.uimanager.ReactShadowNode
8
8
  import com.facebook.react.uimanager.ViewManager
9
9
 
10
10
  class SellwildSdkPackage : ReactPackage {
11
- override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> = emptyList()
11
+ override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> =
12
+ listOf(SellwildModule(reactContext))
12
13
 
13
14
  override fun createViewManagers(
14
15
  reactContext: ReactApplicationContext,
@@ -19,7 +19,9 @@ 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)
25
+ RCT_EXPORT_VIEW_PROPERTY(onAdResize, RCTDirectEventBlock)
24
26
 
25
27
  @end
@@ -47,8 +47,12 @@ 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?
54
+ /// Emitted with the rendered creative size so JS can resize the slot.
55
+ @objc var onAdResize: RCTDirectEventBlock?
52
56
 
53
57
  // MARK: Internals
54
58
 
@@ -98,11 +102,15 @@ final class SellwildBannerHostView: UIView, SellwildAdViewDelegate {
98
102
  view.delegate = self
99
103
  view.translatesAutoresizingMaskIntoConstraints = false
100
104
  addSubview(view)
105
+ // Fill the RN-sized host rather than hard-pinning to the primary size —
106
+ // the host's dimensions are driven by JS style, which the JS component
107
+ // updates from the onAdResize event so the slot tracks the actual
108
+ // rendered creative (multi-size fallback, video, capped native).
101
109
  NSLayoutConstraint.activate([
102
110
  view.leadingAnchor.constraint(equalTo: leadingAnchor),
103
111
  view.topAnchor.constraint(equalTo: topAnchor),
104
- view.widthAnchor.constraint(equalToConstant: adSize.cgSize.width),
105
- view.heightAnchor.constraint(equalToConstant: adSize.cgSize.height),
112
+ view.trailingAnchor.constraint(equalTo: trailingAnchor),
113
+ view.bottomAnchor.constraint(equalTo: bottomAnchor),
106
114
  ])
107
115
  adView = view
108
116
  view.load()
@@ -118,6 +126,10 @@ final class SellwildBannerHostView: UIView, SellwildAdViewDelegate {
118
126
  onAdImpression?(["zoneId": zoneId])
119
127
  }
120
128
 
129
+ func sellwildAdView(_ adView: SellwildAdView, didRecordHouseImpressionForZoneId zoneId: String) {
130
+ onHouseAdImpression?(["zoneId": zoneId])
131
+ }
132
+
121
133
  func sellwildAdViewDidRecordClick(_ adView: SellwildAdView) {
122
134
  onAdClicked?([:])
123
135
  }
@@ -126,6 +138,10 @@ final class SellwildBannerHostView: UIView, SellwildAdViewDelegate {
126
138
  onAdFailed?(["message": error.localizedDescription])
127
139
  }
128
140
 
141
+ func sellwildAdView(_ adView: SellwildAdView, didRenderWithSize size: CGSize) {
142
+ onAdResize?(["width": size.width, "height": size.height])
143
+ }
144
+
129
145
  // MARK: Config marshalling
130
146
 
131
147
  /// Build a minimal SellwildConfig from a JS dictionary. Only the
@@ -139,8 +155,10 @@ final class SellwildBannerHostView: UIView, SellwildAdViewDelegate {
139
155
 
140
156
  if let v = map["appBundleId"] as? String { cfg.appBundleId = v }
141
157
  if let v = map["appStoreUrl"] as? String { cfg.appStoreUrl = v }
158
+ if let geoMap = map["geo"] as? [String: Any] { cfg.geo = SellwildGeo(bridged: geoMap) }
142
159
  if let v = map["gamTag"] as? String { cfg.gamTag = v }
143
160
  if let v = map["debug"] as? Bool { cfg.debug = v }
161
+ if let v = map["pbsDebug"] as? Bool { cfg.pbsDebug = v }
144
162
  if let v = map["adRefreshMax"] as? Int { cfg.adRefreshMax = v }
145
163
  if let v = map["adRefreshMaxMobile"] as? Int { cfg.adRefreshMaxMobile = v }
146
164
 
@@ -163,6 +181,28 @@ final class SellwildBannerHostView: UIView, SellwildAdViewDelegate {
163
181
  )
164
182
  }
165
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
+
166
206
  if let remote = map["remote"] as? NSDictionary,
167
207
  let data = try? JSONSerialization.data(withJSONObject: remote, options: []) {
168
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
@@ -149,16 +166,27 @@ final class SellwildFeedHostView: UIView, SellwildFeedViewDelegate {
149
166
  if let v = map["slug"] as? String { cfg.slug = v }
150
167
  if let v = map["appBundleId"] as? String { cfg.appBundleId = v }
151
168
  if let v = map["appStoreUrl"] as? String { cfg.appStoreUrl = v }
169
+ if let geoMap = map["geo"] as? [String: Any] { cfg.geo = SellwildGeo(bridged: geoMap) }
152
170
  if let v = map["gamTag"] as? String { cfg.gamTag = v }
153
171
  if let v = map["debug"] as? Bool { cfg.debug = v }
172
+ if let v = map["pbsDebug"] as? Bool { cfg.pbsDebug = v }
154
173
  if let v = map["adRefreshMax"] as? Int { cfg.adRefreshMax = v }
155
174
  if let v = map["adRefreshMaxMobile"] as? Int { cfg.adRefreshMaxMobile = v }
156
175
  if let v = map["listingsUrl"] as? String { cfg.listingsUrl = v }
157
176
  if let v = map["priceColor"] as? String { cfg.priceColor = v }
158
177
  if let v = map["bannerZid"] as? String { cfg.bannerZid = v }
159
178
  if let v = map["bottomBannerZid"] as? String { cfg.bottomBannerZid = v }
160
- if let v = map["mobileBannerZid"] as? String { cfg.mobileBannerZid = v }
161
- 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
+ }
162
190
 
163
191
  // JS bridge passes ms; iOS API is seconds (TimeInterval).
164
192
  if let v = map["adRefreshIntervalMs"] as? NSNumber {
@@ -179,6 +207,19 @@ final class SellwildFeedHostView: UIView, SellwildFeedViewDelegate {
179
207
  )
180
208
  }
181
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
+
182
223
  return cfg
183
224
  }
184
225
 
@@ -0,0 +1,10 @@
1
+ #import <React/RCTBridgeModule.h>
2
+
3
+ // Exposes the Swift SellwildRNModule to React Native. Method signatures must
4
+ // match the @objc selectors in SellwildRNModule.swift.
5
+ @interface RCT_EXTERN_MODULE(SellwildRNModule, NSObject)
6
+
7
+ RCT_EXTERN_METHOD(setGeo:(NSDictionary *)geo)
8
+ RCT_EXTERN_METHOD(setExternalUserIds:(NSArray *)eids)
9
+
10
+ @end
@@ -0,0 +1,42 @@
1
+ import Foundation
2
+ import SellwildSDK
3
+
4
+ /// React Native method module for the native Sellwild SDK's runtime setters.
5
+ ///
6
+ /// The RN ad surface is otherwise view-manager-only (config flows as a prop);
7
+ /// this module is the one callable bridge for imperative, session-scoped calls
8
+ /// like `setGeo`. Registered on iOS via `RCT_EXTERN_MODULE` (see the paired
9
+ /// `SellwildRNModule.m`); no manual package wiring is needed.
10
+ @objc(SellwildRNModule)
11
+ final class SellwildRNModule: NSObject {
12
+
13
+ /// Off-main is fine — the setters just update process-wide state.
14
+ @objc static func requiresMainQueueSetup() -> Bool { false }
15
+
16
+ /// JS: `SellwildRNModule.setGeo({ state: "NY", zip: "10001", ... })`.
17
+ /// Pass an empty object to clear. Mirrors the native
18
+ /// `SellwildPrebidMobile.setGeo(_:)` — updates the Prebid auction geo AND the
19
+ /// shared `SellwildGeoStore`.
20
+ @objc(setGeo:)
21
+ func setGeo(_ geo: NSDictionary) {
22
+ let map = geo as? [String: Any] ?? [:]
23
+ SellwildPrebidMobile.setGeo(SellwildGeo(bridged: map))
24
+ }
25
+
26
+ /// JS: `SellwildRNModule.setExternalUserIds([{ source, uids: [{ id, atype, ext? }] }])`.
27
+ /// Pass `[]` to clear. Mirrors `SellwildPrebidMobile.setExternalUserIds(_:)`.
28
+ @objc(setExternalUserIds:)
29
+ func setExternalUserIds(_ eids: NSArray) {
30
+ let mapped: [SellwildEid] = (eids as? [[String: Any]] ?? []).compactMap { dict in
31
+ guard let source = dict["source"] as? String,
32
+ let rawUids = dict["uids"] as? [[String: Any]] else { return nil }
33
+ let uids: [SellwildEidUID] = rawUids.compactMap { u in
34
+ guard let id = u["id"] as? String else { return nil }
35
+ let atype = (u["atype"] as? NSNumber)?.intValue ?? 0
36
+ return SellwildEidUID(id: id, atype: atype, ext: u["ext"] as? [String: Any])
37
+ }
38
+ return SellwildEid(source: source, uids: uids)
39
+ }
40
+ SellwildPrebidMobile.setExternalUserIds(mapped)
41
+ }
42
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sellwild/react-native-sdk",
3
- "version": "1.4.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",
@@ -13,6 +13,29 @@ const AD_DIMENSIONS: Record<AdSize, { width: number; height: number }> = {
13
13
  '1x1': { width: 1, height: 1 },
14
14
  }
15
15
 
16
+ // Parse a BANNER_SIZES value — `["300x250","320x50"]` or `[[300,250],…]`,
17
+ // possibly a JSON string — into {width,height}[]. Mirrors the native
18
+ // SellwildAdSizes parser so the RN slot reasons about the same size set the
19
+ // auction requests.
20
+ function parseSizeList(raw: unknown): Array<{ width: number; height: number }> {
21
+ let arr: unknown = raw
22
+ if (typeof raw === 'string') {
23
+ try { arr = JSON.parse(raw) } catch { arr = [raw] }
24
+ }
25
+ if (!Array.isArray(arr)) return []
26
+ const out: Array<{ width: number; height: number }> = []
27
+ for (const e of arr) {
28
+ if (typeof e === 'string') {
29
+ const [w, h] = e.toLowerCase().split('x').map((s) => Number(s.trim()))
30
+ if (w > 0 && h > 0) out.push({ width: w, height: h })
31
+ } else if (Array.isArray(e) && e.length === 2) {
32
+ const w = Number(e[0]); const h = Number(e[1])
33
+ if (w > 0 && h > 0) out.push({ width: w, height: h })
34
+ }
35
+ }
36
+ return out
37
+ }
38
+
16
39
  // ─── Native component bridge ─────────────────────────────────────────────────
17
40
  //
18
41
  // As of 1.3.0 the React Native banner is backed by a real native ad view —
@@ -39,8 +62,10 @@ interface NativeBannerProps {
39
62
  style?: ViewStyle
40
63
  onAdLoaded?: (e: NativeSyntheticEvent<{}>) => void
41
64
  onAdImpression?: (e: NativeSyntheticEvent<{ zoneId: string }>) => void
65
+ onHouseAdImpression?: (e: NativeSyntheticEvent<{ zoneId: string }>) => void
42
66
  onAdClicked?: (e: NativeSyntheticEvent<{}>) => void
43
67
  onAdFailed?: (e: NativeSyntheticEvent<{ message: string }>) => void
68
+ onAdResize?: (e: NativeSyntheticEvent<{ width: number; height: number }>) => void
44
69
  }
45
70
 
46
71
  const NativeBanner = (() => {
@@ -62,6 +87,11 @@ export interface SellwildBannerProps {
62
87
  zoneId: number | string
63
88
  style?: ViewStyle
64
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
65
95
  onClick?: () => void
66
96
  onError?: (error: Error) => void
67
97
  }
@@ -72,11 +102,45 @@ export function SellwildBanner({
72
102
  zoneId,
73
103
  style,
74
104
  onImpression,
105
+ onHouseImpression,
75
106
  onClick,
76
107
  onError,
77
108
  }: SellwildBannerProps) {
78
109
  const dim = AD_DIMENSIONS[size]
79
- const containerStyle: ViewStyle = { width: dim.width, height: dim.height }
110
+
111
+ // The widest/tallest size the auction may return for this placement: the
112
+ // primary plus any BANNER_SIZES / BANNER_SIZES_BY_ZONE fallbacks. We reserve
113
+ // this as the slot's baseline so a wider or taller fallback creative — e.g. a
114
+ // 320x50 winning a 300-wide MREC request, where 320 > 300 — never clips
115
+ // before, or without, the onAdResize callback. This also covers the Android
116
+ // prebidOnly path, whose rendering BannerView doesn't surface the winning
117
+ // creative size (so onAdResize can't shrink it back down there).
118
+ const baseline = React.useMemo(() => {
119
+ const remote = (config.remote ?? {}) as Record<string, unknown>
120
+ const byZone = remote['BANNER_SIZES_BY_ZONE']
121
+ const zoned = byZone && typeof byZone === 'object'
122
+ ? (byZone as Record<string, unknown>)[String(zoneId)]
123
+ : undefined
124
+ const sizes = [dim, ...parseSizeList(zoned ?? remote['BANNER_SIZES'])]
125
+ return {
126
+ width: Math.max(...sizes.map((s) => s.width)),
127
+ height: Math.max(...sizes.map((s) => s.height)),
128
+ }
129
+ }, [dim, zoneId, config.remote])
130
+
131
+ // The slot starts at the reserved baseline, then tracks whatever the native
132
+ // side actually renders (onAdResize): a multi-size fallback creative, an
133
+ // outstream video, or the capped native template. Where the actual size is
134
+ // reported it shrinks the slot to fit; where it isn't (Android prebidOnly)
135
+ // the baseline reservation prevents a clip.
136
+ const [rendered, setRendered] = React.useState<{ width: number; height: number } | null>(null)
137
+ // Reset to the baseline when the placement identity changes.
138
+ React.useEffect(() => { setRendered(null) }, [size, zoneId])
139
+
140
+ const containerStyle: ViewStyle = {
141
+ width: rendered?.width ?? baseline.width,
142
+ height: rendered?.height ?? baseline.height,
143
+ }
80
144
 
81
145
  if (!NativeBanner) {
82
146
  // Native module not registered. Most common cause: iOS bridge not yet
@@ -102,12 +166,18 @@ export function SellwildBanner({
102
166
  partnerCode: config.partnerCode,
103
167
  appBundleId: config.appBundleId,
104
168
  appStoreUrl: config.appStoreUrl,
169
+ geo: config.geo,
105
170
  gamTag: config.gamTag,
106
171
  debug: config.debug,
172
+ pbsDebug: config.pbsDebug,
107
173
  adRefreshMax: config.adRefreshMax,
108
174
  adRefreshMaxMobile: config.adRefreshMaxMobile,
109
175
  adRefreshIntervalMs: config.adRefreshInterval,
110
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,
111
181
  remote: config.remote,
112
182
  }
113
183
 
@@ -122,11 +192,16 @@ export function SellwildBanner({
122
192
  // No-op event hook today; surfaced for future fill metrics.
123
193
  }}
124
194
  onAdImpression={() => onImpression?.()}
195
+ onHouseAdImpression={(e: NativeSyntheticEvent<{ zoneId: string }>) => onHouseImpression?.(e.nativeEvent?.zoneId)}
125
196
  onAdClicked={() => onClick?.()}
126
197
  onAdFailed={(e: NativeSyntheticEvent<{ message: string }>) => {
127
198
  const msg = e.nativeEvent?.message ?? 'Ad failed'
128
199
  onError?.(new Error(msg))
129
200
  }}
201
+ onAdResize={(e: NativeSyntheticEvent<{ width: number; height: number }>) => {
202
+ const { width, height } = e.nativeEvent ?? { width: 0, height: 0 }
203
+ if (width > 0 && height > 0) setRendered({ width, height })
204
+ }}
130
205
  />
131
206
  )
132
207
  }
@@ -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)
@@ -117,12 +156,16 @@ export function SellwildFeed({
117
156
  slug: config.slug,
118
157
  appBundleId: config.appBundleId,
119
158
  appStoreUrl: config.appStoreUrl,
159
+ geo: config.geo,
120
160
  gamTag: config.gamTag,
121
161
  debug: config.debug,
162
+ pbsDebug: config.pbsDebug,
122
163
  adRefreshMax: config.adRefreshMax,
123
164
  adRefreshMaxMobile: config.adRefreshMaxMobile,
124
165
  adRefreshIntervalMs: config.adRefreshInterval,
125
166
  prebidServer: config.prebidServer,
167
+ // Local localized-listings overrides (remote LOCALIZED_LISTINGS rides `remote`).
168
+ localizedListings: config.localizedListings,
126
169
  remote: config.remote,
127
170
  listingsUrl: config.listingsUrl,
128
171
  priceColor: config.priceColor,
@@ -134,8 +177,16 @@ export function SellwildFeed({
134
177
 
135
178
  return (
136
179
  <NativeFeed
137
- style={[styles.fill, style]}
180
+ style={containerStyle}
138
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
+ }}
139
190
  onFeedLoaded={() => onLoad?.()}
140
191
  onListingTap={(e: NativeSyntheticEvent<{ listing: SellwildListing }>) => {
141
192
  onListingTap?.(e.nativeEvent.listing)
@@ -143,6 +194,9 @@ export function SellwildFeed({
143
194
  onAdImpression={(e: NativeSyntheticEvent<{ zoneId: string }>) => {
144
195
  onAdImpression?.(e.nativeEvent.zoneId)
145
196
  }}
197
+ onHouseAdImpression={(e: NativeSyntheticEvent<{ zoneId: string }>) => {
198
+ onHouseAdImpression?.(e.nativeEvent.zoneId)
199
+ }}
146
200
  onAdClicked={(e: NativeSyntheticEvent<{ zoneId: string }>) => {
147
201
  onAdClicked?.(e.nativeEvent.zoneId)
148
202
  }}
@@ -0,0 +1,37 @@
1
+ import { NativeModules } from 'react-native'
2
+ import type { SellwildEid, SellwildGeo } from '@sellwild/sdk-core'
3
+
4
+ // Imperative bridge to the native Sellwild method module. The ad surface is
5
+ // otherwise view-manager-only (config flows as a prop on <SellwildBanner> /
6
+ // <SellwildFeed>); this file exposes the session-scoped native setters.
7
+ const SellwildRNModule = NativeModules.SellwildRNModule as
8
+ | {
9
+ setGeo?: (geo: Record<string, unknown>) => void
10
+ setExternalUserIds?: (eids: SellwildEid[]) => void
11
+ }
12
+ | undefined
13
+
14
+ /**
15
+ * Set or update partner-supplied geo at runtime (OpenRTB `device.geo`) for
16
+ * native Prebid auctions, and seed the shared native geo store read by other
17
+ * surfaces (e.g. the listings feed). Pass `null` to clear.
18
+ *
19
+ * Prefer `config.geo` for the value known at configure time; use `setGeo` when
20
+ * location resolves or changes mid-session. No-op if the native module isn't
21
+ * linked (e.g. before autolinking, or on an unsupported platform).
22
+ */
23
+ export function setGeo(geo: SellwildGeo | null): void {
24
+ // Native maps an empty object to "clear", so send {} for null.
25
+ SellwildRNModule?.setGeo?.((geo ?? {}) as Record<string, unknown>)
26
+ }
27
+
28
+ /**
29
+ * Set partner-supplied external/extended user IDs, emitted as OpenRTB
30
+ * `user.ext.eids` on every native Prebid auction. Pass `[]` to clear.
31
+ *
32
+ * Re-set on each launch (Prebid Mobile does not persist eids across restarts).
33
+ * No-op if the native module isn't linked.
34
+ */
35
+ export function setExternalUserIds(eids: SellwildEid[]): void {
36
+ SellwildRNModule?.setExternalUserIds?.(eids ?? [])
37
+ }
package/src/index.ts CHANGED
@@ -13,6 +13,9 @@ export type { SellwildListingCardProps } from './SellwildListingCard'
13
13
  export { useSellwildListings } from './useSellwildListings'
14
14
  export type { UseSellwildListingsResult } from './useSellwildListings'
15
15
 
16
+ // Imperative native setters (runtime, session-scoped).
17
+ export { setGeo, setExternalUserIds } from './commands'
18
+
16
19
  // Re-export core types for convenience
17
20
  export type {
18
21
  SellwildConfig,
@@ -22,6 +25,9 @@ export type {
22
25
  SellwildPhoto,
23
26
  SellwildUser,
24
27
  PrebidServerConfig,
28
+ SellwildGeo,
29
+ SellwildEid,
30
+ SellwildEidUid,
25
31
  AdSize,
26
32
  AdPlacement,
27
33
  AdPlacementType,