From be4bc3c1bfdbd30c1325c56f1efdf1e406e130e5 Mon Sep 17 00:00:00 2001 From: kota113 Date: Sun, 26 Apr 2026 14:19:13 +1000 Subject: [PATCH] feat: add Expo Config Plugins to support Continuous Native Generation --- README.md | 127 ++++++++++++++++++++--- app.plugin.js | 18 ++++ package.json | 7 ++ src/plugins/index.js | 53 ++++++++++ src/plugins/withApiKeyAndroid.js | 54 ++++++++++ src/plugins/withApiKeyIos.js | 62 +++++++++++ src/plugins/withCoreLibraryDesugaring.js | 52 ++++++++++ src/plugins/withJetifier.js | 48 +++++++++ 8 files changed, 404 insertions(+), 17 deletions(-) create mode 100644 app.plugin.js create mode 100644 src/plugins/index.js create mode 100644 src/plugins/withApiKeyAndroid.js create mode 100644 src/plugins/withApiKeyIos.js create mode 100644 src/plugins/withCoreLibraryDesugaring.js create mode 100644 src/plugins/withJetifier.js diff --git a/README.md b/README.md index a05753a..72a764a 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ This repository contains a React Native plugin that provides a [Google Navigatio ## React Native Compatibility -The current version of this package has been tested and verified to work with the following React Native versions: +The current version of this package has been tested and verified to work with the following React Native versions: **0.83.1, 0.82.1, 0.81.5, 0.80.3, 0.79.6** @@ -58,6 +58,15 @@ In your TSX or JSX file, import the components you need: import { NavigationView } from '@googlemaps/react-native-navigation-sdk'; ``` +Choose the setup guide that matches your project type: + +- [Bare workflow (plain React Native)](#bare-workflow-setup) +- [Expo managed / prebuild workflow](#expo-workflow-setup) + +--- + +## Bare Workflow Setup + ### Android #### Enable new architecture @@ -127,19 +136,103 @@ ENV['RCT_NEW_ARCH_ENABLED'] = '1' #### Set Google Maps API Key -To set up, specify your API key in the application delegate `ios/Runner/AppDelegate.m`: +To set up, specify your API key in the application delegate `ios/AppDelegate.swift`: -```objective-c -#import +```swift +import GoogleMaps -@implementation AppDelegate +@UIApplicationMain +class AppDelegate: UIResponder, UIApplicationDelegate { -- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions -{ - [GMSServices provideAPIKey:@"API_KEY"]; - return [super application:application didFinishLaunchingWithOptions:launchOptions]; + func application(_ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + GMSServices.provideAPIKey("YOUR_API_KEY") + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } } +``` + +--- + +## Expo Workflow Setup + +This package ships an [Expo Config Plugin](https://docs.expo.dev/guides/config-plugins/) that automatically configures the native Android and iOS projects when you run `expo prebuild` (or `eas build`). No manual edits to `AndroidManifest.xml` or `AppDelegate.swift` are required. + +### 1. Install the package +```shell +npm i @googlemaps/react-native-navigation-sdk +``` + +### 2. Add the plugin to `app.config.ts` + +Pass your Google Maps API keys for Android and iOS via `androidApiKey` and `iosApiKey`: + +```ts +export default { + // ... + plugins: [ + [ + '@googlemaps/react-native-navigation-sdk', + { + androidApiKey: process.env.GOOGLE_MAPS_ANDROID_API_KEY, + iosApiKey: process.env.GOOGLE_MAPS_IOS_API_KEY, + } + ] + ], +}; +``` + +> [!TIP] +> Instead of passing `apiKey` as a plugin option, you can set the platform-specific fields and the plugin will pick them up automatically: +> - **Android**: `android.config.googleMaps.apiKey` +> - **iOS**: `ios.config.googleMapsApiKey` + +### 3. Run prebuild + +```shell +npx expo prebuild +``` + +This will inject the API key into `AndroidManifest.xml` and `AppDelegate.swift` automatically. + +### Android — additional requirements +If you are using Expo 53 or above, there's no need to configure anything else. +
+ If you are using Expo 52 or below + +* You must enable the new architecture in your `app.config.ts`: + ```ts + export default { + // ... + neyArchEnabled: true, + // ... + } + ``` +* You must set the minimum SDK version to 24 or higher: + ```ts + export default { + // ... + android: { + minSdkVersion: 24, + }, + // ... + } + ``` +
+ +Jetifier and core library desugaring are both automatically enabled by the Expo Config Plugin, so no manual changes to `android/gradle.properties` or `android/app/build.gradle` are required. + +### iOS — minimum deployment target + +The minimum iOS deployment target must be 16.0 or higher. Set it in `app.config.ts`: + +```ts +export default { + ios: { + deploymentTarget: '16.0', + }, +}; ``` ## Usage @@ -243,7 +336,7 @@ const initializeNavigation = useCallback(async () => { // Initialize the navigation session and check the status const status = await navigationController.init(); - + switch (status) { case NavigationSessionStatus.OK: console.log('Navigation initialized successfully'); @@ -339,8 +432,8 @@ await navigationController.startGuidance(); #### Adding navigation listeners ```tsx -const { - navigationController, +const { + navigationController, removeAllListeners, setOnArrival, setOnRouteChanged, @@ -595,8 +688,8 @@ The `useNavigation()` hook provides access to the `NavigationController` and lis ```tsx import { useNavigation } from '@googlemaps/react-native-navigation-sdk'; -const { - navigationController, +const { + navigationController, removeAllListeners, setOnArrival, setOnRouteChanged, @@ -681,7 +774,7 @@ useEffect(() => { } }); setOnRouteChanged(() => console.log('Route changed')); - + // Use removeAllListeners() to clear all listeners at once on cleanup // Alternatively, clear individual listeners: setOnArrival(null) return () => removeAllListeners(); @@ -713,8 +806,8 @@ For Android Auto and CarPlay support, the `useNavigationAuto()` hook provides a ```tsx import { useNavigationAuto } from '@googlemaps/react-native-navigation-sdk'; -const { - mapViewAutoController, +const { + mapViewAutoController, removeAllListeners, setOnAutoScreenAvailabilityChanged, setOnCustomNavigationAutoEvent, diff --git a/app.plugin.js b/app.plugin.js new file mode 100644 index 0000000..48976c8 --- /dev/null +++ b/app.plugin.js @@ -0,0 +1,18 @@ +/** + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* eslint-disable */ +module.exports = require('./src/plugins'); diff --git a/package.json b/package.json index 16ebc10..9c94e10 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "android", "ios", "cpp", + "app.plugin.js", "*.podspec", "!ios/build", "!android/build", @@ -87,9 +88,15 @@ "typescript": "^5.8.3" }, "peerDependencies": { + "@expo/config-plugins": "*", "react": "*", "react-native": "*" }, + "peerDependenciesMeta": { + "@expo/config-plugins": { + "optional": true + } + }, "workspaces": [ "example" ], diff --git a/src/plugins/index.js b/src/plugins/index.js new file mode 100644 index 0000000..973ef43 --- /dev/null +++ b/src/plugins/index.js @@ -0,0 +1,53 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* eslint-disable */ +const withApiKeyAndroid = require('./withApiKeyAndroid'); +const withApiKeyIos = require('./withApiKeyIos'); +const withCoreLibraryDesugaring = require('./withCoreLibraryDesugaring'); +const withJetifier = require('./withJetifier'); + +/** + * Expo Config Plugin for @googlemaps/react-native-navigation-sdk + * + * Automatically configures both Android and iOS native projects + * with the required Google Maps API key for the Navigation SDK. + * + * Usage in app.config.ts: + * + * plugins: [ + * [ + * '@googlemaps/react-native-navigation-sdk', + * { + * androidApiKey: 'YOUR_ANDROID_API_KEY', + * iosApiKey: 'YOUR_IOS_API_KEY', + * } + * ] + * ] + * + * Alternatively, set the API key via: + * - android.config.googleMaps.apiKey (Android) + * - ios.config.googleMapsApiKey (iOS) + */ +const withNavigationSdk = (config, options = {}) => { + config = withApiKeyAndroid(config, options); + config = withApiKeyIos(config, options); + config = withCoreLibraryDesugaring(config); + config = withJetifier(config); + return config; +}; + +module.exports = withNavigationSdk; diff --git a/src/plugins/withApiKeyAndroid.js b/src/plugins/withApiKeyAndroid.js new file mode 100644 index 0000000..7e7e930 --- /dev/null +++ b/src/plugins/withApiKeyAndroid.js @@ -0,0 +1,54 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* eslint-disable */ +const { withAndroidManifest } = require('@expo/config-plugins'); + +/** + * Config Plugin for @googlemaps/react-native-navigation-sdk (Android) + * + * Injects the Google Maps API key into AndroidManifest.xml as a entry + */ +const withApiKeyAndroid = (config, { androidApiKey } = {}) => { + const key = androidApiKey ?? config?.android?.config?.googleMaps?.apiKey; + + if (!key) { + throw new Error( + '[withApiKeyAndroid] Google Maps API key is not set. ' + + 'Pass it as a plugin option or set android.config.googleMaps.apiKey in app.config.ts.' + ); + } + + return withAndroidManifest(config, (c) => { + const mainApp = c.modResults.manifest.application?.[0]; + if (mainApp) { + mainApp['meta-data'] = mainApp['meta-data'] ?? []; + const existing = mainApp['meta-data'].find( + (m) => m.$?.['android:name'] === 'com.google.android.geo.API_KEY' + ); + if (existing) { + existing.$['android:value'] = key; + } else { + mainApp['meta-data'].push({ + $: { 'android:name': 'com.google.android.geo.API_KEY', 'android:value': key }, + }); + } + } + return c; + }); +}; + +module.exports = withApiKeyAndroid; diff --git a/src/plugins/withApiKeyIos.js b/src/plugins/withApiKeyIos.js new file mode 100644 index 0000000..718249a --- /dev/null +++ b/src/plugins/withApiKeyIos.js @@ -0,0 +1,62 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* eslint-disable */ +const { withAppDelegate } = require('@expo/config-plugins'); + +/** + * Config Plugin for @googlemaps/react-native-navigation-sdk (iOS) + * + * Injects GMSServices.provideAPIKey() into AppDelegate.swift + */ +const withApiKeyIos = (config, { iosApiKey } = {}) => { + const key = iosApiKey ?? config?.ios?.config?.googleMapsApiKey; + + if (!key) { + throw new Error( + '[withApiKeyIos] Google Maps API key is not set. ' + + 'Pass it as a plugin option or set ios.config.googleMapsApiKey in app.config.ts.' + ); + } + + return withAppDelegate(config, (c) => { + let contents = c.modResults.contents; + + // Already patched + if (contents.includes('GMSServices.provideAPIKey')) { + return c; + } + + // Add import if not present + if (!contents.includes('import GoogleMaps')) { + contents = contents.replace( + /^import Expo/m, + 'import Expo\nimport GoogleMaps' + ); + } + + // Inject provideAPIKey call right after the opening of application(_:didFinishLaunchingWithOptions:) + contents = contents.replace( + /(public override func application\(\s*_ application: UIApplication,\s*didFinishLaunchingWithOptions[^{]*\{)/, + `$1\n GMSServices.provideAPIKey("${key}")` + ); + + c.modResults.contents = contents; + return c; + }); +}; + +module.exports = withApiKeyIos; diff --git a/src/plugins/withCoreLibraryDesugaring.js b/src/plugins/withCoreLibraryDesugaring.js new file mode 100644 index 0000000..e8d79f7 --- /dev/null +++ b/src/plugins/withCoreLibraryDesugaring.js @@ -0,0 +1,52 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* eslint-disable */ +const { withAppBuildGradle } = require('@expo/config-plugins'); + +const TAG = 'withCoreLibraryDesugaring'; + +const block = ` +// @generated begin ${TAG} +android { + compileOptions { + coreLibraryDesugaringEnabled true + } +} + +dependencies { + coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs_nio:2.0.4' +} +// @generated end ${TAG} +`; + +const withCoreLibraryDesugaring = (config) => { + return withAppBuildGradle(config, (config) => { + if (config.modResults.language !== 'groovy') { + throw new Error('Only Groovy build.gradle is supported.'); + } + + const contents = config.modResults.contents; + + if (!contents.includes(`// @generated begin ${TAG}`)) { + config.modResults.contents = `${contents.trimEnd()}\n\n${block}`; + } + + return config; + }); +}; + +module.exports = withCoreLibraryDesugaring; diff --git a/src/plugins/withJetifier.js b/src/plugins/withJetifier.js new file mode 100644 index 0000000..bf39a7d --- /dev/null +++ b/src/plugins/withJetifier.js @@ -0,0 +1,48 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* eslint-disable */ +const { withGradleProperties } = require('@expo/config-plugins'); + +/** + * Config Plugin for @googlemaps/react-native-navigation-sdk + * + * Enables Jetifier in android/gradle.properties to ensure compatibility + * with AndroidX when using the Google Navigation SDK. + */ +const withJetifier = (config) => { + return withGradleProperties(config, (c) => { + const props = c.modResults; + + const existing = props.find( + (p) => p.type === 'property' && p.key === 'android.enableJetifier' + ); + + if (existing) { + existing.value = 'true'; + } else { + props.push({ + type: 'property', + key: 'android.enableJetifier', + value: 'true', + }); + } + + return c; + }); +}; + +module.exports = withJetifier;