@iternio/react-native-auto-play
v0.8.1
Published
Android Auto and Apple CarPlay for react-native
Readme
React Native Auto Play
React Native Auto Play provides a comprehensive solution for integrating your React Native application with both Apple CarPlay and Android Auto. This library allows you to build automotive-specific user interfaces using familiar React Native components and concepts.
Features
- Cross-Platform: Write once, run on both Apple CarPlay and Android Auto.
- New Architecture: Supports React Native new architecture only.
- Template-Based UI: Utilize a rich set of templates like
MapTemplate,ListTemplate,GridTemplate, and more to build UIs that comply with automotive design guidelines. - Navigation APIs: Build full-featured navigation experiences with APIs for trip management, maneuvers, and route guidance.
- Dashboard & Cluster Support: Extend your app's presence to the CarPlay Dashboard (CarPlay only) and instrument cluster displays (CarPlay & Android Auto).
- Hooks-Based API: A modern and intuitive API using React Hooks (
useMapTemplate,useVoiceInput, etc.) for interacting with the automotive system. - Headless Operation: Runs in the background to keep the automotive experience alive even when the main app is not in the foreground.
- Powered by NitroModules
Requirements
- iOS builds require Xcode 27+ (the iOS 27 SDK), even for apps that don't use any
mapConfig/panel features — the library referencesCPMapPanel/CPPaneltypes internally behind@available(iOS 27.0, *)checks, but@availableonly defers runtime execution, not compile-time symbol resolution, so the SDK must be present to build at all. react-native-nitro-modules0.37.1 or newer — the generated native bindings target that runtime shape; an older nitro-modules install fails at native build time.
Installation
Install the package and its peer dependencies:
yarn add @iternio/react-native-auto-play react-native-nitro-modulesFor iOS, install the pods:
cd ios && pod install && cd ..For Android, the library will be autolinked.
Platform Setup
iOS
Bundle identifier
To get the CarPlay app showing up you need to set a proper Bundle Identifier:
- Open your app's
.xcodeprojin Xcode. - Select your app target, go to the Signing & Capabilities tab.
- Under Signing > Bundle Identifier, enter your unique bundle ID (e.g.,
at.g4rb4g3.autoplay.example).
Entitlements
Create a Entitlements.plist file in your project, paste the content below and adjust the com.apple.developer.carplay-maps key to your needs. Check Apple docs for details.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.developer.carplay-maps</key>
<true/>
<key>application-identifier</key>
<string>$(AppIdentifierPrefix)$(CFBundleIdentifier)</string>
</dict>
</plist>- Open
example.xcodeproj - Select the example target, go to the Build Settings tab and filter for entitlement.
- On Code Signing Entitlements enter the path to the Entitlements.plist file you just created.
Scene delegates
Depending on your needs you need to set up the scene delegates. The library brings following delegates:
- WindowApplicationSceneDelegate - The main scene visible on your mobile device
- HeadUnitSceneDelegate - The main scene on CarPlay device
- DashboardSceneDelegate - Scene visible on the CarPlay overview screen usualy with some other widgets like calendar, weather or music.
- ClusterSceneDelegate - Scene visible on a cars instrument cluster.
Paste this into your Info.plist and adjust it to your needs. Check Apple docs for details.
<key>UIApplicationSceneManifest</key>
<dict>
<key>CPSupportsDashboardNavigationScene</key>
<true/>
<key>CPSupportsInstrumentClusterNavigationScene</key>
<true/>
<key>UIApplicationSupportsMultipleScenes</key>
<true/>
<key>UISceneConfigurations</key>
<dict>
<key>CPTemplateApplicationDashboardSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneClassName</key>
<string>CPTemplateApplicationDashboardScene</string>
<key>UISceneConfigurationName</key>
<string>CarPlayDashboard</string>
<key>UISceneDelegateClassName</key>
<string>DashboardSceneDelegate</string>
</dict>
</array>
<key>CPTemplateApplicationInstrumentClusterSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneClassName</key>
<string>CPTemplateApplicationInstrumentClusterScene</string>
<key>UISceneConfigurationName</key>
<string>CarPlayCluster</string>
<key>UISceneDelegateClassName</key>
<string>ClusterSceneDelegate</string>
</dict>
</array>
<key>CPTemplateApplicationSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneClassName</key>
<string>CPTemplateApplicationScene</string>
<key>UISceneConfigurationName</key>
<string>CarPlayHeadUnit</string>
<key>UISceneDelegateClassName</key>
<string>HeadUnitSceneDelegate</string>
</dict>
</array>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneClassName</key>
<string>UIWindowScene</string>
<key>UISceneConfigurationName</key>
<string>WindowApplication</string>
<key>UISceneDelegateClassName</key>
<string>WindowApplicationSceneDelegate</string>
</dict>
</array>
</dict>
</dict>Deep links
WindowApplicationSceneDelegate forwards custom URL schemes and universal links to React Native's Linking API, so no extra AppDelegate or scene wiring is needed.
- App not running: the link is returned by
Linking.getInitialURL(). - App already running: the link is delivered as a
Linking'url'event. This includes the case where CarPlay launched the app and the phone UI is opened by a link afterwards. - Links that arrive before JS has called
getInitialURL()are kept and returned by that first call. Only the first call returns the link; later calls resolve tonull.
Call getInitialURL() once, as early as possible, and before you subscribe to 'url' events. If you subscribe first and call getInitialURL() later, a link that arrives in between is delivered by both, and your app handles it twice. Do not defer the call until a splash screen or sign-in has finished. Read the URL right away and keep it until your app is ready to act on it.
import { useEffect } from 'react';
import { Linking } from 'react-native';
function handleLink(url: string) {
// navigate, or store the URL until the app is ready
}
export function App() {
useEffect(() => {
// Read the initial URL first, then subscribe, in the same effect.
Linking.getInitialURL().then((url) => {
if (url) {
handleLink(url);
}
});
const subscription = Linking.addEventListener('url', ({ url }) => handleLink(url));
return () => subscription.remove();
}, []);
// ...
}React Navigation's linking prop already follows this order, so it needs no extra setup.
MapTemplate
if you want to make use of the MapTemplate and render react components you need to add this to your AppDelegate.swift This is an example that works for bare react-native (>= 0.82) and Expo SDK 57, check this for older versions.
@objc func getRootViewForAutoplay(
moduleName: String,
initialProperties: [String: Any]?
) -> UIView? {
var autoPlayRootView: UIView?
if let factory = reactNativeFactory?.rootViewFactory
as? ExpoReactRootViewFactory
{
autoPlayRootView = factory.superView(
withModuleName: moduleName,
initialProperties: initialProperties,
bundleConfiguration: RCTBundleConfiguration(),
devMenuConfiguration: RCTDevMenuConfiguration(),
)
}
if autoPlayRootView == nil,
let factory = reactNativeFactory?.rootViewFactory
{
autoPlayRootView = factory.view(
withModuleName: moduleName,
initialProperties: initialProperties
)
}
return autoPlayRootView
}MapTemplate maneuver dark & light mode
It is recommended to attach a listener to MapTemplate.onAppearanceDidChange and send maneuver updates based on this to make sure the colors are applied properly.
Reason for this is that CarPlay does not allow for color updates on maneuvers shown on the screen. You need to send maneuvers with a new id to get them updated properly on the screen.
The color properties do not need to handle the mode change, best practice is to use ThemedColor whenever possible and set appropriate light and dark mode colors.
This is mainly required on CarPlay. Android Auto redraws on its own when the day/night state changes, but note that Android Auto 17.8 introduced white templates in day mode while older versions always show dark templates. The car's day/night state (isDarkMode) is the same on both, so it does not tell you which template color you are drawn on. Use the 'default' color for icons that have to stay readable in both cases, see Icon colors and Android Auto light templates.
CPListTemplate day/night header
Known CarPlay platform bug, not fixable in this library: on ListTemplate (CPListTemplate) only, the entire header — title text and buttons alike — doesn't track live light/dark mode switches; each toggle flips it to the opposite of the actual current theme instead, until the template is popped and pushed again. Other templates work fine, this seems to be an iOS 26 issue only.
Dashboard buttons
In case you wanna open up your CarPlay app from one of the CarPlay dashboard buttons set launchHeadUnitScene on the button and add this to your Info.plist. Make sure to apply your "Bundle Identifier" instead of the example one.
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLSchemes</key>
<array>
<string>at.g4rb4g3.autoplay.example</string>
</array>
</dict>
</array>Android Auto
No platform specific setup required - we got you covered with all the required stuff.
ProGuard
In case you have ProGuard enabled (def enableProguardInReleaseBuilds = true in android/app/build.gradle), add the following rule to android/app/proguard-rules.pro:
-keep class com.margelo.nitro.swe.iternio.reactnativeautoplay.** { *; }Native backdrop under the MapTemplate surface
On Android the React content of a MapTemplate is rendered onto the car screen through a virtual display. Some native views cannot be hosted there as React Native views — Fragment-based map SDK wrappers, for example, are bound to the phone Activity. For those the library can place a host-provided native View under the React surface of a display: the Android counterpart of the iOS getRootViewForAutoplay hook above. Your React tree then draws on top of it as an overlay. The factory is asked for the root display and for each cluster display, and may answer null for either.
interface NativeBackdrop {
/** Added as the presentation root's FIRST child, match-parent. */
val view: View
/** The car's day/night changed (CarContext.isDarkMode) — redraw accordingly. */
fun onColorSchemeChanged(dark: Boolean)
/** Release everything; must be idempotent. */
fun destroy()
}
/** Which car display is asking for a backdrop. */
enum class NativeBackdropDisplay { ROOT, CLUSTER }
object NativeBackdropRegistry {
/** Return null to render that display without a backdrop. */
@Volatile
var factory: ((CarContext, NativeBackdropDisplay) -> NativeBackdrop?)? = null
}Register the factory in your Application.onCreate, before the CarAppService can start:
NativeBackdropRegistry.factory = { carContext, display ->
when (display) {
NativeBackdropDisplay.ROOT -> MyMapBackdrop(carContext)
NativeBackdropDisplay.CLUSTER -> null // or a second map view for the cluster
}
}Lifecycle contract:
- The factory is consulted once per presentation of each display — i.e. again after every surface resize — and each backdrop is destroyed when its presentation is replaced or the renderer stops. A
destroy()that throws is logged and does not interrupt teardown. - A factory that throws is logged and ignored; the React surface still renders.
- The React surface view is transparent only while a backdrop is attached. Without a registered factory nothing changes: the surface stays opaque as before.
onColorSchemeChanged(dark)is forwarded from each session'sonCarConfigurationChangedto that display's backdrop, so it can follow the car's day/night setting (car app quality guideline MR-1). It fires regardless of which template is currently on screen.
Android Auto Customization
You can customize certain behaviors of the library on Android Auto by setting properties in your app's android/gradle.properties file.
App Category: Declare which Android Auto / Automotive app type the
CarAppServiceadvertises — the Android counterpart to choosing your CarPlay scene type inInfo.plist.ReactNativeAutoPlay_androidAutoAppCategory=navigationSupported values are
navigation,poi,parking,charging,iot,messaging,calling, andweather(see the supported app categories); each maps toandroidx.car.app.category.<UPPERCASE>. An unrecognized value fails the build. The defaultnavigationkeeps the existing behavior. Any other value selects a lean manifest that declares the chosen category and omits the navigation-only permissions (NAVIGATION_TEMPLATES,MAP_TEMPLATES,ACCESS_SURFACE), theFEATURE_CLUSTERcategory, and theNAVIGATE/geointent filters — a non-navigation, no-map template app needs none of these, and declaring them gets the app reviewed against the navigation-app bar on the Play Store.On Android Automotive, the app-focus helpers on
HybridAndroidAutomotive(requestAppFocus,registerAppFocusListener,getAppFocusState) request/observe navigation focus specifically; they are meant for navigation apps and should not be used with a non-navigation category.Telemetry Update Interval: Control how often telemetry data is updated.
ReactNativeAutoPlay_androidTelemetryUpdateInterval=4000The value is in milliseconds. The default is
4000.UI Scale Factor: Apply a scaling factor to the React Native UI rendered on the car screen. This does not affect the templates.
ReactNativeAutoPlay_androidAutoScaleFactor=1.5fThe default value is
1.5.Cluster Splash Screen: Customize the splash screen shown on the instrument cluster.
# Delay in milliseconds after the root component is rendered before the splash screen hides. ReactNativeAutoPlay_clusterSplashDelayMs=1000 # Duration of the splash screen fade out animation in milliseconds. ReactNativeAutoPlay_clusterSplashDurationMs=500The default values are
1000for the delay and500for the duration.
Android Automotive
This library also supports Android Automotive. To enable Android Automotive support, you need to configure a few properties in your Android project.
minSdkVersion: The minimum API level for Android Automotive is 29. You must setminSdkVersionto at least29. For Android Auto, the minimum is24.isAutomotiveAppflag: You need to inform the library if this is an Automotive app by setting theisAutomotiveAppproperty totrue. For Android Auto, it should befalse.
You can set these properties directly in your android/gradle.properties file. Note the
ReactNativeAutoPlay_ prefix — the library reads rootProject.ext.<name> first and falls
back to the prefixed project property, so an unprefixed isAutomotiveApp=true in
gradle.properties is silently ignored and you get an Android Auto build instead:
# For Android Automotive
ReactNativeAutoPlay_minSdkVersion=29
ReactNativeAutoPlay_isAutomotiveApp=trueIf your app's android/build.gradle already defines ext.minSdkVersion (the React Native
template does), that rootProject.ext value wins over the property above — raise it there
instead.
Alternatively, if you need to support different build variants (e.g., for both Android Auto and Android Automotive from the same codebase), using react-native-config is the recommended approach.
Install
react-native-config:yarn add react-native-configCreate different
.envfiles for your variants. Create a default.envfor Android Auto:# .env (for Android Auto) isAutomotiveApp=false minSdkVersion=24And an
.env.automotivefor Android Automotive:# .env.automotive minSdkVersion=29 isAutomotiveApp=trueIn your
android/app/build.gradle, apply the configuration fromreact-native-configbased on your build flavors.// android/app/build.gradle project.ext.envConfigFiles = [ automotive: ".env.automotive", // other flavors... ] apply from: project(':react-native-config').projectDir.getPath() + "/dotenv.gradle" if (project.ext.has("env")) { rootProject.ext.minSdkVersion = project.ext.env.minSdkVersion rootProject.ext.isAutomotiveApp = project.ext.env.isAutomotiveApp }
Adjust to the build variants your app provides. Check the example app for details.
This approach allows you to dynamically set the required flags based on your build variant, which is demonstrated in the example app.
App launch on Android Automotive
Android Automotive requires you to remove your app activity since it invokes the libraries Android Auto service in a different way. Not doing so will bring up 2 app icons on the Android Automotive launcher. To get rid of your default activity, do an automotive specific build variant and add this AndroidManifest.xml for that variant.
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application>
<activity
android:name=".MainActivity"
tools:node="remove" /> <!-- remove main activity -->
</application>
</manifest>For details check the example app and its build variants.
Non-template Automotive app
The library can also be used for Android Automotive apps that do not make use of templates. In case you do a navigation app HybridAndroidAutomotive provides methods to monitor and request the app focus to let the system know your app is taking over the navigation focus. Do not use app focus related functions on template applications as it fights with the navigation manager used on the map template.
A Note on Android Studio
When using build variants, Android Studio may not be aware of the selected variant during a Gradle sync. This can cause the IDE to show the incorrect implementation of native classes like AndroidTelemetryObserver (e.g., it might show the Android Auto version instead of the Automotive version).
To work around this and allow for debugging or enhancing the Android Automotive-specific implementation, you can temporarily set the automotive flags in your gradle.properties file or your default .env file before running a Gradle sync.
Icons
The library does not bundle any icon font — the consuming app must provide one.
Setup
Add a
.ttffont file to your native projects:- iOS — add
<name>.ttfto your app bundle (noUIAppFontsentry needed — the library registers it via CoreText automatically). - Android — place
<name>.ttfinres/font/.
For cross-platform compatibility use lowercase names with underscores only (e.g.
material_symbols).- iOS — add
or
use expo-font
[ 'expo-font', { android: { fonts: [ { fontFamily: 'MaterialSymbols', fontDefinitions: [ { path: './assets/fonts/material_symbols.ttf', weight: 800, }, ], }, ], }, ios: ['./assets/fonts/material_symbols.ttf'], }, ],For cross-platform compatibility use lowercase names with underscores only (e.g.
material_symbols).Register the font and an optional glyph map at startup:
import { setIconFont } from '@iternio/react-native-auto-play'; import { glyphMap } from './assets/Glyphmap'; setIconFont('material_symbols', glyphMap);Use glyph images by name or code point:
{ type: 'glyph', name: 'directions_car' } { type: 'glyph', codepoint: 0xe531 }
setIconFont must be called once before the first glyph is used (subsequent calls are ignored). If no font is registered, the library throws an error when a glyph image is rendered.
Type-safe glyph names
To get autocompletion and type checking for glyph names, create a declaration file in your app (e.g. autoplay-glyphs.d.ts):
import type { GlyphName } from './assets/Glyphmap';
declare module '@iternio/react-native-auto-play' {
interface AutoPlayGlyphMap extends Record<GlyphName, number> {}
}Without this augmentation, name accepts any string. With it, only keys from your glyph map are allowed and you get full autocompletion.
The example app uses Material Symbols. See apps/example/assets/symbolFont/ for the glyph map generation script.
It is also possible to use custom bundled images (e.g. PNG, WEBP or Vector Drawables). Make sure to add them to your native projects.
- iOS: Add to your
Images.xcassets - Android: Add to
res/drawable
Icon colors and Android Auto light templates
Starting with Android Auto 17.8 the host can show white (light) templates in day mode. Older versions (e.g. 17.6) always use dark templates, even in day mode. Both report the same car app API level, so an app cannot tell them apart, and a fixed icon color that is readable on one (white on dark) can be invisible on the other (white on white).
Use the color 'default' for every monochrome icon that has to stay readable in both cases:
{ type: 'glyph', name: 'search', color: 'default' }
{ type: 'asset', image: require('./icon.png'), color: 'default' }
{ type: 'remote', uri: 'https://example.com/icon.png', color: 'default' }- Android Auto: the host tints the icon with its own default icon color for the template it is currently showing, so it follows dark and light templates on every Android Auto version.
- CarPlay:
'default'resolves to black in light mode and white in dark mode, so it is safe to use on iOS and does not change anything there. - Glyphs use
'default'automatically when nocoloris set. Exception on Android Auto: a glyph with a non-transparentbackgroundColoris not tinted, since the tint would recolor the background as well. It keeps the plain white (dark mode) / black (light mode) glyph color, so setcolorexplicitly if that does not contrast with your background. - Asset and remote images are not tinted unless you set a
color, so colorful images such as a logo keep their original colors. Only pass'default'for monochrome icons. - Any other color (a string or a
ThemedColor) is applied as specified. Only use those where the color works on both dark and light templates, e.g. a colored icon. - Known limitation: the host may not apply the tint to header action icons on Android Auto 17.8. That is an issue in Android Auto itself, not something the library can work around.
Usage
1. Register the AutoPlay Components
You need to register your AutoPlay components in your app's entry file (e.g., index.js). Import @iternio/react-native-auto-play/installTimers — a side-effect-only module that replaces the global setTimeout/setInterval/requestAnimationFrame (and their clear*/cancel* counterparts) with versions that keep running while CarPlay/Android Auto is actively driving the car screen, even if the phone itself is backgrounded or its screen is locked. React Native's own timers throttle or pause in that state regardless of whether the app process is actually still alive, which would otherwise stall ETA updates and telemetry polling. It must run before any other module has a chance to capture a reference to the original globals, which means it must be your entry file's first import — ES import declarations are hoisted and evaluated in source order, so it needs to come before everything else, including react-native itself:
// index.js
import '@iternio/react-native-auto-play/installTimers';
import { AppRegistry } from 'react-native';
import { name as appName } from './app.json';
import App from './src/App';
import registerAutoPlay from './src/AutoPlay'; // Your AutoPlay setup
AppRegistry.registerComponent(appName, () => App);
registerAutoPlay();2. Create the AutoPlay Experience
Create a file (e.g., src/AutoPlay.js) to define your automotive UI. This is where you will configure your templates and the React components they will render.
// src/AutoPlay.tsx
import {
AutoPlayCluster,
CarPlayDashboard,
HybridAutoPlay,
MapTemplate,
useMapTemplate,
} from '@iternio/react-native-auto-play';
import React, { useEffect } from 'react';
import { Platform, Text, View } from 'react-native';
// A simple component that can be reused across different screens
const MyCarScreen = ({ title }: { title: string }) => (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Text style={{ color: 'white', fontSize: 24 }}>{title}</Text>
</View>
);
// The React component to be rendered inside the MapTemplate
const MapScreen = () => {
const mapTemplate = useMapTemplate();
useEffect(() => {
// Show an alert on the car screen when the component mounts
mapTemplate?.showAlert({
id: 'welcome-alert',
title: { text: 'Welcome!' },
subtitle: { text: 'Your app is now running on the car screen.' },
durationMs: 5000,
priority: 'low',
});
}, [mapTemplate]);
return <MyCarScreen title="Hello, Map!" />;
};
const registerAutoPlay = () => {
const onConnect = () => {
// When a car is connected, create a MapTemplate and set it as the root
const rootTemplate = new MapTemplate({
component: MapScreen, // Render our map component
headerActions: {
android: [
{
type: 'image',
image: { name: 'search', type: 'glyph' },
onPress: () => console.log('Search pressed'),
},
{
type: 'image',
image: { name: 'cog', type: 'glyph' },
onPress: () => console.log('Settings pressed'),
},
],
ios: {
leadingNavigationBarButtons: [
{
type: 'image',
image: { name: 'search', type: 'glyph' },
onPress: () => console.log('Search pressed'),
},
],
trailingNavigationBarButtons: [
{
type: 'image',
image: { name: 'cog', type: 'glyph' },
onPress: () => console.log('Settings pressed'),
},
],
},
},
});
rootTemplate.setRootTemplate();
};
// Register components for the Dashboard and Cluster
if (Platform.OS === 'ios') {
CarPlayDashboard.setComponent(() => <MyCarScreen title="Hello, Dashboard!" />);
}
AutoPlayCluster.setComponent(() => <MyCarScreen title="Hello, Cluster!" />);
// Add listeners for car connection and disconnection events
HybridAutoPlay.addListener('didConnect', onConnect);
HybridAutoPlay.addListener('didDisconnect', () => {
console.log('Car disconnected');
});
};
export default registerAutoPlay;API Reference
Main Object
HybridAutoPlay: The primary interface for interacting with the native module, handling connection status and events.
Core Types
AutoText
Most text props accept AutoText so you can localize and provide variants. You can pass either a string or an object with text/variants as used throughout the example app.
AutoImage
Images are provided as AutoImage objects with a type and name. The built-in icon set is Material Symbols (see Icons). You can also use bundled images from your native project.
RootComponentInitialProps
All root components rendered by templates/scenes receive RootComponentInitialProps:
id: Module identifier (e.g.AutoPlayRoot,CarPlayDashboard, or a cluster UUID).rootTag: React Native root tag.colorScheme:'light' | 'dark'initial color scheme (listen toonAppearanceDidChangeonMapTemplatefor updates). On Android Auto this is the car's day/night state and does not tell you whether the templates are dark or white (17.8+ can show white templates in day mode, older versions never do).window:{ width, height, scale }.
Template Configs (Props)
Below is a concise overview of the most important props per template. Optional props are marked as optional. Required props are marked as required.
MapTemplateConfig
| Prop | Type | Required | Notes |
| --- | --- | --- | --- |
| component | React.ComponentType<RootComponentInitialProps> | ✅ | React component to render on the map surface. |
| onStopNavigation | (template: MapTemplate) => void | ✅ | Called when navigation is stopped by the system. |
| headerActions | MapHeaderActions<MapTemplate> | ❌ | Top action strip. See Header Actions below. |
| mapButtons | MapButtons<MapTemplate> | ❌ | 1–4 map buttons shown on the map. To get working gestures on the MapTemplate running on Android Auto you have to add a MapPanButton |
| visibleTravelEstimate | 'first' 'last' | ❌ | Which travel estimate to display. |
| optionsPanel | OptionsPanelConfig<MapTemplate> | ❌ | iOS 27+ only, no-op on Android. Panel shown when tapping the ellipsis button next to the travel estimates during active navigation. See Options Panel below. |
| onDidPan / onDidUpdateZoomGestureWithCenter | callbacks | ❌ | Map gesture events. |
| onAppearanceDidChange | (colorScheme) => void | ❌ | Listen for light/dark mode changes. |
| onAutoDriveEnabled | (template) => void | ⚠️ | Android-only auto drive callback. Make sure to take action when receiving this and simulate a drive to the set destination. Check Android docs for details |
ListTemplateConfig
| Prop | Type | Required | Notes |
| --- | --- | --- | --- |
| title | AutoText | ✅ | Header title. |
| sections | Section<ListTemplate> | ❌ | List sections/rows. Not providing anything here will result in a loading indicator on Android and an empty list on iOS. |
| headerActions | HeaderActions<ListTemplate> | ❌ | Header actions. See Header Actions below. |
| mapConfig | BaseMapTemplateConfig<ListTemplate> | ❌ | Android map-with-content layout. iOS 27+: renders as a CPMapPanel on the current root map template instead. See Map + Content below. |
GridTemplateConfig
| Prop | Type | Required | Notes |
| --- | --- | --- | --- |
| title | AutoText | ✅ | Header title. |
| buttons | GridButton<GridTemplate>[] | ✅ | Grid items. Providing an empty array will result in a loading indicator on Android and an empty template on iOS. |
| headerActions | HeaderActions<GridTemplate> | ❌ | Header actions. See Header Actions below. |
| imageSize | 'unset' 'large' 'medium' 'small' | ❌ | Android only, requires Android Car API 8. Controls grid item image size; defaults to unset (platform default layout). Ignored (with a __DEV__ warning) when mapConfig is also set — MapWithContentTemplate doesn't support the sized grid content type. |
| mapConfig | BaseMapTemplateConfig<GridTemplate> | ❌ | Android map-with-content layout. iOS 27+: renders as a CPMapPanel on the current root map template instead. See Map + Content below. |
SearchTemplateConfig
| Prop | Type | Required | Notes |
| --- | --- | --- | --- |
| title | AutoText | ✅ | Header title. |
| results | SearchSection<SearchTemplate> | ❌ | Initial results. |
| headerActions | HeaderActions<SearchTemplate> | ❌ | Header actions. See Header Actions below. |
| searchHint | string | ❌ | Android-only placeholder. |
| initialSearchText | string | ❌ | Android-only initial value. |
| onSearchTextChanged | (text) => void | ✅ | Fired on text input changes. |
| onSearchTextSubmitted | (text) => void | ✅ | Fired on submit. |
InformationTemplateConfig
| Prop | Type | Required | Notes |
| --- | --- | --- | --- |
| title | AutoText | ✅ | Header title. |
| items | InformationItems | ❌ | 1–4 rows. |
| actions | platform-specific | ❌ | Up to 2 buttons on Android, up to 3 on iOS. iOS 27+ with mapConfig set: at most 1 TextButton plus 1 icon-only ImageButton, enforced at the type level. |
| headerActions | HeaderActions<InformationTemplate> | ❌ | Header actions. See Header Actions below. |
| mapConfig | BaseMapTemplateConfig<InformationTemplate> | ❌ | Android map-with-content layout. iOS 27+: renders as a CPMapPanel on the current root map template instead. See Map + Content below. |
MessageTemplateConfig
| Prop | Type | Required | Notes |
| --- | --- | --- | --- |
| message | AutoText | ✅ | Main message text. |
| title | AutoText | ❌ | Android header title. |
| image | AutoImage | ❌ | Android-only image above the message. |
| actions | platform-specific | ❌ | Up to 2 buttons on Android, up to 3 on iOS. iOS 27+ with mapConfig set: at most 1 TextButton plus 1 icon-only ImageButton, enforced at the type level. |
| headerActions | HeaderActions<MessageTemplate> | ❌ | Header actions. See Header Actions below. iOS: ios only takes effect once this renders as a CPMapPanel (mapConfig set, iOS 27+) — without mapConfig (or below iOS 27) this is a full-screen CPAlertTemplate with no nav bar, so ios is silently unused. |
| mapConfig | BaseMapTemplateConfig<MessageTemplate> | ❌ | Android map-with-content layout. iOS 27+: renders as a CPMapPanel on the current root map template instead, trading the usual full-screen modal alert for panel content. See Map + Content below. |
SignInTemplateConfig (Android-only)
| Prop | Type | Required | Notes |
| --- | --- | --- | --- |
| signInMethod | SignInMethod | ✅ | The sign-in method configuration. Can be QRSignIn, PinSignIn, InputSignIn, GoogleSignIn. |
| title | string | ❌ | Header title. |
| additionalText | string | ❌ | Additional descriptive text. |
| instructions | string | ❌ | Sign-in Instructions text. |
| actions | ActionButton<SignInTemplate>[] | ❌ | Up to 2 buttons. |
| headerActions | SignInHeaderActions<SignInTemplate> | ❌ | Header actions. See Header Actions below. |
SignInMethod
| Method | Type | Notes |
| --- | --- | --- |
| QR | QrSignIn | QR Code sign in |
| PIN | PinSignIn | PIN Code sign in (1–12 characters) |
| Input | InputSignIn | Text sign in, for example mail/username and password |
| Google | GoogleSignIn | Sign in with Google. Requires Google Play services 23.08 or newer — check SignInTemplate.isGoogleSignInAvailable() first. |
GoogleSignIn takes a serverClientId (your Web OAuth client ID), a signInButtonText and a callback(error?, account?). On success the returned GoogleSignInAccount always carries both idToken and serverAuthCode (offline access, openid profile email scopes) for your backend. If the user cancels, or either the account picker or the authorization step fails, callback is called with an error and no account.
Sign in with Google needs a recent Play services. Constructing a SignInTemplate with GoogleSignIn throws playServicesUnavailable (and updateTemplate rejects with it) when Google Play services is missing or older than 23.08 (e.g. Android Automotive builds without GMS). Pick a different method up front instead:
const signInMethod: SignInMethod = SignInTemplate.isGoogleSignInAvailable()
? { method: SignInMethods.GOOGLE, serverClientId, signInButtonText: 'Sign in with Google', callback }
: { method: SignInMethods.QR, url: loginUrl };isGoogleSignInAvailable() always returns false on iOS.
For InputSignIn the keyboard and input fields can be configured with following properties:
KeyboardType enum: DEFAULT, EMAIL, PHONE, NUMBER
TextInputType enum: PASSWORD, DEFAULT
Header Actions (Important)
On Android, header actions may be omitted, although this is not recommended. If headerActions is undefined, the system automatically renders the app icon in the header. Because Android Auto enforces monochrome icons, this can result in a poor-looking button.
Use these rules to avoid crashes:
- For List/Grid/Information/Search/Message/SignIn templates on Android, always pass the structured object format (alignment is implicit via
startHeaderAction/endHeaderActions):
const headerActions: HeaderActions<MyTemplate> = {
android: {
startHeaderAction: { type: 'back', onPress: (t) => HybridAutoPlay.popTemplate() },
endHeaderActions: [
{ type: 'image', image: { name: 'help', type: 'glyph' }, onPress: () => {} },
],
},
ios: {
backButton: { type: 'back', onPress: (t) => HybridAutoPlay.popTemplate() },
trailingNavigationBarButtons: [
{ type: 'image', image: { name: 'close', type: 'glyph' }, onPress: () => {} },
],
},
};⚠️ Do not pass a raw array of actions to headerActions on Android for these templates. Arrays are only valid for MapTemplate header actions (see below). Passing an array for header-based templates results in actions without alignment and can crash on Android.
- For MapTemplate on Android, you can use the array format (1–4 actions) for the action strip:
const mapHeaderActions: MapTemplateConfig['headerActions'] = {
android: [
{ type: 'image', image: { name: 'list', type: 'glyph' }, onPress: () => {} },
{ type: 'image', image: { name: 'search', type: 'glyph' }, onPress: () => {} },
],
ios: {
leadingNavigationBarButtons: [
{ type: 'image', image: { name: 'list', type: 'glyph' }, onPress: () => {} },
],
},
};Header action shapes (structured overview):
| Platform | Property | Shape | Limits / Notes |
| --- | --- | --- | --- |
| Android (header templates) | headerActions.android | { startHeaderAction?, endHeaderActions? } | endHeaderActions: 1–2 buttons. startHeaderAction can be appIcon/back/custom. |
| Android (MapTemplate) | headerActions.android | ActionButton[] | 1–4 action strip buttons. |
| iOS | headerActions.ios | { backButton?, leadingNavigationBarButtons?, trailingNavigationBarButtons? } | Each list supports 1–2 buttons. backButton optional (system back is added if omitted). |
Actions & Button Types (Quick Reference)
- Android header actions use
startHeaderAction+endHeaderActions:startHeaderAction:AppButton | BackButton | ActionButtonendHeaderActions: 1–2 buttons- If
headerActionsis omitted: Android renders the app icon automatically.
- iOS header actions use:
backButton(optional, otherwise iOS provides a default back action)leadingNavigationBarButtons(1–2)trailingNavigationBarButtons(1–2)
- MapTemplate map buttons: 1–4 buttons, including the special
panbutton.
Event & Listener APIs
This section lists the available listeners and lifecycle callbacks so you can wire up connection state, visibility, cluster settings, and system events.
HybridAutoPlay listeners
| API | Payload | Notes |
| --- | --- | --- |
| HybridAutoPlay.addListener(event, cb) | event: 'didConnect' 'didDisconnect' | Connection changes for the head unit. |
| HybridAutoPlay.addListenerRenderState(moduleName, cb) | cb(visibility: 'willAppear' \| 'didAppear' \| 'willDisappear' \| 'didDisappear') | Use AutoPlayModules.* or a cluster UUID. |
| HybridAutoPlay.addListenerVoiceInput(cb) | cb(location?, query?) | Android-only. Fires when the OS triggers a voice input event (e.g. "Hey Google, navigate to…"). For in-app recording use startVoiceInput instead. |
| HybridAutoPlay.addSafeAreaInsetsListener(moduleName, cb) | cb(insets) | Safe area inset changes for any module. |
import { AutoPlayModules, HybridAutoPlay } from '@iternio/react-native-auto-play';
const cleanup = HybridAutoPlay.addListener('didConnect', () => {
console.log('Head unit connected');
});
const removeVisibility = HybridAutoPlay.addListenerRenderState(
AutoPlayModules.AutoPlayRoot,
(state) => console.log('AutoPlayRoot state', state)
);Template lifecycle callbacks
All templates accept these lifecycle callbacks in their config:
onWillAppear(animated?)onDidAppear(animated?)onWillDisappear(animated?)onDidDisappear(animated?)onPopped()(not supported on all iOS templates, see notes in code)
const template = new ListTemplate({
title: { text: 'Menu' },
onWillAppear: () => console.log('will appear'),
onPopped: () => console.log('popped forever'),
});MapTemplate callbacks
Map-specific callbacks live on MapTemplateConfig:
onDidPan({ x, y })onDidUpdateZoomGestureWithCenter({ x, y }, scale)onClick({ x, y })(Android)onDoubleClick({ x, y })(Android)onAppearanceDidChange(colorScheme)onAutoDriveEnabled(template)(Android)onStopNavigation(template)(required)
AutoPlayCluster listeners (instrument cluster)
| API | Payload | Notes |
| --- | --- | --- |
| AutoPlayCluster.addListenerColorScheme(cb) | (clusterId, colorScheme) | iOS + Android. |
| AutoPlayCluster.addListenerZoom(cb) | (clusterId, zoomEvent) | iOS only. |
| AutoPlayCluster.addListenerCompass(cb) | (clusterId, enabled) | iOS only. |
| AutoPlayCluster.addListenerSpeedLimit(cb) | (clusterId, enabled) | iOS only. |
const removeCompass = AutoPlayCluster.addListenerCompass((clusterId, enabled) => {
console.log('Cluster', clusterId, 'compass', enabled);
});CarPlayDashboard listeners (iOS)
| API | Payload | Notes |
| --- | --- | --- |
| CarPlayDashboard.addListener(event, cb) | event: 'didConnect' 'didDisconnect' | Connection changes for the dashboard scene. |
| CarPlayDashboard.addListenerRenderState(cb) | cb(visibility) | Scene visibility changes. |
| CarPlayDashboard.addListenerColorScheme(cb) | cb(colorScheme) | Light/dark changes. |
Localization
The library allows you to pass distances and durations and formats them according to the system defaults. For iOS make sure to provide all supported app languages in Info.plist CFBundleLocalizations for this to work properly, missing languages will use CFBundleDevelopmentRegion as fallback which is en most of the time. This results in a mix up with the region which might result in en_AT instead of de_AT for example.
Component Props
RootComponentInitialProps
Every component registered with a template (e.g., via MapTemplate's component prop) or a scene (e.g., CarPlayDashboard.setComponent) receives RootComponentInitialProps as its props. This object contains important information about the environment where the component is being rendered.
id: A unique identifier for the screen. Can beAutoPlayRootfor the main screen,CarPlayDashboardfor the dashboard, or a UUID for cluster displays.rootTag: The React Native root tag for the view.colorScheme: The initial color scheme ('dark'or'light'). Listen for changes with theonAppearanceDidChangeevent on the template.window: An object containing the dimensions and scale of the screen:width: The width of the screen.height: The height of the screen.scale: The screen's scale factor.
iOS Specific Properties:
On iOS, the component registered with AutoPlayCluster.setComponent receives additional props in its RootComponentInitialProps that indicate user preferences for the cluster display. You can also listen for changes to these settings.
compass: boolean: Indicates if the compass display is enabled by the user. The initial value is passed as a prop.speedLimit: boolean: Indicates if the speed limit display is enabled by the user. The initial value is passed as a prop.
You can listen for changes to these settings using listeners on the AutoPlayCluster object:
// Listen for compass setting changes
const compassCleanup = AutoPlayCluster.addListenerCompass((clusterId, isEnabled) => {
console.log(`Compass is now ${isEnabled ? 'enabled' : 'disabled'} for cluster ${clusterId}`);
});
// Listen for speed limit setting changes
const speedLimitCleanup = AutoPlayCluster.addListenerSpeedLimit((clusterId, isEnabled) => {
console.log(`Speed limit is now ${isEnabled ? 'enabled' : 'disabled'} for cluster ${clusterId}`);
});
// Don't forget to clean up the listeners when your component unmounts
useEffect(() => {
return () => {
compassCleanup();
speedLimitCleanup();
};
}, []);Templates
| Template | Purpose | Notes |
| --- | --- | --- |
| MapTemplate | Navigation, map rendering | Use as root; supports map buttons & navigation APIs. |
| ListTemplate | Lists/menus | Supports sections, radio/toggle rows. Can render as a CarPlay map panel, see Map + Content. |
| GridTemplate | Action grid | Use GridButton items. Can render as a CarPlay map panel, see Map + Content. |
| SearchTemplate | Search UI | Android-only search bar callbacks. |
| InformationTemplate | Info panels | Android uses PaneTemplate; iOS uses InformationTemplate. Can render as a CarPlay map panel, see Map + Content. |
| MessageTemplate | Modal messages | Always shown on top until popped (a true full-screen modal alert on iOS). Can render as a CarPlay map panel instead, see Map + Content. |
Template quick examples:
// MapTemplate
const map = new MapTemplate({
component: MapScreen,
onStopNavigation: () => {},
headerActions: { android: [{ type: 'image', image: { name: 'list', type: 'glyph' }, onPress: () => {} }] },
});
map.setRootTemplate();
// ListTemplate
new ListTemplate({
title: { text: 'Destinations' },
sections: [{ type: 'default', title: 'Recent', items: [{ type: 'default', title: { text: 'Home' }, onPress: () => {} }] }],
headerActions: { android: { startHeaderAction: { type: 'back', onPress: () => {} } } },
}).push();Map + Content (mapConfig)
ListTemplate, GridTemplate, InformationTemplate, and MessageTemplate all accept an optional mapConfig prop. Setting it (an empty object is enough — no actions need to be specified) gives the template a map background instead of its normal full-screen presentation. The two platforms implement this completely differently, so behavior and limitations differ accordingly.
new ListTemplate({
title: { text: 'Nearby' },
sections: [{ type: 'default', title: 'Stops', items: [{ type: 'default', title: { text: 'Charger' }, onPress: () => {} }] }],
mapConfig: {},
}).push();Android
mapConfig wraps the template in a MapWithContentTemplate, giving it a map background while the template's own content (list, grid, info, or message) is laid out on top.
iOS (27+)
mapConfig instead renders the template as a CPMapPanel — an overlay shown on the current root map template (a MapTemplate set via setRootTemplate()). On iOS versions below 27, mapConfig is currently a no-op and the template renders normally (there is no map-background equivalent pre-27).
// Root map template must already be set for the panel to have somewhere to attach to
new MapTemplate({ component: MapScreen, onStopNavigation: () => {} }).setRootTemplate();
// Pushing this on top now shows it as an overlay panel on the map, instead of a full-screen list.
// headerActions.ios.backButton is required here — as the first (and only) panel in the stack it
// gets no native close/back control (see "Things that behave differently in panel mode" below),
// so without it the driver has no way to leave the panel.
new ListTemplate({
title: { text: 'Nearby' },
sections: [{ type: 'default', title: 'Stops', items: [{ type: 'default', title: { text: 'Charger' }, onPress: () => {} }] }],
headerActions: { ios: { backButton: { type: 'back', onPress: () => HybridAutoPlay.popTemplate() } } },
mapConfig: {},
}).push();Panels share the same push/pop stack as regular templates — this is a library-level abstraction, not how Apple's API actually works. From your JS code's perspective, .push(), HybridAutoPlay.popTemplate()/popToRootTemplate()/popToTemplate(), and the lifecycle callbacks (onWillAppear, onDidAppear, etc.) behave the same whether the top of the stack is a panel or a regular pushed template — you can mix and pop through both without caring which is which. Natively, however, CPMapPanel is not part of CPInterfaceController's template stack at all — Apple's API gives it its own, completely separate panel stack that lives on the CPMapTemplate that pushed it (pushPanel/popPanel/CPMapPanelDelegate, unrelated to CPInterfaceController.pushTemplate/popTemplate). This library tracks both stacks together internally and presents one unified stack to JS, so if you go looking at Apple's CarPlay documentation expecting to see panels integrated with CPInterfaceController, you won't find it there — that integration is something this library provides on top.
Things that behave differently in panel mode:
headerActions/mapButtonsownership: while a panel is shown, it takes over the root map template's bar buttons and floating map buttons — using the panel template's ownheaderActions/mapConfig.mapButtons, notmapConfig.headerActions(which is Android-only; on iOS it's ignored, since there's no separate header for the map behind a panel). The map template's own buttons are restored automatically once the panel is popped.- The first panel must provide its own way to be closed: CarPlay's native close button (✕) is always disabled on every panel — this library turns it off globally, and this is required for correct lifecycle tracking, not a style choice. Tapping ✕ on the topmost panel doesn't just pop that one panel — it discards the entire panel stack down to the map, covered panels included — but
CPMapPanelDelegate.panelDidHideonly ever fires once, for the topmost panel. This library would have no callback at all for the covered panels CarPlay silently destroyed underneath it: they'd stay tracked forever,onPoppedwould never fire for them, and their listeners/native templates would leak. The back chevron doesn't have this problem — it only ever pops one level, always the topmost panel — so it's left enabled: once a second panel is pushed, CarPlay shows it automatically to return to the first, and it isn't customizable. The first panel in the stack gets no such control, though, so it needs its own way out:headerActions.ios.backButton(supported by all four panel-capable templates, includingMessageTemplateoncemapConfigis set) or something inside the panel's own content (a list item, orMessageTemplate's requiredactions.ios[0]TextButton) that callspopTemplate()/popToRootTemplate(). Without one, the driver has no way to leave that first panel short ofautoDismissMs. InformationTemplate/MessageTemplateactions: aCPMapPanel's button configuration only supports oneTextButton(with a title) plus one optional icon-onlyImageButton(any title on it is dropped natively) — far fewer than the up-to-3-TextButtonshape available withoutmapConfig. The type system enforces this:actions.iosis restricted to[TextButton]or[TextButton, ImageButton]whenevermapConfigis set.MessageTemplatestops being a true modal: normallyMessageTemplateis a full-screen, blocking alert (CPAlertTemplate) that covers everything regardless of OS version. WithmapConfigset, it instead becomes dismissible panel content in the regular push/pop stack — a deliberate trade-off, not a partial implementation.
Known iOS 27 beta limitations (not something fixable in this library — re-test against newer betas):
- The optional icon-only
symbolButtonin a panel's button configuration does not appear to respond to taps at all on this beta — the button renders correctly, but its press handler is never invoked by CarPlay. togglerow accessory images render noticeably smaller inside a panel than in a regular (non-panel)ListTemplate— this is how Apple sizesCPListItem.accessoryImageon panels specifically, not something this library controls (see theCPListItem.accessoryImageknown issue under Options Panel for the same underlying sizing bug's non-panel form).
Waypoint Rows (type: 'waypoint')
Any list section (ListTemplate.sections, or an OptionsPanel list section — see below) can include a waypoint row alongside the usual default/toggle/radio/text rows:
{
type: 'waypoint',
title: { text: 'Supercharger' },
address: 'Main St 1\n1234 Springfield',
coordinate: { latitude: 48.2, longitude: 16.37 },
travelEstimates: {
distance: { unit: 'kilometers', value: 12 },
duration: { timezone: 'Europe/Vienna', seconds: 600 },
visible: true,
},
image: { type: 'glyph', name: 'pin_drop' },
onPress: () => {},
}iOS 27+ inside a CPMapPanel (i.e. the enclosing ListTemplate/GridTemplate has mapConfig set, or this row is part of an OptionsPanel list section): renders as a real CPMapTemplateWaypoint item — title becomes the name, address the address, image the leading image (see the known-issue note below on image sizing). travelEstimates.distance/.duration are always sent to the native waypoint object (CarPlay requires them structurally), but they're not shown by the waypoint item itself — set travelEstimates.visible: true to additionally insert a sibling native CPTravelEstimates row right after it. This is a static snapshot, not live-updating — re-set distance/duration yourself (e.g. via updateSections/updateOptionsPanel) if it needs to track a changing location; there's no lighter-weight update path for just this value today.
Everywhere else (non-panel ListTemplate, Android, iOS < 27): falls back to a plain row, using title as the row title and address as the detail text — travelEstimates.visible has no effect here. Instead, reference TextPlaceholders.Distance/TextPlaceholders.Duration inside title.text/address yourself and this library fills them in automatically (the same substitution mechanism AutoText.distance/.duration already do everywhere):
{
type: 'waypoint',
title: { text: `Supercharger (${TextPlaceholders.Distance})` },
address: `Main St 1 · ${TextPlaceholders.Duration} away`,
travelEstimates: { distance: { unit: 'kilometers', value: 12 }, duration: { timezone: 'Europe/Vienna', seconds: 600 } },
coordinate: { latitude: 48.2, longitude: 16.37 },
onPress: () => {},
}Options Panel (optionsPanel, iOS 27+)
MapTemplate's optionsPanel prop configures the panel CarPlay shows when the user taps the ellipsis button next to the travel estimates during active navigation. It's a no-op on Android and on iOS below 27.
mapTemplate.updateOptionsPanel({
title: { text: 'Trip options' },
sections: [
{
type: 'list',
title: 'Route',
items: [{ type: 'default', title: { text: 'Avoid tolls' }, onPress: () => {} }],
},
{
type: 'charger',
title: 'Charger',
location: {
name: 'Fast Network Inc.',
address: 'Main St 1',
coordinate: { latitude: 48.2, longitude: 16.37 },
travelEstimates: {
distance: { unit: 'kilometers', value: 12 },
duration: { timezone: 'Europe/Vienna', seconds: 600 },
visible: true,
},
onPress: () => {},
},
outlets: [{ connector: 'ccs2', voltage: 400, powerKw: 300, onPress: () => {} }],
},
],
});A section is one of:
| type | Renders as | Notes |
| --- | --- | --- |
| 'list' | Rows (default/toggle/radio/text/waypoint) | Same row types and behavior as a regular ListTemplate section. |
| 'grid' | A row of GridButtons | Same shape as GridTemplate.buttons. |
| 'charger' | One CPChargingStationConnection item per outlet | outlets[].connector is one of ccs1/ccs2/j1772/chaDeMo/mennekes/gbtDC/gbtAC/nacsDC/nacsAC; powerKw above 1000 is shown in MW natively. location is optional and behaves exactly like a waypoint row's panel behavior above (own CPMapTemplateWaypoint item, travelEstimates.visible for the sibling estimate row) — except it uses location.name instead of a title, since the section's own title is already shown as the header (repeating it on the item would look redundant). |
Known iOS 27 beta issues affecting waypoint/options-panel content (not fixable in this library — re-test against newer betas; each was confirmed by direct testing, several already have Apple Feedback reports filed):
- Custom (non-system) images are unreliable across several of these newer panel APIs. A
waypointrow's/ChargerLocation's glyphimageoverflows atCPNavigationAlert.maximumAvatarImageSizeon iOS 27 — worked around by dividing the requested size bytraitCollection.displayScale, which fixes the overflow but introduces some blur (a real tradeoff, not a full fix). Non-glyph custom images have no known-good size at all — everything from explicit point sizes to real customUIImage.isSymbolImageassets was tried without a reliable, correctly-sized result; only genuine system symbols (UIImage(systemName:)) size correctly there. Expectimageon a waypoint/charger row to render, but not necessarily at a sensible or crisp size. CPListItem.accessoryImage(used fortogglerows) renders at some fixed, undersized footprint on iOS 27, regardless of the image's content, size, scale, or whether it's a real symbol image — confirmed via extensive testing (content proportions, render scale, post-hoc scale metadata, genuineUIImage.isSymbolImageassets from both the app's own bundle and a library-owned resource bundle). Reproduces on a plain (non-panel)ListTemplatetoo, so it isn't specific to panels or to this library's usage of the API. No workaround found; filed as Apple Feedback.
Voice Input
The library provides a cross-platform in-app voice recording API built on top of the car microphone (when connected) or the device microphone (when no car is connected). The voice API lives in HybridVoice.
Permission
import { HybridVoice } from '@iternio/react-native-auto-play';
// Check whether permission is already granted (synchronous)
const granted = HybridVoice.hasVoiceInputPermission();
// Request permission if not yet granted
const granted = await HybridVoice.requestVoiceInputPermission();On iOS: checks/requests both microphone and speech recognition authorization.
On Android: checks/requests RECORD_AUDIO via the car context when connected, otherwise via the RN application context.
Recording
import { HybridVoice, ErrorUtil } from '@iternio/react-native-auto-play';
try {
const result = await HybridVoice.startVoiceInput({
silenceThresholdMs: 1500, // ms of silence before auto-stop (default 1500)
maxDurationMs: 10_000, // hard cap on recording duration (default 10 000)
listeningText: 'Listening…', // iOS CarPlay: text shown on CPVoiceControlTemplate
preferSpeechToText: false, // true → STT transcription; false → raw PCM (default)
startSound: require('./beep_start.mp3'), // played just before recording starts
endSound: require('./beep_end.mp3'), // played just after recording stops
onChunk: (chunk) => {
// chunk.audio — raw PCM ArrayBuffer chunk (PCM mode)
// chunk.partial — partial transcription string (STT mode)
},
});
if (result.transcription) {
console.log('Transcription:', result.transcription);
} else if (result.audio) {
console.log(`PCM audio: ${result.audio.byteLength} bytes`);
}
} catch (e) {
if (ErrorUtil.isVoiceInputCanceledError(e)) {
// User pressed the cancel button on the car screen
console.log('Voice input cancelled');
} else {
console.error(e);
}
}
// Stop recording early — resolves startVoiceInput with audio captured so far
HybridVoice.stopVoiceInput();| Option | Type | Default | Description |
|---|---|---|---|
| silenceThresholdMs | number | 1500 | Auto-stop after this many ms of silence |
| maxDurationMs | number | 10000 | Hard recording time limit |
| listeningText | string | — | iOS only — text shown on CPVoiceControlTemplate |
| listeningImage | VoiceInputImage | — | iOS only — animated image in the CarPlay overlay |
| preferSpeechToText | boolean | false | true → resolve with { transcription }; false → resolve with { audio } |
| startSound | number | — | Metro asset (require('./beep.mp3')) played before recording. Takes audio focus so other apps pause. |
| endSound | number | — | Metro asset played after recording stops |
| onChunk | (chunk) => void | — | Streaming callback: chunk.audio (PCM) or chunk.partial (STT) |
| language | string | system | BCP-47 language tag for the STT recognizer |
PCM result (preferSpeechToText: false, default): resolves with { audio: ArrayBuffer } — raw 16 kHz, 16-bit, mono PCM.
STT result (preferSpeechToText: true): resolves with { transcription: string } on success, or falls back to { audio } if recognition is unavailable.
On Android: uses CarAudioRecord when Android Auto is connected, otherwise falls back to standard AudioRecord. STT uses SpeechRecognizer.
On iOS: presents CPVoiceControlTemplate on the car screen when CarPlay is connected, and captures audio via AVAudioEngine. STT uses SFSpeechRecognizer.
Cancel detection
When the user presses the cancel button on the car screen, startVoiceInput rejects with a voiceInputCancelled error on both platforms. Use ErrorUtil.isVoiceInputCanceledError to distinguish it from other errors:
import { ErrorUtil } from '@iternio/react-native-auto-play';
HybridVoice.startVoiceInput().catch((e) => {
if (ErrorUtil.isVoiceInputCanceledError(e)) {
// user dismissed — no action needed
} else {
throw e;
}
});OS-triggered voice input (Android only)
addListenerVoiceInput fires when the OS itself initiates a voice action (e.g. "Hey Google, navigate to…"). It is a no-op on iOS — use startVoiceInput for in-app recording on both platforms.
const cleanup = HybridAutoPlay.addListenerVoiceInput((location, query) => {
console.log('Voice query:', query, 'near', location);
});useVoiceInput hook (Android only)
A convenience hook that wires up addListenerVoiceInput and exposes the latest location and query values reactively.
import { useVoiceInput } from '@iternio/react-native-auto-play';
const MyScreen = () => {
const { location, query } = useVoiceInput();
return <Text>{query ?? 'Say something…'}</Text>;
};Hooks
useMapTemplate(): Get a reference to the parentMapTemplateinstance.useVoiceInput()
