1import { Android, ExpoConfig, IOS } from '@expo/config-types';
2import * as Fingerprint from '@expo/fingerprint';
3import { getRuntimeVersionForSDKVersion } from '@expo/sdk-runtime-versions';
4import fs from 'fs';
5import { boolish } from 'getenv';
6import path from 'path';
7import resolveFrom from 'resolve-from';
8import semver from 'semver';
9
10import { AndroidConfig, IOSConfig } from '..';
11
12export type ExpoConfigUpdates = Pick<
13  ExpoConfig,
14  'sdkVersion' | 'owner' | 'runtimeVersion' | 'updates' | 'slug'
15>;
16
17export function getExpoUpdatesPackageVersion(projectRoot: string): string | null {
18  const expoUpdatesPackageJsonPath = resolveFrom.silent(projectRoot, 'expo-updates/package.json');
19  if (!expoUpdatesPackageJsonPath || !fs.existsSync(expoUpdatesPackageJsonPath)) {
20    return null;
21  }
22  const packageJson = JSON.parse(fs.readFileSync(expoUpdatesPackageJsonPath, 'utf8'));
23  return packageJson.version;
24}
25
26export function getUpdateUrl(config: Pick<ExpoConfigUpdates, 'updates'>): string | null {
27  return config.updates?.url ?? null;
28}
29
30export function getAppVersion(config: Pick<ExpoConfig, 'version'>): string {
31  return config.version ?? '1.0.0';
32}
33
34export function getNativeVersion(
35  config: Pick<ExpoConfig, 'version'> & {
36    android?: Pick<Android, 'versionCode'>;
37    ios?: Pick<IOS, 'buildNumber'>;
38  },
39  platform: 'android' | 'ios'
40): string {
41  const version = IOSConfig.Version.getVersion(config);
42  switch (platform) {
43    case 'ios': {
44      const buildNumber = IOSConfig.Version.getBuildNumber(config);
45      return `${version}(${buildNumber})`;
46    }
47    case 'android': {
48      const versionCode = AndroidConfig.Version.getVersionCode(config);
49      return `${version}(${versionCode})`;
50    }
51    default: {
52      throw new Error(
53        `"${platform}" is not a supported platform. Choose either "ios" or "android".`
54      );
55    }
56  }
57}
58
59export async function getRuntimeVersionNullableAsync(
60  ...[projectRoot, config, platform]: Parameters<typeof getRuntimeVersionAsync>
61): Promise<string | null> {
62  try {
63    return await getRuntimeVersionAsync(projectRoot, config, platform);
64  } catch (e) {
65    if (boolish('EXPO_DEBUG', false)) {
66      console.log(e);
67    }
68    return null;
69  }
70}
71
72export async function getRuntimeVersionAsync(
73  projectRoot: string,
74  config: Pick<ExpoConfig, 'version' | 'runtimeVersion' | 'sdkVersion'> & {
75    android?: Pick<Android, 'versionCode' | 'runtimeVersion'>;
76    ios?: Pick<IOS, 'buildNumber' | 'runtimeVersion'>;
77  },
78  platform: 'android' | 'ios'
79): Promise<string | null> {
80  const runtimeVersion = config[platform]?.runtimeVersion ?? config.runtimeVersion;
81  if (!runtimeVersion) {
82    return null;
83  }
84
85  if (typeof runtimeVersion === 'string') {
86    return runtimeVersion;
87  } else if (runtimeVersion.policy === 'appVersion') {
88    return getAppVersion(config);
89  } else if (runtimeVersion.policy === 'nativeVersion') {
90    return getNativeVersion(config, platform);
91  } else if (runtimeVersion.policy === 'sdkVersion') {
92    if (!config.sdkVersion) {
93      throw new Error("An SDK version must be defined when using the 'sdkVersion' runtime policy.");
94    }
95    return getRuntimeVersionForSDKVersion(config.sdkVersion);
96  } else if (runtimeVersion.policy === 'fingerprintExperimental') {
97    console.warn(
98      "Use of the experimental 'fingerprintExperimental' runtime policy may result in unexpected system behavior."
99    );
100    return await Fingerprint.createProjectHashAsync(projectRoot);
101  }
102
103  throw new Error(
104    `"${
105      typeof runtimeVersion === 'object' ? JSON.stringify(runtimeVersion) : runtimeVersion
106    }" is not a valid runtime version. getRuntimeVersionAsync only supports a string, "sdkVersion", "appVersion", "nativeVersion" or "fingerprintExperimental" policy.`
107  );
108}
109
110export function getSDKVersion(config: Pick<ExpoConfigUpdates, 'sdkVersion'>): string | null {
111  return typeof config.sdkVersion === 'string' ? config.sdkVersion : null;
112}
113
114export function getUpdatesEnabled(config: Pick<ExpoConfigUpdates, 'updates'>): boolean {
115  // allow override of enabled property
116  if (config.updates?.enabled !== undefined) {
117    return config.updates.enabled;
118  }
119
120  return getUpdateUrl(config) !== null;
121}
122
123export function getUpdatesTimeout(config: Pick<ExpoConfigUpdates, 'updates'>): number {
124  return config.updates?.fallbackToCacheTimeout ?? 0;
125}
126
127export function getUpdatesCheckOnLaunch(
128  config: Pick<ExpoConfigUpdates, 'updates'>,
129  expoUpdatesPackageVersion?: string | null
130): 'NEVER' | 'ERROR_RECOVERY_ONLY' | 'ALWAYS' | 'WIFI_ONLY' {
131  if (config.updates?.checkAutomatically === 'ON_ERROR_RECOVERY') {
132    // native 'ERROR_RECOVERY_ONLY' option was only introduced in 0.11.x
133    if (expoUpdatesPackageVersion && semver.gte(expoUpdatesPackageVersion, '0.11.0')) {
134      return 'ERROR_RECOVERY_ONLY';
135    }
136    return 'NEVER';
137  } else if (config.updates?.checkAutomatically === 'ON_LOAD') {
138    return 'ALWAYS';
139  } else if (config.updates?.checkAutomatically === 'WIFI_ONLY') {
140    return 'WIFI_ONLY';
141  } else if (config.updates?.checkAutomatically === 'NEVER') {
142    return 'NEVER';
143  }
144  return 'ALWAYS';
145}
146
147export function getUpdatesCodeSigningCertificate(
148  projectRoot: string,
149  config: Pick<ExpoConfigUpdates, 'updates'>
150): string | undefined {
151  const codeSigningCertificatePath = config.updates?.codeSigningCertificate;
152  if (!codeSigningCertificatePath) {
153    return undefined;
154  }
155
156  const finalPath = path.join(projectRoot, codeSigningCertificatePath);
157  if (!fs.existsSync(finalPath)) {
158    throw new Error(`File not found at \`updates.codeSigningCertificate\` path: ${finalPath}`);
159  }
160
161  return fs.readFileSync(finalPath, 'utf8');
162}
163
164export function getUpdatesCodeSigningMetadata(
165  config: Pick<ExpoConfigUpdates, 'updates'>
166): NonNullable<ExpoConfigUpdates['updates']>['codeSigningMetadata'] {
167  return config.updates?.codeSigningMetadata;
168}
169
170export function getUpdatesCodeSigningMetadataStringified(
171  config: Pick<ExpoConfigUpdates, 'updates'>
172): string | undefined {
173  const metadata = getUpdatesCodeSigningMetadata(config);
174  if (!metadata) {
175    return undefined;
176  }
177
178  return JSON.stringify(metadata);
179}
180
181export function getUpdatesRequestHeaders(
182  config: Pick<ExpoConfigUpdates, 'updates'>
183): NonNullable<ExpoConfigUpdates['updates']>['requestHeaders'] {
184  return config.updates?.requestHeaders;
185}
186
187export function getUpdatesRequestHeadersStringified(
188  config: Pick<ExpoConfigUpdates, 'updates'>
189): string | undefined {
190  const metadata = getUpdatesRequestHeaders(config);
191  if (!metadata) {
192    return undefined;
193  }
194
195  return JSON.stringify(metadata);
196}
197