@sellwild/react-native-sdk 1.3.7 → 1.5.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.
@@ -28,6 +28,6 @@ Pod::Spec.new do |s|
28
28
  # The host app must already pull in SellwildSDK (the iOS SDK pod). We
29
29
  # depend on it explicitly so `pod install` resolves both with one
30
30
  # declaration in the host Podfile.
31
- s.dependency 'SellwildSDK', '>= 1.3.5'
31
+ s.dependency 'SellwildSDK', '>= 1.4.0'
32
32
  s.dependency 'React-Core'
33
33
  end
@@ -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.3.5"
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
+ }
@@ -8,6 +8,7 @@ import com.facebook.react.uimanager.ThemedReactContext
8
8
  import com.facebook.react.uimanager.annotations.ReactProp
9
9
  import com.facebook.react.uimanager.events.RCTEventEmitter
10
10
  import com.sellwild.sdk.AdSize
11
+ import com.sellwild.sdk.SellwildAdStack
11
12
  import com.sellwild.sdk.SellwildAdView
12
13
  import com.sellwild.sdk.SellwildConfig
13
14
  import org.json.JSONObject
@@ -52,6 +53,9 @@ class SellwildBannerViewManager : SimpleViewManager<SellwildAdView>() {
52
53
  var config: ReadableMap? = null,
53
54
  var size: String? = null,
54
55
  var zoneId: String? = null,
56
+ // Resolved ad stack ('both' | 'gamOnly' | 'prebidOnly'), computed in JS
57
+ // and applied as the native override.
58
+ var adStack: String? = null,
55
59
  // Guards against running fresh auctions on every JS re-render. We
56
60
  // only call setup() + load() once per identity tuple. Refresh of
57
61
  // the rendered ad is driven by the SDK's internal timer.
@@ -83,6 +87,14 @@ class SellwildBannerViewManager : SimpleViewManager<SellwildAdView>() {
83
87
  }
84
88
  emit(reactContext, adView, "onAdFailed", payload)
85
89
  }
90
+
91
+ override fun onAdResize(adView: SellwildAdView, width: Int, height: Int) {
92
+ val payload = com.facebook.react.bridge.Arguments.createMap().apply {
93
+ putInt("width", width)
94
+ putInt("height", height)
95
+ }
96
+ emit(reactContext, adView, "onAdResize", payload)
97
+ }
86
98
  }
87
99
  return view
88
100
  }
@@ -102,6 +114,11 @@ class SellwildBannerViewManager : SimpleViewManager<SellwildAdView>() {
102
114
  pendingFor(view).zoneId = value
103
115
  }
104
116
 
117
+ @ReactProp(name = "adStack")
118
+ fun setAdStack(view: SellwildAdView, value: String?) {
119
+ pendingFor(view).adStack = value
120
+ }
121
+
105
122
  override fun onAfterUpdateTransaction(view: SellwildAdView) {
106
123
  super.onAfterUpdateTransaction(view)
107
124
 
@@ -113,11 +130,14 @@ class SellwildBannerViewManager : SimpleViewManager<SellwildAdView>() {
113
130
  val adSize = adSizeFromLabel(sizeLabel) ?: return
114
131
 
115
132
  // Skip if the props identity hasn't changed since last apply.
116
- val key = "$sizeLabel|$zoneId|${configMap.hashCode()}"
133
+ val key = "$sizeLabel|$zoneId|${p.adStack}|${configMap.hashCode()}"
117
134
  if (p.lastAppliedKey == key) return
118
135
  p.lastAppliedKey = key
119
136
 
120
137
  val config = configFromMap(configMap)
138
+ // JS resolves the stack from config and passes it as the override so
139
+ // RN is deterministic; native still reads the raw `remote` for the rest.
140
+ view.adStackOverride = p.adStack?.let { SellwildAdStack.parse(it) }
121
141
  view.setup(config, adSize, zoneId)
122
142
  view.load()
123
143
  }
@@ -136,6 +156,7 @@ class SellwildBannerViewManager : SimpleViewManager<SellwildAdView>() {
136
156
  .put("onAdImpression", MapBuilder.of("registrationName", "onAdImpression"))
137
157
  .put("onAdClicked", MapBuilder.of("registrationName", "onAdClicked"))
138
158
  .put("onAdFailed", MapBuilder.of("registrationName", "onAdFailed"))
159
+ .put("onAdResize", MapBuilder.of("registrationName", "onAdResize"))
139
160
  .build()
140
161
  }
141
162
 
@@ -174,6 +195,10 @@ class SellwildBannerViewManager : SimpleViewManager<SellwildAdView>() {
174
195
  val appStoreUrl = if (map.hasKey("appStoreUrl")) map.getString("appStoreUrl") else null
175
196
  val gamTag = if (map.hasKey("gamTag")) map.getString("gamTag") else null
176
197
  val debug = map.hasKey("debug") && map.getBoolean("debug")
198
+ val pbsDebug = map.hasKey("pbsDebug") && map.getBoolean("pbsDebug")
199
+ val geo = RnGeo.readableMapToGeo(
200
+ if (map.hasKey("geo") && !map.isNull("geo")) map.getMap("geo") else null
201
+ )
177
202
  val adRefreshMax = if (map.hasKey("adRefreshMax")) map.getInt("adRefreshMax") else 0
178
203
  val adRefreshMaxMobile = if (map.hasKey("adRefreshMaxMobile")) map.getInt("adRefreshMaxMobile") else 0
179
204
  val adRefreshIntervalMs = if (map.hasKey("adRefreshIntervalMs")) map.getDouble("adRefreshIntervalMs").toLong() else 30_000L
@@ -190,6 +215,8 @@ class SellwildBannerViewManager : SimpleViewManager<SellwildAdView>() {
190
215
  appStoreUrl = appStoreUrl,
191
216
  gamTag = gamTag,
192
217
  debug = debug,
218
+ pbsDebug = pbsDebug,
219
+ geo = geo,
193
220
  adRefreshMax = adRefreshMax,
194
221
  adRefreshMaxMobile = adRefreshMaxMobile,
195
222
  adRefreshIntervalMs = adRefreshIntervalMs,
@@ -162,6 +162,8 @@ class SellwildFeedViewManager : SimpleViewManager<SellwildFeedView>() {
162
162
  if (map.hasKey("appStoreUrl")) config = config.copy(appStoreUrl = map.getString("appStoreUrl"))
163
163
  if (map.hasKey("gamTag")) config = config.copy(gamTag = map.getString("gamTag"))
164
164
  if (map.hasKey("debug")) config = config.copy(debug = map.getBoolean("debug"))
165
+ if (map.hasKey("pbsDebug")) config = config.copy(pbsDebug = map.getBoolean("pbsDebug"))
166
+ if (map.hasKey("geo") && !map.isNull("geo")) config = config.copy(geo = RnGeo.readableMapToGeo(map.getMap("geo")))
165
167
  if (map.hasKey("adRefreshMax")) config = config.copy(adRefreshMax = map.getInt("adRefreshMax"))
166
168
  if (map.hasKey("adRefreshMaxMobile")) config = config.copy(adRefreshMaxMobile = map.getInt("adRefreshMaxMobile"))
167
169
  if (map.hasKey("adRefreshIntervalMs")) config = config.copy(adRefreshIntervalMs = map.getDouble("adRefreshIntervalMs").toLong())
@@ -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,
@@ -14,11 +14,13 @@
14
14
  RCT_EXPORT_VIEW_PROPERTY(config, NSDictionary)
15
15
  RCT_EXPORT_VIEW_PROPERTY(size, NSString)
16
16
  RCT_EXPORT_VIEW_PROPERTY(zoneId, NSString)
17
+ RCT_EXPORT_VIEW_PROPERTY(adStack, NSString)
17
18
 
18
19
  // Direct events emitted to JS callbacks.
19
20
  RCT_EXPORT_VIEW_PROPERTY(onAdLoaded, RCTDirectEventBlock)
20
21
  RCT_EXPORT_VIEW_PROPERTY(onAdImpression, RCTDirectEventBlock)
21
22
  RCT_EXPORT_VIEW_PROPERTY(onAdClicked, RCTDirectEventBlock)
22
23
  RCT_EXPORT_VIEW_PROPERTY(onAdFailed, RCTDirectEventBlock)
24
+ RCT_EXPORT_VIEW_PROPERTY(onAdResize, RCTDirectEventBlock)
23
25
 
24
26
  @end
@@ -39,6 +39,9 @@ final class SellwildBannerHostView: UIView, SellwildAdViewDelegate {
39
39
  @objc var config: NSDictionary? { didSet { needsApply = true } }
40
40
  @objc var size: NSString? { didSet { needsApply = true } }
41
41
  @objc var zoneId: NSString? { didSet { needsApply = true } }
42
+ /// Resolved ad stack ('both' | 'gamOnly' | 'prebidOnly'), computed in JS.
43
+ /// Applied as the native override so RN is driven deterministically by JS.
44
+ @objc var adStack: NSString? { didSet { needsApply = true } }
42
45
 
43
46
  // MARK: RN events
44
47
 
@@ -46,6 +49,8 @@ final class SellwildBannerHostView: UIView, SellwildAdViewDelegate {
46
49
  @objc var onAdImpression: RCTDirectEventBlock?
47
50
  @objc var onAdClicked: RCTDirectEventBlock?
48
51
  @objc var onAdFailed: RCTDirectEventBlock?
52
+ /// Emitted with the rendered creative size so JS can resize the slot.
53
+ @objc var onAdResize: RCTDirectEventBlock?
49
54
 
50
55
  // MARK: Internals
51
56
 
@@ -79,7 +84,8 @@ final class SellwildBannerHostView: UIView, SellwildAdViewDelegate {
79
84
  // Skip if the props identity hasn't changed since last apply —
80
85
  // refresh of the rendered ad is driven by the SDK's internal
81
86
  // timer, not by JS re-renders.
82
- let key = "\(sizeStr)|\(zid)|\(cfgMap.hash)"
87
+ let stackStr = (adStack as String?) ?? ""
88
+ let key = "\(sizeStr)|\(zid)|\(stackStr)|\(cfgMap.hash)"
83
89
  if lastAppliedKey == key { return }
84
90
  lastAppliedKey = key
85
91
 
@@ -90,14 +96,19 @@ final class SellwildBannerHostView: UIView, SellwildAdViewDelegate {
90
96
  adView?.removeFromSuperview()
91
97
 
92
98
  let view = SellwildAdView(config: sellwildConfig, adSize: adSize, zoneId: zid)
99
+ view.adStackOverride = (adStack as String?).flatMap { SellwildAdStack.parse($0) }
93
100
  view.delegate = self
94
101
  view.translatesAutoresizingMaskIntoConstraints = false
95
102
  addSubview(view)
103
+ // Fill the RN-sized host rather than hard-pinning to the primary size —
104
+ // the host's dimensions are driven by JS style, which the JS component
105
+ // updates from the onAdResize event so the slot tracks the actual
106
+ // rendered creative (multi-size fallback, video, capped native).
96
107
  NSLayoutConstraint.activate([
97
108
  view.leadingAnchor.constraint(equalTo: leadingAnchor),
98
109
  view.topAnchor.constraint(equalTo: topAnchor),
99
- view.widthAnchor.constraint(equalToConstant: adSize.cgSize.width),
100
- view.heightAnchor.constraint(equalToConstant: adSize.cgSize.height),
110
+ view.trailingAnchor.constraint(equalTo: trailingAnchor),
111
+ view.bottomAnchor.constraint(equalTo: bottomAnchor),
101
112
  ])
102
113
  adView = view
103
114
  view.load()
@@ -121,6 +132,10 @@ final class SellwildBannerHostView: UIView, SellwildAdViewDelegate {
121
132
  onAdFailed?(["message": error.localizedDescription])
122
133
  }
123
134
 
135
+ func sellwildAdView(_ adView: SellwildAdView, didRenderWithSize size: CGSize) {
136
+ onAdResize?(["width": size.width, "height": size.height])
137
+ }
138
+
124
139
  // MARK: Config marshalling
125
140
 
126
141
  /// Build a minimal SellwildConfig from a JS dictionary. Only the
@@ -134,8 +149,10 @@ final class SellwildBannerHostView: UIView, SellwildAdViewDelegate {
134
149
 
135
150
  if let v = map["appBundleId"] as? String { cfg.appBundleId = v }
136
151
  if let v = map["appStoreUrl"] as? String { cfg.appStoreUrl = v }
152
+ if let geoMap = map["geo"] as? [String: Any] { cfg.geo = SellwildGeo(bridged: geoMap) }
137
153
  if let v = map["gamTag"] as? String { cfg.gamTag = v }
138
154
  if let v = map["debug"] as? Bool { cfg.debug = v }
155
+ if let v = map["pbsDebug"] as? Bool { cfg.pbsDebug = v }
139
156
  if let v = map["adRefreshMax"] as? Int { cfg.adRefreshMax = v }
140
157
  if let v = map["adRefreshMaxMobile"] as? Int { cfg.adRefreshMaxMobile = v }
141
158
 
@@ -149,8 +149,10 @@ final class SellwildFeedHostView: UIView, SellwildFeedViewDelegate {
149
149
  if let v = map["slug"] as? String { cfg.slug = v }
150
150
  if let v = map["appBundleId"] as? String { cfg.appBundleId = v }
151
151
  if let v = map["appStoreUrl"] as? String { cfg.appStoreUrl = v }
152
+ if let geoMap = map["geo"] as? [String: Any] { cfg.geo = SellwildGeo(bridged: geoMap) }
152
153
  if let v = map["gamTag"] as? String { cfg.gamTag = v }
153
154
  if let v = map["debug"] as? Bool { cfg.debug = v }
155
+ if let v = map["pbsDebug"] as? Bool { cfg.pbsDebug = v }
154
156
  if let v = map["adRefreshMax"] as? Int { cfg.adRefreshMax = v }
155
157
  if let v = map["adRefreshMaxMobile"] as? Int { cfg.adRefreshMaxMobile = v }
156
158
  if let v = map["listingsUrl"] as? String { cfg.listingsUrl = v }
@@ -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.3.7",
3
+ "version": "1.5.0",
4
4
  "description": "Sellwild mobile advertising SDK for React Native",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -1,6 +1,6 @@
1
1
  import React from 'react'
2
2
  import { Platform, requireNativeComponent, UIManager, ViewStyle, View, Text, StyleSheet, NativeSyntheticEvent } from 'react-native'
3
- import type { SellwildConfig, AdSize } from '@sellwild/sdk-core'
3
+ import { resolveAdStack, type SellwildConfig, type AdSize } from '@sellwild/sdk-core'
4
4
 
5
5
  // Standard IAB mobile ad sizes — used to lock the host View dimensions.
6
6
  // The actual ad size is also propagated to native as the `size` prop label.
@@ -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 —
@@ -29,11 +52,19 @@ interface NativeBannerProps {
29
52
  config: object
30
53
  size: string
31
54
  zoneId: string
55
+ /**
56
+ * Resolved ad stack ('both' | 'gamOnly' | 'prebidOnly'). Computed in JS from
57
+ * the config so RN is deterministic; native treats it as the highest-priority
58
+ * override. The raw `remote` payload still flows through `config` for
59
+ * everything else (bidders, GAM tag, etc.).
60
+ */
61
+ adStack: string
32
62
  style?: ViewStyle
33
63
  onAdLoaded?: (e: NativeSyntheticEvent<{}>) => void
34
64
  onAdImpression?: (e: NativeSyntheticEvent<{ zoneId: string }>) => void
35
65
  onAdClicked?: (e: NativeSyntheticEvent<{}>) => void
36
66
  onAdFailed?: (e: NativeSyntheticEvent<{ message: string }>) => void
67
+ onAdResize?: (e: NativeSyntheticEvent<{ width: number; height: number }>) => void
37
68
  }
38
69
 
39
70
  const NativeBanner = (() => {
@@ -69,7 +100,40 @@ export function SellwildBanner({
69
100
  onError,
70
101
  }: SellwildBannerProps) {
71
102
  const dim = AD_DIMENSIONS[size]
72
- const containerStyle: ViewStyle = { width: dim.width, height: dim.height }
103
+
104
+ // The widest/tallest size the auction may return for this placement: the
105
+ // primary plus any BANNER_SIZES / BANNER_SIZES_BY_ZONE fallbacks. We reserve
106
+ // this as the slot's baseline so a wider or taller fallback creative — e.g. a
107
+ // 320x50 winning a 300-wide MREC request, where 320 > 300 — never clips
108
+ // before, or without, the onAdResize callback. This also covers the Android
109
+ // prebidOnly path, whose rendering BannerView doesn't surface the winning
110
+ // creative size (so onAdResize can't shrink it back down there).
111
+ const baseline = React.useMemo(() => {
112
+ const remote = (config.remote ?? {}) as Record<string, unknown>
113
+ const byZone = remote['BANNER_SIZES_BY_ZONE']
114
+ const zoned = byZone && typeof byZone === 'object'
115
+ ? (byZone as Record<string, unknown>)[String(zoneId)]
116
+ : undefined
117
+ const sizes = [dim, ...parseSizeList(zoned ?? remote['BANNER_SIZES'])]
118
+ return {
119
+ width: Math.max(...sizes.map((s) => s.width)),
120
+ height: Math.max(...sizes.map((s) => s.height)),
121
+ }
122
+ }, [dim, zoneId, config.remote])
123
+
124
+ // The slot starts at the reserved baseline, then tracks whatever the native
125
+ // side actually renders (onAdResize): a multi-size fallback creative, an
126
+ // outstream video, or the capped native template. Where the actual size is
127
+ // reported it shrinks the slot to fit; where it isn't (Android prebidOnly)
128
+ // the baseline reservation prevents a clip.
129
+ const [rendered, setRendered] = React.useState<{ width: number; height: number } | null>(null)
130
+ // Reset to the baseline when the placement identity changes.
131
+ React.useEffect(() => { setRendered(null) }, [size, zoneId])
132
+
133
+ const containerStyle: ViewStyle = {
134
+ width: rendered?.width ?? baseline.width,
135
+ height: rendered?.height ?? baseline.height,
136
+ }
73
137
 
74
138
  if (!NativeBanner) {
75
139
  // Native module not registered. Most common cause: iOS bridge not yet
@@ -95,8 +159,10 @@ export function SellwildBanner({
95
159
  partnerCode: config.partnerCode,
96
160
  appBundleId: config.appBundleId,
97
161
  appStoreUrl: config.appStoreUrl,
162
+ geo: config.geo,
98
163
  gamTag: config.gamTag,
99
164
  debug: config.debug,
165
+ pbsDebug: config.pbsDebug,
100
166
  adRefreshMax: config.adRefreshMax,
101
167
  adRefreshMaxMobile: config.adRefreshMaxMobile,
102
168
  adRefreshIntervalMs: config.adRefreshInterval,
@@ -110,6 +176,7 @@ export function SellwildBanner({
110
176
  config={nativeConfig}
111
177
  size={size}
112
178
  zoneId={String(zoneId)}
179
+ adStack={resolveAdStack(config, zoneId)}
113
180
  onAdLoaded={() => {
114
181
  // No-op event hook today; surfaced for future fill metrics.
115
182
  }}
@@ -119,6 +186,10 @@ export function SellwildBanner({
119
186
  const msg = e.nativeEvent?.message ?? 'Ad failed'
120
187
  onError?.(new Error(msg))
121
188
  }}
189
+ onAdResize={(e: NativeSyntheticEvent<{ width: number; height: number }>) => {
190
+ const { width, height } = e.nativeEvent ?? { width: 0, height: 0 }
191
+ if (width > 0 && height > 0) setRendered({ width, height })
192
+ }}
122
193
  />
123
194
  )
124
195
  }
@@ -117,8 +117,10 @@ export function SellwildFeed({
117
117
  slug: config.slug,
118
118
  appBundleId: config.appBundleId,
119
119
  appStoreUrl: config.appStoreUrl,
120
+ geo: config.geo,
120
121
  gamTag: config.gamTag,
121
122
  debug: config.debug,
123
+ pbsDebug: config.pbsDebug,
122
124
  adRefreshMax: config.adRefreshMax,
123
125
  adRefreshMaxMobile: config.adRefreshMaxMobile,
124
126
  adRefreshIntervalMs: config.adRefreshInterval,
@@ -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,