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