1082815dcSEvan Baconimport JsonFile, { JSONObject, JSONValue } from '@expo/json-file'; 2082815dcSEvan Baconimport plist from '@expo/plist'; 3082815dcSEvan Baconimport assert from 'assert'; 4082815dcSEvan Baconimport fs, { promises } from 'fs'; 5082815dcSEvan Baconimport path from 'path'; 6082815dcSEvan Baconimport xcode, { XcodeProject } from 'xcode'; 7082815dcSEvan Bacon 8*8a424bebSJames Ideimport { ForwardedBaseModOptions, provider, withGeneratedBaseMods } from './createBaseMod'; 9082815dcSEvan Baconimport { ExportedConfig, ModConfig } from '../Plugin.types'; 10082815dcSEvan Baconimport { Entitlements, Paths } from '../ios'; 11082815dcSEvan Baconimport { ensureApplicationTargetEntitlementsFileConfigured } from '../ios/Entitlements'; 12082815dcSEvan Baconimport { InfoPlist } from '../ios/IosConfig.types'; 13082815dcSEvan Baconimport { getPbxproj } from '../ios/utils/Xcodeproj'; 14082815dcSEvan Baconimport { getInfoPlistPathFromPbxproj } from '../ios/utils/getInfoPlistPath'; 15082815dcSEvan Baconimport { fileExists } from '../utils/modules'; 16082815dcSEvan Baconimport { sortObject } from '../utils/sortObject'; 17082815dcSEvan Baconimport { addWarningIOS } from '../utils/warnings'; 18082815dcSEvan Bacon 19082815dcSEvan Baconconst { readFile, writeFile } = promises; 20082815dcSEvan Bacon 21082815dcSEvan Bacontype IosModName = keyof Required<ModConfig>['ios']; 22082815dcSEvan Bacon 23082815dcSEvan Baconfunction getEntitlementsPlistTemplate() { 24082815dcSEvan Bacon // TODO: Fetch the versioned template file if possible 25082815dcSEvan Bacon return {}; 26082815dcSEvan Bacon} 27082815dcSEvan Bacon 28082815dcSEvan Baconfunction getInfoPlistTemplate() { 29082815dcSEvan Bacon // TODO: Fetch the versioned template file if possible 30082815dcSEvan Bacon return { 31082815dcSEvan Bacon CFBundleDevelopmentRegion: '$(DEVELOPMENT_LANGUAGE)', 32082815dcSEvan Bacon CFBundleExecutable: '$(EXECUTABLE_NAME)', 33082815dcSEvan Bacon CFBundleIdentifier: '$(PRODUCT_BUNDLE_IDENTIFIER)', 34082815dcSEvan Bacon CFBundleName: '$(PRODUCT_NAME)', 35082815dcSEvan Bacon CFBundlePackageType: '$(PRODUCT_BUNDLE_PACKAGE_TYPE)', 36082815dcSEvan Bacon CFBundleInfoDictionaryVersion: '6.0', 37082815dcSEvan Bacon CFBundleSignature: '????', 38082815dcSEvan Bacon LSRequiresIPhoneOS: true, 39082815dcSEvan Bacon NSAppTransportSecurity: { 40082815dcSEvan Bacon NSAllowsArbitraryLoads: true, 41082815dcSEvan Bacon NSExceptionDomains: { 42082815dcSEvan Bacon localhost: { 43082815dcSEvan Bacon NSExceptionAllowsInsecureHTTPLoads: true, 44082815dcSEvan Bacon }, 45082815dcSEvan Bacon }, 46082815dcSEvan Bacon }, 47082815dcSEvan Bacon UILaunchStoryboardName: 'SplashScreen', 48082815dcSEvan Bacon UIRequiredDeviceCapabilities: ['armv7'], 49082815dcSEvan Bacon UIViewControllerBasedStatusBarAppearance: false, 50082815dcSEvan Bacon UIStatusBarStyle: 'UIStatusBarStyleDefault', 51e62686cbSEvan Bacon CADisableMinimumFrameDurationOnPhone: true, 52082815dcSEvan Bacon }; 53082815dcSEvan Bacon} 54082815dcSEvan Bacon 55082815dcSEvan Baconconst defaultProviders = { 56082815dcSEvan Bacon dangerous: provider<unknown>({ 57082815dcSEvan Bacon getFilePath() { 58082815dcSEvan Bacon return ''; 59082815dcSEvan Bacon }, 60082815dcSEvan Bacon async read() { 61082815dcSEvan Bacon return {}; 62082815dcSEvan Bacon }, 63082815dcSEvan Bacon async write() {}, 64082815dcSEvan Bacon }), 65082815dcSEvan Bacon // Append a rule to supply AppDelegate data to mods on `mods.ios.appDelegate` 66082815dcSEvan Bacon appDelegate: provider<Paths.AppDelegateProjectFile>({ 67082815dcSEvan Bacon getFilePath({ modRequest: { projectRoot } }) { 68def098a4SEvan Bacon // TODO: Get application AppDelegate file from pbxproj. 69082815dcSEvan Bacon return Paths.getAppDelegateFilePath(projectRoot); 70082815dcSEvan Bacon }, 71082815dcSEvan Bacon async read(filePath) { 72082815dcSEvan Bacon return Paths.getFileInfo(filePath); 73082815dcSEvan Bacon }, 74082815dcSEvan Bacon async write(filePath: string, { modResults: { contents } }) { 75082815dcSEvan Bacon await writeFile(filePath, contents); 76082815dcSEvan Bacon }, 77082815dcSEvan Bacon }), 78082815dcSEvan Bacon // Append a rule to supply Expo.plist data to mods on `mods.ios.expoPlist` 79082815dcSEvan Bacon expoPlist: provider<JSONObject>({ 80082815dcSEvan Bacon isIntrospective: true, 81082815dcSEvan Bacon getFilePath({ modRequest: { platformProjectRoot, projectName } }) { 82082815dcSEvan Bacon const supportingDirectory = path.join(platformProjectRoot, projectName!, 'Supporting'); 83082815dcSEvan Bacon return path.resolve(supportingDirectory, 'Expo.plist'); 84082815dcSEvan Bacon }, 85082815dcSEvan Bacon async read(filePath, { modRequest: { introspect } }) { 86082815dcSEvan Bacon try { 87082815dcSEvan Bacon return plist.parse(await readFile(filePath, 'utf8')); 88082815dcSEvan Bacon } catch (error) { 89082815dcSEvan Bacon if (introspect) { 90082815dcSEvan Bacon return {}; 91082815dcSEvan Bacon } 92082815dcSEvan Bacon throw error; 93082815dcSEvan Bacon } 94082815dcSEvan Bacon }, 95082815dcSEvan Bacon async write(filePath, { modResults, modRequest: { introspect } }) { 96082815dcSEvan Bacon if (introspect) { 97082815dcSEvan Bacon return; 98082815dcSEvan Bacon } 99082815dcSEvan Bacon await writeFile(filePath, plist.build(sortObject(modResults))); 100082815dcSEvan Bacon }, 101082815dcSEvan Bacon }), 102082815dcSEvan Bacon // Append a rule to supply .xcodeproj data to mods on `mods.ios.xcodeproj` 103082815dcSEvan Bacon xcodeproj: provider<XcodeProject>({ 104082815dcSEvan Bacon getFilePath({ modRequest: { projectRoot } }) { 105082815dcSEvan Bacon return Paths.getPBXProjectPath(projectRoot); 106082815dcSEvan Bacon }, 107082815dcSEvan Bacon async read(filePath) { 108082815dcSEvan Bacon const project = xcode.project(filePath); 109082815dcSEvan Bacon project.parseSync(); 110082815dcSEvan Bacon return project; 111082815dcSEvan Bacon }, 112082815dcSEvan Bacon async write(filePath, { modResults }) { 113082815dcSEvan Bacon await writeFile(filePath, modResults.writeSync()); 114082815dcSEvan Bacon }, 115082815dcSEvan Bacon }), 116082815dcSEvan Bacon // Append a rule to supply Info.plist data to mods on `mods.ios.infoPlist` 117082815dcSEvan Bacon infoPlist: provider<InfoPlist, ForwardedBaseModOptions>({ 118082815dcSEvan Bacon isIntrospective: true, 119082815dcSEvan Bacon async getFilePath(config) { 120082815dcSEvan Bacon let project: xcode.XcodeProject | null = null; 121082815dcSEvan Bacon try { 122082815dcSEvan Bacon project = getPbxproj(config.modRequest.projectRoot); 123082815dcSEvan Bacon } catch { 124082815dcSEvan Bacon // noop 125082815dcSEvan Bacon } 126082815dcSEvan Bacon 127082815dcSEvan Bacon // Only check / warn if a project actually exists, this'll provide 128082815dcSEvan Bacon // more accurate warning messages for users in managed projects. 129082815dcSEvan Bacon if (project) { 130082815dcSEvan Bacon const infoPlistBuildProperty = getInfoPlistPathFromPbxproj(project); 131082815dcSEvan Bacon 132082815dcSEvan Bacon if (infoPlistBuildProperty) { 133082815dcSEvan Bacon //: [root]/myapp/ios/MyApp/Info.plist 134082815dcSEvan Bacon const infoPlistPath = path.join( 135082815dcSEvan Bacon //: myapp/ios 136082815dcSEvan Bacon config.modRequest.platformProjectRoot, 137082815dcSEvan Bacon //: MyApp/Info.plist 138082815dcSEvan Bacon infoPlistBuildProperty 139082815dcSEvan Bacon ); 140082815dcSEvan Bacon if (fileExists(infoPlistPath)) { 141082815dcSEvan Bacon return infoPlistPath; 142082815dcSEvan Bacon } 143082815dcSEvan Bacon addWarningIOS( 144082815dcSEvan Bacon 'mods.ios.infoPlist', 145082815dcSEvan Bacon `Info.plist file linked to Xcode project does not exist: ${infoPlistPath}` 146082815dcSEvan Bacon ); 147082815dcSEvan Bacon } else { 148082815dcSEvan Bacon addWarningIOS('mods.ios.infoPlist', 'Failed to find Info.plist linked to Xcode project.'); 149082815dcSEvan Bacon } 150082815dcSEvan Bacon } 151082815dcSEvan Bacon try { 152082815dcSEvan Bacon // Fallback on glob... 153082815dcSEvan Bacon return await Paths.getInfoPlistPath(config.modRequest.projectRoot); 154082815dcSEvan Bacon } catch (error: any) { 155082815dcSEvan Bacon if (config.modRequest.introspect) { 156082815dcSEvan Bacon // fallback to an empty string in introspection mode. 157082815dcSEvan Bacon return ''; 158082815dcSEvan Bacon } 159082815dcSEvan Bacon throw error; 160082815dcSEvan Bacon } 161082815dcSEvan Bacon }, 162082815dcSEvan Bacon async read(filePath, config) { 163082815dcSEvan Bacon // Apply all of the Info.plist values to the expo.ios.infoPlist object 164082815dcSEvan Bacon // TODO: Remove this in favor of just overwriting the Info.plist with the Expo object. This will enable people to actually remove values. 165082815dcSEvan Bacon if (!config.ios) config.ios = {}; 166082815dcSEvan Bacon if (!config.ios.infoPlist) config.ios.infoPlist = {}; 167082815dcSEvan Bacon 168082815dcSEvan Bacon let modResults: InfoPlist; 169082815dcSEvan Bacon try { 170082815dcSEvan Bacon const contents = await readFile(filePath, 'utf8'); 171082815dcSEvan Bacon assert(contents, 'Info.plist is empty'); 172082815dcSEvan Bacon modResults = plist.parse(contents) as InfoPlist; 173082815dcSEvan Bacon } catch (error: any) { 174082815dcSEvan Bacon // Throw errors in introspection mode. 175082815dcSEvan Bacon if (!config.modRequest.introspect) { 176082815dcSEvan Bacon throw error; 177082815dcSEvan Bacon } 178082815dcSEvan Bacon // Fallback to using the infoPlist object from the Expo config. 179082815dcSEvan Bacon modResults = getInfoPlistTemplate(); 180082815dcSEvan Bacon } 181082815dcSEvan Bacon 182082815dcSEvan Bacon config.ios.infoPlist = { 183082815dcSEvan Bacon ...(modResults || {}), 184082815dcSEvan Bacon ...config.ios.infoPlist, 185082815dcSEvan Bacon }; 186082815dcSEvan Bacon 187082815dcSEvan Bacon return config.ios.infoPlist!; 188082815dcSEvan Bacon }, 189082815dcSEvan Bacon async write(filePath, config) { 190082815dcSEvan Bacon // Update the contents of the static infoPlist object 191082815dcSEvan Bacon if (!config.ios) { 192082815dcSEvan Bacon config.ios = {}; 193082815dcSEvan Bacon } 194082815dcSEvan Bacon config.ios.infoPlist = config.modResults; 195082815dcSEvan Bacon 196082815dcSEvan Bacon // Return early without writing, in introspection mode. 197082815dcSEvan Bacon if (config.modRequest.introspect) { 198082815dcSEvan Bacon return; 199082815dcSEvan Bacon } 200082815dcSEvan Bacon 201082815dcSEvan Bacon await writeFile(filePath, plist.build(sortObject(config.modResults))); 202082815dcSEvan Bacon }, 203082815dcSEvan Bacon }), 204082815dcSEvan Bacon // Append a rule to supply .entitlements data to mods on `mods.ios.entitlements` 205082815dcSEvan Bacon entitlements: provider<JSONObject, ForwardedBaseModOptions>({ 206082815dcSEvan Bacon isIntrospective: true, 207082815dcSEvan Bacon 208082815dcSEvan Bacon async getFilePath(config) { 209082815dcSEvan Bacon try { 210082815dcSEvan Bacon ensureApplicationTargetEntitlementsFileConfigured(config.modRequest.projectRoot); 211082815dcSEvan Bacon return Entitlements.getEntitlementsPath(config.modRequest.projectRoot) ?? ''; 212082815dcSEvan Bacon } catch (error: any) { 213082815dcSEvan Bacon if (config.modRequest.introspect) { 214082815dcSEvan Bacon // fallback to an empty string in introspection mode. 215082815dcSEvan Bacon return ''; 216082815dcSEvan Bacon } 217082815dcSEvan Bacon throw error; 218082815dcSEvan Bacon } 219082815dcSEvan Bacon }, 220082815dcSEvan Bacon 221082815dcSEvan Bacon async read(filePath, config) { 222082815dcSEvan Bacon let modResults: JSONObject; 223082815dcSEvan Bacon try { 22442d6b312SCedric van Putten if (!config.modRequest.ignoreExistingNativeFiles && fs.existsSync(filePath)) { 225082815dcSEvan Bacon const contents = await readFile(filePath, 'utf8'); 226082815dcSEvan Bacon assert(contents, 'Entitlements plist is empty'); 227082815dcSEvan Bacon modResults = plist.parse(contents); 228082815dcSEvan Bacon } else { 229082815dcSEvan Bacon modResults = getEntitlementsPlistTemplate(); 230082815dcSEvan Bacon } 231082815dcSEvan Bacon } catch (error: any) { 232082815dcSEvan Bacon // Throw errors in introspection mode. 233082815dcSEvan Bacon if (!config.modRequest.introspect) { 234082815dcSEvan Bacon throw error; 235082815dcSEvan Bacon } 236082815dcSEvan Bacon // Fallback to using the template file. 237082815dcSEvan Bacon modResults = getEntitlementsPlistTemplate(); 238082815dcSEvan Bacon } 239082815dcSEvan Bacon 240082815dcSEvan Bacon // Apply all of the .entitlements values to the expo.ios.entitlements object 241082815dcSEvan Bacon // TODO: Remove this in favor of just overwriting the .entitlements with the Expo object. This will enable people to actually remove values. 242082815dcSEvan Bacon if (!config.ios) config.ios = {}; 243082815dcSEvan Bacon if (!config.ios.entitlements) config.ios.entitlements = {}; 244082815dcSEvan Bacon 245082815dcSEvan Bacon config.ios.entitlements = { 246082815dcSEvan Bacon ...(modResults || {}), 247082815dcSEvan Bacon ...config.ios.entitlements, 248082815dcSEvan Bacon }; 249082815dcSEvan Bacon 250082815dcSEvan Bacon return config.ios.entitlements!; 251082815dcSEvan Bacon }, 252082815dcSEvan Bacon 253082815dcSEvan Bacon async write(filePath, config) { 254082815dcSEvan Bacon // Update the contents of the static entitlements object 255082815dcSEvan Bacon if (!config.ios) { 256082815dcSEvan Bacon config.ios = {}; 257082815dcSEvan Bacon } 258082815dcSEvan Bacon config.ios.entitlements = config.modResults; 259082815dcSEvan Bacon 260082815dcSEvan Bacon // Return early without writing, in introspection mode. 261082815dcSEvan Bacon if (config.modRequest.introspect) { 262082815dcSEvan Bacon return; 263082815dcSEvan Bacon } 264082815dcSEvan Bacon 265082815dcSEvan Bacon await writeFile(filePath, plist.build(sortObject(config.modResults))); 266082815dcSEvan Bacon }, 267082815dcSEvan Bacon }), 268082815dcSEvan Bacon 269082815dcSEvan Bacon // Append a rule to supply Podfile.properties.json data to mods on `mods.ios.podfileProperties` 270082815dcSEvan Bacon podfileProperties: provider<Record<string, JSONValue>>({ 271082815dcSEvan Bacon isIntrospective: true, 272082815dcSEvan Bacon 273082815dcSEvan Bacon getFilePath({ modRequest: { platformProjectRoot } }) { 274082815dcSEvan Bacon return path.resolve(platformProjectRoot, 'Podfile.properties.json'); 275082815dcSEvan Bacon }, 276082815dcSEvan Bacon async read(filePath) { 277082815dcSEvan Bacon let results: Record<string, JSONValue> = {}; 278082815dcSEvan Bacon try { 279082815dcSEvan Bacon results = await JsonFile.readAsync(filePath); 280082815dcSEvan Bacon } catch {} 281082815dcSEvan Bacon return results; 282082815dcSEvan Bacon }, 283082815dcSEvan Bacon async write(filePath, { modResults, modRequest: { introspect } }) { 284082815dcSEvan Bacon if (introspect) { 285082815dcSEvan Bacon return; 286082815dcSEvan Bacon } 287082815dcSEvan Bacon await JsonFile.writeAsync(filePath, modResults); 288082815dcSEvan Bacon }, 289082815dcSEvan Bacon }), 290082815dcSEvan Bacon}; 291082815dcSEvan Bacon 292082815dcSEvan Bacontype IosDefaultProviders = typeof defaultProviders; 293082815dcSEvan Bacon 294082815dcSEvan Baconexport function withIosBaseMods( 295082815dcSEvan Bacon config: ExportedConfig, 296082815dcSEvan Bacon { 297082815dcSEvan Bacon providers, 298082815dcSEvan Bacon ...props 299082815dcSEvan Bacon }: ForwardedBaseModOptions & { providers?: Partial<IosDefaultProviders> } = {} 300082815dcSEvan Bacon): ExportedConfig { 301082815dcSEvan Bacon return withGeneratedBaseMods<IosModName>(config, { 302082815dcSEvan Bacon ...props, 303082815dcSEvan Bacon platform: 'ios', 304082815dcSEvan Bacon providers: providers ?? getIosModFileProviders(), 305082815dcSEvan Bacon }); 306082815dcSEvan Bacon} 307082815dcSEvan Bacon 308082815dcSEvan Baconexport function getIosModFileProviders() { 309082815dcSEvan Bacon return defaultProviders; 310082815dcSEvan Bacon} 311