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