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 = (config) => {
50  return withPlugins(config, [withUpdatesManifest, withRuntimeVersionResource]);
51};
52
53const withUpdatesManifest: ConfigPlugin = (config) => {
54  return withAndroidManifest(config, (config) => {
55    const projectRoot = config.modRequest.projectRoot;
56    const expoUpdatesPackageVersion = getExpoUpdatesPackageVersion(projectRoot);
57    config.modResults = setUpdatesConfig(
58      projectRoot,
59      config,
60      config.modResults,
61      expoUpdatesPackageVersion
62    );
63    return config;
64  });
65};
66
67const withRuntimeVersionResource = createStringsXmlPlugin(
68  applyRuntimeVersionFromConfig,
69  'withRuntimeVersionResource'
70);
71
72export function applyRuntimeVersionFromConfig(
73  config: Pick<ExpoConfigUpdates, 'sdkVersion' | 'runtimeVersion'>,
74  stringsJSON: ResourceXML
75): ResourceXML {
76  const runtimeVersion = getRuntimeVersionNullable(config, 'android');
77  if (runtimeVersion) {
78    return setStringItem(
79      [buildResourceItem({ name: 'expo_runtime_version', value: runtimeVersion })],
80      stringsJSON
81    );
82  }
83  return removeStringItem('expo_runtime_version', stringsJSON);
84}
85
86export function setUpdatesConfig(
87  projectRoot: string,
88  config: ExpoConfigUpdates,
89  androidManifest: AndroidManifest,
90  expoUpdatesPackageVersion?: string | null
91): AndroidManifest {
92  const mainApplication = getMainApplicationOrThrow(androidManifest);
93
94  addMetaDataItemToMainApplication(
95    mainApplication,
96    Config.ENABLED,
97    String(getUpdatesEnabled(config))
98  );
99  addMetaDataItemToMainApplication(
100    mainApplication,
101    Config.CHECK_ON_LAUNCH,
102    getUpdatesCheckOnLaunch(config, expoUpdatesPackageVersion)
103  );
104  addMetaDataItemToMainApplication(
105    mainApplication,
106    Config.LAUNCH_WAIT_MS,
107    String(getUpdatesTimeout(config))
108  );
109
110  const updateUrl = getUpdateUrl(config);
111  if (updateUrl) {
112    addMetaDataItemToMainApplication(mainApplication, Config.UPDATE_URL, updateUrl);
113  } else {
114    removeMetaDataItemFromMainApplication(mainApplication, Config.UPDATE_URL);
115  }
116
117  const codeSigningCertificate = getUpdatesCodeSigningCertificate(projectRoot, config);
118  if (codeSigningCertificate) {
119    addMetaDataItemToMainApplication(
120      mainApplication,
121      Config.CODE_SIGNING_CERTIFICATE,
122      codeSigningCertificate
123    );
124  } else {
125    removeMetaDataItemFromMainApplication(mainApplication, Config.CODE_SIGNING_CERTIFICATE);
126  }
127
128  const codeSigningMetadata = getUpdatesCodeSigningMetadataStringified(config);
129  if (codeSigningMetadata) {
130    addMetaDataItemToMainApplication(
131      mainApplication,
132      Config.CODE_SIGNING_METADATA,
133      codeSigningMetadata
134    );
135  } else {
136    removeMetaDataItemFromMainApplication(mainApplication, Config.CODE_SIGNING_METADATA);
137  }
138
139  const requestHeaders = getUpdatesRequestHeadersStringified(config);
140  if (requestHeaders) {
141    addMetaDataItemToMainApplication(
142      mainApplication,
143      Config.UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY,
144      requestHeaders
145    );
146  } else {
147    removeMetaDataItemFromMainApplication(
148      mainApplication,
149      Config.UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY
150    );
151  }
152
153  return setVersionsConfig(config, androidManifest);
154}
155
156export function setVersionsConfig(
157  config: Pick<ExpoConfigUpdates, 'sdkVersion' | 'runtimeVersion'>,
158  androidManifest: AndroidManifest
159): AndroidManifest {
160  const mainApplication = getMainApplicationOrThrow(androidManifest);
161
162  const runtimeVersion = getRuntimeVersionNullable(config, 'android');
163  if (!runtimeVersion && findMetaDataItem(mainApplication, Config.RUNTIME_VERSION) > -1) {
164    throw new Error(
165      '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.'
166    );
167  }
168  const sdkVersion = getSDKVersion(config);
169  if (runtimeVersion) {
170    removeMetaDataItemFromMainApplication(mainApplication, Config.SDK_VERSION);
171    addMetaDataItemToMainApplication(
172      mainApplication,
173      Config.RUNTIME_VERSION,
174      '@string/expo_runtime_version'
175    );
176  } else if (sdkVersion) {
177    /**
178     * runtime version maybe null in projects using classic updates. In that
179     * case we use SDK version
180     */
181    removeMetaDataItemFromMainApplication(mainApplication, Config.RUNTIME_VERSION);
182    addMetaDataItemToMainApplication(mainApplication, Config.SDK_VERSION, sdkVersion);
183  } else {
184    removeMetaDataItemFromMainApplication(mainApplication, Config.RUNTIME_VERSION);
185    removeMetaDataItemFromMainApplication(mainApplication, Config.SDK_VERSION);
186  }
187
188  return androidManifest;
189}
190export function ensureBuildGradleContainsConfigurationScript(
191  projectRoot: string,
192  buildGradleContents: string
193): string {
194  if (!isBuildGradleConfigured(projectRoot, buildGradleContents)) {
195    let cleanedUpBuildGradleContents;
196
197    const isBuildGradleMisconfigured = buildGradleContents
198      .split('\n')
199      .some((line) => line.includes(CREATE_MANIFEST_ANDROID_PATH));
200    if (isBuildGradleMisconfigured) {
201      cleanedUpBuildGradleContents = buildGradleContents.replace(
202        new RegExp(`(\n// Integration with Expo updates)?\n.*${CREATE_MANIFEST_ANDROID_PATH}.*\n`),
203        ''
204      );
205    } else {
206      cleanedUpBuildGradleContents = buildGradleContents;
207    }
208
209    const gradleScriptApply = formatApplyLineForBuildGradle(projectRoot);
210    return `${cleanedUpBuildGradleContents}\n// Integration with Expo updates\n${gradleScriptApply}\n`;
211  } else {
212    return buildGradleContents;
213  }
214}
215
216export function formatApplyLineForBuildGradle(projectRoot: string): string {
217  const updatesGradleScriptPath = resolveFrom.silent(projectRoot, CREATE_MANIFEST_ANDROID_PATH);
218
219  if (!updatesGradleScriptPath) {
220    throw new Error(
221      "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."
222    );
223  }
224
225  const relativePath = path.relative(
226    path.join(projectRoot, 'android', 'app'),
227    updatesGradleScriptPath
228  );
229  const posixPath = process.platform === 'win32' ? relativePath.replace(/\\/g, '/') : relativePath;
230
231  return `apply from: "${posixPath}"`;
232}
233
234export function isBuildGradleConfigured(projectRoot: string, buildGradleContents: string): boolean {
235  const androidBuildScript = formatApplyLineForBuildGradle(projectRoot);
236
237  return (
238    buildGradleContents
239      .replace(/\r\n/g, '\n')
240      .split('\n')
241      // Check for both single and double quotes
242      .some((line) => line === androidBuildScript || line === androidBuildScript.replace(/"/g, "'"))
243  );
244}
245
246export function isMainApplicationMetaDataSet(androidManifest: AndroidManifest): boolean {
247  const updateUrl = getMainApplicationMetaDataValue(androidManifest, Config.UPDATE_URL);
248  const runtimeVersion = getMainApplicationMetaDataValue(androidManifest, Config.RUNTIME_VERSION);
249  const sdkVersion = getMainApplicationMetaDataValue(androidManifest, Config.SDK_VERSION);
250
251  return Boolean(updateUrl && (sdkVersion || runtimeVersion));
252}
253
254export function isMainApplicationMetaDataSynced(
255  projectRoot: string,
256  config: ExpoConfigUpdates,
257  androidManifest: AndroidManifest
258): boolean {
259  return (
260    getUpdateUrl(config) === getMainApplicationMetaDataValue(androidManifest, Config.UPDATE_URL) &&
261    String(getUpdatesEnabled(config)) ===
262      getMainApplicationMetaDataValue(androidManifest, Config.ENABLED) &&
263    String(getUpdatesTimeout(config)) ===
264      getMainApplicationMetaDataValue(androidManifest, Config.LAUNCH_WAIT_MS) &&
265    getUpdatesCheckOnLaunch(config) ===
266      getMainApplicationMetaDataValue(androidManifest, Config.CHECK_ON_LAUNCH) &&
267    getUpdatesCodeSigningCertificate(projectRoot, config) ===
268      getMainApplicationMetaDataValue(androidManifest, Config.CODE_SIGNING_CERTIFICATE) &&
269    getUpdatesCodeSigningMetadataStringified(config) ===
270      getMainApplicationMetaDataValue(androidManifest, Config.CODE_SIGNING_METADATA) &&
271    areVersionsSynced(config, androidManifest)
272  );
273}
274
275export function areVersionsSynced(
276  config: Pick<ExpoConfigUpdates, 'runtimeVersion' | 'sdkVersion'>,
277  androidManifest: AndroidManifest
278): boolean {
279  const expectedRuntimeVersion = getRuntimeVersionNullable(config, 'android');
280  const expectedSdkVersion = getSDKVersion(config);
281
282  const currentRuntimeVersion = getMainApplicationMetaDataValue(
283    androidManifest,
284    Config.RUNTIME_VERSION
285  );
286  const currentSdkVersion = getMainApplicationMetaDataValue(androidManifest, Config.SDK_VERSION);
287
288  if (expectedRuntimeVersion !== null) {
289    return currentRuntimeVersion === expectedRuntimeVersion && currentSdkVersion === null;
290  } else if (expectedSdkVersion !== null) {
291    return currentSdkVersion === expectedSdkVersion && currentRuntimeVersion === null;
292  } else {
293    return true;
294  }
295}
296