1import type { ExpoConfig } from '@expo/config-types';
2import type { JSONObject } from '@expo/json-file';
3import type { XcodeProject } from 'xcode';
4
5import type { ConfigPlugin, Mod } from '../Plugin.types';
6import type { ExpoPlist, InfoPlist } from '../ios/IosConfig.types';
7import type { AppDelegateProjectFile } from '../ios/Paths';
8import { get } from '../utils/obj';
9import { addWarningIOS } from '../utils/warnings';
10import { withMod } from './withMod';
11
12type MutateInfoPlistAction = (
13  expo: ExpoConfig,
14  infoPlist: InfoPlist
15) => Promise<InfoPlist> | InfoPlist;
16
17/**
18 * Helper method for creating mods from existing config functions.
19 *
20 * @param action
21 */
22export function createInfoPlistPlugin(action: MutateInfoPlistAction, name?: string): ConfigPlugin {
23  const withUnknown: ConfigPlugin = (config) =>
24    withInfoPlist(config, async (config) => {
25      config.modResults = await action(config, config.modResults);
26      return config;
27    });
28  if (name) {
29    Object.defineProperty(withUnknown, 'name', {
30      value: name,
31    });
32  }
33  return withUnknown;
34}
35
36export function createInfoPlistPluginWithPropertyGuard(
37  action: MutateInfoPlistAction,
38  settings: {
39    infoPlistProperty: string;
40    expoConfigProperty: string;
41    expoPropertyGetter?: (config: ExpoConfig) => string;
42  },
43  name?: string
44): ConfigPlugin {
45  const withUnknown: ConfigPlugin = (config) =>
46    withInfoPlist(config, async (config) => {
47      const existingProperty = settings.expoPropertyGetter
48        ? settings.expoPropertyGetter(config)
49        : get(config, settings.expoConfigProperty);
50      // If the user explicitly sets a value in the infoPlist, we should respect that.
51      if (config.modRawConfig.ios?.infoPlist?.[settings.infoPlistProperty] === undefined) {
52        config.modResults = await action(config, config.modResults);
53      } else if (existingProperty !== undefined) {
54        // Only warn if there is a conflict.
55        addWarningIOS(
56          settings.expoConfigProperty,
57          `"ios.infoPlist.${settings.infoPlistProperty}" is set in the config. Ignoring abstract property "${settings.expoConfigProperty}": ${existingProperty}`
58        );
59      }
60
61      return config;
62    });
63  if (name) {
64    Object.defineProperty(withUnknown, 'name', {
65      value: name,
66    });
67  }
68  return withUnknown;
69}
70
71type MutateEntitlementsPlistAction = (expo: ExpoConfig, entitlements: JSONObject) => JSONObject;
72
73/**
74 * Helper method for creating mods from existing config functions.
75 *
76 * @param action
77 */
78export function createEntitlementsPlugin(
79  action: MutateEntitlementsPlistAction,
80  name: string
81): ConfigPlugin {
82  const withUnknown: ConfigPlugin = (config) =>
83    withEntitlementsPlist(config, async (config) => {
84      config.modResults = await action(config, config.modResults);
85      return config;
86    });
87  if (name) {
88    Object.defineProperty(withUnknown, 'name', {
89      value: name,
90    });
91  }
92  return withUnknown;
93}
94
95/**
96 * Provides the AppDelegate file for modification.
97 *
98 * @param config
99 * @param action
100 */
101export const withAppDelegate: ConfigPlugin<Mod<AppDelegateProjectFile>> = (config, action) => {
102  return withMod(config, {
103    platform: 'ios',
104    mod: 'appDelegate',
105    action,
106  });
107};
108
109/**
110 * Provides the Info.plist file for modification.
111 * Keeps the config's expo.ios.infoPlist object in sync with the data.
112 *
113 * @param config
114 * @param action
115 */
116export const withInfoPlist: ConfigPlugin<Mod<InfoPlist>> = (config, action) => {
117  return withMod<InfoPlist>(config, {
118    platform: 'ios',
119    mod: 'infoPlist',
120    async action(config) {
121      config = await action(config);
122      if (!config.ios) {
123        config.ios = {};
124      }
125      config.ios.infoPlist = config.modResults;
126      return config;
127    },
128  });
129};
130
131/**
132 * Provides the main .entitlements file for modification.
133 * Keeps the config's expo.ios.entitlements object in sync with the data.
134 *
135 * @param config
136 * @param action
137 */
138export const withEntitlementsPlist: ConfigPlugin<Mod<JSONObject>> = (config, action) => {
139  return withMod<JSONObject>(config, {
140    platform: 'ios',
141    mod: 'entitlements',
142    async action(config) {
143      config = await action(config);
144      if (!config.ios) {
145        config.ios = {};
146      }
147      config.ios.entitlements = config.modResults;
148      return config;
149    },
150  });
151};
152
153/**
154 * Provides the Expo.plist for modification.
155 *
156 * @param config
157 * @param action
158 */
159export const withExpoPlist: ConfigPlugin<Mod<ExpoPlist>> = (config, action) => {
160  return withMod(config, {
161    platform: 'ios',
162    mod: 'expoPlist',
163    action,
164  });
165};
166
167/**
168 * Provides the main .xcodeproj for modification.
169 *
170 * @param config
171 * @param action
172 */
173export const withXcodeProject: ConfigPlugin<Mod<XcodeProject>> = (config, action) => {
174  return withMod(config, {
175    platform: 'ios',
176    mod: 'xcodeproj',
177    action,
178  });
179};
180
181/**
182 * Provides the Podfile.properties.json for modification.
183 *
184 * @param config
185 * @param action
186 */
187export const withPodfileProperties: ConfigPlugin<Mod<Record<string, string>>> = (
188  config,
189  action
190) => {
191  return withMod(config, {
192    platform: 'ios',
193    mod: 'podfileProperties',
194    action,
195  });
196};
197