@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.
- package/build/PlaylistWindowManager.d.ts +1 -0
- package/build/PlaylistWindowManager.js +7 -0
- package/build/PresentationWindow.d.ts +4 -1
- package/build/PresentationWindow.js +38 -4
- package/build/embeddedApps.d.ts +2 -2
- package/build/embeddedApps.js +26 -15
- package/build/embeddedApps.test.js +53 -17
- package/build/types.d.ts +8 -0
- package/package.json +1 -1
- package/src/PlaylistWindowManager.ts +11 -1
- package/src/PresentationWindow.ts +53 -5
- package/src/embeddedApps.test.ts +80 -12
- package/src/embeddedApps.ts +33 -20
- package/src/types.ts +10 -0
|
@@ -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 =
|
|
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
|
-
|
|
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 =
|
|
543
|
+
url = this.getEmbeddedAppUrl();
|
|
521
544
|
this.logInfo("Create window '" + windowId + "' for embedded app url '" + url + "'");
|
|
522
|
-
return [4 /*yield*/, this.createAndLoadWindow(
|
|
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();
|
package/build/embeddedApps.d.ts
CHANGED
|
@@ -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;
|
package/build/embeddedApps.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
if (
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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) {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
@@ -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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
}
|
package/src/embeddedApps.test.ts
CHANGED
|
@@ -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
|
+
});
|
package/src/embeddedApps.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
+
}
|