18d307f52SEvan Baconimport { ExpoConfig, getAccountUsername, getConfig } from '@expo/config'; 28d307f52SEvan Baconimport chalk from 'chalk'; 38d307f52SEvan Bacon 48d307f52SEvan Baconimport { learnMore } from './link'; 58d307f52SEvan Baconimport { attemptModification } from './modifyConfigAsync'; 68d307f52SEvan Baconimport prompt, { confirmAsync } from './prompts'; 78d307f52SEvan Baconimport { 88d307f52SEvan Bacon assertValidBundleId, 98d307f52SEvan Bacon assertValidPackage, 108d307f52SEvan Bacon getBundleIdWarningAsync, 118d307f52SEvan Bacon getPackageNameWarningAsync, 128d307f52SEvan Bacon validateBundleId, 138d307f52SEvan Bacon validatePackage, 14*51740f69SEvan Bacon validatePackageWithWarning, 158d307f52SEvan Bacon} from './validateApplicationId'; 168a424bebSJames Ideimport * as Log from '../log'; 178d307f52SEvan Bacon 188d307f52SEvan Baconfunction getUsernameAsync(exp: ExpoConfig) { 198d307f52SEvan Bacon // TODO: Use XDL's UserManager 208d307f52SEvan Bacon // import { UserManager } from 'xdl'; 218d307f52SEvan Bacon return getAccountUsername(exp); 228d307f52SEvan Bacon} 238d307f52SEvan Bacon 248d307f52SEvan Baconconst NO_BUNDLE_ID_MESSAGE = `Project must have a \`ios.bundleIdentifier\` set in the Expo config (app.json or app.config.js).`; 258d307f52SEvan Bacon 268d307f52SEvan Baconconst NO_PACKAGE_MESSAGE = `Project must have a \`android.package\` set in the Expo config (app.json or app.config.js).`; 278d307f52SEvan Bacon 288d307f52SEvan Bacon/** 298d307f52SEvan Bacon * Get the bundle identifier from the Expo config or prompt the user to choose a new bundle identifier. 308d307f52SEvan Bacon * Prompted value will be validated against the App Store and a local regex. 318d307f52SEvan Bacon * If the project Expo config is a static JSON file, the bundle identifier will be updated in the config automatically. 328d307f52SEvan Bacon */ 338d307f52SEvan Baconexport async function getOrPromptForBundleIdentifier(projectRoot: string): Promise<string> { 348d307f52SEvan Bacon const { exp } = getConfig(projectRoot); 358d307f52SEvan Bacon 368d307f52SEvan Bacon const current = exp.ios?.bundleIdentifier; 378d307f52SEvan Bacon if (current) { 388d307f52SEvan Bacon assertValidBundleId(current); 398d307f52SEvan Bacon return current; 408d307f52SEvan Bacon } 418d307f52SEvan Bacon 428d307f52SEvan Bacon Log.log( 438d307f52SEvan Bacon chalk`\n{bold iOS Bundle Identifier} {dim ${learnMore( 448d307f52SEvan Bacon 'https://expo.fyi/bundle-identifier' 458d307f52SEvan Bacon )}}\n` 468d307f52SEvan Bacon ); 478d307f52SEvan Bacon 488d307f52SEvan Bacon return await promptForBundleIdAsync(projectRoot, exp); 498d307f52SEvan Bacon} 508d307f52SEvan Bacon 518d307f52SEvan Baconasync function promptForBundleIdAsync(projectRoot: string, exp: ExpoConfig): Promise<string> { 528d307f52SEvan Bacon // Prompt the user for the bundle ID. 538d307f52SEvan Bacon // Even if the project is using a dynamic config we can still 548d307f52SEvan Bacon // prompt a better error message, recommend a default value, and help the user 558d307f52SEvan Bacon // validate their custom bundle ID upfront. 568d307f52SEvan Bacon const { bundleIdentifier } = await prompt( 578d307f52SEvan Bacon { 588d307f52SEvan Bacon type: 'text', 598d307f52SEvan Bacon name: 'bundleIdentifier', 6029975bfdSEvan Bacon initial: (await getRecommendedBundleIdAsync(exp)) ?? undefined, 618d307f52SEvan Bacon // The Apple helps people know this isn't an EAS feature. 628d307f52SEvan Bacon message: `What would you like your iOS bundle identifier to be?`, 638d307f52SEvan Bacon validate: validateBundleId, 648d307f52SEvan Bacon }, 658d307f52SEvan Bacon { 668d307f52SEvan Bacon nonInteractiveHelp: NO_BUNDLE_ID_MESSAGE, 678d307f52SEvan Bacon } 688d307f52SEvan Bacon ); 698d307f52SEvan Bacon 708d307f52SEvan Bacon // Warn the user if the bundle ID is already in use. 718d307f52SEvan Bacon const warning = await getBundleIdWarningAsync(bundleIdentifier); 728d307f52SEvan Bacon if (warning && !(await warnAndConfirmAsync(warning))) { 738d307f52SEvan Bacon // Cycle the Bundle ID prompt to try again. 748d307f52SEvan Bacon return await promptForBundleIdAsync(projectRoot, exp); 758d307f52SEvan Bacon } 768d307f52SEvan Bacon 778d307f52SEvan Bacon // Apply the changes to the config. 788d307f52SEvan Bacon await attemptModification( 798d307f52SEvan Bacon projectRoot, 808d307f52SEvan Bacon { 818d307f52SEvan Bacon ios: { ...(exp.ios || {}), bundleIdentifier }, 828d307f52SEvan Bacon }, 838d307f52SEvan Bacon { ios: { bundleIdentifier } } 848d307f52SEvan Bacon ); 858d307f52SEvan Bacon 868d307f52SEvan Bacon return bundleIdentifier; 878d307f52SEvan Bacon} 888d307f52SEvan Bacon 898d307f52SEvan Baconasync function warnAndConfirmAsync(warning: string): Promise<boolean> { 908d307f52SEvan Bacon Log.log(); 918d307f52SEvan Bacon Log.warn(warning); 928d307f52SEvan Bacon Log.log(); 938d307f52SEvan Bacon if ( 948d307f52SEvan Bacon !(await confirmAsync({ 958d307f52SEvan Bacon message: `Continue?`, 968d307f52SEvan Bacon initial: true, 978d307f52SEvan Bacon })) 988d307f52SEvan Bacon ) { 998d307f52SEvan Bacon return false; 1008d307f52SEvan Bacon } 1018d307f52SEvan Bacon return true; 1028d307f52SEvan Bacon} 1038d307f52SEvan Bacon 1048d307f52SEvan Bacon// Recommend a bundle identifier based on the username and project slug. 1058d307f52SEvan Baconasync function getRecommendedBundleIdAsync(exp: ExpoConfig): Promise<string | null> { 1068d307f52SEvan Bacon // Attempt to use the android package name first since it's convenient to have them aligned. 1078d307f52SEvan Bacon if (exp.android?.package && validateBundleId(exp.android?.package)) { 1088d307f52SEvan Bacon return exp.android?.package; 1098d307f52SEvan Bacon } else { 1108d307f52SEvan Bacon const username = await getUsernameAsync(exp); 1118d307f52SEvan Bacon const possibleId = `com.${username}.${exp.slug}`; 1128d307f52SEvan Bacon if (username && validateBundleId(possibleId)) { 1138d307f52SEvan Bacon return possibleId; 1148d307f52SEvan Bacon } 1158d307f52SEvan Bacon } 1168d307f52SEvan Bacon 1178d307f52SEvan Bacon return null; 1188d307f52SEvan Bacon} 1198d307f52SEvan Bacon 1208d307f52SEvan Bacon// Recommend a package name based on the username and project slug. 1218d307f52SEvan Baconasync function getRecommendedPackageNameAsync(exp: ExpoConfig): Promise<string | null> { 1228d307f52SEvan Bacon // Attempt to use the ios bundle id first since it's convenient to have them aligned. 1238d307f52SEvan Bacon if (exp.ios?.bundleIdentifier && validatePackage(exp.ios.bundleIdentifier)) { 1248d307f52SEvan Bacon return exp.ios.bundleIdentifier; 1258d307f52SEvan Bacon } else { 1268d307f52SEvan Bacon const username = await getUsernameAsync(exp); 1278d307f52SEvan Bacon // It's common to use dashes in your node project name, strip them from the suggested package name. 1288d307f52SEvan Bacon const possibleId = `com.${username}.${exp.slug}`.split('-').join(''); 1298d307f52SEvan Bacon if (username && validatePackage(possibleId)) { 1308d307f52SEvan Bacon return possibleId; 1318d307f52SEvan Bacon } 1328d307f52SEvan Bacon } 1338d307f52SEvan Bacon return null; 1348d307f52SEvan Bacon} 1358d307f52SEvan Bacon 1368d307f52SEvan Bacon/** 1378d307f52SEvan Bacon * Get the package name from the Expo config or prompt the user to choose a new package name. 1388d307f52SEvan Bacon * Prompted value will be validated against the Play Store and a local regex. 1398d307f52SEvan Bacon * If the project Expo config is a static JSON file, the package name will be updated in the config automatically. 1408d307f52SEvan Bacon */ 1418d307f52SEvan Baconexport async function getOrPromptForPackage(projectRoot: string): Promise<string> { 1428d307f52SEvan Bacon const { exp } = getConfig(projectRoot); 1438d307f52SEvan Bacon 1448d307f52SEvan Bacon const current = exp.android?.package; 1458d307f52SEvan Bacon if (current) { 1468d307f52SEvan Bacon assertValidPackage(current); 1478d307f52SEvan Bacon return current; 1488d307f52SEvan Bacon } 1498d307f52SEvan Bacon 1508d307f52SEvan Bacon Log.log( 1518d307f52SEvan Bacon chalk`\n{bold Android package} {dim ${learnMore('https://expo.fyi/android-package')}}\n` 1528d307f52SEvan Bacon ); 1538d307f52SEvan Bacon 1548d307f52SEvan Bacon return await promptForPackageAsync(projectRoot, exp); 1558d307f52SEvan Bacon} 1568d307f52SEvan Bacon 1578d307f52SEvan Baconasync function promptForPackageAsync(projectRoot: string, exp: ExpoConfig): Promise<string> { 1588d307f52SEvan Bacon // Prompt the user for the android package. 1598d307f52SEvan Bacon // Even if the project is using a dynamic config we can still 1608d307f52SEvan Bacon // prompt a better error message, recommend a default value, and help the user 1618d307f52SEvan Bacon // validate their custom android package upfront. 1628d307f52SEvan Bacon const { packageName } = await prompt( 1638d307f52SEvan Bacon { 1648d307f52SEvan Bacon type: 'text', 1658d307f52SEvan Bacon name: 'packageName', 16629975bfdSEvan Bacon initial: (await getRecommendedPackageNameAsync(exp)) ?? undefined, 1678d307f52SEvan Bacon message: `What would you like your Android package name to be?`, 168*51740f69SEvan Bacon validate: validatePackageWithWarning, 1698d307f52SEvan Bacon }, 1708d307f52SEvan Bacon { 1718d307f52SEvan Bacon nonInteractiveHelp: NO_PACKAGE_MESSAGE, 1728d307f52SEvan Bacon } 1738d307f52SEvan Bacon ); 1748d307f52SEvan Bacon 1758d307f52SEvan Bacon // Warn the user if the package name is already in use. 1768d307f52SEvan Bacon const warning = await getPackageNameWarningAsync(packageName); 1778d307f52SEvan Bacon if (warning && !(await warnAndConfirmAsync(warning))) { 1788d307f52SEvan Bacon // Cycle the Package name prompt to try again. 1798d307f52SEvan Bacon return await promptForPackageAsync(projectRoot, exp); 1808d307f52SEvan Bacon } 1818d307f52SEvan Bacon 1828d307f52SEvan Bacon // Apply the changes to the config. 1838d307f52SEvan Bacon await attemptModification( 1848d307f52SEvan Bacon projectRoot, 1858d307f52SEvan Bacon { 1868d307f52SEvan Bacon android: { ...(exp.android || {}), package: packageName }, 1878d307f52SEvan Bacon }, 1888d307f52SEvan Bacon { 1898d307f52SEvan Bacon android: { package: packageName }, 1908d307f52SEvan Bacon } 1918d307f52SEvan Bacon ); 1928d307f52SEvan Bacon 1938d307f52SEvan Bacon return packageName; 1948d307f52SEvan Bacon} 195