1082815dcSEvan Baconimport path from 'path';
2082815dcSEvan Baconimport resolveFrom from 'resolve-from';
3082815dcSEvan Bacon
4*f0d67e12SMateus Craveiroimport { Resources } from '.';
58a424bebSJames Ideimport {
68a424bebSJames Ide  addMetaDataItemToMainApplication,
78a424bebSJames Ide  AndroidManifest,
88a424bebSJames Ide  findMetaDataItem,
98a424bebSJames Ide  getMainApplicationMetaDataValue,
108a424bebSJames Ide  getMainApplicationOrThrow,
118a424bebSJames Ide  removeMetaDataItemFromMainApplication,
128a424bebSJames Ide} from './Manifest';
138a424bebSJames Ideimport { buildResourceItem, ResourceXML } from './Resources';
148a424bebSJames Ideimport { removeStringItem, setStringItem } from './Strings';
15*f0d67e12SMateus Craveiroimport { ConfigPlugin, ExportedConfigWithProps } from '../Plugin.types';
16166385e2SDouglas Lowderimport { createStringsXmlPlugin, withAndroidManifest } from '../plugins/android-plugins';
17166385e2SDouglas Lowderimport { withPlugins } from '../plugins/withPlugins';
18082815dcSEvan Baconimport {
19082815dcSEvan Bacon  ExpoConfigUpdates,
20082815dcSEvan Bacon  getExpoUpdatesPackageVersion,
21*f0d67e12SMateus Craveiro  getRuntimeVersionNullableAsync,
22082815dcSEvan Bacon  getSDKVersion,
23082815dcSEvan Bacon  getUpdatesCheckOnLaunch,
24082815dcSEvan Bacon  getUpdatesCodeSigningCertificate,
25082815dcSEvan Bacon  getUpdatesCodeSigningMetadataStringified,
2609bd1012SUmberto Ghio  getUpdatesRequestHeadersStringified,
27082815dcSEvan Bacon  getUpdatesEnabled,
28082815dcSEvan Bacon  getUpdatesTimeout,
29082815dcSEvan Bacon  getUpdateUrl,
30082815dcSEvan Bacon} from '../utils/Updates';
31082815dcSEvan Bacon
32082815dcSEvan Baconconst CREATE_MANIFEST_ANDROID_PATH = 'expo-updates/scripts/create-manifest-android.gradle';
33082815dcSEvan Bacon
34082815dcSEvan Baconexport enum Config {
35082815dcSEvan Bacon  ENABLED = 'expo.modules.updates.ENABLED',
36082815dcSEvan Bacon  CHECK_ON_LAUNCH = 'expo.modules.updates.EXPO_UPDATES_CHECK_ON_LAUNCH',
37082815dcSEvan Bacon  LAUNCH_WAIT_MS = 'expo.modules.updates.EXPO_UPDATES_LAUNCH_WAIT_MS',
38082815dcSEvan Bacon  SDK_VERSION = 'expo.modules.updates.EXPO_SDK_VERSION',
39082815dcSEvan Bacon  RUNTIME_VERSION = 'expo.modules.updates.EXPO_RUNTIME_VERSION',
40082815dcSEvan Bacon  UPDATE_URL = 'expo.modules.updates.EXPO_UPDATE_URL',
41082815dcSEvan Bacon  RELEASE_CHANNEL = 'expo.modules.updates.EXPO_RELEASE_CHANNEL',
42082815dcSEvan Bacon  UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY = 'expo.modules.updates.UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY',
43082815dcSEvan Bacon  CODE_SIGNING_CERTIFICATE = 'expo.modules.updates.CODE_SIGNING_CERTIFICATE',
44082815dcSEvan Bacon  CODE_SIGNING_METADATA = 'expo.modules.updates.CODE_SIGNING_METADATA',
45082815dcSEvan Bacon}
46082815dcSEvan Bacon
47ae9cd511SWill Schurman// when making changes to this config plugin, ensure the same changes are also made in eas-cli and build-tools
48da5c7fcdSQuinlan Jung// Also ensure the docs are up-to-date: https://docs.expo.dev/bare/installing-updates/
49ae9cd511SWill Schurman
5082ade864SWill Schurmanexport const withUpdates: ConfigPlugin = (config) => {
5182ade864SWill Schurman  return withPlugins(config, [withUpdatesManifest, withRuntimeVersionResource]);
52166385e2SDouglas Lowder};
53166385e2SDouglas Lowder
5482ade864SWill Schurmanconst withUpdatesManifest: ConfigPlugin = (config) => {
55*f0d67e12SMateus Craveiro  return withAndroidManifest(config, async (config) => {
56082815dcSEvan Bacon    const projectRoot = config.modRequest.projectRoot;
57082815dcSEvan Bacon    const expoUpdatesPackageVersion = getExpoUpdatesPackageVersion(projectRoot);
58*f0d67e12SMateus Craveiro    config.modResults = await setUpdatesConfigAsync(
59082815dcSEvan Bacon      projectRoot,
60082815dcSEvan Bacon      config,
61082815dcSEvan Bacon      config.modResults,
62082815dcSEvan Bacon      expoUpdatesPackageVersion
63082815dcSEvan Bacon    );
64082815dcSEvan Bacon    return config;
65082815dcSEvan Bacon  });
66082815dcSEvan Bacon};
67082815dcSEvan Bacon
68166385e2SDouglas Lowderconst withRuntimeVersionResource = createStringsXmlPlugin(
69*f0d67e12SMateus Craveiro  applyRuntimeVersionFromConfigAsync,
70166385e2SDouglas Lowder  'withRuntimeVersionResource'
71166385e2SDouglas Lowder);
72166385e2SDouglas Lowder
73*f0d67e12SMateus Craveiroexport async function applyRuntimeVersionFromConfigAsync(
74*f0d67e12SMateus Craveiro  config: ExportedConfigWithProps<Resources.ResourceXML>,
75166385e2SDouglas Lowder  stringsJSON: ResourceXML
76*f0d67e12SMateus Craveiro): Promise<ResourceXML> {
77*f0d67e12SMateus Craveiro  const projectRoot = config.modRequest.projectRoot;
78*f0d67e12SMateus Craveiro  const runtimeVersion = await getRuntimeVersionNullableAsync(projectRoot, config, 'android');
79166385e2SDouglas Lowder  if (runtimeVersion) {
80166385e2SDouglas Lowder    return setStringItem(
81166385e2SDouglas Lowder      [buildResourceItem({ name: 'expo_runtime_version', value: runtimeVersion })],
82166385e2SDouglas Lowder      stringsJSON
83166385e2SDouglas Lowder    );
84166385e2SDouglas Lowder  }
85166385e2SDouglas Lowder  return removeStringItem('expo_runtime_version', stringsJSON);
86166385e2SDouglas Lowder}
87166385e2SDouglas Lowder
88*f0d67e12SMateus Craveiroexport async function setUpdatesConfigAsync(
89082815dcSEvan Bacon  projectRoot: string,
90082815dcSEvan Bacon  config: ExpoConfigUpdates,
91082815dcSEvan Bacon  androidManifest: AndroidManifest,
92082815dcSEvan Bacon  expoUpdatesPackageVersion?: string | null
93*f0d67e12SMateus Craveiro): Promise<AndroidManifest> {
94082815dcSEvan Bacon  const mainApplication = getMainApplicationOrThrow(androidManifest);
95082815dcSEvan Bacon
96082815dcSEvan Bacon  addMetaDataItemToMainApplication(
97082815dcSEvan Bacon    mainApplication,
98082815dcSEvan Bacon    Config.ENABLED,
9982ade864SWill Schurman    String(getUpdatesEnabled(config))
100082815dcSEvan Bacon  );
101082815dcSEvan Bacon  addMetaDataItemToMainApplication(
102082815dcSEvan Bacon    mainApplication,
103082815dcSEvan Bacon    Config.CHECK_ON_LAUNCH,
104082815dcSEvan Bacon    getUpdatesCheckOnLaunch(config, expoUpdatesPackageVersion)
105082815dcSEvan Bacon  );
106082815dcSEvan Bacon  addMetaDataItemToMainApplication(
107082815dcSEvan Bacon    mainApplication,
108082815dcSEvan Bacon    Config.LAUNCH_WAIT_MS,
109082815dcSEvan Bacon    String(getUpdatesTimeout(config))
110082815dcSEvan Bacon  );
111082815dcSEvan Bacon
11282ade864SWill Schurman  const updateUrl = getUpdateUrl(config);
113082815dcSEvan Bacon  if (updateUrl) {
114082815dcSEvan Bacon    addMetaDataItemToMainApplication(mainApplication, Config.UPDATE_URL, updateUrl);
115082815dcSEvan Bacon  } else {
116082815dcSEvan Bacon    removeMetaDataItemFromMainApplication(mainApplication, Config.UPDATE_URL);
117082815dcSEvan Bacon  }
118082815dcSEvan Bacon
119082815dcSEvan Bacon  const codeSigningCertificate = getUpdatesCodeSigningCertificate(projectRoot, config);
120082815dcSEvan Bacon  if (codeSigningCertificate) {
121082815dcSEvan Bacon    addMetaDataItemToMainApplication(
122082815dcSEvan Bacon      mainApplication,
123082815dcSEvan Bacon      Config.CODE_SIGNING_CERTIFICATE,
124082815dcSEvan Bacon      codeSigningCertificate
125082815dcSEvan Bacon    );
126082815dcSEvan Bacon  } else {
127082815dcSEvan Bacon    removeMetaDataItemFromMainApplication(mainApplication, Config.CODE_SIGNING_CERTIFICATE);
128082815dcSEvan Bacon  }
129082815dcSEvan Bacon
130082815dcSEvan Bacon  const codeSigningMetadata = getUpdatesCodeSigningMetadataStringified(config);
131082815dcSEvan Bacon  if (codeSigningMetadata) {
132082815dcSEvan Bacon    addMetaDataItemToMainApplication(
133082815dcSEvan Bacon      mainApplication,
134082815dcSEvan Bacon      Config.CODE_SIGNING_METADATA,
135082815dcSEvan Bacon      codeSigningMetadata
136082815dcSEvan Bacon    );
137082815dcSEvan Bacon  } else {
138082815dcSEvan Bacon    removeMetaDataItemFromMainApplication(mainApplication, Config.CODE_SIGNING_METADATA);
139082815dcSEvan Bacon  }
140082815dcSEvan Bacon
14109bd1012SUmberto Ghio  const requestHeaders = getUpdatesRequestHeadersStringified(config);
14209bd1012SUmberto Ghio  if (requestHeaders) {
14309bd1012SUmberto Ghio    addMetaDataItemToMainApplication(
14409bd1012SUmberto Ghio      mainApplication,
14509bd1012SUmberto Ghio      Config.UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY,
14609bd1012SUmberto Ghio      requestHeaders
14709bd1012SUmberto Ghio    );
14809bd1012SUmberto Ghio  } else {
149fc285a6fSBartosz Kaszubowski    removeMetaDataItemFromMainApplication(
150fc285a6fSBartosz Kaszubowski      mainApplication,
151fc285a6fSBartosz Kaszubowski      Config.UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY
152fc285a6fSBartosz Kaszubowski    );
15309bd1012SUmberto Ghio  }
15409bd1012SUmberto Ghio
155*f0d67e12SMateus Craveiro  return await setVersionsConfigAsync(projectRoot, config, androidManifest);
156082815dcSEvan Bacon}
157082815dcSEvan Bacon
158*f0d67e12SMateus Craveiroexport async function setVersionsConfigAsync(
159*f0d67e12SMateus Craveiro  projectRoot: string,
160082815dcSEvan Bacon  config: Pick<ExpoConfigUpdates, 'sdkVersion' | 'runtimeVersion'>,
161082815dcSEvan Bacon  androidManifest: AndroidManifest
162*f0d67e12SMateus Craveiro): Promise<AndroidManifest> {
163082815dcSEvan Bacon  const mainApplication = getMainApplicationOrThrow(androidManifest);
164082815dcSEvan Bacon
165*f0d67e12SMateus Craveiro  const runtimeVersion = await getRuntimeVersionNullableAsync(projectRoot, config, 'android');
166082815dcSEvan Bacon  if (!runtimeVersion && findMetaDataItem(mainApplication, Config.RUNTIME_VERSION) > -1) {
167082815dcSEvan Bacon    throw new Error(
168082815dcSEvan Bacon      '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.'
169082815dcSEvan Bacon    );
170082815dcSEvan Bacon  }
171082815dcSEvan Bacon  const sdkVersion = getSDKVersion(config);
172082815dcSEvan Bacon  if (runtimeVersion) {
173082815dcSEvan Bacon    removeMetaDataItemFromMainApplication(mainApplication, Config.SDK_VERSION);
174166385e2SDouglas Lowder    addMetaDataItemToMainApplication(
175166385e2SDouglas Lowder      mainApplication,
176166385e2SDouglas Lowder      Config.RUNTIME_VERSION,
177166385e2SDouglas Lowder      '@string/expo_runtime_version'
178166385e2SDouglas Lowder    );
179082815dcSEvan Bacon  } else if (sdkVersion) {
180082815dcSEvan Bacon    /**
181082815dcSEvan Bacon     * runtime version maybe null in projects using classic updates. In that
182082815dcSEvan Bacon     * case we use SDK version
183082815dcSEvan Bacon     */
184082815dcSEvan Bacon    removeMetaDataItemFromMainApplication(mainApplication, Config.RUNTIME_VERSION);
185082815dcSEvan Bacon    addMetaDataItemToMainApplication(mainApplication, Config.SDK_VERSION, sdkVersion);
186082815dcSEvan Bacon  } else {
187082815dcSEvan Bacon    removeMetaDataItemFromMainApplication(mainApplication, Config.RUNTIME_VERSION);
188082815dcSEvan Bacon    removeMetaDataItemFromMainApplication(mainApplication, Config.SDK_VERSION);
189082815dcSEvan Bacon  }
190082815dcSEvan Bacon
191082815dcSEvan Bacon  return androidManifest;
192082815dcSEvan Bacon}
193082815dcSEvan Baconexport function ensureBuildGradleContainsConfigurationScript(
194082815dcSEvan Bacon  projectRoot: string,
195082815dcSEvan Bacon  buildGradleContents: string
196082815dcSEvan Bacon): string {
197082815dcSEvan Bacon  if (!isBuildGradleConfigured(projectRoot, buildGradleContents)) {
198082815dcSEvan Bacon    let cleanedUpBuildGradleContents;
199082815dcSEvan Bacon
200082815dcSEvan Bacon    const isBuildGradleMisconfigured = buildGradleContents
201082815dcSEvan Bacon      .split('\n')
202082815dcSEvan Bacon      .some((line) => line.includes(CREATE_MANIFEST_ANDROID_PATH));
203082815dcSEvan Bacon    if (isBuildGradleMisconfigured) {
204082815dcSEvan Bacon      cleanedUpBuildGradleContents = buildGradleContents.replace(
205082815dcSEvan Bacon        new RegExp(`(\n// Integration with Expo updates)?\n.*${CREATE_MANIFEST_ANDROID_PATH}.*\n`),
206082815dcSEvan Bacon        ''
207082815dcSEvan Bacon      );
208082815dcSEvan Bacon    } else {
209082815dcSEvan Bacon      cleanedUpBuildGradleContents = buildGradleContents;
210082815dcSEvan Bacon    }
211082815dcSEvan Bacon
212082815dcSEvan Bacon    const gradleScriptApply = formatApplyLineForBuildGradle(projectRoot);
213082815dcSEvan Bacon    return `${cleanedUpBuildGradleContents}\n// Integration with Expo updates\n${gradleScriptApply}\n`;
214082815dcSEvan Bacon  } else {
215082815dcSEvan Bacon    return buildGradleContents;
216082815dcSEvan Bacon  }
217082815dcSEvan Bacon}
218082815dcSEvan Bacon
219082815dcSEvan Baconexport function formatApplyLineForBuildGradle(projectRoot: string): string {
220082815dcSEvan Bacon  const updatesGradleScriptPath = resolveFrom.silent(projectRoot, CREATE_MANIFEST_ANDROID_PATH);
221082815dcSEvan Bacon
222082815dcSEvan Bacon  if (!updatesGradleScriptPath) {
223082815dcSEvan Bacon    throw new Error(
224082815dcSEvan Bacon      "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."
225082815dcSEvan Bacon    );
226082815dcSEvan Bacon  }
227082815dcSEvan Bacon
228082815dcSEvan Bacon  const relativePath = path.relative(
229082815dcSEvan Bacon    path.join(projectRoot, 'android', 'app'),
230082815dcSEvan Bacon    updatesGradleScriptPath
231082815dcSEvan Bacon  );
232082815dcSEvan Bacon  const posixPath = process.platform === 'win32' ? relativePath.replace(/\\/g, '/') : relativePath;
233082815dcSEvan Bacon
234082815dcSEvan Bacon  return `apply from: "${posixPath}"`;
235082815dcSEvan Bacon}
236082815dcSEvan Bacon
237082815dcSEvan Baconexport function isBuildGradleConfigured(projectRoot: string, buildGradleContents: string): boolean {
238082815dcSEvan Bacon  const androidBuildScript = formatApplyLineForBuildGradle(projectRoot);
239082815dcSEvan Bacon
240082815dcSEvan Bacon  return (
241082815dcSEvan Bacon    buildGradleContents
242082815dcSEvan Bacon      .replace(/\r\n/g, '\n')
243082815dcSEvan Bacon      .split('\n')
244082815dcSEvan Bacon      // Check for both single and double quotes
245082815dcSEvan Bacon      .some((line) => line === androidBuildScript || line === androidBuildScript.replace(/"/g, "'"))
246082815dcSEvan Bacon  );
247082815dcSEvan Bacon}
248082815dcSEvan Bacon
249082815dcSEvan Baconexport function isMainApplicationMetaDataSet(androidManifest: AndroidManifest): boolean {
250082815dcSEvan Bacon  const updateUrl = getMainApplicationMetaDataValue(androidManifest, Config.UPDATE_URL);
251082815dcSEvan Bacon  const runtimeVersion = getMainApplicationMetaDataValue(androidManifest, Config.RUNTIME_VERSION);
252082815dcSEvan Bacon  const sdkVersion = getMainApplicationMetaDataValue(androidManifest, Config.SDK_VERSION);
253082815dcSEvan Bacon
254082815dcSEvan Bacon  return Boolean(updateUrl && (sdkVersion || runtimeVersion));
255082815dcSEvan Bacon}
256082815dcSEvan Bacon
257*f0d67e12SMateus Craveiroexport async function isMainApplicationMetaDataSyncedAsync(
258082815dcSEvan Bacon  projectRoot: string,
259082815dcSEvan Bacon  config: ExpoConfigUpdates,
26082ade864SWill Schurman  androidManifest: AndroidManifest
261*f0d67e12SMateus Craveiro): Promise<boolean> {
262082815dcSEvan Bacon  return (
26382ade864SWill Schurman    getUpdateUrl(config) === getMainApplicationMetaDataValue(androidManifest, Config.UPDATE_URL) &&
26482ade864SWill Schurman    String(getUpdatesEnabled(config)) ===
265082815dcSEvan Bacon      getMainApplicationMetaDataValue(androidManifest, Config.ENABLED) &&
266082815dcSEvan Bacon    String(getUpdatesTimeout(config)) ===
267082815dcSEvan Bacon      getMainApplicationMetaDataValue(androidManifest, Config.LAUNCH_WAIT_MS) &&
268082815dcSEvan Bacon    getUpdatesCheckOnLaunch(config) ===
269082815dcSEvan Bacon      getMainApplicationMetaDataValue(androidManifest, Config.CHECK_ON_LAUNCH) &&
270082815dcSEvan Bacon    getUpdatesCodeSigningCertificate(projectRoot, config) ===
271082815dcSEvan Bacon      getMainApplicationMetaDataValue(androidManifest, Config.CODE_SIGNING_CERTIFICATE) &&
272082815dcSEvan Bacon    getUpdatesCodeSigningMetadataStringified(config) ===
273082815dcSEvan Bacon      getMainApplicationMetaDataValue(androidManifest, Config.CODE_SIGNING_METADATA) &&
274*f0d67e12SMateus Craveiro    (await areVersionsSyncedAsync(projectRoot, config, androidManifest))
275082815dcSEvan Bacon  );
276082815dcSEvan Bacon}
277082815dcSEvan Bacon
278*f0d67e12SMateus Craveiroexport async function areVersionsSyncedAsync(
279*f0d67e12SMateus Craveiro  projectRoot: string,
280082815dcSEvan Bacon  config: Pick<ExpoConfigUpdates, 'runtimeVersion' | 'sdkVersion'>,
281082815dcSEvan Bacon  androidManifest: AndroidManifest
282*f0d67e12SMateus Craveiro): Promise<boolean> {
283*f0d67e12SMateus Craveiro  const expectedRuntimeVersion = await getRuntimeVersionNullableAsync(
284*f0d67e12SMateus Craveiro    projectRoot,
285*f0d67e12SMateus Craveiro    config,
286*f0d67e12SMateus Craveiro    'android'
287*f0d67e12SMateus Craveiro  );
288082815dcSEvan Bacon  const expectedSdkVersion = getSDKVersion(config);
289082815dcSEvan Bacon
290082815dcSEvan Bacon  const currentRuntimeVersion = getMainApplicationMetaDataValue(
291082815dcSEvan Bacon    androidManifest,
292082815dcSEvan Bacon    Config.RUNTIME_VERSION
293082815dcSEvan Bacon  );
294082815dcSEvan Bacon  const currentSdkVersion = getMainApplicationMetaDataValue(androidManifest, Config.SDK_VERSION);
295082815dcSEvan Bacon
296082815dcSEvan Bacon  if (expectedRuntimeVersion !== null) {
297082815dcSEvan Bacon    return currentRuntimeVersion === expectedRuntimeVersion && currentSdkVersion === null;
298082815dcSEvan Bacon  } else if (expectedSdkVersion !== null) {
299082815dcSEvan Bacon    return currentSdkVersion === expectedSdkVersion && currentRuntimeVersion === null;
300082815dcSEvan Bacon  } else {
301082815dcSEvan Bacon    return true;
302082815dcSEvan Bacon  }
303082815dcSEvan Bacon}
304