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