1import chalk from 'chalk';
2
3import * as Security from './Security';
4import { getLastDeveloperCodeSigningIdAsync, setLastDeveloperCodeSigningIdAsync } from './settings';
5import * as Log from '../../../log';
6import { CommandError } from '../../../utils/errors';
7import { isInteractive } from '../../../utils/interactive';
8import { learnMore } from '../../../utils/link';
9import { selectAsync } from '../../../utils/prompts';
10
11/**
12 * Sort the code signing items so the last selected item (user's default) is the first suggested.
13 */
14export async function sortDefaultIdToBeginningAsync(
15  identities: Security.CertificateSigningInfo[]
16): Promise<[Security.CertificateSigningInfo[], string | null]> {
17  const lastSelected = await getLastDeveloperCodeSigningIdAsync();
18
19  if (lastSelected) {
20    let iterations = 0;
21    while (identities[0].signingCertificateId !== lastSelected && iterations < identities.length) {
22      identities.push(identities.shift()!);
23      iterations++;
24    }
25  }
26  return [identities, lastSelected];
27}
28
29/**
30 * Assert that the computer needs code signing setup.
31 * This links to an FYI page that was user tested internally.
32 */
33function assertCodeSigningSetup(): never {
34  // TODO: We can probably do this too automatically.
35  Log.log(
36    `\u203A Your computer requires some additional setup before you can build onto physical iOS devices.\n  ${chalk.bold(
37      learnMore('https://expo.fyi/setup-xcode-signing')
38    )}`
39  );
40
41  throw new CommandError('No code signing certificates are available to use.');
42}
43
44/**
45 * Resolve the best certificate signing identity from a given list of IDs.
46 * - If no IDs: Assert that the user has to setup code signing.
47 * - If one ID: Return the first ID.
48 * - If multiple IDs: Ask the user to select one, then store the value to be suggested first next time (since users generally use the same ID).
49 */
50export async function resolveCertificateSigningIdentityAsync(
51  ids: string[]
52): Promise<Security.CertificateSigningInfo> {
53  // The user has no valid code signing identities.
54  if (!ids.length) {
55    assertCodeSigningSetup();
56  }
57
58  //  One ID available �� Program is not interactive
59  //
60  //     using the the first available option
61  if (ids.length === 1 || !isInteractive()) {
62    // This method is cheaper than `resolveIdentitiesAsync` and checking the
63    // cached user preference so we should use this as early as possible.
64    return Security.resolveCertificateSigningInfoAsync(ids[0]);
65  }
66
67  // Get identities and sort by the one that the user is most likely to choose.
68  const [identities, preferred] = await sortDefaultIdToBeginningAsync(
69    await Security.resolveIdentitiesAsync(ids)
70  );
71
72  const selected = await selectDevelopmentTeamAsync(identities, preferred);
73
74  // Store the last used value and suggest it as the first value
75  // next time the user has to select a code signing identity.
76  await setLastDeveloperCodeSigningIdAsync(selected.signingCertificateId);
77
78  return selected;
79}
80
81/** Prompt the user to select a development team, highlighting the preferred value based on the user history. */
82export async function selectDevelopmentTeamAsync(
83  identities: Security.CertificateSigningInfo[],
84  preferredId: string | null
85): Promise<Security.CertificateSigningInfo> {
86  const index = await selectAsync(
87    'Development team for signing the app',
88    identities.map((value, i) => {
89      const format =
90        value.signingCertificateId === preferredId ? chalk.bold : (message: string) => message;
91      return {
92        // Formatted like: `650 Industries, Inc. (A1BCDEF234) - Apple Development: Evan Bacon (AA00AABB0A)`
93        title: format(
94          [value.appleTeamName, `(${value.appleTeamId}) -`, value.codeSigningInfo].join(' ')
95        ),
96        value: i,
97      };
98    })
99  );
100
101  return identities[index];
102}
103