@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.
- package/android/build.gradle +1 -1
- package/android/src/main/java/com/sellwild/rnsdk/RnGeo.kt +31 -0
- package/android/src/main/java/com/sellwild/rnsdk/RnPrebidServer.kt +36 -0
- package/android/src/main/java/com/sellwild/rnsdk/SellwildBannerViewManager.kt +68 -0
- package/android/src/main/java/com/sellwild/rnsdk/SellwildFeedViewManager.kt +43 -0
- package/android/src/main/java/com/sellwild/rnsdk/SellwildModule.kt +67 -0
- package/android/src/main/java/com/sellwild/rnsdk/SellwildSdkPackage.kt +2 -1
- package/ios/SellwildBannerViewManager.m +2 -0
- package/ios/SellwildBannerViewManager.swift +42 -2
- package/ios/SellwildFeedViewManager.m +3 -0
- package/ios/SellwildFeedViewManager.swift +43 -2
- package/ios/SellwildRNModule.m +10 -0
- package/ios/SellwildRNModule.swift +42 -0
- package/package.json +1 -1
- package/src/SellwildBanner.tsx +76 -1
- package/src/SellwildFeed.tsx +56 -2
- package/src/commands.ts +37 -0
- package/src/index.ts +6 -0
package/android/build.gradle
CHANGED
|
@@ -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.
|
|
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> =
|
|
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.
|
|
105
|
-
view.
|
|
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
|
-
|
|
161
|
-
|
|
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
package/src/SellwildBanner.tsx
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|
package/src/SellwildFeed.tsx
CHANGED
|
@@ -29,12 +29,15 @@ const NATIVE_NAME = 'SellwildFeedView'
|
|
|
29
29
|
|
|
30
30
|
interface NativeFeedProps {
|
|
31
31
|
config: object
|
|
32
|
+
scrollEnabled?: boolean
|
|
32
33
|
style?: StyleProp<ViewStyle>
|
|
33
34
|
onFeedLoaded?: (e: NativeSyntheticEvent<{}>) => void
|
|
34
35
|
onListingTap?: (e: NativeSyntheticEvent<{ listing: SellwildListing }>) => void
|
|
35
36
|
onAdImpression?: (e: NativeSyntheticEvent<{ zoneId: string }>) => void
|
|
37
|
+
onHouseAdImpression?: (e: NativeSyntheticEvent<{ zoneId: string }>) => void
|
|
36
38
|
onAdClicked?: (e: NativeSyntheticEvent<{ zoneId: string }>) => void
|
|
37
39
|
onFeedError?: (e: NativeSyntheticEvent<{ message: string }>) => void
|
|
40
|
+
onContentSizeChange?: (e: NativeSyntheticEvent<{ width?: number; height: number }>) => void
|
|
38
41
|
}
|
|
39
42
|
|
|
40
43
|
const NativeFeed = (() => {
|
|
@@ -60,6 +63,24 @@ export interface SellwildFeedProps {
|
|
|
60
63
|
/** Optional style override. The feed expands to its container by default. */
|
|
61
64
|
style?: ViewStyle
|
|
62
65
|
|
|
66
|
+
/**
|
|
67
|
+
* When `false`, the feed's own scrolling is disabled and the component
|
|
68
|
+
* sizes its own container from the reported content height, so it can be
|
|
69
|
+
* embedded inside a parent `ScrollView` (single-scroll pages, e.g.
|
|
70
|
+
* alongside a Taboola feed). The feed then renders every row (no
|
|
71
|
+
* virtualization) and pull-to-refresh is disabled — the host drives
|
|
72
|
+
* refresh. Defaults to `true`; existing full-screen integrations are
|
|
73
|
+
* unaffected.
|
|
74
|
+
*/
|
|
75
|
+
scrollEnabled?: boolean
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Fired whenever the feed's content height changes. Use it to size the
|
|
79
|
+
* feed's container when embedding with `scrollEnabled={false}`; when
|
|
80
|
+
* scroll is disabled the component also applies the height itself.
|
|
81
|
+
*/
|
|
82
|
+
onContentSizeChange?: (e: { width?: number; height: number }) => void
|
|
83
|
+
|
|
63
84
|
/** Fired once after the initial listings fetch resolves successfully. */
|
|
64
85
|
onLoad?: () => void
|
|
65
86
|
|
|
@@ -73,6 +94,12 @@ export interface SellwildFeedProps {
|
|
|
73
94
|
/** Fired when a native ad row records an impression. */
|
|
74
95
|
onAdImpression?: (zoneId: string) => void
|
|
75
96
|
|
|
97
|
+
/**
|
|
98
|
+
* Fired when a house ad backfilled an empty ad row (a no-fill). NOT a paid
|
|
99
|
+
* impression — track it separately. See the `MOBILE_HOUSE_AD_*` config keys.
|
|
100
|
+
*/
|
|
101
|
+
onHouseAdImpression?: (zoneId: string) => void
|
|
102
|
+
|
|
76
103
|
/** Fired when a native ad row is clicked. */
|
|
77
104
|
onAdClicked?: (zoneId: string) => void
|
|
78
105
|
|
|
@@ -83,18 +110,30 @@ export interface SellwildFeedProps {
|
|
|
83
110
|
export function SellwildFeed({
|
|
84
111
|
config,
|
|
85
112
|
style,
|
|
113
|
+
scrollEnabled = true,
|
|
114
|
+
onContentSizeChange,
|
|
86
115
|
onLoad,
|
|
87
116
|
onListingTap,
|
|
88
117
|
onAdImpression,
|
|
118
|
+
onHouseAdImpression,
|
|
89
119
|
onAdClicked,
|
|
90
120
|
onError,
|
|
91
121
|
}: SellwildFeedProps) {
|
|
122
|
+
// When embedded (scrollEnabled === false) the parent ScrollView owns
|
|
123
|
+
// scrolling, so we size our own container from the native-reported content
|
|
124
|
+
// height rather than filling with flex:1.
|
|
125
|
+
const [contentHeight, setContentHeight] = React.useState<number | null>(null)
|
|
126
|
+
const embedded = scrollEnabled === false
|
|
127
|
+
const containerStyle: StyleProp<ViewStyle> = embedded
|
|
128
|
+
? [contentHeight != null ? { height: contentHeight } : undefined, style]
|
|
129
|
+
: [styles.fill, style]
|
|
130
|
+
|
|
92
131
|
if (!NativeFeed) {
|
|
93
132
|
// Native module not registered. Most common cause: the host app was
|
|
94
133
|
// built before the @sellwild/react-native-sdk autolink ran, or this
|
|
95
134
|
// is being rendered in a JS-only test environment.
|
|
96
135
|
return (
|
|
97
|
-
<View style={[
|
|
136
|
+
<View style={[containerStyle, __DEV__ ? styles.devPlaceholder : undefined]}>
|
|
98
137
|
{__DEV__ ? (
|
|
99
138
|
<Text style={styles.devText}>
|
|
100
139
|
Sellwild native feed not available on {Platform.OS} (yet)
|
|
@@ -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={
|
|
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
|
}}
|
package/src/commands.ts
ADDED
|
@@ -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,
|