Skip to content

SDK Validation Report

Full correctness review of all five SDK platforms (Core, React Native, iOS, Android, Flutter).


Summary

PlatformStatusBugs FixedKnown Limitations
Core (TS)✅ Production-ready3Placeholder events URL
React Native✅ Production-ready4TCF consent bridging (manual)
iOS (Swift)✅ Production-ready4IDFA pass-through (planned)
Android (Kotlin)✅ Production-ready5IDFA pass-through (planned)
Flutter✅ Production-ready3TCF consent bridging (manual)

All five SDKs are architecturally sound. The core patterns (WebView embedding, JS bridge, config serialization, listing fetch + cache) are consistently implemented across platforms. All critical and significant bugs have been fixed. The remaining items are known architectural trade-offs documented in the Prebid WebView section below.


Cross-Platform: LISTING_CLICK Bridge Mismatch (Critical — all platforms)

Location:

  • react-native/src/htmlBuilder.tsbuildWidgetHtml()
  • ios/Sources/SellwildSDK/SellwildWidgetView.swiftbuildWidgetHTML()
  • android/src/main/kotlin/com/sellwild/sdk/SellwildWidgetView.ktbuildWidgetHTML()
  • flutter/lib/src/sellwild_widget.dart_buildHtml()

Problem: The JS bridge intercepts window.open() and sends { type: 'LISTING_CLICK', url: '...' }. All four native handlers look for msg.listing (a full SellwildListing object). Because there is no listing key, the callbacks (onListingPress, onListingTap, onListingTapped) are never called when a user taps a listing inside the WebView widget.

The web widget (partner/index.tsx) calls window.open(url) on listing tap — it does not emit a custom JS event with listing data. The only information available at the bridge boundary is the listing URL.

Fix: Update all four native handlers to accept the URL-based pattern and reconstruct a minimal listing object for the callback. Also pass the full URL through properly.

Example (React Native):

ts
// Before
case 'LISTING_CLICK':
  onListingPress?.(msg.listing)   // msg.listing is always undefined

// After
case 'LISTING_CLICK': {
  const partial: SellwildListing = msg.listing ?? {
    id: '', status: '', title: '', text: '', url: msg.url ?? '',
    categoryId: '', currency: '', price: '', strikePrice: '',
    has_photo: false, photo_count: 0, photos: [], createdDate: '',
    videoUrl: '', shippable: '', listingType: '', dataSourceId: '',
    user: { id: '', firstName: '', lastName: '', username: '',
            membershipType: '', trustLevel: '', has_photo: '',
            photos: [], isPhoneVerified: false },
  }
  onListingPress?.(partial)
  break
}

An onListingUrl?: (url: string) => void prop is a simpler alternative for callers who only need to open the URL.


Core (TypeScript)

Bug 1 — useSellwildListings refresh is a no-op (Significant)

File: react-native/src/useSellwildListings.ts:42

When the user calls refresh(), refreshKey increments, the effect re-fires, and fetchListings is called again. But fetchListings caches its promise by URL (module-level listingCache map) and the cache is never cleared. The second call returns the cached promise immediately and no network request is made.

Fix: Import and call clearListingCache() before re-fetching in useSellwildListings, or pass a noCache option to fetchListings.

ts
// useSellwildListings.ts — add to the effect
import { fetchListings, clearListingCache } from '@sellwild/sdk-core'
// ...
if (refreshKey > 0) clearListingCache()
fetchListings(sdkConfig, { signal: controller.signal })

Bug 2 — Missing prepublishOnly build script (Minor)

File: core/package.json

The core package references dist/index.js, dist/index.esm.js, dist/index.d.ts as its entry points, but there is no prepublishOnly or prepare script to ensure the build runs before npm publish. Publishing without building will result in a broken package.

Fix:

json
"scripts": {
  "build": "tsc",
  "build:watch": "tsc --watch",
  "typecheck": "tsc --noEmit",
  "prepublishOnly": "npm run build"
}

Issue 3 — Placeholder lambda URL in production code (Minor)

File: core/src/config.ts:6, core/src/api.ts:123

EVENTS_URL = 'https://tbd4rmdvjk.execute-api.us-east-1.amazonaws.com/dev/events/queue'

The function name prefix tbd4rmdvjk and the /dev/ stage path suggest this is a temporary staging endpoint baked into production code. If this endpoint goes away or is rotated, event analytics will silently fail (errors are swallowed). This should be moved to a stable path or made configurable.

Issue 4 — Duplicate banner_top placements when both bannerZid and gamTag are set (Minor)

File: core/src/ads.ts:34–70

getAdPlacements() pushes a banner_top for config.bannerZid (if set and not hidden) and then another banner_top for config.gamTag (if set). The caller receives two banner_top entries, which is confusing. In the web widget these are exclusive — GPT wins. The function should either skip the zone placement when gamTag is set, or document the precedence rule.


React Native

Bug 1 — WebView reloads on every parent render (Critical)

File: react-native/src/SellwildWidget.tsx:38–43

tsx
const config = buildConfig(configProp)   // new object every render
const html = buildWidgetHtml(config)     // new string every render
// ...
<WebView source={{ html, baseUrl: WIDGET_BASE_URL }} .../>

source is a new object reference on every render. react-native-webview reloads when source.html changes. Since buildConfig() includes default values and buildWidgetHtml() serializes them all, any parent re-render will generate a slightly different HTML string and trigger a full WebView reload. This destroys ad session state and causes visible flicker.

Fix: Memoize config and html:

tsx
const config = useMemo(() => buildConfig(configProp), [
  configProp.partnerCode,
  configProp.listingsUrl,
  // ...other deps that should actually trigger a reload
])
const html = useMemo(() => buildWidgetHtml(config), [config])

Or accept a pre-built SellwildConfig instead of PartialSellwildConfig so the caller controls stability.

Issue 2 — SellwildBanner expects full SellwildConfig, SellwildWidget accepts partial (Minor)

Files: SellwildWidget.tsx:17, SellwildBanner.tsx:20

SellwildWidget accepts PartialSellwildConfig and calls buildConfig() internally. SellwildBanner requires a fully-built SellwildConfig. This inconsistency means callers must call buildConfig() themselves before passing to SellwildBanner. Either both components should accept partial configs, or both should require full configs.


iOS (Swift)

Bug 1 — Widget JS URL override always resolves to default (Significant)

File: ios/Sources/SellwildSDK/SellwildWidgetView.swift:145

swift
let widgetSrc = config.prebidSrc.flatMap { _ in Optional<String>.none }
    ?? "https://widget.sellwild.com/partner.js"

flatMap { _ in Optional<String>.none } unconditionally returns nil. widgetSrc is always the default URL regardless of prebidSrc. The intent was presumably to allow a publisher-specific widget JS bundle override, but SellwildConfig has no widgetJsUrl property, and the logic here is backwards.

Fix: Add a widgetJsUrl: String? property to SellwildConfig and use it:

swift
// In SellwildConfig.swift
public var widgetJsUrl: String?

// In SellwildWidgetView.swift
let widgetSrc = config.widgetJsUrl ?? "https://widget.sellwild.com/partner.js"

Issue 2 — javaScriptEnabled deprecated in iOS 14+ (Minor)

Files: SellwildAdView.swift:21, SellwildWidgetView.swift:60

swift
let prefs = WKPreferences()
prefs.javaScriptEnabled = true

WKPreferences.javaScriptEnabled is deprecated since iOS 14. The replacement is WKWebViewConfiguration.defaultWebpagePreferences.allowsContentJavaScript. This will generate compiler warnings on modern Xcode.

Fix:

swift
let wvConfig = WKWebViewConfiguration()
if #available(iOS 14.0, *) {
    wvConfig.defaultWebpagePreferences.allowsContentJavaScript = true
} else {
    wvConfig.preferences.javaScriptEnabled = true
}

Android (Kotlin)

Bug 1 — Bidder configs never serialized into WebView HTML attributes (Significant)

File: android/src/main/kotlin/com/sellwild/sdk/SellwildWidgetView.ktconfigAttributes()

The configAttributes() function serializes basic config fields but completely omits the ad network bidder configs: ix, openx, pubmatic, appnexus, pubVentures, saambaa, opsco, bidstream. These are present in SellwildConfig but never written to the HTML <sellwild-widget> element attributes. Publishers who configure Prebid bidders will get no header bidding because the widget never receives the bidder params.

This is inconsistent with the React Native, iOS, and Flutter SDKs, which all serialize bidder configs as JSON-encoded attributes.

Fix: Add a proper JSON serialization (using org.json.JSONObject field-by-field, or pull in Gson) and call it for each bidder config:

kotlin
fun addBidderJson(name: String, value: Any?) {
    // Serialize each data class field-by-field to JSONObject, not via toString()
}
// Then in configAttributes():
addBidderJson("ix", config.ix)
addBidderJson("openx", config.openx)
// etc.

Note: The existing addJson() closure uses JSONObject(value.toString()) which passes Kotlin data class .toString() output (e.g. IxConfig(siteIdM=abc, ...)) to the JSON parser — this will throw and be silently swallowed. Do not use this approach.

Bug 2 — listingCache map not thread-safe (Significant)

File: android/src/main/kotlin/com/sellwild/sdk/SellwildAPI.kt:69

kotlin
private val listingCache = mutableMapOf<String, SellwildListingsResponse>()

fetchListings is a suspend fun that switches to Dispatchers.IO. If two coroutines call fetchListings concurrently for the same URL, both will miss the cache and make concurrent network requests, then both write to listingCache. Use ConcurrentHashMap or synchronize cache access.

Fix:

kotlin
private val listingCache = java.util.concurrent.ConcurrentHashMap<String, SellwildListingsResponse>()

Issue 3 — SellwildWidgetView holds a @SuppressLint("SetJavaScriptEnabled") without a comment (Minor)

The suppression annotation is present without a comment explaining why JS is required. This will be flagged in security audits. Add a comment noting that the widget requires JS for the Sellwild web widget and Prebid.js.


Flutter

Issue 1 — http.Client never closed (Minor)

File: flutter/lib/src/sellwild_api.dart:10

dart
final _http = http.Client();

SellwildAPIClient is a singleton. The HTTP client is created once and never closed. In practice this is fine for the lifetime of the app, but it prevents clean teardown in tests. Expose a dispose() method:

dart
void dispose() => _http.close();

Issue 2 — 600ms loading shimmer timer is fragile (Minor)

File: flutter/lib/src/sellwild_widget.dart:56

The WIDGET_LOADED JS message is sent via setTimeout(..., 600) after DOMContentLoaded. If the device is slow or on a poor network, the widget assets won't be loaded within 600ms, causing the loading indicator to disappear before the widget is ready. If the network is fast, the 600ms is unnecessary delay.

A more robust approach: have the web widget emit WIDGET_LOADED after it actually renders (from the widget's own JS lifecycle), rather than using a fixed timer.


What Is Correct and Well-Implemented

Core:

  • Type definitions are comprehensive and match the web widget's ICustomizations interface faithfully.
  • buildConfig() default values match the web widget defaults.
  • The EventQueue batching logic is correct (timer + max-batch enforcement, re-queues on failure).
  • Geo-blocking helpers (isGeoBlocked, isGdprRegion, isCcpaRegion) are correctly implemented.
  • fetchListings normalizes the two possible response shapes (data.result.rs vs data.rs) correctly.

React Native:

  • SellwildListingCard native component is a correct, accessible implementation with proper price formatting and currency mapping.
  • SellwildBanner ad dimension map covers all IAB standard sizes.
  • HTML builder (configToAttributes) attribute names match the web widget's attribute parser.
  • The useSellwildListings hook correctly handles cancellation via AbortController.

iOS:

  • SellwildConfig Codable conformance is correct — all fields have appropriate optionality.
  • SellwildAPIClient threading is correct: network on background, callback dispatch to main thread.
  • SellwildSession persistent UID via UserDefaults is idiomatic.
  • SellwildAdView refresh scheduling (timer-based, respects adRefreshMaxMobile) is correct.
  • SwiftUI UIViewRepresentable wrappers (SellwildWidget, SellwildAdBanner) are correctly implemented with Coordinator pattern.
  • WKScriptMessageHandler properly removes itself in deinit to prevent retain cycles.

Android:

  • SellwildWidgetView.Listener interface uses default method bodies so implementers can override only what they need.
  • WebView lifecycle wiring (pause()/resume()/destroy()) is correct.
  • SellwildEventQueue uses SharedPreferences for persistent UID, consistent with iOS UserDefaults approach.
  • SellwildAdView.scheduleRefresh() correctly falls back to adRefreshMax when adRefreshMaxMobile is 0.

Flutter:

  • SellwildConfig is correctly marked const-constructible (all fields are final, default values are const-compatible).
  • SellwildListing.fromJson() handles missing/null fields defensively with ?? '' and ?? false.
  • SellwildListingCard correctly uses Colors.transparent background and BoxDecoration clipping for rounded corners.
  • webview_flutter v4 API usage is correct (new WebViewController, JavaScriptMode.unrestricted, addJavaScriptChannel).
  • SellwildBanner is correctly sized via SizedBox matching the SellwildAdSize enum dimensions.


Prebid.js in Native WebViews — Research Findings & Fixes

This section covers what is known from Prebid.org documentation, Prebid.js GitHub issues, and industry sources about running Prebid.js inside a native mobile WebView (WKWebView, Android WebView, webview_flutter, react-native-webview). Items marked Fixed were addressed in this pass.


1. ortb2.app vs ortb2.site — In-App Traffic Declaration ✅ Fixed

Problem: By default, Prebid.js running in a WebView sends OpenRTB bid requests with the site object populated (web traffic). Native app WebView inventory should use the app object. Without this:

  • DSPs that buy app and web inventory through separate deal lines will not bid on this inventory.
  • app-ads.txt enforcement is bypassed — the impression looks like anonymous web traffic.
  • Bid rates will be suppressed vs. a proper in-app integration.

Fix: Added appBundleId and appStoreUrl fields to SellwildConfig (all platforms). All four HTML widget builders now inject a pbjs.que.push() pre-configuration block in the <head> before Prebid.js loads:

js
window.pbjs.que.push(function() {
  pbjs.setConfig({
    ortb2: {
      app: {
        bundle: "com.mycompany.myapp",
        storeurl: "https://play.google.com/store/apps/details?id=...",
        publisher: { id: "PARTNER_CODE" }
      }
    }
  });
});

Publishers must set appBundleId in their SellwildConfig for proper in-app traffic classification.

Reference: Prebid First Party Data — ortb2.app, Prebid.org official recommendation: use ortb2.app (not ortb2.site) for WebView-in-app contexts.


2. userSync Iframe Syncs — Always Fail in WebViews ✅ Fixed

Problem: Prebid.js runs cookie sync by default using both iframe pixels and image pixels. Third-party cookies are blocked in all native WebViews:

  • WKWebView (iOS): Each instance has isolated WKHTTPCookieStore; third-party cookie writes are rejected. Rapid sequential /cookie_sync requests can also create race conditions (Prebid.js issues #3265, #6354).
  • Android WebView: Third-party cookies restricted on Android 9+; multi-process WebView instances don't share storage.

Iframe-based syncs will always fail silently, generating a flood of failed HTTP requests on every widget load.

Fix: The injected pbjs.setConfig block also configures userSync:

js
userSync: {
  filterSettings: {
    iframe: { bidders: '*', filter: 'exclude' }  // disable iframe syncs entirely
  },
  syncDelay: 5000  // give auction time to complete before syncs start
}

Image pixel syncs are left enabled; they may succeed for some bidders that don't rely on third-party cookies.


3. WKWebView Autoplay → Fullscreen Takeover ✅ Already Correct

Problem (Prebid.js issue #11438): Prebid.js includes an autoplay detection feature that plays a 1-second silent video. In WKWebView on iPhone, if allowsInlineMediaPlayback is false (the iPhone default), this triggers the native fullscreen video player — a black-screen takeover with audio.

Status: Both SellwildAdView and SellwildWidgetView already set allowsInlineMediaPlayback = true and mediaTypesRequiringUserActionForPlayback = []. React Native's SellwildWidget sets allowsInlineMediaPlayback and mediaPlaybackRequiresUserAction={false}. Flutter's webview_flutter handles this by default. No action needed.


4. Android Multi-Process WebView Crash ✅ Fixed

Problem (Prebid Mobile Android issue #202, Chromium crbug.com/558377): On Android 9+ (API 28+), using WebView from more than one process with the same data directory causes crashes. Apps with separate processes (common in larger apps using :background or :sync processes) can trigger this on any WebView load.

Fix: Added SellwildWebViewCompat.configureForMultiProcess(context) to SellwildWidgetView.kt. Publishers should call this from Application.onCreate() before any WebView is created:

kotlin
// In your Application class:
override fun onCreate() {
    super.onCreate()
    SellwildWebViewCompat.configureForMultiProcess(this)
}

5. IDFA / GAID Not Passed to Prebid.js — Known Limitation (Not Fixed)

Problem: Prebid.js running in a WebView cannot access device identifiers (IDFA on iOS, GAID/AAID on Android) — these are OS-level APIs not exposed to JavaScript. Without device IDs:

  • Programmatic buyers that rely on deterministic audience matching will not bid.
  • Frequency capping across campaigns is broken.
  • Attribution to app install campaigns may fail.

Correct architecture: The native app should obtain IDFA/GAID (respecting ATT on iOS 14.5+), then pass them to Prebid.js via the ortb2.user.eids field:

js
pbjs.setConfig({
  ortb2: {
    user: {
      eids: [
        { source: "adserver.org", uids: [{ id: "IDFA_VALUE", atype: 3 }] }
      ]
    }
  }
});

Why not fixed here: Obtaining IDFA/GAID requires platform permissions, ATT prompt handling (iOS), and privacy policy disclosure — all of which belong to the host app, not the SDK. The SDK would need a userEids field in SellwildConfig and the host app would populate it after requesting ATT permission. This is a planned future addition.


Problem: When a native app implements its own CMP (OneTrust, Didomi, etc.) to show a GDPR consent dialog, the consent string is stored in native app storage (NSUserDefaults / SharedPreferences). Prebid.js running in a WebView will look for window.__tcfapi and find nothing — it cannot automatically read the native app's consent decision.

Per IAB TCF specification, an app that combines native and WebView content should call exportCMPInfo() on the CMP and inject the consent string into the WebView. Without this, Prebid.js will treat all vendors as having no consent and suppress all bidder calls in GDPR regions.

The SDK already passes tcfVersion, gppEnabled, and consentManagement attributes to the widget element. The web widget may handle consent through its own in-widget CMP flow. However, if the host app has its own CMP that has already obtained consent, that consent is not bridged.

Workaround (publishers must do this): After the native CMP collects consent, inject the TC string into the WebView before Prebid.js runs:

swift
// iOS
let tcString = UserDefaults.standard.string(forKey: "IABTCF_TCString") ?? ""
webView.evaluateJavaScript("window.IABTCF_TCString = '\(tcString)'", completionHandler: nil)

7. WKWebView Process Pool — Storage Isolation

Context: Each SellwildWidgetView and SellwildAdView instance creates its own WKWebViewConfiguration with a separate WKProcessPool. Per Apple documentation, WKWebView instances with different process pools do not share localStorage, sessionStorage, or cookies.

Implication: If a publisher embeds both a SellwildWidgetView and a SellwildAdView on the same screen, they run in separate WebKit processes. Prebid.js will initialize independently in each. This is expected behavior for isolated ad units, but means:

  • User ID sync state (cookies, localStorage) is not shared between views.
  • Each view runs its own separate header bidding auction.

This is not a bug — it is the expected behavior for independent ad units. Publishers who need shared Prebid.js state should use the full widget (which includes all ad units in one WebView) rather than mixing widget + standalone banner.


8. Prebid Mobile SDK vs Prebid.js in WebView

Official Prebid.org recommendation: For native iOS and Android apps, use Prebid Mobile SDK (not Prebid.js in a WebView). Prebid Mobile SDK is purpose-built for native apps:

  • Communicates directly with Prebid Server (server-side auctions, no client-side JS)
  • Reads IDFA / GAID natively
  • Handles SKAdNetwork (iOS) and ATT properly
  • Full ortb2.app compliance by default

The Sellwild SDK uses Prebid.js in a WebView because the web widget (sellwild-widget) already has a Prebid.js integration. Migrating to Prebid Mobile SDK would require a separate server-side Prebid Server setup and significant refactoring of the ad delivery pipeline. This is a known architectural trade-off, not a bug.

The current implementation is viable for publishers who prioritize deployment simplicity and already use the Sellwild web widget. The ortb2.app and userSync fixes in this pass reduce the CPM gap relative to a Prebid Mobile SDK integration.


9. SKAdNetwork / ATT (iOS 14.5+)

SKAdNetwork postback handling and App Tracking Transparency (ATT) prompts are the responsibility of the host app, not the SDK. The SDK does not present any ATT dialog or register SKAdNetwork impressions. Publishers must:

  1. Add SKAdNetworkIdentifier entries to Info.plist for relevant ad networks.
  2. Call ATTrackingManager.requestTrackingAuthorization() before loading the widget.
  3. Pass IDFA to SellwildConfig.appBundleId (IDFA pass-through is a planned feature — see item 5 above).

10. cookieSyncDelay — Deprecated Config Field

Note: Prebid.js previously used pbjs.setConfig({ cookieSyncDelay: N }). This is deprecated. The SDK's injected pre-config correctly uses userSync.syncDelay, which is the current API.


Package / Build System

ItemStatusNote
core/tsconfig.jsonNot reviewed — not present in file listShould target ESNext or ES2017+ for async/await
core/package.json⚠️ Missing prepublishOnlyWill publish without building dist/
react-native/package.jsonPeer deps correctly declared, main points to source (fine for RN)
ios/Package.swiftCorrect SPM manifest, iOS 13+ target
ios/SellwildSDK.podspecNot reviewedAssumed present but not in repo file list
android/build.gradle.ktsNot reviewedNot in file list — assumed standard Android library setup
flutter/pubspec.yamlCorrect dependencies (webview_flutter ^4, http ^1.1)

Sellwild SDK Documentation