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