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