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