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 export type ServerTimeEvent = {
35 export type ApiStatusEvent = {
37 payload: Partial<typeof defaultApiStatus>;
39 export type ApiNotificationEvent = {
48 export type ApiLogoutEvent = {
54 export type ApiMissingScopeEvent = {
55 type: 'missing-scopes';
60 resolve: (value: any) => void;
61 reject: (value: any) => void;
64 export type ApiVerificationEvent = {
65 type: 'handle-verification';
69 onVerify: (token: string, tokenType: string) => Promise<any>;
72 resolve: (value: any) => void;
73 reject: (value: any) => void;
76 export type ApiEvent =
79 | ApiNotificationEvent
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;
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 const result = listeners.map((listener) => listener(event));
112 return result.some((value) => value === true);
115 const handleMissingScopes = (data: any) => {
116 if (!listeners.length) {
117 return Promise.reject(data.error);
119 return new Promise((resolve, reject) => {
120 const handled = notify({
121 type: 'missing-scopes',
131 return reject(data.error);
135 const handleVerification = (data: any) => {
136 if (!listeners.length) {
137 return Promise.reject(data.error);
139 return new Promise((resolve, reject) => {
140 const handled = notify({
141 type: 'handle-verification',
151 return reject(data.error);
155 const callWithApiHandlers = withApiHandlers({
157 onMissingScopes: handleMissingScopes,
158 onVerification: handleVerification,
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);
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');
178 payload: updateServerTime(serverTime),
182 payload: defaultApiStatus,
186 if (output === 'stream') {
187 return response.body;
189 if (output === 'raw') {
192 return response[output]();
195 const serverTime = e.response?.headers ? getDateHeader(e.response.headers) : undefined;
199 payload: updateServerTime(serverTime),
203 const { code } = getApiError(e);
204 const errorMessage = getApiErrorMessage(e);
206 const isSilenced = getSilenced(e.config, code);
208 const handleErrorNotification = () => {
209 if (!errorMessage || isSilenced) {
212 const codeExpirations = {
213 [API_CUSTOM_ERROR_CODES.USER_RESTRICTED_STATE]: 10_000,
216 type: 'notification',
220 ...(code in codeExpirations
223 expiration: codeExpirations[code],
226 expiration: config?.notificationExpiration,
232 // Intended for the verify app where we always want to pass an error notification
234 handleErrorNotification();
238 const isOffline = getIsOfflineError(e);
239 const isUnreachable = getIsUnreachableError(e);
242 offlineSet.add(e?.config?.url || '');
247 if (isOffline || isUnreachable) {
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,
261 apiUnreachable: defaultApiStatus.apiUnreachable,
262 offline: defaultApiStatus.offline,
266 if (e.name === 'AbortError' || e.cancel) {
270 if (e.name === 'AppVersionBadError') {
271 notify({ type: 'status', payload: { appVersionBad: true } });
275 if (e.name === 'InactiveSession') {
276 notify({ type: 'logout', payload: { error: e } });
280 handleErrorNotification();
285 const getCallbackWithListeners = (callback: Api) => {
286 Object.defineProperties(callback, {
288 set(value: string | undefined) {
289 callWithApiHandlers.UID = value;
295 return Object.assign(callback as ApiWithListener, {
296 addEventListener: (cb: ApiListenerCallback) => {
299 removeEventListener: (cb: ApiListenerCallback) => {
300 listeners.splice(listeners.indexOf(cb), 1);
305 return getCallbackWithListeners(callback);
308 export default createApi;