@sellwild/react-native-sdk 1.7.3 → 1.8.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/README.md +2 -2
- package/SellwildSDK-RN.podspec +3 -1
- package/android/build.gradle +3 -1
- package/android/src/main/java/com/sellwild/rnsdk/SellwildBannerViewManager.kt +44 -3
- package/android/src/main/java/com/sellwild/rnsdk/SellwildFeedViewManager.kt +71 -10
- package/android/src/main/java/com/sellwild/rnsdk/SellwildModule.kt +18 -0
- package/ios/SellwildBannerViewManager.m +1 -0
- package/ios/SellwildBannerViewManager.swift +14 -2
- package/ios/SellwildFeedViewManager.m +1 -0
- package/ios/SellwildFeedViewManager.swift +18 -11
- package/ios/SellwildRNModule.m +1 -0
- package/ios/SellwildRNModule.swift +14 -0
- package/package.json +4 -6
- package/src/SellwildBanner.tsx +15 -28
- package/src/SellwildFeed.tsx +47 -9
- package/src/appIdentity.ts +2 -3
- package/src/commands.ts +30 -1
- package/src/index.ts +7 -5
- package/src/nativeConfig.ts +34 -0
- package/src/SellwildWidget.tsx +0 -145
- package/src/htmlBuilder.ts +0 -383
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@ Sellwild mobile advertising SDK for React Native. Server-side header bidding thr
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
npm install @sellwild/react-native-sdk
|
|
8
|
+
npm install @sellwild/react-native-sdk
|
|
9
9
|
cd ios && pod install && cd ..
|
|
10
10
|
```
|
|
11
11
|
|
|
@@ -67,7 +67,7 @@ export default function App() {
|
|
|
67
67
|
## Components
|
|
68
68
|
|
|
69
69
|
- **`SellwildBanner`** — Display ad (300x250, 320x50, 728x90)
|
|
70
|
-
- **`
|
|
70
|
+
- **`SellwildFeed`** — All-in-one native feed: listings interleaved with native ads
|
|
71
71
|
- **`SellwildListingCard`** — Individual listing card component
|
|
72
72
|
- **`useSellwildListings`** — Hook for fetching listings data
|
|
73
73
|
|
package/SellwildSDK-RN.podspec
CHANGED
|
@@ -28,6 +28,8 @@ 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
|
-
|
|
31
|
+
# Floor tracks this package's version (lockstep releases): the bridge uses
|
|
32
|
+
# SellwildGeo / SellwildEid / prewarm, which older SellwildSDK pods lack.
|
|
33
|
+
s.dependency 'SellwildSDK', ">= #{package['version']}"
|
|
32
34
|
s.dependency 'React-Core'
|
|
33
35
|
end
|
package/android/build.gradle
CHANGED
|
@@ -76,7 +76,9 @@ 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
|
-
|
|
79
|
+
// Keep in lockstep with the SDK release version: the bridge uses APIs
|
|
80
|
+
// (SellwildGeo, SellwildEid, SellwildSDK.prewarm) that older AARs lack.
|
|
81
|
+
implementation "com.sellwild:sdk:1.8.0"
|
|
80
82
|
}
|
|
81
83
|
|
|
82
84
|
// Force kotlin-stdlib down to the bridge's declared Kotlin version. The
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
package com.sellwild.rnsdk
|
|
2
2
|
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.view.View
|
|
3
5
|
import com.facebook.react.bridge.ReactContext
|
|
4
6
|
import com.facebook.react.bridge.ReadableMap
|
|
5
7
|
import com.facebook.react.common.MapBuilder
|
|
@@ -15,6 +17,32 @@ import com.sellwild.sdk.SellwildGrowthCodeConfig
|
|
|
15
17
|
import com.sellwild.sdk.SellwildLocalizedListingsConfig
|
|
16
18
|
import org.json.JSONObject
|
|
17
19
|
|
|
20
|
+
/**
|
|
21
|
+
* React Native (Paper) only lays out views in its own shadow tree; the child
|
|
22
|
+
* views a native component adds itself — here the GAM/Prebid banner and the
|
|
23
|
+
* WebView the viewability tracker watches — are never measured or laid out, so
|
|
24
|
+
* they render 0-size and fail the impression viewability check
|
|
25
|
+
* (getWidth()>0 / getGlobalVisibleRect), which means NO viewable impression and
|
|
26
|
+
* NO burl fires. Re-run measure + layout on our RN-assigned bounds whenever a
|
|
27
|
+
* child requests layout (e.g. when the ad renders asynchronously). This is the
|
|
28
|
+
* standard RN native-view fix (react-native-webview/maps/video do the same).
|
|
29
|
+
* Not needed on iOS RN, which lays subviews out via Auto Layout constraints.
|
|
30
|
+
*/
|
|
31
|
+
internal class RnSellwildAdView(context: Context) : SellwildAdView(context) {
|
|
32
|
+
private val measureAndLayout = Runnable {
|
|
33
|
+
measure(
|
|
34
|
+
View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
|
|
35
|
+
View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY),
|
|
36
|
+
)
|
|
37
|
+
layout(left, top, right, bottom)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
override fun requestLayout() {
|
|
41
|
+
super.requestLayout()
|
|
42
|
+
post(measureAndLayout)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
18
46
|
/**
|
|
19
47
|
* Bridges the JS <SellwildBanner> component to the native
|
|
20
48
|
* com.sellwild.sdk.SellwildAdView.
|
|
@@ -62,10 +90,13 @@ class SellwildBannerViewManager : SimpleViewManager<SellwildAdView>() {
|
|
|
62
90
|
// only call setup() + load() once per identity tuple. Refresh of
|
|
63
91
|
// the rendered ad is driven by the SDK's internal timer.
|
|
64
92
|
var lastAppliedKey: String? = null,
|
|
93
|
+
// Last applied config, compared by value. A hashCode() in the key is
|
|
94
|
+
// only a probabilistic match (collisions ⇒ a real change is skipped).
|
|
95
|
+
var lastAppliedConfig: Map<String, Any?>? = null,
|
|
65
96
|
)
|
|
66
97
|
|
|
67
98
|
override fun createViewInstance(reactContext: ThemedReactContext): SellwildAdView {
|
|
68
|
-
val view =
|
|
99
|
+
val view = RnSellwildAdView(reactContext)
|
|
69
100
|
pending[view] = PendingProps()
|
|
70
101
|
view.listener = object : SellwildAdView.Listener {
|
|
71
102
|
override fun onAdLoaded(adView: SellwildAdView) {
|
|
@@ -90,6 +121,13 @@ class SellwildBannerViewManager : SimpleViewManager<SellwildAdView>() {
|
|
|
90
121
|
emit(reactContext, adView, "onAdClicked", null)
|
|
91
122
|
}
|
|
92
123
|
|
|
124
|
+
override fun onAdBlocked(adView: SellwildAdView, zoneId: String) {
|
|
125
|
+
val payload = com.facebook.react.bridge.Arguments.createMap().apply {
|
|
126
|
+
putString("zoneId", zoneId)
|
|
127
|
+
}
|
|
128
|
+
emit(reactContext, adView, "onAdBlocked", payload)
|
|
129
|
+
}
|
|
130
|
+
|
|
93
131
|
override fun onAdFailed(adView: SellwildAdView, message: String) {
|
|
94
132
|
val payload = com.facebook.react.bridge.Arguments.createMap().apply {
|
|
95
133
|
putString("message", message)
|
|
@@ -139,9 +177,11 @@ class SellwildBannerViewManager : SimpleViewManager<SellwildAdView>() {
|
|
|
139
177
|
val adSize = adSizeFromLabel(sizeLabel) ?: return
|
|
140
178
|
|
|
141
179
|
// Skip if the props identity hasn't changed since last apply.
|
|
142
|
-
val key = "$sizeLabel|$zoneId|${p.adStack}
|
|
143
|
-
|
|
180
|
+
val key = "$sizeLabel|$zoneId|${p.adStack}"
|
|
181
|
+
val configValue = configMap.toHashMap()
|
|
182
|
+
if (p.lastAppliedKey == key && p.lastAppliedConfig == configValue) return
|
|
144
183
|
p.lastAppliedKey = key
|
|
184
|
+
p.lastAppliedConfig = configValue
|
|
145
185
|
|
|
146
186
|
val config = configFromMap(configMap)
|
|
147
187
|
// JS resolves the stack from config and passes it as the override so
|
|
@@ -171,6 +211,7 @@ class SellwildBannerViewManager : SimpleViewManager<SellwildAdView>() {
|
|
|
171
211
|
.put("onAdClicked", MapBuilder.of("registrationName", "onAdClicked"))
|
|
172
212
|
.put("onAdFailed", MapBuilder.of("registrationName", "onAdFailed"))
|
|
173
213
|
.put("onAdResize", MapBuilder.of("registrationName", "onAdResize"))
|
|
214
|
+
.put("onAdBlocked", MapBuilder.of("registrationName", "onAdBlocked"))
|
|
174
215
|
.build()
|
|
175
216
|
}
|
|
176
217
|
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
package com.sellwild.rnsdk
|
|
2
2
|
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.view.View
|
|
3
5
|
import com.facebook.react.bridge.Arguments
|
|
4
6
|
import com.facebook.react.bridge.ReactContext
|
|
5
7
|
import com.facebook.react.bridge.ReadableMap
|
|
8
|
+
import com.facebook.react.bridge.ReadableType
|
|
6
9
|
import com.facebook.react.bridge.WritableMap
|
|
7
10
|
import com.facebook.react.common.MapBuilder
|
|
8
11
|
import com.facebook.react.uimanager.SimpleViewManager
|
|
@@ -17,6 +20,29 @@ import com.sellwild.sdk.SellwildLocalizedListingsConfig
|
|
|
17
20
|
import com.sellwild.sdk.SellwildSDK
|
|
18
21
|
import org.json.JSONObject
|
|
19
22
|
|
|
23
|
+
/**
|
|
24
|
+
* React Native (Paper) only lays out its own shadow-tree views; children a
|
|
25
|
+
* native component adds itself (the feed's RecyclerView + ad WebViews) are never
|
|
26
|
+
* measured/laid out, so they render 0-size and fail the impression viewability
|
|
27
|
+
* check — no viewable impression, no burl. Re-run measure + layout on our
|
|
28
|
+
* RN-assigned bounds whenever a child requests layout. Standard RN native-view
|
|
29
|
+
* fix; not needed on iOS RN (Auto Layout constraints handle it).
|
|
30
|
+
*/
|
|
31
|
+
internal class RnSellwildFeedView(context: Context) : SellwildFeedView(context) {
|
|
32
|
+
private val measureAndLayout = Runnable {
|
|
33
|
+
measure(
|
|
34
|
+
View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
|
|
35
|
+
View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY),
|
|
36
|
+
)
|
|
37
|
+
layout(left, top, right, bottom)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
override fun requestLayout() {
|
|
41
|
+
super.requestLayout()
|
|
42
|
+
post(measureAndLayout)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
20
46
|
/**
|
|
21
47
|
* Bridges the JS <SellwildFeed> component to the native
|
|
22
48
|
* com.sellwild.sdk.SellwildFeedView (all-in-one native feed: COL1-scheduled
|
|
@@ -27,6 +53,9 @@ import org.json.JSONObject
|
|
|
27
53
|
* The bridge re-runs the CDN decoder against `config.remote` so
|
|
28
54
|
* feed-specific fields (COL1, bgColor, mobileZids, listingsUrl, …)
|
|
29
55
|
* are populated identically to a native [SellwildSDK.configure] call.
|
|
56
|
+
* - scrollEnabled: bool — disable internal scrolling for embedding.
|
|
57
|
+
* - consumeListingTaps: bool — when true the host owns listing taps; the
|
|
58
|
+
* SDK does not open Custom Tabs.
|
|
30
59
|
*
|
|
31
60
|
* Events emitted to JS:
|
|
32
61
|
* - onFeedLoaded
|
|
@@ -48,11 +77,16 @@ class SellwildFeedViewManager : SimpleViewManager<SellwildFeedView>() {
|
|
|
48
77
|
|
|
49
78
|
private data class PendingProps(
|
|
50
79
|
var config: ReadableMap? = null,
|
|
51
|
-
|
|
80
|
+
// When true, listing taps are only forwarded to JS and the SDK does
|
|
81
|
+
// not open Custom Tabs. Read at tap time, so it applies live.
|
|
82
|
+
var consumeListingTaps: Boolean = false,
|
|
83
|
+
// Last applied config, compared by value. A hashCode() key is only a
|
|
84
|
+
// probabilistic match (collisions ⇒ a real config change is skipped).
|
|
85
|
+
var lastAppliedConfig: Map<String, Any?>? = null,
|
|
52
86
|
)
|
|
53
87
|
|
|
54
88
|
override fun createViewInstance(reactContext: ThemedReactContext): SellwildFeedView {
|
|
55
|
-
val view =
|
|
89
|
+
val view = RnSellwildFeedView(reactContext)
|
|
56
90
|
pending[view] = PendingProps()
|
|
57
91
|
view.listener = object : SellwildFeedView.Listener {
|
|
58
92
|
override fun onListingTap(listing: SellwildListing): Boolean {
|
|
@@ -60,8 +94,10 @@ class SellwildFeedViewManager : SimpleViewManager<SellwildFeedView>() {
|
|
|
60
94
|
putMap("listing", listingPayload(listing))
|
|
61
95
|
}
|
|
62
96
|
emit(reactContext, view, "onListingTap", payload)
|
|
63
|
-
//
|
|
64
|
-
|
|
97
|
+
// JS can't return a value through an (async) RN event, so the
|
|
98
|
+
// `consumeListingTaps` prop decides whether the SDK opens
|
|
99
|
+
// Custom Tabs (false, default) or leaves navigation to the host.
|
|
100
|
+
return pending[view]?.consumeListingTaps ?: false
|
|
65
101
|
}
|
|
66
102
|
|
|
67
103
|
override fun onAdImpression(zoneId: String) {
|
|
@@ -111,15 +147,20 @@ class SellwildFeedViewManager : SimpleViewManager<SellwildFeedView>() {
|
|
|
111
147
|
view.scrollEnabled = value
|
|
112
148
|
}
|
|
113
149
|
|
|
150
|
+
@ReactProp(name = "consumeListingTaps", defaultBoolean = false)
|
|
151
|
+
fun setConsumeListingTaps(view: SellwildFeedView, value: Boolean) {
|
|
152
|
+
pendingFor(view).consumeListingTaps = value
|
|
153
|
+
}
|
|
154
|
+
|
|
114
155
|
override fun onAfterUpdateTransaction(view: SellwildFeedView) {
|
|
115
156
|
super.onAfterUpdateTransaction(view)
|
|
116
157
|
|
|
117
158
|
val p = pending[view] ?: return
|
|
118
159
|
val configMap = p.config ?: return
|
|
119
160
|
|
|
120
|
-
val
|
|
121
|
-
if (p.
|
|
122
|
-
p.
|
|
161
|
+
val configValue = configMap.toHashMap()
|
|
162
|
+
if (p.lastAppliedConfig == configValue) return
|
|
163
|
+
p.lastAppliedConfig = configValue
|
|
123
164
|
|
|
124
165
|
val config = configFromMap(configMap)
|
|
125
166
|
view.setup(config)
|
|
@@ -128,6 +169,9 @@ class SellwildFeedViewManager : SimpleViewManager<SellwildFeedView>() {
|
|
|
128
169
|
|
|
129
170
|
override fun onDropViewInstance(view: SellwildFeedView) {
|
|
130
171
|
pending.remove(view)
|
|
172
|
+
// RN unmounted the feed for good: stop its ad rows' refresh and release
|
|
173
|
+
// the Activity (detach alone doesn't when MOBILE_PAUSE_REFRESH_DETACHED=false).
|
|
174
|
+
view.destroy()
|
|
131
175
|
super.onDropViewInstance(view)
|
|
132
176
|
}
|
|
133
177
|
|
|
@@ -160,6 +204,23 @@ class SellwildFeedViewManager : SimpleViewManager<SellwildFeedView>() {
|
|
|
160
204
|
companion object {
|
|
161
205
|
const val REACT_CLASS = "SellwildFeedView"
|
|
162
206
|
|
|
207
|
+
/**
|
|
208
|
+
* A zone id from JS: a string, or a number (core config's default is
|
|
209
|
+
* `0`, meaning unset). `getString` throws on a number, so read either
|
|
210
|
+
* type. Returns null for missing, empty, or `0`.
|
|
211
|
+
*/
|
|
212
|
+
internal fun zoneIdOrNull(map: ReadableMap, key: String): String? {
|
|
213
|
+
if (!map.hasKey(key) || map.isNull(key)) return null
|
|
214
|
+
val s = when (map.getType(key)) {
|
|
215
|
+
ReadableType.String -> map.getString(key)
|
|
216
|
+
ReadableType.Number -> map.getDouble(key).let { d ->
|
|
217
|
+
if (d % 1.0 == 0.0) d.toLong().toString() else d.toString()
|
|
218
|
+
}
|
|
219
|
+
else -> null
|
|
220
|
+
}?.trim()
|
|
221
|
+
return s?.takeIf { it.isNotEmpty() && it != "0" }
|
|
222
|
+
}
|
|
223
|
+
|
|
163
224
|
/**
|
|
164
225
|
* Build a full SellwildConfig from a JS map. The JS side passes the
|
|
165
226
|
* resolved CDN payload under `remote`; we re-run the canonical CDN
|
|
@@ -199,13 +260,13 @@ class SellwildFeedViewManager : SimpleViewManager<SellwildFeedView>() {
|
|
|
199
260
|
if (map.hasKey("slug")) config = config.copy(slug = map.getString("slug") ?: config.slug)
|
|
200
261
|
if (map.hasKey("listingsUrl")) config = config.copy(listingsUrl = map.getString("listingsUrl"))
|
|
201
262
|
if (map.hasKey("priceColor")) config = config.copy(priceColor = map.getString("priceColor") ?: config.priceColor)
|
|
202
|
-
|
|
203
|
-
|
|
263
|
+
zoneIdOrNull(map, "bannerZid")?.let { config = config.copy(bannerZid = it) }
|
|
264
|
+
zoneIdOrNull(map, "bottomBannerZid")?.let { config = config.copy(bottomBannerZid = it) }
|
|
204
265
|
// mobileZids / mobileBannerZid are OS-suffix-resolved by SellwildSDK.apply
|
|
205
266
|
// when `remote` is present; only fall back to the flat (OS-agnostic) JS
|
|
206
267
|
// values when there was no remote payload to resolve from (parity w/ iOS).
|
|
207
268
|
if (!map.hasKey("remote") || map.isNull("remote")) {
|
|
208
|
-
|
|
269
|
+
zoneIdOrNull(map, "mobileBannerZid")?.let { config = config.copy(mobileBannerZid = it) }
|
|
209
270
|
if (map.hasKey("mobileZids") && !map.isNull("mobileZids")) {
|
|
210
271
|
val arr = map.getArray("mobileZids")
|
|
211
272
|
if (arr != null) config = config.copy(mobileZids = (0 until arr.size()).mapNotNull { arr.getString(it) })
|
|
@@ -5,9 +5,11 @@ import com.facebook.react.bridge.ReactContextBaseJavaModule
|
|
|
5
5
|
import com.facebook.react.bridge.ReactMethod
|
|
6
6
|
import com.facebook.react.bridge.ReadableArray
|
|
7
7
|
import com.facebook.react.bridge.ReadableMap
|
|
8
|
+
import com.facebook.react.bridge.UiThreadUtil
|
|
8
9
|
import com.sellwild.sdk.SellwildEid
|
|
9
10
|
import com.sellwild.sdk.SellwildEidUid
|
|
10
11
|
import com.sellwild.sdk.SellwildPrebidMobile
|
|
12
|
+
import com.sellwild.sdk.SellwildSDK
|
|
11
13
|
|
|
12
14
|
/**
|
|
13
15
|
* React Native method module for the native Sellwild SDK's runtime setters.
|
|
@@ -42,6 +44,22 @@ class SellwildModule(reactContext: ReactApplicationContext) :
|
|
|
42
44
|
SellwildPrebidMobile.setExternalUserIds(toEids(eids))
|
|
43
45
|
}
|
|
44
46
|
|
|
47
|
+
/**
|
|
48
|
+
* JS: `SellwildRNModule.prewarm(nativeConfig)`. Pre-initializes the native ad
|
|
49
|
+
* stack (Prebid + ad server SDK) before the first ad view mounts, so the
|
|
50
|
+
* first impression doesn't incur cold-start init latency. Mirrors the native
|
|
51
|
+
* Android-only [SellwildSDK.prewarm]; idempotent. Runs on the main thread —
|
|
52
|
+
* the ad-SDK inits expect it. Reuses the banner manager's config mapping so
|
|
53
|
+
* the payload shape matches `<SellwildBanner config=...>`.
|
|
54
|
+
*/
|
|
55
|
+
@ReactMethod
|
|
56
|
+
fun prewarm(config: ReadableMap?) {
|
|
57
|
+
if (config == null) return
|
|
58
|
+
val ctx = reactApplicationContext
|
|
59
|
+
val cfg = SellwildBannerViewManager.configFromMap(config)
|
|
60
|
+
UiThreadUtil.runOnUiThread { SellwildSDK.prewarm(ctx, cfg) }
|
|
61
|
+
}
|
|
62
|
+
|
|
45
63
|
private fun toEids(arr: ReadableArray?): List<SellwildEid> {
|
|
46
64
|
if (arr == null) return emptyList()
|
|
47
65
|
val out = ArrayList<SellwildEid>()
|
|
@@ -23,5 +23,6 @@ RCT_EXPORT_VIEW_PROPERTY(onHouseAdImpression, RCTDirectEventBlock)
|
|
|
23
23
|
RCT_EXPORT_VIEW_PROPERTY(onAdClicked, RCTDirectEventBlock)
|
|
24
24
|
RCT_EXPORT_VIEW_PROPERTY(onAdFailed, RCTDirectEventBlock)
|
|
25
25
|
RCT_EXPORT_VIEW_PROPERTY(onAdResize, RCTDirectEventBlock)
|
|
26
|
+
RCT_EXPORT_VIEW_PROPERTY(onAdBlocked, RCTDirectEventBlock)
|
|
26
27
|
|
|
27
28
|
@end
|
|
@@ -53,11 +53,18 @@ final class SellwildBannerHostView: UIView, SellwildAdViewDelegate {
|
|
|
53
53
|
@objc var onAdFailed: RCTDirectEventBlock?
|
|
54
54
|
/// Emitted with the rendered creative size so JS can resize the slot.
|
|
55
55
|
@objc var onAdResize: RCTDirectEventBlock?
|
|
56
|
+
/// Boltive blocked the rendered creative (MOBILE_BOLTIVE_ENABLED only).
|
|
57
|
+
@objc var onAdBlocked: RCTDirectEventBlock?
|
|
56
58
|
|
|
57
59
|
// MARK: Internals
|
|
58
60
|
|
|
59
61
|
private var adView: SellwildAdView?
|
|
60
62
|
private var lastAppliedKey: String?
|
|
63
|
+
/// Last applied `config` map, compared by value (`isEqual:` ⇒
|
|
64
|
+
/// `isEqualToDictionary:`, deep). NOT `NSDictionary.hash` — that is
|
|
65
|
+
/// just the entry count, so a partnerCode / slug / geo / remote change
|
|
66
|
+
/// with the same key count would be silently ignored.
|
|
67
|
+
private var lastAppliedConfig: NSDictionary?
|
|
61
68
|
private var needsApply: Bool = false
|
|
62
69
|
|
|
63
70
|
override init(frame: CGRect) {
|
|
@@ -87,9 +94,10 @@ final class SellwildBannerHostView: UIView, SellwildAdViewDelegate {
|
|
|
87
94
|
// refresh of the rendered ad is driven by the SDK's internal
|
|
88
95
|
// timer, not by JS re-renders.
|
|
89
96
|
let stackStr = (adStack as String?) ?? ""
|
|
90
|
-
let key = "\(sizeStr)|\(zid)|\(stackStr)
|
|
91
|
-
if lastAppliedKey == key { return }
|
|
97
|
+
let key = "\(sizeStr)|\(zid)|\(stackStr)"
|
|
98
|
+
if lastAppliedKey == key, lastAppliedConfig?.isEqual(cfgMap) == true { return }
|
|
92
99
|
lastAppliedKey = key
|
|
100
|
+
lastAppliedConfig = cfgMap.copy() as? NSDictionary
|
|
93
101
|
|
|
94
102
|
let sellwildConfig = Self.configFromMap(cfgMap)
|
|
95
103
|
|
|
@@ -138,6 +146,10 @@ final class SellwildBannerHostView: UIView, SellwildAdViewDelegate {
|
|
|
138
146
|
onAdFailed?(["message": error.localizedDescription])
|
|
139
147
|
}
|
|
140
148
|
|
|
149
|
+
func sellwildAdView(_ adView: SellwildAdView, didBlockAdForZoneId zoneId: String) {
|
|
150
|
+
onAdBlocked?(["zoneId": zoneId])
|
|
151
|
+
}
|
|
152
|
+
|
|
141
153
|
func sellwildAdView(_ adView: SellwildAdView, didRenderWithSize size: CGSize) {
|
|
142
154
|
onAdResize?(["width": size.width, "height": size.height])
|
|
143
155
|
}
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
// SellwildFeedHostView.
|
|
14
14
|
RCT_EXPORT_VIEW_PROPERTY(config, NSDictionary)
|
|
15
15
|
RCT_EXPORT_VIEW_PROPERTY(scrollEnabled, BOOL)
|
|
16
|
+
RCT_EXPORT_VIEW_PROPERTY(consumeListingTaps, BOOL)
|
|
16
17
|
|
|
17
18
|
// Direct events emitted to JS callbacks.
|
|
18
19
|
RCT_EXPORT_VIEW_PROPERTY(onFeedLoaded, RCTDirectEventBlock)
|
|
@@ -13,6 +13,9 @@ import UIKit
|
|
|
13
13
|
/// The bridge re-runs the CDN decoder against `config.remote` so
|
|
14
14
|
/// feed-specific fields (COL1, bgColor, mobileZids, …) are
|
|
15
15
|
/// populated even though they're not all typed on the JS side.
|
|
16
|
+
/// - scrollEnabled: bool — disable internal scrolling for embedding.
|
|
17
|
+
/// - consumeListingTaps: bool — when true the host owns listing taps;
|
|
18
|
+
/// the SDK does not open SFSafariViewController.
|
|
16
19
|
///
|
|
17
20
|
/// Events emitted to JS:
|
|
18
21
|
/// - onFeedLoaded
|
|
@@ -41,6 +44,10 @@ final class SellwildFeedHostView: UIView, SellwildFeedViewDelegate {
|
|
|
41
44
|
/// Disable the feed's internal scrolling so it can be embedded inside a
|
|
42
45
|
/// parent scroll view. Applied live and on feed creation.
|
|
43
46
|
@objc var scrollEnabled: Bool = true { didSet { feedView?.scrollEnabled = scrollEnabled } }
|
|
47
|
+
/// When true, listing taps are only forwarded to JS (`onListingTap`) and
|
|
48
|
+
/// the SDK does NOT open SFSafariViewController. Read at tap time, so it
|
|
49
|
+
/// applies live without rebuilding the feed. Default false = SDK opens.
|
|
50
|
+
@objc var consumeListingTaps: Bool = false
|
|
44
51
|
|
|
45
52
|
// MARK: RN events
|
|
46
53
|
|
|
@@ -61,7 +68,11 @@ final class SellwildFeedHostView: UIView, SellwildFeedViewDelegate {
|
|
|
61
68
|
// MARK: Internals
|
|
62
69
|
|
|
63
70
|
private var feedView: SellwildFeedView?
|
|
64
|
-
|
|
71
|
+
/// Last applied `config` map, compared by value (`isEqual:` ⇒
|
|
72
|
+
/// `isEqualToDictionary:`, deep). NOT `NSDictionary.hash` — that is
|
|
73
|
+
/// just the entry count, so a partnerCode / slug / geo / remote change
|
|
74
|
+
/// with the same key count would be silently ignored.
|
|
75
|
+
private var lastAppliedConfig: NSDictionary?
|
|
65
76
|
private var needsApply: Bool = false
|
|
66
77
|
|
|
67
78
|
override init(frame: CGRect) {
|
|
@@ -82,9 +93,8 @@ final class SellwildFeedHostView: UIView, SellwildFeedViewDelegate {
|
|
|
82
93
|
// Skip if the props identity hasn't changed since last apply.
|
|
83
94
|
// Feed refresh is driven by user pull-to-refresh, not JS
|
|
84
95
|
// re-renders.
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
lastAppliedKey = key
|
|
96
|
+
if lastAppliedConfig?.isEqual(cfgMap) == true { return }
|
|
97
|
+
lastAppliedConfig = cfgMap.copy() as? NSDictionary
|
|
88
98
|
|
|
89
99
|
let sellwildConfig = Self.configFromMap(cfgMap)
|
|
90
100
|
|
|
@@ -119,15 +129,12 @@ final class SellwildFeedHostView: UIView, SellwildFeedViewDelegate {
|
|
|
119
129
|
|
|
120
130
|
func sellwildFeed(_ feed: SellwildFeedView, didTapListing listing: SellwildListing) -> Bool {
|
|
121
131
|
// Forward the listing payload to JS as a plain dictionary. JS
|
|
122
|
-
// can't return a value back through a direct event block
|
|
123
|
-
//
|
|
124
|
-
//
|
|
125
|
-
// the tap can subclass via `useNativeNavigation` later;
|
|
126
|
-
// current behaviour matches the WebView widget (always opens
|
|
127
|
-
// in in-app browser) so there's no regression.
|
|
132
|
+
// can't return a value back through a direct event block (events
|
|
133
|
+
// are async), so whether the SDK opens SFSafariViewController is
|
|
134
|
+
// decided here from the `consumeListingTaps` prop instead.
|
|
128
135
|
let payload = Self.listingPayload(listing)
|
|
129
136
|
onListingTap?(["listing": payload])
|
|
130
|
-
return
|
|
137
|
+
return consumeListingTaps
|
|
131
138
|
}
|
|
132
139
|
|
|
133
140
|
func sellwildFeed(_ feed: SellwildFeedView, didRecordAdImpressionForZoneId zoneId: String) {
|
package/ios/SellwildRNModule.m
CHANGED
|
@@ -39,4 +39,18 @@ final class SellwildRNModule: NSObject {
|
|
|
39
39
|
}
|
|
40
40
|
SellwildPrebidMobile.setExternalUserIds(mapped)
|
|
41
41
|
}
|
|
42
|
+
|
|
43
|
+
/// JS: `SellwildRNModule.prewarm(nativeConfig)`. Pre-initializes the native ad
|
|
44
|
+
/// stack before the first ad view mounts so the first impression doesn't incur
|
|
45
|
+
/// cold-start init latency. Idempotent. Reuses the banner manager's config
|
|
46
|
+
/// mapping so the payload matches `<SellwildBanner config=...>`. iOS bootstraps
|
|
47
|
+
/// inside `configure()` too; this is the explicit early opt-in for parity with
|
|
48
|
+
/// Android's `SellwildSDK.prewarm`.
|
|
49
|
+
@objc(prewarm:)
|
|
50
|
+
func prewarm(_ config: NSDictionary) {
|
|
51
|
+
let cfg = SellwildBannerHostView.configFromMap(config)
|
|
52
|
+
DispatchQueue.main.async {
|
|
53
|
+
_ = SellwildPrebidMobile.bootstrap(with: cfg)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
42
56
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sellwild/react-native-sdk",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
4
4
|
"description": "Sellwild mobile advertising SDK for React Native",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"types": "src/index.ts",
|
|
@@ -18,16 +18,14 @@
|
|
|
18
18
|
"advertising",
|
|
19
19
|
"ads",
|
|
20
20
|
"sellwild",
|
|
21
|
-
"marketplace"
|
|
22
|
-
"widget"
|
|
21
|
+
"marketplace"
|
|
23
22
|
],
|
|
24
23
|
"peerDependencies": {
|
|
25
24
|
"react": ">=17.0.0",
|
|
26
|
-
"react-native": ">=0.70.0"
|
|
27
|
-
"react-native-webview": ">=11.0.0"
|
|
25
|
+
"react-native": ">=0.70.0"
|
|
28
26
|
},
|
|
29
27
|
"dependencies": {
|
|
30
|
-
"@sellwild/sdk-core": "^1.
|
|
28
|
+
"@sellwild/sdk-core": "^1.8.0"
|
|
31
29
|
},
|
|
32
30
|
"devDependencies": {
|
|
33
31
|
"@types/react": "^18.0.0",
|
package/src/SellwildBanner.tsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
|
-
import { Platform, requireNativeComponent, UIManager, ViewStyle, View, Text, StyleSheet, NativeSyntheticEvent } from 'react-native'
|
|
2
|
+
import { Platform, requireNativeComponent, UIManager, StyleProp, ViewStyle, View, Text, StyleSheet, NativeSyntheticEvent } from 'react-native'
|
|
3
3
|
import { resolveAdStack, type SellwildConfig, type AdSize } from '@sellwild/sdk-core'
|
|
4
|
-
import {
|
|
4
|
+
import { toNativeConfig } from './nativeConfig'
|
|
5
5
|
|
|
6
6
|
// Standard IAB mobile ad sizes — used to lock the host View dimensions.
|
|
7
7
|
// The actual ad size is also propagated to native as the `size` prop label.
|
|
@@ -60,13 +60,14 @@ interface NativeBannerProps {
|
|
|
60
60
|
* everything else (bidders, GAM tag, etc.).
|
|
61
61
|
*/
|
|
62
62
|
adStack: string
|
|
63
|
-
style?: ViewStyle
|
|
63
|
+
style?: StyleProp<ViewStyle>
|
|
64
64
|
onAdLoaded?: (e: NativeSyntheticEvent<{}>) => void
|
|
65
65
|
onAdImpression?: (e: NativeSyntheticEvent<{ zoneId: string }>) => void
|
|
66
66
|
onHouseAdImpression?: (e: NativeSyntheticEvent<{ zoneId: string }>) => void
|
|
67
67
|
onAdClicked?: (e: NativeSyntheticEvent<{}>) => void
|
|
68
68
|
onAdFailed?: (e: NativeSyntheticEvent<{ message: string }>) => void
|
|
69
69
|
onAdResize?: (e: NativeSyntheticEvent<{ width: number; height: number }>) => void
|
|
70
|
+
onAdBlocked?: (e: NativeSyntheticEvent<{ zoneId: string }>) => void
|
|
70
71
|
}
|
|
71
72
|
|
|
72
73
|
const NativeBanner = (() => {
|
|
@@ -95,6 +96,12 @@ export interface SellwildBannerProps {
|
|
|
95
96
|
onHouseImpression?: (zoneId: string) => void
|
|
96
97
|
onClick?: () => void
|
|
97
98
|
onError?: (error: Error) => void
|
|
99
|
+
/**
|
|
100
|
+
* Fired when Boltive blocks the rendered creative (only with
|
|
101
|
+
* `MOBILE_BOLTIVE_ENABLED` on). The SDK already swaps in the house ad and
|
|
102
|
+
* re-auctions; this is for reporting.
|
|
103
|
+
*/
|
|
104
|
+
onBlocked?: (zoneId: string) => void
|
|
98
105
|
}
|
|
99
106
|
|
|
100
107
|
export function SellwildBanner({
|
|
@@ -106,6 +113,7 @@ export function SellwildBanner({
|
|
|
106
113
|
onHouseImpression,
|
|
107
114
|
onClick,
|
|
108
115
|
onError,
|
|
116
|
+
onBlocked,
|
|
109
117
|
}: SellwildBannerProps) {
|
|
110
118
|
const dim = AD_DIMENSIONS[size]
|
|
111
119
|
|
|
@@ -159,31 +167,9 @@ export function SellwildBanner({
|
|
|
159
167
|
)
|
|
160
168
|
}
|
|
161
169
|
|
|
162
|
-
//
|
|
163
|
-
//
|
|
164
|
-
|
|
165
|
-
// `adRefreshIntervalMs`. The bridge translates.
|
|
166
|
-
// App identity is resolved per-platform here (iOS vs Android) from the raw
|
|
167
|
-
// CDN payload on `config.remote`; the native banner path reads these fields.
|
|
168
|
-
const { appBundleId, appStoreUrl } = resolveAppIdentity(config)
|
|
169
|
-
const nativeConfig = {
|
|
170
|
-
partnerCode: config.partnerCode,
|
|
171
|
-
appBundleId,
|
|
172
|
-
appStoreUrl,
|
|
173
|
-
geo: config.geo,
|
|
174
|
-
gamTag: config.gamTag,
|
|
175
|
-
debug: config.debug,
|
|
176
|
-
pbsDebug: config.pbsDebug,
|
|
177
|
-
adRefreshMax: config.adRefreshMax,
|
|
178
|
-
adRefreshMaxMobile: config.adRefreshMaxMobile,
|
|
179
|
-
adRefreshIntervalMs: config.adRefreshInterval,
|
|
180
|
-
prebidServer: config.prebidServer,
|
|
181
|
-
// Local GrowthCode overrides (remote GROWTHCODE_* keys ride `remote`).
|
|
182
|
-
growthCode: config.growthCode,
|
|
183
|
-
// Local localized-listings overrides (remote LOCALIZED_LISTINGS rides `remote`).
|
|
184
|
-
localizedListings: config.localizedListings,
|
|
185
|
-
remote: config.remote,
|
|
186
|
-
}
|
|
170
|
+
// The fields the native banner path reads. Built via the shared helper so
|
|
171
|
+
// <SellwildBanner> and prewarm() stay in sync (single source of truth).
|
|
172
|
+
const nativeConfig = toNativeConfig(config)
|
|
187
173
|
|
|
188
174
|
return (
|
|
189
175
|
<NativeBanner
|
|
@@ -198,6 +184,7 @@ export function SellwildBanner({
|
|
|
198
184
|
onAdImpression={() => onImpression?.()}
|
|
199
185
|
onHouseAdImpression={(e: NativeSyntheticEvent<{ zoneId: string }>) => onHouseImpression?.(e.nativeEvent?.zoneId)}
|
|
200
186
|
onAdClicked={() => onClick?.()}
|
|
187
|
+
onAdBlocked={(e: NativeSyntheticEvent<{ zoneId: string }>) => onBlocked?.(e.nativeEvent?.zoneId)}
|
|
201
188
|
onAdFailed={(e: NativeSyntheticEvent<{ message: string }>) => {
|
|
202
189
|
const msg = e.nativeEvent?.message ?? 'Ad failed'
|
|
203
190
|
onError?.(new Error(msg))
|