@raydiant/playlist-window-manager 0.18.0 → 0.20.0-rc.1

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.
@@ -89,6 +89,7 @@ export default class PlaylistWindowManager extends EventEmitter {
89
89
  private evalRuleEngine;
90
90
  private handleEnableInterruptLock;
91
91
  private handleDisableInterruptLock;
92
+ private handleAnalyticsEvent;
92
93
  private handleCurrentPresentationComplete;
93
94
  private handleCurrentPresentationError;
94
95
  private handleNextPresentationError;
@@ -109,6 +109,11 @@ var PlaylistWindowManager = /** @class */ (function (_super) {
109
109
  _this.endCurrentPresentation();
110
110
  }
111
111
  };
112
+ _this.handleAnalyticsEvent = function (event) {
113
+ // Emit the event on the current window. Only the current window should
114
+ // send analytics events since the next window is not visible.
115
+ _this.emitCurrentWindowEvent('window_analytics_event', event);
116
+ };
112
117
  _this.handleCurrentPresentationComplete = function () { return __awaiter(_this, void 0, void 0, function () {
113
118
  var logger, isThrottled, now, timeSinceLastComplete, delay, _a, next, preview, nextPreviewPresentationId;
114
119
  var _this = this;
@@ -693,6 +698,7 @@ var PlaylistWindowManager = /** @class */ (function (_super) {
693
698
  this.currentWindow.onError(this.handleCurrentPresentationError);
694
699
  this.currentWindow.onEnableInterruptLock(this.handleEnableInterruptLock);
695
700
  this.currentWindow.onDisableInterruptLock(this.handleDisableInterruptLock);
701
+ this.currentWindow.onAnalyticsEvent(this.handleAnalyticsEvent);
696
702
  return [4 /*yield*/, this.currentWindow.load()];
697
703
  case 1:
698
704
  _a.sent();
@@ -896,6 +902,7 @@ var PlaylistWindowManager = /** @class */ (function (_super) {
896
902
  this.currentWindow.onError(this.handleCurrentPresentationError);
897
903
  this.currentWindow.onEnableInterruptLock(this.handleEnableInterruptLock);
898
904
  this.currentWindow.onDisableInterruptLock(this.handleDisableInterruptLock);
905
+ this.currentWindow.onAnalyticsEvent(this.handleAnalyticsEvent);
899
906
  }
900
907
  // Play new current window.
901
908
  this.playCurrentWindow();
@@ -1,7 +1,7 @@
1
1
  import * as RE from '@raydiant/playlist-rule-engine';
2
2
  import PlaylistWindowManagerState from './PlaylistWindowManagerState';
3
3
  import PlaylistWindowManager from './PlaylistWindowManager';
4
- import { Presentation, ApplicationVersion, Theme, Device, PresentationWindowRenderer, WindowBounds, Logger, BuilderState, WorkerState } from './types';
4
+ import { Presentation, ApplicationVersion, Theme, Device, PresentationWindowRenderer, WindowBounds, Logger, BuilderState, WorkerState, AnalyticsEvent } from './types';
5
5
  export interface PresentationWindowOptions {
6
6
  windowManager?: PlaylistWindowManager;
7
7
  windowId: string;
@@ -49,6 +49,7 @@ export default class PresentationWindow {
49
49
  private isChildWindowLocked;
50
50
  private enableInterruptLockCallback;
51
51
  private disableInterruptLockCallback;
52
+ private analyticsEventCallback;
52
53
  private builderState;
53
54
  private presentation;
54
55
  private playlistWindowManagerState;
@@ -73,6 +74,7 @@ export default class PresentationWindow {
73
74
  onError(callback: ((err: Error) => void) | null): void;
74
75
  onEnableInterruptLock(callback: (() => void) | null): void;
75
76
  onDisableInterruptLock(callback: (() => void) | null): void;
77
+ onAnalyticsEvent(callback: ((event: AnalyticsEvent) => void) | null): void;
76
78
  isReady(): boolean;
77
79
  isComplete(): boolean;
78
80
  isPlaying(): boolean;
@@ -101,6 +103,7 @@ export default class PresentationWindow {
101
103
  private closeChildWindow;
102
104
  private enableInteruptLock;
103
105
  private disableInteruptLock;
106
+ private getEmbeddedAppUrl;
104
107
  private handleEmbeddedMessage;
105
108
  private handleLegacyMessage;
106
109
  private handleReady;
@@ -67,6 +67,15 @@ var defaultAppRendererUrl = 'https://apps.raydiant.com/3.0.0/index.html';
67
67
  var isValidNumber = function (value) {
68
68
  return typeof value === 'number' && !isNaN(value);
69
69
  };
70
+ var isValidAnalyticsEvent = function (value) {
71
+ return value &&
72
+ typeof value === 'object' &&
73
+ typeof value.type === 'string' &&
74
+ typeof value.source === 'string' &&
75
+ (typeof value.productCategory === 'string' ||
76
+ value.productCategory === undefined ||
77
+ value.productCategory === null);
78
+ };
70
79
  var PresentationWindow = /** @class */ (function () {
71
80
  function PresentationWindow(options) {
72
81
  var _this = this;
@@ -87,6 +96,7 @@ var PresentationWindow = /** @class */ (function () {
87
96
  this.isChildWindowLocked = {};
88
97
  this.enableInterruptLockCallback = null;
89
98
  this.disableInterruptLockCallback = null;
99
+ this.analyticsEventCallback = null;
90
100
  this.forceMute = false;
91
101
  this.error = null;
92
102
  this.workerState = {};
@@ -168,6 +178,12 @@ var PresentationWindow = /** @class */ (function () {
168
178
  }
169
179
  break;
170
180
  }
181
+ case 'analytics-event': {
182
+ if (this.analyticsEventCallback && isValidAnalyticsEvent(payload)) {
183
+ this.analyticsEventCallback(payload);
184
+ }
185
+ break;
186
+ }
171
187
  default: {
172
188
  this.logWarning("Received unknown embedded message type: " + type);
173
189
  }
@@ -400,6 +416,9 @@ var PresentationWindow = /** @class */ (function () {
400
416
  PresentationWindow.prototype.onDisableInterruptLock = function (callback) {
401
417
  this.disableInterruptLockCallback = callback;
402
418
  };
419
+ PresentationWindow.prototype.onAnalyticsEvent = function (callback) {
420
+ this.analyticsEventCallback = callback;
421
+ };
403
422
  PresentationWindow.prototype.isReady = function () {
404
423
  // TODO: To support the 'ready' event in embedded apps, they will need to tell the
405
424
  // runtime that it should wait for ready instead of DOMLoaded. This could be a "wait
@@ -449,7 +468,7 @@ var PresentationWindow = /** @class */ (function () {
449
468
  _a = this.options, applicationVersion = _a.applicationVersion, windowId = _a.windowId;
450
469
  if (!this.isEmbeddedApp()) return [3 /*break*/, 3];
451
470
  if (!applicationVersion.embeddedUrlFormat) return [3 /*break*/, 2];
452
- url = embeddedApps_1.default(this.presentation, applicationVersion);
471
+ url = this.getEmbeddedAppUrl();
453
472
  this.logInfo("Reload window '" + windowId + "' for embedded app url '" + url + "'");
454
473
  return [4 /*yield*/, this.loadWindow(url)];
455
474
  case 1:
@@ -466,7 +485,11 @@ var PresentationWindow = /** @class */ (function () {
466
485
  });
467
486
  };
468
487
  PresentationWindow.prototype.runGetProperties = function (builderState) {
469
- this.sendMessage('run_get_properties', builderState);
488
+ // Do no support conditional properties for embedded apps. We will likely
489
+ // change the way conditional properties work.
490
+ if (!this.isEmbeddedApp()) {
491
+ this.sendMessage('run_get_properties', builderState);
492
+ }
470
493
  };
471
494
  PresentationWindow.prototype.isEmbeddedApp = function () {
472
495
  var _a = this.options, applicationVersion = _a.applicationVersion, isPreview = _a.isPreview;
@@ -517,9 +540,9 @@ var PresentationWindow = /** @class */ (function () {
517
540
  if (!applicationVersion.embeddedUrlFormat) {
518
541
  throw new Error("Error loading embedded app, missing embeddedUrlFormat for '" + applicationVersion.id + "'");
519
542
  }
520
- url = embeddedApps_1.default(this.presentation, applicationVersion);
543
+ url = this.getEmbeddedAppUrl();
521
544
  this.logInfo("Create window '" + windowId + "' for embedded app url '" + url + "'");
522
- return [4 /*yield*/, this.createAndLoadWindow(this.appendWindowIdToHash(url, windowId),
545
+ return [4 /*yield*/, this.createAndLoadWindow(url,
523
546
  // Embedded apps should not have a backgroundColor because some sites expect
524
547
  // the browser default of white will be applied.
525
548
  '#ffffff', this.handleEmbeddedMessage)];
@@ -762,6 +785,9 @@ var PresentationWindow = /** @class */ (function () {
762
785
  windowManager.on('window_end_error', function (metadata) {
763
786
  parentWindowManager.emit('window_end_error', metadata);
764
787
  });
788
+ windowManager.on('window_analytics_event', function (metadata) {
789
+ parentWindowManager.emit('window_analytics_event', metadata);
790
+ });
765
791
  }
766
792
  this.windowManagers[windowName] = windowManager;
767
793
  windowManager.setRenderer(renderer);
@@ -827,6 +853,14 @@ var PresentationWindow = /** @class */ (function () {
827
853
  this.disableInterruptLockCallback();
828
854
  }
829
855
  };
856
+ PresentationWindow.prototype.getEmbeddedAppUrl = function () {
857
+ var windowId = this.options.windowId;
858
+ var metadata = {
859
+ renderMode: this.options.isPreview ? 'preview' : 'live'
860
+ };
861
+ var url = embeddedApps_1.default(this.presentation, this.options.applicationVersion, metadata);
862
+ return this.appendWindowIdToHash(url, windowId);
863
+ };
830
864
  PresentationWindow.prototype.handleReady = function () {
831
865
  this.didReceiveReady = true;
832
866
  this.checkIsReadyCallback();
@@ -1,2 +1,2 @@
1
- import { Presentation, ApplicationVersion } from './types';
2
- export default function createEmbeddedApp(presentation: Presentation, applicationVersion: ApplicationVersion): string;
1
+ import { Presentation, ApplicationVersion, EmbeddedAppMetadata } from './types';
2
+ export default function createEmbeddedApp(presentation: Presentation, applicationVersion: ApplicationVersion, metadata: EmbeddedAppMetadata): string;
@@ -1,8 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- function createEmbeddedApp(presentation, applicationVersion) {
3
+ function createEmbeddedApp(presentation, applicationVersion, metadata) {
4
4
  // build matches
5
- var parameterRegex = /(:([\w-]+))(.?$|[^\w-])/gm;
5
+ var parameterRegex = /(:([\w-\$]+))(.?$|[^\w-\$])/gm;
6
6
  var matches = [];
7
7
  var match = null;
8
8
  do {
@@ -17,19 +17,30 @@ function createEmbeddedApp(presentation, applicationVersion) {
17
17
  var queryParamVariableName = m[1]; // Where the value of your input will be injected. E.g. ":location"
18
18
  var queryParamName = m[2]; // The query param name. E.g. "location"
19
19
  var _a = queryParamVariableName.split(':'), variableName = _a[1];
20
- // Don't replace variables that are numbers to allow setting
21
- // port numbers in the embedded app url.
22
- if (!isNaN(Number(variableName)))
23
- return;
24
- var presentationProperty = applicationVersion.presentationProperties.find(function (p) { return p.name === variableName; });
25
- // If the presentation property is not found (this shouldn't happen), default to encoding the value.
26
- // Otherwise, only skip encoding if the presentation property's encode property is false.
27
- var shouldEncodeValue = !presentationProperty || presentationProperty.encode !== false;
28
- var value = presentation.applicationVariables[queryParamName];
29
- if (value === undefined)
30
- value = '';
31
- if (Array.isArray(value)) {
32
- value = JSON.stringify(value);
20
+ var shouldEncodeValue = true;
21
+ var value = '';
22
+ if (variableName.charAt(0) === '$') {
23
+ var metadataKey = variableName.slice(1);
24
+ if (metadataKey === 'renderMode') {
25
+ value = metadata.renderMode;
26
+ }
27
+ }
28
+ else {
29
+ // Don't replace variables that are numbers to allow setting
30
+ // port numbers in the embedded app url.
31
+ if (!isNaN(Number(variableName)))
32
+ return;
33
+ var presentationProperty = applicationVersion.presentationProperties.find(function (p) { return p.name === variableName; });
34
+ // If the presentation property is not found (this shouldn't happen), default to encoding the value.
35
+ // Otherwise, only skip encoding if the presentation property's encode property is false.
36
+ shouldEncodeValue =
37
+ !presentationProperty || presentationProperty.encode !== false;
38
+ value = presentation.applicationVariables[queryParamName];
39
+ if (value === undefined)
40
+ value = '';
41
+ if (Array.isArray(value)) {
42
+ value = JSON.stringify(value);
43
+ }
33
44
  }
34
45
  if (shouldEncodeValue) {
35
46
  value = encodeURIComponent(value);
@@ -15,50 +15,60 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  var embeddedApps_1 = __importDefault(require("./embeddedApps"));
18
- var createPresentation = function (overrides) { return (__assign({ id: 'id', name: 'name', applicationDeploymentId: 'applicationDeploymentId', themeId: null, applicationVariables: {}, duration: 0, r: {
19
- resource: {
20
- updatedAt: '',
21
- createdAt: ''
22
- },
23
- presentationsPlaylists: []
24
- } }, overrides)); };
25
- var createApplicationVersion = function (overrides) { return (__assign({ id: 'id', name: 'name', applicationId: 'applicationId', embeddedUrlFormat: null, configurableDuration: false, sourceUrl: null, presentationProperties: [] }, overrides)); };
18
+ var createPresentation = function (overrides) {
19
+ if (overrides === void 0) { overrides = {}; }
20
+ return (__assign({ id: 'id', name: 'name', applicationDeploymentId: 'applicationDeploymentId', themeId: null, applicationVariables: {}, duration: 0, r: {
21
+ resource: {
22
+ updatedAt: '',
23
+ createdAt: ''
24
+ },
25
+ presentationsPlaylists: []
26
+ } }, overrides));
27
+ };
28
+ var createApplicationVersion = function (overrides) {
29
+ if (overrides === void 0) { overrides = {}; }
30
+ return (__assign({ id: 'id', name: 'name', applicationId: 'applicationId', embeddedUrlFormat: null, configurableDuration: false, sourceUrl: null, presentationProperties: [] }, overrides));
31
+ };
32
+ var createMetadata = function (overrides) {
33
+ if (overrides === void 0) { overrides = {}; }
34
+ return (__assign({ renderMode: 'live' }, overrides));
35
+ };
26
36
  test('Replaces variable in query parameteers', function () {
27
37
  expect(embeddedApps_1.default(createPresentation({ applicationVariables: { foo: 'bar' } }), createApplicationVersion({
28
38
  embeddedUrlFormat: 'https://example.com?foo=:foo'
29
- }))).toBe('https://example.com?foo=bar');
39
+ }), createMetadata())).toBe('https://example.com?foo=bar');
30
40
  });
31
41
  test('Replaces variable in path', function () {
32
42
  expect(embeddedApps_1.default(createPresentation({ applicationVariables: { foo: 'bar' } }), createApplicationVersion({
33
43
  embeddedUrlFormat: 'https://example.com/:foo'
34
- }))).toBe('https://example.com/bar');
44
+ }), createMetadata())).toBe('https://example.com/bar');
35
45
  });
36
46
  test('Replaces undefined variable with empty string', function () {
37
47
  expect(embeddedApps_1.default(createPresentation({ applicationVariables: {} }), createApplicationVersion({
38
48
  embeddedUrlFormat: 'https://example.com/:foo'
39
- }))).toBe('https://example.com/');
49
+ }), createMetadata())).toBe('https://example.com/');
40
50
  });
41
51
  test('Replaces variable with underscore', function () {
42
52
  expect(embeddedApps_1.default(createPresentation({ applicationVariables: { foo_1: 'bar' } }), createApplicationVersion({
43
53
  embeddedUrlFormat: 'https://example.com/:foo_1'
44
- }))).toBe('https://example.com/bar');
54
+ }), createMetadata())).toBe('https://example.com/bar');
45
55
  });
46
56
  test('Replaces variable with hyphen', function () {
47
57
  expect(embeddedApps_1.default(createPresentation({ applicationVariables: { 'foo-1': 'bar' } }), createApplicationVersion({
48
58
  embeddedUrlFormat: 'https://example.com/:foo-1'
49
- }))).toBe('https://example.com/bar');
59
+ }), createMetadata())).toBe('https://example.com/bar');
50
60
  });
51
61
  test('Does not replace variable that is a number', function () {
52
62
  expect(embeddedApps_1.default(createPresentation({ applicationVariables: { foo: 'bar' } }), createApplicationVersion({
53
63
  embeddedUrlFormat: 'http://localhost:8080?foo=:foo'
54
- }))).toBe('http://localhost:8080?foo=bar');
64
+ }), createMetadata())).toBe('http://localhost:8080?foo=bar');
55
65
  });
56
66
  test('Encodes a variables containing special characters', function () {
57
67
  var fooValue = 'Lorem Ipsum. !@#$%^&*()-. More Text';
58
68
  var fooValueEncoded = 'Lorem%20Ipsum.%20!%40%23%24%25%5E%26*()-.%20More%20Text';
59
69
  expect(embeddedApps_1.default(createPresentation({ applicationVariables: { foo: fooValue } }), createApplicationVersion({
60
70
  embeddedUrlFormat: "http://localhost:8080?foo=:foo"
61
- }))).toBe("http://localhost:8080?foo=" + fooValueEncoded);
71
+ }), createMetadata())).toBe("http://localhost:8080?foo=" + fooValueEncoded);
62
72
  });
63
73
  test('Does not encode value if presentation property disables encoding', function () {
64
74
  expect(embeddedApps_1.default(createPresentation({
@@ -66,12 +76,38 @@ test('Does not encode value if presentation property disables encoding', functio
66
76
  }), createApplicationVersion({
67
77
  embeddedUrlFormat: ":url",
68
78
  presentationProperties: [{ name: 'url', encode: false }]
69
- }))).toBe("https://raydiant.com");
79
+ }), createMetadata())).toBe("https://raydiant.com");
70
80
  });
71
81
  test('Encodes a variables containing an array', function () {
72
82
  var fooValue = ['option-a', 'option-b', 'option-c'];
73
83
  var fooValueEncoded = '%5B%22option-a%22%2C%22option-b%22%2C%22option-c%22%5D';
74
84
  expect(embeddedApps_1.default(createPresentation({ applicationVariables: { foo: fooValue } }), createApplicationVersion({
75
85
  embeddedUrlFormat: "http://localhost:8080?foo=:foo"
76
- }))).toBe("http://localhost:8080?foo=" + fooValueEncoded);
86
+ }), createMetadata())).toBe("http://localhost:8080?foo=" + fooValueEncoded);
87
+ });
88
+ test('Replaces $renderMode with metadata', function () {
89
+ var presentation = createPresentation();
90
+ var applicationVersion = createApplicationVersion({
91
+ embeddedUrlFormat: 'https://example.com?renderMode=:$renderMode'
92
+ });
93
+ expect(embeddedApps_1.default(presentation, applicationVersion, createMetadata({ renderMode: 'preview' }))).toBe('https://example.com?renderMode=preview');
94
+ expect(embeddedApps_1.default(presentation, applicationVersion, createMetadata({ renderMode: 'live' }))).toBe('https://example.com?renderMode=live');
95
+ });
96
+ test('Replaces undefined metadata with empty string', function () {
97
+ var presentation = createPresentation();
98
+ var applicationVersion = createApplicationVersion({
99
+ embeddedUrlFormat: 'https://example.com?invalid=:$invalid'
100
+ });
101
+ expect(embeddedApps_1.default(presentation, applicationVersion, createMetadata())).toBe('https://example.com?invalid=');
102
+ });
103
+ test('Replace protocol, ip and port', function () {
104
+ expect(embeddedApps_1.default(createPresentation({
105
+ applicationVariables: {
106
+ protocol: 'http',
107
+ ip: '192.168.1.0',
108
+ port: 3000
109
+ }
110
+ }), createApplicationVersion({
111
+ embeddedUrlFormat: ":protocol://:ip::port"
112
+ }), createMetadata())).toBe("http://192.168.1.0:3000");
77
113
  });
package/build/types.d.ts CHANGED
@@ -127,3 +127,11 @@ export interface InputState {
127
127
  path: Path;
128
128
  state: any;
129
129
  }
130
+ export interface EmbeddedAppMetadata {
131
+ renderMode: 'preview' | 'live';
132
+ }
133
+ export interface AnalyticsEvent {
134
+ type: string;
135
+ source: string;
136
+ productCategory?: string | null;
137
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@raydiant/playlist-window-manager",
3
- "version": "0.18.0",
3
+ "version": "0.20.0-rc.1",
4
4
  "description": "Window manager for playlist playback.",
5
5
  "main": "build/index.js",
6
6
  "scripts": {
@@ -7,7 +7,8 @@ import {
7
7
  WindowBounds,
8
8
  Theme,
9
9
  Logger,
10
- WorkerState
10
+ WorkerState,
11
+ AnalyticsEvent
11
12
  } from './types';
12
13
  import PresentationWindow from './PresentationWindow';
13
14
  import PlaylistWindowManagerState from './PlaylistWindowManagerState';
@@ -521,6 +522,8 @@ export default class PlaylistWindowManager extends EventEmitter {
521
522
  this.currentWindow.onError(this.handleCurrentPresentationError);
522
523
  this.currentWindow.onEnableInterruptLock(this.handleEnableInterruptLock);
523
524
  this.currentWindow.onDisableInterruptLock(this.handleDisableInterruptLock);
525
+ this.currentWindow.onAnalyticsEvent(this.handleAnalyticsEvent);
526
+
524
527
  await this.currentWindow.load();
525
528
  }
526
529
 
@@ -691,6 +694,7 @@ export default class PlaylistWindowManager extends EventEmitter {
691
694
  this.currentWindow.onDisableInterruptLock(
692
695
  this.handleDisableInterruptLock
693
696
  );
697
+ this.currentWindow.onAnalyticsEvent(this.handleAnalyticsEvent);
694
698
  }
695
699
 
696
700
  // Play new current window.
@@ -754,6 +758,12 @@ export default class PlaylistWindowManager extends EventEmitter {
754
758
  }
755
759
  };
756
760
 
761
+ private handleAnalyticsEvent = (event: AnalyticsEvent) => {
762
+ // Emit the event on the current window. Only the current window should
763
+ // send analytics events since the next window is not visible.
764
+ this.emitCurrentWindowEvent('window_analytics_event', event);
765
+ };
766
+
757
767
  private handleCurrentPresentationComplete = async () => {
758
768
  const { logger } = this.options;
759
769
 
@@ -15,7 +15,9 @@ import {
15
15
  Logger,
16
16
  OpenChildWindowOptions,
17
17
  BuilderState,
18
- WorkerState
18
+ WorkerState,
19
+ EmbeddedAppMetadata,
20
+ AnalyticsEvent
19
21
  } from './types';
20
22
 
21
23
  export interface PresentationWindowOptions {
@@ -56,6 +58,15 @@ const defaultAppRendererUrl = 'https://apps.raydiant.com/3.0.0/index.html';
56
58
  const isValidNumber = (value: any) =>
57
59
  typeof value === 'number' && !isNaN(value);
58
60
 
61
+ const isValidAnalyticsEvent = (value: any): value is AnalyticsEvent =>
62
+ value &&
63
+ typeof value === 'object' &&
64
+ typeof value.type === 'string' &&
65
+ typeof value.source === 'string' &&
66
+ (typeof value.productCategory === 'string' ||
67
+ value.productCategory === undefined ||
68
+ value.productCategory === null);
69
+
59
70
  export default class PresentationWindow {
60
71
  private didReceiveReady: boolean = false;
61
72
  private readyCallback: (() => void) | null = null;
@@ -75,6 +86,9 @@ export default class PresentationWindow {
75
86
  private isChildWindowLocked: { [windowId: string]: boolean } = {};
76
87
  private enableInterruptLockCallback: (() => void) | null = null;
77
88
  private disableInterruptLockCallback: (() => void) | null = null;
89
+ private analyticsEventCallback:
90
+ | ((event: AnalyticsEvent) => void)
91
+ | null = null;
78
92
 
79
93
  private builderState: BuilderState | undefined;
80
94
  private presentation: Presentation;
@@ -263,6 +277,10 @@ export default class PresentationWindow {
263
277
  this.disableInterruptLockCallback = callback;
264
278
  }
265
279
 
280
+ public onAnalyticsEvent(callback: ((event: AnalyticsEvent) => void) | null) {
281
+ this.analyticsEventCallback = callback;
282
+ }
283
+
266
284
  public isReady() {
267
285
  // TODO: To support the 'ready' event in embedded apps, they will need to tell the
268
286
  // runtime that it should wait for ready instead of DOMLoaded. This could be a "wait
@@ -324,7 +342,7 @@ export default class PresentationWindow {
324
342
 
325
343
  if (this.isEmbeddedApp()) {
326
344
  if (applicationVersion.embeddedUrlFormat) {
327
- const url = createEmbeddedApp(this.presentation, applicationVersion);
345
+ const url = this.getEmbeddedAppUrl();
328
346
 
329
347
  this.logInfo(
330
348
  `Reload window '${windowId}' for embedded app url '${url}'`
@@ -338,7 +356,11 @@ export default class PresentationWindow {
338
356
  }
339
357
 
340
358
  public runGetProperties(builderState?: BuilderState) {
341
- this.sendMessage('run_get_properties', builderState);
359
+ // Do no support conditional properties for embedded apps. We will likely
360
+ // change the way conditional properties work.
361
+ if (!this.isEmbeddedApp()) {
362
+ this.sendMessage('run_get_properties', builderState);
363
+ }
342
364
  }
343
365
 
344
366
  private isEmbeddedApp() {
@@ -399,11 +421,11 @@ export default class PresentationWindow {
399
421
  );
400
422
  }
401
423
 
402
- const url = createEmbeddedApp(this.presentation, applicationVersion);
424
+ const url = this.getEmbeddedAppUrl();
403
425
  this.logInfo(`Create window '${windowId}' for embedded app url '${url}'`);
404
426
 
405
427
  await this.createAndLoadWindow(
406
- this.appendWindowIdToHash(url, windowId),
428
+ url,
407
429
  // Embedded apps should not have a backgroundColor because some sites expect
408
430
  // the browser default of white will be applied.
409
431
  '#ffffff',
@@ -666,6 +688,9 @@ export default class PresentationWindow {
666
688
  windowManager.on('window_end_error', metadata => {
667
689
  parentWindowManager.emit('window_end_error', metadata);
668
690
  });
691
+ windowManager.on('window_analytics_event', metadata => {
692
+ parentWindowManager.emit('window_analytics_event', metadata);
693
+ });
669
694
  }
670
695
 
671
696
  this.windowManagers[windowName] = windowManager;
@@ -735,6 +760,22 @@ export default class PresentationWindow {
735
760
  }
736
761
  }
737
762
 
763
+ private getEmbeddedAppUrl() {
764
+ const { windowId } = this.options;
765
+
766
+ const metadata: EmbeddedAppMetadata = {
767
+ renderMode: this.options.isPreview ? 'preview' : 'live'
768
+ };
769
+
770
+ const url = createEmbeddedApp(
771
+ this.presentation,
772
+ this.options.applicationVersion,
773
+ metadata
774
+ );
775
+
776
+ return this.appendWindowIdToHash(url, windowId);
777
+ }
778
+
738
779
  private handleEmbeddedMessage = async (type: string, payload?: any) => {
739
780
  const { windowId } = this.options;
740
781
 
@@ -833,6 +874,13 @@ export default class PresentationWindow {
833
874
  break;
834
875
  }
835
876
 
877
+ case 'analytics-event': {
878
+ if (this.analyticsEventCallback && isValidAnalyticsEvent(payload)) {
879
+ this.analyticsEventCallback(payload);
880
+ }
881
+ break;
882
+ }
883
+
836
884
  default: {
837
885
  this.logWarning(`Received unknown embedded message type: ${type}`);
838
886
  }
@@ -1,8 +1,8 @@
1
1
  import createEmbeddedApp from './embeddedApps';
2
- import { Presentation, ApplicationVersion } from './types';
2
+ import { Presentation, ApplicationVersion, EmbeddedAppMetadata } from './types';
3
3
 
4
4
  const createPresentation = (
5
- overrides: Partial<Presentation>
5
+ overrides: Partial<Presentation> = {}
6
6
  ): Presentation => ({
7
7
  id: 'id',
8
8
  name: 'name',
@@ -21,7 +21,7 @@ const createPresentation = (
21
21
  });
22
22
 
23
23
  const createApplicationVersion = (
24
- overrides: Partial<ApplicationVersion>
24
+ overrides: Partial<ApplicationVersion> = {}
25
25
  ): ApplicationVersion => ({
26
26
  id: 'id',
27
27
  name: 'name',
@@ -33,13 +33,21 @@ const createApplicationVersion = (
33
33
  ...overrides
34
34
  });
35
35
 
36
+ const createMetadata = (
37
+ overrides: Partial<EmbeddedAppMetadata> = {}
38
+ ): EmbeddedAppMetadata => ({
39
+ renderMode: 'live',
40
+ ...overrides
41
+ });
42
+
36
43
  test('Replaces variable in query parameteers', () => {
37
44
  expect(
38
45
  createEmbeddedApp(
39
46
  createPresentation({ applicationVariables: { foo: 'bar' } }),
40
47
  createApplicationVersion({
41
48
  embeddedUrlFormat: 'https://example.com?foo=:foo'
42
- })
49
+ }),
50
+ createMetadata()
43
51
  )
44
52
  ).toBe('https://example.com?foo=bar');
45
53
  });
@@ -50,7 +58,8 @@ test('Replaces variable in path', () => {
50
58
  createPresentation({ applicationVariables: { foo: 'bar' } }),
51
59
  createApplicationVersion({
52
60
  embeddedUrlFormat: 'https://example.com/:foo'
53
- })
61
+ }),
62
+ createMetadata()
54
63
  )
55
64
  ).toBe('https://example.com/bar');
56
65
  });
@@ -61,7 +70,8 @@ test('Replaces undefined variable with empty string', () => {
61
70
  createPresentation({ applicationVariables: {} }),
62
71
  createApplicationVersion({
63
72
  embeddedUrlFormat: 'https://example.com/:foo'
64
- })
73
+ }),
74
+ createMetadata()
65
75
  )
66
76
  ).toBe('https://example.com/');
67
77
  });
@@ -72,7 +82,8 @@ test('Replaces variable with underscore', () => {
72
82
  createPresentation({ applicationVariables: { foo_1: 'bar' } }),
73
83
  createApplicationVersion({
74
84
  embeddedUrlFormat: 'https://example.com/:foo_1'
75
- })
85
+ }),
86
+ createMetadata()
76
87
  )
77
88
  ).toBe('https://example.com/bar');
78
89
  });
@@ -83,7 +94,8 @@ test('Replaces variable with hyphen', () => {
83
94
  createPresentation({ applicationVariables: { 'foo-1': 'bar' } }),
84
95
  createApplicationVersion({
85
96
  embeddedUrlFormat: 'https://example.com/:foo-1'
86
- })
97
+ }),
98
+ createMetadata()
87
99
  )
88
100
  ).toBe('https://example.com/bar');
89
101
  });
@@ -94,7 +106,8 @@ test('Does not replace variable that is a number', () => {
94
106
  createPresentation({ applicationVariables: { foo: 'bar' } }),
95
107
  createApplicationVersion({
96
108
  embeddedUrlFormat: 'http://localhost:8080?foo=:foo'
97
- })
109
+ }),
110
+ createMetadata()
98
111
  )
99
112
  ).toBe('http://localhost:8080?foo=bar');
100
113
  });
@@ -109,7 +122,8 @@ test('Encodes a variables containing special characters', () => {
109
122
  createPresentation({ applicationVariables: { foo: fooValue } }),
110
123
  createApplicationVersion({
111
124
  embeddedUrlFormat: `http://localhost:8080?foo=:foo`
112
- })
125
+ }),
126
+ createMetadata()
113
127
  )
114
128
  ).toBe(`http://localhost:8080?foo=${fooValueEncoded}`);
115
129
  });
@@ -123,7 +137,8 @@ test('Does not encode value if presentation property disables encoding', () => {
123
137
  createApplicationVersion({
124
138
  embeddedUrlFormat: `:url`,
125
139
  presentationProperties: [{ name: 'url', encode: false }]
126
- })
140
+ }),
141
+ createMetadata()
127
142
  )
128
143
  ).toBe(`https://raydiant.com`);
129
144
  });
@@ -138,7 +153,60 @@ test('Encodes a variables containing an array', () => {
138
153
  createPresentation({ applicationVariables: { foo: fooValue } }),
139
154
  createApplicationVersion({
140
155
  embeddedUrlFormat: `http://localhost:8080?foo=:foo`
141
- })
156
+ }),
157
+ createMetadata()
142
158
  )
143
159
  ).toBe(`http://localhost:8080?foo=${fooValueEncoded}`);
144
160
  });
161
+
162
+ test('Replaces $renderMode with metadata', () => {
163
+ const presentation = createPresentation();
164
+ const applicationVersion = createApplicationVersion({
165
+ embeddedUrlFormat: 'https://example.com?renderMode=:$renderMode'
166
+ });
167
+
168
+ expect(
169
+ createEmbeddedApp(
170
+ presentation,
171
+ applicationVersion,
172
+ createMetadata({ renderMode: 'preview' })
173
+ )
174
+ ).toBe('https://example.com?renderMode=preview');
175
+
176
+ expect(
177
+ createEmbeddedApp(
178
+ presentation,
179
+ applicationVersion,
180
+ createMetadata({ renderMode: 'live' })
181
+ )
182
+ ).toBe('https://example.com?renderMode=live');
183
+ });
184
+
185
+ test('Replaces undefined metadata with empty string', () => {
186
+ const presentation = createPresentation();
187
+ const applicationVersion = createApplicationVersion({
188
+ embeddedUrlFormat: 'https://example.com?invalid=:$invalid'
189
+ });
190
+
191
+ expect(
192
+ createEmbeddedApp(presentation, applicationVersion, createMetadata())
193
+ ).toBe('https://example.com?invalid=');
194
+ });
195
+
196
+ test('Replace protocol, ip and port', () => {
197
+ expect(
198
+ createEmbeddedApp(
199
+ createPresentation({
200
+ applicationVariables: {
201
+ protocol: 'http',
202
+ ip: '192.168.1.0',
203
+ port: 3000
204
+ }
205
+ }),
206
+ createApplicationVersion({
207
+ embeddedUrlFormat: `:protocol://:ip::port`
208
+ }),
209
+ createMetadata()
210
+ )
211
+ ).toBe(`http://192.168.1.0:3000`);
212
+ });
@@ -1,11 +1,12 @@
1
- import { Presentation, ApplicationVersion } from './types';
1
+ import { Presentation, ApplicationVersion, EmbeddedAppMetadata } from './types';
2
2
 
3
3
  export default function createEmbeddedApp(
4
4
  presentation: Presentation,
5
- applicationVersion: ApplicationVersion
5
+ applicationVersion: ApplicationVersion,
6
+ metadata: EmbeddedAppMetadata
6
7
  ) {
7
8
  // build matches
8
- const parameterRegex = /(:([\w-]+))(.?$|[^\w-])/gm;
9
+ const parameterRegex = /(:([\w-\$]+))(.?$|[^\w-\$])/gm;
9
10
  const matches = [];
10
11
  let match = null;
11
12
  do {
@@ -23,24 +24,36 @@ export default function createEmbeddedApp(
23
24
  const queryParamName = m[2]; // The query param name. E.g. "location"
24
25
 
25
26
  const [, variableName] = queryParamVariableName.split(':');
26
- // Don't replace variables that are numbers to allow setting
27
- // port numbers in the embedded app url.
28
- if (!isNaN(Number(variableName))) return;
29
-
30
- const presentationProperty = applicationVersion.presentationProperties.find(
31
- p => p.name === variableName
32
- );
33
-
34
- // If the presentation property is not found (this shouldn't happen), default to encoding the value.
35
- // Otherwise, only skip encoding if the presentation property's encode property is false.
36
- const shouldEncodeValue =
37
- !presentationProperty || presentationProperty.encode !== false;
38
-
39
- let value = presentation.applicationVariables[queryParamName];
40
- if (value === undefined) value = '';
41
- if (Array.isArray(value)) {
42
- value = JSON.stringify(value);
27
+
28
+ let shouldEncodeValue = true;
29
+ let value: any = '';
30
+
31
+ if (variableName.charAt(0) === '$') {
32
+ const metadataKey = variableName.slice(1);
33
+ if (metadataKey === 'renderMode') {
34
+ value = metadata.renderMode;
35
+ }
36
+ } else {
37
+ // Don't replace variables that are numbers to allow setting
38
+ // port numbers in the embedded app url.
39
+ if (!isNaN(Number(variableName))) return;
40
+
41
+ const presentationProperty = applicationVersion.presentationProperties.find(
42
+ p => p.name === variableName
43
+ );
44
+
45
+ // If the presentation property is not found (this shouldn't happen), default to encoding the value.
46
+ // Otherwise, only skip encoding if the presentation property's encode property is false.
47
+ shouldEncodeValue =
48
+ !presentationProperty || presentationProperty.encode !== false;
49
+
50
+ value = presentation.applicationVariables[queryParamName];
51
+ if (value === undefined) value = '';
52
+ if (Array.isArray(value)) {
53
+ value = JSON.stringify(value);
54
+ }
43
55
  }
56
+
44
57
  if (shouldEncodeValue) {
45
58
  value = encodeURIComponent(value);
46
59
  }
package/src/types.ts CHANGED
@@ -152,3 +152,13 @@ export interface InputState {
152
152
  path: Path;
153
153
  state: any;
154
154
  }
155
+
156
+ export interface EmbeddedAppMetadata {
157
+ renderMode: 'preview' | 'live';
158
+ }
159
+
160
+ export interface AnalyticsEvent {
161
+ type: string;
162
+ source: string;
163
+ productCategory?: string | null;
164
+ }