1import chalk from 'chalk';
2import { Ora } from 'ora';
3import os from 'os';
4import path from 'path';
5
6import { ensureDirectory } from '../../../utils/dir';
7import { env } from '../../../utils/env';
8import { CommandError } from '../../../utils/errors';
9import { ora } from '../../../utils/ora';
10import { confirmAsync } from '../../../utils/prompts';
11import * as AppleDevice from './AppleDevice';
12
13/** Get the app_delta folder for faster subsequent rebuilds on devices. */
14export function getAppDeltaDirectory(bundleId: string): string {
15  // TODO: Maybe use .expo folder instead for debugging
16  // TODO: Reuse existing folder from xcode?
17  const deltaFolder = path.join(os.tmpdir(), 'ios', 'app-delta', bundleId);
18  ensureDirectory(deltaFolder);
19  return deltaFolder;
20}
21
22/**
23 * Wraps the apple device method for installing and running an app,
24 * adds indicator and retry loop for when the device is locked.
25 */
26export async function installOnDeviceAsync(props: {
27  bundle: string;
28  bundleIdentifier: string;
29  appDeltaDirectory: string;
30  udid: string;
31  deviceName: string;
32}): Promise<void> {
33  const { bundle, bundleIdentifier, appDeltaDirectory, udid, deviceName } = props;
34  let indicator: Ora | undefined;
35
36  try {
37    // TODO: Connect for logs
38    await AppleDevice.runOnDevice({
39      udid,
40      appPath: bundle,
41      bundleId: bundleIdentifier,
42      waitForApp: false,
43      deltaPath: appDeltaDirectory,
44      onProgress({
45        status,
46        isComplete,
47        progress,
48      }: {
49        status: string;
50        isComplete: boolean;
51        progress: number;
52      }) {
53        if (!indicator) {
54          indicator = ora(status).start();
55        }
56        indicator.text = `${chalk.bold(status)} ${progress}%`;
57        if (isComplete) {
58          indicator.succeed();
59        }
60      },
61    });
62  } catch (error: any) {
63    if (indicator) {
64      indicator.fail();
65    }
66    if (error.code === 'APPLE_DEVICE_LOCKED') {
67      // Get the app name from the binary path.
68      const appName = path.basename(bundle).split('.')[0] ?? 'app';
69      if (
70        !env.CI &&
71        (await confirmAsync({
72          message: `Cannot launch ${appName} because the device is locked. Unlock ${deviceName} to continue...`,
73          initial: true,
74        }))
75      ) {
76        return installOnDeviceAsync(props);
77      }
78      throw new CommandError(
79        `Cannot launch ${appName} on ${deviceName} because the device is locked.`
80      );
81    }
82    throw error;
83  }
84}
85