Merge branch 'renovate/all-minor-patch' into 'main'
[ProtonMail-WebClient.git] / packages / chargebee / src / message-bus.ts
blobab28ef592dbbeffb521e7714a430aaad33caae46
1 import noop from '@proton/utils/noop';
3 import type {
4     BinData,
5     CardFormRenderMode,
6     CbIframeConfig,
7     CbIframeResponseStatus,
8     ChargebeeSavedCardAuthorizationSuccess,
9     ChargebeeSubmitDirectDebitEventPayload,
10     ChargebeeSubmitEventPayload,
11     ChargebeeSubmitEventResponse,
12     ChargebeeVerifySavedCardEventPayload,
13     FormValidationErrors,
14     GetHeightResponsePayload,
15     MessageBusResponse,
16     PaypalAuthorizedPayload,
17     PaypalCancelledMessage,
18     PaypalClickedMessage,
19     PaypalFailedMessage,
20     SavedCardVerificationFailureMessage,
21     SavedCardVerificationSuccessMessage,
22     ThreeDsChallengeMessage,
23     ThreeDsChallengePayload,
24     ThreeDsFailedMessage,
25     ThreeDsRequiredForSavedCardMessage,
26     UnhandledErrorMessage,
27     UpdateFieldsPayload,
28 } from '../lib';
29 import {
30     paypalAuthorizedMessageType,
31     paypalCancelledMessageType,
32     paypalClickedMessageType,
33     paypalFailedMessageType,
34     threeDsChallengeMessageType,
35     unhandledError,
36 } from '../lib';
37 import { addCheckpoint, chargebeeWrapperVersion, getCheckpoints } from './checkpoints';
39 function isChargebeeEvent(event: any): boolean {
40     return !!event?.cbEvent;
43 type SendResponseToParent<T> = (response: MessageBusResponse<T>) => void;
45 // SetConfigurationEvent
47 export type SetConfigurationEvent = {
48     type: 'set-configuration';
49     correlationId: string;
50 } & CbIframeConfig;
52 function isSetConfigurationEvent(event: any): event is SetConfigurationEvent {
53     return event?.type === 'set-configuration';
56 // OnSubmitHandler
58 export type ChargebeeSubmitEvent = {
59     type: 'chargebee-submit';
60     correlationId: string;
61 } & ChargebeeSubmitEventPayload;
63 export function isChargebeeSubmitEvent(event: any): event is ChargebeeSubmitEvent {
64     return event?.type === 'chargebee-submit';
67 export type OnSubmitHandler = (
68     event: ChargebeeSubmitEvent,
69     sendResponseToParent: SendResponseToParent<ChargebeeSubmitEventResponse>
70 ) => void;
72 export type SetPaypalPaymentIntentEvent = {
73     type: 'set-paypal-payment-intent';
74     correlationId: string;
75     paypalButtonHeight?: number;
76 } & ChargebeeSubmitEventPayload;
78 export function isSetPaypalPaymentIntentEvent(event: any): event is SetPaypalPaymentIntentEvent {
79     return event?.type === 'set-paypal-payment-intent';
82 // GetHeightEvent
84 export type GetHeightEvent = {
85     type: 'get-height';
86     correlationId: string;
89 export function isGetHeightEvent(event: any): event is GetHeightEvent {
90     return event?.type === 'get-height';
93 export type OnSetPaypalPaymentIntentHandler = (
94     event: SetPaypalPaymentIntentEvent,
95     sendResponseToParent: SendResponseToParent<void>
96 ) => void;
98 // GetBinEvent
100 export type GetBinEvent = {
101     type: 'get-bin';
102     correlationId: string;
105 export function isGetBinEvent(event: any): event is GetBinEvent {
106     return event?.type === 'get-bin';
109 export type OnGetBinHandler = (event: GetBinEvent, sendResponseToParent: SendResponseToParent<BinData | null>) => void;
111 // ValidateFormEvent
113 export type ValidateFormEvent = {
114     type: 'validate-form';
115     correlationId: string;
118 export function isValidateFormEvent(event: any): event is ValidateFormEvent {
119     return event?.type === 'validate-form';
122 export type OnValidateFormHandler = (
123     event: ValidateFormEvent,
124     sendResponseToParent: SendResponseToParent<FormValidationErrors>
125 ) => void;
127 // VerifySavedCardEvent
129 export const verifySavedCardMessageType = 'chargebee-verify-saved-card';
131 export type VerifySavedCardEvent = {
132     type: typeof verifySavedCardMessageType;
133     correlationId: string;
134 } & ChargebeeVerifySavedCardEventPayload;
136 export function isVerifySavedCardEvent(event: any): event is VerifySavedCardEvent {
137     return event?.type === verifySavedCardMessageType;
140 export type OnVerifySavedCardHandler = (
141     event: VerifySavedCardEvent,
142     sendResponseToParent: SendResponseToParent<ChargebeeSubmitEventResponse>
143 ) => void;
145 // ChangeRenderModeEvent
147 export const changeRenderModeMessageType = 'change-render-mode';
149 export type ChangeRenderModeEvent = {
150     type: typeof changeRenderModeMessageType;
151     correlationId: string;
152     renderMode: CardFormRenderMode;
155 export function isChangeRenderModeEvent(event: any): event is ChangeRenderModeEvent {
156     return event?.type === changeRenderModeMessageType;
159 export type OnChangeRenderModeHandler = (
160     event: ChangeRenderModeEvent,
161     sendResponseToParent: SendResponseToParent<{}>
162 ) => void;
164 // UpdateFieldsEvent
166 export const updateFieldsMessageType = 'update-fields';
168 export type UpdateFieldsEvent = {
169     type: typeof updateFieldsMessageType;
170     correlationId: string;
171 } & UpdateFieldsPayload;
173 export function isUpdateFieldsEvent(event: any): event is UpdateFieldsEvent {
174     return event?.type === updateFieldsMessageType;
177 export type OnUpdateFieldsHandler = (event: UpdateFieldsEvent, sendResponseToParent: SendResponseToParent<{}>) => void;
179 // onDirectDebitSubmit handler
180 export const directDebitSubmitMessageType = 'direct-debit-submit';
182 export type DirectDebitSubmitEvent = {
183     type: typeof directDebitSubmitMessageType;
184     correlationId: string;
185 } & ChargebeeSubmitDirectDebitEventPayload;
187 export function isDirectDebitSubmitEvent(event: any): event is DirectDebitSubmitEvent {
188     return event?.type === directDebitSubmitMessageType;
191 export type OnDirectDebitSubmitHandler = (
192     event: DirectDebitSubmitEvent,
193     sendResponseToParent: SendResponseToParent<{}>
194 ) => void;
196 export interface ParentMessagesProps {
197     onSetConfiguration?: (event: SetConfigurationEvent, sendResponseToParent: SendResponseToParent<{}>) => void;
198     onSubmit?: OnSubmitHandler;
199     onSetPaypalPaymentIntent?: OnSetPaypalPaymentIntentHandler;
200     onGetHeight?: (event: GetHeightEvent, sendResponseToParent: SendResponseToParent<GetHeightResponsePayload>) => void;
201     onGetBin?: OnGetBinHandler;
202     onValidateForm?: OnValidateFormHandler;
203     onVerifySavedCard?: OnVerifySavedCardHandler;
204     onChangeRenderMode?: OnChangeRenderModeHandler;
205     onUpdateFields?: OnUpdateFieldsHandler;
206     onDirectDebitSubmit?: OnDirectDebitSubmitHandler;
209 // the event handler function must be async to make sure that we catch all errors, sync and async
210 const getEventListener = (messageBus: MessageBus) => async (e: MessageEvent) => {
211     const parseEvent = (data: any) => {
212         if (typeof data !== 'string') {
213             return data;
214         }
216         let props;
217         try {
218             props = JSON.parse(data);
219         } catch (error) {
220             props = {};
221         }
222         return props;
223     };
225     const event = parseEvent(e.data);
227     try {
228         if (isSetConfigurationEvent(event)) {
229             // Do not remove await here or anywhere else in the function. It ensures that all errors are caught
230             await messageBus.onSetConfiguration(event, (result) => {
231                 messageBus.sendMessage({
232                     type: 'set-configuration-response',
233                     correlationId: event.correlationId,
234                     ...result,
235                 });
236             });
237         } else if (isChargebeeSubmitEvent(event)) {
238             await messageBus.onSubmit(event, (result) => {
239                 messageBus.sendMessage({
240                     type: 'chargebee-submit-response',
241                     correlationId: event.correlationId,
242                     ...result,
243                 });
244             });
245         } else if (isSetPaypalPaymentIntentEvent(event)) {
246             await messageBus.onSetPaypalPaymentIntent(event, (result) => {
247                 messageBus.sendMessage({
248                     type: 'set-paypal-payment-intent-response',
249                     correlationId: event.correlationId,
250                     ...result,
251                 });
252             });
253         } else if (isGetHeightEvent(event)) {
254             await messageBus.onGetHeight(event, (result) => {
255                 messageBus.sendMessage({
256                     type: 'get-height-response',
257                     correlationId: event.correlationId,
258                     ...result,
259                 });
260             });
261         } else if (isGetBinEvent(event)) {
262             await messageBus.onGetBin(event, (result) => {
263                 messageBus.sendMessage({
264                     type: 'get-bin-response',
265                     correlationId: event.correlationId,
266                     ...result,
267                 });
268             });
269         } else if (isValidateFormEvent(event)) {
270             await messageBus.onValidateForm(event, (result) => {
271                 messageBus.sendMessage({
272                     type: 'validate-form-response',
273                     correlationId: event.correlationId,
274                     ...result,
275                 });
276             });
277         } else if (isVerifySavedCardEvent(event)) {
278             await messageBus.onVerifySavedCard(event, (result) => {
279                 messageBus.sendMessage({
280                     type: 'chargebee-verify-saved-card-response',
281                     correlationId: event.correlationId,
282                     ...result,
283                 });
284             });
285         } else if (isChangeRenderModeEvent(event)) {
286             await messageBus.onChangeRenderMode(event, (result) => {
287                 messageBus.sendMessage({
288                     type: 'change-render-mode-response',
289                     correlationId: event.correlationId,
290                     ...result,
291                 });
292             });
293         } else if (isUpdateFieldsEvent(event)) {
294             await messageBus.onUpdateFields(event, (result) => {
295                 messageBus.sendMessage({
296                     type: 'update-fields-response',
297                     correlationId: event.correlationId,
298                     ...result,
299                 });
300             });
301         } else if (isDirectDebitSubmitEvent(event)) {
302             await messageBus.onDirectDebitSubmit(event, (result) => {
303                 messageBus.sendMessage({
304                     type: 'direct-debit-submit-response',
305                     correlationId: event.correlationId,
306                     ...result,
307                 });
308             });
309         } else if (isChargebeeEvent(event)) {
310             // ignore chargebee event
311         } else {
312             // ignore unknown event
313         }
314     } catch (error) {
315         addCheckpoint('failed_to_handle_parent_message', { error, event, eventRawData: e.data });
316         messageBus.sendUnhandledErrorMessage(error);
317     }
320 export class MessageBus {
321     public onSetConfiguration;
323     public onSubmit;
325     public onSetPaypalPaymentIntent;
327     public onGetHeight;
329     public onGetBin;
331     public onValidateForm;
333     public onVerifySavedCard;
335     public onChangeRenderMode;
337     public onUpdateFields;
339     public onDirectDebitSubmit;
341     private eventListener: ((e: MessageEvent) => void) | null = null;
343     constructor({
344         onSetConfiguration,
345         onSubmit,
346         onSetPaypalPaymentIntent,
347         onGetHeight,
348         onGetBin,
349         onValidateForm,
350         onVerifySavedCard,
351         onChangeRenderMode,
352         onUpdateFields,
353         onDirectDebitSubmit,
354     }: ParentMessagesProps) {
355         this.onSetConfiguration = onSetConfiguration ?? noop;
356         this.onSubmit = onSubmit ?? noop;
357         this.onSetPaypalPaymentIntent = onSetPaypalPaymentIntent ?? noop;
358         this.onGetHeight = onGetHeight ?? noop;
359         this.onGetBin = onGetBin ?? noop;
360         this.onValidateForm = onValidateForm ?? noop;
361         this.onVerifySavedCard = onVerifySavedCard ?? noop;
362         this.onChangeRenderMode = onChangeRenderMode ?? noop;
363         this.onUpdateFields = onUpdateFields ?? noop;
364         this.onDirectDebitSubmit = onDirectDebitSubmit ?? noop;
365     }
367     initialize() {
368         this.eventListener = getEventListener(this);
369         window.addEventListener('message', this.eventListener);
370     }
372     destroy() {
373         if (this.eventListener) {
374             window.removeEventListener('message', this.eventListener);
375             this.eventListener = null;
376         }
377     }
379     sendPaypalAuthorizedMessage(data: PaypalAuthorizedPayload) {
380         const message: MessageBusResponse<PaypalAuthorizedPayload> = {
381             status: 'success',
382             data,
383         };
385         this.sendMessage({
386             type: paypalAuthorizedMessageType,
387             ...message,
388         });
389     }
391     sendPaypalFailedMessage(error: any) {
392         const message: PaypalFailedMessage = {
393             type: paypalFailedMessageType,
394             status: 'failure',
395             error,
396         };
398         this.sendMessage(message);
399     }
401     sendPaypalClickedMessage() {
402         const message: PaypalClickedMessage = {
403             type: paypalClickedMessageType,
404             status: 'success',
405             data: {},
406         };
408         this.sendMessage(message);
409     }
411     sendPaypalCancelledMessage() {
412         const message: PaypalCancelledMessage = {
413             type: paypalCancelledMessageType,
414             status: 'success',
415             data: {},
416         };
418         this.sendMessage(message);
419     }
421     send3dsChallengeMessage(data: ThreeDsChallengePayload, correlationId?: string) {
422         const message: ThreeDsChallengeMessage = {
423             type: threeDsChallengeMessageType,
424             status: 'success',
425             data,
426         };
428         this.sendMessage({ ...message, correlationId });
429     }
431     send3dsFailedMessage(error: any, correlationId?: string) {
432         const message: ThreeDsFailedMessage = {
433             type: `chargebee-submit-response`,
434             status: 'failure',
435             error,
436         };
438         this.sendMessage({ ...message, correlationId });
439     }
441     sendFormValidationErrorMessage(errors: FormValidationErrors, correlationId?: string) {
442         const message: MessageBusResponse<FormValidationErrors> = {
443             status: 'failure',
444             error: errors,
445         };
447         this.sendMessage({
448             type: 'chargebee-submit-response',
449             ...message,
450             correlationId,
451         });
452     }
454     send3dsSuccessMessage(paymentIntent: ChargebeeSubmitEventResponse, correlationId?: string) {
455         this.sendMessage({
456             type: 'chargebee-submit-response',
457             status: 'success',
458             data: paymentIntent,
459             correlationId,
460         });
461     }
463     send3dsRequiredForSavedCardMessage(data: ThreeDsChallengePayload, correlationId?: string) {
464         const message: ThreeDsRequiredForSavedCardMessage = {
465             type: threeDsChallengeMessageType,
466             status: 'success',
467             data,
468         };
470         this.sendMessage({ ...message, correlationId });
471     }
473     sendSavedCardVerificationSuccessMessage(payload: ChargebeeSavedCardAuthorizationSuccess, correlationId?: string) {
474         const message: SavedCardVerificationSuccessMessage = {
475             type: 'chargebee-verify-saved-card-response',
476             status: 'success',
477             data: payload,
478         };
480         this.sendMessage({ ...message, correlationId });
481     }
483     sendSavedCardVerificationFailureMessage(error: any, correlationId?: string) {
484         const message: SavedCardVerificationFailureMessage = {
485             type: 'chargebee-verify-saved-card-response',
486             status: 'failure',
487             error,
488         };
490         this.sendMessage({ ...message, correlationId });
491     }
493     sendDirectDebitSuccessMessage(data: any, correlationId?: string) {
494         this.sendMessage({
495             type: 'direct-debit-submit-response',
496             status: 'success',
497             data,
498             correlationId,
499         });
500     }
502     sendDirectDebitFailureMessage(error: any, correlationId?: string) {
503         this.sendMessage({
504             type: 'direct-debit-submit-response',
505             status: 'failure',
506             error,
507             correlationId,
508         });
509     }
511     sendUnhandledErrorMessage(errorObj: any) {
512         try {
513             const error = {
514                 ...this.formatError(errorObj),
515                 checkpoints: getCheckpoints(),
516                 chargebeeWrapperVersion,
517                 origin: window?.location?.origin,
518             };
520             const message: UnhandledErrorMessage = {
521                 type: unhandledError,
522                 status: 'failure',
523                 error,
524             };
526             this.sendMessage(message);
527         } catch (error) {
528             console.error('Failed to send error message to parent');
529             throw error;
530         }
531     }
533     sendMessage(message: {
534         type: string;
535         correlationId?: string;
536         status?: CbIframeResponseStatus;
537         data?: any;
538         error?: any;
539     }) {
540         const messageToSend = {
541             ...message,
542             error: this.formatError(message.error),
543         };
545         window.parent.postMessage(JSON.stringify(messageToSend), '*');
546     }
548     private isPlainError(error: any): error is Error {
549         return (
550             !!error && !!error.message && !error.checkpoints && !error.chargebeeWrapperVersion && !Array.isArray(error)
551         );
552     }
554     private formatError(error: any) {
555         if (this.isPlainError(error)) {
556             return {
557                 message: error.message,
558                 stack: error.stack,
559                 name: error.name,
560             };
561         }
563         return error;
564     }
567 let messageBus: MessageBus | null = null;
569 export function createMessageBus(props: ParentMessagesProps) {
570     let parentMessages = new MessageBus(props);
571     parentMessages.initialize();
573     messageBus = parentMessages;
575     return parentMessages;
578 export function getMessageBus(): MessageBus {
579     if (!messageBus) {
580         throw new Error('MessageBus is not initialized');
581     }
583     return messageBus;