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