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 = {
20 interface SilenceConfig {
21 silence?: boolean | number[];
24 const getSilenced = ({ silence }: SilenceConfig = {}, code: number) => {
25 if (Array.isArray(silence)) {
26 return silence.includes(code);
31 type ServerTimeEvent = {
35 type ApiStatusEvent = {
37 payload: Partial<typeof defaultApiStatus>;
39 type ApiNotificationEvent = {
48 type ApiLogoutEvent = {
54 type ApiMissingScopeEvent = {
55 type: 'missing-scopes';
60 resolve: (value: any) => void;
61 reject: (value: any) => void;
64 type ApiVerificationEvent = {
65 type: 'handle-verification';
69 onVerify: (token: string, tokenType: string) => Promise<any>;
72 resolve: (value: any) => void;
73 reject: (value: any) => void;
79 | ApiNotificationEvent
81 | ApiMissingScopeEvent
82 | ApiVerificationEvent;
83 export type ApiListenerCallback = (event: ApiEvent) => void;
85 export type ApiWithListener = Api & {
86 UID: string | undefined;
87 addEventListener: (cb: ApiListenerCallback) => void;
88 removeEventListener: (cb: ApiListenerCallback) => void;
97 sendLocaleHeaders?: boolean;
100 noErrorState?: boolean;
101 }): ApiWithListener => {
102 const call = configureApi({
105 clientID: getClientID(config.APP_NAME),
109 const listeners: ApiListenerCallback[] = [];
110 const notify = (event: ApiEvent) => {
111 listeners.forEach((listener) => listener(event));
114 const handleMissingScopes = (data: any) => {
115 if (!listeners.length) {
116 return Promise.reject(data.error);
118 return new Promise((resolve, reject) => {
120 type: 'missing-scopes',
130 const handleVerification = (data: any) => {
131 if (!listeners.length) {
132 return Promise.reject(data.error);
134 return new Promise((resolve, reject) => {
136 type: 'handle-verification',
146 const callWithApiHandlers = withApiHandlers({
148 onMissingScopes: handleMissingScopes,
149 onVerification: handleVerification,
152 const offlineSet = new Set<string>();
154 const callback: Api = ({ output = 'json', ...rest }: any) => {
155 // Only need to send locale headers in public app
156 const config = sendLocaleHeaders ? withLocaleHeaders(localeCode, rest) : rest;
157 return callWithApiHandlers(config)
158 .then((response: any) => {
159 const serverTime = getDateHeader(response.headers);
161 // The HTTP Date header is mandatory, so this should never occur.
162 // We need the server time for proper time sync:
163 // falling back to the local time can result in e.g. unverifiable signatures
164 throw new Error('Could not fetch server time');
169 payload: updateServerTime(serverTime),
173 payload: defaultApiStatus,
177 if (output === 'stream') {
178 return response.body;
180 if (output === 'raw') {
183 return response[output]();
186 const serverTime = e.response?.headers ? getDateHeader(e.response.headers) : undefined;
190 payload: updateServerTime(serverTime),
194 const { code } = getApiError(e);
195 const errorMessage = getApiErrorMessage(e);
197 const isSilenced = getSilenced(e.config, code);
199 const handleErrorNotification = () => {
200 if (!errorMessage || isSilenced) {
203 const codeExpirations = {
204 [API_CUSTOM_ERROR_CODES.USER_RESTRICTED_STATE]: 10_000,
207 type: 'notification',
211 ...(code in codeExpirations
214 expiration: codeExpirations[code],
217 expiration: config?.notificationExpiration,
223 // Intended for the verify app where we always want to pass an error notification
225 handleErrorNotification();
229 const isOffline = getIsOfflineError(e);
230 const isUnreachable = getIsUnreachableError(e);
233 offlineSet.add(e?.config?.url || '');
238 if (isOffline || isUnreachable) {
242 apiUnreachable: isUnreachable ? errorMessage || '' : '',
243 // We wait to notify offline until at least 2 unique urls have been seen as offline
244 offline: isOffline && offlineSet.size > 1,
252 apiUnreachable: defaultApiStatus.apiUnreachable,
253 offline: defaultApiStatus.offline,
257 if (e.name === 'AbortError' || e.cancel) {
261 if (e.name === 'AppVersionBadError') {
262 notify({ type: 'status', payload: { appVersionBad: true } });
266 if (e.name === 'InactiveSession') {
267 notify({ type: 'logout', payload: { error: e } });
271 handleErrorNotification();
276 const getCallbackWithListeners = (callback: Api) => {
277 Object.defineProperties(callback, {
279 set(value: string | undefined) {
280 callWithApiHandlers.UID = value;
286 return Object.assign(callback as ApiWithListener, {
287 addEventListener: (cb: ApiListenerCallback) => {
290 removeEventListener: (cb: ApiListenerCallback) => {
291 listeners.splice(listeners.indexOf(cb), 1);
296 return getCallbackWithListeners(callback);
299 export default createApi;