@iternio/react-native-auto-play 0.5.14-beta.1 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (201) hide show
  1. package/README.md +270 -36
  2. package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/AndroidAutoSession.kt +5 -0
  3. package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/HybridMapTemplate.kt +9 -2
  4. package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/NativeBackdrop.kt +40 -0
  5. package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/VirtualRenderer.kt +52 -0
  6. package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/template/GridTemplate.kt +47 -13
  7. package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/template/Parser.kt +69 -15
  8. package/ios/Types.swift +3 -0
  9. package/ios/extensions/UIImage+Toggle.swift +5 -2
  10. package/ios/hybrid/HybridAutoPlay.swift +28 -16
  11. package/ios/hybrid/HybridMapTemplate.swift +22 -5
  12. package/ios/scenes/AutoPlayInterfaceController.swift +261 -72
  13. package/ios/templates/AutoPlayMapPanelDelegate.swift +190 -0
  14. package/ios/templates/AutoPlayTemplate.swift +24 -2
  15. package/ios/templates/GridTemplate.swift +57 -57
  16. package/ios/templates/InformationTemplate.swift +84 -9
  17. package/ios/templates/ListTemplate.swift +66 -11
  18. package/ios/templates/MapTemplate.swift +83 -68
  19. package/ios/templates/MessageTemplate.swift +72 -6
  20. package/ios/templates/Parser.swift +564 -69
  21. package/ios/templates/SearchTemplate.swift +1 -1
  22. package/ios/templates/TemplateStore.swift +5 -15
  23. package/ios/utils/SymbolFont.swift +13 -28
  24. package/jest.js +2 -0
  25. package/lib/Constants.d.ts +4 -0
  26. package/lib/Constants.js +6 -0
  27. package/lib/index.d.ts +1 -0
  28. package/lib/index.js +1 -0
  29. package/lib/index.web.d.ts +73 -0
  30. package/lib/index.web.js +72 -1
  31. package/lib/specs/MapTemplate.nitro.d.ts +3 -1
  32. package/lib/templates/GridTemplate.d.ts +23 -3
  33. package/lib/templates/GridTemplate.js +8 -0
  34. package/lib/templates/InformationTemplate.d.ts +38 -12
  35. package/lib/templates/ListTemplate.d.ts +16 -47
  36. package/lib/templates/MapTemplate.d.ts +26 -2
  37. package/lib/templates/MapTemplate.js +16 -2
  38. package/lib/templates/MessageTemplate.d.ts +52 -11
  39. package/lib/templates/MessageTemplate.js +1 -1
  40. package/lib/types/Image.d.ts +6 -1
  41. package/lib/utils/NitroColor.d.ts +1 -0
  42. package/lib/utils/NitroColor.js +8 -0
  43. package/lib/utils/NitroImage.js +1 -1
  44. package/lib/utils/NitroManeuver.js +1 -3
  45. package/lib/utils/NitroOptionsPanel.d.ts +103 -0
  46. package/lib/utils/NitroOptionsPanel.js +63 -0
  47. package/lib/utils/NitroSection.d.ts +89 -2
  48. package/lib/utils/NitroSection.js +49 -2
  49. package/lib-cjs/AutoPlayHeadlessJsTask.js +16 -0
  50. package/lib-cjs/Constants.js +9 -0
  51. package/lib-cjs/components/MapTemplateContext.js +10 -0
  52. package/lib-cjs/components/SafeAreaInsetsContext.js +21 -0
  53. package/lib-cjs/components/SafeAreaView.js +22 -0
  54. package/lib-cjs/components/WindowInformationWrapper.js +20 -0
  55. package/lib-cjs/hooks/useAndroidAutoTelemetry.js +115 -0
  56. package/lib-cjs/hooks/useFocusedEffect.js +35 -0
  57. package/lib-cjs/hooks/useMapTemplate.js +13 -0
  58. package/lib-cjs/hooks/useSafeAreaInsets.js +9 -0
  59. package/lib-cjs/hooks/useVoiceInput.js +42 -0
  60. package/lib-cjs/hybrid/HybridAndroidAutoTelemetry.js +4 -0
  61. package/lib-cjs/hybrid/HybridAndroidWindowInformation.js +4 -0
  62. package/lib-cjs/hybrid/HybridAutoPlay.js +5 -0
  63. package/lib-cjs/hybrid/HybridVoice.js +61 -0
  64. package/lib-cjs/index.js +76 -0
  65. package/lib-cjs/index.web.js +98 -0
  66. package/lib-cjs/scenes/AutoPlayCluster.js +140 -0
  67. package/lib-cjs/scenes/CarPlayDashboardScene.js +100 -0
  68. package/lib-cjs/specs/AndroidAutoTelemetry.nitro.js +2 -0
  69. package/lib-cjs/specs/AndroidAutomotive.nitro.js +16 -0
  70. package/lib-cjs/specs/AndroidWindowInformation.nitro.js +2 -0
  71. package/lib-cjs/specs/AutoPlay.nitro.js +2 -0
  72. package/lib-cjs/specs/CarPlayDashboard.nitro.js +2 -0
  73. package/lib-cjs/specs/Cluster.nitro.js +2 -0
  74. package/lib-cjs/specs/GridTemplate.nitro.js +2 -0
  75. package/lib-cjs/specs/InformationTemplate.nitro.js +2 -0
  76. package/lib-cjs/specs/ListTemplate.nitro.js +2 -0
  77. package/lib-cjs/specs/MapTemplate.nitro.js +2 -0
  78. package/lib-cjs/specs/MessageTemplate.nitro.js +2 -0
  79. package/lib-cjs/specs/SearchTemplate.nitro.js +2 -0
  80. package/lib-cjs/specs/SignInTemplate.nitro.js +2 -0
  81. package/lib-cjs/specs/Voice.nitro.js +2 -0
  82. package/lib-cjs/templates/GridTemplate.js +41 -0
  83. package/lib-cjs/templates/InformationTemplate.js +49 -0
  84. package/lib-cjs/templates/ListTemplate.js +33 -0
  85. package/lib-cjs/templates/MapTemplate.js +168 -0
  86. package/lib-cjs/templates/MessageTemplate.js +50 -0
  87. package/lib-cjs/templates/SearchTemplate.js +32 -0
  88. package/lib-cjs/templates/SignInTemplate.js +55 -0
  89. package/lib-cjs/templates/Template.js +40 -0
  90. package/lib-cjs/types/Button.js +12 -0
  91. package/lib-cjs/types/Event.js +2 -0
  92. package/lib-cjs/types/Image.js +2 -0
  93. package/lib-cjs/types/Maneuver.js +77 -0
  94. package/lib-cjs/types/RootComponent.js +2 -0
  95. package/lib-cjs/types/SignInMethod.js +22 -0
  96. package/lib-cjs/types/Telemetry.js +24 -0
  97. package/lib-cjs/types/Text.js +8 -0
  98. package/lib-cjs/types/Trip.js +2 -0
  99. package/lib-cjs/types/Voice.js +2 -0
  100. package/lib-cjs/utils/ErrorUtil.js +34 -0
  101. package/lib-cjs/utils/NitroAction.js +129 -0
  102. package/lib-cjs/utils/NitroAlert.js +19 -0
  103. package/lib-cjs/utils/NitroAttributedString.js +14 -0
  104. package/lib-cjs/utils/NitroColor.js +32 -0
  105. package/lib-cjs/utils/NitroGrid.js +12 -0
  106. package/lib-cjs/utils/NitroImage.js +97 -0
  107. package/lib-cjs/utils/NitroManeuver.js +79 -0
  108. package/lib-cjs/utils/NitroMapButton.js +33 -0
  109. package/lib-cjs/utils/NitroOptionsPanel.js +66 -0
  110. package/lib-cjs/utils/NitroSection.js +112 -0
  111. package/nitrogen/generated/android/ReactNativeAutoPlay+autolinking.cmake +1 -0
  112. package/nitrogen/generated/android/c++/JChargingConnector.hpp +79 -0
  113. package/nitrogen/generated/android/c++/JGridImageSize.hpp +64 -0
  114. package/nitrogen/generated/android/c++/JGridTemplateConfig.hpp +7 -1
  115. package/nitrogen/generated/android/c++/JHybridGridTemplateSpec.cpp +4 -0
  116. package/nitrogen/generated/android/c++/JHybridInformationTemplateSpec.cpp +12 -0
  117. package/nitrogen/generated/android/c++/JHybridListTemplateSpec.cpp +12 -0
  118. package/nitrogen/generated/android/c++/JHybridMapTemplateSpec.cpp +78 -3
  119. package/nitrogen/generated/android/c++/JHybridMapTemplateSpec.hpp +2 -1
  120. package/nitrogen/generated/android/c++/JHybridSearchTemplateSpec.cpp +12 -0
  121. package/nitrogen/generated/android/c++/JInformationTemplateConfig.hpp +6 -0
  122. package/nitrogen/generated/android/c++/JListImageType.hpp +67 -0
  123. package/nitrogen/generated/android/c++/JListTemplateConfig.hpp +6 -0
  124. package/nitrogen/generated/android/c++/JNitroChargerLocation.hpp +115 -0
  125. package/nitrogen/generated/android/c++/JNitroChargerOutlet.hpp +82 -0
  126. package/nitrogen/generated/android/c++/JNitroColor.hpp +8 -4
  127. package/nitrogen/generated/android/c++/JNitroOptionsPanelChargerSection.hpp +112 -0
  128. package/nitrogen/generated/android/c++/JNitroOptionsPanelConfig.hpp +157 -0
  129. package/nitrogen/generated/android/c++/JNitroOptionsPanelGridSection.hpp +102 -0
  130. package/nitrogen/generated/android/c++/JNitroOptionsPanelSection.cpp +30 -0
  131. package/nitrogen/generated/android/c++/JNitroOptionsPanelSection.hpp +129 -0
  132. package/nitrogen/generated/android/c++/JNitroRow.hpp +33 -3
  133. package/nitrogen/generated/android/c++/JNitroSection.hpp +6 -0
  134. package/nitrogen/generated/android/c++/JSearchTemplateConfig.hpp +6 -0
  135. package/nitrogen/generated/android/c++/JWaypointCoordinate.hpp +65 -0
  136. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/ChargingConnector.kt +30 -0
  137. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/GridImageSize.kt +25 -0
  138. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/GridTemplateConfig.kt +9 -4
  139. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/HybridMapTemplateSpec.kt +5 -1
  140. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/ListImageType.kt +26 -0
  141. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroChargerLocation.kt +90 -0
  142. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroChargerOutlet.kt +70 -0
  143. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroColor.kt +9 -4
  144. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroOptionsPanelChargerSection.kt +61 -0
  145. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroOptionsPanelConfig.kt +61 -0
  146. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroOptionsPanelGridSection.kt +56 -0
  147. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroOptionsPanelSection.kt +75 -0
  148. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroRow.kt +36 -6
  149. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/WaypointCoordinate.kt +61 -0
  150. package/nitrogen/generated/ios/ReactNativeAutoPlay-Swift-Cxx-Bridge.hpp +174 -0
  151. package/nitrogen/generated/ios/ReactNativeAutoPlay-Swift-Cxx-Umbrella.hpp +27 -0
  152. package/nitrogen/generated/ios/c++/HybridGridTemplateSpecSwift.hpp +3 -0
  153. package/nitrogen/generated/ios/c++/HybridInformationTemplateSpecSwift.hpp +9 -0
  154. package/nitrogen/generated/ios/c++/HybridListTemplateSpecSwift.hpp +9 -0
  155. package/nitrogen/generated/ios/c++/HybridMapTemplateSpecSwift.hpp +47 -1
  156. package/nitrogen/generated/ios/c++/HybridSearchTemplateSpecSwift.hpp +9 -0
  157. package/nitrogen/generated/ios/swift/ChargingConnector.swift +68 -0
  158. package/nitrogen/generated/ios/swift/GridImageSize.swift +48 -0
  159. package/nitrogen/generated/ios/swift/GridTemplateConfig.swift +12 -1
  160. package/nitrogen/generated/ios/swift/HybridMapTemplateSpec.swift +2 -1
  161. package/nitrogen/generated/ios/swift/HybridMapTemplateSpec_cxx.swift +31 -4
  162. package/nitrogen/generated/ios/swift/ListImageType.swift +52 -0
  163. package/nitrogen/generated/ios/swift/NitroChargerLocation.swift +161 -0
  164. package/nitrogen/generated/ios/swift/NitroChargerOutlet.swift +65 -0
  165. package/nitrogen/generated/ios/swift/NitroColor.swift +20 -2
  166. package/nitrogen/generated/ios/swift/NitroOptionsPanelChargerSection.swift +64 -0
  167. package/nitrogen/generated/ios/swift/NitroOptionsPanelConfig.swift +94 -0
  168. package/nitrogen/generated/ios/swift/NitroOptionsPanelGridSection.swift +53 -0
  169. package/nitrogen/generated/ios/swift/NitroOptionsPanelSection.swift +32 -0
  170. package/nitrogen/generated/ios/swift/NitroRow.swift +81 -1
  171. package/nitrogen/generated/ios/swift/WaypointCoordinate.swift +52 -0
  172. package/nitrogen/generated/shared/c++/ChargingConnector.hpp +104 -0
  173. package/nitrogen/generated/shared/c++/GridImageSize.hpp +84 -0
  174. package/nitrogen/generated/shared/c++/GridTemplateConfig.hpp +8 -1
  175. package/nitrogen/generated/shared/c++/HybridMapTemplateSpec.cpp +1 -0
  176. package/nitrogen/generated/shared/c++/HybridMapTemplateSpec.hpp +5 -1
  177. package/nitrogen/generated/shared/c++/ListImageType.hpp +88 -0
  178. package/nitrogen/generated/shared/c++/NitroChargerLocation.hpp +131 -0
  179. package/nitrogen/generated/shared/c++/NitroChargerOutlet.hpp +98 -0
  180. package/nitrogen/generated/shared/c++/NitroColor.hpp +7 -3
  181. package/nitrogen/generated/shared/c++/NitroOptionsPanelChargerSection.hpp +98 -0
  182. package/nitrogen/generated/shared/c++/NitroOptionsPanelConfig.hpp +107 -0
  183. package/nitrogen/generated/shared/c++/NitroOptionsPanelGridSection.hpp +91 -0
  184. package/nitrogen/generated/shared/c++/NitroRow.hpp +39 -2
  185. package/nitrogen/generated/shared/c++/WaypointCoordinate.hpp +91 -0
  186. package/package.json +7 -3
  187. package/src/Constants.ts +9 -0
  188. package/src/index.ts +1 -0
  189. package/src/index.web.ts +166 -0
  190. package/src/specs/MapTemplate.nitro.ts +3 -1
  191. package/src/templates/GridTemplate.ts +38 -3
  192. package/src/templates/InformationTemplate.ts +60 -29
  193. package/src/templates/ListTemplate.ts +27 -55
  194. package/src/templates/MapTemplate.ts +60 -2
  195. package/src/templates/MessageTemplate.ts +71 -24
  196. package/src/types/Image.ts +6 -1
  197. package/src/utils/NitroColor.ts +10 -1
  198. package/src/utils/NitroImage.ts +1 -5
  199. package/src/utils/NitroManeuver.ts +1 -4
  200. package/src/utils/NitroOptionsPanel.ts +218 -0
  201. package/src/utils/NitroSection.ts +160 -5
package/README.md CHANGED
@@ -22,6 +22,10 @@
22
22
  - **Headless Operation:** Runs in the background to keep the automotive experience alive even when the main app is not in the foreground.
23
23
  - **Powered by [NitroModules](https://nitro.margelo.com/)**
24
24
 
25
+ ## Requirements
26
+
27
+ - **iOS builds require Xcode 27+** (the iOS 27 SDK), even for apps that don't use any `mapConfig`/panel features — the library references `CPMapPanel`/`CPPanel` types internally behind `@available(iOS 27.0, *)` checks, but `@available` only defers *runtime* execution, not compile-time symbol resolution, so the SDK must be present to build at all.
28
+
25
29
  ## Installation
26
30
 
27
31
  1. **Install the package and its peer dependencies:**
@@ -44,8 +48,8 @@
44
48
 
45
49
  #### Bundle identifier
46
50
  To get the CarPlay app showing up you need to set a proper Bundle Identifier:
47
- - Open `example.xcodeproj`
48
- - Select the example target, go to the **Signing & Capabilities** tab.
51
+ - Open your app's `.xcodeproj` in Xcode.
52
+ - Select your app target, go to the **Signing & Capabilities** tab.
49
53
  - Under **Signing > Bundle Identifier**, enter your unique bundle ID (e.g., `at.g4rb4g3.autoplay.example`).
50
54
 
51
55
  #### Entitlements
@@ -137,37 +141,36 @@ Paste this into your Info.plist and adjust it to your needs. Check [Apple docs](
137
141
 
138
142
  #### MapTemplate
139
143
  if you want to make use of the MapTemplate and render react components you need to add this to your AppDelegate.swift
140
- This should cover old and new architecture, adjust to your needs!
144
+ This is an example that works for bare react-native (>= 0.82) and Expo SDK 57, check [this](https://github.com/Iternio-Planning-AB/react-native-auto-play/blob/dbd33ff32ee58338282ffe0f8a970e687e1e3520/packages/react-native-autoplay/README.md?plain=1#L139) for older versions.
141
145
 
142
146
  ```swift
143
- @objc func getRootViewForAutoplay(
147
+ @objc func getRootViewForAutoplay(
144
148
  moduleName: String,
145
149
  initialProperties: [String: Any]?
146
150
  ) -> UIView? {
147
- if RCTIsNewArchEnabled() {
148
- if let factory = reactNativeFactory?.rootViewFactory as? ExpoReactRootViewFactory {
149
- return factory.superView(
150
- withModuleName: moduleName,
151
- initialProperties: initialProperties,
152
- launchOptions: nil
153
- )
154
- }
155
-
156
- return reactNativeFactory?.rootViewFactory.view(
151
+ var autoPlayRootView: UIView?
152
+
153
+ if let factory = reactNativeFactory?.rootViewFactory
154
+ as? ExpoReactRootViewFactory
155
+ {
156
+ autoPlayRootView = factory.superView(
157
157
  withModuleName: moduleName,
158
- initialProperties: initialProperties
158
+ initialProperties: initialProperties,
159
+ bundleConfiguration: RCTBundleConfiguration(),
160
+ devMenuConfiguration: RCTDevMenuConfiguration(),
159
161
  )
160
162
  }
161
163
 
162
- if let rootView = window?.rootViewController?.view as? RCTRootView {
163
- return RCTRootView(
164
- bridge: rootView.bridge,
165
- moduleName: moduleName,
164
+ if autoPlayRootView == nil,
165
+ let factory = reactNativeFactory?.rootViewFactory
166
+ {
167
+ autoPlayRootView = factory.view(
168
+ withModuleName: moduleName,
166
169
  initialProperties: initialProperties
167
170
  )
168
171
  }
169
172
 
170
- return nil
173
+ return autoPlayRootView
171
174
  }
172
175
  ```
173
176
 
@@ -175,7 +178,11 @@ This should cover old and new architecture, adjust to your needs!
175
178
  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.
176
179
  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.
177
180
  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.
178
- This is mainly required on CarPlay for now since Android Auto lacks light mode.
181
+ 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**.
182
+
183
+ #### CPListTemplate day/night header
184
+
185
+ **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.
179
186
 
180
187
  #### Dashboard buttons
181
188
  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.
@@ -203,6 +210,49 @@ In case you have ProGuard enabled (`def enableProguardInReleaseBuilds = true` in
203
210
  -keep class com.margelo.nitro.swe.iternio.reactnativeautoplay.** { *; }
204
211
  ```
205
212
 
213
+ #### Native backdrop under the MapTemplate surface
214
+ 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.
215
+
216
+ ```kotlin
217
+ interface NativeBackdrop {
218
+ /** Added as the presentation root's FIRST child, match-parent. */
219
+ val view: View
220
+
221
+ /** The car's day/night changed (CarContext.isDarkMode) — redraw accordingly. */
222
+ fun onColorSchemeChanged(dark: Boolean)
223
+
224
+ /** Release everything; must be idempotent. */
225
+ fun destroy()
226
+ }
227
+
228
+ /** Which car display is asking for a backdrop. */
229
+ enum class NativeBackdropDisplay { ROOT, CLUSTER }
230
+
231
+ object NativeBackdropRegistry {
232
+ /** Return null to render that display without a backdrop. */
233
+ @Volatile
234
+ var factory: ((CarContext, NativeBackdropDisplay) -> NativeBackdrop?)? = null
235
+ }
236
+ ```
237
+
238
+ Register the factory in your `Application.onCreate`, before the `CarAppService` can start:
239
+
240
+ ```kotlin
241
+ NativeBackdropRegistry.factory = { carContext, display ->
242
+ when (display) {
243
+ NativeBackdropDisplay.ROOT -> MyMapBackdrop(carContext)
244
+ NativeBackdropDisplay.CLUSTER -> null // or a second map view for the cluster
245
+ }
246
+ }
247
+ ```
248
+
249
+ Lifecycle contract:
250
+
251
+ - 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.
252
+ - A factory that throws is logged and ignored; the React surface still renders.
253
+ - The React surface view is transparent only while a backdrop is attached. Without a registered factory nothing changes: the surface stays opaque as before.
254
+ - `onColorSchemeChanged(dark)` is forwarded from each session's `onCarConfigurationChanged` to 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.
255
+
206
256
  ### Android Auto Customization
207
257
  You can customize certain behaviors of the library on Android Auto by setting properties in your app's `android/gradle.properties` file.
208
258
 
@@ -243,13 +293,21 @@ This library also supports Android Automotive. To enable Android Automotive supp
243
293
 
244
294
  - **`isAutomotiveApp` flag**: You need to inform the library if this is an Automotive app by setting the `isAutomotiveApp` property to `true`. For Android Auto, it should be `false`.
245
295
 
246
- You can set these properties directly in your `android/gradle.properties` file:
296
+ You can set these properties directly in your `android/gradle.properties` file. **Note the
297
+ `ReactNativeAutoPlay_` prefix** — the library reads `rootProject.ext.<name>` first and falls
298
+ back to the prefixed project property, so an unprefixed `isAutomotiveApp=true` in
299
+ `gradle.properties` is silently ignored and you get an Android Auto build instead:
300
+
247
301
  ```properties
248
302
  # For Android Automotive
249
- minSdkVersion=29
250
- isAutomotiveApp=true
303
+ ReactNativeAutoPlay_minSdkVersion=29
304
+ ReactNativeAutoPlay_isAutomotiveApp=true
251
305
  ```
252
306
 
307
+ If your app's `android/build.gradle` already defines `ext.minSdkVersion` (the React Native
308
+ template does), that `rootProject.ext` value wins over the property above — raise it there
309
+ instead.
310
+
253
311
  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.
254
312
 
255
313
  1. Install `react-native-config`:
@@ -393,6 +451,25 @@ It is also possible to use custom bundled images (e.g. PNG, WEBP or Vector Drawa
393
451
  - iOS: Add to your `Images.xcassets`
394
452
  - Android: Add to `res/drawable`
395
453
 
454
+ ### Icon colors and Android Auto light templates
455
+
456
+ 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).
457
+
458
+ Use the color `'default'` for every monochrome icon that has to stay readable in both cases:
459
+
460
+ ```ts
461
+ { type: 'glyph', name: 'search', color: 'default' }
462
+ { type: 'asset', image: require('./icon.png'), color: 'default' }
463
+ { type: 'remote', uri: 'https://example.com/icon.png', color: 'default' }
464
+ ```
465
+
466
+ - **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.
467
+ - **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.
468
+ - **Glyphs** use `'default'` automatically when no `color` is set. Exception on Android Auto: a glyph with a non-transparent `backgroundColor` is not tinted, since the tint would recolor the background as well. It keeps the plain white (dark mode) / black (light mode) glyph color, so set `color` explicitly if that does not contrast with your background.
469
+ - **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.
470
+ - 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.
471
+ - 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.
472
+
396
473
  ## Usage
397
474
 
398
475
  ### 1. Register the AutoPlay Components
@@ -526,7 +603,7 @@ All root components rendered by templates/scenes receive `RootComponentInitialPr
526
603
 
527
604
  - `id`: Module identifier (e.g. `AutoPlayRoot`, `CarPlayDashboard`, or a cluster UUID).
528
605
  - `rootTag`: React Native root tag.
529
- - `colorScheme`: `'light' | 'dark'` initial color scheme (listen to `onAppearanceDidChange` on `MapTemplate` for updates).
606
+ - `colorScheme`: `'light' | 'dark'` initial color scheme (listen to `onAppearanceDidChange` on `MapTemplate` for 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).
530
607
  - `window`: `{ width, height, scale }`.
531
608
 
532
609
  ### Template Configs (Props)
@@ -542,6 +619,7 @@ Below is a concise overview of the most important props per template. Optional p
542
619
  | `headerActions` | `MapHeaderActions<MapTemplate>` | ❌ | Top action strip. See **Header Actions** below. |
543
620
  | `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` |
544
621
  | `visibleTravelEstimate` | `'first'` `'last'` | ❌ | Which travel estimate to display. |
622
+ | `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. |
545
623
  | `onDidPan` / `onDidUpdateZoomGestureWithCenter` | callbacks | ❌ | Map gesture events. |
546
624
  | `onAppearanceDidChange` | `(colorScheme) => void` | ❌ | Listen for light/dark mode changes. |
547
625
  | `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](https://developer.android.com/reference/androidx/car/app/navigation/NavigationManagerCallback#onAutoDriveEnabled()) |
@@ -553,7 +631,7 @@ Below is a concise overview of the most important props per template. Optional p
553
631
  | `title` | `AutoText` | ✅ | Header title. |
554
632
  | `sections` | `Section<ListTemplate>` | ❌ | List sections/rows. Not providing anything here will result in a loading indicator on Android and an empty list on iOS. |
555
633
  | `headerActions` | `HeaderActions<ListTemplate>` | ❌ | Header actions. See **Header Actions** below. |
556
- | `mapConfig` | `BaseMapTemplateConfig<ListTemplate>` | ❌ | Android map-with-content layout. |
634
+ | `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. |
557
635
 
558
636
  #### GridTemplateConfig
559
637
 
@@ -562,7 +640,8 @@ Below is a concise overview of the most important props per template. Optional p
562
640
  | `title` | `AutoText` | ✅ | Header title. |
563
641
  | `buttons` | `GridButton<GridTemplate>[]` | ✅ | Grid items. Providing an empty array will result in a loading indicator on Android and an empty template on iOS. |
564
642
  | `headerActions` | `HeaderActions<GridTemplate>` | ❌ | Header actions. See **Header Actions** below. |
565
- | `mapConfig` | `BaseMapTemplateConfig<GridTemplate>` | ❌ | Android map-with-content layout. |
643
+ | `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. |
644
+ | `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. |
566
645
 
567
646
  #### SearchTemplateConfig
568
647
 
@@ -582,9 +661,9 @@ Below is a concise overview of the most important props per template. Optional p
582
661
  | --- | --- | --- | --- |
583
662
  | `title` | `AutoText` | ✅ | Header title. |
584
663
  | `items` | `InformationItems` | ❌ | 1–4 rows. |
585
- | `actions` | platform-specific | ❌ | Up to 2 buttons on Android, up to 3 on iOS. |
664
+ | `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. |
586
665
  | `headerActions` | `HeaderActions<InformationTemplate>` | ❌ | Header actions. See **Header Actions** below. |
587
- | `mapConfig` | `BaseMapTemplateConfig<InformationTemplate>` | ❌ | Android map-with-content layout. |
666
+ | `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. |
588
667
 
589
668
  #### MessageTemplateConfig
590
669
 
@@ -593,9 +672,9 @@ Below is a concise overview of the most important props per template. Optional p
593
672
  | `message` | `AutoText` | ✅ | Main message text. |
594
673
  | `title` | `AutoText` | ❌ | Android header title. |
595
674
  | `image` | `AutoImage` | ❌ | Android-only image above the message. |
596
- | `actions` | platform-specific | ❌ | Up to 2 buttons on Android, up to 3 on iOS. |
597
- | `headerActions` | `HeaderActionsAndroid<MessageTemplate>` | ❌ | Android-only header actions. |
598
- | `mapConfig` | `BaseMapTemplateConfig<MessageTemplate>` | ❌ | Android map-with-content layout. |
675
+ | `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. |
676
+ | `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. |
677
+ | `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. |
599
678
 
600
679
  #### SignInTemplateConfig (Android-only)
601
680
 
@@ -818,11 +897,11 @@ useEffect(() => {
818
897
  | Template | Purpose | Notes |
819
898
  | --- | --- | --- |
820
899
  | `MapTemplate` | Navigation, map rendering | Use as root; supports map buttons & navigation APIs. |
821
- | `ListTemplate` | Lists/menus | Supports sections, radio/toggle rows. |
822
- | `GridTemplate` | Action grid | Use `GridButton` items. |
900
+ | `ListTemplate` | Lists/menus | Supports sections, radio/toggle rows. Can render as a CarPlay map panel, see **Map + Content**. |
901
+ | `GridTemplate` | Action grid | Use `GridButton` items. Can render as a CarPlay map panel, see **Map + Content**. |
823
902
  | `SearchTemplate` | Search UI | Android-only search bar callbacks. |
824
- | `InformationTemplate` | Info panels | Android uses PaneTemplate; iOS uses InformationTemplate. |
825
- | `MessageTemplate` | Modal messages | Always shown on top until popped. |
903
+ | `InformationTemplate` | Info panels | Android uses PaneTemplate; iOS uses InformationTemplate. Can render as a CarPlay map panel, see **Map + Content**. |
904
+ | `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**. |
826
905
 
827
906
  **Template quick examples:**
828
907
 
@@ -843,6 +922,137 @@ new ListTemplate({
843
922
  }).push();
844
923
  ```
845
924
 
925
+ ### Map + Content (`mapConfig`)
926
+
927
+ `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.
928
+
929
+ ```ts
930
+ new ListTemplate({
931
+ title: { text: 'Nearby' },
932
+ sections: [{ type: 'default', title: 'Stops', items: [{ type: 'default', title: { text: 'Charger' }, onPress: () => {} }] }],
933
+ mapConfig: {},
934
+ }).push();
935
+ ```
936
+
937
+ #### Android
938
+
939
+ `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.
940
+
941
+ #### iOS (27+)
942
+
943
+ `mapConfig` instead renders the template as a [`CPMapPanel`](https://developer.apple.com/documentation/carplay/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).
944
+
945
+ ```ts
946
+ // Root map template must already be set for the panel to have somewhere to attach to
947
+ new MapTemplate({ component: MapScreen, onStopNavigation: () => {} }).setRootTemplate();
948
+
949
+ // Pushing this on top now shows it as an overlay panel on the map, instead of a full-screen list.
950
+ // headerActions.ios.backButton is required here — as the first (and only) panel in the stack it
951
+ // gets no native close/back control (see "Things that behave differently in panel mode" below),
952
+ // so without it the driver has no way to leave the panel.
953
+ new ListTemplate({
954
+ title: { text: 'Nearby' },
955
+ sections: [{ type: 'default', title: 'Stops', items: [{ type: 'default', title: { text: 'Charger' }, onPress: () => {} }] }],
956
+ headerActions: { ios: { backButton: { type: 'back', onPress: () => HybridAutoPlay.popTemplate() } } },
957
+ mapConfig: {},
958
+ }).push();
959
+ ```
960
+
961
+ **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.
962
+
963
+ **Things that behave differently in panel mode:**
964
+
965
+ - **`headerActions`/`mapButtons` ownership**: while a panel is shown, it takes over the root map template's bar buttons and floating map buttons — using the panel template's **own** `headerActions`/`mapConfig.mapButtons`, not `mapConfig.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.
966
+ - **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.panelDidHide` only 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, `onPopped` would 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, including `MessageTemplate` once `mapConfig` is set) or something inside the panel's own content (a list item, or `MessageTemplate`'s required `actions.ios[0]` `TextButton`) that calls `popTemplate()`/`popToRootTemplate()`. Without one, the driver has no way to leave that first panel short of `autoDismissMs`.
967
+ - **`InformationTemplate`/`MessageTemplate` `actions`**: a `CPMapPanel`'s button configuration only supports one `TextButton` (with a title) plus one optional icon-only `ImageButton` (any title on it is dropped natively) — far fewer than the up-to-3-`TextButton` shape available without `mapConfig`. The type system enforces this: `actions.ios` is restricted to `[TextButton]` or `[TextButton, ImageButton]` whenever `mapConfig` is set.
968
+ - **`MessageTemplate` stops being a true modal**: normally `MessageTemplate` is a full-screen, blocking alert (`CPAlertTemplate`) that covers everything regardless of OS version. With `mapConfig` set, it instead becomes dismissible panel content in the regular push/pop stack — a deliberate trade-off, not a partial implementation.
969
+
970
+ **Known iOS 27 beta limitations** (not something fixable in this library — re-test against newer betas):
971
+
972
+ - The optional icon-only `symbolButton` in 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.
973
+ - `toggle` row accessory images render noticeably smaller inside a panel than in a regular (non-panel) `ListTemplate` — this is how Apple sizes `CPListItem.accessoryImage` on panels specifically, not something this library controls (see the `CPListItem.accessoryImage` known issue under **Options Panel** for the same underlying sizing bug's non-panel form).
974
+
975
+ ### Waypoint Rows (`type: 'waypoint'`)
976
+
977
+ 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:
978
+
979
+ ```ts
980
+ {
981
+ type: 'waypoint',
982
+ title: { text: 'Supercharger' },
983
+ address: 'Main St 1\n1234 Springfield',
984
+ coordinate: { latitude: 48.2, longitude: 16.37 },
985
+ travelEstimates: {
986
+ distance: { unit: 'kilometers', value: 12 },
987
+ duration: { timezone: 'Europe/Vienna', seconds: 600 },
988
+ visible: true,
989
+ },
990
+ image: { type: 'glyph', name: 'pin_drop' },
991
+ onPress: () => {},
992
+ }
993
+ ```
994
+
995
+ **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`](https://developer.apple.com/documentation/carplay/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`](https://developer.apple.com/documentation/carplay/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.
996
+
997
+ **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):
998
+
999
+ ```ts
1000
+ {
1001
+ type: 'waypoint',
1002
+ title: { text: `Supercharger (${TextPlaceholders.Distance})` },
1003
+ address: `Main St 1 · ${TextPlaceholders.Duration} away`,
1004
+ travelEstimates: { distance: { unit: 'kilometers', value: 12 }, duration: { timezone: 'Europe/Vienna', seconds: 600 } },
1005
+ coordinate: { latitude: 48.2, longitude: 16.37 },
1006
+ onPress: () => {},
1007
+ }
1008
+ ```
1009
+
1010
+ ### Options Panel (`optionsPanel`, iOS 27+)
1011
+
1012
+ `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.
1013
+
1014
+ ```ts
1015
+ mapTemplate.updateOptionsPanel({
1016
+ title: { text: 'Trip options' },
1017
+ sections: [
1018
+ {
1019
+ type: 'list',
1020
+ title: 'Route',
1021
+ items: [{ type: 'default', title: { text: 'Avoid tolls' }, onPress: () => {} }],
1022
+ },
1023
+ {
1024
+ type: 'charger',
1025
+ title: 'Charger',
1026
+ location: {
1027
+ name: 'Fast Network Inc.',
1028
+ address: 'Main St 1',
1029
+ coordinate: { latitude: 48.2, longitude: 16.37 },
1030
+ travelEstimates: {
1031
+ distance: { unit: 'kilometers', value: 12 },
1032
+ duration: { timezone: 'Europe/Vienna', seconds: 600 },
1033
+ visible: true,
1034
+ },
1035
+ onPress: () => {},
1036
+ },
1037
+ outlets: [{ connector: 'ccs2', voltage: 400, powerKw: 300, onPress: () => {} }],
1038
+ },
1039
+ ],
1040
+ });
1041
+ ```
1042
+
1043
+ A section is one of:
1044
+
1045
+ | `type` | Renders as | Notes |
1046
+ | --- | --- | --- |
1047
+ | `'list'` | Rows (`default`/`toggle`/`radio`/`text`/`waypoint`) | Same row types and behavior as a regular `ListTemplate` section. |
1048
+ | `'grid'` | A row of `GridButton`s | Same shape as `GridTemplate.buttons`. |
1049
+ | `'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). |
1050
+
1051
+ **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):
1052
+
1053
+ - **Custom (non-system) images are unreliable across several of these newer panel APIs.** A `waypoint` row's/`ChargerLocation`'s glyph `image` overflows at `CPNavigationAlert.maximumAvatarImageSize` on iOS 27 — worked around by dividing the requested size by `traitCollection.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 custom `UIImage.isSymbolImage` assets was tried without a reliable, correctly-sized result; only genuine **system** symbols (`UIImage(systemName:)`) size correctly there. Expect `image` on a waypoint/charger row to render, but not necessarily at a sensible or crisp size.
1054
+ - **`CPListItem.accessoryImage` (used for `toggle` rows) 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, genuine `UIImage.isSymbolImage` assets from both the app's own bundle and a library-owned resource bundle). Reproduces on a plain (non-panel) `ListTemplate` too, so it isn't specific to panels or to this library's usage of the API. No workaround found; filed as Apple Feedback.
1055
+
846
1056
  ### Voice Input
847
1057
 
848
1058
  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`.
@@ -1076,6 +1286,28 @@ CarPlayDashboard.setButtons([
1076
1286
  - `setAttributedInactiveDescriptionVariants(variants)` — iOS only inactive text.
1077
1287
  - `addListenerColorScheme(cb)` / `addListenerZoom(cb)` / `addListenerCompass(cb)` / `addListenerSpeedLimit(cb)`.
1078
1288
 
1289
+ ## Testing with Jest
1290
+
1291
+ The real package needs native modules and ships ESM, so it can't run under Jest. Use the bundled CommonJS mock instead, one line in your Jest setup file:
1292
+
1293
+ ```js
1294
+ // jest.setup.js
1295
+ jest.mock('@iternio/react-native-auto-play', () =>
1296
+ require('@iternio/react-native-auto-play/jest')
1297
+ );
1298
+ ```
1299
+
1300
+ Templates, `HybridAutoPlay`, `HybridVoice`, `AutoPlayCluster`, `CarPlayDashboard` and the hooks that need a car surface are safe no-ops (any method call returns `undefined`), `Constants.isIos27OrGreater` is `false`, and all types are unchanged. Tests that need to record constructions or assert on calls should extend it per test file:
1301
+
1302
+ ```ts
1303
+ jest.mock('@iternio/react-native-auto-play', () => {
1304
+ const actual = jest.requireActual('@iternio/react-native-auto-play/jest');
1305
+ return { ...actual, ListTemplate: class { push = jest.fn(() => Promise.resolve()); } };
1306
+ });
1307
+ ```
1308
+
1309
+ The same no-op surface is what `react-native-web` builds get automatically via `index.web.ts`.
1310
+
1079
1311
  ## Known Issues
1080
1312
 
1081
1313
  ### iOS
@@ -1095,6 +1327,8 @@ In case you are using Expo SDK >= 56 make sure to set `buildReactNativeFromSourc
1095
1327
  // Hide the splash screen for the CarPlay screen
1096
1328
  hideAsync(AutoPlayModules.AutoPlayRoot);
1097
1329
  ```
1330
+ - **CarPlay map panels (iOS 27 beta)**: a panel's optional icon-only `symbolButton` does not respond to taps. See **Map + Content** above for details. This is a beta platform limitation, not a bug in this library — re-test against newer iOS 27 betas.
1331
+ - **Waypoint/options-panel images and toggle-row sizing (iOS 27 beta)**: custom images on a `waypoint` row/`ChargerLocation` have no reliable size, and `CPListItem.accessoryImage` (`toggle` rows) renders at an undersized fixed footprint regardless of the image supplied. See **Waypoint Rows** above for details. Beta platform limitations, not bugs in this library — an Apple Feedback report has been filed for the `accessoryImage` issue.
1098
1332
  ### Android
1099
1333
  - **Broken exceptions with `react-native`** up to version 0.79
1100
1334
  When using react-native before 0.80.0 exceptions are broken and are reported as `Unknown runtime_error` or similar.
@@ -113,6 +113,11 @@ class AndroidAutoSession(sessionInfo: SessionInfo) :
113
113
  override fun onCarConfigurationChanged(configuration: Configuration) {
114
114
  val colorScheme = if (carContext.isDarkMode) ColorScheme.DARK else ColorScheme.LIGHT
115
115
 
116
+ // This display's native backdrop (root or cluster) must follow the car's day/night
117
+ // (car-app quality MR-1). Forwarded before the early returns below — the root template
118
+ // may not be a MapTemplate yet (e.g. a pre-trip MessageTemplate) and must still switch.
119
+ VirtualRenderer.onColorSchemeChanged(moduleName, carContext.isDarkMode)
120
+
116
121
  if (clusterId != null) {
117
122
  HybridCluster.emitColorScheme(clusterId, colorScheme)
118
123
  AndroidAutoScreen.getScreen(clusterId)?.applyConfigUpdate(invalidate = true)
@@ -147,8 +147,10 @@ class HybridMapTemplate : HybridMapTemplateSpec() {
147
147
 
148
148
  override fun startNavigation(
149
149
  templateId: String, trip: TripConfig
150
- ) {
151
- MapTemplate.startNavigation(trip)
150
+ ): Promise<Unit> {
151
+ return Promise.async {
152
+ MapTemplate.startNavigation(trip)
153
+ }
152
154
  }
153
155
 
154
156
  override fun stopNavigation(templateId: String) {
@@ -158,4 +160,9 @@ class HybridMapTemplate : HybridMapTemplateSpec() {
158
160
  override fun setManeuverState(templateId: String, state: ManeuverState) {
159
161
  // Android Auto does not have an equivalent to CPManeuverState
160
162
  }
163
+
164
+ override fun updateOptionsPanel(templateId: String, config: NitroOptionsPanelConfig?): Promise<Unit> {
165
+ // Android Auto has no equivalent to CarPlay's navigation session options panel
166
+ return Promise.async {}
167
+ }
161
168
  }
@@ -0,0 +1,40 @@
1
+ package com.margelo.nitro.swe.iternio.reactnativeautoplay
2
+
3
+ import android.view.View
4
+ import androidx.car.app.CarContext
5
+
6
+ /**
7
+ * An optional host-app-provided native View rendered under the React surface of an
8
+ * Android Auto display (root or cluster) — the Android counterpart of the iOS
9
+ * `getRootViewForAutoplay` AppDelegate hook. Lets a host compose a native map beneath its React overlay, e.g. a
10
+ * native map view that cannot be hosted as a React Native view on the virtual display
11
+ * (Fragment-based map SDK wrappers are bound to the phone Activity).
12
+ *
13
+ * Contract: register `NativeBackdropRegistry.factory` before the CarAppService starts
14
+ * (Application.onCreate). The factory is consulted once per presentation (i.e. again after
15
+ * every surface resize); each backdrop is destroyed when its presentation is replaced or the
16
+ * renderer stops. A factory that throws is logged and ignored — the surface still renders.
17
+ */
18
+ interface NativeBackdrop {
19
+ /** Added as the presentation root's FIRST child, match-parent. */
20
+ val view: View
21
+
22
+ /** The car's day/night changed (CarContext.isDarkMode) — redraw accordingly. */
23
+ fun onColorSchemeChanged(dark: Boolean)
24
+
25
+ /** Release everything; must be idempotent. */
26
+ fun destroy()
27
+ }
28
+
29
+ /** Which car display is asking for a backdrop. */
30
+ enum class NativeBackdropDisplay { ROOT, CLUSTER }
31
+
32
+ object NativeBackdropRegistry {
33
+ /**
34
+ * Consulted once per presentation for the root display AND for each cluster display.
35
+ * Return null to render that display without a backdrop (e.g. a host with a single
36
+ * native map view may serve the root only). A throwing factory is logged and ignored.
37
+ */
38
+ @Volatile
39
+ var factory: ((CarContext, NativeBackdropDisplay) -> NativeBackdrop?)? = null
40
+ }
@@ -7,6 +7,7 @@ import android.graphics.Rect
7
7
  import android.hardware.display.DisplayManager
8
8
  import android.hardware.display.VirtualDisplay
9
9
  import android.os.Bundle
10
+ import android.util.Log
10
11
  import android.view.ContextThemeWrapper
11
12
  import android.view.Display
12
13
  import android.view.LayoutInflater
@@ -42,6 +43,10 @@ class VirtualRenderer(
42
43
  ) {
43
44
  private var virtualDisplay: VirtualDisplay? = null
44
45
  private val pendingDisplays = mutableListOf<VirtualDisplay>()
46
+ // Optional host view under the React surface (see NativeBackdrop), one per presentation;
47
+ // parked ones are destroyed in the same sweep as pendingDisplays
48
+ private var currentBackdrop: NativeBackdrop? = null
49
+ private val pendingBackdrops = mutableListOf<NativeBackdrop>()
45
50
 
46
51
  private var reactSurfaceImpl: ReactSurfaceImpl? = null
47
52
  private var reactSurfaceView: ReactSurfaceView? = null
@@ -408,14 +413,39 @@ class VirtualRenderer(
408
413
  }
409
414
 
410
415
 
416
+ // Ask the host for a native backdrop for this display; the host is told whether it
417
+ // is the root or a cluster and may answer null for either. `this@VirtualRenderer.context`
418
+ // is the CarContext — the presentation's own `context` parameter is the ReactContext
419
+ // and shadows it. A throwing factory is logged and ignored so the React surface still
420
+ // renders.
421
+ val display = if (isCluster) NativeBackdropDisplay.CLUSTER else NativeBackdropDisplay.ROOT
422
+ val backdrop: NativeBackdrop? = NativeBackdropRegistry.factory?.let { factory ->
423
+ runCatching { factory(this@VirtualRenderer.context, display) }
424
+ .onFailure { Log.w(TAG, "native backdrop factory failed ($display); rendering without it", it) }
425
+ .getOrNull()
426
+ }
427
+ currentBackdrop?.let { pendingBackdrops.add(it) }
428
+ currentBackdrop = backdrop
429
+
411
430
  val rootContainer = FrameLayout(themedContext).apply {
412
431
  layoutParams = FrameLayout.LayoutParams(
413
432
  FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT
414
433
  )
415
434
  clipChildren = false
416
435
 
436
+ backdrop?.let {
437
+ // A view the host hands out more than once must not crash Presentation.onCreate
438
+ (it.view.parent as? ViewGroup)?.removeView(it.view)
439
+ addView(it.view, FrameLayout.LayoutParams(
440
+ FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT
441
+ ))
442
+ }
417
443
  addView(reactSurfaceView)
418
444
  }
445
+ // The surface view is painted opaque DKGRAY at construction and survives resizes by
446
+ // re-parenting, so (re)apply per presentation: see-through only while a backdrop is
447
+ // actually attached.
448
+ reactSurfaceView?.setBackgroundColor(if (backdrop != null) Color.TRANSPARENT else Color.DKGRAY)
419
449
 
420
450
  splashScreenView?.let {
421
451
  rootContainer.addView(it)
@@ -434,6 +464,9 @@ class VirtualRenderer(
434
464
  it.release()
435
465
  }
436
466
  pendingDisplays.clear()
467
+ // Backdrops share the lifetime of the displays they drew on
468
+ pendingBackdrops.forEach { destroyBackdropQuietly(it, "parked") }
469
+ pendingBackdrops.clear()
437
470
  }
438
471
  }
439
472
  })
@@ -509,8 +542,22 @@ class VirtualRenderer(
509
542
  }
510
543
  }
511
544
 
545
+ /** Host code, same trust boundary as the factory: log and continue on failure. */
546
+ private fun destroyBackdropQuietly(backdrop: NativeBackdrop, which: String) {
547
+ runCatching { backdrop.destroy() }
548
+ .onFailure { Log.w(TAG, "native backdrop ($which) destroy failed; continuing teardown", it) }
549
+ }
550
+
512
551
  @MainThread
513
552
  private fun stop() {
553
+ // Current and parked — a stop during a resize must not leak the backdrop whose
554
+ // replacement never drew. Host destroy() is guarded like the factory call: a
555
+ // throwing host must not skip the virtual-display release and surface teardown below.
556
+ currentBackdrop?.let { destroyBackdropQuietly(it, "current") }
557
+ currentBackdrop = null
558
+ pendingBackdrops.forEach { destroyBackdropQuietly(it, "parked") }
559
+ pendingBackdrops.clear()
560
+
514
561
  virtualDisplay?.release()
515
562
  virtualDisplay = null
516
563
 
@@ -554,5 +601,10 @@ class VirtualRenderer(
554
601
  virtualRenderer[moduleId]?.stop()
555
602
  virtualRenderer.remove(moduleId)
556
603
  }
604
+
605
+ // Forwarded by AndroidAutoSession.onCarConfigurationChanged
606
+ fun onColorSchemeChanged(moduleId: String, dark: Boolean) {
607
+ virtualRenderer[moduleId]?.currentBackdrop?.onColorSchemeChanged(dark)
608
+ }
557
609
  }
558
610
  }