1import { ExpoConfig } from '@expo/config-types';
2import { XcodeProject } from 'xcode';
3
4import { InfoPlist } from './IosConfig.types';
5import { findFirstNativeTarget } from './Target';
6import {
7  ConfigurationSectionEntry,
8  getBuildConfigurationsForListId,
9  sanitizedName,
10} from './utils/Xcodeproj';
11import { ConfigPlugin } from '../Plugin.types';
12import { createInfoPlistPluginWithPropertyGuard, withXcodeProject } from '../plugins/ios-plugins';
13
14export const withDisplayName = createInfoPlistPluginWithPropertyGuard(
15  setDisplayName,
16  {
17    infoPlistProperty: 'CFBundleDisplayName',
18    expoConfigProperty: 'name',
19  },
20  'withDisplayName'
21);
22
23export const withName = createInfoPlistPluginWithPropertyGuard(
24  setName,
25  {
26    infoPlistProperty: 'CFBundleName',
27    expoConfigProperty: 'name',
28  },
29  'withName'
30);
31
32/** Set the PRODUCT_NAME variable in the xcproj file based on the app.json name property. */
33export const withProductName: ConfigPlugin = (config) => {
34  return withXcodeProject(config, (config) => {
35    config.modResults = setProductName(config, config.modResults);
36    return config;
37  });
38};
39
40export function getName(config: Pick<ExpoConfig, 'name'>) {
41  return typeof config.name === 'string' ? config.name : null;
42}
43
44/**
45 * CFBundleDisplayName is used for most things: the name on the home screen, in
46 * notifications, and others.
47 */
48export function setDisplayName(
49  configOrName: Pick<ExpoConfig, 'name'> | string,
50  { CFBundleDisplayName, ...infoPlist }: InfoPlist
51): InfoPlist {
52  let name: string | null = null;
53  if (typeof configOrName === 'string') {
54    name = configOrName;
55  } else {
56    name = getName(configOrName);
57  }
58
59  if (!name) {
60    return infoPlist;
61  }
62
63  return {
64    ...infoPlist,
65    CFBundleDisplayName: name,
66  };
67}
68
69/**
70 * CFBundleName is recommended to be 16 chars or less and is used in lists, eg:
71 * sometimes on the App Store
72 */
73export function setName(
74  config: Pick<ExpoConfig, 'name'>,
75  { CFBundleName, ...infoPlist }: InfoPlist
76): InfoPlist {
77  const name = getName(config);
78
79  if (!name) {
80    return infoPlist;
81  }
82
83  return {
84    ...infoPlist,
85    CFBundleName: name,
86  };
87}
88
89export function setProductName(
90  config: Pick<ExpoConfig, 'name'>,
91  project: XcodeProject
92): XcodeProject {
93  const name = sanitizedName(getName(config) ?? '');
94
95  if (!name) {
96    return project;
97  }
98  const quotedName = ensureQuotes(name);
99
100  const [, nativeTarget] = findFirstNativeTarget(project);
101
102  getBuildConfigurationsForListId(project, nativeTarget.buildConfigurationList).forEach(
103    ([, item]: ConfigurationSectionEntry) => {
104      item.buildSettings.PRODUCT_NAME = quotedName;
105    }
106  );
107
108  return project;
109}
110
111const ensureQuotes = (value: string) => {
112  if (!value.match(/^['"]/)) {
113    return `"${value}"`;
114  }
115  return value;
116};
117