@raydiant/playlist-window-manager 0.22.0-rc-electron-upgrade-26-03 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/BrowserWindowRenderer.js +11 -15
- package/build/PlaylistWindowManager.d.ts +6 -1
- package/build/PlaylistWindowManager.js +42 -3
- package/build/PresentationWindow.d.ts +8 -1
- package/build/PresentationWindow.js +100 -3
- package/build/PresentationWindow.test.js +99 -0
- package/build/testUtils.js +1 -0
- package/build/types.d.ts +20 -0
- package/package.json +2 -2
- package/src/BrowserWindowRenderer.ts +3 -12
- package/src/PlaylistWindowManager.ts +59 -6
- package/src/PresentationWindow.test.ts +112 -0
- package/src/PresentationWindow.ts +120 -3
- package/src/testUtils.ts +1 -0
- package/src/types.ts +24 -0
|
@@ -76,10 +76,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
76
76
|
var path = __importStar(require("path"));
|
|
77
77
|
var electron_1 = require("electron");
|
|
78
78
|
var WindowUnresponsiveError_1 = __importDefault(require("./WindowUnresponsiveError"));
|
|
79
|
-
var globalWebPreferences = {
|
|
80
|
-
nodeIntegration: true,
|
|
81
|
-
contextIsolation: false,
|
|
82
|
-
};
|
|
83
79
|
var BrowserWindowRenderer = /** @class */ (function () {
|
|
84
80
|
function BrowserWindowRenderer(opts) {
|
|
85
81
|
this.opts = opts;
|
|
@@ -92,7 +88,9 @@ var BrowserWindowRenderer = /** @class */ (function () {
|
|
|
92
88
|
show: false,
|
|
93
89
|
frame: this.opts.frame,
|
|
94
90
|
hasShadow: this.opts.hasShadow,
|
|
95
|
-
webPreferences:
|
|
91
|
+
webPreferences: {
|
|
92
|
+
preload: path.join(__dirname, 'preload.js'),
|
|
93
|
+
},
|
|
96
94
|
});
|
|
97
95
|
this.proxySettings = null;
|
|
98
96
|
this.offscreenWindow.setBounds(opts.bounds);
|
|
@@ -118,7 +116,9 @@ var BrowserWindowRenderer = /** @class */ (function () {
|
|
|
118
116
|
// render all child windows as browser views.
|
|
119
117
|
view: parentWindowId && this.opts.renderChildWindowsAsBrowserViews
|
|
120
118
|
? this.createOrGetPooledBrowserView(windowId, {
|
|
121
|
-
webPreferences:
|
|
119
|
+
webPreferences: {
|
|
120
|
+
preload: path.join(__dirname, 'preload.js'),
|
|
121
|
+
},
|
|
122
122
|
})
|
|
123
123
|
: this.createOrGetPooledBrowserWindow(windowId, {
|
|
124
124
|
show: false,
|
|
@@ -130,7 +130,9 @@ var BrowserWindowRenderer = /** @class */ (function () {
|
|
|
130
130
|
: undefined,
|
|
131
131
|
transparent: backgroundColor === 'transparent',
|
|
132
132
|
hasShadow: this.opts.hasShadow,
|
|
133
|
-
webPreferences:
|
|
133
|
+
webPreferences: {
|
|
134
|
+
preload: path.join(__dirname, 'preload.js'),
|
|
135
|
+
},
|
|
134
136
|
}),
|
|
135
137
|
// If we are creating a child window and renderChildWindowsAsBrowserViews is false that means
|
|
136
138
|
// we are rendering child windows with BrowserWindow. BrowserWindows are positioned absolutely so
|
|
@@ -157,10 +159,7 @@ var BrowserWindowRenderer = /** @class */ (function () {
|
|
|
157
159
|
},
|
|
158
160
|
};
|
|
159
161
|
// Attached crash handlers to skip presentation on error.
|
|
160
|
-
window.view.webContents.on(
|
|
161
|
-
// FIXME
|
|
162
|
-
// @ts-ignore
|
|
163
|
-
'render-process-gone', window.renderProcessGoneCallback);
|
|
162
|
+
window.view.webContents.on('render-process-gone', window.renderProcessGoneCallback);
|
|
164
163
|
window.view.webContents.on('unresponsive', window.unresponsiveCallback);
|
|
165
164
|
if (window.view instanceof electron_1.BrowserView) {
|
|
166
165
|
this.moveWindowOffscreen(window);
|
|
@@ -266,10 +265,7 @@ var BrowserWindowRenderer = /** @class */ (function () {
|
|
|
266
265
|
throw new Error("Failed to remove window, missing window id '".concat(windowId, "'"));
|
|
267
266
|
}
|
|
268
267
|
// Remove crash handlers
|
|
269
|
-
window.view.webContents.removeListener(
|
|
270
|
-
// FIXME
|
|
271
|
-
// @ts-ignore
|
|
272
|
-
'render-process-gone', window.renderProcessGoneCallback);
|
|
268
|
+
window.view.webContents.removeListener('render-process-gone', window.renderProcessGoneCallback);
|
|
273
269
|
window.view.webContents.removeListener('unresponsive', window.unresponsiveCallback);
|
|
274
270
|
// Remove message handler
|
|
275
271
|
electron_1.ipcMain.removeListener(windowId, window.receiveMessageCallback);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as RE from '@raydiant/playlist-rule-engine';
|
|
2
2
|
import EventEmitter from 'eventemitter3';
|
|
3
|
-
import { PresentationWindowRenderer, Device, WindowBounds, Theme, Logger, WorkerState } from './types';
|
|
3
|
+
import { PresentationWindowRenderer, Device, WindowBounds, Theme, Logger, WorkerState, Charge, ChargeResponse, Terminal } from './types';
|
|
4
4
|
import PlaylistWindowManagerState from './PlaylistWindowManagerState';
|
|
5
5
|
export interface PlaylistWindowManagerOptions {
|
|
6
6
|
ruleEngine: RE.RuleEngine;
|
|
@@ -18,6 +18,7 @@ export interface PlaylistWindowManagerOptions {
|
|
|
18
18
|
isPreview?: boolean;
|
|
19
19
|
onEnableInterruptLock?: () => void;
|
|
20
20
|
onDisableInterruptLock?: () => void;
|
|
21
|
+
getTerminal?: () => Terminal | null;
|
|
21
22
|
}
|
|
22
23
|
export default class PlaylistWindowManager extends EventEmitter {
|
|
23
24
|
private options;
|
|
@@ -75,6 +76,9 @@ export default class PlaylistWindowManager extends EventEmitter {
|
|
|
75
76
|
isReady(): boolean;
|
|
76
77
|
isLocked(): boolean;
|
|
77
78
|
runWorkerSubscriptions(state: WorkerState): void;
|
|
79
|
+
sendChargeResponse(response: ChargeResponse): void;
|
|
80
|
+
sendCharge(charge: Charge): void;
|
|
81
|
+
sendTerminal(terminal: Terminal | null): void;
|
|
78
82
|
private createWindow;
|
|
79
83
|
private loadCurrentWindow;
|
|
80
84
|
private shouldPreload;
|
|
@@ -90,6 +94,7 @@ export default class PlaylistWindowManager extends EventEmitter {
|
|
|
90
94
|
private handleEnableInterruptLock;
|
|
91
95
|
private handleDisableInterruptLock;
|
|
92
96
|
private handleAnalyticsEvent;
|
|
97
|
+
private handleChargeRequest;
|
|
93
98
|
private handleCurrentPresentationComplete;
|
|
94
99
|
private handleCurrentPresentationError;
|
|
95
100
|
private handleNextPresentationError;
|
|
@@ -117,6 +117,11 @@ var PlaylistWindowManager = /** @class */ (function (_super) {
|
|
|
117
117
|
// send analytics events since the next window is not visible.
|
|
118
118
|
_this.emitCurrentWindowEvent('window_analytics_event', event);
|
|
119
119
|
};
|
|
120
|
+
_this.handleChargeRequest = function (request) {
|
|
121
|
+
// Emit the event on the current window. Only the current window should
|
|
122
|
+
// send charge requests since the next window is not visible.
|
|
123
|
+
_this.emitCurrentWindowEvent('window_charge_request', request);
|
|
124
|
+
};
|
|
120
125
|
_this.handleCurrentPresentationComplete = function () { return __awaiter(_this, void 0, void 0, function () {
|
|
121
126
|
var logger, isThrottled, now, timeSinceLastComplete, delay, _a, next, preview, nextPreviewPresentationId;
|
|
122
127
|
var _this = this;
|
|
@@ -640,6 +645,37 @@ var PlaylistWindowManager = /** @class */ (function (_super) {
|
|
|
640
645
|
this.nextWindow.runWorkerSubscriptions(state);
|
|
641
646
|
}
|
|
642
647
|
};
|
|
648
|
+
PlaylistWindowManager.prototype.sendChargeResponse = function (response) {
|
|
649
|
+
// Send charge response to the current window. The window will check if
|
|
650
|
+
// has a matching request id and if so, send the response to the app.
|
|
651
|
+
if (this.currentWindow) {
|
|
652
|
+
this.currentWindow.sendChargeResponse(response);
|
|
653
|
+
}
|
|
654
|
+
// Send charge response to the next window.
|
|
655
|
+
if (this.nextWindow) {
|
|
656
|
+
this.nextWindow.sendChargeResponse(response);
|
|
657
|
+
}
|
|
658
|
+
};
|
|
659
|
+
PlaylistWindowManager.prototype.sendCharge = function (charge) {
|
|
660
|
+
// Send charge to the current window.
|
|
661
|
+
if (this.currentWindow) {
|
|
662
|
+
this.currentWindow.sendCharge(charge);
|
|
663
|
+
}
|
|
664
|
+
// Send charge to the next window.
|
|
665
|
+
if (this.nextWindow) {
|
|
666
|
+
this.nextWindow.sendCharge(charge);
|
|
667
|
+
}
|
|
668
|
+
};
|
|
669
|
+
PlaylistWindowManager.prototype.sendTerminal = function (terminal) {
|
|
670
|
+
// Send terminal to the current window.
|
|
671
|
+
if (this.currentWindow) {
|
|
672
|
+
this.currentWindow.sendTerminal(terminal);
|
|
673
|
+
}
|
|
674
|
+
// Send terminal to the next window.
|
|
675
|
+
if (this.nextWindow) {
|
|
676
|
+
this.nextWindow.sendTerminal(terminal);
|
|
677
|
+
}
|
|
678
|
+
};
|
|
643
679
|
PlaylistWindowManager.prototype.createWindow = function (pr) {
|
|
644
680
|
var _a = this.options, readyTimeoutDurationSeconds = _a.readyTimeoutDurationSeconds, appRendererUrl = _a.appRendererUrl, backgroundColor = _a.backgroundColor, device = _a.device, logger = _a.logger, windowName = _a.windowName, parentWindowId = _a.parentWindowId, isPreview = _a.isPreview;
|
|
645
681
|
if (!this.ruleEngine) {
|
|
@@ -682,7 +718,8 @@ var PlaylistWindowManager = /** @class */ (function (_super) {
|
|
|
682
718
|
ruleEngineState: this.ruleEngine.state,
|
|
683
719
|
globalRule: this.ruleEngine.globalRule,
|
|
684
720
|
isPreview: isPreview,
|
|
685
|
-
workerState: this.workerState
|
|
721
|
+
workerState: this.workerState,
|
|
722
|
+
getTerminal: this.options.getTerminal,
|
|
686
723
|
});
|
|
687
724
|
};
|
|
688
725
|
PlaylistWindowManager.prototype.loadCurrentWindow = function () {
|
|
@@ -702,6 +739,7 @@ var PlaylistWindowManager = /** @class */ (function (_super) {
|
|
|
702
739
|
this.currentWindow.onEnableInterruptLock(this.handleEnableInterruptLock);
|
|
703
740
|
this.currentWindow.onDisableInterruptLock(this.handleDisableInterruptLock);
|
|
704
741
|
this.currentWindow.onAnalyticsEvent(this.handleAnalyticsEvent);
|
|
742
|
+
this.currentWindow.onChargeRequest(this.handleChargeRequest);
|
|
705
743
|
return [4 /*yield*/, this.currentWindow.load()];
|
|
706
744
|
case 1:
|
|
707
745
|
_a.sent();
|
|
@@ -906,6 +944,7 @@ var PlaylistWindowManager = /** @class */ (function (_super) {
|
|
|
906
944
|
this.currentWindow.onEnableInterruptLock(this.handleEnableInterruptLock);
|
|
907
945
|
this.currentWindow.onDisableInterruptLock(this.handleDisableInterruptLock);
|
|
908
946
|
this.currentWindow.onAnalyticsEvent(this.handleAnalyticsEvent);
|
|
947
|
+
this.currentWindow.onChargeRequest(this.handleChargeRequest);
|
|
909
948
|
}
|
|
910
949
|
// Play new current window.
|
|
911
950
|
this.playCurrentWindow();
|
|
@@ -997,12 +1036,12 @@ var PlaylistWindowManager = /** @class */ (function (_super) {
|
|
|
997
1036
|
};
|
|
998
1037
|
PlaylistWindowManager.prototype.emitCurrentWindowErrorEvent = function (event, error) {
|
|
999
1038
|
return this.emitCurrentWindowEvent(event, {
|
|
1000
|
-
errorMessage: this.getErrorMessage(error)
|
|
1039
|
+
errorMessage: this.getErrorMessage(error),
|
|
1001
1040
|
});
|
|
1002
1041
|
};
|
|
1003
1042
|
PlaylistWindowManager.prototype.emitNextWindowErrorEvent = function (event, error) {
|
|
1004
1043
|
return this.emitNextWindowEvent(event, {
|
|
1005
|
-
errorMessage: this.getErrorMessage(error)
|
|
1044
|
+
errorMessage: this.getErrorMessage(error),
|
|
1006
1045
|
});
|
|
1007
1046
|
};
|
|
1008
1047
|
PlaylistWindowManager.prototype.getErrorMessage = function (error) {
|
|
@@ -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, AnalyticsEvent } from './types';
|
|
4
|
+
import { Presentation, ApplicationVersion, Theme, Device, PresentationWindowRenderer, WindowBounds, Logger, BuilderState, WorkerState, AnalyticsEvent, Charge, ChargeRequest, ChargeResponse, Terminal } from './types';
|
|
5
5
|
export interface PresentationWindowOptions {
|
|
6
6
|
windowManager?: PlaylistWindowManager;
|
|
7
7
|
windowId: string;
|
|
@@ -31,6 +31,7 @@ export interface PresentationWindowOptions {
|
|
|
31
31
|
onPresentationProperties?: (properties: any[], string: {
|
|
32
32
|
[key: string]: string;
|
|
33
33
|
}) => void;
|
|
34
|
+
getTerminal?: () => Terminal | null;
|
|
34
35
|
}
|
|
35
36
|
export default class PresentationWindow {
|
|
36
37
|
private options;
|
|
@@ -50,6 +51,7 @@ export default class PresentationWindow {
|
|
|
50
51
|
private enableInterruptLockCallback;
|
|
51
52
|
private disableInterruptLockCallback;
|
|
52
53
|
private analyticsEventCallback;
|
|
54
|
+
private chargeRequestCallback;
|
|
53
55
|
private builderState;
|
|
54
56
|
private presentation;
|
|
55
57
|
private playlistWindowManagerState;
|
|
@@ -62,6 +64,7 @@ export default class PresentationWindow {
|
|
|
62
64
|
private runtimeSubscriptions;
|
|
63
65
|
private previewSubscriptions;
|
|
64
66
|
private previewState;
|
|
67
|
+
private paymentSubscriptions;
|
|
65
68
|
private createObjectURLResolvers;
|
|
66
69
|
private createObjectURLTimeouts;
|
|
67
70
|
constructor(options: PresentationWindowOptions);
|
|
@@ -79,6 +82,7 @@ export default class PresentationWindow {
|
|
|
79
82
|
onEnableInterruptLock(callback: (() => void) | null): void;
|
|
80
83
|
onDisableInterruptLock(callback: (() => void) | null): void;
|
|
81
84
|
onAnalyticsEvent(callback: ((event: AnalyticsEvent) => void) | null): void;
|
|
85
|
+
onChargeRequest(callback: ((request: ChargeRequest) => void) | null): void;
|
|
82
86
|
isReady(): boolean;
|
|
83
87
|
isComplete(): boolean;
|
|
84
88
|
isPlaying(): boolean;
|
|
@@ -92,6 +96,9 @@ export default class PresentationWindow {
|
|
|
92
96
|
reload(): Promise<void>;
|
|
93
97
|
runGetProperties(builderState?: BuilderState): void;
|
|
94
98
|
createObjectURL(blob: Blob): Promise<string>;
|
|
99
|
+
sendChargeResponse(response: ChargeResponse): void;
|
|
100
|
+
sendCharge(charge: Charge): void;
|
|
101
|
+
sendTerminal(terminal: Terminal | null): void;
|
|
95
102
|
private isEmbeddedApp;
|
|
96
103
|
private allChildWindowManagersReady;
|
|
97
104
|
private checkIsReadyCallback;
|
|
@@ -81,7 +81,7 @@ var PlaylistWindowManager_1 = __importDefault(require("./PlaylistWindowManager")
|
|
|
81
81
|
var WindowTimeoutError_1 = __importDefault(require("./WindowTimeoutError"));
|
|
82
82
|
var WindowLoadError_1 = __importDefault(require("./WindowLoadError"));
|
|
83
83
|
// TODO: Automatically inject latest version when building in CI.
|
|
84
|
-
var defaultAppRendererUrl = 'https://apps.raydiant.com/3.0.
|
|
84
|
+
var defaultAppRendererUrl = 'https://apps.raydiant.com/3.0.4/index.html';
|
|
85
85
|
var isValidNumber = function (value) {
|
|
86
86
|
return typeof value === 'number' && !isNaN(value);
|
|
87
87
|
};
|
|
@@ -94,6 +94,18 @@ var isValidAnalyticsEvent = function (value) {
|
|
|
94
94
|
value.productCategory === undefined ||
|
|
95
95
|
value.productCategory === null);
|
|
96
96
|
};
|
|
97
|
+
var isValidChargeRequest = function (value) {
|
|
98
|
+
return value &&
|
|
99
|
+
typeof value === 'object' &&
|
|
100
|
+
typeof value.requestId === 'string' &&
|
|
101
|
+
typeof value.amount === 'number' &&
|
|
102
|
+
(value.tags === undefined ||
|
|
103
|
+
value.tags === null ||
|
|
104
|
+
typeof value.tags === 'object') &&
|
|
105
|
+
(value.idempotencyId === undefined ||
|
|
106
|
+
value.idempotencyId === null ||
|
|
107
|
+
typeof value.idempotencyId === 'string');
|
|
108
|
+
};
|
|
97
109
|
var PresentationWindow = /** @class */ (function () {
|
|
98
110
|
function PresentationWindow(options) {
|
|
99
111
|
var _this = this;
|
|
@@ -114,6 +126,7 @@ var PresentationWindow = /** @class */ (function () {
|
|
|
114
126
|
this.enableInterruptLockCallback = null;
|
|
115
127
|
this.disableInterruptLockCallback = null;
|
|
116
128
|
this.analyticsEventCallback = null;
|
|
129
|
+
this.chargeRequestCallback = null;
|
|
117
130
|
this.forceMute = false;
|
|
118
131
|
this.error = null;
|
|
119
132
|
this.workerState = {};
|
|
@@ -121,6 +134,7 @@ var PresentationWindow = /** @class */ (function () {
|
|
|
121
134
|
this.runtimeSubscriptions = new Set();
|
|
122
135
|
this.previewSubscriptions = new Set();
|
|
123
136
|
this.previewState = { inputs: {} };
|
|
137
|
+
this.paymentSubscriptions = new Set();
|
|
124
138
|
this.createObjectURLResolvers = {};
|
|
125
139
|
this.createObjectURLTimeouts = {};
|
|
126
140
|
this.handleEmbeddedMessage = function (type, payload) { return __awaiter(_this, void 0, void 0, function () {
|
|
@@ -200,8 +214,13 @@ var PresentationWindow = /** @class */ (function () {
|
|
|
200
214
|
break;
|
|
201
215
|
}
|
|
202
216
|
case 'analytics-event': {
|
|
203
|
-
if (
|
|
204
|
-
this.analyticsEventCallback
|
|
217
|
+
if (isValidAnalyticsEvent(payload)) {
|
|
218
|
+
if (this.analyticsEventCallback) {
|
|
219
|
+
this.analyticsEventCallback(payload);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
this.logWarning("Invalid payload for analytics-event message: ".concat(payload));
|
|
205
224
|
}
|
|
206
225
|
break;
|
|
207
226
|
}
|
|
@@ -245,6 +264,44 @@ var PresentationWindow = /** @class */ (function () {
|
|
|
245
264
|
}
|
|
246
265
|
break;
|
|
247
266
|
}
|
|
267
|
+
case 'payments-charge-request': {
|
|
268
|
+
if (isValidChargeRequest(payload)) {
|
|
269
|
+
if (this.chargeRequestCallback) {
|
|
270
|
+
this.chargeRequestCallback(payload);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
this.logWarning("Invalid payload for payments-charge-request message: ".concat(payload));
|
|
275
|
+
}
|
|
276
|
+
break;
|
|
277
|
+
}
|
|
278
|
+
case 'payments-subscribe': {
|
|
279
|
+
if (typeof payload === 'string') {
|
|
280
|
+
this.logInfo("Received 'worker-subscribe' from window '".concat(windowId, "'"));
|
|
281
|
+
// The payload is the payment subscription type (ex. 'charge', 'terminal')
|
|
282
|
+
this.paymentSubscriptions.add(payload);
|
|
283
|
+
// Immediately send terminal when subscribing.
|
|
284
|
+
if (payload === 'terminal' && this.options.getTerminal) {
|
|
285
|
+
this.sendMessageObject('payments-terminal-event', {
|
|
286
|
+
terminal: this.options.getTerminal(),
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
this.logWarning("Invalid payload for payments-subscribe message: ".concat(payload));
|
|
292
|
+
}
|
|
293
|
+
break;
|
|
294
|
+
}
|
|
295
|
+
case 'payments-unsubscribe': {
|
|
296
|
+
if (typeof payload === 'string') {
|
|
297
|
+
this.logInfo("Received 'payments-unsubscribe' from window '".concat(windowId, "'"));
|
|
298
|
+
this.paymentSubscriptions.delete(payload);
|
|
299
|
+
}
|
|
300
|
+
else {
|
|
301
|
+
this.logWarning("Invalid payload for payments-unsubscribe message: ".concat(payload));
|
|
302
|
+
}
|
|
303
|
+
break;
|
|
304
|
+
}
|
|
248
305
|
default: {
|
|
249
306
|
this.logWarning("Received unknown embedded message type: ".concat(type));
|
|
250
307
|
}
|
|
@@ -480,6 +537,9 @@ var PresentationWindow = /** @class */ (function () {
|
|
|
480
537
|
PresentationWindow.prototype.onAnalyticsEvent = function (callback) {
|
|
481
538
|
this.analyticsEventCallback = callback;
|
|
482
539
|
};
|
|
540
|
+
PresentationWindow.prototype.onChargeRequest = function (callback) {
|
|
541
|
+
this.chargeRequestCallback = callback;
|
|
542
|
+
};
|
|
483
543
|
PresentationWindow.prototype.isReady = function () {
|
|
484
544
|
// TODO: To support the 'ready' event in embedded apps, they will need to tell the
|
|
485
545
|
// runtime that it should wait for ready instead of DOMLoaded. This could be a "wait
|
|
@@ -571,6 +631,39 @@ var PresentationWindow = /** @class */ (function () {
|
|
|
571
631
|
});
|
|
572
632
|
});
|
|
573
633
|
};
|
|
634
|
+
PresentationWindow.prototype.sendChargeResponse = function (response) {
|
|
635
|
+
// TODO: Instead of sending the charge to all windows, we should only send it to the app
|
|
636
|
+
// that requested the charge. We probably don't want to use the windowId to avoid missing
|
|
637
|
+
// the events in case the window was closed/restarted. Since we are only sending the charge
|
|
638
|
+
// id and status this seems ok for now but need to be mindful of sending any sensitive data.
|
|
639
|
+
this.sendMessageObject('payments-charge-response', {
|
|
640
|
+
requestId: response.requestId,
|
|
641
|
+
charge: response.charge,
|
|
642
|
+
error: response.error,
|
|
643
|
+
});
|
|
644
|
+
// Forward the event to the child window managers.
|
|
645
|
+
Object.values(this.windowManagers).forEach(function (wm) {
|
|
646
|
+
return wm.sendChargeResponse(response);
|
|
647
|
+
});
|
|
648
|
+
};
|
|
649
|
+
PresentationWindow.prototype.sendCharge = function (charge) {
|
|
650
|
+
// TODO: Instead of sending the charge to all windows, we should only send it to the app
|
|
651
|
+
// that requested the charge.
|
|
652
|
+
if (this.paymentSubscriptions.has('charge')) {
|
|
653
|
+
this.sendMessageObject('payments-charge-event', { charge: charge });
|
|
654
|
+
}
|
|
655
|
+
// Forward the event to the child window managers.
|
|
656
|
+
Object.values(this.windowManagers).forEach(function (wm) { return wm.sendCharge(charge); });
|
|
657
|
+
};
|
|
658
|
+
PresentationWindow.prototype.sendTerminal = function (terminal) {
|
|
659
|
+
if (this.paymentSubscriptions.has('terminal')) {
|
|
660
|
+
this.sendMessageObject('payments-terminal-event', { terminal: terminal });
|
|
661
|
+
}
|
|
662
|
+
// Forward the event to the child window managers.
|
|
663
|
+
Object.values(this.windowManagers).forEach(function (wm) {
|
|
664
|
+
return wm.sendTerminal(terminal);
|
|
665
|
+
});
|
|
666
|
+
};
|
|
574
667
|
PresentationWindow.prototype.isEmbeddedApp = function () {
|
|
575
668
|
var _a = this.options, applicationVersion = _a.applicationVersion, isPreview = _a.isPreview;
|
|
576
669
|
// If the application has a source url, always use it for rendering the preview.
|
|
@@ -859,6 +952,7 @@ var PresentationWindow = /** @class */ (function () {
|
|
|
859
952
|
isPreview: isPreview,
|
|
860
953
|
onEnableInterruptLock: function () { return _this.enableInteruptLock(windowName); },
|
|
861
954
|
onDisableInterruptLock: function () { return _this.disableInteruptLock(windowName); },
|
|
955
|
+
getTerminal: this.options.getTerminal,
|
|
862
956
|
});
|
|
863
957
|
// Hook up child window manager events to bubble up to the parent.
|
|
864
958
|
if (parentWindowManager) {
|
|
@@ -877,6 +971,9 @@ var PresentationWindow = /** @class */ (function () {
|
|
|
877
971
|
windowManager.on('window_analytics_event', function (metadata) {
|
|
878
972
|
parentWindowManager.emit('window_analytics_event', metadata);
|
|
879
973
|
});
|
|
974
|
+
windowManager.on('window_charge_request', function (metadata) {
|
|
975
|
+
parentWindowManager.emit('window_charge_request', metadata);
|
|
976
|
+
});
|
|
880
977
|
}
|
|
881
978
|
this.windowManagers[windowName] = windowManager;
|
|
882
979
|
windowManager.setRenderer(renderer);
|
|
@@ -238,4 +238,103 @@ describe('embedded apps', function () {
|
|
|
238
238
|
});
|
|
239
239
|
}); });
|
|
240
240
|
});
|
|
241
|
+
describe('payments', function () {
|
|
242
|
+
test('charge request', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
243
|
+
var testRenderer, window, callback;
|
|
244
|
+
return __generator(this, function (_a) {
|
|
245
|
+
switch (_a.label) {
|
|
246
|
+
case 0:
|
|
247
|
+
testRenderer = (0, testUtils_1.createRenderer)();
|
|
248
|
+
window = (0, testUtils_1.createPresentationWindow)({
|
|
249
|
+
windowId: 'test-charge-request',
|
|
250
|
+
presentation: (0, testUtils_1.createPresentation)(),
|
|
251
|
+
applicationVersion: (0, testUtils_1.createApplicationVersion)({
|
|
252
|
+
embeddedUrlFormat: 'http://test.local/:input',
|
|
253
|
+
}),
|
|
254
|
+
renderer: testRenderer,
|
|
255
|
+
});
|
|
256
|
+
callback = jest.fn();
|
|
257
|
+
window.onChargeRequest(callback);
|
|
258
|
+
return [4 /*yield*/, window.load()];
|
|
259
|
+
case 1:
|
|
260
|
+
_a.sent();
|
|
261
|
+
testRenderer.runReceiveMessageCallback('payments-charge-request', {
|
|
262
|
+
requestId: 'test',
|
|
263
|
+
amount: 100,
|
|
264
|
+
});
|
|
265
|
+
expect(callback).toHaveBeenCalledWith({
|
|
266
|
+
requestId: 'test',
|
|
267
|
+
amount: 100,
|
|
268
|
+
});
|
|
269
|
+
// Send charge response.
|
|
270
|
+
window.sendChargeResponse({
|
|
271
|
+
requestId: 'test',
|
|
272
|
+
charge: { id: '1', status: 'pending' },
|
|
273
|
+
});
|
|
274
|
+
expect(testRenderer.sendMessageObject).toHaveBeenCalledWith('test-charge-request', 'payments-charge-response', { requestId: 'test', charge: { id: '1', status: 'pending' } });
|
|
275
|
+
return [2 /*return*/];
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
}); });
|
|
279
|
+
test('subscribe to charge', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
280
|
+
var testRenderer, window;
|
|
281
|
+
return __generator(this, function (_a) {
|
|
282
|
+
switch (_a.label) {
|
|
283
|
+
case 0:
|
|
284
|
+
testRenderer = (0, testUtils_1.createRenderer)();
|
|
285
|
+
window = (0, testUtils_1.createPresentationWindow)({
|
|
286
|
+
windowId: 'test-payments-subscribe-charge',
|
|
287
|
+
presentation: (0, testUtils_1.createPresentation)(),
|
|
288
|
+
applicationVersion: (0, testUtils_1.createApplicationVersion)({
|
|
289
|
+
embeddedUrlFormat: 'http://test.local/:input',
|
|
290
|
+
}),
|
|
291
|
+
renderer: testRenderer,
|
|
292
|
+
});
|
|
293
|
+
return [4 /*yield*/, window.load()];
|
|
294
|
+
case 1:
|
|
295
|
+
_a.sent();
|
|
296
|
+
testRenderer.runReceiveMessageCallback('payments-subscribe', 'charge');
|
|
297
|
+
window.sendCharge({ id: 'test', status: 'success' });
|
|
298
|
+
expect(testRenderer.sendMessageObject).toHaveBeenCalledWith('test-payments-subscribe-charge', 'payments-charge-event', { charge: { id: 'test', status: 'success' } });
|
|
299
|
+
// Unsubscribe should not send a message.
|
|
300
|
+
testRenderer.runReceiveMessageCallback('payments-unsubscribe', 'charge');
|
|
301
|
+
window.sendCharge({ id: 'test2', status: 'success' });
|
|
302
|
+
expect(testRenderer.sendMessageObject).toHaveBeenCalledTimes(1);
|
|
303
|
+
return [2 /*return*/];
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
}); });
|
|
307
|
+
test('subscribe to terminal', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
308
|
+
var testRenderer, window;
|
|
309
|
+
return __generator(this, function (_a) {
|
|
310
|
+
switch (_a.label) {
|
|
311
|
+
case 0:
|
|
312
|
+
testRenderer = (0, testUtils_1.createRenderer)();
|
|
313
|
+
window = (0, testUtils_1.createPresentationWindow)({
|
|
314
|
+
windowId: 'test-payments-subscribe-terminal',
|
|
315
|
+
presentation: (0, testUtils_1.createPresentation)(),
|
|
316
|
+
applicationVersion: (0, testUtils_1.createApplicationVersion)({
|
|
317
|
+
embeddedUrlFormat: 'http://test.local/:input',
|
|
318
|
+
}),
|
|
319
|
+
renderer: testRenderer,
|
|
320
|
+
getTerminal: function () { return null; },
|
|
321
|
+
});
|
|
322
|
+
return [4 /*yield*/, window.load()];
|
|
323
|
+
case 1:
|
|
324
|
+
_a.sent();
|
|
325
|
+
testRenderer.runReceiveMessageCallback('payments-subscribe', 'terminal');
|
|
326
|
+
// Terminal subscriptions should receive the current terminal immediately after subscribing.
|
|
327
|
+
expect(testRenderer.sendMessageObject).toHaveBeenCalledWith('test-payments-subscribe-terminal', 'payments-terminal-event', { terminal: null });
|
|
328
|
+
// Send terminal update event.
|
|
329
|
+
window.sendTerminal({ status: 'open' });
|
|
330
|
+
expect(testRenderer.sendMessageObject).toHaveBeenCalledWith('test-payments-subscribe-terminal', 'payments-terminal-event', { terminal: { status: 'open' } });
|
|
331
|
+
// Unsubscribe should not send a message.
|
|
332
|
+
testRenderer.runReceiveMessageCallback('payments-unsubscribe', 'terminal');
|
|
333
|
+
window.sendTerminal({ status: 'closed' });
|
|
334
|
+
expect(testRenderer.sendMessageObject).toHaveBeenCalledTimes(2);
|
|
335
|
+
return [2 /*return*/];
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
}); });
|
|
339
|
+
});
|
|
241
340
|
// TODO: Backfill tests for all legacy and embedded apps.
|
package/build/testUtils.js
CHANGED
package/build/types.d.ts
CHANGED
|
@@ -140,3 +140,23 @@ export interface AnalyticsEvent {
|
|
|
140
140
|
source: string;
|
|
141
141
|
productCategory?: string | null;
|
|
142
142
|
}
|
|
143
|
+
export interface ChargeRequest {
|
|
144
|
+
requestId: string;
|
|
145
|
+
amount: number;
|
|
146
|
+
tags?: Record<string, undefined | null | boolean | number | string>;
|
|
147
|
+
idempotencyId?: string;
|
|
148
|
+
}
|
|
149
|
+
export interface Charge {
|
|
150
|
+
id: string;
|
|
151
|
+
status: string;
|
|
152
|
+
}
|
|
153
|
+
export interface ChargeResponse {
|
|
154
|
+
requestId: string;
|
|
155
|
+
charge?: Charge;
|
|
156
|
+
error?: {
|
|
157
|
+
message: string;
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
export interface Terminal {
|
|
161
|
+
status: string;
|
|
162
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@raydiant/playlist-window-manager",
|
|
3
|
-
"version": "0.22.0
|
|
3
|
+
"version": "0.22.0",
|
|
4
4
|
"description": "Window manager for playlist playback.",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"@types/jschannel": "^1.0.1",
|
|
23
23
|
"@types/jest": "^24.0.15",
|
|
24
24
|
"@types/uuid": "8.3.0",
|
|
25
|
-
"electron": "
|
|
25
|
+
"electron": "10.1.5",
|
|
26
26
|
"jest": "^24.8.0",
|
|
27
27
|
"ts-jest": "^24.0.2",
|
|
28
28
|
"typescript": "^4.1.2"
|
|
@@ -46,11 +46,6 @@ interface BrowserWindowRendererOptions {
|
|
|
46
46
|
windowCrashHandler?: () => void;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
const globalWebPreferences = {
|
|
50
|
-
// nodeIntegration: true,
|
|
51
|
-
contextIsolation: false,
|
|
52
|
-
};
|
|
53
|
-
|
|
54
49
|
export default class BrowserWindowRenderer
|
|
55
50
|
implements PresentationWindowRenderer
|
|
56
51
|
{
|
|
@@ -69,7 +64,6 @@ export default class BrowserWindowRenderer
|
|
|
69
64
|
hasShadow: this.opts.hasShadow,
|
|
70
65
|
webPreferences: {
|
|
71
66
|
preload: path.join(__dirname, 'preload.js'),
|
|
72
|
-
...globalWebPreferences,
|
|
73
67
|
},
|
|
74
68
|
});
|
|
75
69
|
|
|
@@ -107,7 +101,6 @@ export default class BrowserWindowRenderer
|
|
|
107
101
|
? this.createOrGetPooledBrowserView(windowId, {
|
|
108
102
|
webPreferences: {
|
|
109
103
|
preload: path.join(__dirname, 'preload.js'),
|
|
110
|
-
...globalWebPreferences,
|
|
111
104
|
},
|
|
112
105
|
})
|
|
113
106
|
: this.createOrGetPooledBrowserWindow(windowId, {
|
|
@@ -122,7 +115,6 @@ export default class BrowserWindowRenderer
|
|
|
122
115
|
hasShadow: this.opts.hasShadow,
|
|
123
116
|
webPreferences: {
|
|
124
117
|
preload: path.join(__dirname, 'preload.js'),
|
|
125
|
-
...globalWebPreferences,
|
|
126
118
|
},
|
|
127
119
|
}),
|
|
128
120
|
// If we are creating a child window and renderChildWindowsAsBrowserViews is false that means
|
|
@@ -177,12 +169,12 @@ export default class BrowserWindowRenderer
|
|
|
177
169
|
|
|
178
170
|
// Attached crash handlers to skip presentation on error.
|
|
179
171
|
window.view.webContents.on(
|
|
180
|
-
// FIXME
|
|
181
|
-
// @ts-ignore
|
|
182
172
|
'render-process-gone',
|
|
183
173
|
window.renderProcessGoneCallback
|
|
184
174
|
);
|
|
175
|
+
|
|
185
176
|
window.view.webContents.on('unresponsive', window.unresponsiveCallback);
|
|
177
|
+
|
|
186
178
|
if (window.view instanceof BrowserView) {
|
|
187
179
|
this.moveWindowOffscreen(window);
|
|
188
180
|
}
|
|
@@ -190,6 +182,7 @@ export default class BrowserWindowRenderer
|
|
|
190
182
|
this.windows[windowId] = window;
|
|
191
183
|
// Listen for messages from window.
|
|
192
184
|
ipcMain.on(windowId, window.receiveMessageCallback);
|
|
185
|
+
|
|
193
186
|
if (backgroundColor) {
|
|
194
187
|
// To make a transparent window you need to set it's value to #00000000
|
|
195
188
|
// Electron issue: https://github.com/electron/electron/issues/20357
|
|
@@ -286,8 +279,6 @@ export default class BrowserWindowRenderer
|
|
|
286
279
|
|
|
287
280
|
// Remove crash handlers
|
|
288
281
|
window.view.webContents.removeListener(
|
|
289
|
-
// FIXME
|
|
290
|
-
// @ts-ignore
|
|
291
282
|
'render-process-gone',
|
|
292
283
|
window.renderProcessGoneCallback
|
|
293
284
|
);
|
|
@@ -8,7 +8,11 @@ import {
|
|
|
8
8
|
Theme,
|
|
9
9
|
Logger,
|
|
10
10
|
WorkerState,
|
|
11
|
-
AnalyticsEvent
|
|
11
|
+
AnalyticsEvent,
|
|
12
|
+
Charge,
|
|
13
|
+
ChargeRequest,
|
|
14
|
+
ChargeResponse,
|
|
15
|
+
Terminal,
|
|
12
16
|
} from './types';
|
|
13
17
|
import PresentationWindow from './PresentationWindow';
|
|
14
18
|
import PlaylistWindowManagerState from './PlaylistWindowManagerState';
|
|
@@ -67,6 +71,9 @@ export interface PlaylistWindowManagerOptions {
|
|
|
67
71
|
|
|
68
72
|
// Called when the current window wants to disable the interrupt lock
|
|
69
73
|
onDisableInterruptLock?: () => void;
|
|
74
|
+
|
|
75
|
+
// Called when a window needs the current terminal object.
|
|
76
|
+
getTerminal?: () => Terminal | null;
|
|
70
77
|
}
|
|
71
78
|
|
|
72
79
|
// Wait at least 3 seconds before restarting or moving onto the next window
|
|
@@ -440,6 +447,43 @@ export default class PlaylistWindowManager extends EventEmitter {
|
|
|
440
447
|
}
|
|
441
448
|
}
|
|
442
449
|
|
|
450
|
+
public sendChargeResponse(response: ChargeResponse) {
|
|
451
|
+
// Send charge response to the current window. The window will check if
|
|
452
|
+
// has a matching request id and if so, send the response to the app.
|
|
453
|
+
if (this.currentWindow) {
|
|
454
|
+
this.currentWindow.sendChargeResponse(response);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Send charge response to the next window.
|
|
458
|
+
if (this.nextWindow) {
|
|
459
|
+
this.nextWindow.sendChargeResponse(response);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
public sendCharge(charge: Charge) {
|
|
464
|
+
// Send charge to the current window.
|
|
465
|
+
if (this.currentWindow) {
|
|
466
|
+
this.currentWindow.sendCharge(charge);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// Send charge to the next window.
|
|
470
|
+
if (this.nextWindow) {
|
|
471
|
+
this.nextWindow.sendCharge(charge);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
public sendTerminal(terminal: Terminal | null) {
|
|
476
|
+
// Send terminal to the current window.
|
|
477
|
+
if (this.currentWindow) {
|
|
478
|
+
this.currentWindow.sendTerminal(terminal);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Send terminal to the next window.
|
|
482
|
+
if (this.nextWindow) {
|
|
483
|
+
this.nextWindow.sendTerminal(terminal);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
443
487
|
private createWindow(pr: RE.NextPresentationID): PresentationWindow {
|
|
444
488
|
const {
|
|
445
489
|
readyTimeoutDurationSeconds,
|
|
@@ -449,7 +493,7 @@ export default class PlaylistWindowManager extends EventEmitter {
|
|
|
449
493
|
logger,
|
|
450
494
|
windowName,
|
|
451
495
|
parentWindowId,
|
|
452
|
-
isPreview
|
|
496
|
+
isPreview,
|
|
453
497
|
} = this.options;
|
|
454
498
|
|
|
455
499
|
if (!this.ruleEngine) {
|
|
@@ -503,7 +547,8 @@ export default class PlaylistWindowManager extends EventEmitter {
|
|
|
503
547
|
ruleEngineState: this.ruleEngine.state,
|
|
504
548
|
globalRule: this.ruleEngine.globalRule,
|
|
505
549
|
isPreview,
|
|
506
|
-
workerState: this.workerState
|
|
550
|
+
workerState: this.workerState,
|
|
551
|
+
getTerminal: this.options.getTerminal,
|
|
507
552
|
});
|
|
508
553
|
}
|
|
509
554
|
|
|
@@ -523,6 +568,7 @@ export default class PlaylistWindowManager extends EventEmitter {
|
|
|
523
568
|
this.currentWindow.onEnableInterruptLock(this.handleEnableInterruptLock);
|
|
524
569
|
this.currentWindow.onDisableInterruptLock(this.handleDisableInterruptLock);
|
|
525
570
|
this.currentWindow.onAnalyticsEvent(this.handleAnalyticsEvent);
|
|
571
|
+
this.currentWindow.onChargeRequest(this.handleChargeRequest);
|
|
526
572
|
|
|
527
573
|
await this.currentWindow.load();
|
|
528
574
|
}
|
|
@@ -695,6 +741,7 @@ export default class PlaylistWindowManager extends EventEmitter {
|
|
|
695
741
|
this.handleDisableInterruptLock
|
|
696
742
|
);
|
|
697
743
|
this.currentWindow.onAnalyticsEvent(this.handleAnalyticsEvent);
|
|
744
|
+
this.currentWindow.onChargeRequest(this.handleChargeRequest);
|
|
698
745
|
}
|
|
699
746
|
|
|
700
747
|
// Play new current window.
|
|
@@ -764,6 +811,12 @@ export default class PlaylistWindowManager extends EventEmitter {
|
|
|
764
811
|
this.emitCurrentWindowEvent('window_analytics_event', event);
|
|
765
812
|
};
|
|
766
813
|
|
|
814
|
+
private handleChargeRequest = (request: ChargeRequest) => {
|
|
815
|
+
// Emit the event on the current window. Only the current window should
|
|
816
|
+
// send charge requests since the next window is not visible.
|
|
817
|
+
this.emitCurrentWindowEvent('window_charge_request', request);
|
|
818
|
+
};
|
|
819
|
+
|
|
767
820
|
private handleCurrentPresentationComplete = async () => {
|
|
768
821
|
const { logger } = this.options;
|
|
769
822
|
|
|
@@ -1005,7 +1058,7 @@ export default class PlaylistWindowManager extends EventEmitter {
|
|
|
1005
1058
|
if (window) {
|
|
1006
1059
|
this.emit(event, {
|
|
1007
1060
|
...window.metadata(),
|
|
1008
|
-
data
|
|
1061
|
+
data,
|
|
1009
1062
|
});
|
|
1010
1063
|
} else {
|
|
1011
1064
|
logger.warn(`Attempted to emit ${label} window event but window not set`);
|
|
@@ -1022,13 +1075,13 @@ export default class PlaylistWindowManager extends EventEmitter {
|
|
|
1022
1075
|
|
|
1023
1076
|
private emitCurrentWindowErrorEvent(event: string, error: any) {
|
|
1024
1077
|
return this.emitCurrentWindowEvent(event, {
|
|
1025
|
-
errorMessage: this.getErrorMessage(error)
|
|
1078
|
+
errorMessage: this.getErrorMessage(error),
|
|
1026
1079
|
});
|
|
1027
1080
|
}
|
|
1028
1081
|
|
|
1029
1082
|
private emitNextWindowErrorEvent(event: string, error: any) {
|
|
1030
1083
|
return this.emitNextWindowEvent(event, {
|
|
1031
|
-
errorMessage: this.getErrorMessage(error)
|
|
1084
|
+
errorMessage: this.getErrorMessage(error),
|
|
1032
1085
|
});
|
|
1033
1086
|
}
|
|
1034
1087
|
|
|
@@ -218,4 +218,116 @@ describe('embedded apps', () => {
|
|
|
218
218
|
});
|
|
219
219
|
});
|
|
220
220
|
|
|
221
|
+
describe('payments', () => {
|
|
222
|
+
test('charge request', async () => {
|
|
223
|
+
const testRenderer = createRenderer();
|
|
224
|
+
|
|
225
|
+
const window = createPresentationWindow({
|
|
226
|
+
windowId: 'test-charge-request',
|
|
227
|
+
presentation: createPresentation(),
|
|
228
|
+
applicationVersion: createApplicationVersion({
|
|
229
|
+
embeddedUrlFormat: 'http://test.local/:input',
|
|
230
|
+
}),
|
|
231
|
+
renderer: testRenderer,
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
const callback = jest.fn();
|
|
235
|
+
window.onChargeRequest(callback);
|
|
236
|
+
|
|
237
|
+
await window.load();
|
|
238
|
+
|
|
239
|
+
testRenderer.runReceiveMessageCallback('payments-charge-request', {
|
|
240
|
+
requestId: 'test',
|
|
241
|
+
amount: 100,
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
expect(callback).toHaveBeenCalledWith({
|
|
245
|
+
requestId: 'test',
|
|
246
|
+
amount: 100,
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
// Send charge response.
|
|
250
|
+
|
|
251
|
+
window.sendChargeResponse({
|
|
252
|
+
requestId: 'test',
|
|
253
|
+
charge: { id: '1', status: 'pending' },
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
expect(testRenderer.sendMessageObject).toHaveBeenCalledWith(
|
|
257
|
+
'test-charge-request',
|
|
258
|
+
'payments-charge-response',
|
|
259
|
+
{ requestId: 'test', charge: { id: '1', status: 'pending' } }
|
|
260
|
+
);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
test('subscribe to charge', async () => {
|
|
264
|
+
const testRenderer = createRenderer();
|
|
265
|
+
|
|
266
|
+
const window = createPresentationWindow({
|
|
267
|
+
windowId: 'test-payments-subscribe-charge',
|
|
268
|
+
presentation: createPresentation(),
|
|
269
|
+
applicationVersion: createApplicationVersion({
|
|
270
|
+
embeddedUrlFormat: 'http://test.local/:input',
|
|
271
|
+
}),
|
|
272
|
+
renderer: testRenderer,
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
await window.load();
|
|
276
|
+
|
|
277
|
+
testRenderer.runReceiveMessageCallback('payments-subscribe', 'charge');
|
|
278
|
+
|
|
279
|
+
window.sendCharge({ id: 'test', status: 'success' });
|
|
280
|
+
|
|
281
|
+
expect(testRenderer.sendMessageObject).toHaveBeenCalledWith(
|
|
282
|
+
'test-payments-subscribe-charge',
|
|
283
|
+
'payments-charge-event',
|
|
284
|
+
{ charge: { id: 'test', status: 'success' } }
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
// Unsubscribe should not send a message.
|
|
288
|
+
testRenderer.runReceiveMessageCallback('payments-unsubscribe', 'charge');
|
|
289
|
+
window.sendCharge({ id: 'test2', status: 'success' });
|
|
290
|
+
expect(testRenderer.sendMessageObject).toHaveBeenCalledTimes(1);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
test('subscribe to terminal', async () => {
|
|
294
|
+
const testRenderer = createRenderer();
|
|
295
|
+
|
|
296
|
+
const window = createPresentationWindow({
|
|
297
|
+
windowId: 'test-payments-subscribe-terminal',
|
|
298
|
+
presentation: createPresentation(),
|
|
299
|
+
applicationVersion: createApplicationVersion({
|
|
300
|
+
embeddedUrlFormat: 'http://test.local/:input',
|
|
301
|
+
}),
|
|
302
|
+
renderer: testRenderer,
|
|
303
|
+
getTerminal: () => null,
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
await window.load();
|
|
307
|
+
|
|
308
|
+
testRenderer.runReceiveMessageCallback('payments-subscribe', 'terminal');
|
|
309
|
+
|
|
310
|
+
// Terminal subscriptions should receive the current terminal immediately after subscribing.
|
|
311
|
+
expect(testRenderer.sendMessageObject).toHaveBeenCalledWith(
|
|
312
|
+
'test-payments-subscribe-terminal',
|
|
313
|
+
'payments-terminal-event',
|
|
314
|
+
{ terminal: null }
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
// Send terminal update event.
|
|
318
|
+
window.sendTerminal({ status: 'open' });
|
|
319
|
+
|
|
320
|
+
expect(testRenderer.sendMessageObject).toHaveBeenCalledWith(
|
|
321
|
+
'test-payments-subscribe-terminal',
|
|
322
|
+
'payments-terminal-event',
|
|
323
|
+
{ terminal: { status: 'open' } }
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
// Unsubscribe should not send a message.
|
|
327
|
+
testRenderer.runReceiveMessageCallback('payments-unsubscribe', 'terminal');
|
|
328
|
+
window.sendTerminal({ status: 'closed' });
|
|
329
|
+
expect(testRenderer.sendMessageObject).toHaveBeenCalledTimes(2);
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
|
|
221
333
|
// TODO: Backfill tests for all legacy and embedded apps.
|
|
@@ -20,6 +20,10 @@ import {
|
|
|
20
20
|
EmbeddedAppMetadata,
|
|
21
21
|
AnalyticsEvent,
|
|
22
22
|
PreviewState,
|
|
23
|
+
Charge,
|
|
24
|
+
ChargeRequest,
|
|
25
|
+
ChargeResponse,
|
|
26
|
+
Terminal,
|
|
23
27
|
} from './types';
|
|
24
28
|
|
|
25
29
|
export interface PresentationWindowOptions {
|
|
@@ -52,10 +56,11 @@ export interface PresentationWindowOptions {
|
|
|
52
56
|
properties: any[],
|
|
53
57
|
string: { [key: string]: string }
|
|
54
58
|
) => void;
|
|
59
|
+
getTerminal?: () => Terminal | null;
|
|
55
60
|
}
|
|
56
61
|
|
|
57
62
|
// TODO: Automatically inject latest version when building in CI.
|
|
58
|
-
const defaultAppRendererUrl = 'https://apps.raydiant.com/3.0.
|
|
63
|
+
const defaultAppRendererUrl = 'https://apps.raydiant.com/3.0.4/index.html';
|
|
59
64
|
|
|
60
65
|
const isValidNumber = (value: any) =>
|
|
61
66
|
typeof value === 'number' && !isNaN(value);
|
|
@@ -69,6 +74,18 @@ const isValidAnalyticsEvent = (value: any): value is AnalyticsEvent =>
|
|
|
69
74
|
value.productCategory === undefined ||
|
|
70
75
|
value.productCategory === null);
|
|
71
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
|
+
|
|
72
89
|
export default class PresentationWindow {
|
|
73
90
|
private didReceiveReady: boolean = false;
|
|
74
91
|
private readyCallback: (() => void) | null = null;
|
|
@@ -88,6 +105,8 @@ export default class PresentationWindow {
|
|
|
88
105
|
private disableInterruptLockCallback: (() => void) | null = null;
|
|
89
106
|
private analyticsEventCallback: ((event: AnalyticsEvent) => void) | null =
|
|
90
107
|
null;
|
|
108
|
+
private chargeRequestCallback: ((request: ChargeRequest) => void) | null =
|
|
109
|
+
null;
|
|
91
110
|
|
|
92
111
|
private builderState: BuilderState | undefined;
|
|
93
112
|
private presentation: Presentation;
|
|
@@ -102,6 +121,7 @@ export default class PresentationWindow {
|
|
|
102
121
|
private runtimeSubscriptions = new Set<string>();
|
|
103
122
|
private previewSubscriptions = new Set<string>();
|
|
104
123
|
private previewState: PreviewState = { inputs: {} };
|
|
124
|
+
private paymentSubscriptions = new Set<string>();
|
|
105
125
|
|
|
106
126
|
private createObjectURLResolvers: Record<string, (url: string) => void> = {};
|
|
107
127
|
private createObjectURLTimeouts: Record<
|
|
@@ -288,6 +308,10 @@ export default class PresentationWindow {
|
|
|
288
308
|
this.analyticsEventCallback = callback;
|
|
289
309
|
}
|
|
290
310
|
|
|
311
|
+
public onChargeRequest(callback: ((request: ChargeRequest) => void) | null) {
|
|
312
|
+
this.chargeRequestCallback = callback;
|
|
313
|
+
}
|
|
314
|
+
|
|
291
315
|
public isReady() {
|
|
292
316
|
// TODO: To support the 'ready' event in embedded apps, they will need to tell the
|
|
293
317
|
// runtime that it should wait for ready instead of DOMLoaded. This could be a "wait
|
|
@@ -394,6 +418,42 @@ export default class PresentationWindow {
|
|
|
394
418
|
return URL.createObjectURL(blob);
|
|
395
419
|
}
|
|
396
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
|
+
|
|
397
457
|
private isEmbeddedApp() {
|
|
398
458
|
const { applicationVersion, isPreview } = this.options;
|
|
399
459
|
|
|
@@ -714,6 +774,7 @@ export default class PresentationWindow {
|
|
|
714
774
|
isPreview,
|
|
715
775
|
onEnableInterruptLock: () => this.enableInteruptLock(windowName),
|
|
716
776
|
onDisableInterruptLock: () => this.disableInteruptLock(windowName),
|
|
777
|
+
getTerminal: this.options.getTerminal,
|
|
717
778
|
});
|
|
718
779
|
|
|
719
780
|
// Hook up child window manager events to bubble up to the parent.
|
|
@@ -733,6 +794,9 @@ export default class PresentationWindow {
|
|
|
733
794
|
windowManager.on('window_analytics_event', (metadata) => {
|
|
734
795
|
parentWindowManager.emit('window_analytics_event', metadata);
|
|
735
796
|
});
|
|
797
|
+
windowManager.on('window_charge_request', (metadata) => {
|
|
798
|
+
parentWindowManager.emit('window_charge_request', metadata);
|
|
799
|
+
});
|
|
736
800
|
}
|
|
737
801
|
|
|
738
802
|
this.windowManagers[windowName] = windowManager;
|
|
@@ -917,8 +981,14 @@ export default class PresentationWindow {
|
|
|
917
981
|
}
|
|
918
982
|
|
|
919
983
|
case 'analytics-event': {
|
|
920
|
-
if (
|
|
921
|
-
this.analyticsEventCallback
|
|
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
|
+
);
|
|
922
992
|
}
|
|
923
993
|
break;
|
|
924
994
|
}
|
|
@@ -973,6 +1043,53 @@ export default class PresentationWindow {
|
|
|
973
1043
|
break;
|
|
974
1044
|
}
|
|
975
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
|
+
|
|
976
1093
|
default: {
|
|
977
1094
|
this.logWarning(`Received unknown embedded message type: ${type}`);
|
|
978
1095
|
}
|
package/src/testUtils.ts
CHANGED
package/src/types.ts
CHANGED
|
@@ -173,3 +173,27 @@ export interface AnalyticsEvent {
|
|
|
173
173
|
source: string;
|
|
174
174
|
productCategory?: string | null;
|
|
175
175
|
}
|
|
176
|
+
|
|
177
|
+
export interface ChargeRequest {
|
|
178
|
+
requestId: string;
|
|
179
|
+
amount: number;
|
|
180
|
+
tags?: Record<string, undefined | null | boolean | number | string>;
|
|
181
|
+
idempotencyId?: string;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export interface Charge {
|
|
185
|
+
id: string;
|
|
186
|
+
status: string;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export interface ChargeResponse {
|
|
190
|
+
requestId: string;
|
|
191
|
+
charge?: Charge;
|
|
192
|
+
error?: {
|
|
193
|
+
message: string;
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export interface Terminal {
|
|
198
|
+
status: string;
|
|
199
|
+
}
|