@streamlayer/feature-advertisement 1.0.0 → 1.1.1-rc.1342
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/classic/package.json +5 -0
- package/lib/cjs/advertisement.d.ts +64 -0
- package/lib/cjs/advertisement.js +307 -0
- package/lib/cjs/index.d.ts +19 -0
- package/lib/cjs/index.js +39 -0
- package/lib/cjs/package.json +1 -0
- package/lib/cjs/units/external-ad.d.ts +58 -0
- package/lib/cjs/units/external-ad.js +390 -0
- package/lib/cjs/units/external-pause-ad.d.ts +50 -0
- package/lib/cjs/units/external-pause-ad.js +253 -0
- package/lib/cjs/units/gam.d.ts +16 -0
- package/lib/cjs/units/gam.js +104 -0
- package/lib/cjs/units/parse-vast-extensions.d.ts +17 -0
- package/lib/cjs/units/parse-vast-extensions.js +52 -0
- package/lib/cjs/units/pause-ad-card.d.ts +49 -0
- package/lib/cjs/units/pause-ad-card.js +136 -0
- package/lib/cjs/units/pause-ad.d.ts +16 -0
- package/lib/cjs/units/pause-ad.js +52 -0
- package/lib/cjs/units/ssai-ad-break.d.ts +21 -0
- package/lib/cjs/units/ssai-ad-break.js +11 -0
- package/lib/cjs/units/ssai-creative.d.ts +89 -0
- package/lib/cjs/units/ssai-creative.js +136 -0
- package/lib/cjs/units/ssai-detector.d.ts +180 -0
- package/lib/cjs/units/ssai-detector.js +1156 -0
- package/lib/cjs/units/ssai-preload.d.ts +20 -0
- package/lib/cjs/units/ssai-preload.js +62 -0
- package/lib/cjs/units/ssai-providers.d.ts +117 -0
- package/lib/cjs/units/ssai-providers.js +263 -0
- package/lib/cjs/units/vast-tracker.d.ts +45 -0
- package/lib/cjs/units/vast-tracker.js +64 -0
- package/lib/classic/_virtual/_rollupPluginBabelHelpers.js +71 -0
- package/lib/classic/advertisement.js +331 -0
- package/lib/classic/cjs/_virtual/_rollupPluginBabelHelpers.js +77 -0
- package/lib/classic/cjs/advertisement.js +333 -0
- package/lib/classic/cjs/index.js +47 -0
- package/lib/classic/cjs/package.json +1 -0
- package/lib/classic/cjs/units/external-ad.js +453 -0
- package/lib/classic/cjs/units/external-pause-ad.js +274 -0
- package/lib/classic/cjs/units/gam.js +110 -0
- package/lib/classic/cjs/units/parse-vast-extensions.js +56 -0
- package/lib/classic/cjs/units/pause-ad-card.js +134 -0
- package/lib/classic/cjs/units/pause-ad.js +72 -0
- package/lib/classic/cjs/units/ssai-ad-break.js +11 -0
- package/lib/classic/cjs/units/ssai-creative.js +142 -0
- package/lib/classic/cjs/units/ssai-detector.js +1170 -0
- package/lib/classic/cjs/units/ssai-preload.js +72 -0
- package/lib/classic/cjs/units/ssai-providers.js +299 -0
- package/lib/classic/cjs/units/vast-tracker.js +81 -0
- package/lib/classic/index.js +28 -0
- package/lib/classic/units/external-ad.js +447 -0
- package/lib/classic/units/external-pause-ad.js +271 -0
- package/lib/classic/units/gam.js +108 -0
- package/lib/classic/units/parse-vast-extensions.js +54 -0
- package/lib/classic/units/pause-ad-card.js +131 -0
- package/lib/classic/units/pause-ad.js +70 -0
- package/lib/classic/units/ssai-ad-break.js +9 -0
- package/lib/classic/units/ssai-creative.js +140 -0
- package/lib/classic/units/ssai-detector.js +1167 -0
- package/lib/classic/units/ssai-preload.js +68 -0
- package/lib/classic/units/ssai-providers.js +289 -0
- package/lib/classic/units/vast-tracker.js +77 -0
- package/lib/index.d.ts +2 -1
- package/lib/index.js +6 -0
- package/lib/units/pause-ad-card.d.ts +49 -0
- package/lib/units/pause-ad-card.js +130 -0
- package/lib/units/pause-ad.d.ts +1 -18
- package/lib/units/pause-ad.js +2 -49
- package/package.json +42 -9
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import 'core-js/modules/es.array.iterator.js';
|
|
2
|
+
import 'core-js/modules/es.promise.js';
|
|
3
|
+
import 'core-js/modules/esnext.iterator.constructor.js';
|
|
4
|
+
import 'core-js/modules/esnext.iterator.filter.js';
|
|
5
|
+
import 'core-js/modules/web.dom-collections.iterator.js';
|
|
6
|
+
import { createLogger } from '@streamlayer/sdk-web-logger/classic';
|
|
7
|
+
|
|
8
|
+
const log = createLogger('ssai:preload');
|
|
9
|
+
/**
|
|
10
|
+
* How long the overlay is willing to wait for artwork. A slow or dead image host
|
|
11
|
+
* must never hold the bar back — the ad is already playing, and a bar that shows
|
|
12
|
+
* late is worse than one that shows without its logo (the logo is hidden on error
|
|
13
|
+
* anyway). Warming the browser cache is the goal; blocking is not.
|
|
14
|
+
*/
|
|
15
|
+
const DEFAULT_PRELOAD_TIMEOUT_MS = 2000;
|
|
16
|
+
/**
|
|
17
|
+
* Fetch images into the browser cache and resolve once they are all settled, or
|
|
18
|
+
* once `timeoutMs` elapses — whichever comes first. Never rejects: a creative
|
|
19
|
+
* whose artwork fails is still a creative worth showing.
|
|
20
|
+
*/
|
|
21
|
+
const preloadImages = (urls, timeoutMs = DEFAULT_PRELOAD_TIMEOUT_MS) => {
|
|
22
|
+
const targets = urls.filter(url => !!url);
|
|
23
|
+
// `Image` is absent in SSR/worker contexts; there is nothing to warm there.
|
|
24
|
+
if (targets.length === 0 || typeof Image === 'undefined') {
|
|
25
|
+
return Promise.resolve();
|
|
26
|
+
}
|
|
27
|
+
return new Promise(resolve => {
|
|
28
|
+
let settled = false;
|
|
29
|
+
let remaining = targets.length;
|
|
30
|
+
const finish = () => {
|
|
31
|
+
if (settled) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
settled = true;
|
|
35
|
+
clearTimeout(timer);
|
|
36
|
+
resolve();
|
|
37
|
+
};
|
|
38
|
+
const timer = setTimeout(() => {
|
|
39
|
+
log.debug({
|
|
40
|
+
remaining
|
|
41
|
+
}, 'ssai preload timed out, showing anyway');
|
|
42
|
+
finish();
|
|
43
|
+
}, timeoutMs);
|
|
44
|
+
const one = () => {
|
|
45
|
+
remaining -= 1;
|
|
46
|
+
if (remaining <= 0) {
|
|
47
|
+
finish();
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
for (const url of targets) {
|
|
51
|
+
const image = new Image();
|
|
52
|
+
image.onload = one;
|
|
53
|
+
image.onerror = one;
|
|
54
|
+
image.src = url;
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Warm every image the creative carries — the overlay's brand logo, and the
|
|
60
|
+
* companion banner for hosts that render one — so the bar fades in complete
|
|
61
|
+
* rather than popping its artwork in a beat later.
|
|
62
|
+
*/
|
|
63
|
+
const preloadCreativeAssets = (creative, timeoutMs) => {
|
|
64
|
+
var _creative$companion;
|
|
65
|
+
return preloadImages([creative.iconUrl, (_creative$companion = creative.companion) === null || _creative$companion === void 0 ? void 0 : _creative$companion.staticResource], timeoutMs);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export { DEFAULT_PRELOAD_TIMEOUT_MS, preloadCreativeAssets, preloadImages };
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import { asyncToGenerator as _asyncToGenerator, objectSpread2 as _objectSpread2 } from '../_virtual/_rollupPluginBabelHelpers.js';
|
|
2
|
+
import 'core-js/modules/es.error.cause.js';
|
|
3
|
+
import 'core-js/modules/es.array.iterator.js';
|
|
4
|
+
import 'core-js/modules/es.promise.js';
|
|
5
|
+
import 'core-js/modules/esnext.iterator.constructor.js';
|
|
6
|
+
import 'core-js/modules/esnext.iterator.filter.js';
|
|
7
|
+
import 'core-js/modules/esnext.iterator.find.js';
|
|
8
|
+
import 'core-js/modules/esnext.iterator.map.js';
|
|
9
|
+
import 'core-js/modules/web.dom-collections.iterator.js';
|
|
10
|
+
import 'core-js/modules/web.dom-exception.stack.js';
|
|
11
|
+
import { createLogger } from '@streamlayer/sdk-web-logger/classic';
|
|
12
|
+
import { VASTClient } from '@dailymotion/vast-client';
|
|
13
|
+
|
|
14
|
+
const log = createLogger('ssai:provider');
|
|
15
|
+
const DEFAULT_CREATIVE_API_BASE = 'https://hls.next.streamlayer.io/api/creatives/';
|
|
16
|
+
/**
|
|
17
|
+
* Shared low-level beacon helper. Prefers `navigator.sendBeacon` and falls back
|
|
18
|
+
* to a no-cors `fetch`. Used by the default impression path and available to
|
|
19
|
+
* providers that fire URL-based tracking pixels.
|
|
20
|
+
*/
|
|
21
|
+
const fireBeacon = url => {
|
|
22
|
+
try {
|
|
23
|
+
if (typeof navigator !== 'undefined' && typeof navigator.sendBeacon === 'function') {
|
|
24
|
+
navigator.sendBeacon(url);
|
|
25
|
+
} else {
|
|
26
|
+
void fetch(url, {
|
|
27
|
+
mode: 'no-cors'
|
|
28
|
+
}).catch(error => {
|
|
29
|
+
log.warn(error, 'ssai beacon failed');
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
} catch (error) {
|
|
33
|
+
log.warn(error, 'ssai beacon failed');
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
const resolveAdvertiser = (advertiser, title) => {
|
|
37
|
+
if (advertiser) {
|
|
38
|
+
return advertiser;
|
|
39
|
+
}
|
|
40
|
+
if (title) {
|
|
41
|
+
return title;
|
|
42
|
+
}
|
|
43
|
+
return 'Ad';
|
|
44
|
+
};
|
|
45
|
+
const buildCreativeUrl = (base, creativeId) => {
|
|
46
|
+
const normalizedBase = base.endsWith('/') ? base : `${base}/`;
|
|
47
|
+
return `${normalizedBase}${encodeURIComponent(creativeId)}`;
|
|
48
|
+
};
|
|
49
|
+
const mapApiResponse = raw => {
|
|
50
|
+
const trackingEvents = Array.isArray(raw.trackingEvents) ? raw.trackingEvents.filter(entry => Boolean((entry === null || entry === void 0 ? void 0 : entry.event) && (entry === null || entry === void 0 ? void 0 : entry.url))).map(entry => ({
|
|
51
|
+
event: entry.event,
|
|
52
|
+
url: entry.url
|
|
53
|
+
})) : [];
|
|
54
|
+
const rawCompanion = Array.isArray(raw.companions) ? raw.companions.find(entry => !!(entry !== null && entry !== void 0 && entry.staticResource)) : undefined;
|
|
55
|
+
const companion = rawCompanion !== null && rawCompanion !== void 0 && rawCompanion.staticResource ? {
|
|
56
|
+
staticResource: rawCompanion.staticResource,
|
|
57
|
+
width: Number(rawCompanion.width) || 0,
|
|
58
|
+
height: Number(rawCompanion.height) || 0,
|
|
59
|
+
clickThrough: rawCompanion.clickThrough,
|
|
60
|
+
clickTracking: Array.isArray(rawCompanion.clickTracking) ? rawCompanion.clickTracking.filter(url => typeof url === 'string' && !!url) : []
|
|
61
|
+
} : undefined;
|
|
62
|
+
return {
|
|
63
|
+
creativeId: raw.creativeId,
|
|
64
|
+
advertiser: resolveAdvertiser(raw.advertiser, raw.title),
|
|
65
|
+
title: raw.title || resolveAdvertiser(raw.advertiser, raw.title),
|
|
66
|
+
iconUrl: raw.iconUrl || undefined,
|
|
67
|
+
clickThroughUrl: raw.clickThroughUrl || undefined,
|
|
68
|
+
trackingEvents,
|
|
69
|
+
companion
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* Fire every tracking event with the given name, and report how many went out.
|
|
74
|
+
*
|
|
75
|
+
* ALL matches, not the first: the creative contract states that a creative may
|
|
76
|
+
* repeat an `event` value to carry several URLs, and that consumers must not
|
|
77
|
+
* assume uniqueness. Today's payloads carry one URL per event, so this is not a
|
|
78
|
+
* change in what goes on the wire — it just stops being wrong the day a server
|
|
79
|
+
* sends two.
|
|
80
|
+
*/
|
|
81
|
+
const fireTrackingEvents = (creative, event) => {
|
|
82
|
+
const matches = creative.trackingEvents.filter(entry => entry.event === event);
|
|
83
|
+
for (const match of matches) {
|
|
84
|
+
fireBeacon(match.url);
|
|
85
|
+
}
|
|
86
|
+
return matches.length;
|
|
87
|
+
};
|
|
88
|
+
/**
|
|
89
|
+
* Shared default impression behavior: fire the `impression` tracking events.
|
|
90
|
+
* Used by the StreamLayer provider and as the fallback in
|
|
91
|
+
* {@link SsaiCreativeCache} when a provider doesn't override `fireImpression`.
|
|
92
|
+
* Exported so both call sites share one definition.
|
|
93
|
+
*/
|
|
94
|
+
const fireDefaultImpression = creative => {
|
|
95
|
+
fireTrackingEvents(creative, 'impression');
|
|
96
|
+
};
|
|
97
|
+
/**
|
|
98
|
+
* StreamLayer's internal creative provider. Resolves creatives from the JSON
|
|
99
|
+
* creative API (`GET {creativeApiBase}{encodeURIComponent(creativeId)}`) and
|
|
100
|
+
* fires impressions from the `'impression'` tracking event via `sendBeacon`.
|
|
101
|
+
*/
|
|
102
|
+
const createStreamLayerProvider = (config = {}) => {
|
|
103
|
+
var _config$creativeApiBa;
|
|
104
|
+
const creativeApiBase = (_config$creativeApiBa = config.creativeApiBase) !== null && _config$creativeApiBa !== void 0 ? _config$creativeApiBa : DEFAULT_CREATIVE_API_BASE;
|
|
105
|
+
return {
|
|
106
|
+
id: 'streamlayer',
|
|
107
|
+
resolve: function () {
|
|
108
|
+
var _ref = _asyncToGenerator(function* (creativeId, signal) {
|
|
109
|
+
const url = buildCreativeUrl(creativeApiBase, creativeId);
|
|
110
|
+
const response = yield fetch(url, {
|
|
111
|
+
signal
|
|
112
|
+
});
|
|
113
|
+
if (!response.ok) {
|
|
114
|
+
throw new Error(`creative request failed: ${response.status}`);
|
|
115
|
+
}
|
|
116
|
+
const raw = yield response.json();
|
|
117
|
+
return mapApiResponse(_objectSpread2(_objectSpread2({}, raw), {}, {
|
|
118
|
+
creativeId: raw.creativeId || creativeId
|
|
119
|
+
}));
|
|
120
|
+
});
|
|
121
|
+
return function resolve(_x, _x2) {
|
|
122
|
+
return _ref.apply(this, arguments);
|
|
123
|
+
};
|
|
124
|
+
}(),
|
|
125
|
+
fireImpression: fireDefaultImpression
|
|
126
|
+
};
|
|
127
|
+
};
|
|
128
|
+
const buildQuortexUrl = (base, tenantName, adId) => {
|
|
129
|
+
// Deliberately string concatenation rather than `URL`/`URLSearchParams`: this
|
|
130
|
+
// package ships to 2016-era TV engines (see the chrome-32 classic build).
|
|
131
|
+
const separator = base.indexOf('?') === -1 ? '?' : '&';
|
|
132
|
+
const tenant = tenantName ? `TenantName=${encodeURIComponent(tenantName)}&` : '';
|
|
133
|
+
return `${base}${separator}${tenant}adID=${encodeURIComponent(adId)}&info=1`;
|
|
134
|
+
};
|
|
135
|
+
/**
|
|
136
|
+
* Quortex / Synamedia DAI ad metadata provider.
|
|
137
|
+
*
|
|
138
|
+
* `GET {adApiBase}?TenantName={tenantName}&adID={adId}&info=1` (the `TenantName`
|
|
139
|
+
* part omitted when `adApiBase` already carries it) — the ad id comes
|
|
140
|
+
* from the manifest's `X-AD-ID` DATERANGE attribute (see `ssai-detector.ts`, which
|
|
141
|
+
* reconstructs the ad window from the tracking-marker burst those tags form).
|
|
142
|
+
*
|
|
143
|
+
* The response is byte-compatible with the StreamLayer creative API, so the same
|
|
144
|
+
* `mapApiResponse` normalizes both.
|
|
145
|
+
*
|
|
146
|
+
* Known limitation: impressions fire the `impression` tracking event from the API
|
|
147
|
+
* response, not the per-session beacon the manifest carries in `X-AD-URL`. The two
|
|
148
|
+
* differ in `trackingid`/`USERID`, so Quortex-side attribution will see the
|
|
149
|
+
* generic id rather than the viewer's session until the manifest beacons are
|
|
150
|
+
* plumbed through to the provider.
|
|
151
|
+
*/
|
|
152
|
+
const createQuortexProvider = (config = {}) => {
|
|
153
|
+
const {
|
|
154
|
+
adApiBase,
|
|
155
|
+
tenantName
|
|
156
|
+
} = config;
|
|
157
|
+
if (!adApiBase) {
|
|
158
|
+
// Loud, but not thrown: this runs inside the host's render effect via
|
|
159
|
+
// `startSsaiMonitoring`, and taking the app down over a config typo is worse
|
|
160
|
+
// than an overlay that never appears and says why.
|
|
161
|
+
log.error({
|
|
162
|
+
adApiBase,
|
|
163
|
+
tenantName
|
|
164
|
+
}, 'quortex provider requires `adApiBase`');
|
|
165
|
+
}
|
|
166
|
+
return {
|
|
167
|
+
id: 'quortex',
|
|
168
|
+
resolve: function () {
|
|
169
|
+
var _ref2 = _asyncToGenerator(function* (adId, signal) {
|
|
170
|
+
if (!adApiBase) {
|
|
171
|
+
throw new Error('quortex provider is missing `adApiBase`');
|
|
172
|
+
}
|
|
173
|
+
const response = yield fetch(buildQuortexUrl(adApiBase, tenantName, adId), {
|
|
174
|
+
signal
|
|
175
|
+
});
|
|
176
|
+
if (!response.ok) {
|
|
177
|
+
throw new Error(`quortex creative request failed: ${response.status}`);
|
|
178
|
+
}
|
|
179
|
+
const raw = yield response.json();
|
|
180
|
+
return mapApiResponse(_objectSpread2(_objectSpread2({}, raw), {}, {
|
|
181
|
+
creativeId: raw.creativeId || adId
|
|
182
|
+
}));
|
|
183
|
+
});
|
|
184
|
+
return function resolve(_x3, _x4) {
|
|
185
|
+
return _ref2.apply(this, arguments);
|
|
186
|
+
};
|
|
187
|
+
}(),
|
|
188
|
+
fireImpression: fireDefaultImpression
|
|
189
|
+
};
|
|
190
|
+
};
|
|
191
|
+
// --- VAST provider ---------------------------------------------------------
|
|
192
|
+
const mapVastResponse = /*#__PURE__*/function () {
|
|
193
|
+
var _ref3 = _asyncToGenerator(function* (creativeId, signal) {
|
|
194
|
+
var _validAd$advertiser$f;
|
|
195
|
+
const vastClient = new VASTClient();
|
|
196
|
+
const parsedVAST = yield vastClient.get(creativeId, {
|
|
197
|
+
timeout: 8000
|
|
198
|
+
});
|
|
199
|
+
if (signal.aborted) {
|
|
200
|
+
throw new DOMException('aborted', 'AbortError');
|
|
201
|
+
}
|
|
202
|
+
const validAd = parsedVAST.ads.find(ad => ad.creatives.length > 0);
|
|
203
|
+
if (!validAd) {
|
|
204
|
+
throw new Error('no_valid_ad');
|
|
205
|
+
}
|
|
206
|
+
const advertiserValue = Array.isArray(validAd.advertiser) ? (_validAd$advertiser$f = validAd.advertiser.find(entry => !!(entry !== null && entry !== void 0 && entry.value))) === null || _validAd$advertiser$f === void 0 ? void 0 : _validAd$advertiser$f.value : undefined;
|
|
207
|
+
const advertiser = resolveAdvertiser(advertiserValue, validAd.title || undefined);
|
|
208
|
+
let iconUrl;
|
|
209
|
+
let clickThroughUrl;
|
|
210
|
+
let companion;
|
|
211
|
+
for (const creative of validAd.creatives) {
|
|
212
|
+
if (creative.type === 'linear') {
|
|
213
|
+
var _linear$icons, _linear$videoClickThr;
|
|
214
|
+
const linear = creative;
|
|
215
|
+
iconUrl = iconUrl || ((_linear$icons = linear.icons) === null || _linear$icons === void 0 || (_linear$icons = _linear$icons.find(icon => !!(icon !== null && icon !== void 0 && icon.staticResource))) === null || _linear$icons === void 0 ? void 0 : _linear$icons.staticResource) || undefined;
|
|
216
|
+
clickThroughUrl = clickThroughUrl || ((_linear$videoClickThr = linear.videoClickThroughURLTemplate) === null || _linear$videoClickThr === void 0 ? void 0 : _linear$videoClickThr.url);
|
|
217
|
+
}
|
|
218
|
+
if (creative.type === 'companion') {
|
|
219
|
+
var _creative$variations, _variation$staticReso;
|
|
220
|
+
const variation = (_creative$variations = creative.variations) === null || _creative$variations === void 0 ? void 0 : _creative$variations[0];
|
|
221
|
+
const staticResource = variation === null || variation === void 0 || (_variation$staticReso = variation.staticResources) === null || _variation$staticReso === void 0 || (_variation$staticReso = _variation$staticReso[0]) === null || _variation$staticReso === void 0 ? void 0 : _variation$staticReso.url;
|
|
222
|
+
if (staticResource && !companion) {
|
|
223
|
+
var _variation$companionC, _variation$companionC2;
|
|
224
|
+
companion = {
|
|
225
|
+
staticResource,
|
|
226
|
+
width: Number(variation === null || variation === void 0 ? void 0 : variation.width) || 0,
|
|
227
|
+
height: Number(variation === null || variation === void 0 ? void 0 : variation.height) || 0,
|
|
228
|
+
clickThrough: variation === null || variation === void 0 || (_variation$companionC = variation.companionClickThroughURLTemplate) === null || _variation$companionC === void 0 ? void 0 : _variation$companionC.url,
|
|
229
|
+
clickTracking: ((_variation$companionC2 = variation === null || variation === void 0 ? void 0 : variation.companionClickTrackingURLTemplates) !== null && _variation$companionC2 !== void 0 ? _variation$companionC2 : []).map(entry => entry === null || entry === void 0 ? void 0 : entry.url).filter(url => !!url)
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return {
|
|
235
|
+
creativeId,
|
|
236
|
+
advertiser,
|
|
237
|
+
title: validAd.title || advertiser,
|
|
238
|
+
iconUrl,
|
|
239
|
+
clickThroughUrl,
|
|
240
|
+
trackingEvents: [],
|
|
241
|
+
companion
|
|
242
|
+
};
|
|
243
|
+
});
|
|
244
|
+
return function mapVastResponse(_x5, _x6) {
|
|
245
|
+
return _ref3.apply(this, arguments);
|
|
246
|
+
};
|
|
247
|
+
}();
|
|
248
|
+
/**
|
|
249
|
+
* Maps a VAST tag into the normalized SsaiCreative shape.
|
|
250
|
+
*
|
|
251
|
+
* Note: `fireImpression` is intentionally left to the shared default for now.
|
|
252
|
+
* Proper VAST impression/tracking wiring through `VASTTracker` is a documented
|
|
253
|
+
* follow-up; it requires retaining the parsed ad/tracker per creative.
|
|
254
|
+
*/
|
|
255
|
+
const createVastProvider = (_config = {}) => {
|
|
256
|
+
return {
|
|
257
|
+
id: 'vast',
|
|
258
|
+
resolve: mapVastResponse
|
|
259
|
+
};
|
|
260
|
+
};
|
|
261
|
+
/** Built-in provider factories keyed by name. */
|
|
262
|
+
const BUILTIN_PROVIDERS = {
|
|
263
|
+
streamlayer: config => createStreamLayerProvider(config),
|
|
264
|
+
quortex: config => createQuortexProvider(config),
|
|
265
|
+
vast: () => createVastProvider()
|
|
266
|
+
};
|
|
267
|
+
const isProviderInstance = option => {
|
|
268
|
+
return typeof option === 'object' && typeof option.resolve === 'function';
|
|
269
|
+
};
|
|
270
|
+
/**
|
|
271
|
+
* Resolve a {@link SsaiProviderOption} into a concrete {@link SsaiCreativeProvider}.
|
|
272
|
+
* Defaults to the StreamLayer provider (using `fallbackConfig`) when no option is
|
|
273
|
+
* supplied.
|
|
274
|
+
*/
|
|
275
|
+
const resolveProvider = (option, fallbackConfig) => {
|
|
276
|
+
var _option$config;
|
|
277
|
+
if (!option) {
|
|
278
|
+
return createStreamLayerProvider(fallbackConfig);
|
|
279
|
+
}
|
|
280
|
+
if (typeof option === 'string') {
|
|
281
|
+
return BUILTIN_PROVIDERS[option](fallbackConfig);
|
|
282
|
+
}
|
|
283
|
+
if (isProviderInstance(option)) {
|
|
284
|
+
return option;
|
|
285
|
+
}
|
|
286
|
+
return BUILTIN_PROVIDERS[option.name]((_option$config = option.config) !== null && _option$config !== void 0 ? _option$config : fallbackConfig);
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
export { BUILTIN_PROVIDERS, DEFAULT_CREATIVE_API_BASE, createQuortexProvider, createStreamLayerProvider, createVastProvider, fireBeacon, fireDefaultImpression, fireTrackingEvents, resolveProvider };
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { VASTTracker } from '@dailymotion/vast-client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* One place that constructs a `VASTTracker`.
|
|
5
|
+
*
|
|
6
|
+
* Every StreamLayer surface that reports VAST beacons — the GAM video player, both pause-ad
|
|
7
|
+
* renderers and the Standard Ad sidebar — needs the same `(ad, creative, variation)` triple, and
|
|
8
|
+
* each was assembling it by hand behind its own `@ts-ignore`. The parsed shapes those triples
|
|
9
|
+
* come from (`AdData` / `AdCompanion` / `AdNonLinear`) are defined in this package, so the
|
|
10
|
+
* mapping belongs here too.
|
|
11
|
+
*
|
|
12
|
+
* Deliberately *only* construction: when a beacon may fire, and how a surface de-duplicates its
|
|
13
|
+
* impression, stays with that surface. Those policies differ on purpose (per-ad-id in module
|
|
14
|
+
* scope for the sidebar, per component instance for the pause ads, per tracker identity for the
|
|
15
|
+
* player) and they are billing-visible, so collapsing them here would silently change counts.
|
|
16
|
+
*/
|
|
17
|
+
// eslint-disable-next-line @typescript-eslint/triple-slash-reference
|
|
18
|
+
/// <reference path="../module.d.ts" />
|
|
19
|
+
const createVastTracker = ({
|
|
20
|
+
ad,
|
|
21
|
+
creative,
|
|
22
|
+
variation
|
|
23
|
+
}) =>
|
|
24
|
+
// The first argument is the `VASTClient` the tracker may update; every surface here drives
|
|
25
|
+
// playback itself and only wants the beacons, so it is always `null`.
|
|
26
|
+
//
|
|
27
|
+
// The type shim declares the 3rd argument as `VastCreativeLinear` and the 4th as another
|
|
28
|
+
// *creative*. At runtime `vast-client` accepts any creative in the 3rd and expects the
|
|
29
|
+
// `<Companion>` / `<NonLinear>` *variation* in the 4th. The casts encode that runtime contract
|
|
30
|
+
// once here rather than at every call site.
|
|
31
|
+
new VASTTracker(null, ad, creative, variation);
|
|
32
|
+
/**
|
|
33
|
+
* A tracker for a single `<Companion>`. A `<CompanionAds>` creative has no creative-level
|
|
34
|
+
* `<TrackingEvents>` — each variation owns its own `creativeView` — so a companion is reported
|
|
35
|
+
* through its own tracker. `undefined` when the companion was parsed without them (nothing to
|
|
36
|
+
* report against).
|
|
37
|
+
*/
|
|
38
|
+
const createCompanionTracker = (ad, companion) => companion.creative && companion.variation ? createVastTracker({
|
|
39
|
+
ad,
|
|
40
|
+
creative: companion.creative,
|
|
41
|
+
variation: companion.variation
|
|
42
|
+
}) : undefined;
|
|
43
|
+
/**
|
|
44
|
+
* The creative a parsed ad reports its impression / playback beacons through, in the order the
|
|
45
|
+
* shells draw them: the `<Linear>` creative first, then the `<NonLinear>` still, then — for a tag
|
|
46
|
+
* whose only creative is `<CompanionAds>` — the companion that was actually drawn.
|
|
47
|
+
*
|
|
48
|
+
* `drawnCompanion` is a parameter rather than `adData.companion` because only the rendering shell
|
|
49
|
+
* knows which companion reached the screen, and reporting one that did not is a false view.
|
|
50
|
+
*/
|
|
51
|
+
const resolveVastTrackerTarget = (adData, drawnCompanion) => {
|
|
52
|
+
// `AdData.creative` is not optional in the type but `getCreative` can return nothing: a
|
|
53
|
+
// programmatic tag does not have to carry a `<Linear>` creative.
|
|
54
|
+
if (adData.creative) {
|
|
55
|
+
return {
|
|
56
|
+
ad: adData.ad,
|
|
57
|
+
creative: adData.creative
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
if (adData.nonLinear) {
|
|
61
|
+
return {
|
|
62
|
+
ad: adData.ad,
|
|
63
|
+
creative: adData.nonLinear.creative,
|
|
64
|
+
variation: adData.nonLinear.variation
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
if (drawnCompanion !== null && drawnCompanion !== void 0 && drawnCompanion.creative && drawnCompanion.variation) {
|
|
68
|
+
return {
|
|
69
|
+
ad: adData.ad,
|
|
70
|
+
creative: drawnCompanion.creative,
|
|
71
|
+
variation: drawnCompanion.variation
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
return undefined;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export { createCompanionTracker, createVastTracker, resolveVastTrackerTarget };
|
package/lib/index.d.ts
CHANGED
|
@@ -6,7 +6,8 @@ declare module '@streamlayer/sdk-web-interfaces' {
|
|
|
6
6
|
}
|
|
7
7
|
export { type ExternalAdStore, type ExternalPauseAdStore, type SsaiMonitorOptions } from './advertisement';
|
|
8
8
|
export { type GAMStaticCore, type PauseAdTemplate, isFullbleedVideoTemplate } from './units/external-pause-ad';
|
|
9
|
-
export { type PauseAdCard, type
|
|
9
|
+
export { INSIGHT_BODY_MAX_LENGTH, type PauseAdCard, type PauseAdKind, type PauseAdMediaMode, } from './units/pause-ad-card';
|
|
10
|
+
export { type PauseAdStore } from './units/pause-ad';
|
|
10
11
|
export { type AdStore, type AdData, type AdCompanion, type AdNonLinear } from './units/external-ad';
|
|
11
12
|
export { gam, type GamNonceResult, type PalNonceManager } from './units/gam';
|
|
12
13
|
export { createCompanionTracker, createVastTracker, resolveVastTrackerTarget, type VastTrackerTarget, } from './units/vast-tracker';
|
package/lib/index.js
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { Advertisement } from './advertisement';
|
|
2
2
|
export { isFullbleedVideoTemplate } from './units/external-pause-ad';
|
|
3
|
+
// Card shapes come straight from the pure module rather than through `units/pause-ad`. NOT a bundle
|
|
4
|
+
// guarantee for barrel consumers — this file value-imports `Advertisement` on line 3, which imports
|
|
5
|
+
// the store module anyway, and the package declares no `sideEffects: false`. What it does buy: the
|
|
6
|
+
// unit spec and any deep importer get the card shapes without the store, and dropping the
|
|
7
|
+
// `export *` keeps `units/pause-ad` from silently re-exporting `parsePauseAdCards`.
|
|
8
|
+
export { INSIGHT_BODY_MAX_LENGTH, } from './units/pause-ad-card';
|
|
3
9
|
export { gam } from './units/gam';
|
|
4
10
|
export { createCompanionTracker, createVastTracker, resolveVastTrackerTarget, } from './units/vast-tracker';
|
|
5
11
|
export { DEFAULT_CREATIVE_API_BASE, } from './units/ssai-creative';
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { type Question } from '@streamlayer/sl-eslib/interactive/interactive.common_pb';
|
|
2
|
+
export type PauseAdLayout = 'left' | 'center' | 'image';
|
|
3
|
+
/**
|
|
4
|
+
* Which full-bleed renderer draws the card. Absent === `'standard'`.
|
|
5
|
+
*
|
|
6
|
+
* - `'standard'` — the Standard/Image pause ad (promotion card).
|
|
7
|
+
* - `'insight'` — the Insight Pause Ad (factoid card): sponsor logo, heading and a
|
|
8
|
+
* single-column body beside an optional media image (SC-353).
|
|
9
|
+
*/
|
|
10
|
+
export type PauseAdKind = 'standard' | 'insight';
|
|
11
|
+
/** How the Insight media image is cropped — mirrors Studio's Media switcher. */
|
|
12
|
+
export type PauseAdMediaMode = 'image' | 'circle' | 'none';
|
|
13
|
+
/**
|
|
14
|
+
* Insight body cap (SC-353). Studio enforces it on the Additional Content field; the
|
|
15
|
+
* SDK clamps as well so a card authored before the limit landed cannot overflow the
|
|
16
|
+
* screen — the format is explicitly designed to fit without scrolling.
|
|
17
|
+
*/
|
|
18
|
+
export declare const INSIGHT_BODY_MAX_LENGTH = 600;
|
|
19
|
+
export type PauseAdCard = {
|
|
20
|
+
id: string;
|
|
21
|
+
parentId: string;
|
|
22
|
+
/** Optional: a video-only card (programmatic VAST, SC-322) has no image. */
|
|
23
|
+
backgroundImageSrc?: string;
|
|
24
|
+
/** Linear MP4 from VAST → renders full-bleed `<video>` instead of the image (SC-322). */
|
|
25
|
+
backgroundVideoSrc?: string;
|
|
26
|
+
layout: PauseAdLayout;
|
|
27
|
+
isInteractive: boolean;
|
|
28
|
+
logoSrc?: string;
|
|
29
|
+
title?: string;
|
|
30
|
+
description?: string;
|
|
31
|
+
ctaLabel?: string;
|
|
32
|
+
ctaUrl?: string;
|
|
33
|
+
qrCodeUrl?: string;
|
|
34
|
+
qrCodeLabel?: string;
|
|
35
|
+
/** Absent === `'standard'`. */
|
|
36
|
+
kind?: PauseAdKind;
|
|
37
|
+
/** Insight only: the Media image (`FactoidOptions.image`). */
|
|
38
|
+
mediaSrc?: string;
|
|
39
|
+
/** Insight only: how `mediaSrc` is cropped. */
|
|
40
|
+
mediaMode?: PauseAdMediaMode;
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* The `pauseAdList` response, shaped for the renderers: keep the cards the SDK has chrome for,
|
|
44
|
+
* turn each into a `PauseAdCard`, drop the ones that cannot be drawn.
|
|
45
|
+
*
|
|
46
|
+
* Pure on purpose — `pause-ad.ts` owns the store and the transport, this owns the card shapes,
|
|
47
|
+
* and only this half needs unit tests.
|
|
48
|
+
*/
|
|
49
|
+
export declare const parsePauseAdCards: (questions: Question[]) => PauseAdCard[];
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { CardFormat, FactoidMediaMode, NotificationSponsorLogoMode, PauseAdLayout as PauseAdLayoutProto, PromotionButtonType, PromotionFormat, QRCode, UseGeneralLogo, UserAccountInfo, } from '@streamlayer/sl-eslib/interactive/interactive.common_pb';
|
|
2
|
+
/**
|
|
3
|
+
* Insight body cap (SC-353). Studio enforces it on the Additional Content field; the
|
|
4
|
+
* SDK clamps as well so a card authored before the limit landed cannot overflow the
|
|
5
|
+
* screen — the format is explicitly designed to fit without scrolling.
|
|
6
|
+
*/
|
|
7
|
+
export const INSIGHT_BODY_MAX_LENGTH = 600;
|
|
8
|
+
/**
|
|
9
|
+
* Sponsor logo for a factoid card. Same General-tab opt-in as the promotion path below,
|
|
10
|
+
* but the logo lives on `question.promotion.sponsor` (a factoid card carries no
|
|
11
|
+
* `PromotionOptions`) and Studio writes the uploaded file to `logo`, not `centerLogo`.
|
|
12
|
+
*
|
|
13
|
+
* `logo` therefore WINS over `centerLogo`: the latter is an optional field carried over from the
|
|
14
|
+
* notification/header surfaces, so a card that kept a stale `centerLogo` from an earlier revision
|
|
15
|
+
* would otherwise render it instead of the Insight logo the author just uploaded.
|
|
16
|
+
*/
|
|
17
|
+
const resolveInsightSponsorLogo = (question) => {
|
|
18
|
+
const sponsor = question.promotion?.sponsor;
|
|
19
|
+
if (sponsor?.logoPosition === NotificationSponsorLogoMode.NOTIFICATION_SPONSOR_LOGO_NONE) {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
return sponsor?.useGeneralLogo === UseGeneralLogo.ENABLED
|
|
23
|
+
? question.promotion?.generalTabMetadata?.advertiserTransparentLogo || undefined
|
|
24
|
+
: sponsor?.logo || sponsor?.centerLogo || undefined;
|
|
25
|
+
};
|
|
26
|
+
const MEDIA_MODE_BY_PROTO = {
|
|
27
|
+
[FactoidMediaMode.IMAGE]: 'image',
|
|
28
|
+
[FactoidMediaMode.CIRCLE]: 'circle',
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Insight Pause Ad, Full Bleed (SC-353). A factoid card, so none of the promotion
|
|
32
|
+
* plumbing above applies: the heading is the question subject, the body is
|
|
33
|
+
* `FactoidOptions.body`, and the sponsor logo comes off `question.promotion.sponsor`.
|
|
34
|
+
*
|
|
35
|
+
* `FACTOID_MEDIA_MODE_VIDEO` is deliberately treated as "no media": the full-bleed
|
|
36
|
+
* format has no player chrome and no way to start playback, unlike the sidebar variant.
|
|
37
|
+
*/
|
|
38
|
+
const parseInsightPauseAdCard = (question) => {
|
|
39
|
+
const factoidOptions = question.options?.options.case === 'factoid' ? question.options.options.value : undefined;
|
|
40
|
+
if (!factoidOptions) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
const mediaMode = MEDIA_MODE_BY_PROTO[factoidOptions.mode] ?? 'none';
|
|
44
|
+
const body = factoidOptions.body.slice(0, INSIGHT_BODY_MAX_LENGTH);
|
|
45
|
+
return {
|
|
46
|
+
id: question.id,
|
|
47
|
+
parentId: question.eventId || question.id,
|
|
48
|
+
// `landscapeUrl` ONLY. `portraitUrl` keeps stale artwork from earlier revisions of the
|
|
49
|
+
// card that Studio no longer shows in its Background Image slot, so falling back to it
|
|
50
|
+
// paints a background the author has already removed. Unlike the standard card, an
|
|
51
|
+
// Insight one renders fine with no background at all — its copy is the ad.
|
|
52
|
+
backgroundImageSrc: question.backgroundImage?.landscapeUrl || undefined,
|
|
53
|
+
kind: 'insight',
|
|
54
|
+
layout: 'left',
|
|
55
|
+
isInteractive: false,
|
|
56
|
+
logoSrc: resolveInsightSponsorLogo(question),
|
|
57
|
+
title: question.subject || undefined,
|
|
58
|
+
description: body || undefined,
|
|
59
|
+
mediaSrc: mediaMode === 'none' ? undefined : factoidOptions.image || undefined,
|
|
60
|
+
mediaMode,
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
const parsePauseAdCard = (question) => {
|
|
64
|
+
if (question.options?.options.case === 'factoid') {
|
|
65
|
+
return parseInsightPauseAdCard(question);
|
|
66
|
+
}
|
|
67
|
+
const backgroundImageSrc = question.backgroundImage?.landscapeUrl;
|
|
68
|
+
if (!backgroundImageSrc) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
const promotionOptions = question.options?.options.case === 'promotion' ? question.options.options.value : undefined;
|
|
72
|
+
const pauseAdOptions = promotionOptions?.pauseAdOptions;
|
|
73
|
+
const optionsSponsor = promotionOptions?.sponsor;
|
|
74
|
+
const layoutProto = pauseAdOptions?.layout ?? PauseAdLayoutProto.UNSET;
|
|
75
|
+
const layout = layoutProto === PauseAdLayoutProto.CENTER ? 'center' : layoutProto === PauseAdLayoutProto.IMAGE ? 'image' : 'left';
|
|
76
|
+
const isInteractive = promotionOptions?.buttonType === PromotionButtonType.SEND_TO_USER;
|
|
77
|
+
const hasQr = pauseAdOptions?.qrCode === QRCode.QR_CODE_ENABLED && !!pauseAdOptions?.qrCodeUrl;
|
|
78
|
+
let ctaLabel;
|
|
79
|
+
let ctaUrl;
|
|
80
|
+
if (isInteractive && promotionOptions?.buttonOptions.case === 'sendToUserOptions') {
|
|
81
|
+
const accountInfo = promotionOptions.buttonOptions.value.userAccountInfo;
|
|
82
|
+
const isPhone = accountInfo === UserAccountInfo.PHONE;
|
|
83
|
+
ctaUrl = isPhone ? 'sendToPhone:' : 'sendToEmail:';
|
|
84
|
+
ctaLabel = isPhone ? 'Send info to account phone' : 'Send info to account email';
|
|
85
|
+
}
|
|
86
|
+
const logoSrc = optionsSponsor?.logoPosition === NotificationSponsorLogoMode.NOTIFICATION_SPONSOR_LOGO_NONE
|
|
87
|
+
? undefined
|
|
88
|
+
: optionsSponsor?.useGeneralLogo === UseGeneralLogo.ENABLED
|
|
89
|
+
? question.promotion?.generalTabMetadata?.advertiserTransparentLogo || undefined
|
|
90
|
+
: optionsSponsor?.centerLogo || undefined;
|
|
91
|
+
return {
|
|
92
|
+
id: question.id,
|
|
93
|
+
parentId: question.eventId || question.id,
|
|
94
|
+
backgroundImageSrc,
|
|
95
|
+
layout,
|
|
96
|
+
logoSrc,
|
|
97
|
+
title: promotionOptions?.banner?.title || undefined,
|
|
98
|
+
description: promotionOptions?.banner?.body || undefined,
|
|
99
|
+
isInteractive,
|
|
100
|
+
ctaLabel: ctaLabel || undefined,
|
|
101
|
+
ctaUrl: ctaUrl || undefined,
|
|
102
|
+
qrCodeUrl: hasQr ? (pauseAdOptions?.qrCodeUrl ?? undefined) : undefined,
|
|
103
|
+
qrCodeLabel: hasQr ? (pauseAdOptions?.qrCodeLabel ?? undefined) : undefined,
|
|
104
|
+
};
|
|
105
|
+
};
|
|
106
|
+
/**
|
|
107
|
+
* Which cards from `pauseAdList` the SDK can actually draw. Both renderers are full bleed; a
|
|
108
|
+
* Sidebar-format pause ad has no chrome yet and is skipped.
|
|
109
|
+
*/
|
|
110
|
+
const isFullBleedPauseAd = (question) => {
|
|
111
|
+
if (question.options?.options.case === 'factoid') {
|
|
112
|
+
// Insight Pause Ad (SC-353). Studio does not write `CardFormat` on these cards yet
|
|
113
|
+
// (SLW-7062), so an UNSET format is taken as Full Bleed — the only Insight pause-ad chrome
|
|
114
|
+
// the SDK has. An explicit Sidebar stays out. Drop the UNSET arm once Studio sets the field.
|
|
115
|
+
return question.format === CardFormat.FULL_BLEED || question.format === CardFormat.UNSET;
|
|
116
|
+
}
|
|
117
|
+
const promotionOptions = question.options?.options.case === 'promotion' ? question.options.options.value : undefined;
|
|
118
|
+
return promotionOptions?.format === PromotionFormat.FULL_BLEED || question.format === CardFormat.FULL_BLEED;
|
|
119
|
+
};
|
|
120
|
+
/**
|
|
121
|
+
* The `pauseAdList` response, shaped for the renderers: keep the cards the SDK has chrome for,
|
|
122
|
+
* turn each into a `PauseAdCard`, drop the ones that cannot be drawn.
|
|
123
|
+
*
|
|
124
|
+
* Pure on purpose — `pause-ad.ts` owns the store and the transport, this owns the card shapes,
|
|
125
|
+
* and only this half needs unit tests.
|
|
126
|
+
*/
|
|
127
|
+
export const parsePauseAdCards = (questions) => questions
|
|
128
|
+
.filter(isFullBleedPauseAd)
|
|
129
|
+
.map(parsePauseAdCard)
|
|
130
|
+
.filter((card) => card !== null);
|
package/lib/units/pause-ad.d.ts
CHANGED
|
@@ -1,24 +1,7 @@
|
|
|
1
1
|
import { createSingleStore, StreamLayerContext } from '@streamlayer/sdk-web-interfaces';
|
|
2
2
|
import '@streamlayer/sdk-web-api';
|
|
3
3
|
import '@streamlayer/sdk-web-core/store';
|
|
4
|
-
|
|
5
|
-
export type PauseAdCard = {
|
|
6
|
-
id: string;
|
|
7
|
-
parentId: string;
|
|
8
|
-
/** Optional: a video-only card (programmatic VAST, SC-322) has no image. */
|
|
9
|
-
backgroundImageSrc?: string;
|
|
10
|
-
/** Linear MP4 from VAST → renders full-bleed `<video>` instead of the image (SC-322). */
|
|
11
|
-
backgroundVideoSrc?: string;
|
|
12
|
-
layout: PauseAdLayout;
|
|
13
|
-
isInteractive: boolean;
|
|
14
|
-
logoSrc?: string;
|
|
15
|
-
title?: string;
|
|
16
|
-
description?: string;
|
|
17
|
-
ctaLabel?: string;
|
|
18
|
-
ctaUrl?: string;
|
|
19
|
-
qrCodeUrl?: string;
|
|
20
|
-
qrCodeLabel?: string;
|
|
21
|
-
};
|
|
4
|
+
import { type PauseAdCard } from './pause-ad-card';
|
|
22
5
|
export type PauseAdListOptions = {
|
|
23
6
|
refetchInterval: number;
|
|
24
7
|
$enabled: boolean;
|