18d307f52SEvan Baconimport semver from 'semver'; 28d307f52SEvan Bacon 3*8a424bebSJames Ideimport type { DeviceManager } from './DeviceManager'; 48d307f52SEvan Baconimport { getVersionsAsync } from '../../api/getVersions'; 58d307f52SEvan Baconimport * as Log from '../../log'; 68d307f52SEvan Baconimport { downloadExpoGoAsync } from '../../utils/downloadExpoGoAsync'; 7e32ccf9fSEvan Baconimport { env } from '../../utils/env'; 874e3651eSCedric van Puttenimport { CommandError } from '../../utils/errors'; 98d307f52SEvan Baconimport { logNewSection } from '../../utils/ora'; 108d307f52SEvan Baconimport { confirmAsync } from '../../utils/prompts'; 118d307f52SEvan Bacon 12474a7a4bSEvan Baconconst debug = require('debug')('expo:utils:ExpoGoInstaller') as typeof console.log; 13474a7a4bSEvan Bacon 148d307f52SEvan Bacon/** Given a platform, appId, and sdkVersion, this module will ensure that Expo Go is up-to-date on the provided device. */ 158d307f52SEvan Baconexport class ExpoGoInstaller<IDevice> { 168d307f52SEvan Bacon // Keep a list of [platform-deviceId] so we can prevent asking multiple times if a user wants to upgrade. 178d307f52SEvan Bacon // This can prevent annoying interactions when they don't want to upgrade for whatever reason. 188d307f52SEvan Bacon static cache: Record<string, boolean> = {}; 198d307f52SEvan Bacon 208d307f52SEvan Bacon constructor( 218d307f52SEvan Bacon private platform: 'ios' | 'android', 228d307f52SEvan Bacon // Ultimately this should be inlined since we know the platform. 238d307f52SEvan Bacon private appId: string, 248d307f52SEvan Bacon private sdkVersion?: string 258d307f52SEvan Bacon ) {} 268d307f52SEvan Bacon 278d307f52SEvan Bacon /** Returns true if the installed app matching the previously provided `appId` is outdated. */ 288d307f52SEvan Bacon async isClientOutdatedAsync(device: DeviceManager<IDevice>): Promise<boolean> { 298d307f52SEvan Bacon const installedVersion = await device.getAppVersionAsync(this.appId); 308d307f52SEvan Bacon if (!installedVersion) { 318d307f52SEvan Bacon return true; 328d307f52SEvan Bacon } 338d307f52SEvan Bacon const version = await this._getExpectedClientVersionAsync(); 34474a7a4bSEvan Bacon debug(`Expected Expo Go version: ${version}, installed version: ${installedVersion}`); 356079da0aSEvan Bacon return version ? !semver.eq(installedVersion, version) : true; 368d307f52SEvan Bacon } 378d307f52SEvan Bacon 388d307f52SEvan Bacon /** Returns the expected version of Expo Go given the project SDK Version. Exposed for testing. */ 398d307f52SEvan Bacon async _getExpectedClientVersionAsync(): Promise<string | null> { 408d307f52SEvan Bacon const versions = await getVersionsAsync(); 418d307f52SEvan Bacon // Like `sdkVersions['44.0.0']['androidClientVersion'] = '1.0.0'` 428d307f52SEvan Bacon const specificVersion = 4329975bfdSEvan Bacon versions?.sdkVersions?.[this.sdkVersion!]?.[`${this.platform}ClientVersion`]; 448d307f52SEvan Bacon const latestVersion = versions[`${this.platform}Version`]; 458d307f52SEvan Bacon return specificVersion ?? latestVersion ?? null; 468d307f52SEvan Bacon } 478d307f52SEvan Bacon 488d307f52SEvan Bacon /** Returns a boolean indicating if Expo Go should be installed. Returns `true` if the app was uninstalled. */ 498d307f52SEvan Bacon async uninstallExpoGoIfOutdatedAsync(deviceManager: DeviceManager<IDevice>): Promise<boolean> { 508d307f52SEvan Bacon const cacheId = `${this.platform}-${deviceManager.identifier}`; 518d307f52SEvan Bacon 528d307f52SEvan Bacon if (ExpoGoInstaller.cache[cacheId]) { 5388643930SEvan Bacon debug('skipping subsequent upgrade check'); 548d307f52SEvan Bacon return false; 558d307f52SEvan Bacon } 5688643930SEvan Bacon ExpoGoInstaller.cache[cacheId] = true; 576a116c5fSEvan Bacon 588d307f52SEvan Bacon if (await this.isClientOutdatedAsync(deviceManager)) { 596a116c5fSEvan Bacon if (this.sdkVersion === 'UNVERSIONED') { 606a116c5fSEvan Bacon // This should only happen in the expo/expo repo, e.g. `apps/test-suite` 616a116c5fSEvan Bacon Log.log( 626a116c5fSEvan Bacon `Skipping Expo Go upgrade check for UNVERSIONED project. Manually ensure the Expo Go app is built from source.` 636a116c5fSEvan Bacon ); 646a116c5fSEvan Bacon return false; 656a116c5fSEvan Bacon } 666a116c5fSEvan Bacon 678d307f52SEvan Bacon // Only prompt once per device, per run. 688d307f52SEvan Bacon const confirm = await confirmAsync({ 698d307f52SEvan Bacon initial: true, 708d307f52SEvan Bacon message: `Expo Go on ${deviceManager.name} is outdated, would you like to upgrade?`, 718d307f52SEvan Bacon }); 728d307f52SEvan Bacon if (confirm) { 738d307f52SEvan Bacon // Don't need to uninstall to update on iOS. 748d307f52SEvan Bacon if (this.platform !== 'ios') { 758d307f52SEvan Bacon Log.log(`Uninstalling Expo Go from ${this.platform} device ${deviceManager.name}.`); 768d307f52SEvan Bacon await deviceManager.uninstallAppAsync(this.appId); 778d307f52SEvan Bacon } 788d307f52SEvan Bacon return true; 798d307f52SEvan Bacon } 808d307f52SEvan Bacon } 818d307f52SEvan Bacon return false; 828d307f52SEvan Bacon } 838d307f52SEvan Bacon 848d307f52SEvan Bacon /** Check if a given device has Expo Go installed, if not then download and install it. */ 858d307f52SEvan Bacon async ensureAsync(deviceManager: DeviceManager<IDevice>): Promise<boolean> { 868d307f52SEvan Bacon let shouldInstall = !(await deviceManager.isAppInstalledAsync(this.appId)); 878d307f52SEvan Bacon 88e32ccf9fSEvan Bacon if (env.EXPO_OFFLINE) { 89e32ccf9fSEvan Bacon if (!shouldInstall) { 9074e3651eSCedric van Putten Log.warn(`Skipping Expo Go version validation in offline mode`); 9174e3651eSCedric van Putten return false; 92e32ccf9fSEvan Bacon } 9374e3651eSCedric van Putten throw new CommandError( 9474e3651eSCedric van Putten 'NO_EXPO_GO', 95e32ccf9fSEvan Bacon `Expo Go is not installed on device "${deviceManager.name}", while running in offline mode. Manually install Expo Go or run without --offline flag (or EXPO_OFFLINE environment variable).` 9674e3651eSCedric van Putten ); 9774e3651eSCedric van Putten } 9874e3651eSCedric van Putten 998d307f52SEvan Bacon if (!shouldInstall) { 1008d307f52SEvan Bacon shouldInstall = await this.uninstallExpoGoIfOutdatedAsync(deviceManager); 1018d307f52SEvan Bacon } 1028d307f52SEvan Bacon 1038d307f52SEvan Bacon if (shouldInstall) { 1048d307f52SEvan Bacon // Download the Expo Go app from the Expo servers. 1058a782c0fSEvan Bacon const binaryPath = await downloadExpoGoAsync(this.platform, { sdkVersion: this.sdkVersion }); 1068d307f52SEvan Bacon // Install the app on the device. 1078d307f52SEvan Bacon const ora = logNewSection(`Installing Expo Go on ${deviceManager.name}`); 1088d307f52SEvan Bacon try { 1098d307f52SEvan Bacon await deviceManager.installAppAsync(binaryPath); 1108d307f52SEvan Bacon } finally { 1118d307f52SEvan Bacon ora.stop(); 1128d307f52SEvan Bacon } 1138d307f52SEvan Bacon return true; 1148d307f52SEvan Bacon } 1158d307f52SEvan Bacon return false; 1168d307f52SEvan Bacon } 1178d307f52SEvan Bacon} 118