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