capacitor-google-navigation
v0.2.2
Published
Google maps turn by turn navigation for capcitor
Maintainers
Readme
capacitor-google-navigation
A Capacitor 8 plugin for Google Navigation SDK — turn-by-turn navigation inside your iOS and Android app.
Native only. This plugin has no web implementation. It must be run on a physical or emulated iOS/Android device.
Requirements
Google Cloud Console
- Open or create a project at console.cloud.google.com
- Enable the Navigation SDK API
- Enable billing on your project
- Create an API key under APIs & Services → Credentials
iOS
- iOS 15.0+
- Xcode 14+
- CocoaPods — Swift Package Manager is not supported (Google Navigation SDK has no SPM distribution)
Android
- Android API 24 (Android 7.0)+
- Google Play Services on the device
Installation
npm install capacitor-google-navigation
npx cap synciOS Setup
1. Install pods
cd ios/App
pod installThe GoogleNavigation ~> 9.0 pod is declared in the plugin's podspec and is pulled in automatically.
2. Add location permissions to Info.plist
<key>NSLocationWhenInUseUsageDescription</key>
<string>This app uses your location for turn-by-turn navigation.</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>This app uses your location for navigation, including in the background.</string>3. Register your API key
Production (recommended)
Store the key in Info.plist under the key GoogleNavigationAPIKey. The plugin reads it automatically — no key needs to be passed from JS.
<key>GoogleNavigationAPIKey</key>
<string>YOUR_IOS_API_KEY</string>Then call initialize() with no key:
await GoogleNavigation.initialize({});Keep the key out of source control by injecting it at build time via an .xcconfig file:
// Config.xcconfig (gitignored)
GOOGLE_NAV_API_KEY = AIzaSy...<!-- Info.plist -->
<key>GoogleNavigationAPIKey</key>
<string>$(GOOGLE_NAV_API_KEY)</string>Development only
You can pass the key directly from JS for quick local testing. Do not ship this in production — the key will be visible in your compiled JS bundle.
await GoogleNavigation.initialize({ apiKey: 'YOUR_IOS_API_KEY' });Restrict your API key
In Google Cloud Console → APIs & Services → Credentials, add an iOS app restriction with your app's bundle ID. This ensures the key is rejected if extracted and used outside your app.
Android Setup
1. Add the API key and permissions to android/app/src/main/AndroidManifest.xml
The Android Navigation SDK reads the key directly from the manifest at startup — it is never passed from JS.
<manifest>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<application>
<meta-data
android:name="com.google.android.geo.API_KEY"
android:value="${GOOGLE_NAV_API_KEY}" />
</application>
</manifest>Keep the key out of source control using gradle.properties (gitignored):
# android/gradle.properties (gitignored)
GOOGLE_NAV_API_KEY=AIzaSy...Then in android/app/build.gradle, expose it to the manifest:
android {
defaultConfig {
manifestPlaceholders = [GOOGLE_NAV_API_KEY: project.findProperty("GOOGLE_NAV_API_KEY") ?: ""]
}
}Restrict the key in Google Cloud Console by adding your app's SHA-1 certificate fingerprint and package name under Android app restrictions.
Important: On Android the Navigation SDK reads the API key from
AndroidManifest.xml— not from theapiKeyparameter passed toinitialize(). TheapiKeyparameter is used on iOS only.
2. Request location permission at runtime
The plugin does not request permissions itself. You must request ACCESS_FINE_LOCATION before calling initialize(). In an Ionic React app use @capacitor/geolocation or the browser Permissions API:
import { Geolocation } from '@capacitor/geolocation';
await Geolocation.requestPermissions();How it works
Calling showNavigationView({ show: true }) presents a full-screen native navigation UI on top of your app. Your Ionic/web UI stays alive underneath. Calling showNavigationView({ show: false }) dismisses the native view and restores your app UI.
Call order:
initialize()
↓ fires onNavigationReady when SDK is ready
showNavigationView({ show: true })
↓ presents native map full-screen
startNavigation({ lat, lng, travelMode })
↓ sets destination, begins guidance
↓ fires onArrival when user arrives
↓ fires onRouteChanged on recalculation
stopNavigation()
↓ ends guidance, clears destination
showNavigationView({ show: false }) — or user taps the ✕ close button
↓ dismisses native map, fires onNavigationClosed, restores app UIUsage — Ionic React
1. Create a useGoogleNavigation hook
// src/hooks/useGoogleNavigation.ts
import { useEffect, useRef, useCallback } from 'react';
import { GoogleNavigation } from 'capacitor-google-navigation';
import type { PluginListenerHandle } from 'capacitor-google-navigation';
interface UseNavigationOptions {
apiKey: string;
onArrival?: (event: any) => void;
onRouteChanged?: () => void;
onNavigationClosed?: () => void;
}
export function useGoogleNavigation({ apiKey, onArrival, onRouteChanged, onNavigationClosed }: UseNavigationOptions) {
const listeners = useRef<PluginListenerHandle[]>([]);
useEffect(() => {
const setup = async () => {
const readyHandle = await GoogleNavigation.addListener('onNavigationReady', () => {
console.log('Navigation SDK ready');
});
listeners.current.push(readyHandle);
if (onArrival) {
const h = await GoogleNavigation.addListener('onArrival', onArrival);
listeners.current.push(h);
}
if (onRouteChanged) {
const h = await GoogleNavigation.addListener('onRouteChanged', onRouteChanged);
listeners.current.push(h);
}
if (onNavigationClosed) {
const h = await GoogleNavigation.addListener('onNavigationClosed', onNavigationClosed);
listeners.current.push(h);
}
await GoogleNavigation.initialize({ apiKey });
};
setup().catch(console.error);
return () => {
listeners.current.forEach(h => h.remove());
listeners.current = [];
};
}, [apiKey]);
const navigate = useCallback(async (
latitude: number,
longitude: number,
travelMode: 'DRIVING' | 'WALKING' | 'CYCLING' | 'TWO_WHEELER' = 'DRIVING',
) => {
await GoogleNavigation.showNavigationView({ show: true });
await GoogleNavigation.startNavigation({
destinationLatitude: latitude,
destinationLongitude: longitude,
travelMode,
});
}, []);
const stop = useCallback(async () => {
await GoogleNavigation.stopNavigation();
await GoogleNavigation.showNavigationView({ show: false });
}, []);
return { navigate, stop };
}2. Use the hook in a page
// src/pages/NavigationPage.tsx
import React from 'react';
import {
IonPage,
IonHeader,
IonToolbar,
IonTitle,
IonContent,
IonButton,
IonAlert,
} from '@ionic/react';
import { useGoogleNavigation } from '../hooks/useGoogleNavigation';
const NavigationPage: React.FC = () => {
const [showArrival, setShowArrival] = React.useState(false);
const { navigate, stop } = useGoogleNavigation({
apiKey: import.meta.env.VITE_GOOGLE_NAV_API_KEY as string,
onArrival: () => setShowArrival(true),
onRouteChanged: () => console.log('Route recalculated'),
onNavigationClosed: () => console.log('User closed navigation'),
});
return (
<IonPage>
<IonHeader>
<IonToolbar>
<IonTitle>Navigation</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent className="ion-padding">
<IonButton
expand="block"
onClick={() => navigate(37.7749, -122.4194, 'DRIVING')}
>
Navigate to San Francisco
</IonButton>
<IonButton expand="block" color="medium" onClick={() => navigate(34.0522, -118.2437, 'WALKING')}>
Walk to Los Angeles
</IonButton>
<IonButton expand="block" color="danger" onClick={stop}>
Stop Navigation
</IonButton>
</IonContent>
<IonAlert
isOpen={showArrival}
header="Arrived!"
message="You have reached your destination."
buttons={['OK']}
onDidDismiss={() => setShowArrival(false)}
/>
</IonPage>
);
};
export default NavigationPage;3. Store your API key in .env
Create a .env file in your app root (never commit this):
VITE_GOOGLE_NAV_API_KEY=AIzaSy...Usage — Vanilla TypeScript / JavaScript
import { GoogleNavigation } from 'capacitor-google-navigation';
// 1. Attach event listeners first
const readyHandle = await GoogleNavigation.addListener('onNavigationReady', () => {
console.log('SDK ready');
});
const arrivalHandle = await GoogleNavigation.addListener('onArrival', (event) => {
console.log('Arrived:', event);
});
const routeHandle = await GoogleNavigation.addListener('onRouteChanged', () => {
console.log('Route recalculated');
});
const closedHandle = await GoogleNavigation.addListener('onNavigationClosed', () => {
// Fired when the user taps the ✕ close button on the native navigation view
console.log('Navigation closed by user');
});
// 2. Initialize the SDK (fires onNavigationReady when done)
await GoogleNavigation.initialize({ apiKey: 'YOUR_API_KEY' });
// 3. Show the native navigation view
await GoogleNavigation.showNavigationView({ show: true });
// 4. Start navigation
await GoogleNavigation.startNavigation({
destinationLatitude: 37.7749,
destinationLongitude: -122.4194,
travelMode: 'DRIVING', // DRIVING | WALKING | CYCLING | TWO_WHEELER
});
// 5. Stop guidance and dismiss
await GoogleNavigation.stopNavigation();
await GoogleNavigation.showNavigationView({ show: false });
// 6. Clean up
await readyHandle.remove();
await arrivalHandle.remove();
await routeHandle.remove();
// or:
await GoogleNavigation.removeAllListeners();API
initialize(...)
initialize(options: { apiKey: string; }) => Promise<{ success: boolean; }>Initialize the Navigation SDK with API key
| Param | Type |
| ------------- | -------------------------------- |
| options | { apiKey: string; } |
Returns: Promise<{ success: boolean; }>
startNavigation(...)
startNavigation(options: { destinationLatitude: number; destinationLongitude: number; travelMode?: 'DRIVING' | 'WALKING' | 'CYCLING' | 'TWO_WHEELER'; }) => Promise<{ success: boolean; }>Start navigation to a destination
| Param | Type |
| ------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| options | { destinationLatitude: number; destinationLongitude: number; travelMode?: 'DRIVING' | 'WALKING' | 'CYCLING' | 'TWO_WHEELER'; } |
Returns: Promise<{ success: boolean; }>
stopNavigation()
stopNavigation() => Promise<{ success: boolean; }>Stop navigation
Returns: Promise<{ success: boolean; }>
showNavigationView(...)
showNavigationView(options: { show: boolean; }) => Promise<{ success: boolean; }>Show/hide navigation view
| Param | Type |
| ------------- | -------------------------------- |
| options | { show: boolean; } |
Returns: Promise<{ success: boolean; }>
addListener(...)
addListener(eventName: 'onArrival' | 'onRouteChanged' | 'onNavigationReady' | 'onNavigationClosed', listenerFunc: (event: any) => void) => Promise<PluginListenerHandle>Add listener for navigation events
| Param | Type |
| ------------------ | ----------------------------------------------------------------- |
| eventName | 'onArrival' | 'onRouteChanged' | 'onNavigationReady' | 'onNavigationClosed' |
| listenerFunc | (event: any) => void |
Returns: Promise<PluginListenerHandle>
removeAllListeners()
removeAllListeners() => Promise<void>Remove all listeners
Interfaces
PluginListenerHandle
| Prop | Type |
| ------------ | ----------------------------------------- |
| remove | () => Promise<void> |
Events
| Event | Payload | Fired when |
|-------|---------|-----------|
| onNavigationReady | {} | SDK has initialized and the navigator is available |
| onArrival | { latitude, longitude, title } | User arrives at the destination waypoint |
| onRouteChanged | {} | The route is recalculated (traffic, missed turn, etc.) |
| onNavigationClosed | {} | User tapped the ✕ close button on the native navigation view (iOS) |
Platform notes
| Feature | iOS | Android |
|---------|-----|---------|
| Turn-by-turn guidance | ✅ | ✅ |
| Full-screen native UI | ✅ | ✅ |
| onArrival event | ✅ | ✅ |
| onRouteChanged event | ✅ | ✅ |
| API key via initialize() | ✅ | ⚠️ Manifest only |
| Web | ❌ | ❌ |
| Swift Package Manager | ❌ | — |
Troubleshooting
showNavigationView must be called before startNavigation
The navigator instance is only available after the native navigation view is presented. Calling startNavigation without first calling showNavigationView({ show: true }) will return an error.
Blank map on Android
All NavigationView lifecycle events must be delegated — the plugin handles this via NavigationFragment. If you see a blank map, check that your Activity extends AppCompatActivity (Capacitor does this by default).
initialize() fails on Android
The Android Navigation SDK validates the API key from AndroidManifest.xml. Ensure the key is present and the Navigation SDK is enabled in your Google Cloud project with billing active.
iOS — "This app has attempted to access privacy-sensitive data"
Add both NSLocationWhenInUseUsageDescription and NSLocationAlwaysAndWhenInUseUsageDescription to Info.plist before calling initialize().
iOS — App crashes with "Invalid parameter not satisfying: CLClientIsBackgroundable" The Navigation SDK requires background location capability to track position during guidance. In Xcode:
- Select your app target → Signing & Capabilities → + Capability → Background Modes
- Check Location updates
Also ensure all three keys are present in Info.plist:
<key>NSLocationWhenInUseUsageDescription</key>
<string>This app uses your location for navigation.</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>This app uses your location for navigation, including in the background.</string>
<key>NSLocationAlwaysUsageDescription</key>
<string>This app uses your location for navigation, including in the background.</string>iOS — "This application has been blocked by the Google Navigation SDK" The Navigation SDK requires explicit enrollment — it is not available to all Google Cloud projects by default. Ensure:
- The Navigation SDK for iOS is enabled under APIs & Services → Library in Google Cloud Console
- Your project has been granted access (you may need to request it via the Navigation SDK get started page)
- Billing is active on the project
Android — Duplicate class build error
The Navigation SDK bundles Maps SDK and Location classes internally. Do not add play-services-maps or play-services-location as separate dependencies in your app or any plugin — they will conflict with the classes already inside the Navigation SDK AAR and cause a dex merge failure.
If you see an error like:
Duplicate class com.google.android.gms.maps.* found in modules ...Check android/app/build.gradle and any plugin build.gradle files and remove any explicit play-services-maps or play-services-location dependencies.
Android — core library desugaring build error
The Navigation SDK requires core library desugaring to be enabled in the consuming app. If you see:
Dependency 'com.google.android.libraries.navigation:navigation:x.x.x' requires
core library desugaring to be enabled for :app.Add the following to your app's android/app/build.gradle:
android {
compileOptions {
coreLibraryDesugaringEnabled true
}
}
dependencies {
coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:2.1.4"
}CocoaPods not found / pod install fails
Make sure CocoaPods is installed (sudo gem install cocoapods) and run npx cap sync before pod install.
iOS — cap sync fails with "transitive dependencies that include statically linked binaries"
The GoogleNavigation SDK is distributed as a static XCFramework, which conflicts with the default dynamic use_frameworks! directive. In your app's ios/App/Podfile, change:
use_frameworks!to:
use_frameworks! :linkage => :staticThen re-run pod install and npx cap sync.
License
MIT
