@streamlayer/feature-advertisement 0.3.2 → 0.4.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,15 +1,19 @@
1
1
  import { AbstractFeatureUnit, StreamLayerContext } from '@streamlayer/sdk-web-interfaces';
2
2
  import { externalAd, type ExternalAdOptions } from './units/external-ad';
3
3
  import { externalPauseAd, type ExternalPauseAdOptions } from './units/external-pause-ad';
4
+ import { type PauseAdListOptions, type PauseAdStore } from './units/pause-ad';
4
5
  export type ExternalAdStore = ReturnType<ReturnType<typeof externalAd>>;
5
6
  export type ExternalPauseAdStore = ReturnType<ReturnType<typeof externalPauseAd>>;
6
7
  export declare class Advertisement extends AbstractFeatureUnit {
7
8
  getExternalAd: (adUrl: string, options: ExternalAdOptions) => [ExternalAdStore, string];
8
9
  getExternalPauseAd: (adUrl: string, options: ExternalPauseAdOptions) => [ExternalPauseAdStore, string];
10
+ getPauseAdList: (options: PauseAdListOptions) => PauseAdStore;
9
11
  private externalAdStores;
10
12
  private externalAdEnabled;
11
13
  private externalPauseAdStores;
12
14
  private externalPauseAdEnabled;
15
+ private pauseAdStore;
16
+ private pauseAdEnabled;
13
17
  private log;
14
18
  constructor(instance: StreamLayerContext);
15
19
  connect: () => void;
@@ -2,19 +2,42 @@ import { AbstractFeatureUnit, createSingleStore } from '@streamlayer/sdk-web-int
2
2
  import { createLogger } from '@streamlayer/sdk-web-logger';
3
3
  import { externalAd } from './units/external-ad';
4
4
  import { externalPauseAd } from './units/external-pause-ad';
5
+ import { pauseAdListStore } from './units/pause-ad';
5
6
  export class Advertisement extends AbstractFeatureUnit {
6
7
  getExternalAd;
7
8
  getExternalPauseAd;
9
+ getPauseAdList;
8
10
  externalAdStores = new Map();
9
11
  externalAdEnabled = new Map();
10
12
  externalPauseAdStores = new Map();
11
13
  externalPauseAdEnabled = new Map();
14
+ pauseAdStore = null;
15
+ pauseAdEnabled = null;
12
16
  log;
13
17
  constructor(instance) {
14
18
  super();
15
19
  this.log = createLogger('advertisement');
16
20
  const externalAdGenerator = externalAd(instance);
17
21
  const externalPauseAdGenerator = externalPauseAd(instance);
22
+ const pauseAdGenerator = pauseAdListStore(instance);
23
+ this.getPauseAdList = (options) => {
24
+ if (this.pauseAdStore) {
25
+ const $enabled = this.pauseAdEnabled;
26
+ const wasDisabled = $enabled.get() === false;
27
+ $enabled.set(options.$enabled);
28
+ if (wasDisabled && options.$enabled) {
29
+ window.requestAnimationFrame(() => {
30
+ if (this.pauseAdStore)
31
+ this.pauseAdStore.$list.revalidate();
32
+ });
33
+ }
34
+ return this.pauseAdStore;
35
+ }
36
+ const $enabled = createSingleStore(options.$enabled);
37
+ this.pauseAdEnabled = $enabled;
38
+ this.pauseAdStore = pauseAdGenerator($enabled, options);
39
+ return this.pauseAdStore;
40
+ };
18
41
  this.getExternalAd = (adUrl, options) => {
19
42
  if (this.externalAdStores.has(adUrl)) {
20
43
  const $enabled = this.externalAdEnabled.get(adUrl);
@@ -75,5 +98,12 @@ export class Advertisement extends AbstractFeatureUnit {
75
98
  });
76
99
  this.externalPauseAdEnabled.clear();
77
100
  this.externalPauseAdStores.clear();
101
+ if (this.pauseAdStore) {
102
+ this.pauseAdStore.$list.off();
103
+ this.pauseAdStore.$index.off();
104
+ this.pauseAdEnabled?.off();
105
+ this.pauseAdStore = null;
106
+ this.pauseAdEnabled = null;
107
+ }
78
108
  };
79
109
  }
package/lib/index.d.ts CHANGED
@@ -7,4 +7,5 @@ declare module '@streamlayer/sdk-web-interfaces' {
7
7
  export { type ExternalAdStore, type ExternalPauseAdStore } from './advertisement';
8
8
  export { type GAMStaticAdCore } from './units/external-ad';
9
9
  export { type GAMStaticCore } from './units/external-pause-ad';
10
+ export { type PauseAdCard, type PauseAdStore } from './units/pause-ad';
10
11
  export declare const advertisement: (instance: StreamLayerContext, _opts: unknown, done: () => void) => void;
@@ -6,6 +6,9 @@ export type GAMStaticCore = {
6
6
  url: string;
7
7
  imageSrc?: string;
8
8
  adUrl?: string;
9
+ ad?: any;
10
+ creative?: any;
11
+ variation?: any;
9
12
  };
10
13
  export type ExternalPauseAdOptions = {
11
14
  refetchInterval: number;
@@ -36,10 +36,9 @@ export const externalPauseAd = (instance) => {
36
36
  log.debug(nonlinear, 'nonlinear');
37
37
  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
38
38
  // @ts-ignore
39
- const staticResource = nonlinear.variations?.[0]?.staticResource;
40
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
41
- // @ts-ignore
42
- const adUrlTemplate = nonlinear.variations?.[0]?.nonlinearClickThroughURLTemplate;
39
+ const variation = nonlinear.variations?.[0];
40
+ const staticResource = variation?.staticResource;
41
+ const adUrlTemplate = variation?.nonlinearClickThroughURLTemplate;
43
42
  if (staticResource) {
44
43
  await preloadImage(staticResource);
45
44
  eventBus.emit('exposedPauseAd', {
@@ -55,6 +54,9 @@ export const externalPauseAd = (instance) => {
55
54
  url: adUrl,
56
55
  imageSrc: staticResource,
57
56
  adUrl: adUrlTemplate,
57
+ ad: validAd,
58
+ creative: nonlinear,
59
+ variation,
58
60
  };
59
61
  }
60
62
  }
@@ -62,11 +64,12 @@ export const externalPauseAd = (instance) => {
62
64
  log.debug(companion, 'companion');
63
65
  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
64
66
  // @ts-ignore
65
- const staticResource = companion?.variations?.[0]?.staticResources?.[0]?.url;
66
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
67
- // @ts-ignore
68
- const adUrlTemplate = companion?.variations?.[0]?.companionClickThroughURLTemplate;
69
- if (staticResource) {
67
+ const variation = companion?.variations?.[0];
68
+ const staticResource = variation?.staticResources?.[0]?.url;
69
+ const adUrlTemplate = variation?.companionClickThroughURLTemplate;
70
+ // `&& companion` is for TS narrowing only (companion.adId access below);
71
+ // it is runtime-redundant since staticResource derives from companion.
72
+ if (staticResource && companion) {
70
73
  const id = companion.adId || companion.id || parentId;
71
74
  await preloadImage(staticResource);
72
75
  eventBus.emit('exposedPauseAd', {
@@ -82,6 +85,9 @@ export const externalPauseAd = (instance) => {
82
85
  url: adUrl,
83
86
  imageSrc: staticResource,
84
87
  adUrl: adUrlTemplate,
88
+ ad: validAd,
89
+ creative: companion,
90
+ variation,
85
91
  };
86
92
  }
87
93
  }
@@ -0,0 +1,30 @@
1
+ import { createSingleStore, StreamLayerContext } from '@streamlayer/sdk-web-interfaces';
2
+ import '@streamlayer/sdk-web-api';
3
+ import '@streamlayer/sdk-web-core/store';
4
+ export type PauseAdLayout = 'left' | 'center' | 'image';
5
+ export type PauseAdCard = {
6
+ id: string;
7
+ parentId: string;
8
+ backgroundImageSrc: string;
9
+ layout: PauseAdLayout;
10
+ isInteractive: boolean;
11
+ logoSrc?: string;
12
+ title?: string;
13
+ description?: string;
14
+ ctaLabel?: string;
15
+ ctaUrl?: string;
16
+ qrCodeUrl?: string;
17
+ qrCodeLabel?: string;
18
+ };
19
+ export type PauseAdListOptions = {
20
+ refetchInterval: number;
21
+ $enabled: boolean;
22
+ };
23
+ export declare const pauseAdListStore: (instance: StreamLayerContext) => ($enabled: ReturnType<typeof createSingleStore<boolean>>, options: PauseAdListOptions) => {
24
+ $list: import("@nanostores/query").FetcherStore<PauseAdCard[], any>;
25
+ $index: import("nanostores").PreinitializedWritableAtom<number> & object;
26
+ next: () => void;
27
+ reset: () => void;
28
+ getCurrent: () => PauseAdCard | undefined;
29
+ };
30
+ export type PauseAdStore = ReturnType<ReturnType<typeof pauseAdListStore>>;
@@ -0,0 +1,94 @@
1
+ import { createLogger } from '@streamlayer/sdk-web-logger';
2
+ import { atom } from 'nanostores';
3
+ import '@streamlayer/sdk-web-api';
4
+ import '@streamlayer/sdk-web-core/store';
5
+ import { CardFormat, NotificationSponsorLogoMode, PauseAdLayout as PauseAdLayoutProto, PromotionButtonType, PromotionFormat, QRCode, UseGeneralLogo, UserAccountInfo, } from '@streamlayer/sl-eslib/interactive/interactive.common_pb';
6
+ import { Feed } from '@streamlayer/sl-eslib/interactive/feed/interactive.feed_pb';
7
+ const log = createLogger('advertisement:pause-ad');
8
+ const parsePauseAdCard = (question) => {
9
+ const backgroundImageSrc = question.backgroundImage?.landscapeUrl;
10
+ if (!backgroundImageSrc) {
11
+ return null;
12
+ }
13
+ const promotionOptions = question.options?.options.case === 'promotion' ? question.options.options.value : undefined;
14
+ const pauseAdOptions = promotionOptions?.pauseAdOptions;
15
+ const optionsSponsor = promotionOptions?.sponsor;
16
+ const layoutProto = pauseAdOptions?.layout ?? PauseAdLayoutProto.UNSET;
17
+ const layout = layoutProto === PauseAdLayoutProto.CENTER ? 'center' : layoutProto === PauseAdLayoutProto.IMAGE ? 'image' : 'left';
18
+ const isInteractive = promotionOptions?.buttonType === PromotionButtonType.SEND_TO_USER;
19
+ const hasQr = pauseAdOptions?.qrCode === QRCode.QR_CODE_ENABLED && !!pauseAdOptions?.qrCodeUrl;
20
+ let ctaLabel;
21
+ let ctaUrl;
22
+ if (isInteractive && promotionOptions?.buttonOptions.case === 'sendToUserOptions') {
23
+ const accountInfo = promotionOptions.buttonOptions.value.userAccountInfo;
24
+ const isPhone = accountInfo === UserAccountInfo.PHONE;
25
+ ctaUrl = isPhone ? 'sendToPhone:' : 'sendToEmail:';
26
+ ctaLabel = isPhone ? 'Send info to account phone' : 'Send info to account email';
27
+ }
28
+ const logoSrc = optionsSponsor?.logoPosition === NotificationSponsorLogoMode.NOTIFICATION_SPONSOR_LOGO_NONE
29
+ ? undefined
30
+ : optionsSponsor?.useGeneralLogo === UseGeneralLogo.ENABLED
31
+ ? question.promotion?.generalTabMetadata?.advertiserTransparentLogo || undefined
32
+ : optionsSponsor?.centerLogo || undefined;
33
+ return {
34
+ id: question.id,
35
+ parentId: question.eventId || question.id,
36
+ backgroundImageSrc,
37
+ layout,
38
+ logoSrc,
39
+ title: promotionOptions?.banner?.title || undefined,
40
+ description: promotionOptions?.banner?.body || undefined,
41
+ isInteractive,
42
+ ctaLabel: ctaLabel || undefined,
43
+ ctaUrl: ctaUrl || undefined,
44
+ qrCodeUrl: hasQr ? (pauseAdOptions?.qrCodeUrl ?? undefined) : undefined,
45
+ qrCodeLabel: hasQr ? (pauseAdOptions?.qrCodeLabel ?? undefined) : undefined,
46
+ };
47
+ };
48
+ export const pauseAdListStore = (instance) => {
49
+ const $slStreamId = instance.stores.slStreamId.getAtomStore();
50
+ return ($enabled, options) => {
51
+ const { client, queryKey } = instance.transport.createPromiseClient(Feed, {
52
+ method: 'pauseAdList',
53
+ params: [$slStreamId, $enabled],
54
+ });
55
+ const $list = instance.transport.nanoquery.createFetcherStore(queryKey, {
56
+ dedupeTime: 0,
57
+ revalidateInterval: options.refetchInterval > 0 ? options.refetchInterval : 0,
58
+ fetcher: async (_, __, eventId, isEnabled) => {
59
+ if (!eventId || typeof eventId !== 'string' || !isEnabled) {
60
+ return [];
61
+ }
62
+ log.debug({ eventId }, 'fetching pause ad list');
63
+ const res = await client.pauseAdList({ eventId });
64
+ const questions = res.data ?? [];
65
+ const cards = questions
66
+ .filter((q) => {
67
+ const opts = q.options?.options.case === 'promotion' ? q.options.options.value : undefined;
68
+ return opts?.format === PromotionFormat.FULL_BLEED || q.format === CardFormat.FULL_BLEED;
69
+ })
70
+ .map((q) => parsePauseAdCard(q))
71
+ .filter((c) => c !== null);
72
+ log.debug({ count: cards.length }, 'parsed %d fullbleed pause ad cards');
73
+ return cards;
74
+ },
75
+ });
76
+ const $index = atom(0);
77
+ const next = () => {
78
+ const list = $list.get().data;
79
+ if (!list?.length)
80
+ return;
81
+ $index.set(($index.get() + 1) % list.length);
82
+ };
83
+ const reset = () => {
84
+ $index.set(0);
85
+ };
86
+ const getCurrent = () => {
87
+ const list = $list.get().data;
88
+ if (!list?.length)
89
+ return undefined;
90
+ return list[$index.get()];
91
+ };
92
+ return { $list, $index, next, reset, getCurrent };
93
+ };
94
+ };
package/package.json CHANGED
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "name": "@streamlayer/feature-advertisement",
3
- "version": "0.3.2",
3
+ "version": "0.4.0",
4
4
  "peerDependencies": {
5
5
  "@dailymotion/vast-client": "^6.4.2",
6
6
  "@streamlayer/sl-eslib": "^5.229.1",
7
7
  "nanostores": "^1.1.0",
8
- "@streamlayer/sdk-web-core": "^1.22.2",
9
- "@streamlayer/sdk-web-api": "^1.17.2",
10
- "@streamlayer/sdk-web-interfaces": "^1.10.2",
11
- "@streamlayer/sdk-web-types": "^1.18.2",
12
- "@streamlayer/sdk-web-logger": "^1.1.2"
8
+ "@streamlayer/sdk-web-core": "^1.22.3",
9
+ "@streamlayer/sdk-web-api": "^1.17.3",
10
+ "@streamlayer/sdk-web-interfaces": "^1.10.3",
11
+ "@streamlayer/sdk-web-types": "^1.18.3",
12
+ "@streamlayer/sdk-web-logger": "^1.1.3"
13
13
  },
14
14
  "devDependencies": {
15
15
  "@types/vast-client": "^3.0.4"