18d307f52SEvan Baconimport assert from 'assert'; 28d307f52SEvan Baconimport chalk from 'chalk'; 38d307f52SEvan Bacon 4e32ccf9fSEvan Baconimport { env } from './env'; 5e32ccf9fSEvan Baconimport { memoize } from './fn'; 68d307f52SEvan Baconimport { learnMore } from './link'; 78d307f52SEvan Baconimport { isUrlAvailableAsync } from './url'; 88a424bebSJames Ideimport { fetchAsync } from '../api/rest/client'; 98a424bebSJames Ideimport { Log } from '../log'; 108d307f52SEvan Bacon 11474a7a4bSEvan Baconconst debug = require('debug')('expo:utils:validateApplicationId') as typeof console.log; 12474a7a4bSEvan Bacon 138d307f52SEvan Baconconst IOS_BUNDLE_ID_REGEX = /^[a-zA-Z0-9-.]+$/; 14*f4cccd85SAlan Hughesconst ANDROID_PACKAGE_REGEX = /^(?!.*\bnative\b)[a-zA-Z][a-zA-Z0-9_]*(\.[a-zA-Z][a-zA-Z0-9_]*)+$/; 158d307f52SEvan Bacon 168d307f52SEvan Bacon/** Validate an iOS bundle identifier. */ 178d307f52SEvan Baconexport function validateBundleId(value: string): boolean { 188d307f52SEvan Bacon return IOS_BUNDLE_ID_REGEX.test(value); 198d307f52SEvan Bacon} 208d307f52SEvan Bacon 218d307f52SEvan Bacon/** Validate an Android package name. */ 228d307f52SEvan Baconexport function validatePackage(value: string): boolean { 2351740f69SEvan Bacon return validatePackageWithWarning(value) === true; 248d307f52SEvan Bacon} 258d307f52SEvan Bacon 2651740f69SEvan Bacon/** Validate an Android package name and return the reason if invalid. */ 2751740f69SEvan Baconexport function validatePackageWithWarning(value: string): true | string { 2851740f69SEvan Bacon const parts = value.split('.'); 2951740f69SEvan Bacon for (const segment of parts) { 3051740f69SEvan Bacon if (RESERVED_ANDROID_PACKAGE_NAME_SEGMENTS.includes(segment)) { 3151740f69SEvan Bacon return `"${segment}" is a reserved Java keyword.`; 3251740f69SEvan Bacon } 3351740f69SEvan Bacon } 3451740f69SEvan Bacon if (parts.length < 2) { 3551740f69SEvan Bacon return `Package name must contain more than one segment, separated by ".", e.g. com.${value}`; 3651740f69SEvan Bacon } 3751740f69SEvan Bacon if (!ANDROID_PACKAGE_REGEX.test(value)) { 3851740f69SEvan Bacon return 'Invalid characters in Android package name. Only alphanumeric characters, "." and "_" are allowed, and each "." must be followed by a letter or number.'; 3951740f69SEvan Bacon } 4051740f69SEvan Bacon 4151740f69SEvan Bacon return true; 4251740f69SEvan Bacon} 4351740f69SEvan Bacon 4451740f69SEvan Bacon// https://en.wikipedia.org/wiki/List_of_Java_keywords 4551740f69SEvan Bacon// Running the following in the console and pruning the "Reserved Identifiers" section: 4651740f69SEvan Bacon// [...document.querySelectorAll('dl > dt > code')].map(node => node.innerText) 4751740f69SEvan Baconconst RESERVED_ANDROID_PACKAGE_NAME_SEGMENTS = [ 4851740f69SEvan Bacon // List of Java keywords 4951740f69SEvan Bacon '_', 5051740f69SEvan Bacon 'abstract', 5151740f69SEvan Bacon 'assert', 5251740f69SEvan Bacon 'boolean', 5351740f69SEvan Bacon 'break', 5451740f69SEvan Bacon 'byte', 5551740f69SEvan Bacon 'case', 5651740f69SEvan Bacon 'catch', 5751740f69SEvan Bacon 'char', 5851740f69SEvan Bacon 'class', 5951740f69SEvan Bacon 'const', 6051740f69SEvan Bacon 'continue', 6151740f69SEvan Bacon 'default', 6251740f69SEvan Bacon 'do', 6351740f69SEvan Bacon 'double', 6451740f69SEvan Bacon 'else', 6551740f69SEvan Bacon 'enum', 6651740f69SEvan Bacon 'extends', 6751740f69SEvan Bacon 'final', 6851740f69SEvan Bacon 'finally', 6951740f69SEvan Bacon 'float', 7051740f69SEvan Bacon 'for', 7151740f69SEvan Bacon 'goto', 7251740f69SEvan Bacon 'if', 7351740f69SEvan Bacon 'implements', 7451740f69SEvan Bacon 'import', 7551740f69SEvan Bacon 'instanceof', 7651740f69SEvan Bacon 'int', 7751740f69SEvan Bacon 'interface', 7851740f69SEvan Bacon 'long', 7951740f69SEvan Bacon 'native', 8051740f69SEvan Bacon 'new', 8151740f69SEvan Bacon 'package', 8251740f69SEvan Bacon 'private', 8351740f69SEvan Bacon 'protected', 8451740f69SEvan Bacon 'public', 8551740f69SEvan Bacon 'return', 8651740f69SEvan Bacon 'short', 8751740f69SEvan Bacon 'static', 8851740f69SEvan Bacon 'super', 8951740f69SEvan Bacon 'switch', 9051740f69SEvan Bacon 'synchronized', 9151740f69SEvan Bacon 'this', 9251740f69SEvan Bacon 'throw', 9351740f69SEvan Bacon 'throws', 9451740f69SEvan Bacon 'transient', 9551740f69SEvan Bacon 'try', 9651740f69SEvan Bacon 'void', 9751740f69SEvan Bacon 'volatile', 9851740f69SEvan Bacon 'while', 9951740f69SEvan Bacon // Reserved words for literal values 10051740f69SEvan Bacon 'true', 10151740f69SEvan Bacon 'false', 10251740f69SEvan Bacon 'null', 10351740f69SEvan Bacon // Unused 10451740f69SEvan Bacon 'const', 10551740f69SEvan Bacon 'goto', 10651740f69SEvan Bacon 'strictfp', 10751740f69SEvan Bacon]; 10851740f69SEvan Bacon 1098d307f52SEvan Baconexport function assertValidBundleId(value: string) { 1108d307f52SEvan Bacon assert.match( 1118d307f52SEvan Bacon value, 1128d307f52SEvan Bacon IOS_BUNDLE_ID_REGEX, 1138d307f52SEvan Bacon `The ios.bundleIdentifier defined in your Expo config is not formatted properly. Only alphanumeric characters, '.', '-', and '_' are allowed, and each '.' must be followed by a letter.` 1148d307f52SEvan Bacon ); 1158d307f52SEvan Bacon} 1168d307f52SEvan Bacon 1178d307f52SEvan Baconexport function assertValidPackage(value: string) { 1188d307f52SEvan Bacon assert.match( 1198d307f52SEvan Bacon value, 1208d307f52SEvan Bacon ANDROID_PACKAGE_REGEX, 121*f4cccd85SAlan Hughes `Invalid format of Android package name. Only alphanumeric characters, '.' and '_' are allowed, and each '.' must be followed by a letter. The Java keyword 'native' is not allowed.` 1228d307f52SEvan Bacon ); 1238d307f52SEvan Bacon} 1248d307f52SEvan Bacon 125e32ccf9fSEvan Bacon/** @private */ 126e32ccf9fSEvan Baconexport async function getBundleIdWarningInternalAsync(bundleId: string): Promise<string | null> { 127e32ccf9fSEvan Bacon if (env.EXPO_OFFLINE) { 128e32ccf9fSEvan Bacon Log.warn('Skipping Apple bundle identifier reservation validation in offline-mode.'); 129e32ccf9fSEvan Bacon return null; 1308d307f52SEvan Bacon } 1318d307f52SEvan Bacon 1328d307f52SEvan Bacon if (!(await isUrlAvailableAsync('itunes.apple.com'))) { 133474a7a4bSEvan Bacon debug( 134474a7a4bSEvan Bacon `Couldn't connect to iTunes Store to check bundle ID ${bundleId}. itunes.apple.com may be down.` 135474a7a4bSEvan Bacon ); 1368d307f52SEvan Bacon // If no network, simply skip the warnings since they'll just lead to more confusion. 1378d307f52SEvan Bacon return null; 1388d307f52SEvan Bacon } 1398d307f52SEvan Bacon 1408d307f52SEvan Bacon const url = `http://itunes.apple.com/lookup?bundleId=${bundleId}`; 1418d307f52SEvan Bacon try { 142474a7a4bSEvan Bacon debug(`Checking iOS bundle ID '${bundleId}' at: ${url}`); 1438c8eefe0SEvan Bacon const response = await fetchAsync(url); 1448d307f52SEvan Bacon const json = await response.json(); 1458d307f52SEvan Bacon if (json.resultCount > 0) { 1468d307f52SEvan Bacon const firstApp = json.results[0]; 147e32ccf9fSEvan Bacon return formatInUseWarning(firstApp.trackName, firstApp.sellerName, bundleId); 1488d307f52SEvan Bacon } 149474a7a4bSEvan Bacon } catch (error: any) { 150474a7a4bSEvan Bacon debug(`Error checking bundle ID ${bundleId}: ${error.message}`); 1518d307f52SEvan Bacon // Error fetching itunes data. 1528d307f52SEvan Bacon } 1538d307f52SEvan Bacon return null; 1548d307f52SEvan Bacon} 1558d307f52SEvan Bacon 156e32ccf9fSEvan Bacon/** Returns a warning message if an iOS bundle identifier is potentially already in use. */ 157e32ccf9fSEvan Baconexport const getBundleIdWarningAsync = memoize(getBundleIdWarningInternalAsync); 158e32ccf9fSEvan Bacon 159e32ccf9fSEvan Bacon/** @private */ 160e32ccf9fSEvan Baconexport async function getPackageNameWarningInternalAsync( 161e32ccf9fSEvan Bacon packageName: string 162e32ccf9fSEvan Bacon): Promise<string | null> { 163e32ccf9fSEvan Bacon if (env.EXPO_OFFLINE) { 164e32ccf9fSEvan Bacon Log.warn('Skipping Android package name reservation validation in offline-mode.'); 165e32ccf9fSEvan Bacon return null; 1668d307f52SEvan Bacon } 1678d307f52SEvan Bacon 1688d307f52SEvan Bacon if (!(await isUrlAvailableAsync('play.google.com'))) { 169474a7a4bSEvan Bacon debug( 170474a7a4bSEvan Bacon `Couldn't connect to Play Store to check package name ${packageName}. play.google.com may be down.` 171474a7a4bSEvan Bacon ); 1728d307f52SEvan Bacon // If no network, simply skip the warnings since they'll just lead to more confusion. 1738d307f52SEvan Bacon return null; 1748d307f52SEvan Bacon } 1758d307f52SEvan Bacon 1768d307f52SEvan Bacon const url = `https://play.google.com/store/apps/details?id=${packageName}`; 1778d307f52SEvan Bacon try { 178474a7a4bSEvan Bacon debug(`Checking Android package name '${packageName}' at: ${url}`); 1798c8eefe0SEvan Bacon const response = await fetchAsync(url); 1808d307f52SEvan Bacon // If the page exists, then warn the user. 1818d307f52SEvan Bacon if (response.status === 200) { 1828d307f52SEvan Bacon // There is no JSON API for the Play Store so we can't concisely 1838d307f52SEvan Bacon // locate the app name and developer to match the iOS warning. 184e32ccf9fSEvan Bacon return `⚠️ The package ${chalk.bold(packageName)} is already in use. ${chalk.dim( 1858d307f52SEvan Bacon learnMore(url) 1868d307f52SEvan Bacon )}`; 1878d307f52SEvan Bacon } 188474a7a4bSEvan Bacon } catch (error: any) { 1898d307f52SEvan Bacon // Error fetching play store data or the page doesn't exist. 190e32ccf9fSEvan Bacon debug(`Error checking package name ${packageName}: ${error.message}`); 1918d307f52SEvan Bacon } 1928d307f52SEvan Bacon return null; 1938d307f52SEvan Bacon} 1948d307f52SEvan Bacon 1958d307f52SEvan Baconfunction formatInUseWarning(appName: string, author: string, id: string): string { 1968d307f52SEvan Bacon return `⚠️ The app ${chalk.bold(appName)} by ${chalk.italic( 1978d307f52SEvan Bacon author 1988d307f52SEvan Bacon )} is already using ${chalk.bold(id)}`; 1998d307f52SEvan Bacon} 200e32ccf9fSEvan Bacon 201e32ccf9fSEvan Bacon/** Returns a warning message if an Android package name is potentially already in use. */ 202e32ccf9fSEvan Baconexport const getPackageNameWarningAsync = memoize(getPackageNameWarningInternalAsync); 203