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(mainApplication, Config.UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY);
153  }
154
155  return setVersionsConfig(config, androidManifest);
156}
157
158export function setVersionsConfig(
159  config: Pick<ExpoConfigUpdates, 'sdkVersion' | 'runtimeVersion'>,
160  androidManifest: AndroidManifest
161): AndroidManifest {
162  const mainApplication = getMainApplicationOrThrow(androidManifest);
163
164  const runtimeVersion = getRuntimeVersionNullable(config, 'android');
165  if (!runtimeVersion && findMetaDataItem(mainApplication, Config.RUNTIME_VERSION) > -1) {
166    throw new Error(
167      '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.'
168    );
169  }
170  const sdkVersion = getSDKVersion(config);
171  if (runtimeVersion) {
172    removeMetaDataItemFromMainApplication(mainApplication, Config.SDK_VERSION);
173    addMetaDataItemToMainApplication(
174      mainApplication,
175      Config.RUNTIME_VERSION,
176      '@string/expo_runtime_version'
177    );
178  } else if (sdkVersion) {
179    /**
180     * runtime version maybe null in projects using classic updates. In that
181     * case we use SDK version
182     */
183    removeMetaDataItemFromMainApplication(mainApplication, Config.RUNTIME_VERSION);
184    addMetaDataItemToMainApplication(mainApplication, Config.SDK_VERSION, sdkVersion);
185  } else {
186    removeMetaDataItemFromMainApplication(mainApplication, Config.RUNTIME_VERSION);
187    removeMetaDataItemFromMainApplication(mainApplication, Config.SDK_VERSION);
188  }
189
190  return androidManifest;
191}
192export function ensureBuildGradleContainsConfigurationScript(
193  projectRoot: string,
194  buildGradleContents: string
195): string {
196  if (!isBuildGradleConfigured(projectRoot, buildGradleContents)) {
197    let cleanedUpBuildGradleContents;
198
199    const isBuildGradleMisconfigured = buildGradleContents
200      .split('\n')
201      .some((line) => line.includes(CREATE_MANIFEST_ANDROID_PATH));
202    if (isBuildGradleMisconfigured) {
203      cleanedUpBuildGradleContents = buildGradleContents.replace(
204        new RegExp(`(\n// Integration with Expo updates)?\n.*${CREATE_MANIFEST_ANDROID_PATH}.*\n`),
205        ''
206      );
207    } else {
208      cleanedUpBuildGradleContents = buildGradleContents;
209    }
210
211    const gradleScriptApply = formatApplyLineForBuildGradle(projectRoot);
212    return `${cleanedUpBuildGradleContents}\n// Integration with Expo updates\n${gradleScriptApply}\n`;
213  } else {
214    return buildGradleContents;
215  }
216}
217
218export function formatApplyLineForBuildGradle(projectRoot: string): string {
219  const updatesGradleScriptPath = resolveFrom.silent(projectRoot, CREATE_MANIFEST_ANDROID_PATH);
220
221  if (!updatesGradleScriptPath) {
222    throw new Error(
223      "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."
224    );
225  }
226
227  const relativePath = path.relative(
228    path.join(projectRoot, 'android', 'app'),
229    updatesGradleScriptPath
230  );
231  const posixPath = process.platform === 'win32' ? relativePath.replace(/\\/g, '/') : relativePath;
232
233  return `apply from: "${posixPath}"`;
234}
235
236export function isBuildGradleConfigured(projectRoot: string, buildGradleContents: string): boolean {
237  const androidBuildScript = formatApplyLineForBuildGradle(projectRoot);
238
239  return (
240    buildGradleContents
241      .replace(/\r\n/g, '\n')
242      .split('\n')
243      // Check for both single and double quotes
244      .some((line) => line === androidBuildScript || line === androidBuildScript.replace(/"/g, "'"))
245  );
246}
247
248export function isMainApplicationMetaDataSet(androidManifest: AndroidManifest): boolean {
249  const updateUrl = getMainApplicationMetaDataValue(androidManifest, Config.UPDATE_URL);
250  const runtimeVersion = getMainApplicationMetaDataValue(androidManifest, Config.RUNTIME_VERSION);
251  const sdkVersion = getMainApplicationMetaDataValue(androidManifest, Config.SDK_VERSION);
252
253  return Boolean(updateUrl && (sdkVersion || runtimeVersion));
254}
255
256export function isMainApplicationMetaDataSynced(
257  projectRoot: string,
258  config: ExpoConfigUpdates,
259  androidManifest: AndroidManifest,
260  username: string | null
261): boolean {
262  return (
263    getUpdateUrl(config, username) ===
264      getMainApplicationMetaDataValue(androidManifest, Config.UPDATE_URL) &&
265    String(getUpdatesEnabled(config)) ===
266      getMainApplicationMetaDataValue(androidManifest, Config.ENABLED) &&
267    String(getUpdatesTimeout(config)) ===
268      getMainApplicationMetaDataValue(androidManifest, Config.LAUNCH_WAIT_MS) &&
269    getUpdatesCheckOnLaunch(config) ===
270      getMainApplicationMetaDataValue(androidManifest, Config.CHECK_ON_LAUNCH) &&
271    getUpdatesCodeSigningCertificate(projectRoot, config) ===
272      getMainApplicationMetaDataValue(androidManifest, Config.CODE_SIGNING_CERTIFICATE) &&
273    getUpdatesCodeSigningMetadataStringified(config) ===
274      getMainApplicationMetaDataValue(androidManifest, Config.CODE_SIGNING_METADATA) &&
275    areVersionsSynced(config, androidManifest)
276  );
277}
278
279export function areVersionsSynced(
280  config: Pick<ExpoConfigUpdates, 'runtimeVersion' | 'sdkVersion'>,
281  androidManifest: AndroidManifest
282): boolean {
283  const expectedRuntimeVersion = getRuntimeVersionNullable(config, 'android');
284  const expectedSdkVersion = getSDKVersion(config);
285
286  const currentRuntimeVersion = getMainApplicationMetaDataValue(
287    androidManifest,
288    Config.RUNTIME_VERSION
289  );
290  const currentSdkVersion = getMainApplicationMetaDataValue(androidManifest, Config.SDK_VERSION);
291
292  if (expectedRuntimeVersion !== null) {
293    return currentRuntimeVersion === expectedRuntimeVersion && currentSdkVersion === null;
294  } else if (expectedSdkVersion !== null) {
295    return currentSdkVersion === expectedSdkVersion && currentRuntimeVersion === null;
296  } else {
297    return true;
298  }
299}
300