| 1 | "use strict"; |
| 2 | |
| 3 | // expo-sharing intentionally uses a fixed target name and also uses that |
| 4 | // internal target name as CFBundleDisplayName. Brand the extension while |
| 5 | // leaving its initially-empty signing team intact: Expo uses that state to |
| 6 | // select ios.appleTeamId and enable first-time profile provisioning. |
| 7 | // |
| 8 | // ORDERING: list this plugin BEFORE expo-sharing. Expo runs same-type mods in |
| 9 | // reverse registration order, so this executes after the extension exists. |
| 10 | |
| 11 | const fs = require("fs"); |
| 12 | const path = require("path"); |
| 13 | const { withDangerousMod, withXcodeProject } = require("expo/config-plugins"); |
| 14 | |
| 15 | const TARGET_NAME = "expo-sharing-extension"; |
| 16 | |
| 17 | function stripComments(section) { |
| 18 | return Object.fromEntries( |
| 19 | Object.entries(section ?? {}).filter(([key]) => !key.endsWith("_comment")), |
| 20 | ); |
| 21 | } |
| 22 | |
| 23 | function findByName(section, name) { |
| 24 | return Object.entries(stripComments(section)).find(([, value]) => value?.name === name) ?? null; |
| 25 | } |
| 26 | |
| 27 | function escapeXml(value) { |
| 28 | return value |
| 29 | .replaceAll("&", "&") |
| 30 | .replaceAll("<", "&lt;") |
| 31 | .replaceAll(">", "&gt;") |
| 32 | .replaceAll('"', "&quot;") |
| 33 | .replaceAll("'", "&apos;"); |
| 34 | } |
| 35 | |
| 36 | function withDisplayNamePlist(config, displayName) { |
| 37 | return withDangerousMod(config, [ |
| 38 | "ios", |
| 39 | (cfg) => { |
| 40 | const plistPath = path.join(cfg.modRequest.platformProjectRoot, TARGET_NAME, "Info.plist"); |
| 41 | if (!fs.existsSync(plistPath)) { |
| 42 | throw new Error( |
| 43 | `${TARGET_NAME}/Info.plist was not generated. Register withShareExtensionDisplayName before expo-sharing.`, |
| 44 | ); |
| 45 | } |
| 46 | const source = fs.readFileSync(plistPath, "utf8"); |
| 47 | const displayNamePattern = /(<key>CFBundleDisplayName<\/key>\s*<string>)[^<]*(<\/string>)/; |
| 48 | if (!displayNamePattern.test(source)) { |
| 49 | throw new Error(`Could not update CFBundleDisplayName in ${plistPath}.`); |
| 50 | } |
| 51 | const next = source.replace(displayNamePattern, `$1${escapeXml(displayName)}$2`); |
| 52 | if (next !== source) { |
| 53 | fs.writeFileSync(plistPath, next); |
| 54 | } |
| 55 | return cfg; |
| 56 | }, |
| 57 | ]); |
| 58 | } |
| 59 | |
| 60 | function withExtensionDisplayNameBuildSetting(config, displayName) { |
| 61 | return withXcodeProject(config, (cfg) => { |
| 62 | const objects = cfg.modResults.hash.project.objects; |
| 63 | const targetEntry = findByName(objects.PBXNativeTarget, TARGET_NAME); |
| 64 | if (!targetEntry) { |
| 65 | throw new Error( |
| 66 | `${TARGET_NAME} Xcode target was not generated. Register withShareExtensionDisplayName before expo-sharing.`, |
| 67 | ); |
| 68 | } |
| 69 | const target = targetEntry[1]; |
| 70 | const configurationList = stripComments(objects.XCConfigurationList)[ |
| 71 | target.buildConfigurationList |
| 72 | ]; |
| 73 | if (!configurationList) { |
| 74 | throw new Error(`Could not find build configurations for ${TARGET_NAME}.`); |
| 75 | } |
| 76 | const buildConfigurations = stripComments(objects.XCBuildConfiguration); |
| 77 | for (const reference of configurationList.buildConfigurations ?? []) { |
| 78 | const buildConfiguration = buildConfigurations[reference.value]; |
| 79 | if (buildConfiguration?.buildSettings) { |
| 80 | buildConfiguration.buildSettings.INFOPLIST_KEY_CFBundleDisplayName = `"${displayName}"`; |
| 81 | } |
| 82 | } |
| 83 | return cfg; |
| 84 | }); |
| 85 | } |
| 86 | |
| 87 | module.exports = function withShareExtensionDisplayName(config) { |
| 88 | const displayName = config.name; |
| 89 | return withExtensionDisplayNameBuildSetting( |
| 90 | withDisplayNamePlist(config, displayName), |
| 91 | displayName, |
| 92 | ); |
| 93 | }; |