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