1082815dcSEvan Baconimport { ExpoConfig } from '@expo/config-types'; 2082815dcSEvan Baconimport fs from 'fs'; 3082815dcSEvan Baconimport path from 'path'; 4082815dcSEvan Baconimport resolveFrom from 'resolve-from'; 5082815dcSEvan Bacon 6082815dcSEvan Baconimport { ConfigPlugin, InfoPlist } from '../Plugin.types'; 7082815dcSEvan Baconimport { createInfoPlistPlugin, withAppDelegate } from '../plugins/ios-plugins'; 8082815dcSEvan Baconimport { withDangerousMod } from '../plugins/withDangerousMod'; 9082815dcSEvan Baconimport { mergeContents, MergeResults, removeContents } from '../utils/generateCode'; 10082815dcSEvan Bacon 11082815dcSEvan Baconconst debug = require('debug')('expo:config-plugins:ios:maps') as typeof console.log; 12082815dcSEvan Bacon 13082815dcSEvan Baconexport const MATCH_INIT = 14*857ad32fSBrent Vatne /-\s*\(BOOL\)\s*application:\s*\(UIApplication\s*\*\s*\)\s*\w+\s+didFinishLaunchingWithOptions:/g; 15082815dcSEvan Bacon 16082815dcSEvan Baconconst withGoogleMapsKey = createInfoPlistPlugin(setGoogleMapsApiKey, 'withGoogleMapsKey'); 17082815dcSEvan Bacon 18082815dcSEvan Baconexport const withMaps: ConfigPlugin = (config) => { 19082815dcSEvan Bacon config = withGoogleMapsKey(config); 20082815dcSEvan Bacon 21082815dcSEvan Bacon const apiKey = getGoogleMapsApiKey(config); 22082815dcSEvan Bacon // Technically adds react-native-maps (Apple maps) and google maps. 23082815dcSEvan Bacon 24082815dcSEvan Bacon debug('Google Maps API Key:', apiKey); 25082815dcSEvan Bacon config = withMapsCocoaPods(config, { useGoogleMaps: !!apiKey }); 26082815dcSEvan Bacon 27082815dcSEvan Bacon // Adds/Removes AppDelegate setup for Google Maps API on iOS 28082815dcSEvan Bacon config = withGoogleMapsAppDelegate(config, { apiKey }); 29082815dcSEvan Bacon 30082815dcSEvan Bacon return config; 31082815dcSEvan Bacon}; 32082815dcSEvan Bacon 33082815dcSEvan Baconexport function getGoogleMapsApiKey(config: Pick<ExpoConfig, 'ios'>) { 34082815dcSEvan Bacon return config.ios?.config?.googleMapsApiKey ?? null; 35082815dcSEvan Bacon} 36082815dcSEvan Bacon 37082815dcSEvan Baconexport function setGoogleMapsApiKey( 38082815dcSEvan Bacon config: Pick<ExpoConfig, 'ios'>, 39082815dcSEvan Bacon { GMSApiKey, ...infoPlist }: InfoPlist 40082815dcSEvan Bacon): InfoPlist { 41082815dcSEvan Bacon const apiKey = getGoogleMapsApiKey(config); 42082815dcSEvan Bacon 43082815dcSEvan Bacon if (apiKey === null) { 44082815dcSEvan Bacon return infoPlist; 45082815dcSEvan Bacon } 46082815dcSEvan Bacon 47082815dcSEvan Bacon return { 48082815dcSEvan Bacon ...infoPlist, 49082815dcSEvan Bacon GMSApiKey: apiKey, 50082815dcSEvan Bacon }; 51082815dcSEvan Bacon} 52082815dcSEvan Bacon 53082815dcSEvan Baconexport function addGoogleMapsAppDelegateImport(src: string): MergeResults { 54082815dcSEvan Bacon const newSrc = []; 55082815dcSEvan Bacon newSrc.push( 56082815dcSEvan Bacon '#if __has_include(<GoogleMaps/GoogleMaps.h>)', 57082815dcSEvan Bacon '#import <GoogleMaps/GoogleMaps.h>', 58082815dcSEvan Bacon '#endif' 59082815dcSEvan Bacon ); 60082815dcSEvan Bacon 61082815dcSEvan Bacon return mergeContents({ 62082815dcSEvan Bacon tag: 'react-native-maps-import', 63082815dcSEvan Bacon src, 64082815dcSEvan Bacon newSrc: newSrc.join('\n'), 65082815dcSEvan Bacon anchor: /#import "AppDelegate\.h"/, 66082815dcSEvan Bacon offset: 1, 67082815dcSEvan Bacon comment: '//', 68082815dcSEvan Bacon }); 69082815dcSEvan Bacon} 70082815dcSEvan Bacon 71082815dcSEvan Baconexport function removeGoogleMapsAppDelegateImport(src: string): MergeResults { 72082815dcSEvan Bacon return removeContents({ 73082815dcSEvan Bacon tag: 'react-native-maps-import', 74082815dcSEvan Bacon src, 75082815dcSEvan Bacon }); 76082815dcSEvan Bacon} 77082815dcSEvan Bacon 78082815dcSEvan Baconexport function addGoogleMapsAppDelegateInit(src: string, apiKey: string): MergeResults { 79082815dcSEvan Bacon const newSrc = []; 80082815dcSEvan Bacon newSrc.push( 81082815dcSEvan Bacon '#if __has_include(<GoogleMaps/GoogleMaps.h>)', 82082815dcSEvan Bacon ` [GMSServices provideAPIKey:@"${apiKey}"];`, 83082815dcSEvan Bacon '#endif' 84082815dcSEvan Bacon ); 85082815dcSEvan Bacon 86082815dcSEvan Bacon return mergeContents({ 87082815dcSEvan Bacon tag: 'react-native-maps-init', 88082815dcSEvan Bacon src, 89082815dcSEvan Bacon newSrc: newSrc.join('\n'), 90082815dcSEvan Bacon anchor: MATCH_INIT, 91*857ad32fSBrent Vatne offset: 2, 92082815dcSEvan Bacon comment: '//', 93082815dcSEvan Bacon }); 94082815dcSEvan Bacon} 95082815dcSEvan Bacon 96082815dcSEvan Baconexport function removeGoogleMapsAppDelegateInit(src: string): MergeResults { 97082815dcSEvan Bacon return removeContents({ 98082815dcSEvan Bacon tag: 'react-native-maps-init', 99082815dcSEvan Bacon src, 100082815dcSEvan Bacon }); 101082815dcSEvan Bacon} 102082815dcSEvan Bacon 103082815dcSEvan Bacon/** 104082815dcSEvan Bacon * @param src The contents of the Podfile. 105082815dcSEvan Bacon * @returns Podfile with Google Maps added. 106082815dcSEvan Bacon */ 107082815dcSEvan Baconexport function addMapsCocoaPods(src: string): MergeResults { 108082815dcSEvan Bacon return mergeContents({ 109082815dcSEvan Bacon tag: 'react-native-maps', 110082815dcSEvan Bacon src, 111082815dcSEvan Bacon newSrc: ` pod 'react-native-google-maps', path: File.dirname(\`node --print "require.resolve('react-native-maps/package.json')"\`)`, 112082815dcSEvan Bacon anchor: /use_native_modules/, 113082815dcSEvan Bacon offset: 0, 114082815dcSEvan Bacon comment: '#', 115082815dcSEvan Bacon }); 116082815dcSEvan Bacon} 117082815dcSEvan Bacon 118082815dcSEvan Baconexport function removeMapsCocoaPods(src: string): MergeResults { 119082815dcSEvan Bacon return removeContents({ 120082815dcSEvan Bacon tag: 'react-native-maps', 121082815dcSEvan Bacon src, 122082815dcSEvan Bacon }); 123082815dcSEvan Bacon} 124082815dcSEvan Bacon 125082815dcSEvan Baconfunction isReactNativeMapsInstalled(projectRoot: string): string | null { 126082815dcSEvan Bacon const resolved = resolveFrom.silent(projectRoot, 'react-native-maps/package.json'); 127082815dcSEvan Bacon return resolved ? path.dirname(resolved) : null; 128082815dcSEvan Bacon} 129082815dcSEvan Bacon 130082815dcSEvan Baconfunction isReactNativeMapsAutolinked(config: Pick<ExpoConfig, '_internal'>): boolean { 131082815dcSEvan Bacon // Only add the native code changes if we know that the package is going to be linked natively. 132082815dcSEvan Bacon // This is specifically for monorepo support where one app might have react-native-maps (adding it to the node_modules) 133082815dcSEvan Bacon // but another app will not have it installed in the package.json, causing it to not be linked natively. 134082815dcSEvan Bacon // This workaround only exists because react-native-maps doesn't have a config plugin vendored in the package. 135082815dcSEvan Bacon 136082815dcSEvan Bacon // TODO: `react-native-maps` doesn't use Expo autolinking so we cannot safely disable the module. 137082815dcSEvan Bacon return true; 138082815dcSEvan Bacon 139082815dcSEvan Bacon // return ( 140082815dcSEvan Bacon // !config._internal?.autolinkedModules || 141082815dcSEvan Bacon // config._internal.autolinkedModules.includes('react-native-maps') 142082815dcSEvan Bacon // ); 143082815dcSEvan Bacon} 144082815dcSEvan Bacon 145082815dcSEvan Baconconst withMapsCocoaPods: ConfigPlugin<{ useGoogleMaps: boolean }> = (config, { useGoogleMaps }) => { 146082815dcSEvan Bacon return withDangerousMod(config, [ 147082815dcSEvan Bacon 'ios', 148082815dcSEvan Bacon async (config) => { 149082815dcSEvan Bacon const filePath = path.join(config.modRequest.platformProjectRoot, 'Podfile'); 150082815dcSEvan Bacon const contents = await fs.promises.readFile(filePath, 'utf-8'); 151082815dcSEvan Bacon let results: MergeResults; 152082815dcSEvan Bacon // Only add the block if react-native-maps is installed in the project (best effort). 153082815dcSEvan Bacon // Generally prebuild runs after a yarn install so this should always work as expected. 154082815dcSEvan Bacon const googleMapsPath = isReactNativeMapsInstalled(config.modRequest.projectRoot); 155082815dcSEvan Bacon const isLinked = isReactNativeMapsAutolinked(config); 156082815dcSEvan Bacon debug('Is Expo Autolinked:', isLinked); 157082815dcSEvan Bacon debug('react-native-maps path:', googleMapsPath); 158082815dcSEvan Bacon if (isLinked && googleMapsPath && useGoogleMaps) { 159082815dcSEvan Bacon try { 160082815dcSEvan Bacon results = addMapsCocoaPods(contents); 161082815dcSEvan Bacon } catch (error: any) { 162082815dcSEvan Bacon if (error.code === 'ERR_NO_MATCH') { 163082815dcSEvan Bacon throw new Error( 164082815dcSEvan Bacon `Cannot add react-native-maps to the project's ios/Podfile because it's malformed. Please report this with a copy of your project Podfile.` 165082815dcSEvan Bacon ); 166082815dcSEvan Bacon } 167082815dcSEvan Bacon throw error; 168082815dcSEvan Bacon } 169082815dcSEvan Bacon } else { 170082815dcSEvan Bacon // If the package is no longer installed, then remove the block. 171082815dcSEvan Bacon results = removeMapsCocoaPods(contents); 172082815dcSEvan Bacon } 173082815dcSEvan Bacon if (results.didMerge || results.didClear) { 174082815dcSEvan Bacon await fs.promises.writeFile(filePath, results.contents); 175082815dcSEvan Bacon } 176082815dcSEvan Bacon return config; 177082815dcSEvan Bacon }, 178082815dcSEvan Bacon ]); 179082815dcSEvan Bacon}; 180082815dcSEvan Bacon 181082815dcSEvan Baconconst withGoogleMapsAppDelegate: ConfigPlugin<{ apiKey: string | null }> = (config, { apiKey }) => { 182082815dcSEvan Bacon return withAppDelegate(config, (config) => { 183082815dcSEvan Bacon if (['objc', 'objcpp'].includes(config.modResults.language)) { 184082815dcSEvan Bacon if ( 185082815dcSEvan Bacon apiKey && 186082815dcSEvan Bacon isReactNativeMapsAutolinked(config) && 187082815dcSEvan Bacon isReactNativeMapsInstalled(config.modRequest.projectRoot) 188082815dcSEvan Bacon ) { 189082815dcSEvan Bacon try { 190082815dcSEvan Bacon config.modResults.contents = addGoogleMapsAppDelegateImport( 191082815dcSEvan Bacon config.modResults.contents 192082815dcSEvan Bacon ).contents; 193082815dcSEvan Bacon config.modResults.contents = addGoogleMapsAppDelegateInit( 194082815dcSEvan Bacon config.modResults.contents, 195082815dcSEvan Bacon apiKey 196082815dcSEvan Bacon ).contents; 197082815dcSEvan Bacon } catch (error: any) { 198082815dcSEvan Bacon if (error.code === 'ERR_NO_MATCH') { 199082815dcSEvan Bacon throw new Error( 200082815dcSEvan Bacon `Cannot add Google Maps to the project's AppDelegate because it's malformed. Please report this with a copy of your project AppDelegate.` 201082815dcSEvan Bacon ); 202082815dcSEvan Bacon } 203082815dcSEvan Bacon throw error; 204082815dcSEvan Bacon } 205082815dcSEvan Bacon } else { 206082815dcSEvan Bacon config.modResults.contents = removeGoogleMapsAppDelegateImport( 207082815dcSEvan Bacon config.modResults.contents 208082815dcSEvan Bacon ).contents; 209082815dcSEvan Bacon config.modResults.contents = removeGoogleMapsAppDelegateInit( 210082815dcSEvan Bacon config.modResults.contents 211082815dcSEvan Bacon ).contents; 212082815dcSEvan Bacon } 213082815dcSEvan Bacon } else { 214082815dcSEvan Bacon throw new Error( 215082815dcSEvan Bacon `Cannot setup Google Maps because the project AppDelegate is not a supported language: ${config.modResults.language}` 216082815dcSEvan Bacon ); 217082815dcSEvan Bacon } 218082815dcSEvan Bacon return config; 219082815dcSEvan Bacon }); 220082815dcSEvan Bacon}; 221