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