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