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