1import assert from 'assert';
2import chalk from 'chalk';
3
4import { fetchAsync } from '../api/rest/client';
5import { Log } from '../log';
6import { env } from './env';
7import { memoize } from './fn';
8import { learnMore } from './link';
9import { isUrlAvailableAsync } from './url';
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