1import { ExpoConfig } from '@expo/config-types';
2import { JSONObject } from '@expo/json-file';
3import fs from 'fs';
4import path from 'path';
5import slash from 'slash';
6import { XCBuildConfiguration } from 'xcode';
7
8import { findFirstNativeTarget, getXCBuildConfigurationFromPbxproj } from './Target';
9import {
10  getBuildConfigurationsForListId,
11  getPbxproj,
12  getProductName,
13  getProjectName,
14} from './utils/Xcodeproj';
15import { trimQuotes } from './utils/string';
16import { createEntitlementsPlugin } from '../plugins/ios-plugins';
17
18export const withAssociatedDomains = createEntitlementsPlugin(
19  setAssociatedDomains,
20  'withAssociatedDomains'
21);
22
23export function setAssociatedDomains(
24  config: ExpoConfig,
25  { 'com.apple.developer.associated-domains': _, ...entitlementsPlist }: JSONObject
26): JSONObject {
27  if (config.ios?.associatedDomains) {
28    return {
29      ...entitlementsPlist,
30      'com.apple.developer.associated-domains': config.ios.associatedDomains,
31    };
32  }
33
34  return entitlementsPlist;
35}
36
37export function getEntitlementsPath(
38  projectRoot: string,
39  {
40    targetName,
41    buildConfiguration = 'Release',
42  }: { targetName?: string; buildConfiguration?: string } = {}
43): string | null {
44  const project = getPbxproj(projectRoot);
45  const xcBuildConfiguration = getXCBuildConfigurationFromPbxproj(project, {
46    targetName,
47    buildConfiguration,
48  });
49  if (!xcBuildConfiguration) {
50    return null;
51  }
52  const entitlementsPath = getEntitlementsPathFromBuildConfiguration(
53    projectRoot,
54    xcBuildConfiguration
55  );
56  return entitlementsPath && fs.existsSync(entitlementsPath) ? entitlementsPath : null;
57}
58
59function getEntitlementsPathFromBuildConfiguration(
60  projectRoot: string,
61  xcBuildConfiguration: XCBuildConfiguration
62): string | null {
63  const entitlementsPathRaw = xcBuildConfiguration?.buildSettings?.CODE_SIGN_ENTITLEMENTS as
64    | string
65    | undefined;
66  if (entitlementsPathRaw) {
67    return path.normalize(path.join(projectRoot, 'ios', trimQuotes(entitlementsPathRaw)));
68  } else {
69    return null;
70  }
71}
72
73export function ensureApplicationTargetEntitlementsFileConfigured(projectRoot: string): void {
74  const project = getPbxproj(projectRoot);
75  const projectName = getProjectName(projectRoot);
76  const productName = getProductName(project);
77
78  const [, applicationTarget] = findFirstNativeTarget(project);
79  const buildConfigurations = getBuildConfigurationsForListId(
80    project,
81    applicationTarget.buildConfigurationList
82  );
83  let hasChangesToWrite = false;
84  for (const [, xcBuildConfiguration] of buildConfigurations) {
85    const oldEntitlementPath = getEntitlementsPathFromBuildConfiguration(
86      projectRoot,
87      xcBuildConfiguration
88    );
89    if (oldEntitlementPath && fs.existsSync(oldEntitlementPath)) {
90      return;
91    }
92    hasChangesToWrite = true;
93    // Use posix formatted path, even on Windows
94    const entitlementsRelativePath = slash(path.join(projectName, `${productName}.entitlements`));
95    const entitlementsPath = path.normalize(
96      path.join(projectRoot, 'ios', entitlementsRelativePath)
97    );
98    fs.mkdirSync(path.dirname(entitlementsPath), { recursive: true });
99    if (!fs.existsSync(entitlementsPath)) {
100      fs.writeFileSync(entitlementsPath, ENTITLEMENTS_TEMPLATE);
101    }
102    xcBuildConfiguration.buildSettings.CODE_SIGN_ENTITLEMENTS = entitlementsRelativePath;
103  }
104  if (hasChangesToWrite) {
105    fs.writeFileSync(project.filepath, project.writeSync());
106  }
107}
108
109const ENTITLEMENTS_TEMPLATE = `
110<?xml version="1.0" encoding="UTF-8"?>
111<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
112<plist version="1.0">
113<dict>
114</dict>
115</plist>
116`;
117