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