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