1import path from 'path';
2import resolveFrom from 'resolve-from';
3
4import { Resources } from '.';
5import {
6  addMetaDataItemToMainApplication,
7  AndroidManifest,
8  findMetaDataItem,
9  getMainApplicationMetaDataValue,
10  getMainApplicationOrThrow,
11  removeMetaDataItemFromMainApplication,
12} from './Manifest';
13import { buildResourceItem, ResourceXML } from './Resources';
14import { removeStringItem, setStringItem } from './Strings';
15import { ConfigPlugin, ExportedConfigWithProps } from '../Plugin.types';
16import { createStringsXmlPlugin, withAndroidManifest } from '../plugins/android-plugins';
17import { withPlugins } from '../plugins/withPlugins';
18import {
19  ExpoConfigUpdates,
20  getExpoUpdatesPackageVersion,
21  getRuntimeVersionNullableAsync,
22  getSDKVersion,
23  getUpdatesCheckOnLaunch,
24  getUpdatesCodeSigningCertificate,
25  getUpdatesCodeSigningMetadataStringified,
26  getUpdatesRequestHeadersStringified,
27  getUpdatesEnabled,
28  getUpdatesTimeout,
29  getUpdateUrl,
30} from '../utils/Updates';
31
32const CREATE_MANIFEST_ANDROID_PATH = 'expo-updates/scripts/create-manifest-android.gradle';
33
34export enum Config {
35  ENABLED = 'expo.modules.updates.ENABLED',
36  CHECK_ON_LAUNCH = 'expo.modules.updates.EXPO_UPDATES_CHECK_ON_LAUNCH',
37  LAUNCH_WAIT_MS = 'expo.modules.updates.EXPO_UPDATES_LAUNCH_WAIT_MS',
38  SDK_VERSION = 'expo.modules.updates.EXPO_SDK_VERSION',
39  RUNTIME_VERSION = 'expo.modules.updates.EXPO_RUNTIME_VERSION',
40  UPDATE_URL = 'expo.modules.updates.EXPO_UPDATE_URL',
41  RELEASE_CHANNEL = 'expo.modules.updates.EXPO_RELEASE_CHANNEL',
42  UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY = 'expo.modules.updates.UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY',
43  CODE_SIGNING_CERTIFICATE = 'expo.modules.updates.CODE_SIGNING_CERTIFICATE',
44  CODE_SIGNING_METADATA = 'expo.modules.updates.CODE_SIGNING_METADATA',
45}
46
47// when making changes to this config plugin, ensure the same changes are also made in eas-cli and build-tools
48// Also ensure the docs are up-to-date: https://docs.expo.dev/bare/installing-updates/
49
50export const withUpdates: ConfigPlugin = (config) => {
51  return withPlugins(config, [withUpdatesManifest, withRuntimeVersionResource]);
52};
53
54const withUpdatesManifest: ConfigPlugin = (config) => {
55  return withAndroidManifest(config, async (config) => {
56    const projectRoot = config.modRequest.projectRoot;
57    const expoUpdatesPackageVersion = getExpoUpdatesPackageVersion(projectRoot);
58    config.modResults = await setUpdatesConfigAsync(
59      projectRoot,
60      config,
61      config.modResults,
62      expoUpdatesPackageVersion
63    );
64    return config;
65  });
66};
67
68const withRuntimeVersionResource = createStringsXmlPlugin(
69  applyRuntimeVersionFromConfigAsync,
70  'withRuntimeVersionResource'
71);
72
73export async function applyRuntimeVersionFromConfigAsync(
74  config: ExportedConfigWithProps<Resources.ResourceXML>,
75  stringsJSON: ResourceXML
76): Promise<ResourceXML> {
77  const projectRoot = config.modRequest.projectRoot;
78  const runtimeVersion = await getRuntimeVersionNullableAsync(projectRoot, config, 'android');
79  if (runtimeVersion) {
80    return setStringItem(
81      [buildResourceItem({ name: 'expo_runtime_version', value: runtimeVersion })],
82      stringsJSON
83    );
84  }
85  return removeStringItem('expo_runtime_version', stringsJSON);
86}
87
88export async function setUpdatesConfigAsync(
89  projectRoot: string,
90  config: ExpoConfigUpdates,
91  androidManifest: AndroidManifest,
92  expoUpdatesPackageVersion?: string | null
93): Promise<AndroidManifest> {
94  const mainApplication = getMainApplicationOrThrow(androidManifest);
95
96  addMetaDataItemToMainApplication(
97    mainApplication,
98    Config.ENABLED,
99    String(getUpdatesEnabled(config))
100  );
101  addMetaDataItemToMainApplication(
102    mainApplication,
103    Config.CHECK_ON_LAUNCH,
104    getUpdatesCheckOnLaunch(config, expoUpdatesPackageVersion)
105  );
106  addMetaDataItemToMainApplication(
107    mainApplication,
108    Config.LAUNCH_WAIT_MS,
109    String(getUpdatesTimeout(config))
110  );
111
112  const updateUrl = getUpdateUrl(config);
113  if (updateUrl) {
114    addMetaDataItemToMainApplication(mainApplication, Config.UPDATE_URL, updateUrl);
115  } else {
116    removeMetaDataItemFromMainApplication(mainApplication, Config.UPDATE_URL);
117  }
118
119  const codeSigningCertificate = getUpdatesCodeSigningCertificate(projectRoot, config);
120  if (codeSigningCertificate) {
121    addMetaDataItemToMainApplication(
122      mainApplication,
123      Config.CODE_SIGNING_CERTIFICATE,
124      codeSigningCertificate
125    );
126  } else {
127    removeMetaDataItemFromMainApplication(mainApplication, Config.CODE_SIGNING_CERTIFICATE);
128  }
129
130  const codeSigningMetadata = getUpdatesCodeSigningMetadataStringified(config);
131  if (codeSigningMetadata) {
132    addMetaDataItemToMainApplication(
133      mainApplication,
134      Config.CODE_SIGNING_METADATA,
135      codeSigningMetadata
136    );
137  } else {
138    removeMetaDataItemFromMainApplication(mainApplication, Config.CODE_SIGNING_METADATA);
139  }
140
141  const requestHeaders = getUpdatesRequestHeadersStringified(config);
142  if (requestHeaders) {
143    addMetaDataItemToMainApplication(
144      mainApplication,
145      Config.UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY,
146      requestHeaders
147    );
148  } else {
149    removeMetaDataItemFromMainApplication(
150      mainApplication,
151      Config.UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY
152    );
153  }
154
155  return await setVersionsConfigAsync(projectRoot, config, androidManifest);
156}
157
158export async function setVersionsConfigAsync(
159  projectRoot: string,
160  config: Pick<ExpoConfigUpdates, 'sdkVersion' | 'runtimeVersion'>,
161  androidManifest: AndroidManifest
162): Promise<AndroidManifest> {
163  const mainApplication = getMainApplicationOrThrow(androidManifest);
164
165  const runtimeVersion = await getRuntimeVersionNullableAsync(projectRoot, config, 'android');
166  if (!runtimeVersion && findMetaDataItem(mainApplication, Config.RUNTIME_VERSION) > -1) {
167    throw new Error(
168      'A runtime version is set in your AndroidManifest.xml, but is missing from your app.json/app.config.js. Please either set runtimeVersion in your app.json/app.config.js or remove expo.modules.updates.EXPO_RUNTIME_VERSION from your AndroidManifest.xml.'
169    );
170  }
171  const sdkVersion = getSDKVersion(config);
172  if (runtimeVersion) {
173    removeMetaDataItemFromMainApplication(mainApplication, Config.SDK_VERSION);
174    addMetaDataItemToMainApplication(
175      mainApplication,
176      Config.RUNTIME_VERSION,
177      '@string/expo_runtime_version'
178    );
179  } else if (sdkVersion) {
180    /**
181     * runtime version maybe null in projects using classic updates. In that
182     * case we use SDK version
183     */
184    removeMetaDataItemFromMainApplication(mainApplication, Config.RUNTIME_VERSION);
185    addMetaDataItemToMainApplication(mainApplication, Config.SDK_VERSION, sdkVersion);
186  } else {
187    removeMetaDataItemFromMainApplication(mainApplication, Config.RUNTIME_VERSION);
188    removeMetaDataItemFromMainApplication(mainApplication, Config.SDK_VERSION);
189  }
190
191  return androidManifest;
192}
193export function ensureBuildGradleContainsConfigurationScript(
194  projectRoot: string,
195  buildGradleContents: string
196): string {
197  if (!isBuildGradleConfigured(projectRoot, buildGradleContents)) {
198    let cleanedUpBuildGradleContents;
199
200    const isBuildGradleMisconfigured = buildGradleContents
201      .split('\n')
202      .some((line) => line.includes(CREATE_MANIFEST_ANDROID_PATH));
203    if (isBuildGradleMisconfigured) {
204      cleanedUpBuildGradleContents = buildGradleContents.replace(
205        new RegExp(`(\n// Integration with Expo updates)?\n.*${CREATE_MANIFEST_ANDROID_PATH}.*\n`),
206        ''
207      );
208    } else {
209      cleanedUpBuildGradleContents = buildGradleContents;
210    }
211
212    const gradleScriptApply = formatApplyLineForBuildGradle(projectRoot);
213    return `${cleanedUpBuildGradleContents}\n// Integration with Expo updates\n${gradleScriptApply}\n`;
214  } else {
215    return buildGradleContents;
216  }
217}
218
219export function formatApplyLineForBuildGradle(projectRoot: string): string {
220  const updatesGradleScriptPath = resolveFrom.silent(projectRoot, CREATE_MANIFEST_ANDROID_PATH);
221
222  if (!updatesGradleScriptPath) {
223    throw new Error(
224      "Could not find the build script for Android. This could happen in case of outdated 'node_modules'. Run 'npm install' to make sure that it's up-to-date."
225    );
226  }
227
228  const relativePath = path.relative(
229    path.join(projectRoot, 'android', 'app'),
230    updatesGradleScriptPath
231  );
232  const posixPath = process.platform === 'win32' ? relativePath.replace(/\\/g, '/') : relativePath;
233
234  return `apply from: "${posixPath}"`;
235}
236
237export function isBuildGradleConfigured(projectRoot: string, buildGradleContents: string): boolean {
238  const androidBuildScript = formatApplyLineForBuildGradle(projectRoot);
239
240  return (
241    buildGradleContents
242      .replace(/\r\n/g, '\n')
243      .split('\n')
244      // Check for both single and double quotes
245      .some((line) => line === androidBuildScript || line === androidBuildScript.replace(/"/g, "'"))
246  );
247}
248
249export function isMainApplicationMetaDataSet(androidManifest: AndroidManifest): boolean {
250  const updateUrl = getMainApplicationMetaDataValue(androidManifest, Config.UPDATE_URL);
251  const runtimeVersion = getMainApplicationMetaDataValue(androidManifest, Config.RUNTIME_VERSION);
252  const sdkVersion = getMainApplicationMetaDataValue(androidManifest, Config.SDK_VERSION);
253
254  return Boolean(updateUrl && (sdkVersion || runtimeVersion));
255}
256
257export async function isMainApplicationMetaDataSyncedAsync(
258  projectRoot: string,
259  config: ExpoConfigUpdates,
260  androidManifest: AndroidManifest
261): Promise<boolean> {
262  return (
263    getUpdateUrl(config) === getMainApplicationMetaDataValue(androidManifest, Config.UPDATE_URL) &&
264    String(getUpdatesEnabled(config)) ===
265      getMainApplicationMetaDataValue(androidManifest, Config.ENABLED) &&
266    String(getUpdatesTimeout(config)) ===
267      getMainApplicationMetaDataValue(androidManifest, Config.LAUNCH_WAIT_MS) &&
268    getUpdatesCheckOnLaunch(config) ===
269      getMainApplicationMetaDataValue(androidManifest, Config.CHECK_ON_LAUNCH) &&
270    getUpdatesCodeSigningCertificate(projectRoot, config) ===
271      getMainApplicationMetaDataValue(androidManifest, Config.CODE_SIGNING_CERTIFICATE) &&
272    getUpdatesCodeSigningMetadataStringified(config) ===
273      getMainApplicationMetaDataValue(androidManifest, Config.CODE_SIGNING_METADATA) &&
274    (await areVersionsSyncedAsync(projectRoot, config, androidManifest))
275  );
276}
277
278export async function areVersionsSyncedAsync(
279  projectRoot: string,
280  config: Pick<ExpoConfigUpdates, 'runtimeVersion' | 'sdkVersion'>,
281  androidManifest: AndroidManifest
282): Promise<boolean> {
283  const expectedRuntimeVersion = await getRuntimeVersionNullableAsync(
284    projectRoot,
285    config,
286    'android'
287  );
288  const expectedSdkVersion = getSDKVersion(config);
289
290  const currentRuntimeVersion = getMainApplicationMetaDataValue(
291    androidManifest,
292    Config.RUNTIME_VERSION
293  );
294  const currentSdkVersion = getMainApplicationMetaDataValue(androidManifest, Config.SDK_VERSION);
295
296  if (expectedRuntimeVersion !== null) {
297    return currentRuntimeVersion === expectedRuntimeVersion && currentSdkVersion === null;
298  } else if (expectedSdkVersion !== null) {
299    return currentSdkVersion === expectedSdkVersion && currentRuntimeVersion === null;
300  } else {
301    return true;
302  }
303}
304