1082815dcSEvan Baconimport { Android, ExpoConfig, IOS } from '@expo/config-types';
2*f0d67e12SMateus Craveiroimport * as Fingerprint from '@expo/fingerprint';
3082815dcSEvan Baconimport { getRuntimeVersionForSDKVersion } from '@expo/sdk-runtime-versions';
4082815dcSEvan Baconimport fs from 'fs';
5082815dcSEvan Baconimport { boolish } from 'getenv';
6082815dcSEvan Baconimport path from 'path';
7082815dcSEvan Baconimport resolveFrom from 'resolve-from';
8082815dcSEvan Baconimport semver from 'semver';
9082815dcSEvan Bacon
10082815dcSEvan Baconimport { AndroidConfig, IOSConfig } from '..';
11082815dcSEvan Bacon
12082815dcSEvan Baconexport type ExpoConfigUpdates = Pick<
13082815dcSEvan Bacon  ExpoConfig,
14082815dcSEvan Bacon  'sdkVersion' | 'owner' | 'runtimeVersion' | 'updates' | 'slug'
15082815dcSEvan Bacon>;
16082815dcSEvan Bacon
17082815dcSEvan Baconexport function getExpoUpdatesPackageVersion(projectRoot: string): string | null {
18082815dcSEvan Bacon  const expoUpdatesPackageJsonPath = resolveFrom.silent(projectRoot, 'expo-updates/package.json');
19082815dcSEvan Bacon  if (!expoUpdatesPackageJsonPath || !fs.existsSync(expoUpdatesPackageJsonPath)) {
20082815dcSEvan Bacon    return null;
21082815dcSEvan Bacon  }
22082815dcSEvan Bacon  const packageJson = JSON.parse(fs.readFileSync(expoUpdatesPackageJsonPath, 'utf8'));
23082815dcSEvan Bacon  return packageJson.version;
24082815dcSEvan Bacon}
25082815dcSEvan Bacon
2682ade864SWill Schurmanexport function getUpdateUrl(config: Pick<ExpoConfigUpdates, 'updates'>): string | null {
2782ade864SWill Schurman  return config.updates?.url ?? null;
28082815dcSEvan Bacon}
29082815dcSEvan Bacon
30053187fcSWojciech Kozyraexport function getAppVersion(config: Pick<ExpoConfig, 'version'>): string {
31053187fcSWojciech Kozyra  return config.version ?? '1.0.0';
32053187fcSWojciech Kozyra}
33053187fcSWojciech Kozyra
34082815dcSEvan Baconexport function getNativeVersion(
35082815dcSEvan Bacon  config: Pick<ExpoConfig, 'version'> & {
36082815dcSEvan Bacon    android?: Pick<Android, 'versionCode'>;
37082815dcSEvan Bacon    ios?: Pick<IOS, 'buildNumber'>;
38082815dcSEvan Bacon  },
39082815dcSEvan Bacon  platform: 'android' | 'ios'
40082815dcSEvan Bacon): string {
41082815dcSEvan Bacon  const version = IOSConfig.Version.getVersion(config);
42082815dcSEvan Bacon  switch (platform) {
43082815dcSEvan Bacon    case 'ios': {
44082815dcSEvan Bacon      const buildNumber = IOSConfig.Version.getBuildNumber(config);
45082815dcSEvan Bacon      return `${version}(${buildNumber})`;
46082815dcSEvan Bacon    }
47082815dcSEvan Bacon    case 'android': {
48082815dcSEvan Bacon      const versionCode = AndroidConfig.Version.getVersionCode(config);
49082815dcSEvan Bacon      return `${version}(${versionCode})`;
50082815dcSEvan Bacon    }
51082815dcSEvan Bacon    default: {
52082815dcSEvan Bacon      throw new Error(
53082815dcSEvan Bacon        `"${platform}" is not a supported platform. Choose either "ios" or "android".`
54082815dcSEvan Bacon      );
55082815dcSEvan Bacon    }
56082815dcSEvan Bacon  }
57082815dcSEvan Bacon}
58082815dcSEvan Bacon
59*f0d67e12SMateus Craveiroexport async function getRuntimeVersionNullableAsync(
60*f0d67e12SMateus Craveiro  ...[projectRoot, config, platform]: Parameters<typeof getRuntimeVersionAsync>
61*f0d67e12SMateus Craveiro): Promise<string | null> {
62082815dcSEvan Bacon  try {
63*f0d67e12SMateus Craveiro    return await getRuntimeVersionAsync(projectRoot, config, platform);
64082815dcSEvan Bacon  } catch (e) {
65082815dcSEvan Bacon    if (boolish('EXPO_DEBUG', false)) {
66082815dcSEvan Bacon      console.log(e);
67082815dcSEvan Bacon    }
68082815dcSEvan Bacon    return null;
69082815dcSEvan Bacon  }
70082815dcSEvan Bacon}
71082815dcSEvan Bacon
72*f0d67e12SMateus Craveiroexport async function getRuntimeVersionAsync(
73*f0d67e12SMateus Craveiro  projectRoot: string,
74082815dcSEvan Bacon  config: Pick<ExpoConfig, 'version' | 'runtimeVersion' | 'sdkVersion'> & {
75082815dcSEvan Bacon    android?: Pick<Android, 'versionCode' | 'runtimeVersion'>;
76082815dcSEvan Bacon    ios?: Pick<IOS, 'buildNumber' | 'runtimeVersion'>;
77082815dcSEvan Bacon  },
78082815dcSEvan Bacon  platform: 'android' | 'ios'
79*f0d67e12SMateus Craveiro): Promise<string | null> {
80082815dcSEvan Bacon  const runtimeVersion = config[platform]?.runtimeVersion ?? config.runtimeVersion;
81082815dcSEvan Bacon  if (!runtimeVersion) {
82082815dcSEvan Bacon    return null;
83082815dcSEvan Bacon  }
84082815dcSEvan Bacon
85082815dcSEvan Bacon  if (typeof runtimeVersion === 'string') {
86082815dcSEvan Bacon    return runtimeVersion;
87053187fcSWojciech Kozyra  } else if (runtimeVersion.policy === 'appVersion') {
88053187fcSWojciech Kozyra    return getAppVersion(config);
89082815dcSEvan Bacon  } else if (runtimeVersion.policy === 'nativeVersion') {
90082815dcSEvan Bacon    return getNativeVersion(config, platform);
91082815dcSEvan Bacon  } else if (runtimeVersion.policy === 'sdkVersion') {
92082815dcSEvan Bacon    if (!config.sdkVersion) {
93082815dcSEvan Bacon      throw new Error("An SDK version must be defined when using the 'sdkVersion' runtime policy.");
94082815dcSEvan Bacon    }
95082815dcSEvan Bacon    return getRuntimeVersionForSDKVersion(config.sdkVersion);
96*f0d67e12SMateus Craveiro  } else if (runtimeVersion.policy === 'fingerprintExperimental') {
97*f0d67e12SMateus Craveiro    console.warn(
98*f0d67e12SMateus Craveiro      "Use of the experimental 'fingerprintExperimental' runtime policy may result in unexpected system behavior."
99*f0d67e12SMateus Craveiro    );
100*f0d67e12SMateus Craveiro    return await Fingerprint.createProjectHashAsync(projectRoot);
101082815dcSEvan Bacon  }
102082815dcSEvan Bacon
103082815dcSEvan Bacon  throw new Error(
104082815dcSEvan Bacon    `"${
105082815dcSEvan Bacon      typeof runtimeVersion === 'object' ? JSON.stringify(runtimeVersion) : runtimeVersion
106*f0d67e12SMateus Craveiro    }" is not a valid runtime version. getRuntimeVersionAsync only supports a string, "sdkVersion", "appVersion", "nativeVersion" or "fingerprintExperimental" policy.`
107082815dcSEvan Bacon  );
108082815dcSEvan Bacon}
109082815dcSEvan Bacon
110082815dcSEvan Baconexport function getSDKVersion(config: Pick<ExpoConfigUpdates, 'sdkVersion'>): string | null {
111082815dcSEvan Bacon  return typeof config.sdkVersion === 'string' ? config.sdkVersion : null;
112082815dcSEvan Bacon}
113082815dcSEvan Bacon
11482ade864SWill Schurmanexport function getUpdatesEnabled(config: Pick<ExpoConfigUpdates, 'updates'>): boolean {
1152fae8288SWill Schurman  // allow override of enabled property
1162fae8288SWill Schurman  if (config.updates?.enabled !== undefined) {
1172fae8288SWill Schurman    return config.updates.enabled;
1182fae8288SWill Schurman  }
1192fae8288SWill Schurman
12082ade864SWill Schurman  return getUpdateUrl(config) !== null;
121082815dcSEvan Bacon}
122082815dcSEvan Bacon
123082815dcSEvan Baconexport function getUpdatesTimeout(config: Pick<ExpoConfigUpdates, 'updates'>): number {
124082815dcSEvan Bacon  return config.updates?.fallbackToCacheTimeout ?? 0;
125082815dcSEvan Bacon}
126082815dcSEvan Bacon
127082815dcSEvan Baconexport function getUpdatesCheckOnLaunch(
128082815dcSEvan Bacon  config: Pick<ExpoConfigUpdates, 'updates'>,
129082815dcSEvan Bacon  expoUpdatesPackageVersion?: string | null
130b8b0e64dSDouglas Lowder): 'NEVER' | 'ERROR_RECOVERY_ONLY' | 'ALWAYS' | 'WIFI_ONLY' {
131082815dcSEvan Bacon  if (config.updates?.checkAutomatically === 'ON_ERROR_RECOVERY') {
132082815dcSEvan Bacon    // native 'ERROR_RECOVERY_ONLY' option was only introduced in 0.11.x
133082815dcSEvan Bacon    if (expoUpdatesPackageVersion && semver.gte(expoUpdatesPackageVersion, '0.11.0')) {
134082815dcSEvan Bacon      return 'ERROR_RECOVERY_ONLY';
135082815dcSEvan Bacon    }
136082815dcSEvan Bacon    return 'NEVER';
137082815dcSEvan Bacon  } else if (config.updates?.checkAutomatically === 'ON_LOAD') {
138082815dcSEvan Bacon    return 'ALWAYS';
139b8b0e64dSDouglas Lowder  } else if (config.updates?.checkAutomatically === 'WIFI_ONLY') {
140b8b0e64dSDouglas Lowder    return 'WIFI_ONLY';
141b8b0e64dSDouglas Lowder  } else if (config.updates?.checkAutomatically === 'NEVER') {
142b8b0e64dSDouglas Lowder    return 'NEVER';
143082815dcSEvan Bacon  }
144082815dcSEvan Bacon  return 'ALWAYS';
145082815dcSEvan Bacon}
146082815dcSEvan Bacon
147082815dcSEvan Baconexport function getUpdatesCodeSigningCertificate(
148082815dcSEvan Bacon  projectRoot: string,
149082815dcSEvan Bacon  config: Pick<ExpoConfigUpdates, 'updates'>
150082815dcSEvan Bacon): string | undefined {
151082815dcSEvan Bacon  const codeSigningCertificatePath = config.updates?.codeSigningCertificate;
152082815dcSEvan Bacon  if (!codeSigningCertificatePath) {
153082815dcSEvan Bacon    return undefined;
154082815dcSEvan Bacon  }
155082815dcSEvan Bacon
156082815dcSEvan Bacon  const finalPath = path.join(projectRoot, codeSigningCertificatePath);
157082815dcSEvan Bacon  if (!fs.existsSync(finalPath)) {
158082815dcSEvan Bacon    throw new Error(`File not found at \`updates.codeSigningCertificate\` path: ${finalPath}`);
159082815dcSEvan Bacon  }
160082815dcSEvan Bacon
161082815dcSEvan Bacon  return fs.readFileSync(finalPath, 'utf8');
162082815dcSEvan Bacon}
163082815dcSEvan Bacon
164082815dcSEvan Baconexport function getUpdatesCodeSigningMetadata(
165082815dcSEvan Bacon  config: Pick<ExpoConfigUpdates, 'updates'>
166082815dcSEvan Bacon): NonNullable<ExpoConfigUpdates['updates']>['codeSigningMetadata'] {
167082815dcSEvan Bacon  return config.updates?.codeSigningMetadata;
168082815dcSEvan Bacon}
169082815dcSEvan Bacon
170082815dcSEvan Baconexport function getUpdatesCodeSigningMetadataStringified(
171082815dcSEvan Bacon  config: Pick<ExpoConfigUpdates, 'updates'>
172082815dcSEvan Bacon): string | undefined {
173082815dcSEvan Bacon  const metadata = getUpdatesCodeSigningMetadata(config);
174082815dcSEvan Bacon  if (!metadata) {
175082815dcSEvan Bacon    return undefined;
176082815dcSEvan Bacon  }
177082815dcSEvan Bacon
178082815dcSEvan Bacon  return JSON.stringify(metadata);
179082815dcSEvan Bacon}
18009bd1012SUmberto Ghio
18109bd1012SUmberto Ghioexport function getUpdatesRequestHeaders(
18209bd1012SUmberto Ghio  config: Pick<ExpoConfigUpdates, 'updates'>
18309bd1012SUmberto Ghio): NonNullable<ExpoConfigUpdates['updates']>['requestHeaders'] {
18409bd1012SUmberto Ghio  return config.updates?.requestHeaders;
18509bd1012SUmberto Ghio}
18609bd1012SUmberto Ghio
18709bd1012SUmberto Ghioexport function getUpdatesRequestHeadersStringified(
18809bd1012SUmberto Ghio  config: Pick<ExpoConfigUpdates, 'updates'>
18909bd1012SUmberto Ghio): string | undefined {
19009bd1012SUmberto Ghio  const metadata = getUpdatesRequestHeaders(config);
19109bd1012SUmberto Ghio  if (!metadata) {
19209bd1012SUmberto Ghio    return undefined;
19309bd1012SUmberto Ghio  }
19409bd1012SUmberto Ghio
19509bd1012SUmberto Ghio  return JSON.stringify(metadata);
19609bd1012SUmberto Ghio}
197