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