xref: /expo/home/api/APIV2Client.ts (revision 8a424beb)
1import { Platform } from 'react-native';
2
3import { ApiError } from './ApiError';
4import Config from './Config';
5import { GenericError } from './GenericError';
6import * as Kernel from '../kernel/Kernel';
7import Store from '../redux/Store';
8
9type SendOptions = {
10  method?: string;
11  headers?: Record<string, string>;
12  body?: object;
13  searchParams?: Record<string, string>;
14};
15export class APIV2Client {
16  private async sendApiV2Request<TData>(route: string, options: SendOptions): Promise<TData> {
17    const url = new URL(`${Config.api.origin}/--/api/v2/${route}`);
18    if (options.searchParams) {
19      url.search = new URLSearchParams(options?.searchParams).toString();
20    }
21
22    let response: Response;
23    try {
24      response = await fetch(url.toString(), {
25        method: options.method ?? 'POST',
26        body: options.body ? JSON.stringify(options.body) : null,
27        headers: {
28          ...options.headers,
29          accept: 'application/json',
30          ...(options.body ? { 'content-type': 'application/json' } : null),
31        },
32      });
33    } catch (error) {
34      throw new GenericError(
35        `Something went wrong when connecting to Expo: ${(error as Error).message}.`
36      );
37    }
38
39    let text: string;
40    try {
41      text = await response.text();
42    } catch (error) {
43      throw new GenericError(
44        `Something went wrong when reading the response (HTTP ${response.status}) from Expo: ${
45          (error as Error).message
46        }.`
47      );
48    }
49
50    let body: any;
51    try {
52      body = JSON.parse(text);
53    } catch {
54      throw new GenericError(`The Expo server responded in an unexpected way: ${text}`);
55    }
56
57    if (Array.isArray(body.errors) && body.errors.length > 0) {
58      const responseError = body.errors[0];
59      const errorMessage = responseError.details
60        ? responseError.details.message
61        : responseError.message;
62      const error = new ApiError(errorMessage, responseError.code);
63      error.serverStack = responseError.stack;
64      error.metadata = responseError.metadata;
65      throw error;
66    }
67
68    if (!response.ok) {
69      throw new GenericError(`The Expo server responded with a ${response.status} error.`);
70    }
71
72    return body.data;
73  }
74
75  public async sendAuthenticatedApiV2Request<TData>(
76    route: string,
77    options: SendOptions = {}
78  ): Promise<TData> {
79    const { session } = Store.getState();
80
81    const sessionSecret = session.sessionSecret;
82
83    if (!sessionSecret) {
84      throw new ApiError('Must be logged in to perform request');
85    }
86
87    const newOptions = {
88      ...options,
89      headers: {
90        ...options.headers,
91        ...(sessionSecret
92          ? {
93              'Expo-SDK-Version': Kernel.sdkVersions,
94              'Expo-Platform': Platform.OS,
95              'Expo-Session': sessionSecret,
96            }
97          : {}),
98      },
99    };
100    return await this.sendApiV2Request(route, newOptions);
101  }
102
103  public async sendOptionallyAuthenticatedApiV2Request<TData>(
104    route: string,
105    options: SendOptions = {}
106  ): Promise<TData> {
107    const { session } = Store.getState();
108
109    const sessionSecret = session.sessionSecret;
110    const newOptions = {
111      ...options,
112      headers: {
113        ...options.headers,
114        ...(sessionSecret
115          ? {
116              'Expo-SDK-Version': Kernel.sdkVersions,
117              'Expo-Platform': Platform.OS,
118              'Expo-Session': sessionSecret,
119            }
120          : {}),
121      },
122    };
123    return await this.sendApiV2Request(route, newOptions);
124  }
125
126  public async sendUnauthenticatedApiV2Request<TData>(
127    route: string,
128    options: SendOptions = {}
129  ): Promise<TData> {
130    return await this.sendApiV2Request(route, options);
131  }
132}
133