@vouchfor/embeds 0.0.0-experiment.ace6cf6 → 0.0.0-experiment.ae8c0d8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vouchfor/embeds",
3
- "version": "0.0.0-experiment.ace6cf6",
3
+ "version": "0.0.0-experiment.ae8c0d8",
4
4
  "license": "MIT",
5
5
  "author": "Aaron Williams",
6
6
  "main": "dist/es/embeds.js",
@@ -36,7 +36,7 @@
36
36
  },
37
37
  "dependencies": {
38
38
  "@lit/task": "^1.0.0",
39
- "@vouchfor/media-player": "0.0.0-experiment.ace6cf6",
39
+ "@vouchfor/media-player": "0.0.0-experiment.ae8c0d8",
40
40
  "uuid": "^9.0.1"
41
41
  },
42
42
  "peerDependencies": {
@@ -10,7 +10,17 @@ type EmbedArgs = EmbedProps & {
10
10
  showVouch?: boolean;
11
11
  };
12
12
 
13
- const _Embed = ({ vouchId, templateId, preload, autoplay, env, apiKey, controls, aspectRatio }: EmbedArgs) => {
13
+ const _Embed = ({
14
+ vouchId,
15
+ templateId,
16
+ questions,
17
+ preload,
18
+ autoplay,
19
+ env,
20
+ apiKey,
21
+ controls,
22
+ aspectRatio
23
+ }: EmbedArgs) => {
14
24
  return html`
15
25
  <div style="height: 100vh">
16
26
  <vouch-embed
@@ -18,10 +28,12 @@ const _Embed = ({ vouchId, templateId, preload, autoplay, env, apiKey, controls,
18
28
  apiKey=${ifDefined(apiKey)}
19
29
  vouchId=${ifDefined(vouchId)}
20
30
  templateId=${ifDefined(templateId)}
31
+ .questions=${questions}
21
32
  .controls=${controls}
22
33
  ?autoplay=${autoplay}
23
34
  preload=${ifDefined(preload)}
24
35
  aspectRatio=${ifDefined(aspectRatio)}
36
+ @error=${console.log}
25
37
  ></vouch-embed>
26
38
  </div>
27
39
  `;
@@ -43,6 +55,7 @@ const Embed: Story = {
43
55
  apiKey: 'TVik9uTMgE-PD25UTHIS6gyl0hMBWC7AT4dkpdlLBT4VIfDWZJrQiCk6Ak7m1',
44
56
  vouchId: '6JQEIPeStt',
45
57
  templateId: '357fc118-e179-4171-9446-ff2b8e9d1b29',
58
+ questions: [],
46
59
  aspectRatio: 0,
47
60
  preload: 'none',
48
61
  autoplay: false
@@ -9,7 +9,7 @@ import { getEnvUrls } from '~/utils/env';
9
9
 
10
10
  type EmbedHost = ReactiveControllerHost & Embed;
11
11
 
12
- type TaskDeps = [
12
+ type FetchTaskDeps = [
13
13
  EmbedProps['env'],
14
14
  EmbedProps['apiKey'],
15
15
  EmbedProps['data'],
@@ -17,10 +17,13 @@ type TaskDeps = [
17
17
  EmbedProps['templateId']
18
18
  ];
19
19
 
20
+ type FilterTaskDeps = [EmbedProps['data'], EmbedProps['questions']];
21
+
20
22
  class FetcherController {
21
23
  host: EmbedHost;
22
24
 
23
25
  private _fetching = false;
26
+ private _vouch: EmbedProps['data'];
24
27
 
25
28
  set fetching(value) {
26
29
  if (this._fetching !== value) {
@@ -45,13 +48,13 @@ class FetcherController {
45
48
  });
46
49
 
47
50
  const vouch = await res.json();
48
- this.host.dispatchEvent(new CustomEvent('vouch:loaded', { detail: vouchId }));
51
+ this.host.dispatchEvent(new CustomEvent('vouch:loaded', { detail: vouch?.id }));
49
52
 
50
53
  // HACK: we're currently using API Gateway caching on the embed API without any invalidation logic,
51
54
  // so to ensure that the cache stays up to date, whenever we detect a cache hit we trigger another
52
55
  // API call with the `Cache-Control` header which will re-fill the cache
53
56
  const resCacheCheck = res?.headers?.get('X-Cache-Check');
54
- if (resCacheCheck && resCacheCheck !== cacheCheck) {
57
+ if (resCacheCheck !== cacheCheck) {
55
58
  fetch(`${embedApiUrl}/vouches/${vouchId}`, {
56
59
  method: 'GET',
57
60
  headers: [
@@ -81,7 +84,7 @@ class FetcherController {
81
84
  // so to ensure that the cache stays up to date, whenever we detect a cache hit we trigger another
82
85
  // API call with the `Cache-Control` header which will re-fill the cache
83
86
  const resCacheCheck = res?.headers?.get('X-Cache-Check');
84
- if (resCacheCheck && resCacheCheck !== cacheCheck) {
87
+ if (resCacheCheck !== cacheCheck) {
85
88
  fetch(`${embedApiUrl}/templates/${templateId}`, {
86
89
  method: 'GET',
87
90
  headers: [
@@ -96,9 +99,9 @@ class FetcherController {
96
99
 
97
100
  constructor(host: EmbedHost) {
98
101
  this.host = host;
99
- new Task<TaskDeps, void>(
102
+ new Task<FetchTaskDeps, void>(
100
103
  this.host,
101
- async ([env, apiKey, data, vouchId, templateId]: TaskDeps) => {
104
+ async ([env, apiKey, data, vouchId, templateId]: FetchTaskDeps) => {
102
105
  try {
103
106
  host.vouch = undefined;
104
107
  host.template = undefined;
@@ -109,7 +112,7 @@ class FetcherController {
109
112
  this.fetching = true;
110
113
  template = await this.getTemplate(env, apiKey, templateId);
111
114
  }
112
- host.vouch = data;
115
+ this._vouch = data;
113
116
  host.template = template ?? data?.settings?.template?.instance;
114
117
  } else if (vouchId) {
115
118
  this.fetching = true;
@@ -118,7 +121,7 @@ class FetcherController {
118
121
  this.getVouch(env, apiKey, vouchId),
119
122
  templateId ? this.getTemplate(env, apiKey, templateId) : null
120
123
  ]);
121
- host.vouch = vouch;
124
+ this._vouch = vouch;
122
125
  host.template = template ?? vouch?.settings?.template?.instance;
123
126
  }
124
127
  } finally {
@@ -127,6 +130,22 @@ class FetcherController {
127
130
  },
128
131
  () => [host.env, host.apiKey, host.data, host.vouchId, host.templateId]
129
132
  );
133
+
134
+ // This second task is to be able to filter the vouch without fetching it again if only the questions changed
135
+ new Task<FilterTaskDeps, void>(
136
+ this.host,
137
+ ([vouch, questions]: FilterTaskDeps) => {
138
+ host.vouch = vouch
139
+ ? {
140
+ ...vouch,
141
+ questions: {
142
+ items: vouch?.questions.items.filter((_, index) => !questions?.length || questions?.includes(index + 1))
143
+ }
144
+ }
145
+ : undefined;
146
+ },
147
+ () => [this._vouch, host.questions]
148
+ );
130
149
  }
131
150
  }
132
151
 
@@ -4,15 +4,16 @@ import type { Embed } from '..';
4
4
  import type { VideoEventDetail } from '@vouchfor/media-player';
5
5
  import type { ReactiveController, ReactiveControllerHost } from 'lit';
6
6
 
7
+ import packageJson from '../../../../package.json';
7
8
  import { getEnvUrls } from '~/utils/env';
8
9
 
9
- // In seconds due to checking against node.currentTime
10
- const STREAMED_THROTTLE = 10;
10
+ const MINIMUM_SEND_THRESHOLD = 1;
11
11
 
12
12
  type EmbedHost = ReactiveControllerHost & Embed;
13
13
 
14
14
  type TrackingEvent = 'VOUCH_LOADED' | 'VOUCH_RESPONSE_VIEWED' | 'VIDEO_PLAYED' | 'VIDEO_STREAMED';
15
15
  type TrackingPayload = {
16
+ vouchId?: string;
16
17
  answerId?: string;
17
18
  streamStart?: number;
18
19
  streamEnd?: number;
@@ -45,13 +46,14 @@ class TrackingController implements ReactiveController {
45
46
  host.addController(this);
46
47
  }
47
48
 
48
- private _findVouchId() {
49
+ private _findVouchId(payload?: TrackingPayload) {
50
+ if (payload && 'vouchId' in payload) {
51
+ return payload.vouchId;
52
+ }
49
53
  if (this.host.vouch) {
50
- if ('uuid' in this.host.vouch) {
51
- return this.host.vouch.uuid;
52
- }
53
54
  return this.host.vouch.id;
54
55
  }
56
+ return null;
55
57
  }
56
58
 
57
59
  private _createVisitor = (visitorId: string) => {
@@ -130,7 +132,7 @@ class TrackingController implements ReactiveController {
130
132
  };
131
133
 
132
134
  private _sendTrackingEvent = (event: TrackingEvent, payload?: TrackingPayload) => {
133
- const vouchId = this._findVouchId();
135
+ const vouchId = this._findVouchId(payload);
134
136
 
135
137
  if (!vouchId || this.host.disableTracking) {
136
138
  return;
@@ -140,21 +142,45 @@ class TrackingController implements ReactiveController {
140
142
  const { client, tab, request, visitor } = this._getUids();
141
143
 
142
144
  navigator.sendBeacon(
143
- `${publicApiUrl}/api/events`,
145
+ `${publicApiUrl}/api/v2/events`,
144
146
  JSON.stringify({
145
147
  event,
146
- payload,
148
+ payload: {
149
+ ...payload,
150
+ vouchId
151
+ },
147
152
  context: {
148
153
  'x-uid-client': client,
149
154
  'x-uid-tab': tab,
150
155
  'x-uid-request': request,
151
156
  'x-uid-visitor': visitor,
152
- 'x-reporting-metadata': this._getReportingMetadata()
157
+ 'x-reporting-metadata': this._getReportingMetadata(),
158
+ 'x-embeds-version': packageJson.version
153
159
  }
154
160
  })
155
161
  );
156
162
  };
157
163
 
164
+ private _streamEnded = () => {
165
+ if (this._currentlyPlayingVideo) {
166
+ const { id, key } = this._currentlyPlayingVideo;
167
+ // Don't send a tracking event when seeking backwards
168
+ if (this._streamLatestTime[key] > this._streamStartTime[key] + MINIMUM_SEND_THRESHOLD) {
169
+ // Send a video streamed event any time the stream ends to capture the time between starting
170
+ // the video and the video stopping for any reason (pausing, deleting the embed node or closing the browser)
171
+ this._sendTrackingEvent('VIDEO_STREAMED', {
172
+ answerId: id,
173
+ streamStart: this._streamStartTime[key],
174
+ streamEnd: this._streamLatestTime[key]
175
+ });
176
+ }
177
+
178
+ // Make sure these events are only sent once by deleting the start and latest times
179
+ delete this._streamStartTime[key];
180
+ delete this._streamLatestTime[key];
181
+ }
182
+ };
183
+
158
184
  private _handleVouchLoaded = ({ detail: vouchId }: CustomEvent<string>) => {
159
185
  if (!vouchId) {
160
186
  return;
@@ -162,7 +188,7 @@ class TrackingController implements ReactiveController {
162
188
 
163
189
  // Only send loaded event once per session
164
190
  if (!this._hasLoaded[vouchId]) {
165
- this._sendTrackingEvent('VOUCH_LOADED');
191
+ this._sendTrackingEvent('VOUCH_LOADED', { vouchId });
166
192
  this._hasLoaded[vouchId] = true;
167
193
  }
168
194
  };
@@ -186,54 +212,60 @@ class TrackingController implements ReactiveController {
186
212
  this._answersViewed[key] = true;
187
213
  }
188
214
 
189
- this._streamStartTime[key] = node.currentTime;
190
- this._streamLatestTime[key] = node.currentTime;
191
- };
192
-
193
- private _handleVideoTimeUpdate = ({ detail: { id, key, node } }: CustomEvent<VideoEventDetail>) => {
194
- // We only want to count any time that the video is actually playing
195
- if (!this.host.paused) {
196
- this._currentlyPlayingVideo = { id, key, node };
215
+ if (!this._streamStartTime[key]) {
216
+ this._streamStartTime[key] = node.currentTime;
197
217
  this._streamLatestTime[key] = node.currentTime;
198
218
  }
219
+ };
199
220
 
221
+ private _handleVideoTimeUpdate = ({ detail: { id, key, node } }: CustomEvent<VideoEventDetail>) => {
200
222
  if (
201
- !node.paused &&
223
+ // We only want to count any time that the video is actually playing
202
224
  !this.host.paused &&
203
- // Only fire the video seeked event when this video is the active one
204
- id === this.host.scene?.video?.id &&
205
- // Throttle the frequency that we send streamed events while playing
206
- this._streamLatestTime[key] - this._streamStartTime[key] > STREAMED_THROTTLE
225
+ // Only update the latest time if this event fires for the currently active video
226
+ id === this.host.scene?.video?.id
207
227
  ) {
208
- this._sendTrackingEvent('VIDEO_STREAMED', {
209
- answerId: id,
210
- streamStart: this._streamStartTime[key],
211
- streamEnd: this._streamLatestTime[key]
212
- });
213
-
214
- this._streamStartTime[key] = node.currentTime;
228
+ this._currentlyPlayingVideo = { id, key, node };
229
+ this._streamLatestTime[key] = node.currentTime;
215
230
  }
216
231
  };
217
232
 
218
233
  private _handleVideoPause = ({ detail: { id, key } }: CustomEvent<VideoEventDetail>) => {
219
- // Don't send a tracking event when seeking backwards
220
- if (this._streamLatestTime[key] > this._streamStartTime[key]) {
221
- // Send a video streamed event any time the video pauses then reset the streamed state
222
- // We do this to capture the last bit of time that the video was played between the previous
223
- // stream event and the video being paused manually or stopping because it ended
234
+ if (this._streamLatestTime[key] > this._streamStartTime[key] + MINIMUM_SEND_THRESHOLD) {
224
235
  this._sendTrackingEvent('VIDEO_STREAMED', {
225
236
  answerId: id,
226
237
  streamStart: this._streamStartTime[key],
227
238
  streamEnd: this._streamLatestTime[key]
228
239
  });
229
240
  }
230
- this._currentlyPlayingVideo = null;
231
241
  delete this._streamStartTime[key];
232
242
  delete this._streamLatestTime[key];
233
243
  };
234
244
 
245
+ private _pageUnloading = () => {
246
+ this._streamEnded();
247
+ // This will try to send the same stream event again so we delete the start and latest
248
+ // time in stream ended so that there is no times to send and the pause event does nothing
249
+ this.host.pause();
250
+ };
251
+
252
+ private _handleVisibilityChange = () => {
253
+ if (document.visibilityState === 'hidden') {
254
+ this._pageUnloading();
255
+ }
256
+ };
257
+
258
+ private _handlePageHide = () => {
259
+ this._pageUnloading();
260
+ };
261
+
235
262
  hostConnected() {
236
263
  requestAnimationFrame(() => {
264
+ if ('onvisibilitychange' in document) {
265
+ document.addEventListener('visibilitychange', this._handleVisibilityChange);
266
+ } else {
267
+ window.addEventListener('pagehide', this._handlePageHide);
268
+ }
237
269
  this.host.addEventListener('vouch:loaded', this._handleVouchLoaded);
238
270
  this.host.mediaPlayer?.addEventListener('play', this._handlePlay);
239
271
  this.host.mediaPlayer?.addEventListener('video:play', this._handleVideoPlay);
@@ -243,20 +275,12 @@ class TrackingController implements ReactiveController {
243
275
  }
244
276
 
245
277
  hostDisconnected() {
246
- if (this._currentlyPlayingVideo) {
247
- const { id, key } = this._currentlyPlayingVideo;
248
- if (this._streamLatestTime[key] > this._streamStartTime[key]) {
249
- // Send a video streamed event any time the video pauses then reset the streamed state
250
- // We do this to capture the last bit of time that the video was played between the previous
251
- // stream event and the video being paused manually or stopping because it ended
252
- this._sendTrackingEvent('VIDEO_STREAMED', {
253
- answerId: id,
254
- streamStart: this._streamStartTime[key],
255
- streamEnd: this._streamLatestTime[key]
256
- });
257
- }
278
+ this._streamEnded();
279
+ if ('onvisibilitychange' in document) {
280
+ document.removeEventListener('visibilitychange', this._handleVisibilityChange);
281
+ } else {
282
+ window.removeEventListener('pagehide', this._handlePageHide);
258
283
  }
259
-
260
284
  this.host.removeEventListener('vouch:loaded', this._handleVouchLoaded);
261
285
  this.host.mediaPlayer?.removeEventListener('play', this._handlePlay);
262
286
  this.host.mediaPlayer?.removeEventListener('video:play', this._handleVideoPlay);
@@ -1,9 +1,9 @@
1
- import { html, LitElement } from 'lit';
1
+ import { css, html, LitElement } from 'lit';
2
2
  import { customElement, property, state } from 'lit/decorators.js';
3
3
  import { ifDefined } from 'lit/directives/if-defined.js';
4
4
  import { createRef, ref } from 'lit/directives/ref.js';
5
5
 
6
- import type { Scene, TemplateInstance } from '@vouchfor/canvas-video';
6
+ import type { Scene, Scenes, TemplateInstance } from '@vouchfor/canvas-video';
7
7
  import type { MediaPlayer, MediaPlayerProps } from '@vouchfor/media-player';
8
8
  import type { Ref } from 'lit/directives/ref.js';
9
9
  import type { Environment } from '~/utils/env';
@@ -21,15 +21,26 @@ type EmbedProps = Pick<MediaPlayerProps, 'data' | 'aspectRatio' | 'preload' | 'a
21
21
  trackingSource?: string;
22
22
  vouchId?: string;
23
23
  templateId?: string;
24
+ // Index of the questions to include starting from 1
25
+ questions?: number[];
24
26
  };
25
27
 
26
28
  @customElement('vouch-embed')
27
29
  class Embed extends LitElement {
30
+ static styles = [
31
+ css`
32
+ :host {
33
+ display: flex;
34
+ }
35
+ `
36
+ ];
37
+
28
38
  private _mediaPlayerRef: Ref<MediaPlayer> = createRef();
29
39
 
30
- @property({ type: Object, attribute: 'data' }) data: EmbedProps['data'];
40
+ @property({ type: Object }) data: EmbedProps['data'];
31
41
  @property({ type: String }) vouchId: EmbedProps['vouchId'];
32
42
  @property({ type: String }) templateId: EmbedProps['templateId'];
43
+ @property({ type: Array }) questions: EmbedProps['questions'];
33
44
 
34
45
  @property({ type: String }) env: EmbedProps['env'] = 'prod';
35
46
  @property({ type: String }) apiKey: EmbedProps['apiKey'] = '';
@@ -149,6 +160,10 @@ class Embed extends LitElement {
149
160
  return this._mediaPlayerRef.value?.scenes ?? [];
150
161
  }
151
162
 
163
+ get sceneConfig(): Scenes | null {
164
+ return this._mediaPlayerRef.value?.sceneConfig ?? null;
165
+ }
166
+
152
167
  get videoState() {
153
168
  return this._mediaPlayerRef.value?.videoState;
154
169
  }
@@ -171,7 +186,7 @@ class Embed extends LitElement {
171
186
 
172
187
  render() {
173
188
  return html`
174
- <vmp-new-media-player
189
+ <vmp-media-player
175
190
  ${ref(this._mediaPlayerRef)}
176
191
  ${this.eventController.register()}
177
192
  ?autoplay=${this.autoplay}
@@ -181,7 +196,7 @@ class Embed extends LitElement {
181
196
  aspectRatio=${ifDefined(this.aspectRatio)}
182
197
  preload=${ifDefined(this.preload)}
183
198
  .controls=${this.controls}
184
- ></vmp-new-media-player>
199
+ ></vmp-media-player>
185
200
  `;
186
201
  }
187
202
  }
File without changes
File without changes
File without changes