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