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