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