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