1082815dcSEvan Baconimport * as path from 'path'; 2082815dcSEvan Baconimport resolveFrom from 'resolve-from'; 3082815dcSEvan Baconimport xcode from 'xcode'; 4082815dcSEvan Bacon 58a424bebSJames Ideimport { ExpoPlist } from './IosConfig.types'; 6082815dcSEvan Baconimport { ConfigPlugin } from '../Plugin.types'; 7082815dcSEvan Baconimport { withExpoPlist } from '../plugins/ios-plugins'; 8082815dcSEvan Baconimport { 9082815dcSEvan Bacon ExpoConfigUpdates, 10082815dcSEvan Bacon getExpoUpdatesPackageVersion, 11*f0d67e12SMateus Craveiro getRuntimeVersionNullableAsync, 12082815dcSEvan Bacon getSDKVersion, 13082815dcSEvan Bacon getUpdatesCheckOnLaunch, 14082815dcSEvan Bacon getUpdatesCodeSigningCertificate, 15082815dcSEvan Bacon getUpdatesCodeSigningMetadata, 1609bd1012SUmberto Ghio getUpdatesRequestHeaders, 17082815dcSEvan Bacon getUpdatesEnabled, 18082815dcSEvan Bacon getUpdatesTimeout, 19082815dcSEvan Bacon getUpdateUrl, 20082815dcSEvan Bacon} from '../utils/Updates'; 21082815dcSEvan Bacon 22082815dcSEvan Baconconst CREATE_MANIFEST_IOS_PATH = 'expo-updates/scripts/create-manifest-ios.sh'; 23082815dcSEvan Bacon 24082815dcSEvan Baconexport enum Config { 25082815dcSEvan Bacon ENABLED = 'EXUpdatesEnabled', 26082815dcSEvan Bacon CHECK_ON_LAUNCH = 'EXUpdatesCheckOnLaunch', 27082815dcSEvan Bacon LAUNCH_WAIT_MS = 'EXUpdatesLaunchWaitMs', 28082815dcSEvan Bacon RUNTIME_VERSION = 'EXUpdatesRuntimeVersion', 29082815dcSEvan Bacon SDK_VERSION = 'EXUpdatesSDKVersion', 30082815dcSEvan Bacon UPDATE_URL = 'EXUpdatesURL', 31082815dcSEvan Bacon RELEASE_CHANNEL = 'EXUpdatesReleaseChannel', 32082815dcSEvan Bacon UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY = 'EXUpdatesRequestHeaders', 33082815dcSEvan Bacon CODE_SIGNING_CERTIFICATE = 'EXUpdatesCodeSigningCertificate', 34082815dcSEvan Bacon CODE_SIGNING_METADATA = 'EXUpdatesCodeSigningMetadata', 35082815dcSEvan Bacon} 36082815dcSEvan Bacon 37ae9cd511SWill Schurman// when making changes to this config plugin, ensure the same changes are also made in eas-cli and build-tools 38da5c7fcdSQuinlan Jung// Also ensure the docs are up-to-date: https://docs.expo.dev/bare/installing-updates/ 39ae9cd511SWill Schurman 4082ade864SWill Schurmanexport const withUpdates: ConfigPlugin = (config) => { 41*f0d67e12SMateus Craveiro return withExpoPlist(config, async (config) => { 42082815dcSEvan Bacon const projectRoot = config.modRequest.projectRoot; 43082815dcSEvan Bacon const expoUpdatesPackageVersion = getExpoUpdatesPackageVersion(projectRoot); 44*f0d67e12SMateus Craveiro config.modResults = await setUpdatesConfigAsync( 45082815dcSEvan Bacon projectRoot, 46082815dcSEvan Bacon config, 47082815dcSEvan Bacon config.modResults, 48082815dcSEvan Bacon expoUpdatesPackageVersion 49082815dcSEvan Bacon ); 50082815dcSEvan Bacon return config; 51082815dcSEvan Bacon }); 52082815dcSEvan Bacon}; 53082815dcSEvan Bacon 54*f0d67e12SMateus Craveiroexport async function setUpdatesConfigAsync( 55082815dcSEvan Bacon projectRoot: string, 56082815dcSEvan Bacon config: ExpoConfigUpdates, 57082815dcSEvan Bacon expoPlist: ExpoPlist, 58082815dcSEvan Bacon expoUpdatesPackageVersion?: string | null 59*f0d67e12SMateus Craveiro): Promise<ExpoPlist> { 60082815dcSEvan Bacon const newExpoPlist = { 61082815dcSEvan Bacon ...expoPlist, 6282ade864SWill Schurman [Config.ENABLED]: getUpdatesEnabled(config), 63082815dcSEvan Bacon [Config.CHECK_ON_LAUNCH]: getUpdatesCheckOnLaunch(config, expoUpdatesPackageVersion), 64082815dcSEvan Bacon [Config.LAUNCH_WAIT_MS]: getUpdatesTimeout(config), 65082815dcSEvan Bacon }; 66082815dcSEvan Bacon 6782ade864SWill Schurman const updateUrl = getUpdateUrl(config); 68082815dcSEvan Bacon if (updateUrl) { 69082815dcSEvan Bacon newExpoPlist[Config.UPDATE_URL] = updateUrl; 70082815dcSEvan Bacon } else { 71082815dcSEvan Bacon delete newExpoPlist[Config.UPDATE_URL]; 72082815dcSEvan Bacon } 73082815dcSEvan Bacon 74082815dcSEvan Bacon const codeSigningCertificate = getUpdatesCodeSigningCertificate(projectRoot, config); 75082815dcSEvan Bacon if (codeSigningCertificate) { 76082815dcSEvan Bacon newExpoPlist[Config.CODE_SIGNING_CERTIFICATE] = codeSigningCertificate; 77082815dcSEvan Bacon } else { 78082815dcSEvan Bacon delete newExpoPlist[Config.CODE_SIGNING_CERTIFICATE]; 79082815dcSEvan Bacon } 80082815dcSEvan Bacon 81082815dcSEvan Bacon const codeSigningMetadata = getUpdatesCodeSigningMetadata(config); 82082815dcSEvan Bacon if (codeSigningMetadata) { 83082815dcSEvan Bacon newExpoPlist[Config.CODE_SIGNING_METADATA] = codeSigningMetadata; 84082815dcSEvan Bacon } else { 85082815dcSEvan Bacon delete newExpoPlist[Config.CODE_SIGNING_METADATA]; 86082815dcSEvan Bacon } 87082815dcSEvan Bacon 8809bd1012SUmberto Ghio const requestHeaders = getUpdatesRequestHeaders(config); 8909bd1012SUmberto Ghio if (requestHeaders) { 9009bd1012SUmberto Ghio newExpoPlist[Config.UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY] = requestHeaders; 9109bd1012SUmberto Ghio } else { 9209bd1012SUmberto Ghio delete newExpoPlist[Config.UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY]; 9309bd1012SUmberto Ghio } 9409bd1012SUmberto Ghio 95*f0d67e12SMateus Craveiro return await setVersionsConfigAsync(projectRoot, config, newExpoPlist); 96082815dcSEvan Bacon} 97082815dcSEvan Bacon 98*f0d67e12SMateus Craveiroexport async function setVersionsConfigAsync( 99*f0d67e12SMateus Craveiro projectRoot: string, 100*f0d67e12SMateus Craveiro config: ExpoConfigUpdates, 101*f0d67e12SMateus Craveiro expoPlist: ExpoPlist 102*f0d67e12SMateus Craveiro): Promise<ExpoPlist> { 103082815dcSEvan Bacon const newExpoPlist = { ...expoPlist }; 104082815dcSEvan Bacon 105*f0d67e12SMateus Craveiro const runtimeVersion = await getRuntimeVersionNullableAsync(projectRoot, config, 'ios'); 106082815dcSEvan Bacon if (!runtimeVersion && expoPlist[Config.RUNTIME_VERSION]) { 107082815dcSEvan Bacon throw new Error( 108082815dcSEvan Bacon 'A runtime version is set in your Expo.plist, but is missing from your app.json/app.config.js. Please either set runtimeVersion in your app.json/app.config.js or remove EXUpdatesRuntimeVersion from your Expo.plist.' 109082815dcSEvan Bacon ); 110082815dcSEvan Bacon } 111082815dcSEvan Bacon const sdkVersion = getSDKVersion(config); 112082815dcSEvan Bacon if (runtimeVersion) { 113082815dcSEvan Bacon delete newExpoPlist[Config.SDK_VERSION]; 114082815dcSEvan Bacon newExpoPlist[Config.RUNTIME_VERSION] = runtimeVersion; 115082815dcSEvan Bacon } else if (sdkVersion) { 116082815dcSEvan Bacon /** 117082815dcSEvan Bacon * runtime version maybe null in projects using classic updates. In that 118082815dcSEvan Bacon * case we use SDK version 119082815dcSEvan Bacon */ 120082815dcSEvan Bacon delete newExpoPlist[Config.RUNTIME_VERSION]; 121082815dcSEvan Bacon newExpoPlist[Config.SDK_VERSION] = sdkVersion; 122082815dcSEvan Bacon } else { 123082815dcSEvan Bacon delete newExpoPlist[Config.SDK_VERSION]; 124082815dcSEvan Bacon delete newExpoPlist[Config.RUNTIME_VERSION]; 125082815dcSEvan Bacon } 126082815dcSEvan Bacon 127082815dcSEvan Bacon return newExpoPlist; 128082815dcSEvan Bacon} 129082815dcSEvan Bacon 130082815dcSEvan Baconfunction formatConfigurationScriptPath(projectRoot: string): string { 131082815dcSEvan Bacon const buildScriptPath = resolveFrom.silent(projectRoot, CREATE_MANIFEST_IOS_PATH); 132082815dcSEvan Bacon 133082815dcSEvan Bacon if (!buildScriptPath) { 134082815dcSEvan Bacon throw new Error( 135082815dcSEvan Bacon "Could not find the build script for iOS. This could happen in case of outdated 'node_modules'. Run 'npm install' to make sure that it's up-to-date." 136082815dcSEvan Bacon ); 137082815dcSEvan Bacon } 138082815dcSEvan Bacon 139082815dcSEvan Bacon const relativePath = path.relative(path.join(projectRoot, 'ios'), buildScriptPath); 140082815dcSEvan Bacon return process.platform === 'win32' ? relativePath.replace(/\\/g, '/') : relativePath; 141082815dcSEvan Bacon} 142082815dcSEvan Bacon 143082815dcSEvan Baconinterface ShellScriptBuildPhase { 144082815dcSEvan Bacon isa: 'PBXShellScriptBuildPhase'; 145082815dcSEvan Bacon name: string; 146082815dcSEvan Bacon shellScript: string; 147082815dcSEvan Bacon [key: string]: any; 148082815dcSEvan Bacon} 149082815dcSEvan Bacon 150082815dcSEvan Baconexport function getBundleReactNativePhase(project: xcode.XcodeProject): ShellScriptBuildPhase { 151082815dcSEvan Bacon const shellScriptBuildPhase = project.hash.project.objects.PBXShellScriptBuildPhase as Record< 152082815dcSEvan Bacon string, 153082815dcSEvan Bacon ShellScriptBuildPhase 154082815dcSEvan Bacon >; 155082815dcSEvan Bacon const bundleReactNative = Object.values(shellScriptBuildPhase).find( 156082815dcSEvan Bacon (buildPhase) => buildPhase.name === '"Bundle React Native code and images"' 157082815dcSEvan Bacon ); 158082815dcSEvan Bacon 159082815dcSEvan Bacon if (!bundleReactNative) { 160082815dcSEvan Bacon throw new Error(`Couldn't find a build phase "Bundle React Native code and images"`); 161082815dcSEvan Bacon } 162082815dcSEvan Bacon 163082815dcSEvan Bacon return bundleReactNative; 164082815dcSEvan Bacon} 165082815dcSEvan Bacon 166082815dcSEvan Baconexport function ensureBundleReactNativePhaseContainsConfigurationScript( 167082815dcSEvan Bacon projectRoot: string, 168082815dcSEvan Bacon project: xcode.XcodeProject 169082815dcSEvan Bacon): xcode.XcodeProject { 170082815dcSEvan Bacon const bundleReactNative = getBundleReactNativePhase(project); 171082815dcSEvan Bacon const buildPhaseShellScriptPath = formatConfigurationScriptPath(projectRoot); 172082815dcSEvan Bacon 173082815dcSEvan Bacon if (!isShellScriptBuildPhaseConfigured(projectRoot, project)) { 174082815dcSEvan Bacon // check if there's already another path to create-manifest-ios.sh 175082815dcSEvan Bacon // this might be the case for monorepos 176082815dcSEvan Bacon if (bundleReactNative.shellScript.includes(CREATE_MANIFEST_IOS_PATH)) { 177082815dcSEvan Bacon bundleReactNative.shellScript = bundleReactNative.shellScript.replace( 178082815dcSEvan Bacon new RegExp(`(\\\\n)(\\.\\.)+/node_modules/${CREATE_MANIFEST_IOS_PATH}`), 179082815dcSEvan Bacon '' 180082815dcSEvan Bacon ); 181082815dcSEvan Bacon } 182082815dcSEvan Bacon bundleReactNative.shellScript = `${bundleReactNative.shellScript.replace( 183082815dcSEvan Bacon /"$/, 184082815dcSEvan Bacon '' 185082815dcSEvan Bacon )}${buildPhaseShellScriptPath}\\n"`; 186082815dcSEvan Bacon } 187082815dcSEvan Bacon return project; 188082815dcSEvan Bacon} 189082815dcSEvan Bacon 190082815dcSEvan Baconexport function isShellScriptBuildPhaseConfigured( 191082815dcSEvan Bacon projectRoot: string, 192082815dcSEvan Bacon project: xcode.XcodeProject 193082815dcSEvan Bacon): boolean { 194082815dcSEvan Bacon const bundleReactNative = getBundleReactNativePhase(project); 195082815dcSEvan Bacon const buildPhaseShellScriptPath = formatConfigurationScriptPath(projectRoot); 196082815dcSEvan Bacon return bundleReactNative.shellScript.includes(buildPhaseShellScriptPath); 197082815dcSEvan Bacon} 198082815dcSEvan Bacon 199082815dcSEvan Baconexport function isPlistConfigurationSet(expoPlist: ExpoPlist): boolean { 200082815dcSEvan Bacon return Boolean( 201082815dcSEvan Bacon expoPlist.EXUpdatesURL && (expoPlist.EXUpdatesSDKVersion || expoPlist.EXUpdatesRuntimeVersion) 202082815dcSEvan Bacon ); 203082815dcSEvan Bacon} 204082815dcSEvan Bacon 205*f0d67e12SMateus Craveiroexport async function isPlistConfigurationSyncedAsync( 206082815dcSEvan Bacon projectRoot: string, 207082815dcSEvan Bacon config: ExpoConfigUpdates, 20882ade864SWill Schurman expoPlist: ExpoPlist 209*f0d67e12SMateus Craveiro): Promise<boolean> { 210082815dcSEvan Bacon return ( 21182ade864SWill Schurman getUpdateUrl(config) === expoPlist.EXUpdatesURL && 21282ade864SWill Schurman getUpdatesEnabled(config) === expoPlist.EXUpdatesEnabled && 213082815dcSEvan Bacon getUpdatesTimeout(config) === expoPlist.EXUpdatesLaunchWaitMs && 214082815dcSEvan Bacon getUpdatesCheckOnLaunch(config) === expoPlist.EXUpdatesCheckOnLaunch && 215082815dcSEvan Bacon getUpdatesCodeSigningCertificate(projectRoot, config) === 216082815dcSEvan Bacon expoPlist.EXUpdatesCodeSigningCertificate && 217082815dcSEvan Bacon getUpdatesCodeSigningMetadata(config) === expoPlist.EXUpdatesCodeSigningMetadata && 218*f0d67e12SMateus Craveiro (await isPlistVersionConfigurationSyncedAsync(projectRoot, config, expoPlist)) 219082815dcSEvan Bacon ); 220082815dcSEvan Bacon} 221082815dcSEvan Bacon 222*f0d67e12SMateus Craveiroexport async function isPlistVersionConfigurationSyncedAsync( 223*f0d67e12SMateus Craveiro projectRoot: string, 224082815dcSEvan Bacon config: Pick<ExpoConfigUpdates, 'sdkVersion' | 'runtimeVersion'>, 225082815dcSEvan Bacon expoPlist: ExpoPlist 226*f0d67e12SMateus Craveiro): Promise<boolean> { 227*f0d67e12SMateus Craveiro const expectedRuntimeVersion = await getRuntimeVersionNullableAsync(projectRoot, config, 'ios'); 228082815dcSEvan Bacon const expectedSdkVersion = getSDKVersion(config); 229082815dcSEvan Bacon 230082815dcSEvan Bacon const currentRuntimeVersion = expoPlist.EXUpdatesRuntimeVersion ?? null; 231082815dcSEvan Bacon const currentSdkVersion = expoPlist.EXUpdatesSDKVersion ?? null; 232082815dcSEvan Bacon 233082815dcSEvan Bacon if (expectedRuntimeVersion !== null) { 234082815dcSEvan Bacon return currentRuntimeVersion === expectedRuntimeVersion && currentSdkVersion === null; 235082815dcSEvan Bacon } else if (expectedSdkVersion !== null) { 236082815dcSEvan Bacon return currentSdkVersion === expectedSdkVersion && currentRuntimeVersion === null; 237082815dcSEvan Bacon } else { 238082815dcSEvan Bacon return true; 239082815dcSEvan Bacon } 240082815dcSEvan Bacon} 241