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