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