1import assert from 'assert'; 2import chalk from 'chalk'; 3 4import { fetchAsync } from '../api/rest/client'; 5import { learnMore } from './link'; 6import { isUrlAvailableAsync } from './url'; 7 8const debug = require('debug')('expo:utils:validateApplicationId') as typeof console.log; 9 10const IOS_BUNDLE_ID_REGEX = /^[a-zA-Z0-9-.]+$/; 11const ANDROID_PACKAGE_REGEX = /^[a-zA-Z][a-zA-Z0-9_]*(\.[a-zA-Z][a-zA-Z0-9_]*)+$/; 12 13/** Validate an iOS bundle identifier. */ 14export function validateBundleId(value: string): boolean { 15 return IOS_BUNDLE_ID_REGEX.test(value); 16} 17 18/** Validate an Android package name. */ 19export function validatePackage(value: string): boolean { 20 return ANDROID_PACKAGE_REGEX.test(value); 21} 22 23export function assertValidBundleId(value: string) { 24 assert.match( 25 value, 26 IOS_BUNDLE_ID_REGEX, 27 `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.` 28 ); 29} 30 31export function assertValidPackage(value: string) { 32 assert.match( 33 value, 34 ANDROID_PACKAGE_REGEX, 35 `Invalid format of Android package name. Only alphanumeric characters, '.' and '_' are allowed, and each '.' must be followed by a letter.` 36 ); 37} 38 39const cachedBundleIdResults: Record<string, string> = {}; 40const cachedPackageNameResults: Record<string, string> = {}; 41 42/** Returns a warning message if an iOS bundle identifier is potentially already in use. */ 43export async function getBundleIdWarningAsync(bundleId: string): Promise<string | null> { 44 // Prevent fetching for the same ID multiple times. 45 if (cachedBundleIdResults[bundleId]) { 46 return cachedBundleIdResults[bundleId]; 47 } 48 49 if (!(await isUrlAvailableAsync('itunes.apple.com'))) { 50 debug( 51 `Couldn't connect to iTunes Store to check bundle ID ${bundleId}. itunes.apple.com may be down.` 52 ); 53 // If no network, simply skip the warnings since they'll just lead to more confusion. 54 return null; 55 } 56 57 const url = `http://itunes.apple.com/lookup?bundleId=${bundleId}`; 58 try { 59 debug(`Checking iOS bundle ID '${bundleId}' at: ${url}`); 60 const response = await fetchAsync(url); 61 const json = await response.json(); 62 if (json.resultCount > 0) { 63 const firstApp = json.results[0]; 64 const message = formatInUseWarning(firstApp.trackName, firstApp.sellerName, bundleId); 65 cachedBundleIdResults[bundleId] = message; 66 return message; 67 } 68 } catch (error: any) { 69 debug(`Error checking bundle ID ${bundleId}: ${error.message}`); 70 // Error fetching itunes data. 71 } 72 return null; 73} 74 75/** Returns a warning message if an Android package name is potentially already in use. */ 76export async function getPackageNameWarningAsync(packageName: string): Promise<string | null> { 77 // Prevent fetching for the same ID multiple times. 78 if (cachedPackageNameResults[packageName]) { 79 return cachedPackageNameResults[packageName]; 80 } 81 82 if (!(await isUrlAvailableAsync('play.google.com'))) { 83 debug( 84 `Couldn't connect to Play Store to check package name ${packageName}. play.google.com may be down.` 85 ); 86 // If no network, simply skip the warnings since they'll just lead to more confusion. 87 return null; 88 } 89 90 const url = `https://play.google.com/store/apps/details?id=${packageName}`; 91 try { 92 debug(`Checking Android package name '${packageName}' at: ${url}`); 93 const response = await fetchAsync(url); 94 // If the page exists, then warn the user. 95 if (response.status === 200) { 96 // There is no JSON API for the Play Store so we can't concisely 97 // locate the app name and developer to match the iOS warning. 98 const message = `⚠️ The package ${chalk.bold(packageName)} is already in use. ${chalk.dim( 99 learnMore(url) 100 )}`; 101 cachedPackageNameResults[packageName] = message; 102 return message; 103 } 104 } catch (error: any) { 105 debug(`Error checking package name ${packageName}: ${error.message}`); 106 // Error fetching play store data or the page doesn't exist. 107 } 108 return null; 109} 110 111function formatInUseWarning(appName: string, author: string, id: string): string { 112 return `⚠️ The app ${chalk.bold(appName)} by ${chalk.italic( 113 author 114 )} is already using ${chalk.bold(id)}`; 115} 116