1import semver from 'semver'; 2 3import { getVersionsAsync } from '../../api/getVersions'; 4import { APISettings } from '../../api/settings'; 5import * as Log from '../../log'; 6import { downloadExpoGoAsync } from '../../utils/downloadExpoGoAsync'; 7import { CommandError } from '../../utils/errors'; 8import { logNewSection } from '../../utils/ora'; 9import { confirmAsync } from '../../utils/prompts'; 10import type { DeviceManager } from './DeviceManager'; 11 12const debug = require('debug')('expo:utils:ExpoGoInstaller') as typeof console.log; 13 14/** Given a platform, appId, and sdkVersion, this module will ensure that Expo Go is up-to-date on the provided device. */ 15export class ExpoGoInstaller<IDevice> { 16 // Keep a list of [platform-deviceId] so we can prevent asking multiple times if a user wants to upgrade. 17 // This can prevent annoying interactions when they don't want to upgrade for whatever reason. 18 static cache: Record<string, boolean> = {}; 19 20 constructor( 21 private platform: 'ios' | 'android', 22 // Ultimately this should be inlined since we know the platform. 23 private appId: string, 24 private sdkVersion?: string 25 ) {} 26 27 /** Returns true if the installed app matching the previously provided `appId` is outdated. */ 28 async isClientOutdatedAsync(device: DeviceManager<IDevice>): Promise<boolean> { 29 const installedVersion = await device.getAppVersionAsync(this.appId); 30 if (!installedVersion) { 31 return true; 32 } 33 const version = await this._getExpectedClientVersionAsync(); 34 debug(`Expected Expo Go version: ${version}, installed version: ${installedVersion}`); 35 return version ? !semver.eq(installedVersion, version) : true; 36 } 37 38 /** Returns the expected version of Expo Go given the project SDK Version. Exposed for testing. */ 39 async _getExpectedClientVersionAsync(): Promise<string | null> { 40 const versions = await getVersionsAsync(); 41 // Like `sdkVersions['44.0.0']['androidClientVersion'] = '1.0.0'` 42 const specificVersion = 43 versions?.sdkVersions?.[this.sdkVersion!]?.[`${this.platform}ClientVersion`]; 44 const latestVersion = versions[`${this.platform}Version`]; 45 return specificVersion ?? latestVersion ?? null; 46 } 47 48 /** Returns a boolean indicating if Expo Go should be installed. Returns `true` if the app was uninstalled. */ 49 async uninstallExpoGoIfOutdatedAsync(deviceManager: DeviceManager<IDevice>): Promise<boolean> { 50 const cacheId = `${this.platform}-${deviceManager.identifier}`; 51 52 if (ExpoGoInstaller.cache[cacheId]) { 53 debug('skipping subsequent upgrade check'); 54 return false; 55 } 56 ExpoGoInstaller.cache[cacheId] = true; 57 if (await this.isClientOutdatedAsync(deviceManager)) { 58 // Only prompt once per device, per run. 59 const confirm = await confirmAsync({ 60 initial: true, 61 message: `Expo Go on ${deviceManager.name} is outdated, would you like to upgrade?`, 62 }); 63 if (confirm) { 64 // Don't need to uninstall to update on iOS. 65 if (this.platform !== 'ios') { 66 Log.log(`Uninstalling Expo Go from ${this.platform} device ${deviceManager.name}.`); 67 await deviceManager.uninstallAppAsync(this.appId); 68 } 69 return true; 70 } 71 } 72 return false; 73 } 74 75 /** Check if a given device has Expo Go installed, if not then download and install it. */ 76 async ensureAsync(deviceManager: DeviceManager<IDevice>): Promise<boolean> { 77 let shouldInstall = !(await deviceManager.isAppInstalledAsync(this.appId)); 78 79 if (APISettings.isOffline && !shouldInstall) { 80 Log.warn(`Skipping Expo Go version validation in offline mode`); 81 return false; 82 } else if (APISettings.isOffline && shouldInstall) { 83 throw new CommandError( 84 'NO_EXPO_GO', 85 `Expo Go is not installed on device "${deviceManager.name}", while running in offline mode. Manually install Expo Go or run without --offline flag.` 86 ); 87 } 88 89 if (!shouldInstall) { 90 shouldInstall = await this.uninstallExpoGoIfOutdatedAsync(deviceManager); 91 } 92 93 if (shouldInstall) { 94 // Download the Expo Go app from the Expo servers. 95 const binaryPath = await downloadExpoGoAsync(this.platform, { sdkVersion: this.sdkVersion }); 96 // Install the app on the device. 97 const ora = logNewSection(`Installing Expo Go on ${deviceManager.name}`); 98 try { 99 await deviceManager.installAppAsync(binaryPath); 100 } finally { 101 ora.stop(); 102 } 103 return true; 104 } 105 return false; 106 } 107} 108