@raydiant/playlist-window-manager 0.0.0-test-27 → 0.0.0-test-29

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.
@@ -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,6 +56,7 @@ 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.
@@ -69,32 +74,46 @@ 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
- private didReceiveReady = false;
90
+ private didReceiveReady: boolean = false;
74
91
  private readyCallback: (() => void) | null = null;
75
- private didReceiveComplete = false;
92
+ private didReceiveComplete: boolean = false;
76
93
  private completeCallback: (() => void) | null = null;
77
94
  private errorCallback: ((err: Error) => void) | null = null;
78
95
  private readyTimeout: ReturnType<typeof setTimeout> | null = null;
79
96
  private windowManagers: { [windowId: string]: PlaylistWindowManager } = {};
80
97
  private focusedWindowManager: PlaylistWindowManager | null = null;
81
- private didEmbeddedAppLoad = false;
98
+ private didEmbeddedAppLoad: boolean = false;
82
99
  private lastPlayedAt: Date | null = null;
83
100
  private configurableDurationTimeout: ReturnType<typeof setTimeout> | null =
84
101
  null;
85
- private isWindowLocked = false;
102
+ private isWindowLocked: boolean = false;
86
103
  private isChildWindowLocked: { [windowId: string]: boolean } = {};
87
104
  private enableInterruptLockCallback: (() => void) | null = null;
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;
94
113
  private playlistWindowManagerState: PlaylistWindowManagerState;
95
114
  private ruleEngineState: RE.State;
96
115
  private theme: Theme | undefined;
97
- private forceMute = false;
116
+ private forceMute: boolean = false;
98
117
  private error: Error | null = null;
99
118
 
100
119
  private workerState: WorkerState = {};
@@ -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<
@@ -126,7 +146,7 @@ export default class PresentationWindow {
126
146
  return this.loadLegacyApp();
127
147
  }
128
148
 
129
- public show(isFocused = true): void {
149
+ public show(isFocused: boolean = true): void {
130
150
  const { windowId, renderer } = this.options;
131
151
 
132
152
  // Show the main window and focus it before any child windows. Showing a child
@@ -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
 
@@ -490,19 +550,12 @@ export default class PresentationWindow {
490
550
  backgroundColor?: string,
491
551
  receiveMessageCallback?: (type: string, payload?: any) => void
492
552
  ) {
493
- const { windowId, parentWindowId, renderer, bounds, logger } = this.options;
494
-
495
- logger.info(
496
- `playlist-window-manager/src/PresentationWindow.ts createAndLoadWindow1: ${JSON.stringify(
497
- bounds
498
- )}`
499
- );
553
+ const { windowId, parentWindowId, renderer, bounds } = this.options;
500
554
 
501
555
  renderer.createWindow({
502
556
  windowId,
503
557
  parentWindowId,
504
558
  bounds,
505
- logger,
506
559
  backgroundColor,
507
560
  receiveMessageCallback,
508
561
  errorCallback: this.handleError,
@@ -646,8 +699,8 @@ export default class PresentationWindow {
646
699
  windowName: string,
647
700
  playlistId: string,
648
701
  bounds: WindowBounds,
649
- forceMute = false,
650
- isFocused = false,
702
+ forceMute: boolean = false,
703
+ isFocused: boolean = false,
651
704
  themeOverride?: any
652
705
  ) {
653
706
  const {
@@ -673,18 +726,7 @@ export default class PresentationWindow {
673
726
  ? !deepEqual(bounds, prevBounds)
674
727
  : true;
675
728
 
676
- this.logInfo(
677
- `playlist-window-manager/src/PresentationWindow.ts openChildWindow1: ${JSON.stringify(
678
- prevBounds
679
- )}`
680
- );
681
-
682
729
  if (didBoundsChange) {
683
- this.logInfo(
684
- `playlist-window-manager/src/PresentationWindow.ts openChildWindow2: ${JSON.stringify(
685
- bounds
686
- )}`
687
- );
688
730
  windowManager.updateBounds(bounds);
689
731
  }
690
732
 
@@ -711,11 +753,6 @@ export default class PresentationWindow {
711
753
  );
712
754
  }
713
755
  } else {
714
- this.logInfo(
715
- `playlist-window-manager/src/PresentationWindow.ts openChildWindow3: ${JSON.stringify(
716
- bounds
717
- )}`
718
- );
719
756
  windowManager = new PlaylistWindowManager({
720
757
  parentWindowId: windowId,
721
758
  windowName,
@@ -737,6 +774,7 @@ export default class PresentationWindow {
737
774
  isPreview,
738
775
  onEnableInterruptLock: () => this.enableInteruptLock(windowName),
739
776
  onDisableInterruptLock: () => this.disableInteruptLock(windowName),
777
+ getTerminal: this.options.getTerminal,
740
778
  });
741
779
 
742
780
  // Hook up child window manager events to bubble up to the parent.
@@ -756,6 +794,9 @@ export default class PresentationWindow {
756
794
  windowManager.on('window_analytics_event', (metadata) => {
757
795
  parentWindowManager.emit('window_analytics_event', metadata);
758
796
  });
797
+ windowManager.on('window_charge_request', (metadata) => {
798
+ parentWindowManager.emit('window_charge_request', metadata);
799
+ });
759
800
  }
760
801
 
761
802
  this.windowManagers[windowName] = windowManager;
@@ -940,8 +981,14 @@ export default class PresentationWindow {
940
981
  }
941
982
 
942
983
  case 'analytics-event': {
943
- if (this.analyticsEventCallback && isValidAnalyticsEvent(payload)) {
944
- this.analyticsEventCallback(payload);
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
+ );
945
992
  }
946
993
  break;
947
994
  }
@@ -996,6 +1043,53 @@ export default class PresentationWindow {
996
1043
  break;
997
1044
  }
998
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
+
999
1093
  default: {
1000
1094
  this.logWarning(`Received unknown embedded message type: ${type}`);
1001
1095
  }
package/src/testUtils.ts CHANGED
@@ -38,6 +38,7 @@ export function createPresentationWindow(
38
38
  muteWindow() {},
39
39
  unMuteWindow() {},
40
40
  },
41
+ getTerminal: overrides.getTerminal,
41
42
  });
42
43
  }
43
44
 
package/src/types.ts CHANGED
@@ -86,7 +86,6 @@ export interface CreateWindowOptions {
86
86
  windowId: string;
87
87
  parentWindowId?: string;
88
88
  bounds?: WindowBounds;
89
- logger: Logger;
90
89
  backgroundColor?: string;
91
90
 
92
91
  // Called when attaching a message listener to the inner window.
@@ -174,3 +173,27 @@ export interface AnalyticsEvent {
174
173
  source: string;
175
174
  productCategory?: string | null;
176
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
+ }
@@ -10,4 +10,4 @@ mkdir build
10
10
  yarn build
11
11
 
12
12
  # Publish
13
- yarn publish --access public
13
+ yarn publish --non-interactive --access public
@@ -1,3 +0,0 @@
1
- import { WindowBounds } from './types';
2
- export declare const isFullscreen: (bounds: WindowBounds) => boolean;
3
- export declare const areBoundsEqual: (bounds1: WindowBounds, bounds2: WindowBounds) => boolean;
@@ -1,16 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.areBoundsEqual = exports.isFullscreen = void 0;
4
- var electron_1 = require("electron");
5
- var isFullscreen = function (bounds) {
6
- var screenBounds = electron_1.screen.getPrimaryDisplay().bounds;
7
- return (bounds.width === screenBounds.width && bounds.height === screenBounds.height);
8
- };
9
- exports.isFullscreen = isFullscreen;
10
- var areBoundsEqual = function (bounds1, bounds2) {
11
- return (bounds1.x === bounds2.x &&
12
- bounds1.y === bounds2.y &&
13
- bounds1.width === bounds2.width &&
14
- bounds1.height === bounds2.height);
15
- };
16
- exports.areBoundsEqual = areBoundsEqual;
package/src/boundUtils.ts DELETED
@@ -1,21 +0,0 @@
1
- import { screen } from 'electron';
2
- import { WindowBounds } from './types';
3
-
4
- export const isFullscreen = (bounds: WindowBounds): boolean => {
5
- const screenBounds = screen.getPrimaryDisplay().bounds;
6
- return (
7
- bounds.width === screenBounds.width && bounds.height === screenBounds.height
8
- );
9
- };
10
-
11
- export const areBoundsEqual = (
12
- bounds1: WindowBounds,
13
- bounds2: WindowBounds
14
- ): boolean => {
15
- return (
16
- bounds1.x === bounds2.x &&
17
- bounds1.y === bounds2.y &&
18
- bounds1.width === bounds2.width &&
19
- bounds1.height === bounds2.height
20
- );
21
- };