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