@sellwild/react-native-sdk 1.3.3 → 1.3.6

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.
@@ -1,15 +1,17 @@
1
1
  require 'json'
2
2
 
3
- package = JSON.parse(File.read(File.join(__dir__, '..', 'package.json')))
3
+ package = JSON.parse(File.read(File.join(__dir__, 'package.json')))
4
4
 
5
5
  Pod::Spec.new do |s|
6
6
  s.name = 'SellwildSDK-RN'
7
7
  s.version = package['version']
8
8
  s.summary = 'React Native bridge for the Sellwild iOS SDK'
9
9
  s.description = <<-DESC
10
- Bridges <SellwildBanner> from @sellwild/react-native-sdk to the
11
- native iOS SellwildAdView (Prebid Mobile + AdManagerBannerView).
12
- There is no WebView in the ad path.
10
+ Bridges the @sellwild/react-native-sdk components to native iOS:
11
+ - <SellwildBanner> → SellwildAdView (Prebid Mobile + AdManagerBannerView)
12
+ - <SellwildFeed> → SellwildFeedView (all-in-one COL1-scheduled
13
+ feed of native listing cards + native ads)
14
+ There is no WebView in either path.
13
15
  DESC
14
16
 
15
17
  s.homepage = 'https://github.com/Antengo/sellwild-sdk'
@@ -20,12 +22,12 @@ Pod::Spec.new do |s|
20
22
  s.platforms = { :ios => '13.0' }
21
23
  s.swift_version = '5.5'
22
24
 
23
- s.source_files = '*.{swift,m,h}'
25
+ s.source_files = 'ios/*.{swift,m,h}'
24
26
  s.requires_arc = true
25
27
 
26
28
  # The host app must already pull in SellwildSDK (the iOS SDK pod). We
27
29
  # depend on it explicitly so `pod install` resolves both with one
28
30
  # declaration in the host Podfile.
29
- s.dependency 'SellwildSDK', '>= 1.3.0'
31
+ s.dependency 'SellwildSDK', '>= 1.3.5'
30
32
  s.dependency 'React-Core'
31
33
  end
@@ -64,5 +64,5 @@ dependencies {
64
64
  // The native SDK that owns SellwildAdView, SellwildPrebidMobile, etc.
65
65
  // Resolves to the published AAR off https://maven.sellwild.com/releases
66
66
  // for npm consumers, or to mavenLocal in the SDK monorepo dev workflow.
67
- implementation "com.sellwild:sdk:1.3.2"
67
+ implementation "com.sellwild:sdk:1.3.5"
68
68
  }
@@ -0,0 +1,183 @@
1
+ package com.sellwild.rnsdk
2
+
3
+ import com.facebook.react.bridge.Arguments
4
+ import com.facebook.react.bridge.ReactContext
5
+ import com.facebook.react.bridge.ReadableMap
6
+ import com.facebook.react.bridge.WritableMap
7
+ import com.facebook.react.common.MapBuilder
8
+ import com.facebook.react.uimanager.SimpleViewManager
9
+ import com.facebook.react.uimanager.ThemedReactContext
10
+ import com.facebook.react.uimanager.annotations.ReactProp
11
+ import com.facebook.react.uimanager.events.RCTEventEmitter
12
+ import com.sellwild.sdk.SellwildConfig
13
+ import com.sellwild.sdk.SellwildFeedView
14
+ import com.sellwild.sdk.SellwildListing
15
+ import com.sellwild.sdk.SellwildSDK
16
+ import org.json.JSONObject
17
+
18
+ /**
19
+ * Bridges the JS <SellwildFeed> component to the native
20
+ * com.sellwild.sdk.SellwildFeedView (all-in-one native feed: COL1-scheduled
21
+ * listing cards + Prebid + GAM ads, no WebView).
22
+ *
23
+ * Props (set from JS):
24
+ * - config: object — the resolved SellwildConfig (from configure()).
25
+ * The bridge re-runs the CDN decoder against `config.remote` so
26
+ * feed-specific fields (COL1, bgColor, mobileZids, listingsUrl, …)
27
+ * are populated identically to a native [SellwildSDK.configure] call.
28
+ *
29
+ * Events emitted to JS:
30
+ * - onFeedLoaded
31
+ * - onListingTap { listing }
32
+ * - onAdImpression { zoneId }
33
+ * - onAdClicked { zoneId }
34
+ * - onFeedError { message }
35
+ */
36
+ class SellwildFeedViewManager : SimpleViewManager<SellwildFeedView>() {
37
+
38
+ override fun getName(): String = REACT_CLASS
39
+
40
+ /**
41
+ * Per-view scratch state. SellwildFeedView.setup() kicks off a network
42
+ * fetch + auctions for every COL1 ad slot, so we defer it to
43
+ * onAfterUpdateTransaction and skip re-applying on JS re-renders.
44
+ */
45
+ private val pending = java.util.WeakHashMap<SellwildFeedView, PendingProps>()
46
+
47
+ private data class PendingProps(
48
+ var config: ReadableMap? = null,
49
+ var lastAppliedKey: String? = null,
50
+ )
51
+
52
+ override fun createViewInstance(reactContext: ThemedReactContext): SellwildFeedView {
53
+ val view = SellwildFeedView(reactContext)
54
+ pending[view] = PendingProps()
55
+ view.listener = object : SellwildFeedView.Listener {
56
+ override fun onListingTap(listing: SellwildListing): Boolean {
57
+ val payload = Arguments.createMap().apply {
58
+ putMap("listing", listingPayload(listing))
59
+ }
60
+ emit(reactContext, view, "onListingTap", payload)
61
+ // SDK still owns navigation (Custom Tabs); JS just observes.
62
+ return false
63
+ }
64
+
65
+ override fun onAdImpression(zoneId: String) {
66
+ val payload = Arguments.createMap().apply { putString("zoneId", zoneId) }
67
+ emit(reactContext, view, "onAdImpression", payload)
68
+ }
69
+
70
+ override fun onAdClicked(zoneId: String) {
71
+ val payload = Arguments.createMap().apply { putString("zoneId", zoneId) }
72
+ emit(reactContext, view, "onAdClicked", payload)
73
+ }
74
+
75
+ override fun onLoad() {
76
+ emit(reactContext, view, "onFeedLoaded", null)
77
+ }
78
+
79
+ override fun onError(message: String) {
80
+ val payload = Arguments.createMap().apply { putString("message", message) }
81
+ emit(reactContext, view, "onFeedError", payload)
82
+ }
83
+ }
84
+ return view
85
+ }
86
+
87
+ @ReactProp(name = "config")
88
+ fun setConfig(view: SellwildFeedView, value: ReadableMap?) {
89
+ pendingFor(view).config = value
90
+ }
91
+
92
+ override fun onAfterUpdateTransaction(view: SellwildFeedView) {
93
+ super.onAfterUpdateTransaction(view)
94
+
95
+ val p = pending[view] ?: return
96
+ val configMap = p.config ?: return
97
+
98
+ val key = "${configMap.hashCode()}"
99
+ if (p.lastAppliedKey == key) return
100
+ p.lastAppliedKey = key
101
+
102
+ val config = configFromMap(configMap)
103
+ view.setup(config)
104
+ view.load()
105
+ }
106
+
107
+ override fun onDropViewInstance(view: SellwildFeedView) {
108
+ pending.remove(view)
109
+ super.onDropViewInstance(view)
110
+ }
111
+
112
+ private fun pendingFor(view: SellwildFeedView): PendingProps =
113
+ pending.getOrPut(view) { PendingProps() }
114
+
115
+ override fun getExportedCustomDirectEventTypeConstants(): Map<String, Any> {
116
+ return MapBuilder.builder<String, Any>()
117
+ .put("onFeedLoaded", MapBuilder.of("registrationName", "onFeedLoaded"))
118
+ .put("onListingTap", MapBuilder.of("registrationName", "onListingTap"))
119
+ .put("onAdImpression", MapBuilder.of("registrationName", "onAdImpression"))
120
+ .put("onAdClicked", MapBuilder.of("registrationName", "onAdClicked"))
121
+ .put("onFeedError", MapBuilder.of("registrationName", "onFeedError"))
122
+ .build()
123
+ }
124
+
125
+ private fun emit(
126
+ context: ReactContext,
127
+ view: SellwildFeedView,
128
+ name: String,
129
+ payload: WritableMap?,
130
+ ) {
131
+ context.getJSModule(RCTEventEmitter::class.java)
132
+ .receiveEvent(view.id, name, payload)
133
+ }
134
+
135
+ companion object {
136
+ const val REACT_CLASS = "SellwildFeedView"
137
+
138
+ /**
139
+ * Build a full SellwildConfig from a JS map. The JS side passes the
140
+ * resolved CDN payload under `remote`; we re-run the canonical CDN
141
+ * decoder ([SellwildSDK.apply]) against it so feed-specific fields
142
+ * (COL1, bgColor, mobileZids, listingsUrl, …) land identically to a
143
+ * native [SellwildSDK.configure] call. Explicit JS overrides
144
+ * (e.g. appBundleId from the host app) win.
145
+ */
146
+ internal fun configFromMap(map: ReadableMap): SellwildConfig {
147
+ val partnerCode = if (map.hasKey("partnerCode")) map.getString("partnerCode") ?: "" else ""
148
+
149
+ var config = SellwildConfig(partnerCode = partnerCode)
150
+
151
+ // Apply the raw CDN payload first.
152
+ if (map.hasKey("remote") && !map.isNull("remote")) {
153
+ val remoteMap = map.getMap("remote")!!.toHashMap()
154
+ val remoteJson = JSONObject(remoteMap)
155
+ config = SellwildSDK.apply(remoteJson, config).copy(
156
+ remoteJson = remoteJson.toString()
157
+ )
158
+ }
159
+
160
+ // JS-side overrides.
161
+ if (map.hasKey("appBundleId")) config = config.copy(appBundleId = map.getString("appBundleId"))
162
+ if (map.hasKey("appStoreUrl")) config = config.copy(appStoreUrl = map.getString("appStoreUrl"))
163
+ if (map.hasKey("gamTag")) config = config.copy(gamTag = map.getString("gamTag"))
164
+ if (map.hasKey("debug")) config = config.copy(debug = map.getBoolean("debug"))
165
+ if (map.hasKey("adRefreshMax")) config = config.copy(adRefreshMax = map.getInt("adRefreshMax"))
166
+ if (map.hasKey("adRefreshMaxMobile")) config = config.copy(adRefreshMaxMobile = map.getInt("adRefreshMaxMobile"))
167
+ if (map.hasKey("adRefreshIntervalMs")) config = config.copy(adRefreshIntervalMs = map.getDouble("adRefreshIntervalMs").toLong())
168
+
169
+ return config
170
+ }
171
+
172
+ /** Surface the listing payload to JS. Mirrors `SellwildListing` in @sellwild/sdk-core. */
173
+ internal fun listingPayload(listing: SellwildListing): WritableMap = Arguments.createMap().apply {
174
+ putString("id", listing.id)
175
+ putString("title", listing.title)
176
+ listing.url?.let { putString("url", it) }
177
+ listing.currency?.let { putString("currency", it) }
178
+ listing.price?.let { putString("price", it) }
179
+ listing.remoteUrl?.let { putString("remoteUrl", it) }
180
+ listing.primaryPhotoUrl?.let { putString("photoUrl", it) }
181
+ }
182
+ }
183
+ }
@@ -14,5 +14,6 @@ class SellwildSdkPackage : ReactPackage {
14
14
  reactContext: ReactApplicationContext,
15
15
  ): List<ViewManager<out View, out ReactShadowNode<*>>> = listOf(
16
16
  SellwildBannerViewManager(),
17
+ SellwildFeedViewManager(),
17
18
  )
18
19
  }
@@ -0,0 +1,23 @@
1
+ #import <React/RCTViewManager.h>
2
+ #import <React/RCTUIManager.h>
3
+ #import <React/RCTBridgeModule.h>
4
+
5
+ // Register the view manager class with React Native. We export it under
6
+ // the JS name "SellwildFeedView" to match the Android side
7
+ // (com.sellwild.rnsdk.SellwildFeedViewManager.REACT_CLASS).
8
+ @interface RCT_EXTERN_REMAP_MODULE(SellwildFeedView,
9
+ SellwildFeedViewManager,
10
+ RCTViewManager)
11
+
12
+ // Props set from JS — these forward to @objc properties on
13
+ // SellwildFeedHostView.
14
+ RCT_EXPORT_VIEW_PROPERTY(config, NSDictionary)
15
+
16
+ // Direct events emitted to JS callbacks.
17
+ RCT_EXPORT_VIEW_PROPERTY(onFeedLoaded, RCTDirectEventBlock)
18
+ RCT_EXPORT_VIEW_PROPERTY(onListingTap, RCTDirectEventBlock)
19
+ RCT_EXPORT_VIEW_PROPERTY(onAdImpression, RCTDirectEventBlock)
20
+ RCT_EXPORT_VIEW_PROPERTY(onAdClicked, RCTDirectEventBlock)
21
+ RCT_EXPORT_VIEW_PROPERTY(onFeedError, RCTDirectEventBlock)
22
+
23
+ @end
@@ -0,0 +1,200 @@
1
+ import Foundation
2
+ import React
3
+ import SellwildSDK
4
+ import UIKit
5
+
6
+ /// Bridges the JS `<SellwildFeed>` component to the native
7
+ /// `SellwildFeedView` (all-in-one native feed: COL1-scheduled listing
8
+ /// cards + Prebid + GAM ads, zero WebView). Mirrors the Android
9
+ /// `SellwildFeedViewManager` shape.
10
+ ///
11
+ /// Props (set from JS):
12
+ /// - config: object — the resolved SellwildConfig (from configure()).
13
+ /// The bridge re-runs the CDN decoder against `config.remote` so
14
+ /// feed-specific fields (COL1, bgColor, mobileZids, …) are
15
+ /// populated even though they're not all typed on the JS side.
16
+ ///
17
+ /// Events emitted to JS:
18
+ /// - onFeedLoaded
19
+ /// - onListingTap { listing }
20
+ /// - onAdImpression { zoneId }
21
+ /// - onAdClicked { zoneId }
22
+ /// - onFeedError { message }
23
+ @objc(SellwildFeedViewManager)
24
+ public final class SellwildFeedViewManager: RCTViewManager {
25
+
26
+ public override static func requiresMainQueueSetup() -> Bool { true }
27
+
28
+ public override func view() -> UIView! {
29
+ return SellwildFeedHostView()
30
+ }
31
+ }
32
+
33
+ /// Hosts a `SellwildFeedView`. We can't construct the feed until the
34
+ /// `config` prop has arrived, so we cache it and apply on
35
+ /// `didSetProps:` — same pattern as the banner host view.
36
+ final class SellwildFeedHostView: UIView, SellwildFeedViewDelegate {
37
+
38
+ // MARK: RN-set props
39
+
40
+ @objc var config: NSDictionary? { didSet { needsApply = true } }
41
+
42
+ // MARK: RN events
43
+
44
+ @objc var onFeedLoaded: RCTDirectEventBlock?
45
+ @objc var onListingTap: RCTDirectEventBlock?
46
+ @objc var onAdImpression: RCTDirectEventBlock?
47
+ @objc var onAdClicked: RCTDirectEventBlock?
48
+ @objc var onFeedError: RCTDirectEventBlock?
49
+
50
+ // MARK: Internals
51
+
52
+ private var feedView: SellwildFeedView?
53
+ private var lastAppliedKey: String?
54
+ private var needsApply: Bool = false
55
+
56
+ override init(frame: CGRect) {
57
+ super.init(frame: frame)
58
+ }
59
+
60
+ required init?(coder: NSCoder) { fatalError("init(coder:) not used") }
61
+
62
+ override func didSetProps(_ changedProps: [String]) {
63
+ guard needsApply else { return }
64
+ needsApply = false
65
+ applyIfReady()
66
+ }
67
+
68
+ private func applyIfReady() {
69
+ guard let cfgMap = config else { return }
70
+
71
+ // Skip if the props identity hasn't changed since last apply.
72
+ // Feed refresh is driven by user pull-to-refresh, not JS
73
+ // re-renders.
74
+ let key = "\(cfgMap.hash)"
75
+ if lastAppliedKey == key { return }
76
+ lastAppliedKey = key
77
+
78
+ let sellwildConfig = Self.configFromMap(cfgMap)
79
+
80
+ // Tear down any previous feed view; SellwildFeedView holds its
81
+ // own table state and we want a clean fetch on identity change.
82
+ feedView?.removeFromSuperview()
83
+
84
+ let view = SellwildFeedView(config: sellwildConfig)
85
+ view.delegate = self
86
+ view.translatesAutoresizingMaskIntoConstraints = false
87
+ addSubview(view)
88
+ NSLayoutConstraint.activate([
89
+ view.leadingAnchor.constraint(equalTo: leadingAnchor),
90
+ view.trailingAnchor.constraint(equalTo: trailingAnchor),
91
+ view.topAnchor.constraint(equalTo: topAnchor),
92
+ view.bottomAnchor.constraint(equalTo: bottomAnchor),
93
+ ])
94
+ feedView = view
95
+ view.load()
96
+ }
97
+
98
+ // MARK: SellwildFeedViewDelegate
99
+
100
+ func sellwildFeedDidLoad(_ feed: SellwildFeedView) {
101
+ onFeedLoaded?([:])
102
+ }
103
+
104
+ func sellwildFeed(_ feed: SellwildFeedView, didTapListing listing: SellwildListing) -> Bool {
105
+ // Forward the listing payload to JS as a plain dictionary. JS
106
+ // can't return a value back through a direct event block, so
107
+ // the SDK still owns whether to open the URL in
108
+ // SFSafariViewController. Partners who want to fully consume
109
+ // the tap can subclass via `useNativeNavigation` later;
110
+ // current behaviour matches the WebView widget (always opens
111
+ // in in-app browser) so there's no regression.
112
+ let payload = Self.listingPayload(listing)
113
+ onListingTap?(["listing": payload])
114
+ return false
115
+ }
116
+
117
+ func sellwildFeed(_ feed: SellwildFeedView, didRecordAdImpressionForZoneId zoneId: String) {
118
+ onAdImpression?(["zoneId": zoneId])
119
+ }
120
+
121
+ func sellwildFeed(_ feed: SellwildFeedView, didRecordAdClickForZoneId zoneId: String) {
122
+ onAdClicked?(["zoneId": zoneId])
123
+ }
124
+
125
+ func sellwildFeed(_ feed: SellwildFeedView, didFailWithError message: String) {
126
+ onFeedError?(["message": message])
127
+ }
128
+
129
+ // MARK: Config marshalling
130
+
131
+ /// Build a full SellwildConfig from a JS dictionary. The JS side
132
+ /// passes the raw CDN payload under `remote`; we re-run the
133
+ /// canonical CDN decoder against it so feed-specific fields (COL1,
134
+ /// bgColor, mobileZids, listingsUrl, …) are populated identically
135
+ /// to a native `SellwildSDK.configure(...)` call.
136
+ static func configFromMap(_ map: NSDictionary) -> SellwildConfig {
137
+ let partnerCode = (map["partnerCode"] as? String) ?? ""
138
+ var cfg = SellwildConfig(partnerCode: partnerCode)
139
+
140
+ // Apply the raw CDN payload first so explicit JS overrides
141
+ // (e.g. appBundleId from the host app) win.
142
+ if let remote = map["remote"] as? [String: Any] {
143
+ cfg = SellwildSDK.apply(remote, to: cfg)
144
+ if let data = try? JSONSerialization.data(withJSONObject: remote, options: []) {
145
+ cfg.remoteJSON = data
146
+ }
147
+ }
148
+
149
+ if let v = map["slug"] as? String { cfg.slug = v }
150
+ if let v = map["appBundleId"] as? String { cfg.appBundleId = v }
151
+ if let v = map["appStoreUrl"] as? String { cfg.appStoreUrl = v }
152
+ if let v = map["gamTag"] as? String { cfg.gamTag = v }
153
+ if let v = map["debug"] as? Bool { cfg.debug = v }
154
+ if let v = map["adRefreshMax"] as? Int { cfg.adRefreshMax = v }
155
+ if let v = map["adRefreshMaxMobile"] as? Int { cfg.adRefreshMaxMobile = v }
156
+ if let v = map["listingsUrl"] as? String { cfg.listingsUrl = v }
157
+ if let v = map["priceColor"] as? String { cfg.priceColor = v }
158
+ if let v = map["bannerZid"] as? String { cfg.bannerZid = v }
159
+ if let v = map["bottomBannerZid"] as? String { cfg.bottomBannerZid = v }
160
+ if let v = map["mobileBannerZid"] as? String { cfg.mobileBannerZid = v }
161
+ if let v = map["mobileZids"] as? [String] { cfg.mobileZids = v }
162
+
163
+ // JS bridge passes ms; iOS API is seconds (TimeInterval).
164
+ if let v = map["adRefreshIntervalMs"] as? NSNumber {
165
+ cfg.adRefreshInterval = v.doubleValue / 1000.0
166
+ }
167
+
168
+ if let prebid = map["prebidServer"] as? NSDictionary,
169
+ let accountId = prebid["accountId"] as? String,
170
+ let endpoint = prebid["endpoint"] as? String {
171
+ let bidders = (prebid["bidders"] as? [String]) ?? []
172
+ let timeout = (prebid["timeout"] as? Int) ?? 1500
173
+ cfg.prebidServer = PrebidServerConfig(
174
+ accountId: accountId,
175
+ endpoint: endpoint,
176
+ bidders: bidders,
177
+ timeout: timeout,
178
+ syncEndpoint: prebid["syncEndpoint"] as? String
179
+ )
180
+ }
181
+
182
+ return cfg
183
+ }
184
+
185
+ /// Serialize a SellwildListing to a JS-friendly dictionary. Surfaces
186
+ /// the fields the JS callback is most likely to act on; the rest
187
+ /// can be added on demand.
188
+ static func listingPayload(_ listing: SellwildListing) -> [String: Any] {
189
+ var dict: [String: Any] = [
190
+ "id": listing.id,
191
+ "title": listing.title,
192
+ ]
193
+ if let v = listing.url { dict["url"] = v }
194
+ if let v = listing.currency { dict["currency"] = v }
195
+ if let v = listing.price { dict["price"] = v }
196
+ if let v = listing.remoteUrl { dict["remoteUrl"] = v }
197
+ if let firstPhoto = listing.photos?.first?.url { dict["photoUrl"] = firstPhoto }
198
+ return dict
199
+ }
200
+ }
package/package.json CHANGED
@@ -1,13 +1,17 @@
1
1
  {
2
2
  "name": "@sellwild/react-native-sdk",
3
- "version": "1.3.3",
3
+ "version": "1.3.6",
4
4
  "description": "Sellwild mobile advertising SDK for React Native",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
7
7
  "files": [
8
8
  "src",
9
- "android",
10
- "ios"
9
+ "android/build.gradle",
10
+ "android/src/**/*",
11
+ "ios/*.swift",
12
+ "ios/*.m",
13
+ "ios/*.h",
14
+ "SellwildSDK-RN.podspec"
11
15
  ],
12
16
  "keywords": [
13
17
  "react-native",
@@ -0,0 +1,174 @@
1
+ import React from 'react'
2
+ import {
3
+ Platform,
4
+ requireNativeComponent,
5
+ StyleProp,
6
+ StyleSheet,
7
+ Text,
8
+ UIManager,
9
+ View,
10
+ ViewStyle,
11
+ NativeSyntheticEvent,
12
+ } from 'react-native'
13
+ import type { SellwildConfig, SellwildListing } from '@sellwild/sdk-core'
14
+
15
+ // ─── Native component bridge ─────────────────────────────────────────────────
16
+ //
17
+ // SellwildFeed is the "all-in-one" native surface: a single-column scroll
18
+ // of native listing cards interleaved with native Prebid + GAM ads,
19
+ // scheduled by the CDN-published COL1 token string. There is **no
20
+ // WebView** in this surface — every row is native on both platforms.
21
+ //
22
+ // iOS: SellwildFeedView (UITableView-backed)
23
+ // Android: com.sellwild.sdk.SellwildFeedView (RecyclerView-backed)
24
+ //
25
+ // This component is the drop-in native replacement for the WebView-based
26
+ // <SellwildWidget>. Same one-component integration shape; native rails.
27
+
28
+ const NATIVE_NAME = 'SellwildFeedView'
29
+
30
+ interface NativeFeedProps {
31
+ config: object
32
+ style?: StyleProp<ViewStyle>
33
+ onFeedLoaded?: (e: NativeSyntheticEvent<{}>) => void
34
+ onListingTap?: (e: NativeSyntheticEvent<{ listing: SellwildListing }>) => void
35
+ onAdImpression?: (e: NativeSyntheticEvent<{ zoneId: string }>) => void
36
+ onAdClicked?: (e: NativeSyntheticEvent<{ zoneId: string }>) => void
37
+ onFeedError?: (e: NativeSyntheticEvent<{ message: string }>) => void
38
+ }
39
+
40
+ const NativeFeed = (() => {
41
+ // requireNativeComponent crashes loudly if the view manager isn't
42
+ // registered. Probe first so we can render a friendly fallback on
43
+ // platforms where the bridge isn't in this build yet.
44
+ const config = UIManager.getViewManagerConfig?.(NATIVE_NAME)
45
+ if (!config) {
46
+ return null
47
+ }
48
+ return requireNativeComponent<NativeFeedProps>(NATIVE_NAME)
49
+ })()
50
+
51
+ // ─── Public component ────────────────────────────────────────────────────────
52
+
53
+ export interface SellwildFeedProps {
54
+ /**
55
+ * Resolved Sellwild config from `configure()`. The native feed reads
56
+ * the COL1 schedule, listings URL, ad zones, and theme out of this.
57
+ */
58
+ config: SellwildConfig
59
+
60
+ /** Optional style override. The feed expands to its container by default. */
61
+ style?: ViewStyle
62
+
63
+ /** Fired once after the initial listings fetch resolves successfully. */
64
+ onLoad?: () => void
65
+
66
+ /**
67
+ * Fired when a listing card is tapped. Return `true` to consume the
68
+ * event; return `false` (or omit) to let the SDK open `listing.url`
69
+ * in the platform in-app browser (Custom Tabs / SFSafariViewController).
70
+ */
71
+ onListingTap?: (listing: SellwildListing) => boolean | void
72
+
73
+ /** Fired when a native ad row records an impression. */
74
+ onAdImpression?: (zoneId: string) => void
75
+
76
+ /** Fired when a native ad row is clicked. */
77
+ onAdClicked?: (zoneId: string) => void
78
+
79
+ /** Fired when the listings fetch fails or a row fails to render. */
80
+ onError?: (error: Error) => void
81
+ }
82
+
83
+ export function SellwildFeed({
84
+ config,
85
+ style,
86
+ onLoad,
87
+ onListingTap,
88
+ onAdImpression,
89
+ onAdClicked,
90
+ onError,
91
+ }: SellwildFeedProps) {
92
+ if (!NativeFeed) {
93
+ // Native module not registered. Most common cause: the host app was
94
+ // built before the @sellwild/react-native-sdk autolink ran, or this
95
+ // is being rendered in a JS-only test environment.
96
+ return (
97
+ <View style={[styles.fill, style, __DEV__ ? styles.devPlaceholder : undefined]}>
98
+ {__DEV__ ? (
99
+ <Text style={styles.devText}>
100
+ Sellwild native feed not available on {Platform.OS} (yet)
101
+ </Text>
102
+ ) : null}
103
+ </View>
104
+ )
105
+ }
106
+
107
+ // Pass the fields the native feed reads, plus the raw CDN payload
108
+ // under `remote`. The native bridge re-runs the CDN decoder against
109
+ // `remote` to populate feed-specific fields (COL1 schedule, bgColor,
110
+ // mobileZids, mobileBannerZid, listingsUrl), so we don't need to
111
+ // mirror every field as a typed property on the JS side.
112
+ //
113
+ // Note: TS core uses `adRefreshInterval` (ms); the native side
114
+ // reads it as `adRefreshIntervalMs`. The bridge translates.
115
+ const nativeConfig: Record<string, unknown> = {
116
+ partnerCode: config.partnerCode,
117
+ slug: config.slug,
118
+ appBundleId: config.appBundleId,
119
+ appStoreUrl: config.appStoreUrl,
120
+ gamTag: config.gamTag,
121
+ debug: config.debug,
122
+ adRefreshMax: config.adRefreshMax,
123
+ adRefreshMaxMobile: config.adRefreshMaxMobile,
124
+ adRefreshIntervalMs: config.adRefreshInterval,
125
+ prebidServer: config.prebidServer,
126
+ remote: config.remote,
127
+ listingsUrl: config.listingsUrl,
128
+ priceColor: config.priceColor,
129
+ bannerZid: config.bannerZid,
130
+ bottomBannerZid: config.bottomBannerZid,
131
+ mobileBannerZid: config.mobileBannerZid,
132
+ mobileZids: config.mobileZids,
133
+ }
134
+
135
+ return (
136
+ <NativeFeed
137
+ style={[styles.fill, style]}
138
+ config={nativeConfig}
139
+ onFeedLoaded={() => onLoad?.()}
140
+ onListingTap={(e: NativeSyntheticEvent<{ listing: SellwildListing }>) => {
141
+ onListingTap?.(e.nativeEvent.listing)
142
+ }}
143
+ onAdImpression={(e: NativeSyntheticEvent<{ zoneId: string }>) => {
144
+ onAdImpression?.(e.nativeEvent.zoneId)
145
+ }}
146
+ onAdClicked={(e: NativeSyntheticEvent<{ zoneId: string }>) => {
147
+ onAdClicked?.(e.nativeEvent.zoneId)
148
+ }}
149
+ onFeedError={(e: NativeSyntheticEvent<{ message: string }>) => {
150
+ const msg = e.nativeEvent?.message ?? 'Feed failed'
151
+ onError?.(new Error(msg))
152
+ }}
153
+ />
154
+ )
155
+ }
156
+
157
+ const styles = StyleSheet.create({
158
+ fill: {
159
+ flex: 1,
160
+ },
161
+ devPlaceholder: {
162
+ backgroundColor: '#FEE2E2',
163
+ borderWidth: 1,
164
+ borderColor: '#FCA5A5',
165
+ alignItems: 'center',
166
+ justifyContent: 'center',
167
+ padding: 12,
168
+ },
169
+ devText: {
170
+ color: '#991B1B',
171
+ fontSize: 12,
172
+ textAlign: 'center',
173
+ },
174
+ })
package/src/index.ts CHANGED
@@ -4,6 +4,9 @@ export type { SellwildWidgetProps } from './SellwildWidget'
4
4
  export { SellwildBanner } from './SellwildBanner'
5
5
  export type { SellwildBannerProps } from './SellwildBanner'
6
6
 
7
+ export { SellwildFeed } from './SellwildFeed'
8
+ export type { SellwildFeedProps } from './SellwildFeed'
9
+
7
10
  export { SellwildListingCard } from './SellwildListingCard'
8
11
  export type { SellwildListingCardProps } from './SellwildListingCard'
9
12