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