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