@raydiant/playlist-window-manager 0.0.0-electron-upgrade-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/.eslintrc +6 -0
- package/README.md +16 -0
- package/babel.config.js +13 -0
- package/build/BrowserViewRenderer.d.ts +19 -0
- package/build/BrowserViewRenderer.js +213 -0
- package/build/BrowserWindowRenderer.d.ts +47 -0
- package/build/BrowserWindowRenderer.js +524 -0
- package/build/IFrameRenderer.d.ts +20 -0
- package/build/IFrameRenderer.js +226 -0
- package/build/PlaylistWindowManager.d.ts +111 -0
- package/build/PlaylistWindowManager.js +1056 -0
- package/build/PlaylistWindowManagerState.d.ts +28 -0
- package/build/PlaylistWindowManagerState.js +60 -0
- package/build/PresentationWindow.d.ts +149 -0
- package/build/PresentationWindow.js +1235 -0
- package/build/PresentationWindow.test.d.ts +1 -0
- package/build/PresentationWindow.test.js +340 -0
- package/build/WindowLoadError.d.ts +3 -0
- package/build/WindowLoadError.js +29 -0
- package/build/WindowTimeoutError.d.ts +3 -0
- package/build/WindowTimeoutError.js +29 -0
- package/build/WindowUnresponsiveError.d.ts +3 -0
- package/build/WindowUnresponsiveError.js +29 -0
- package/build/embeddedApps.d.ts +2 -0
- package/build/embeddedApps.js +59 -0
- package/build/embeddedApps.test.d.ts +1 -0
- package/build/embeddedApps.test.js +123 -0
- package/build/index.d.ts +10 -0
- package/build/index.js +30 -0
- package/build/preload.d.ts +1 -0
- package/build/preload.js +20 -0
- package/build/testUtils.d.ts +43 -0
- package/build/testUtils.js +209 -0
- package/build/types.d.ts +162 -0
- package/build/types.js +2 -0
- package/jest.config.js +6 -0
- package/package.json +30 -0
- package/src/BrowserViewRenderer.ts +180 -0
- package/src/BrowserWindowRenderer.ts +595 -0
- package/src/IFrameRenderer.ts +251 -0
- package/src/PlaylistWindowManager.ts +1094 -0
- package/src/PlaylistWindowManagerState.ts +70 -0
- package/src/PresentationWindow.test.ts +333 -0
- package/src/PresentationWindow.ts +1434 -0
- package/src/WindowLoadError.ts +8 -0
- package/src/WindowTimeoutError.ts +8 -0
- package/src/WindowUnresponsiveError.ts +8 -0
- package/src/embeddedApps.test.ts +231 -0
- package/src/embeddedApps.ts +74 -0
- package/src/index.ts +13 -0
- package/src/preload.ts +27 -0
- package/src/testUtils.ts +160 -0
- package/src/types.ts +199 -0
- package/tasks/npm-publish.sh +13 -0
- package/tsconfig.json +7 -0
|
@@ -0,0 +1,1434 @@
|
|
|
1
|
+
import * as RE from '@raydiant/playlist-rule-engine';
|
|
2
|
+
import deepEqual from 'fast-deep-equal';
|
|
3
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
4
|
+
import createEmbeddedApp from './embeddedApps';
|
|
5
|
+
import PlaylistWindowManagerState from './PlaylistWindowManagerState';
|
|
6
|
+
import PlaylistWindowManager from './PlaylistWindowManager';
|
|
7
|
+
import WindowTimeoutError from './WindowTimeoutError';
|
|
8
|
+
import WindowLoadError from './WindowLoadError';
|
|
9
|
+
import {
|
|
10
|
+
Presentation,
|
|
11
|
+
ApplicationVersion,
|
|
12
|
+
Theme,
|
|
13
|
+
Device,
|
|
14
|
+
PresentationWindowRenderer,
|
|
15
|
+
WindowBounds,
|
|
16
|
+
Logger,
|
|
17
|
+
OpenChildWindowOptions,
|
|
18
|
+
BuilderState,
|
|
19
|
+
WorkerState,
|
|
20
|
+
EmbeddedAppMetadata,
|
|
21
|
+
AnalyticsEvent,
|
|
22
|
+
PreviewState,
|
|
23
|
+
Charge,
|
|
24
|
+
ChargeRequest,
|
|
25
|
+
ChargeResponse,
|
|
26
|
+
Terminal,
|
|
27
|
+
} from './types';
|
|
28
|
+
|
|
29
|
+
export interface PresentationWindowOptions {
|
|
30
|
+
windowManager?: PlaylistWindowManager;
|
|
31
|
+
windowId: string;
|
|
32
|
+
windowName?: string;
|
|
33
|
+
parentWindowId?: string;
|
|
34
|
+
builderState?: BuilderState;
|
|
35
|
+
presentation: Presentation;
|
|
36
|
+
applicationVersion: ApplicationVersion;
|
|
37
|
+
theme?: Theme;
|
|
38
|
+
device?: Device;
|
|
39
|
+
playlistId?: string;
|
|
40
|
+
playlistName?: string;
|
|
41
|
+
rule?: RE.Rule | null;
|
|
42
|
+
publishedAt: string;
|
|
43
|
+
playlistWindowManagerState: PlaylistWindowManagerState;
|
|
44
|
+
ruleEngineState: RE.State;
|
|
45
|
+
logger: Logger;
|
|
46
|
+
globalRule?: RE.Rule;
|
|
47
|
+
renderer: PresentationWindowRenderer;
|
|
48
|
+
readyTimeoutDurationSeconds?: number;
|
|
49
|
+
appRendererUrl?: string;
|
|
50
|
+
bounds?: WindowBounds;
|
|
51
|
+
backgroundColor?: string;
|
|
52
|
+
isPreview?: boolean;
|
|
53
|
+
forceMute?: boolean;
|
|
54
|
+
workerState: WorkerState;
|
|
55
|
+
onPresentationProperties?: (
|
|
56
|
+
properties: any[],
|
|
57
|
+
string: { [key: string]: string }
|
|
58
|
+
) => void;
|
|
59
|
+
getTerminal?: () => Terminal | null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// TODO: Automatically inject latest version when building in CI.
|
|
63
|
+
const defaultAppRendererUrl = 'https://apps.raydiant.com/3.0.4/index.html';
|
|
64
|
+
|
|
65
|
+
const isValidNumber = (value: any) =>
|
|
66
|
+
typeof value === 'number' && !isNaN(value);
|
|
67
|
+
|
|
68
|
+
const isValidAnalyticsEvent = (value: any): value is AnalyticsEvent =>
|
|
69
|
+
value &&
|
|
70
|
+
typeof value === 'object' &&
|
|
71
|
+
typeof value.type === 'string' &&
|
|
72
|
+
typeof value.source === 'string' &&
|
|
73
|
+
(typeof value.productCategory === 'string' ||
|
|
74
|
+
value.productCategory === undefined ||
|
|
75
|
+
value.productCategory === null);
|
|
76
|
+
|
|
77
|
+
const isValidChargeRequest = (value: any): value is ChargeRequest =>
|
|
78
|
+
value &&
|
|
79
|
+
typeof value === 'object' &&
|
|
80
|
+
typeof value.requestId === 'string' &&
|
|
81
|
+
typeof value.amount === 'number' &&
|
|
82
|
+
(value.tags === undefined ||
|
|
83
|
+
value.tags === null ||
|
|
84
|
+
typeof value.tags === 'object') &&
|
|
85
|
+
(value.idempotencyId === undefined ||
|
|
86
|
+
value.idempotencyId === null ||
|
|
87
|
+
typeof value.idempotencyId === 'string');
|
|
88
|
+
|
|
89
|
+
export default class PresentationWindow {
|
|
90
|
+
private didReceiveReady: boolean = false;
|
|
91
|
+
private readyCallback: (() => void) | null = null;
|
|
92
|
+
private didReceiveComplete: boolean = false;
|
|
93
|
+
private completeCallback: (() => void) | null = null;
|
|
94
|
+
private errorCallback: ((err: Error) => void) | null = null;
|
|
95
|
+
private readyTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
96
|
+
private windowManagers: { [windowId: string]: PlaylistWindowManager } = {};
|
|
97
|
+
private focusedWindowManager: PlaylistWindowManager | null = null;
|
|
98
|
+
private didEmbeddedAppLoad: boolean = false;
|
|
99
|
+
private lastPlayedAt: Date | null = null;
|
|
100
|
+
private configurableDurationTimeout: ReturnType<typeof setTimeout> | null =
|
|
101
|
+
null;
|
|
102
|
+
private isWindowLocked: boolean = false;
|
|
103
|
+
private isChildWindowLocked: { [windowId: string]: boolean } = {};
|
|
104
|
+
private enableInterruptLockCallback: (() => void) | null = null;
|
|
105
|
+
private disableInterruptLockCallback: (() => void) | null = null;
|
|
106
|
+
private analyticsEventCallback: ((event: AnalyticsEvent) => void) | null =
|
|
107
|
+
null;
|
|
108
|
+
private chargeRequestCallback: ((request: ChargeRequest) => void) | null =
|
|
109
|
+
null;
|
|
110
|
+
|
|
111
|
+
private builderState: BuilderState | undefined;
|
|
112
|
+
private presentation: Presentation;
|
|
113
|
+
private playlistWindowManagerState: PlaylistWindowManagerState;
|
|
114
|
+
private ruleEngineState: RE.State;
|
|
115
|
+
private theme: Theme | undefined;
|
|
116
|
+
private forceMute: boolean = false;
|
|
117
|
+
private error: Error | null = null;
|
|
118
|
+
|
|
119
|
+
private workerState: WorkerState = {};
|
|
120
|
+
private workerSubscriptions = new Set<string>();
|
|
121
|
+
private runtimeSubscriptions = new Set<string>();
|
|
122
|
+
private previewSubscriptions = new Set<string>();
|
|
123
|
+
private previewState: PreviewState = { inputs: {} };
|
|
124
|
+
private paymentSubscriptions = new Set<string>();
|
|
125
|
+
|
|
126
|
+
private createObjectURLResolvers: Record<string, (url: string) => void> = {};
|
|
127
|
+
private createObjectURLTimeouts: Record<
|
|
128
|
+
string,
|
|
129
|
+
ReturnType<typeof setTimeout>
|
|
130
|
+
> = {};
|
|
131
|
+
|
|
132
|
+
constructor(private options: PresentationWindowOptions) {
|
|
133
|
+
this.builderState = options.builderState;
|
|
134
|
+
this.presentation = options.presentation;
|
|
135
|
+
this.playlistWindowManagerState = options.playlistWindowManagerState;
|
|
136
|
+
this.ruleEngineState = options.ruleEngineState;
|
|
137
|
+
this.theme = options.theme;
|
|
138
|
+
this.forceMute = options.forceMute || false;
|
|
139
|
+
this.workerState = options.workerState;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
public load(): Promise<void> {
|
|
143
|
+
if (this.isEmbeddedApp()) {
|
|
144
|
+
return this.loadEmbeddedApp();
|
|
145
|
+
}
|
|
146
|
+
return this.loadLegacyApp();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
public show(isFocused: boolean = true): void {
|
|
150
|
+
const { windowId, renderer } = this.options;
|
|
151
|
+
|
|
152
|
+
// Show the main window and focus it before any child windows. Showing a child
|
|
153
|
+
// window that requires focus will move focus from the main window to the child.
|
|
154
|
+
renderer.showWindow(windowId, isFocused);
|
|
155
|
+
|
|
156
|
+
// Windows are created muted, check if we should unmute the window when showing.
|
|
157
|
+
// This ensures presentations that use audio take precendence over SYB playback on device.
|
|
158
|
+
// This checks the presentation for mute properties when shown but does not handle the case
|
|
159
|
+
// when an app becomes unmuted or muted while running (ie. someone interacting with a video player).
|
|
160
|
+
// A possible better solution would be to have apps fire an event when they need audio.
|
|
161
|
+
const isMuted = this.shouldMute();
|
|
162
|
+
if (!isMuted) {
|
|
163
|
+
renderer.unMuteWindow(windowId);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Show child windows after main window, setting focus if necessary.
|
|
167
|
+
let isChildWindowFocused = false;
|
|
168
|
+
Object.values(this.windowManagers).map((wm) => {
|
|
169
|
+
if (
|
|
170
|
+
isFocused &&
|
|
171
|
+
this.focusedWindowManager &&
|
|
172
|
+
this.focusedWindowManager === wm
|
|
173
|
+
) {
|
|
174
|
+
isChildWindowFocused = true;
|
|
175
|
+
wm.show(true);
|
|
176
|
+
} else {
|
|
177
|
+
wm.show(false);
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
this.logInfo(
|
|
182
|
+
`Show window '${windowId}' isMuted=${isMuted} isFocused=${isFocused}${
|
|
183
|
+
isChildWindowFocused ? ` isChildWindowFocused=true` : ''
|
|
184
|
+
}`
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
public hide(): void {
|
|
189
|
+
const { windowId, renderer } = this.options;
|
|
190
|
+
// Hide child windows before main window.
|
|
191
|
+
Object.values(this.windowManagers).map((wm) => wm.hide());
|
|
192
|
+
renderer.hideWindow(windowId);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
public async remove(): Promise<void> {
|
|
196
|
+
const { windowId, renderer } = this.options;
|
|
197
|
+
|
|
198
|
+
// Clean up callbacks
|
|
199
|
+
this.readyCallback = null;
|
|
200
|
+
this.completeCallback = null;
|
|
201
|
+
this.errorCallback = null;
|
|
202
|
+
this.enableInterruptLockCallback = null;
|
|
203
|
+
this.disableInterruptLockCallback = null;
|
|
204
|
+
|
|
205
|
+
// Clean up subscriptions
|
|
206
|
+
this.workerSubscriptions.clear();
|
|
207
|
+
|
|
208
|
+
// Clear timers
|
|
209
|
+
if (this.readyTimeout) {
|
|
210
|
+
clearTimeout(this.readyTimeout);
|
|
211
|
+
this.readyTimeout = null;
|
|
212
|
+
}
|
|
213
|
+
if (this.configurableDurationTimeout) {
|
|
214
|
+
clearTimeout(this.configurableDurationTimeout);
|
|
215
|
+
this.configurableDurationTimeout = null;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Remove any child window managers.
|
|
219
|
+
Object.values(this.windowManagers).forEach((wm) => {
|
|
220
|
+
wm.destroy();
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// Remove the main window after removing all the child window managers.
|
|
224
|
+
try {
|
|
225
|
+
await renderer.removeWindow(windowId);
|
|
226
|
+
this.logInfo(`Removed window '${windowId}'`);
|
|
227
|
+
} catch (err: any) {
|
|
228
|
+
this.logError(
|
|
229
|
+
`Failed to remove window '${windowId}' with '${err.message}'`
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Method to modify the rule engine state for the inner playlist windows (called from outer playlist window manager)
|
|
235
|
+
public reinitializePresentationWindowRuleEngineState(
|
|
236
|
+
newRulesDataStoreObj: RE.RulesDataStore
|
|
237
|
+
) {
|
|
238
|
+
Object.values(this.windowManagers).forEach((wm) => {
|
|
239
|
+
wm.reinitializeRuleEngineState(newRulesDataStoreObj);
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
public play(): void {
|
|
244
|
+
const { applicationVersion, presentation } = this.options;
|
|
245
|
+
|
|
246
|
+
if (applicationVersion.configurableDuration) {
|
|
247
|
+
const durationMs = Math.max(presentation.duration * 1000, 5000);
|
|
248
|
+
if (this.configurableDurationTimeout) {
|
|
249
|
+
clearTimeout(this.configurableDurationTimeout);
|
|
250
|
+
}
|
|
251
|
+
this.configurableDurationTimeout = setTimeout(() => {
|
|
252
|
+
if (this.completeCallback) {
|
|
253
|
+
this.completeCallback();
|
|
254
|
+
}
|
|
255
|
+
}, durationMs);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (!this.isEmbeddedApp()) {
|
|
259
|
+
this.sendMessageString('play');
|
|
260
|
+
|
|
261
|
+
// Play any child window managers.
|
|
262
|
+
Object.values(this.windowManagers).forEach((wm) => {
|
|
263
|
+
// We only want to trigger play on child windows the first play of the main window or if
|
|
264
|
+
// the child window is paused.
|
|
265
|
+
if (!wm.isPlaying() || wm.isPaused()) {
|
|
266
|
+
wm.play();
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
} else if (this.runtimeSubscriptions.has('play')) {
|
|
270
|
+
// If the embedded app has subscribed to the play event, send it a play event.
|
|
271
|
+
this.sendMessageString('play');
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
this.lastPlayedAt = new Date();
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
public pause() {
|
|
278
|
+
// Pause all child window managers.
|
|
279
|
+
Object.values(this.windowManagers).forEach((wm) => wm.pause());
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
public resume() {
|
|
283
|
+
// Resume all child window managers.
|
|
284
|
+
Object.values(this.windowManagers).forEach((wm) => wm.resume());
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
public reset(): void {
|
|
288
|
+
this.didReceiveComplete = false;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
public onComplete(callback: (() => void) | null) {
|
|
292
|
+
this.completeCallback = callback;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
public onError(callback: ((err: Error) => void) | null) {
|
|
296
|
+
this.errorCallback = callback;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
public onEnableInterruptLock(callback: (() => void) | null) {
|
|
300
|
+
this.enableInterruptLockCallback = callback;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
public onDisableInterruptLock(callback: (() => void) | null) {
|
|
304
|
+
this.disableInterruptLockCallback = callback;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
public onAnalyticsEvent(callback: ((event: AnalyticsEvent) => void) | null) {
|
|
308
|
+
this.analyticsEventCallback = callback;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
public onChargeRequest(callback: ((request: ChargeRequest) => void) | null) {
|
|
312
|
+
this.chargeRequestCallback = callback;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
public isReady() {
|
|
316
|
+
// TODO: To support the 'ready' event in embedded apps, they will need to tell the
|
|
317
|
+
// runtime that it should wait for ready instead of DOMLoaded. This could be a "wait
|
|
318
|
+
// for ready" option in the Developer Portal.
|
|
319
|
+
if (this.isEmbeddedApp()) {
|
|
320
|
+
return this.didEmbeddedAppLoad;
|
|
321
|
+
}
|
|
322
|
+
return this.didReceiveReady && this.allChildWindowManagersReady();
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
public isComplete() {
|
|
326
|
+
return this.didReceiveComplete;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
public isPlaying() {
|
|
330
|
+
return this.lastPlayedAt !== null;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
public updateBounds(bounds: WindowBounds) {
|
|
334
|
+
const { windowId, renderer } = this.options;
|
|
335
|
+
renderer.updateWindowBounds(windowId, bounds);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
public didError() {
|
|
339
|
+
return !!this.error;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
public isLocked() {
|
|
343
|
+
return (
|
|
344
|
+
this.isWindowLocked ||
|
|
345
|
+
Object.values(this.isChildWindowLocked).some(Boolean)
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
public setPresentation(
|
|
350
|
+
presentation: Presentation,
|
|
351
|
+
playlistWindowManagerState: PlaylistWindowManagerState,
|
|
352
|
+
ruleEngineState: RE.State
|
|
353
|
+
) {
|
|
354
|
+
this.presentation = presentation;
|
|
355
|
+
this.ruleEngineState = ruleEngineState;
|
|
356
|
+
this.playlistWindowManagerState = playlistWindowManagerState;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
public setTheme(theme?: Theme) {
|
|
360
|
+
this.theme = theme;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
public setBuilderState(builderState?: BuilderState) {
|
|
364
|
+
this.builderState = builderState;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
public setForceMute(forceMute: boolean) {
|
|
368
|
+
this.forceMute = forceMute;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
public async reload() {
|
|
372
|
+
const { applicationVersion, windowId } = this.options;
|
|
373
|
+
|
|
374
|
+
if (this.isEmbeddedApp()) {
|
|
375
|
+
if (applicationVersion.embeddedUrlFormat) {
|
|
376
|
+
if (this.previewSubscriptions.has('inputs')) {
|
|
377
|
+
this.logInfo(
|
|
378
|
+
`Reload window '${windowId}' for embedded app preview state`
|
|
379
|
+
);
|
|
380
|
+
|
|
381
|
+
const updatedInputs = transformInputValues(
|
|
382
|
+
this.presentation.applicationVariables
|
|
383
|
+
);
|
|
384
|
+
|
|
385
|
+
if (!deepEqual(updatedInputs, this.previewState.inputs)) {
|
|
386
|
+
this.sendMessageObject('preview-inputs', updatedInputs);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
this.previewState.inputs = updatedInputs;
|
|
390
|
+
} else {
|
|
391
|
+
const url = this.getEmbeddedAppUrl();
|
|
392
|
+
|
|
393
|
+
this.logInfo(
|
|
394
|
+
`Reload window '${windowId}' for embedded app url '${url}'`
|
|
395
|
+
);
|
|
396
|
+
await this.loadWindow(url);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
} else {
|
|
400
|
+
this.logInfo(`Reload window '${windowId}'`);
|
|
401
|
+
this.sendRenderMessage();
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
public runGetProperties(builderState?: BuilderState) {
|
|
406
|
+
// Do no support conditional properties for embedded apps. We will likely
|
|
407
|
+
// change the way conditional properties work.
|
|
408
|
+
if (!this.isEmbeddedApp()) {
|
|
409
|
+
this.sendMessageString('run_get_properties', builderState);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
public async createObjectURL(blob: Blob): Promise<string> {
|
|
414
|
+
if (this.isEmbeddedApp()) {
|
|
415
|
+
return this.createObjectURLForEmbeddedApp(blob);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
return URL.createObjectURL(blob);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
public sendChargeResponse(response: ChargeResponse) {
|
|
422
|
+
// TODO: Instead of sending the charge to all windows, we should only send it to the app
|
|
423
|
+
// that requested the charge. We probably don't want to use the windowId to avoid missing
|
|
424
|
+
// the events in case the window was closed/restarted. Since we are only sending the charge
|
|
425
|
+
// id and status this seems ok for now but need to be mindful of sending any sensitive data.
|
|
426
|
+
this.sendMessageObject('payments-charge-response', {
|
|
427
|
+
requestId: response.requestId,
|
|
428
|
+
charge: response.charge,
|
|
429
|
+
error: response.error,
|
|
430
|
+
});
|
|
431
|
+
// Forward the event to the child window managers.
|
|
432
|
+
Object.values(this.windowManagers).forEach((wm) =>
|
|
433
|
+
wm.sendChargeResponse(response)
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
public sendCharge(charge: Charge) {
|
|
438
|
+
// TODO: Instead of sending the charge to all windows, we should only send it to the app
|
|
439
|
+
// that requested the charge.
|
|
440
|
+
if (this.paymentSubscriptions.has('charge')) {
|
|
441
|
+
this.sendMessageObject('payments-charge-event', { charge });
|
|
442
|
+
}
|
|
443
|
+
// Forward the event to the child window managers.
|
|
444
|
+
Object.values(this.windowManagers).forEach((wm) => wm.sendCharge(charge));
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
public sendTerminal(terminal: Terminal | null) {
|
|
448
|
+
if (this.paymentSubscriptions.has('terminal')) {
|
|
449
|
+
this.sendMessageObject('payments-terminal-event', { terminal });
|
|
450
|
+
}
|
|
451
|
+
// Forward the event to the child window managers.
|
|
452
|
+
Object.values(this.windowManagers).forEach((wm) =>
|
|
453
|
+
wm.sendTerminal(terminal)
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
private isEmbeddedApp() {
|
|
458
|
+
const { applicationVersion, isPreview } = this.options;
|
|
459
|
+
|
|
460
|
+
// If the application has a source url, always use it for rendering the preview.
|
|
461
|
+
// This fixes an issue with Hoopla which is an embedded app but uses a legacy app
|
|
462
|
+
// for it's preview.
|
|
463
|
+
if (isPreview && applicationVersion.sourceUrl) {
|
|
464
|
+
return false;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
return !!applicationVersion.embeddedUrlFormat;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
private allChildWindowManagersReady(): boolean {
|
|
471
|
+
// Return true if the presentation doesn't contain any playlists.
|
|
472
|
+
if (this.presentation.r.presentationsPlaylists.length === 0) {
|
|
473
|
+
return true;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
return Object.values(this.windowManagers).every((wm) => wm.isReady());
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
private checkIsReadyCallback() {
|
|
480
|
+
if (this.isReady()) {
|
|
481
|
+
if (this.readyTimeout) {
|
|
482
|
+
clearTimeout(this.readyTimeout);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
if (this.readyCallback) {
|
|
486
|
+
this.readyCallback();
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Trigger the enable interrupt lock callback if the presentation fired
|
|
490
|
+
// enable_interrupt_lock before presentation_ready.
|
|
491
|
+
if (this.isLocked() && this.enableInterruptLockCallback) {
|
|
492
|
+
this.enableInterruptLockCallback();
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
private appendWindowIdToHash(urlStr: string, windowId: string) {
|
|
498
|
+
const url = new URL(urlStr);
|
|
499
|
+
const hashParams = new URLSearchParams(url.hash.slice(1));
|
|
500
|
+
hashParams.set('__raydiantWindowId', windowId);
|
|
501
|
+
url.hash = hashParams.toString();
|
|
502
|
+
|
|
503
|
+
return url.toString();
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
private async loadEmbeddedApp(): Promise<void> {
|
|
507
|
+
const { windowId, applicationVersion } = this.options;
|
|
508
|
+
|
|
509
|
+
if (!applicationVersion.embeddedUrlFormat) {
|
|
510
|
+
throw new Error(
|
|
511
|
+
`Error loading embedded app, missing embeddedUrlFormat for '${applicationVersion.id}'`
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
const url = this.getEmbeddedAppUrl();
|
|
516
|
+
this.logInfo(`Create window '${windowId}' for embedded app url '${url}'`);
|
|
517
|
+
|
|
518
|
+
await this.createAndLoadWindow(
|
|
519
|
+
url,
|
|
520
|
+
// Embedded apps should not have a backgroundColor because some sites expect
|
|
521
|
+
// the browser default of white will be applied.
|
|
522
|
+
'#ffffff',
|
|
523
|
+
this.handleEmbeddedMessage
|
|
524
|
+
);
|
|
525
|
+
this.didEmbeddedAppLoad = true;
|
|
526
|
+
|
|
527
|
+
this.checkIsReadyCallback();
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
private async loadLegacyApp(): Promise<void> {
|
|
531
|
+
const {
|
|
532
|
+
windowId,
|
|
533
|
+
appRendererUrl = defaultAppRendererUrl,
|
|
534
|
+
backgroundColor,
|
|
535
|
+
} = this.options;
|
|
536
|
+
|
|
537
|
+
this.logInfo(`Create window '${windowId}'`);
|
|
538
|
+
|
|
539
|
+
await this.createAndLoadWindow(
|
|
540
|
+
this.appendWindowIdToHash(appRendererUrl, windowId),
|
|
541
|
+
backgroundColor || '#000000', // Default to black bg for Raydiant apps
|
|
542
|
+
this.handleLegacyMessage
|
|
543
|
+
);
|
|
544
|
+
|
|
545
|
+
await this.waitForPesentationReady();
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
private async createAndLoadWindow(
|
|
549
|
+
url: string,
|
|
550
|
+
backgroundColor?: string,
|
|
551
|
+
receiveMessageCallback?: (type: string, payload?: any) => void
|
|
552
|
+
) {
|
|
553
|
+
const { windowId, parentWindowId, renderer, bounds } = this.options;
|
|
554
|
+
|
|
555
|
+
renderer.createWindow({
|
|
556
|
+
windowId,
|
|
557
|
+
parentWindowId,
|
|
558
|
+
bounds,
|
|
559
|
+
backgroundColor,
|
|
560
|
+
receiveMessageCallback,
|
|
561
|
+
errorCallback: this.handleError,
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
// Mute new window. This ensures preloaded windows are muted by default and
|
|
565
|
+
// prevents opening an audio channel on the device for SYB playback. When using
|
|
566
|
+
// the BrowserWindowRenderer this will call setAudioMuted(true) but the IFrameRenderer
|
|
567
|
+
// has no API for muting so we rely on apps that use mute presentation props to be
|
|
568
|
+
// initially muted before playing.
|
|
569
|
+
this.logInfo(`Mute window ${windowId}`);
|
|
570
|
+
renderer.muteWindow(windowId);
|
|
571
|
+
|
|
572
|
+
await this.loadWindow(url);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
private async loadWindow(url: string) {
|
|
576
|
+
const { windowId, renderer } = this.options;
|
|
577
|
+
try {
|
|
578
|
+
await renderer.loadWindow(windowId, url);
|
|
579
|
+
this.logInfo(`Loaded window '${windowId}'`);
|
|
580
|
+
} catch (err: any) {
|
|
581
|
+
this.handleError(
|
|
582
|
+
new WindowLoadError(`Failed to load window with '${err.message}'`)
|
|
583
|
+
);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// Send a JSON stringified message to the renderer. This is the legacy way of sending
|
|
588
|
+
// messages and should be deprecated in favor of sendMessageObject.
|
|
589
|
+
private sendMessageString(type: string, payload?: any) {
|
|
590
|
+
const { windowId, renderer } = this.options;
|
|
591
|
+
const message = JSON.stringify({ type, payload, windowId });
|
|
592
|
+
|
|
593
|
+
this.logInfo(`Sending '${type}' to window '${windowId}'`);
|
|
594
|
+
renderer.sendMessageString(windowId, message);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// Send a message object to the renderer. This is the preferred way of sending messages
|
|
598
|
+
// going forward since it allows for more complex payloads like ArrayBuffers (for File Inputs).
|
|
599
|
+
private sendMessageObject(type: string, payload?: any) {
|
|
600
|
+
const { windowId, renderer } = this.options;
|
|
601
|
+
|
|
602
|
+
this.logInfo(`Sending '${type}' to window '${windowId}'`);
|
|
603
|
+
renderer.sendMessageObject(windowId, type, payload);
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
private waitForPesentationReady(): Promise<void> {
|
|
607
|
+
const { windowId, readyTimeoutDurationSeconds = 30 } = this.options;
|
|
608
|
+
|
|
609
|
+
return new Promise<void>((resolve) => {
|
|
610
|
+
if (this.didReceiveReady) {
|
|
611
|
+
resolve();
|
|
612
|
+
} else {
|
|
613
|
+
this.readyCallback = () => {
|
|
614
|
+
resolve();
|
|
615
|
+
};
|
|
616
|
+
|
|
617
|
+
if (this.readyTimeout) {
|
|
618
|
+
clearTimeout(this.readyTimeout);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
this.readyTimeout = setTimeout(() => {
|
|
622
|
+
this.handleError(
|
|
623
|
+
new WindowTimeoutError(
|
|
624
|
+
`Window ${windowId} timed out after ${readyTimeoutDurationSeconds} seconds`
|
|
625
|
+
)
|
|
626
|
+
);
|
|
627
|
+
// Resolve the promise is the window times out. This ensures windowManager.load() resolves
|
|
628
|
+
// even if the current window times out. Fixes an issue where the boot screen isn't removed
|
|
629
|
+
// if the first presentation times out.
|
|
630
|
+
resolve();
|
|
631
|
+
}, readyTimeoutDurationSeconds * 1000);
|
|
632
|
+
}
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
private buildTheme(): Theme | undefined {
|
|
637
|
+
if (!this.theme) return;
|
|
638
|
+
|
|
639
|
+
const nextTheme = { ...this.theme };
|
|
640
|
+
|
|
641
|
+
if (nextTheme.headingFontNew && nextTheme.headingFontNew.fileUpload) {
|
|
642
|
+
nextTheme.headingFont =
|
|
643
|
+
nextTheme.headingFontNew.fileUpload.url || nextTheme.headingFont;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
if (nextTheme.heading2FontNew && nextTheme.heading2FontNew.fileUpload) {
|
|
647
|
+
nextTheme.heading2Font =
|
|
648
|
+
nextTheme.heading2FontNew.fileUpload.url || nextTheme.heading2Font;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
if (nextTheme.bodyFontNew && nextTheme.bodyFontNew.fileUpload) {
|
|
652
|
+
nextTheme.bodyFont =
|
|
653
|
+
nextTheme.bodyFontNew.fileUpload.url || nextTheme.bodyFont;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
if (nextTheme.backgroundImageFileUpload) {
|
|
657
|
+
nextTheme.backgroundImage =
|
|
658
|
+
nextTheme.backgroundImageFileUpload.url || nextTheme.backgroundImage;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
return nextTheme;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
private sendRenderMessage() {
|
|
665
|
+
const { applicationVersion, isPreview, device } = this.options;
|
|
666
|
+
// IFrames do not have a mute/unmute API so we are overriding any mute properties
|
|
667
|
+
// to support muting MZ zones when using the IFrameRenderer (SecondScreen).
|
|
668
|
+
if (this.forceMute) {
|
|
669
|
+
this.sendMessageString('render', {
|
|
670
|
+
builderState: this.builderState,
|
|
671
|
+
presentation: {
|
|
672
|
+
...this.presentation,
|
|
673
|
+
applicationVariables: {
|
|
674
|
+
...this.presentation.applicationVariables,
|
|
675
|
+
// TODO: We need a better way to identify mute properties (ie. PropType.mute())
|
|
676
|
+
// Force mute the presentation, existing apps use either mute or muted.
|
|
677
|
+
mute: true,
|
|
678
|
+
muted: true,
|
|
679
|
+
},
|
|
680
|
+
},
|
|
681
|
+
theme: this.buildTheme(),
|
|
682
|
+
applicationVersion,
|
|
683
|
+
device,
|
|
684
|
+
isPreview,
|
|
685
|
+
});
|
|
686
|
+
} else {
|
|
687
|
+
this.sendMessageString('render', {
|
|
688
|
+
builderState: this.builderState,
|
|
689
|
+
presentation: this.presentation,
|
|
690
|
+
theme: this.buildTheme(),
|
|
691
|
+
applicationVersion,
|
|
692
|
+
device,
|
|
693
|
+
isPreview,
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
private async openChildWindow(
|
|
699
|
+
windowName: string,
|
|
700
|
+
playlistId: string,
|
|
701
|
+
bounds: WindowBounds,
|
|
702
|
+
forceMute: boolean = false,
|
|
703
|
+
isFocused: boolean = false,
|
|
704
|
+
themeOverride?: any
|
|
705
|
+
) {
|
|
706
|
+
const {
|
|
707
|
+
windowId,
|
|
708
|
+
renderer,
|
|
709
|
+
device,
|
|
710
|
+
readyTimeoutDurationSeconds,
|
|
711
|
+
appRendererUrl,
|
|
712
|
+
logger,
|
|
713
|
+
isPreview,
|
|
714
|
+
} = this.options;
|
|
715
|
+
const parentWindowManager = this.options.windowManager;
|
|
716
|
+
|
|
717
|
+
let windowManager = this.windowManagers[windowName];
|
|
718
|
+
|
|
719
|
+
if (windowManager) {
|
|
720
|
+
const prevBounds = windowManager.getBounds();
|
|
721
|
+
const prevThemeOverride = windowManager.getThemeOverride();
|
|
722
|
+
const prevRuleEngine = windowManager.getRuleEngine();
|
|
723
|
+
const prevForceMute = windowManager.getForceMute();
|
|
724
|
+
|
|
725
|
+
const didBoundsChange = prevBounds
|
|
726
|
+
? !deepEqual(bounds, prevBounds)
|
|
727
|
+
: true;
|
|
728
|
+
|
|
729
|
+
if (didBoundsChange) {
|
|
730
|
+
windowManager.updateBounds(bounds);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
const didForceMuteChange = forceMute !== prevForceMute;
|
|
734
|
+
const didThemeOverrideChange = prevThemeOverride
|
|
735
|
+
? !deepEqual(themeOverride, prevThemeOverride)
|
|
736
|
+
: true;
|
|
737
|
+
|
|
738
|
+
if (didForceMuteChange || didThemeOverrideChange) {
|
|
739
|
+
windowManager.setThemeOverride(themeOverride);
|
|
740
|
+
windowManager.setForceMute(forceMute);
|
|
741
|
+
windowManager.reload();
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
const didPlaylistChange = prevRuleEngine
|
|
745
|
+
? playlistId !== prevRuleEngine.playlistId
|
|
746
|
+
: true;
|
|
747
|
+
|
|
748
|
+
if (didPlaylistChange) {
|
|
749
|
+
windowManager.reinitialize(
|
|
750
|
+
this.playlistWindowManagerState,
|
|
751
|
+
this.createRuleEngine(playlistId),
|
|
752
|
+
true
|
|
753
|
+
);
|
|
754
|
+
}
|
|
755
|
+
} else {
|
|
756
|
+
windowManager = new PlaylistWindowManager({
|
|
757
|
+
parentWindowId: windowId,
|
|
758
|
+
windowName,
|
|
759
|
+
ruleEngine: this.createRuleEngine(playlistId),
|
|
760
|
+
state: this.playlistWindowManagerState,
|
|
761
|
+
logger,
|
|
762
|
+
device,
|
|
763
|
+
readyTimeoutDurationSeconds: readyTimeoutDurationSeconds,
|
|
764
|
+
appRendererUrl: appRendererUrl,
|
|
765
|
+
bounds: {
|
|
766
|
+
x: bounds.x,
|
|
767
|
+
y: bounds.y,
|
|
768
|
+
width: bounds.width,
|
|
769
|
+
height: bounds.height,
|
|
770
|
+
},
|
|
771
|
+
backgroundColor: 'transparent',
|
|
772
|
+
themeOverride,
|
|
773
|
+
forceMute,
|
|
774
|
+
isPreview,
|
|
775
|
+
onEnableInterruptLock: () => this.enableInteruptLock(windowName),
|
|
776
|
+
onDisableInterruptLock: () => this.disableInteruptLock(windowName),
|
|
777
|
+
getTerminal: this.options.getTerminal,
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
// Hook up child window manager events to bubble up to the parent.
|
|
781
|
+
if (parentWindowManager) {
|
|
782
|
+
windowManager.on('window_preload_error', (metadata) => {
|
|
783
|
+
parentWindowManager.emit('window_preload_error', metadata);
|
|
784
|
+
});
|
|
785
|
+
windowManager.on('window_play', (metadata) => {
|
|
786
|
+
parentWindowManager.emit('window_play', metadata);
|
|
787
|
+
});
|
|
788
|
+
windowManager.on('window_end_success', (metadata) => {
|
|
789
|
+
parentWindowManager.emit('window_end_success', metadata);
|
|
790
|
+
});
|
|
791
|
+
windowManager.on('window_end_error', (metadata) => {
|
|
792
|
+
parentWindowManager.emit('window_end_error', metadata);
|
|
793
|
+
});
|
|
794
|
+
windowManager.on('window_analytics_event', (metadata) => {
|
|
795
|
+
parentWindowManager.emit('window_analytics_event', metadata);
|
|
796
|
+
});
|
|
797
|
+
windowManager.on('window_charge_request', (metadata) => {
|
|
798
|
+
parentWindowManager.emit('window_charge_request', metadata);
|
|
799
|
+
});
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
this.windowManagers[windowName] = windowManager;
|
|
803
|
+
|
|
804
|
+
windowManager.setRenderer(renderer);
|
|
805
|
+
await windowManager.load();
|
|
806
|
+
|
|
807
|
+
// The child window manager should be focused instead of the main window when focus() is called
|
|
808
|
+
// on the main window. Setting focus currently only happens on window create and can't be updated.
|
|
809
|
+
// This really only exists to support using VR with the remote in MZ. Only one child window can have
|
|
810
|
+
// focus, the last window to send an open window message wins. Handling focus needs more thought now
|
|
811
|
+
// that we have Secondscreen. We need to be extra careful about forcing focus in a touch environment.
|
|
812
|
+
if (isFocused) {
|
|
813
|
+
this.focusedWindowManager = windowManager;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
// If a child window is opened and the presentation is already playing (Kiosk),
|
|
817
|
+
// play the window after opening it. Otherwise we'll will wait until the presentation
|
|
818
|
+
// is ready and has received play before playing it.
|
|
819
|
+
if (this.isPlaying()) {
|
|
820
|
+
windowManager.play();
|
|
821
|
+
} else {
|
|
822
|
+
this.checkIsReadyCallback();
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
private closeChildWindow(windowName: string) {
|
|
828
|
+
const windowManager = this.windowManagers[windowName];
|
|
829
|
+
if (windowManager) {
|
|
830
|
+
windowManager.destroy();
|
|
831
|
+
delete this.windowManagers[windowName];
|
|
832
|
+
} else {
|
|
833
|
+
this.logWarning(
|
|
834
|
+
`Failed to close child window manager for '${windowName}'.`
|
|
835
|
+
);
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
private enableInteruptLock(windowName?: string) {
|
|
840
|
+
if (windowName) {
|
|
841
|
+
this.isChildWindowLocked[windowName] = true;
|
|
842
|
+
} else {
|
|
843
|
+
this.isWindowLocked = true;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
// If the presentation is ready and locked, trigger the enable interrupt callback.
|
|
847
|
+
if (this.isReady() && this.isLocked() && this.enableInterruptLockCallback) {
|
|
848
|
+
this.enableInterruptLockCallback();
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
private disableInteruptLock(windowName?: string) {
|
|
853
|
+
if (windowName) {
|
|
854
|
+
this.isChildWindowLocked[windowName] = false;
|
|
855
|
+
} else {
|
|
856
|
+
this.isWindowLocked = false;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
// If the presentation is ready and unlocked, trigger the disable interrupt callback.
|
|
860
|
+
if (
|
|
861
|
+
this.isReady() &&
|
|
862
|
+
!this.isLocked() &&
|
|
863
|
+
this.disableInterruptLockCallback
|
|
864
|
+
) {
|
|
865
|
+
this.disableInterruptLockCallback();
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
private getEmbeddedAppUrl() {
|
|
870
|
+
const { windowId } = this.options;
|
|
871
|
+
|
|
872
|
+
const metadata: EmbeddedAppMetadata = {
|
|
873
|
+
renderMode: this.options.isPreview ? 'preview' : 'live',
|
|
874
|
+
};
|
|
875
|
+
|
|
876
|
+
const url = createEmbeddedApp(
|
|
877
|
+
this.presentation,
|
|
878
|
+
this.options.applicationVersion,
|
|
879
|
+
metadata
|
|
880
|
+
);
|
|
881
|
+
|
|
882
|
+
return this.appendWindowIdToHash(url, windowId);
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
private handleEmbeddedMessage = async (type: string, payload?: any) => {
|
|
886
|
+
const { windowId } = this.options;
|
|
887
|
+
|
|
888
|
+
switch (type) {
|
|
889
|
+
case 'ready': {
|
|
890
|
+
this.logInfo(`Received 'ready' from window '${windowId}'`);
|
|
891
|
+
this.handleReady();
|
|
892
|
+
break;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
case 'complete': {
|
|
896
|
+
if (payload === undefined) {
|
|
897
|
+
this.logInfo(`Received 'complete' from window '${windowId}'`);
|
|
898
|
+
this.handleComplete();
|
|
899
|
+
} else {
|
|
900
|
+
this.logInfo(
|
|
901
|
+
`Received 'complete' with error from window '${windowId}, error=${payload}'`
|
|
902
|
+
);
|
|
903
|
+
this.handleError(payload);
|
|
904
|
+
}
|
|
905
|
+
break;
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
case 'runtime-subscribe': {
|
|
909
|
+
if (typeof payload === 'string') {
|
|
910
|
+
this.logInfo(
|
|
911
|
+
`Received 'runtime-subscribe' from window '${windowId}'`
|
|
912
|
+
);
|
|
913
|
+
// The payload is the event name (ex. 'play').
|
|
914
|
+
this.runtimeSubscriptions.add(payload);
|
|
915
|
+
|
|
916
|
+
// Immediately send play if the app if already playing.
|
|
917
|
+
if (this.isPlaying()) {
|
|
918
|
+
this.sendMessageString('play');
|
|
919
|
+
}
|
|
920
|
+
} else {
|
|
921
|
+
this.logWarning(
|
|
922
|
+
`Invalid payload for runtime-subscribe message: ${payload}`
|
|
923
|
+
);
|
|
924
|
+
}
|
|
925
|
+
break;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
case 'runtime-unsubscribe': {
|
|
929
|
+
if (typeof payload === 'string') {
|
|
930
|
+
this.logInfo(
|
|
931
|
+
`Received 'runtime-unsubscribe' from window '${windowId}'`
|
|
932
|
+
);
|
|
933
|
+
// The payload is the event name (ex. 'play').
|
|
934
|
+
this.runtimeSubscriptions.delete(payload);
|
|
935
|
+
} else {
|
|
936
|
+
this.logWarning(
|
|
937
|
+
`Invalid payload for runtime-subscribe message: ${payload}`
|
|
938
|
+
);
|
|
939
|
+
}
|
|
940
|
+
break;
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
case 'worker-subscribe': {
|
|
944
|
+
if (typeof payload === 'string') {
|
|
945
|
+
this.logInfo(`Received 'worker-subscribe' from window '${windowId}'`);
|
|
946
|
+
|
|
947
|
+
// The payload is the worker name.
|
|
948
|
+
this.workerSubscriptions.add(payload);
|
|
949
|
+
|
|
950
|
+
// Send the current state to the worker if it exists as soon
|
|
951
|
+
// as the app subscribes.
|
|
952
|
+
const workerState = this.workerState[payload];
|
|
953
|
+
if (workerState !== undefined) {
|
|
954
|
+
this.sendMessageString('worker-state', {
|
|
955
|
+
name: payload,
|
|
956
|
+
state: workerState,
|
|
957
|
+
});
|
|
958
|
+
}
|
|
959
|
+
} else {
|
|
960
|
+
this.logWarning(
|
|
961
|
+
`Invalid payload for worker-subscribe message: ${payload}`
|
|
962
|
+
);
|
|
963
|
+
}
|
|
964
|
+
break;
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
case 'worker-unsubscribe': {
|
|
968
|
+
if (typeof payload === 'string') {
|
|
969
|
+
this.logInfo(
|
|
970
|
+
`Received 'worker-unsubscribe' from window '${windowId}'`
|
|
971
|
+
);
|
|
972
|
+
|
|
973
|
+
// The payload is the worker name.
|
|
974
|
+
this.workerSubscriptions.delete(payload);
|
|
975
|
+
} else {
|
|
976
|
+
this.logWarning(
|
|
977
|
+
`Invalid payload for worker-unsubscribe message: ${payload}`
|
|
978
|
+
);
|
|
979
|
+
}
|
|
980
|
+
break;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
case 'analytics-event': {
|
|
984
|
+
if (isValidAnalyticsEvent(payload)) {
|
|
985
|
+
if (this.analyticsEventCallback) {
|
|
986
|
+
this.analyticsEventCallback(payload);
|
|
987
|
+
}
|
|
988
|
+
} else {
|
|
989
|
+
this.logWarning(
|
|
990
|
+
`Invalid payload for analytics-event message: ${payload}`
|
|
991
|
+
);
|
|
992
|
+
}
|
|
993
|
+
break;
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
case 'create-object-url-result': {
|
|
997
|
+
if (isValidCreateObjectURLResultPayload(payload)) {
|
|
998
|
+
// Resolve promise with the local blob URL.
|
|
999
|
+
if (this.createObjectURLResolvers[payload.requestId]) {
|
|
1000
|
+
this.createObjectURLResolvers[payload.requestId](payload.url);
|
|
1001
|
+
delete this.createObjectURLResolvers[payload.requestId];
|
|
1002
|
+
}
|
|
1003
|
+
// Clear the timeout for the request.
|
|
1004
|
+
if (this.createObjectURLTimeouts[payload.requestId]) {
|
|
1005
|
+
clearTimeout(this.createObjectURLTimeouts[payload.requestId]);
|
|
1006
|
+
delete this.createObjectURLTimeouts[payload.requestId];
|
|
1007
|
+
}
|
|
1008
|
+
} else {
|
|
1009
|
+
this.logWarning(
|
|
1010
|
+
`Invalid payload for create-object-url-result message: ${payload}`
|
|
1011
|
+
);
|
|
1012
|
+
}
|
|
1013
|
+
break;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
case 'preview-subscribe': {
|
|
1017
|
+
if (typeof payload === 'string') {
|
|
1018
|
+
this.logInfo(
|
|
1019
|
+
`Received 'preview-subscribe' from window '${windowId}'`
|
|
1020
|
+
);
|
|
1021
|
+
// The payload is the event name (ex. 'inputs').
|
|
1022
|
+
this.previewSubscriptions.add(payload);
|
|
1023
|
+
} else {
|
|
1024
|
+
this.logWarning(
|
|
1025
|
+
`Invalid payload for preview-subscribe message: ${payload}`
|
|
1026
|
+
);
|
|
1027
|
+
}
|
|
1028
|
+
break;
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
case 'preview-unsubscribe': {
|
|
1032
|
+
if (typeof payload === 'string') {
|
|
1033
|
+
this.logInfo(
|
|
1034
|
+
`Received 'preview-unsubscribe' from window '${windowId}'`
|
|
1035
|
+
);
|
|
1036
|
+
// The payload is the event name (ex. 'inputs').
|
|
1037
|
+
this.previewSubscriptions.delete(payload);
|
|
1038
|
+
} else {
|
|
1039
|
+
this.logWarning(
|
|
1040
|
+
`Invalid payload for preview-subscribe message: ${payload}`
|
|
1041
|
+
);
|
|
1042
|
+
}
|
|
1043
|
+
break;
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
case 'payments-charge-request': {
|
|
1047
|
+
if (isValidChargeRequest(payload)) {
|
|
1048
|
+
if (this.chargeRequestCallback) {
|
|
1049
|
+
this.chargeRequestCallback(payload);
|
|
1050
|
+
}
|
|
1051
|
+
} else {
|
|
1052
|
+
this.logWarning(
|
|
1053
|
+
`Invalid payload for payments-charge-request message: ${payload}`
|
|
1054
|
+
);
|
|
1055
|
+
}
|
|
1056
|
+
break;
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
case 'payments-subscribe': {
|
|
1060
|
+
if (typeof payload === 'string') {
|
|
1061
|
+
this.logInfo(`Received 'worker-subscribe' from window '${windowId}'`);
|
|
1062
|
+
// The payload is the payment subscription type (ex. 'charge', 'terminal')
|
|
1063
|
+
this.paymentSubscriptions.add(payload);
|
|
1064
|
+
|
|
1065
|
+
// Immediately send terminal when subscribing.
|
|
1066
|
+
if (payload === 'terminal' && this.options.getTerminal) {
|
|
1067
|
+
this.sendMessageObject('payments-terminal-event', {
|
|
1068
|
+
terminal: this.options.getTerminal(),
|
|
1069
|
+
});
|
|
1070
|
+
}
|
|
1071
|
+
} else {
|
|
1072
|
+
this.logWarning(
|
|
1073
|
+
`Invalid payload for payments-subscribe message: ${payload}`
|
|
1074
|
+
);
|
|
1075
|
+
}
|
|
1076
|
+
break;
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
case 'payments-unsubscribe': {
|
|
1080
|
+
if (typeof payload === 'string') {
|
|
1081
|
+
this.logInfo(
|
|
1082
|
+
`Received 'payments-unsubscribe' from window '${windowId}'`
|
|
1083
|
+
);
|
|
1084
|
+
this.paymentSubscriptions.delete(payload);
|
|
1085
|
+
} else {
|
|
1086
|
+
this.logWarning(
|
|
1087
|
+
`Invalid payload for payments-unsubscribe message: ${payload}`
|
|
1088
|
+
);
|
|
1089
|
+
}
|
|
1090
|
+
break;
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
default: {
|
|
1094
|
+
this.logWarning(`Received unknown embedded message type: ${type}`);
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
};
|
|
1098
|
+
|
|
1099
|
+
private handleLegacyMessage = async (type: string, payload?: any) => {
|
|
1100
|
+
const { windowId, onPresentationProperties } = this.options;
|
|
1101
|
+
|
|
1102
|
+
if (type === 'loaded') {
|
|
1103
|
+
this.logInfo(`Received 'loaded' from window '${windowId}'`);
|
|
1104
|
+
this.sendRenderMessage();
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
if (type === 'presentation_ready') {
|
|
1108
|
+
this.logInfo(`Received 'presentation_ready' from window '${windowId}'`);
|
|
1109
|
+
this.handleReady();
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
if (type === 'presentation_complete') {
|
|
1113
|
+
this.logInfo(
|
|
1114
|
+
`Received 'presentation_complete' from window '${windowId}'`
|
|
1115
|
+
);
|
|
1116
|
+
this.handleComplete();
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
if (type === 'enable_interrupt_lock') {
|
|
1120
|
+
this.logInfo(
|
|
1121
|
+
`Received 'enable_interrupt_lock' from window '${windowId}'`
|
|
1122
|
+
);
|
|
1123
|
+
this.enableInteruptLock();
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
if (type === 'disable_interrupt_lock') {
|
|
1127
|
+
this.logInfo(
|
|
1128
|
+
`Received 'disable_interrupt_lock' from window '${windowId}'`
|
|
1129
|
+
);
|
|
1130
|
+
this.disableInteruptLock();
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
if (type === 'open_child_window') {
|
|
1134
|
+
this.logInfo(
|
|
1135
|
+
`Received 'open_child_window' from window '${windowId}', windowId=${payload.windowId} playlistId=${payload.playlistId} isMuted=${payload.isMuted} x=${payload.x} y=${payload.y} width=${payload.width} height=${payload.height}`
|
|
1136
|
+
);
|
|
1137
|
+
if (this.isValidOpenChildWindowPayload(payload)) {
|
|
1138
|
+
this.openChildWindow(
|
|
1139
|
+
payload.windowId,
|
|
1140
|
+
payload.playlistId,
|
|
1141
|
+
{
|
|
1142
|
+
x: Math.round(payload.x),
|
|
1143
|
+
y: Math.round(payload.y),
|
|
1144
|
+
width: Math.round(payload.width),
|
|
1145
|
+
height: Math.round(payload.height),
|
|
1146
|
+
},
|
|
1147
|
+
payload.isMuted,
|
|
1148
|
+
payload.isFocused,
|
|
1149
|
+
payload.theme
|
|
1150
|
+
);
|
|
1151
|
+
} else {
|
|
1152
|
+
this.logWarning(
|
|
1153
|
+
`Invalid payload received for 'open_child_window', ignoring`
|
|
1154
|
+
);
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
if (type === 'close_child_window') {
|
|
1159
|
+
// TODO: Validate close_child_window message payload.
|
|
1160
|
+
this.logInfo(
|
|
1161
|
+
`Received 'close_child_window', windowId=${payload.windowId}`
|
|
1162
|
+
);
|
|
1163
|
+
this.closeChildWindow(payload.windowId);
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
if (type === 'log_error') {
|
|
1167
|
+
this.handleError(
|
|
1168
|
+
`Received error from window '${windowId}', error=${JSON.stringify(
|
|
1169
|
+
payload
|
|
1170
|
+
)}`
|
|
1171
|
+
);
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
if (type === 'log_info') {
|
|
1175
|
+
this.logInfo(
|
|
1176
|
+
`Received info log from window '${windowId}', message=${JSON.stringify(
|
|
1177
|
+
payload
|
|
1178
|
+
)}`
|
|
1179
|
+
);
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
if (type === 'log_debug') {
|
|
1183
|
+
this.logDebug(
|
|
1184
|
+
`Received info debug from window '${windowId}', message=${JSON.stringify(
|
|
1185
|
+
payload
|
|
1186
|
+
)}`
|
|
1187
|
+
);
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
if (type === 'presentation_properties') {
|
|
1191
|
+
this.logInfo(
|
|
1192
|
+
`Received 'presentation_properties' from window '${windowId}'`
|
|
1193
|
+
);
|
|
1194
|
+
if (onPresentationProperties) {
|
|
1195
|
+
onPresentationProperties(payload.properties, payload.strings);
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
};
|
|
1199
|
+
|
|
1200
|
+
private handleReady() {
|
|
1201
|
+
this.didReceiveReady = true;
|
|
1202
|
+
this.checkIsReadyCallback();
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
private handleComplete() {
|
|
1206
|
+
this.didReceiveComplete = true;
|
|
1207
|
+
if (this.completeCallback) {
|
|
1208
|
+
this.completeCallback();
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
private handleError = (error: any) => {
|
|
1213
|
+
this.error = error;
|
|
1214
|
+
|
|
1215
|
+
const message = error instanceof Error ? error.message : error;
|
|
1216
|
+
this.logError(message);
|
|
1217
|
+
|
|
1218
|
+
if (this.errorCallback) {
|
|
1219
|
+
this.errorCallback(error);
|
|
1220
|
+
}
|
|
1221
|
+
};
|
|
1222
|
+
|
|
1223
|
+
private logDebug(message: any) {
|
|
1224
|
+
this.options.logger.debug(message, this.getLogContext());
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
private logInfo(message: any) {
|
|
1228
|
+
this.options.logger.info(message, this.getLogContext());
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
private logWarning(message: any) {
|
|
1232
|
+
this.options.logger.warn(message, this.getLogContext());
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
private logError(message: any) {
|
|
1236
|
+
this.options.logger.error(message, this.getLogContext());
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
private getLogContext() {
|
|
1240
|
+
const {
|
|
1241
|
+
windowId,
|
|
1242
|
+
windowName,
|
|
1243
|
+
parentWindowId,
|
|
1244
|
+
presentation,
|
|
1245
|
+
applicationVersion,
|
|
1246
|
+
publishedAt,
|
|
1247
|
+
playlistId,
|
|
1248
|
+
playlistName,
|
|
1249
|
+
rule,
|
|
1250
|
+
} = this.options;
|
|
1251
|
+
|
|
1252
|
+
return {
|
|
1253
|
+
windowId,
|
|
1254
|
+
windowName,
|
|
1255
|
+
parentWindowId,
|
|
1256
|
+
presentationId: presentation.id,
|
|
1257
|
+
presentationName: presentation.name,
|
|
1258
|
+
applicationId: applicationVersion.applicationId,
|
|
1259
|
+
applicationVersionName: applicationVersion.name,
|
|
1260
|
+
applicationVersionId: applicationVersion.id,
|
|
1261
|
+
publishedAt,
|
|
1262
|
+
playlistId,
|
|
1263
|
+
playlistName,
|
|
1264
|
+
rule,
|
|
1265
|
+
};
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
public metadata() {
|
|
1269
|
+
return this.getLogContext();
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
public runWorkerSubscriptions(state: WorkerState) {
|
|
1273
|
+
// For each worker subscription, compare the previous state to the new state.
|
|
1274
|
+
// If the state has changed, send the new state to the subscription.
|
|
1275
|
+
for (const workerName of Array.from(this.workerSubscriptions)) {
|
|
1276
|
+
const workerState = state[workerName];
|
|
1277
|
+
const previousWorkerState = this.workerState[workerName];
|
|
1278
|
+
|
|
1279
|
+
if (
|
|
1280
|
+
workerState !== undefined &&
|
|
1281
|
+
!deepEqual(workerState, previousWorkerState)
|
|
1282
|
+
) {
|
|
1283
|
+
this.sendMessageString('worker-state', {
|
|
1284
|
+
name: workerName,
|
|
1285
|
+
state: workerState,
|
|
1286
|
+
});
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
// Persist the new state so we can compare if the previous values have changed..
|
|
1291
|
+
this.workerState = state;
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
private createRuleEngine(playlistId: string) {
|
|
1295
|
+
const { globalRule } = this.options;
|
|
1296
|
+
return new RE.RuleEngine(
|
|
1297
|
+
playlistId,
|
|
1298
|
+
this.ruleEngineState,
|
|
1299
|
+
globalRule
|
|
1300
|
+
? {
|
|
1301
|
+
// Append to the global rule with an AND to filter out other presentations
|
|
1302
|
+
// that have playlists. Prevents MZ in MZ, Kiosk in Kiosk, MZ in Kiosk, etc...
|
|
1303
|
+
type: 'and',
|
|
1304
|
+
group: [
|
|
1305
|
+
globalRule,
|
|
1306
|
+
{
|
|
1307
|
+
type: 'equals',
|
|
1308
|
+
left: { var: ['target', 'hasPlaylists'] },
|
|
1309
|
+
right: { const: false },
|
|
1310
|
+
},
|
|
1311
|
+
],
|
|
1312
|
+
}
|
|
1313
|
+
: {
|
|
1314
|
+
// A global rule was not provided so we don't need to combine the rule with an AND.
|
|
1315
|
+
type: 'equals',
|
|
1316
|
+
left: { var: ['target', 'hasPlaylists'] },
|
|
1317
|
+
right: { const: false },
|
|
1318
|
+
}
|
|
1319
|
+
);
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
private isValidOpenChildWindowPayload(
|
|
1323
|
+
payload?: any
|
|
1324
|
+
): payload is OpenChildWindowOptions {
|
|
1325
|
+
if (!payload) return false;
|
|
1326
|
+
if (typeof payload !== 'object') return false;
|
|
1327
|
+
if (!payload.windowId) return false;
|
|
1328
|
+
if (!payload.playlistId) return false;
|
|
1329
|
+
if (!isValidNumber(payload.x)) return false;
|
|
1330
|
+
if (!isValidNumber(payload.y)) return false;
|
|
1331
|
+
if (!isValidNumber(payload.width)) return false;
|
|
1332
|
+
if (!isValidNumber(payload.height)) return false;
|
|
1333
|
+
|
|
1334
|
+
return true;
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
private shouldMute() {
|
|
1338
|
+
if (this.forceMute) {
|
|
1339
|
+
return true;
|
|
1340
|
+
}
|
|
1341
|
+
// Check app vars for muted properties. This is gross because it relies on
|
|
1342
|
+
// presentation properties having specific names. A better solution is needed
|
|
1343
|
+
// long term.
|
|
1344
|
+
const appVars = this.presentation.applicationVariables;
|
|
1345
|
+
const hasMuteProp = appVars.mute === true || appVars.muted === true;
|
|
1346
|
+
if (hasMuteProp) {
|
|
1347
|
+
return true;
|
|
1348
|
+
}
|
|
1349
|
+
const isMutedMultizone =
|
|
1350
|
+
appVars.muteZone1 === true && appVars.muteZone2 === true;
|
|
1351
|
+
return isMutedMultizone;
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
private createObjectURLForEmbeddedApp(blob: Blob): Promise<string> {
|
|
1355
|
+
return new Promise((resolve, reject) => {
|
|
1356
|
+
// Handle multiple concurrent createObjectURL calls by creating a request id for each
|
|
1357
|
+
// call. We send the request id along with the message to the running app and the app
|
|
1358
|
+
// sends the request id back with the result so we can resolve the correct promise.
|
|
1359
|
+
const requestId = uuidv4();
|
|
1360
|
+
this.createObjectURLResolvers[requestId] = resolve;
|
|
1361
|
+
|
|
1362
|
+
// Start a timer to reject the promise if the app doesn't respond with the create object
|
|
1363
|
+
// url result within 1 second. This should be more or less instant.
|
|
1364
|
+
this.createObjectURLTimeouts[requestId] = setTimeout(() => {
|
|
1365
|
+
const error = new Error(
|
|
1366
|
+
`Timed out waiting for the create-object-url-result message for request id ${requestId}`
|
|
1367
|
+
);
|
|
1368
|
+
reject(error);
|
|
1369
|
+
delete this.createObjectURLResolvers[requestId];
|
|
1370
|
+
delete this.createObjectURLTimeouts[requestId];
|
|
1371
|
+
}, 1000);
|
|
1372
|
+
|
|
1373
|
+
// Create a FileReader object to read the file data as an ArrayBuffer.
|
|
1374
|
+
const reader = new FileReader();
|
|
1375
|
+
|
|
1376
|
+
reader.addEventListener(
|
|
1377
|
+
'load',
|
|
1378
|
+
() => {
|
|
1379
|
+
const buffer = reader.result;
|
|
1380
|
+
|
|
1381
|
+
// Check if buffer is an ArrayBuffer
|
|
1382
|
+
if (buffer instanceof ArrayBuffer) {
|
|
1383
|
+
// Send the ArrayBuffer to the running app to create the local file blob URL.
|
|
1384
|
+
this.sendMessageObject('create-object-url', { requestId, buffer });
|
|
1385
|
+
}
|
|
1386
|
+
},
|
|
1387
|
+
false
|
|
1388
|
+
);
|
|
1389
|
+
|
|
1390
|
+
reader.readAsArrayBuffer(blob);
|
|
1391
|
+
|
|
1392
|
+
return '';
|
|
1393
|
+
});
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
const isValidCreateObjectURLResultPayload = (
|
|
1398
|
+
payload: any
|
|
1399
|
+
): payload is { requestId: string; url: string } => {
|
|
1400
|
+
return (
|
|
1401
|
+
// Ensure payload is an object.
|
|
1402
|
+
typeof payload === 'object' &&
|
|
1403
|
+
payload !== null &&
|
|
1404
|
+
// Ensure requestId is a string.
|
|
1405
|
+
'requestId' in payload &&
|
|
1406
|
+
typeof payload['requestId'] === 'string' &&
|
|
1407
|
+
// Ensure payload.file is an object.
|
|
1408
|
+
'url' in payload &&
|
|
1409
|
+
typeof payload['url'] === 'string'
|
|
1410
|
+
);
|
|
1411
|
+
};
|
|
1412
|
+
|
|
1413
|
+
const transformInputValues = (inputs: Record<string, any>) => {
|
|
1414
|
+
const transformed: Record<string, any> = {};
|
|
1415
|
+
|
|
1416
|
+
// Recursively traverse inputs, transforming any file input values to remove internal
|
|
1417
|
+
// properties like file_upload_id.
|
|
1418
|
+
for (const [key, value] of Object.entries(inputs)) {
|
|
1419
|
+
if (typeof value === 'object' && value !== null && 'url' in value) {
|
|
1420
|
+
// Value is a file input, strip out the file_upload_id property.
|
|
1421
|
+
transformed[key] = { url: value.url };
|
|
1422
|
+
} else if (Array.isArray(value)) {
|
|
1423
|
+
// Value is an array input, traverse each item in the array for file input values.
|
|
1424
|
+
transformed[key] = [];
|
|
1425
|
+
for (const item of value) {
|
|
1426
|
+
transformed[key].push(transformInputValues(item));
|
|
1427
|
+
}
|
|
1428
|
+
} else {
|
|
1429
|
+
transformed[key] = value;
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
return transformed;
|
|
1434
|
+
};
|