1import { ExpoConfig, getAccountUsername, getConfig } from '@expo/config'; 2import chalk from 'chalk'; 3 4import * as Log from '../log'; 5import { learnMore } from './link'; 6import { attemptModification } from './modifyConfigAsync'; 7import prompt, { confirmAsync } from './prompts'; 8import { 9 assertValidBundleId, 10 assertValidPackage, 11 getBundleIdWarningAsync, 12 getPackageNameWarningAsync, 13 validateBundleId, 14 validatePackage, 15} from './validateApplicationId'; 16 17function getUsernameAsync(exp: ExpoConfig) { 18 // TODO: Use XDL's UserManager 19 // import { UserManager } from 'xdl'; 20 return getAccountUsername(exp); 21} 22 23const NO_BUNDLE_ID_MESSAGE = `Project must have a \`ios.bundleIdentifier\` set in the Expo config (app.json or app.config.js).`; 24 25const NO_PACKAGE_MESSAGE = `Project must have a \`android.package\` set in the Expo config (app.json or app.config.js).`; 26 27/** 28 * Get the bundle identifier from the Expo config or prompt the user to choose a new bundle identifier. 29 * Prompted value will be validated against the App Store and a local regex. 30 * If the project Expo config is a static JSON file, the bundle identifier will be updated in the config automatically. 31 */ 32export async function getOrPromptForBundleIdentifier(projectRoot: string): Promise<string> { 33 const { exp } = getConfig(projectRoot); 34 35 const current = exp.ios?.bundleIdentifier; 36 if (current) { 37 assertValidBundleId(current); 38 return current; 39 } 40 41 Log.log( 42 chalk`\n{bold iOS Bundle Identifier} {dim ${learnMore( 43 'https://expo.fyi/bundle-identifier' 44 )}}\n` 45 ); 46 47 return await promptForBundleIdAsync(projectRoot, exp); 48} 49 50async function promptForBundleIdAsync(projectRoot: string, exp: ExpoConfig): Promise<string> { 51 // Prompt the user for the bundle ID. 52 // Even if the project is using a dynamic config we can still 53 // prompt a better error message, recommend a default value, and help the user 54 // validate their custom bundle ID upfront. 55 const { bundleIdentifier } = await prompt( 56 { 57 type: 'text', 58 name: 'bundleIdentifier', 59 initial: (await getRecommendedBundleIdAsync(exp)) ?? undefined, 60 // The Apple helps people know this isn't an EAS feature. 61 message: `What would you like your iOS bundle identifier to be?`, 62 validate: validateBundleId, 63 }, 64 { 65 nonInteractiveHelp: NO_BUNDLE_ID_MESSAGE, 66 } 67 ); 68 69 // Warn the user if the bundle ID is already in use. 70 const warning = await getBundleIdWarningAsync(bundleIdentifier); 71 if (warning && !(await warnAndConfirmAsync(warning))) { 72 // Cycle the Bundle ID prompt to try again. 73 return await promptForBundleIdAsync(projectRoot, exp); 74 } 75 76 // Apply the changes to the config. 77 await attemptModification( 78 projectRoot, 79 { 80 ios: { ...(exp.ios || {}), bundleIdentifier }, 81 }, 82 { ios: { bundleIdentifier } } 83 ); 84 85 return bundleIdentifier; 86} 87 88async function warnAndConfirmAsync(warning: string): Promise<boolean> { 89 Log.log(); 90 Log.warn(warning); 91 Log.log(); 92 if ( 93 !(await confirmAsync({ 94 message: `Continue?`, 95 initial: true, 96 })) 97 ) { 98 return false; 99 } 100 return true; 101} 102 103// Recommend a bundle identifier based on the username and project slug. 104async function getRecommendedBundleIdAsync(exp: ExpoConfig): Promise<string | null> { 105 // Attempt to use the android package name first since it's convenient to have them aligned. 106 if (exp.android?.package && validateBundleId(exp.android?.package)) { 107 return exp.android?.package; 108 } else { 109 const username = await getUsernameAsync(exp); 110 const possibleId = `com.${username}.${exp.slug}`; 111 if (username && validateBundleId(possibleId)) { 112 return possibleId; 113 } 114 } 115 116 return null; 117} 118 119// Recommend a package name based on the username and project slug. 120async function getRecommendedPackageNameAsync(exp: ExpoConfig): Promise<string | null> { 121 // Attempt to use the ios bundle id first since it's convenient to have them aligned. 122 if (exp.ios?.bundleIdentifier && validatePackage(exp.ios.bundleIdentifier)) { 123 return exp.ios.bundleIdentifier; 124 } else { 125 const username = await getUsernameAsync(exp); 126 // It's common to use dashes in your node project name, strip them from the suggested package name. 127 const possibleId = `com.${username}.${exp.slug}`.split('-').join(''); 128 if (username && validatePackage(possibleId)) { 129 return possibleId; 130 } 131 } 132 return null; 133} 134 135/** 136 * Get the package name from the Expo config or prompt the user to choose a new package name. 137 * Prompted value will be validated against the Play Store and a local regex. 138 * If the project Expo config is a static JSON file, the package name will be updated in the config automatically. 139 */ 140export async function getOrPromptForPackage(projectRoot: string): Promise<string> { 141 const { exp } = getConfig(projectRoot); 142 143 const current = exp.android?.package; 144 if (current) { 145 assertValidPackage(current); 146 return current; 147 } 148 149 Log.log( 150 chalk`\n{bold Android package} {dim ${learnMore('https://expo.fyi/android-package')}}\n` 151 ); 152 153 return await promptForPackageAsync(projectRoot, exp); 154} 155 156async function promptForPackageAsync(projectRoot: string, exp: ExpoConfig): Promise<string> { 157 // Prompt the user for the android package. 158 // Even if the project is using a dynamic config we can still 159 // prompt a better error message, recommend a default value, and help the user 160 // validate their custom android package upfront. 161 const { packageName } = await prompt( 162 { 163 type: 'text', 164 name: 'packageName', 165 initial: (await getRecommendedPackageNameAsync(exp)) ?? undefined, 166 message: `What would you like your Android package name to be?`, 167 validate: validatePackage, 168 }, 169 { 170 nonInteractiveHelp: NO_PACKAGE_MESSAGE, 171 } 172 ); 173 174 // Warn the user if the package name is already in use. 175 const warning = await getPackageNameWarningAsync(packageName); 176 if (warning && !(await warnAndConfirmAsync(warning))) { 177 // Cycle the Package name prompt to try again. 178 return await promptForPackageAsync(projectRoot, exp); 179 } 180 181 // Apply the changes to the config. 182 await attemptModification( 183 projectRoot, 184 { 185 android: { ...(exp.android || {}), package: packageName }, 186 }, 187 { 188 android: { package: packageName }, 189 } 190 ); 191 192 return packageName; 193} 194