@sellwild/react-native-sdk 1.7.4 → 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.
@@ -1,383 +0,0 @@
1
- import type { SellwildConfig, AdSize } from '@sellwild/sdk-core'
2
- import { resolveListingsUrl } from '@sellwild/sdk-core'
3
-
4
- const WIDGET_CDN = 'https://widget.sellwild.com'
5
-
6
- // ─────────────────────────────────────────────────────────────────────────────
7
- // Prebid.js WebView pre-configuration
8
- //
9
- // This script runs BEFORE prebid.js loads by enqueuing via pbjs.que.
10
- // It addresses two critical WebView issues:
11
- //
12
- // 1. ortb2.app — Prebid.js running in a native WebView must declare itself as
13
- // in-app inventory (OpenRTB app object) rather than web (site object).
14
- // Without this, bid requests look like browser traffic; DSPs that buy app
15
- // inventory separately will not bid, and app-ads.txt enforcement is bypassed.
16
- //
17
- // 2. userSync iframe filtering — Third-party cookies are blocked in all native
18
- // WebViews (WKWebView, Android WebView). Iframe-based cookie syncs will
19
- // always fail silently, wasting network requests and adding latency. Image
20
- // pixel syncs may still work for some bidders. A longer syncDelay gives the
21
- // auction time to complete before sync requests compete for bandwidth.
22
- // ─────────────────────────────────────────────────────────────────────────────
23
- function buildPrebidPreConfigScript(config: SellwildConfig): string {
24
- const ortb2App: Record<string, unknown> = {
25
- publisher: { id: config.partnerCode },
26
- }
27
- if (config.appBundleId) ortb2App['bundle'] = config.appBundleId
28
- if (config.appStoreUrl) ortb2App['storeurl'] = config.appStoreUrl
29
-
30
- // GDPR / privacy signals — PBS defaults to gdpr=1 (GDPR applies) and
31
- // blocks all bidder calls if no consent string is present. The SDK must
32
- // explicitly declare the user's jurisdiction so the server knows whether
33
- // to enforce consent. The host app provides gdprApplies and tcString
34
- // via SellwildConfig; default is gdpr=0 (US traffic, not subject).
35
- const ortb2Regs: Record<string, unknown> = {
36
- ext: { gdpr: (config as any).gdprApplies ? 1 : 0 },
37
- }
38
- const ortb2User: Record<string, unknown> = {}
39
- if ((config as any).gdprApplies && (config as any).tcString) {
40
- ortb2User['ext'] = { consent: (config as any).tcString }
41
- }
42
-
43
- let s2sConfigBlock = ''
44
- if (config.prebidServer) {
45
- const ps = config.prebidServer
46
- const s2s = {
47
- accountId: ps.accountId,
48
- bidders: ps.bidders,
49
- timeout: ps.timeout ?? 1500,
50
- adapter: 'prebidServer',
51
- endpoint: { p1Consent: ps.endpoint, noP1Consent: ps.endpoint },
52
- ...(ps.syncEndpoint
53
- ? { syncEndpoint: { p1Consent: ps.syncEndpoint, noP1Consent: ps.syncEndpoint } }
54
- : {}),
55
- }
56
- s2sConfigBlock = `,
57
- // Route all Prebid bidder calls through Prebid Server (S2S mode).
58
- // Solves cookie/IDFA limitations — the auction runs server-to-server.
59
- s2sConfig: ${JSON.stringify(s2s)}`
60
- }
61
-
62
- return `
63
- <script>
64
- // Prebid.js WebView pre-configuration — runs before prebid.js initialises.
65
- window.pbjs = window.pbjs || {};
66
- window.pbjs.que = window.pbjs.que || [];
67
- window.pbjs.que.push(function() {
68
- window.pbjs.setConfig({
69
- // Declare in-app inventory so DSPs bid on app traffic, not web traffic.
70
- // Include regs.ext.gdpr so PBS knows whether to enforce consent.
71
- ortb2: {
72
- app: ${JSON.stringify(ortb2App)},
73
- regs: ${JSON.stringify(ortb2Regs)}${Object.keys(ortb2User).length ? `,
74
- user: ${JSON.stringify(ortb2User)}` : ''}
75
- },
76
- // Iframe cookie syncs always fail in WebViews — disable them.
77
- // Image pixel syncs may still work for some bidders.
78
- userSync: {
79
- filterSettings: {
80
- iframe: { bidders: '*', filter: 'exclude' }
81
- },
82
- syncDelay: 5000
83
- }${s2sConfigBlock}${config.debug ? `,
84
- // debug mode
85
- debug: true` : ''}
86
- });
87
- });
88
- </script>`
89
- }
90
-
91
- // ─────────────────────────────────────────────────────────────────────────────
92
- // Config → element attributes
93
- //
94
- // The sellwild-widget reads configuration from HTML element attributes via
95
- // withCustomizationsFromElement() in src/providers/Customizations/index.ts.
96
- // Attribute names can be in any case (kebab-case, camelCase, CONSTANT_CASE).
97
- // Complex objects are JSON-stringified; arrays are comma-separated.
98
- // ─────────────────────────────────────────────────────────────────────────────
99
- function configToAttributes(config: SellwildConfig): string {
100
- const parts: string[] = []
101
-
102
- const add = (name: string, value: unknown) => {
103
- if (value === undefined || value === null || value === '' || value === false || value === 0) return
104
- if (typeof value === 'object') {
105
- parts.push(`${name}='${JSON.stringify(value).replace(/'/g, '&#39;')}'`)
106
- } else {
107
- parts.push(`${name}="${String(value)}"`)
108
- }
109
- }
110
-
111
- // Identity / listings
112
- add('partner-code', config.partnerCode)
113
- add('listings', resolveListingsUrl(config))
114
-
115
- // Disable remote customization fetch — the widget defaults to fetching
116
- // https://widget.sellwild.com/{partnerCode}/{slug}.json which (a) doesn't
117
- // exist for SDK consumers and (b) is unnecessary because the SDK supplies
118
- // every customization via element attributes already.
119
- parts.push('customize="false"')
120
-
121
- // Ad system selection — REQUIRED. AdStack.initializeAdStack() switches on
122
- // theme.adType and silently does nothing if it's unset, meaning Prebid never
123
- // loads and no auctions ever run. PrebidOnly is the dominant value across
124
- // production publisher widgets. Override via config.adType if needed.
125
- add('ad-type', (config as any).adType || 'PrebidOnly')
126
-
127
- // Display
128
- add('title', config.title)
129
- add('link-text', config.linkText)
130
- add('buy-now-text', config.buyNowText)
131
- add('title-color', config.titleColor)
132
- add('title-size', config.titleSize)
133
- add('link-color', config.linkColor)
134
- add('link-size', config.linkSize)
135
- add('font-size', config.fontSize)
136
- add('font-family', config.fontFamily)
137
- add('font-color', config.fontColor)
138
- add('price-color', config.priceColor)
139
- add('price-font-color', config.priceFontColor)
140
- add('margin-bottom', config.marginBottom)
141
- add('card-width', config.cardWidth)
142
- add('card-height', config.cardHeight)
143
- add('colors', config.colors?.join(','))
144
- add('watermark', config.watermark || undefined)
145
- add('watermark-title', config.watermarkTitle)
146
- add('overlay-title', config.overlayTitle || undefined)
147
- add('css', config.css)
148
-
149
- // Ads – display
150
- add('gam-tag', config.gamTag)
151
- add('gam-tag-desc', config.gamTagDesc)
152
- add('banner-zid', config.bannerZid)
153
- add('bottom-banner-zid', config.bottomBannerZid)
154
- add('mobile-banner-zid', config.mobileBannerZid)
155
- // Filter empties before joining — the widget's parser splits on ',' and
156
- // does not strip empty strings, so a stray comma yields [""] not [].
157
- add('mobile-zid', config.mobileZids?.filter(Boolean).join(','))
158
- add('display-zid', config.displayZids?.filter(Boolean).join(','))
159
- add('skyscraper-zid', config.skyscraperZid)
160
- add('hide-banner-top', config.hideBannerTop || undefined)
161
- add('hide-banner-bottom', config.hideBannerBottom || undefined)
162
- add('gpt-proxy-url', config.gptProxyUrl)
163
- add('disable-gpt', config.disableGpt || undefined)
164
- add('ad-disable-display', config.adDisableDisplay || undefined)
165
- add('safe-frame', config.safeFrame || undefined)
166
-
167
- // Ads – refresh
168
- add('ad-refresh-max', config.adRefreshMax)
169
- add('ad-refresh-max-mobile', config.adRefreshMaxMobile)
170
- add('ad-refresh-interval', config.adRefreshInterval)
171
- add('max-failed-auctions', config.maxFailedAuctions)
172
- add('prebid-src', config.prebidSrc)
173
- add('prebid-defer', config.prebidDefer)
174
- add('floor-multiplier', config.floorMultiplier !== 1 ? config.floorMultiplier : undefined)
175
-
176
- // Ads – geo
177
- add('ad-geo-block', config.adGeoBlock)
178
- add('ad-geo-block-refresh', config.adGeoBlockRefresh)
179
-
180
- // Ads – compliance
181
- add('gpp-enabled', config.gppEnabled || undefined)
182
- add('tcf-version', config.tcfVersion)
183
- add('consent-management', config.consentManagement)
184
- add('schain-sid', config.schainSid)
185
- add('s2s-config', config.s2sConfig)
186
- add('iab-cats', Array.isArray(config.iabCats) ? config.iabCats.join(',') : config.iabCats)
187
-
188
- // Third-party (typed fields with defaults — kept emitted explicitly for
189
- // backwards compatibility with static buildConfig() callers)
190
- add('boltive', config.boltive || undefined)
191
- add('boltive-client-id', config.boltiveClientId)
192
- add('lotame', config.lotame || undefined)
193
- add('growthcode', config.growthcode)
194
- add('bh-tag', config.bhTag)
195
-
196
- // Mobile ad controls
197
- add('enable-interstitial', config.enableInterstitial || undefined)
198
- add('enable-fullscreen-video', config.enableFullscreenVideo || undefined)
199
- add('interstitials-per-session', config.interstitialsPerSession)
200
- add('video-takeovers-per-session', config.videoTakeoversPerSession)
201
-
202
- // Debug
203
- add('debug', config.debug || undefined)
204
- add('membership-type', config.membershipType)
205
-
206
- // ─── Remote passthrough ────────────────────────────────────────────────────
207
- // Forward the raw CDN payload verbatim. The widget's attribute parser is
208
- // case-insensitive (kebab-case / camelCase / CONSTANT_CASE all work) and
209
- // accepts unknown keys, so every CMS-defined bidder, waterfall partner, or
210
- // ad-network setting flows through to the WebView without an SDK release.
211
- //
212
- // Skip keys that we already emitted via typed fields above to avoid
213
- // double-emission. Identity fields (CODE, SLUG, NAME, LISTINGS) are handled
214
- // by the typed block; everything else (bidders, third-party, etc.) lands
215
- // here as the canonical CONSTANT_CASE attribute.
216
- const emittedFromTyped = new Set<string>([
217
- 'CODE', 'SLUG', 'NAME', 'LISTINGS',
218
- 'TITLE', 'LINK_TEXT', 'BUY_NOW_TEXT', 'TITLE_COLOR', 'LINK_COLOR',
219
- 'FONT_FAMILY', 'FONT_URL', 'FONT_COLOR', 'PRICE_COLOR', 'PRICE_FONT_COLOR',
220
- 'MARGIN_BOTTOM', 'CARD_WIDTH', 'OVERLAY_TITLE', 'COLORS', 'CSS',
221
- 'WATERMARK', 'WATERMARK_TITLE',
222
- 'BANNER_ZID', 'BOTTOM_BANNER_ZID', 'MOBILE_BANNER_ZID', 'MOBILE_ZID',
223
- 'DISPLAY_ZID', 'HIDE_BANNER_TOP', 'HIDE_BANNER_BOTTOM', 'GAM',
224
- 'DISABLE_GPT', 'AD_UNITS', 'SAFE_FRAME', 'AD_DISABLE_DISPLAY',
225
- 'AD_REFRESH_MAX', 'AD_REFRESH_MAX_MOBILE', 'AD_REFRESH_INTERVAL',
226
- 'MAX_FAILED_AUCTIONS', 'PREBID_DEFER', 'PREBID_SRC',
227
- 'AD_GEO_BLOCK', 'AD_GEO_BLOCK_REFRESH',
228
- 'GPP_ENABLED', 'TCF_VERSION', 'CONSENT_MANAGEMENT', 'SCHAIN_SID',
229
- 'S2S_CONFIG', 'IAB_CATS',
230
- 'ENABLE_INTERSTITIAL', 'ENABLE_FULLSCREEN_VIDEO',
231
- 'INTERSTITIALS_PER_SESSION', 'VIDEO_TAKEOVERS_PER_SESSION',
232
- 'BOLTIVE', 'BOLTIVE_CLIENT_ID', 'LOTAME', 'GROWTHCODE', 'BH_TAG',
233
- ])
234
- if (config.remote) {
235
- for (const [key, value] of Object.entries(config.remote)) {
236
- if (emittedFromTyped.has(key)) continue
237
- add(key, value)
238
- }
239
- }
240
-
241
- return parts.join('\n ')
242
- }
243
-
244
- // ─────────────────────────────────────────────────────────────────────────────
245
- // Widget HTML builder
246
- //
247
- // The widget JS URL can be:
248
- // - A publisher-specific bundle: https://widget.sellwild.com/{CODE}/{SLUG}.js
249
- // (config already baked in at build time; element attributes are optional)
250
- // - The generic bundle: https://widget.sellwild.com/partner.js
251
- // (reads all config from element attributes — SDK default)
252
- //
253
- // Pass widgetJsUrl in config to override the default generic bundle.
254
- // ─────────────────────────────────────────────────────────────────────────────
255
- export function buildWidgetHtml(config: SellwildConfig & { widgetJsUrl?: string }): string {
256
- // partner.js is the canonical generic widget bundle. It loads its own Prebid
257
- // build internally (from cache.sellwild.com), so the SDK MUST NOT inject a
258
- // separate prebid <script> tag — doing so causes a double-load and breaks
259
- // header bidding initialization order.
260
- const widgetSrc = config.widgetJsUrl ?? `${WIDGET_CDN}/partner.js`
261
- const attrs = configToAttributes(config)
262
-
263
- const prebidPreConfig = buildPrebidPreConfigScript(config)
264
-
265
- return `<!DOCTYPE html>
266
- <html>
267
- <head>
268
- <meta charset="utf-8">
269
- <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
270
- <style>
271
- * { box-sizing: border-box; margin: 0; padding: 0; }
272
- html, body { width: 100%; background: transparent; overflow-x: hidden; }
273
- </style>
274
- ${prebidPreConfig}
275
- </head>
276
- <body>
277
- <sellwild-widget
278
- ${attrs}
279
- ></sellwild-widget>
280
-
281
- <script>
282
- (function() {
283
- function send(type, payload) {
284
- try {
285
- window.ReactNativeWebView.postMessage(JSON.stringify(Object.assign({ type: type }, payload || {})));
286
- } catch(e) {}
287
- }
288
-
289
- // partner/index.tsx calls window.open() on listing tap — intercept ALL
290
- // calls. Listings link to external sites (eBay, Amazon, dealer sites, etc.)
291
- // so we can't filter by domain. The widget only uses window.open for listings.
292
- var _open = window.open;
293
- window.open = function(url) {
294
- if (url) {
295
- send('LISTING_CLICK', { url: url });
296
- return null;
297
- }
298
- return _open.apply(window, arguments);
299
- };
300
-
301
- document.addEventListener('DOMContentLoaded', function() {
302
- setTimeout(function() { send('WIDGET_LOADED'); }, 600);
303
- });
304
-
305
- window.addEventListener('error', function(e) {
306
- send('ERROR', { message: e.message || 'Widget load error' });
307
- });
308
- })();
309
- </script>
310
-
311
- <script async src="${widgetSrc}"></script>
312
- </body>
313
- </html>`
314
- }
315
-
316
- // ─────────────────────────────────────────────────────────────────────────────
317
- // Banner HTML builder
318
- // ─────────────────────────────────────────────────────────────────────────────
319
- export function buildBannerHtml(
320
- config: SellwildConfig,
321
- zoneId: number | string,
322
- size: AdSize
323
- ): string {
324
- const [width, height] = size.split('x').map(Number)
325
- const gptSrc = config.gptProxyUrl
326
- ? `${config.gptProxyUrl}/tag/js/gpt.js`
327
- : 'https://securepubads.g.doubleclick.net/tag/js/gpt.js'
328
-
329
- const adScript = config.gamTag && !config.disableGpt
330
- ? buildGptScript(config.gamTag, gptSrc, width, height)
331
- : buildZoneScript(String(zoneId), width, height)
332
-
333
- return `<!DOCTYPE html>
334
- <html>
335
- <head>
336
- <meta charset="utf-8">
337
- <meta name="viewport" content="width=device-width, initial-scale=1">
338
- <style>
339
- * { box-sizing: border-box; margin: 0; padding: 0; }
340
- html, body { width: ${width}px; height: ${height}px; overflow: hidden; background: transparent; }
341
- #ad { width: ${width}px; height: ${height}px; }
342
- </style>
343
- </head>
344
- <body>
345
- <div id="ad"></div>
346
- <script>
347
- function notify(type) {
348
- try { window.ReactNativeWebView.postMessage(JSON.stringify({ type: type })); } catch(e) {}
349
- }
350
- ${adScript}
351
- </script>
352
- </body>
353
- </html>`
354
- }
355
-
356
- function buildGptScript(gamTag: string, gptSrc: string, w: number, h: number): string {
357
- return `
358
- window.googletag = window.googletag || { cmd: [] };
359
- var s = document.createElement('script');
360
- s.src = '${gptSrc}'; s.async = true;
361
- document.head.appendChild(s);
362
- googletag.cmd.push(function() {
363
- var slot = googletag.defineSlot('${gamTag}', [${w}, ${h}], 'ad');
364
- if (slot) {
365
- slot.addService(googletag.pubads());
366
- googletag.pubads().enableSingleRequest();
367
- googletag.pubads().addEventListener('slotRenderEnded', function(e) {
368
- if (!e.isEmpty) notify('AD_IMPRESSION');
369
- });
370
- googletag.enableServices();
371
- googletag.display('ad');
372
- }
373
- });`
374
- }
375
-
376
- function buildZoneScript(zoneId: string, w: number, h: number): string {
377
- return `
378
- var s = document.createElement('script');
379
- s.src = 'https://bidstream.sellwild.com/ads?zone=${zoneId}&w=${w}&h=${h}';
380
- s.async = true;
381
- s.onload = function() { notify('AD_IMPRESSION'); };
382
- document.getElementById('ad').appendChild(s);`
383
- }