1 import { c } from 'ttag';
3 import getRandomString from '@proton/utils/getRandomString';
4 import lastItem from '@proton/utils/lastItem';
6 import type { ExtensionForkPayload } from '../authentication/fork/extension';
7 import { APPS, EXTENSIONS } from '../constants';
8 import { browserAPI, isChromiumBased, isSafari } from '../helpers/browser';
10 export type ExtensionForkMessage = { type: 'fork'; payload: ExtensionForkPayload };
11 export type ExtensionAuthenticatedMessage = { type: 'auth-ext' };
12 export type PassInstalledMessage = { type: 'pass-installed' };
13 export type PassOnboardingMessage = { type: 'pass-onboarding' };
15 export type ExtensionMessage =
16 | ExtensionForkMessage
17 | ExtensionAuthenticatedMessage
18 | PassInstalledMessage
19 | PassOnboardingMessage;
21 export type ExtensionApp = keyof typeof EXTENSIONS;
23 /* extension communicating with account should
24 * conform to this message response type */
25 export type ExtensionMessageResponse<P = any> =
26 | { type: 'success'; payload: P }
27 | { type: 'error'; payload?: P; error?: string };
29 export type ExtensionMessageFallbackResponse<P = any> = ExtensionMessageResponse<P> & {
33 export const sendMessageSupported = () =>
34 (isChromiumBased() || isSafari()) && browserAPI?.runtime?.sendMessage !== undefined;
36 const isValidExtensionResponse = <R = any>(response: any): response is ExtensionMessageResponse<R> =>
37 response?.type === 'success' || response?.type === 'error';
39 type SendMessageResult = { ok: boolean; response: ExtensionMessageResponse };
41 const sendMessage = async (extensionId: string, message: any): Promise<SendMessageResult> => {
42 const onError = (error?: string): SendMessageResult => ({
46 error: error ?? c('Warning').t`Please check that the extension is properly installed and enabled`,
50 const onResponse = (response: unknown): SendMessageResult => {
51 if (browserAPI.runtime.lastError || response === undefined) {
55 if (!isValidExtensionResponse(response)) {
60 error: c('Warning').t`Please update the extension to its latest version`,
65 return { ok: true, response };
68 return new Promise<unknown>((resolve) => browserAPI.runtime.sendMessage(extensionId, message, resolve))
70 .catch(() => onError());
73 /** When dealing with extensions residing in multiple stores, it's essential to handle
74 * all possible extensionIds when dispatching a message. In this scenario, the first
75 * extension to respond takes precedence. The messages are dispatched sequentially
76 * to ensure accurate `lastError` checking. If no extensions respond, last runtime
77 * error will be returned. This broadcasting mechanism only works for extensions and
78 * browsers supporting the `externally_connectable` API. */
79 const broadcastMessage = async (
80 extensionIds: readonly string[],
81 message: ExtensionMessage
82 ): Promise<ExtensionMessageResponse> => {
83 /* Iterate until we have a successful link with one of the extensionIds. If establishing
84 * connection with all of the extensionIds failed, returns the last failure response. */
85 const results = await Promise.all(extensionIds.map((extensionId) => sendMessage(extensionId, message)));
87 return results.find(({ ok }) => ok)?.response ?? lastItem(results)?.response ?? { type: 'error' };
90 /** Extension messaging must account for Chrome & Firefox specifics :
91 * Chrome : we can leverage the `externally_connectable` permissions
92 * Firefox : we have to result to fallback postMessaging via content
93 * script injections */
94 export const sendExtensionMessage = async <R = any>(
95 message: ExtensionMessage,
96 options: { app: ExtensionApp; maxTimeout?: number }
97 ): Promise<ExtensionMessageResponse<R>> => {
98 let timeout: ReturnType<typeof setTimeout> | undefined;
99 const extension = EXTENSIONS[options.app];
100 const token = getRandomString(16);
102 return new Promise<ExtensionMessageResponse<R>>((resolve) => {
103 const onFallbackMessage = ({ data, source }: MessageEvent) => {
104 if (source === window && data?.from !== APPS.PROTONACCOUNT && data?.token === token) {
105 clearTimeout(timeout);
106 window.removeEventListener('message', onFallbackMessage);
107 resolve(isValidExtensionResponse(data) ? data : { type: 'error', error: 'Bad format' });
111 timeout = setTimeout(() => {
112 window.removeEventListener('message', onFallbackMessage);
113 resolve({ type: 'error', error: 'Extension timed out' });
114 }, options.maxTimeout ?? 15_000);
116 if (sendMessageSupported()) {
117 return broadcastMessage(extension.IDs, message).then((result) => {
118 clearTimeout(timeout);
123 /* Firefox extensions should prefer checking the `message.data.app` property.
124 * `message.data.extension` is kept for backward compatibility but will
125 * be deprecated in the future */
129 extension: extension.IDs[0],
130 from: APPS.PROTONACCOUNT,
137 window.addEventListener('message', onFallbackMessage);