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