@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.
- package/SellwildSDK-RN.podspec +1 -1
- 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/SellwildBannerViewManager.kt +28 -1
- package/android/src/main/java/com/sellwild/rnsdk/SellwildFeedViewManager.kt +2 -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 +20 -3
- package/ios/SellwildFeedViewManager.swift +2 -0
- package/ios/SellwildRNModule.m +10 -0
- package/ios/SellwildRNModule.swift +42 -0
- package/package.json +1 -1
- package/src/SellwildBanner.tsx +73 -2
- package/src/SellwildFeed.tsx +2 -0
- package/src/commands.ts +37 -0
- package/src/index.ts +6 -0
package/SellwildSDK-RN.podspec
CHANGED
|
@@ -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.
|
|
31
|
+
s.dependency 'SellwildSDK', '>= 1.4.0'
|
|
32
32
|
s.dependency 'React-Core'
|
|
33
33
|
end
|
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.
|
|
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> =
|
|
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
|
|
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.
|
|
100
|
-
view.
|
|
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
package/src/SellwildBanner.tsx
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
}
|
package/src/SellwildFeed.tsx
CHANGED
|
@@ -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,
|
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,
|