1import { ExpoConfig } from '@expo/config-types';
2import plist from '@expo/plist';
3import assert from 'assert';
4import fs from 'fs';
5import path from 'path';
6import { XcodeProject } from 'xcode';
7
8import { ConfigPlugin, ModProps } from '../Plugin.types';
9import { withInfoPlist, withXcodeProject } from '../plugins/ios-plugins';
10import { InfoPlist } from './IosConfig.types';
11import { getSourceRoot } from './Paths';
12import { appendScheme } from './Scheme';
13import { addResourceFileToGroup, getProjectName } from './utils/Xcodeproj';
14
15export const withGoogle: ConfigPlugin = (config) => {
16  return withInfoPlist(config, (config) => {
17    config.modResults = setGoogleConfig(config, config.modResults, config.modRequest);
18    return config;
19  });
20};
21
22export const withGoogleServicesFile: ConfigPlugin = (config) => {
23  return withXcodeProject(config, (config) => {
24    config.modResults = setGoogleServicesFile(config, {
25      projectRoot: config.modRequest.projectRoot,
26      project: config.modResults,
27    });
28    return config;
29  });
30};
31
32function readGoogleServicesInfoPlist(
33  relativePath: string,
34  { projectRoot }: { projectRoot: string }
35) {
36  const googleServiceFilePath = path.resolve(projectRoot, relativePath);
37  const contents = fs.readFileSync(googleServiceFilePath, 'utf8');
38  assert(contents, 'GoogleService-Info.plist is empty');
39  return plist.parse(contents);
40}
41
42export function getGoogleSignInReservedClientId(
43  config: Pick<ExpoConfig, 'ios'>,
44  modRequest: Pick<ModProps<InfoPlist>, 'projectRoot'>
45): string | null {
46  const reservedClientId = config.ios?.config?.googleSignIn?.reservedClientId ?? null;
47  if (reservedClientId) {
48    return reservedClientId;
49  }
50
51  const googleServicesFileRelativePath = getGoogleServicesFile(config);
52  if (googleServicesFileRelativePath === null) {
53    return null;
54  }
55
56  const infoPlist = readGoogleServicesInfoPlist(googleServicesFileRelativePath, modRequest);
57
58  return infoPlist.REVERSED_CLIENT_ID ?? null;
59}
60
61export function getGoogleServicesFile(config: Pick<ExpoConfig, 'ios'>) {
62  return config.ios?.googleServicesFile ?? null;
63}
64
65export function setGoogleSignInReservedClientId(
66  config: Pick<ExpoConfig, 'ios'>,
67  infoPlist: InfoPlist,
68  modRequest: Pick<ModProps<InfoPlist>, 'projectRoot'>
69): InfoPlist {
70  const reservedClientId = getGoogleSignInReservedClientId(config, modRequest);
71
72  if (reservedClientId === null) {
73    return infoPlist;
74  }
75
76  return appendScheme(reservedClientId, infoPlist);
77}
78
79export function setGoogleConfig(
80  config: Pick<ExpoConfig, 'ios'>,
81  infoPlist: InfoPlist,
82  modRequest: ModProps<InfoPlist>
83): InfoPlist {
84  infoPlist = setGoogleSignInReservedClientId(config, infoPlist, modRequest);
85  return infoPlist;
86}
87
88export function setGoogleServicesFile(
89  config: Pick<ExpoConfig, 'ios'>,
90  { projectRoot, project }: { project: XcodeProject; projectRoot: string }
91): XcodeProject {
92  const googleServicesFileRelativePath = getGoogleServicesFile(config);
93  if (googleServicesFileRelativePath === null) {
94    return project;
95  }
96
97  const googleServiceFilePath = path.resolve(projectRoot, googleServicesFileRelativePath);
98  fs.copyFileSync(
99    googleServiceFilePath,
100    path.join(getSourceRoot(projectRoot), 'GoogleService-Info.plist')
101  );
102
103  const projectName = getProjectName(projectRoot);
104  const plistFilePath = `${projectName}/GoogleService-Info.plist`;
105  if (!project.hasFile(plistFilePath)) {
106    project = addResourceFileToGroup({
107      filepath: plistFilePath,
108      groupName: projectName,
109      project,
110      isBuildFile: true,
111      verbose: true,
112    });
113  }
114  return project;
115}
116