Cleanup - unused files / unused exports / duplicate exports
[ProtonMail-WebClient.git] / packages / shared / lib / api / createApi.ts
blob89e8561e1bd6611c54f848b3a646c9de33336351
1 import { updateServerTime } from '@proton/crypto';
3 import configureApi from '../api';
4 import { getClientID } from '../apps/helper';
5 import { API_CUSTOM_ERROR_CODES } from '../errors';
6 import xhr from '../fetch/fetch';
7 import { withLocaleHeaders } from '../fetch/headers';
8 import { getDateHeader } from '../fetch/helpers';
9 import { localeCode } from '../i18n';
10 import type { Api } from '../interfaces';
11 import { getApiError, getApiErrorMessage, getIsOfflineError, getIsUnreachableError } from './helpers/apiErrorHelper';
12 import withApiHandlers from './helpers/withApiHandlers';
14 export const defaultApiStatus = {
15     offline: false,
16     apiUnreachable: '',
17     appVersionBad: false,
20 interface SilenceConfig {
21     silence?: boolean | number[];
24 const getSilenced = ({ silence }: SilenceConfig = {}, code: number) => {
25     if (Array.isArray(silence)) {
26         return silence.includes(code);
27     }
28     return !!silence;
31 export type ServerTimeEvent = {
32     type: 'server-time';
33     payload: Date;
35 export type ApiStatusEvent = {
36     type: 'status';
37     payload: Partial<typeof defaultApiStatus>;
39 export type ApiNotificationEvent = {
40     type: 'notification';
41     payload: {
42         type: 'error';
43         text: string;
44         id?: number;
45         expiration?: number;
46     };
48 export type ApiLogoutEvent = {
49     type: 'logout';
50     payload: {
51         error: any;
52     };
54 export type ApiMissingScopeEvent = {
55     type: 'missing-scopes';
56     payload: {
57         scopes: string[];
58         error: any;
59         options: any;
60         resolve: (value: any) => void;
61         reject: (value: any) => void;
62     };
64 export type ApiVerificationEvent = {
65     type: 'handle-verification';
66     payload: {
67         token: string;
68         methods: any[];
69         onVerify: (token: string, tokenType: string) => Promise<any>;
70         title: any;
71         error: any;
72         resolve: (value: any) => void;
73         reject: (value: any) => void;
74     };
76 export type ApiEvent =
77     | ServerTimeEvent
78     | ApiStatusEvent
79     | ApiNotificationEvent
80     | ApiLogoutEvent
81     | ApiMissingScopeEvent
82     | ApiVerificationEvent;
83 export type ApiListenerCallback = (event: ApiEvent) => boolean;
85 export type ApiWithListener = Api & {
86     UID: string | undefined;
87     addEventListener: (cb: ApiListenerCallback) => void;
88     removeEventListener: (cb: ApiListenerCallback) => void;
91 const createApi = ({
92     config,
93     defaultHeaders,
94     noErrorState,
95     sendLocaleHeaders,
96 }: {
97     sendLocaleHeaders?: boolean;
98     defaultHeaders?: any;
99     config: any;
100     noErrorState?: boolean;
101 }): ApiWithListener => {
102     const call = configureApi({
103         ...config,
104         defaultHeaders,
105         clientID: getClientID(config.APP_NAME),
106         xhr,
107     }) as any;
109     const listeners: ApiListenerCallback[] = [];
110     const notify = (event: ApiEvent) => {
111         const result = listeners.map((listener) => listener(event));
112         return result.some((value) => value === true);
113     };
115     const handleMissingScopes = (data: any) => {
116         if (!listeners.length) {
117             return Promise.reject(data.error);
118         }
119         return new Promise((resolve, reject) => {
120             const handled = notify({
121                 type: 'missing-scopes',
122                 payload: {
123                     ...data,
124                     resolve,
125                     reject,
126                 },
127             });
128             if (handled) {
129                 return;
130             }
131             return reject(data.error);
132         });
133     };
135     const handleVerification = (data: any) => {
136         if (!listeners.length) {
137             return Promise.reject(data.error);
138         }
139         return new Promise((resolve, reject) => {
140             const handled = notify({
141                 type: 'handle-verification',
142                 payload: {
143                     ...data,
144                     resolve,
145                     reject,
146                 },
147             });
148             if (handled) {
149                 return;
150             }
151             return reject(data.error);
152         });
153     };
155     const callWithApiHandlers = withApiHandlers({
156         call,
157         onMissingScopes: handleMissingScopes,
158         onVerification: handleVerification,
159     }) as any;
161     const offlineSet = new Set<string>();
163     const callback: Api = ({ output = 'json', ...rest }: any) => {
164         // Only need to send locale headers in public app
165         const config = sendLocaleHeaders ? withLocaleHeaders(localeCode, rest) : rest;
166         return callWithApiHandlers(config)
167             .then((response: any) => {
168                 const serverTime = getDateHeader(response.headers);
169                 if (!serverTime) {
170                     // The HTTP Date header is mandatory, so this should never occur.
171                     // We need the server time for proper time sync:
172                     // falling back to the local time can result in e.g. unverifiable signatures
173                     throw new Error('Could not fetch server time');
174                 }
176                 notify({
177                     type: 'server-time',
178                     payload: updateServerTime(serverTime),
179                 });
180                 notify({
181                     type: 'status',
182                     payload: defaultApiStatus,
183                 });
184                 offlineSet.clear();
186                 if (output === 'stream') {
187                     return response.body;
188                 }
189                 if (output === 'raw') {
190                     return response;
191                 }
192                 return response[output]();
193             })
194             .catch((e: any) => {
195                 const serverTime = e.response?.headers ? getDateHeader(e.response.headers) : undefined;
196                 if (serverTime) {
197                     notify({
198                         type: 'server-time',
199                         payload: updateServerTime(serverTime),
200                     });
201                 }
203                 const { code } = getApiError(e);
204                 const errorMessage = getApiErrorMessage(e);
206                 const isSilenced = getSilenced(e.config, code);
208                 const handleErrorNotification = () => {
209                     if (!errorMessage || isSilenced) {
210                         return;
211                     }
212                     const codeExpirations = {
213                         [API_CUSTOM_ERROR_CODES.USER_RESTRICTED_STATE]: 10_000,
214                     };
215                     notify({
216                         type: 'notification',
217                         payload: {
218                             type: 'error',
219                             text: errorMessage,
220                             ...(code in codeExpirations
221                                 ? {
222                                       key: code,
223                                       expiration: codeExpirations[code],
224                                   }
225                                 : {
226                                       expiration: config?.notificationExpiration,
227                                   }),
228                         },
229                     });
230                 };
232                 // Intended for the verify app where we always want to pass an error notification
233                 if (noErrorState) {
234                     handleErrorNotification();
235                     throw e;
236                 }
238                 const isOffline = getIsOfflineError(e);
239                 const isUnreachable = getIsUnreachableError(e);
241                 if (isOffline) {
242                     offlineSet.add(e?.config?.url || '');
243                 } else {
244                     offlineSet.clear();
245                 }
247                 if (isOffline || isUnreachable) {
248                     notify({
249                         type: 'status',
250                         payload: {
251                             apiUnreachable: isUnreachable ? errorMessage || '' : '',
252                             // We wait to notify offline until at least 2 unique urls have been seen as offline
253                             offline: isOffline && offlineSet.size > 1,
254                         },
255                     });
256                     throw e;
257                 }
258                 notify({
259                     type: 'status',
260                     payload: {
261                         apiUnreachable: defaultApiStatus.apiUnreachable,
262                         offline: defaultApiStatus.offline,
263                     },
264                 });
266                 if (e.name === 'AbortError' || e.cancel) {
267                     throw e;
268                 }
270                 if (e.name === 'AppVersionBadError') {
271                     notify({ type: 'status', payload: { appVersionBad: true } });
272                     throw e;
273                 }
275                 if (e.name === 'InactiveSession') {
276                     notify({ type: 'logout', payload: { error: e } });
277                     throw e;
278                 }
280                 handleErrorNotification();
281                 throw e;
282             });
283     };
285     const getCallbackWithListeners = (callback: Api) => {
286         Object.defineProperties(callback, {
287             UID: {
288                 set(value: string | undefined) {
289                     callWithApiHandlers.UID = value;
290                     call.UID = value;
291                 },
292             },
293         });
295         return Object.assign(callback as ApiWithListener, {
296             addEventListener: (cb: ApiListenerCallback) => {
297                 listeners.push(cb);
298             },
299             removeEventListener: (cb: ApiListenerCallback) => {
300                 listeners.splice(listeners.indexOf(cb), 1);
301             },
302         });
303     };
305     return getCallbackWithListeners(callback);
308 export default createApi;