diff --git a/LuggMaps.podspec b/LuggMaps.podspec index c4eeb42..f7ddefb 100644 --- a/LuggMaps.podspec +++ b/LuggMaps.podspec @@ -16,7 +16,20 @@ Pod::Spec.new do |s| s.source_files = "ios/**/*.{h,m,mm,swift,cpp}" s.private_header_files = "ios/**/*.h" - s.dependency "GoogleMaps" + # `$LuggMapsGoogleEnabled = false` in the Podfile drops the Google Maps SDK (Apple Maps only) + google_enabled = defined?($LuggMapsGoogleEnabled) ? $LuggMapsGoogleEnabled : true + s.pod_target_xcconfig = { + "GCC_PREPROCESSOR_DEFINITIONS" => "$(inherited) LUGG_GOOGLE_MAPS_ENABLED=#{google_enabled ? 1 : 0}" + } + if google_enabled + s.dependency "GoogleMaps" + else + unless defined?($LuggMapsGoogleDisabledLogged) + Pod::UI.puts "[LuggMaps] Google Maps SDK is disabled, Apple Maps only.".yellow + $LuggMapsGoogleDisabledLogged = true + end + s.exclude_files = "ios/core/Google*", "ios/core/GMS*" + end s.frameworks = "MapKit" install_modules_dependencies(s) diff --git a/docs/content/docs/installation.mdx b/docs/content/docs/installation.mdx index 82a012a..dd605ab 100644 --- a/docs/content/docs/installation.mdx +++ b/docs/content/docs/installation.mdx @@ -38,6 +38,26 @@ Apple Maps works out of the box on iOS. Google Maps requires an API key on every ```sh npx expo prebuild --clean ``` + + #### Apple Maps only + + If you only use Apple Maps on iOS, set `iosGoogleMapsEnabled` to `false` to exclude the Google Maps SDK: + + ```json title="app.json" + [ + "@lugg/maps", + { + "iosGoogleMapsEnabled": false, + "androidGoogleMapsApiKey": "YOUR_ANDROID_API_KEY" + } + ] + ``` + + The plugin ignores `iosGoogleMapsApiKey` and removes the Google Maps initialization from `AppDelegate.swift`. + After changing this option, run `npx expo prebuild --platform ios` and rebuild your app. + To enable Google Maps again, set `iosGoogleMapsEnabled` to `true` and provide `iosGoogleMapsApiKey`. + + With the SDK excluded, a `MapView` requesting `provider="google"` falls back to Apple Maps and logs a warning. ### iOS @@ -57,6 +77,20 @@ Apple Maps works out of the box on iOS. Google Maps requires an API key on every cd ios && pod install ``` + #### Apple Maps only + + If you only use Apple Maps on iOS, add this flag at the top of your `Podfile`: + + ```ruby title="Podfile" + $LuggMapsGoogleEnabled = false + ``` + + Remove `import GoogleMaps` and `GMSServices.provideAPIKey(...)` from `AppDelegate.swift`. + Then run `pod install` and rebuild your app. + To enable Google Maps again, remove the flag and restore the API key initialization before running `pod install`. + + With the SDK excluded, a `MapView` requesting `provider="google"` falls back to Apple Maps and logs a warning. + ### Android Add your Google Maps API key to `AndroidManifest.xml`: diff --git a/ios/LuggMapView.mm b/ios/LuggMapView.mm index 93ebb84..2147237 100644 --- a/ios/LuggMapView.mm +++ b/ios/LuggMapView.mm @@ -8,8 +8,10 @@ #import "LuggTileOverlayView.h" #import "core/AppleMapProvider.h" #import "core/AppleStaticMapProvider.h" +#if LUGG_GOOGLE_MAPS_ENABLED #import "core/GoogleMapProvider.h" #import "core/GoogleStaticMapProvider.h" +#endif #import "core/MapProviderDelegate.h" #import "events/CameraIdleEvent.h" #import "events/CameraMoveEvent.h" @@ -329,6 +331,7 @@ - (void)initializeProviderWithCoordinate:(CLLocationCoordinate2D)coordinate if (_providerType == LuggMapViewProvider::Apple) { _provider = _staticMode ? [[AppleStaticMapProvider alloc] init] : [[AppleMapProvider alloc] init]; +#if LUGG_GOOGLE_MAPS_ENABLED } else if (_staticMode) { GoogleStaticMapProvider *google = [[GoogleStaticMapProvider alloc] init]; google.mapId = _mapId; @@ -338,6 +341,14 @@ - (void)initializeProviderWithCoordinate:(CLLocationCoordinate2D)coordinate google.mapId = _mapId; _provider = google; } +#else + } else { + NSLog(@"[LuggMaps] provider=\"google\" requested but the Google Maps SDK " + @"is excluded ($LuggMapsGoogleEnabled = false); using Apple Maps"); + _provider = _staticMode ? [[AppleStaticMapProvider alloc] init] + : [[AppleMapProvider alloc] init]; + } +#endif _provider.delegate = self; _provider.staticMode = _staticMode; @@ -617,7 +628,7 @@ - (void)reload { _provider = nil; _initialized = NO; [self initializeProviderWithCoordinate:coordinate zoom:zoom]; - if (_providerType == LuggMapViewProvider::Apple) { + if ([_provider isKindOfClass:[AppleMapProvider class]]) { // The captured camera already includes the previous inset offset. [_provider moveCamera:coordinate.latitude longitude:coordinate.longitude diff --git a/plugin/__tests__/withLuggMapsIOS.test.ts b/plugin/__tests__/withLuggMapsIOS.test.ts new file mode 100644 index 0000000..a3138bf --- /dev/null +++ b/plugin/__tests__/withLuggMapsIOS.test.ts @@ -0,0 +1,128 @@ +import type { + ExportedConfig, + InfoPlist, + IOSConfig, + Mod, +} from '@expo/config-plugins'; + +import { + type MapsIOSPluginProps, + withLuggMapsIOS, +} from '../src/withLuggMapsIOS'; + +const nativeFiles = { + podfile: "platform :ios, '15.1'\n", + appDelegate: `import Expo +class AppDelegate { + func application(_ application: UIApplication) -> Bool { + return true + } +} +`, + infoPlist: { CFBundleDisplayName: 'Maps' } as InfoPlist, +}; + +async function runMod(mod: Mod | undefined, modResults: T) { + if (!mod) { + return modResults; + } + + const config = { name: 'Maps', slug: 'maps' }; + const result = await mod({ + ...config, + modResults, + modRawConfig: config, + modRequest: { + projectRoot: '/tmp/maps', + platformProjectRoot: '/tmp/maps/ios', + platform: 'ios', + modName: 'test', + introspect: false, + }, + }); + return result.modResults; +} + +async function applyPlugin(props: MapsIOSPluginProps, files = nativeFiles) { + const config: ExportedConfig = withLuggMapsIOS( + { name: 'Maps', slug: 'maps' }, + props + ); + const mods = config.mods?.ios; + const podfileMod = ( + mods as { podfile?: Mod } + )?.podfile; + const podfile = await runMod(podfileMod, { + path: '/tmp/maps/ios/Podfile', + language: 'rb' as const, + contents: files.podfile, + }); + const appDelegate = await runMod(mods?.appDelegate, { + path: '/tmp/maps/ios/AppDelegate.swift', + language: 'swift' as const, + contents: files.appDelegate, + }); + const infoPlist = await runMod(mods?.infoPlist, { ...files.infoPlist }); + return { + podfile: podfile.contents, + appDelegate: appDelegate.contents, + infoPlist, + }; +} + +it('keeps Google Maps enabled by default', async () => { + const files = await applyPlugin({ apiKey: 'test-key' }); + expect(files.podfile).toBe(nativeFiles.podfile); + expect(files.appDelegate).toContain('import GoogleMaps'); + expect(files.appDelegate).toContain('GMSServices.provideAPIKey("test-key")'); + expect(files.infoPlist.GMSApiKey).toBe('test-key'); +}); + +it('excludes Google Maps on a fresh project even when an API key is provided', async () => { + const files = await applyPlugin({ googleEnabled: false, apiKey: 'test-key' }); + expect(files.podfile).toBe( + `$LuggMapsGoogleEnabled = false\n${nativeFiles.podfile}` + ); + expect(files.appDelegate).toBe(nativeFiles.appDelegate); + expect(files.infoPlist).toEqual(nativeFiles.infoPlist); +}); + +it('removes Google Maps initialization when disabling an existing project', async () => { + const enabled = await applyPlugin({ apiKey: 'test-key' }); + const disabled = await applyPlugin({ googleEnabled: false }, enabled); + expect(disabled.podfile).toContain('$LuggMapsGoogleEnabled = false'); + expect(disabled.appDelegate).toBe(nativeFiles.appDelegate); + expect(disabled.infoPlist).toEqual(nativeFiles.infoPlist); +}); + +it.each([true, undefined])( + 'restores Google Maps after disabling it (googleEnabled=%s)', + async (googleEnabled) => { + const disabled = await applyPlugin({ googleEnabled: false }); + const enabled = await applyPlugin( + { googleEnabled, apiKey: 'test-key' }, + disabled + ); + expect(enabled.podfile).toBe(nativeFiles.podfile); + expect(enabled.appDelegate).toContain('import GoogleMaps'); + expect(enabled.appDelegate).toContain( + 'GMSServices.provideAPIKey("test-key")' + ); + expect(enabled.infoPlist.GMSApiKey).toBe('test-key'); + } +); + +it('removes the exclusion flag without requiring an API key', async () => { + const disabled = await applyPlugin({ googleEnabled: false }); + const enabled = await applyPlugin({ googleEnabled: true }, disabled); + expect(enabled).toEqual(nativeFiles); +}); + +it.each([true, false])( + 'can run repeatedly without changing the result (googleEnabled=%s)', + async (googleEnabled) => { + const props = { googleEnabled, apiKey: 'test-key' }; + const files = await applyPlugin(props); + expect(await applyPlugin(props, files)).toEqual(files); + } +); diff --git a/plugin/src/index.ts b/plugin/src/index.ts index bcc2879..f121cb0 100644 --- a/plugin/src/index.ts +++ b/plugin/src/index.ts @@ -17,12 +17,25 @@ export interface MapsPluginProps { * Required for Android as it only supports Google Maps. */ androidGoogleMapsApiKey?: string; + + /** + * Whether to link the Google Maps SDK on iOS. Set to `false` when only + * Apple Maps is used to drop the SDK from the app. Defaults to `true`. + */ + iosGoogleMapsEnabled?: boolean; } const withMaps: ConfigPlugin = (config, props = {}) => { - const { iosGoogleMapsApiKey, androidGoogleMapsApiKey } = props ?? {}; - - config = withLuggMapsIOS(config, { apiKey: iosGoogleMapsApiKey }); + const { + iosGoogleMapsApiKey, + androidGoogleMapsApiKey, + iosGoogleMapsEnabled = true, + } = props ?? {}; + + config = withLuggMapsIOS(config, { + apiKey: iosGoogleMapsApiKey, + googleEnabled: iosGoogleMapsEnabled, + }); config = withLuggMapsAndroid(config, { apiKey: androidGoogleMapsApiKey }); return config; diff --git a/plugin/src/withLuggMapsIOS.ts b/plugin/src/withLuggMapsIOS.ts index 7b30701..227eed7 100644 --- a/plugin/src/withLuggMapsIOS.ts +++ b/plugin/src/withLuggMapsIOS.ts @@ -2,26 +2,56 @@ import { type ConfigPlugin, withInfoPlist, withAppDelegate, + withPodfile, } from '@expo/config-plugins'; export interface MapsIOSPluginProps { apiKey?: string; + googleEnabled?: boolean; } +const GMS_EXCLUSION_PODFILE_FLAG = '$LuggMapsGoogleEnabled = false'; + export const withLuggMapsIOS: ConfigPlugin = ( config, - { apiKey } + { apiKey, googleEnabled = true } ) => { - if (!apiKey) { + config = withPodfile(config, (c) => { + if (!googleEnabled) { + console.log('[LuggMaps] Google Maps SDK is disabled, Apple Maps only.'); + } + + const contents = c.modResults.contents.replace( + /^\$LuggMapsGoogleEnabled = false\r?\n/gm, + '' + ); + c.modResults.contents = googleEnabled + ? contents + : `${GMS_EXCLUSION_PODFILE_FLAG}\n${contents}`; + return c; + }); + + if (googleEnabled && !apiKey) { return config; } config = withInfoPlist(config, (c) => { - c.modResults.GMSApiKey = apiKey; + if (googleEnabled) { + c.modResults.GMSApiKey = apiKey; + } else { + delete c.modResults.GMSApiKey; + } return c; }); config = withAppDelegate(config, (c) => { + if (!googleEnabled) { + c.modResults.contents = c.modResults.contents + .replace(/^[ \t]*import GoogleMaps\r?\n/gm, '') + .replace(/^[ \t]*GMSServices\.provideAPIKey\("[^"\r\n]*"\)\r?\n/gm, ''); + return c; + } + const contents = c.modResults.contents; // Add import for GoogleMaps @@ -36,7 +66,7 @@ export const withLuggMapsIOS: ConfigPlugin = ( if (!c.modResults.contents.includes('GMSServices.provideAPIKey')) { c.modResults.contents = c.modResults.contents.replace( /(func application\([^)]+\)[^{]*\{)/, - `$1\n GMSServices.provideAPIKey("${apiKey}")\n` + `$1\n GMSServices.provideAPIKey("${apiKey}")` ); }