--- url: /getting-started/introduction.md --- # Introduction Voltra is a library that brings new "platforms" to React Native. Up until now, creating features like iOS Live Activities, Dynamic Island layouts, or Android Home Screen Widgets required writing native code in Swift or Kotlin. Voltra changes this by providing a JavaScript-based API and JSX components that get automatically converted to native primitives (SwiftUI on iOS, Jetpack Compose Glance on Android). ## Why Voltra? - **React Native Everywhere:** Extend your React Native app with native platform features using the same JSX syntax you already know. - **No Native Code Required:** Build complex widget layouts and live activities without touching Xcode or Android Studio for UI code. - **Platform-native JSX:** Use platform-specific primitives that map directly to SwiftUI on iOS and Glance on Android. - **Real-time Updates:** Stream updates to your activities and widgets via push notifications (APNS/FCM) from any JavaScript runtime. ## How it works Voltra works by serializing your JSX components into a lightweight JSON format that the native platform extensions can interpret. This enables features like hot reloading during development and server-side rendering for push updates. Here's how simple it is to create a live activity: ```tsx import { Voltra } from '@use-voltra/ios' import { startLiveActivity } from '@use-voltra/ios-client' const activityUI = ( Driver en route Building A · Lobby pickup Contact driver ) // Start the live activity await startLiveActivity({ lockScreen: activityUI, }) ``` If you prefer using the hook API (`useLiveActivity`), you'll get live reloads for live activities, with changes appearing in milliseconds without manual restarts. ## Server-side updates via push notifications Voltra also supports server-side updates through push notifications. You can use Voltra's server-side rendering to convert JSX into JSON payloads that you send to devices via Apple's Push Notification Service (APNS) or Firebase Cloud Messaging (FCM). This enables real-time updates without keeping your app running. The same components you use in your app work on the server: ```tsx import { Voltra, renderLiveActivityToString } from '@use-voltra/ios-server' // Render JSX to JSON payload on your server const payload = renderLiveActivityToString({ lockScreen: ( Driver arrived Ready for pickup ), }) ``` Ready to get started? Head over to the [Installation](/getting-started/installation.md) guide, or explore platform-specific guides for [iOS](/ios/introduction.md) and [Android](/android/introduction.md). --- url: /ios/introduction.md --- # Introduction Adding live activities to your iOS app has traditionally been a time-consuming and complex process. JavaScript developers need to learn Xcode, master SwiftUI, understand how to start live activities, and figure out how to manage them throughout their lifecycle. This creates barriers between your app development and these dynamic features. ## Voltra + JSX = Live Activity Voltra changes all of that by providing a JavaScript-based API you can use to display live activities in your app. Instead of writing SwiftUI code, you write JSX using Voltra components that get automatically converted to SwiftUI and displayed just like native code. Here's how simple it is to create a live activity: ```tsx import { Voltra } from '@use-voltra/ios' import { startLiveActivity } from '@use-voltra/ios-client' const activityUI = ( Driver en route Building A · Lobby pickup Contact driver ) // Start the live activity await startLiveActivity({ lockScreen: activityUI, }) ``` If you prefer using the hook API (`useLiveActivity`), you'll get live reloads for live activities, with changes appearing in milliseconds without manual restarts. ## Server-side updates via push notifications Voltra also supports server-side updates through push notifications. You can use Voltra's server-side rendering to convert JSX into JSON payloads that you send to devices via Apple's Push Notification Service (APNS). This enables real-time updates without keeping your app running. The same components you use in your app work on the server: ```tsx import { Voltra, renderLiveActivityToString } from '@use-voltra/ios-server' // Render JSX to JSON payload on your server const payload = renderLiveActivityToString({ lockScreen: ( Driver arrived Ready for pickup ), }) ``` You're ready to dive into the [setup guide](/ios/setup.md) and get started with live activities in your app. --- url: /android/introduction.md --- # Android Introduction :::warning Experimental Support Android support is **experimental**. Although it should work just fine, the API may change. Stay vigilant. ::: Voltra brings the power of JSX-based UI to Android Home Screen widgets. Using Jetpack Compose Glance under the hood, Voltra allows you to define Android widgets using a set of primitives that map directly to Glance components. ## Widgets on Android Android widgets have different layout and styling rules compared to iOS Live Activities. While iOS uses SwiftUI-based primitives (VStack, HStack, etc.), Android uses Jetpack Compose Glance primitives (Column, Row, Box). Voltra abstracts these differences where possible, but provides platform-specific namespaces to ensure your UI looks and behaves correctly on each platform. Voltra also exposes Android-specific semantic dynamic colors through `AndroidDynamicColors`, which lets widgets follow the current Material palette without requiring a JavaScript re-render. See [Dynamic Colors](/android/development/dynamic-colors.md). Voltra also supports Android ongoing notifications for app-driven, persistent status updates. See [Managing Android Ongoing Notifications](/android/development/managing-ongoing-notifications.md). ### Simple Android Widget ```tsx import { VoltraAndroid } from '@use-voltra/android' const MyWidget = () => ( Android Widget Powered by Voltra & Glance ) ``` ## Key Differences - **Primitives:** Use `VoltraAndroid.Column`, `VoltraAndroid.Row`, and `VoltraAndroid.Box` instead of stacks. - **Alignment:** Android uses specific alignment props like `verticalAlignment` and `horizontalAlignment`. - **Sizing:** Use `"100%"` for full size or `"auto"` for wrapping content. ## Testing and Previews You can preview your Android widgets directly in your app using the `VoltraWidgetPreview` component. This allows for fast iteration without needing to constantly check the home screen. Learn more in the [Testing and Previews guide](/android/development/testing-and-previews.md). ## Next Steps Check out the [Setup guide](/android/setup.md) to set up Voltra for Android. For notification-based experiences, see [Managing Android Ongoing Notifications](/android/development/managing-ongoing-notifications.md). --- url: /android/api/plugin-configuration.md --- # Plugin Configuration (Android) The Voltra Expo config plugin accepts Android-specific configuration options in your `app.json` or `app.config.js`: ```json { "expo": { "plugins": [ [ "@use-voltra/android-client", { "enableNotifications": true, "widgets": [ { "id": "weather", "displayName": "Weather Widget", "description": "Shows current weather conditions", "targetCellWidth": 2, "targetCellHeight": 2, "initialStatePath": "./widgets/weather-initial.tsx", "previewImage": "./assets/widgets/weather-preview.png" } ] } ] ] } } ``` ## Android-Specific Configuration ### `enableNotifications` (optional) Enables Android notification-related manifest plumbing used by Voltra features such as ongoing notifications. When enabled, the config plugin adds: - `android.permission.POST_NOTIFICATIONS` - `android.permission.POST_PROMOTED_NOTIFICATIONS` - `voltra.VoltraOngoingNotificationDismissedReceiver` This does not grant runtime notification permission automatically. Your app still needs to request notification permission on Android 13 and above. For setup and usage examples, see [Managing Android Ongoing Notifications](/android/development/managing-ongoing-notifications.md). ### `widgets` (optional) Array of widget configurations for Home Screen widgets. Each widget will be available in the Android widget picker. **Widget Configuration Properties:** - `id`: Unique identifier for the widget (alphanumeric with underscores only) - `displayName`: Name shown in the widget picker (plain string, or per-locale map; same rules as iOS `widgets[].displayName`) - `description`: Description shown in the widget picker (same rules as `displayName`) - `targetCellWidth`: Target widget width in grid cells (1-5, required) - `targetCellHeight`: Target widget height in grid cells (1-5, required) - `minCellWidth`: (optional) Minimum width in grid cells (defaults to targetCellWidth) - `minCellHeight`: (optional) Minimum height in grid cells (defaults to targetCellHeight) - `minWidth`: (optional) Minimum width in dp (overrides minCellWidth calculation) - `minHeight`: (optional) Minimum height in dp (overrides minCellHeight calculation) - `resizeMode`: (optional) Widget resize behavior (`"none"` | `"horizontal"` | `"vertical"` | `"horizontal|vertical"`, default: `"horizontal|vertical"`) - `widgetCategory`: (optional) Widget category (`"home_screen"` | `"keyguard"` | `"home_screen|keyguard"`, default: `"home_screen"`) - `initialStatePath`: (optional) Path to a file that exports initial widget state, or a locale map of paths for localized build-time pre-rendering (see [Widget Pre-rendering](/android/development/widget-pre-rendering.md)) - `previewImage`: (optional) Path to preview image for widget picker (PNG/JPG/WebP) - `previewLayout`: (optional) Path to custom XML layout for widget picker preview (Android 12+) - `serverUpdate`: (optional) Enable server-driven updates. See [Server-driven widgets](/android/development/server-driven-widgets.md) for full details. - `url`: The Voltra SSR endpoint URL - `intervalMinutes`: Update interval in minutes (default: `15`, minimum 15 per WorkManager) - `refresh`: Show a native refresh button (default: `false`) ### Localizing `displayName` and `description` Use a locale map when the widget picker label should be translated: ```json { "widgets": [ { "id": "weather", "displayName": { "en": "Weather", "pl": "Pogoda", "zh-Hans": "天气" }, "description": { "en": "Current weather conditions", "pl": "Aktualne warunki pogodowe", "zh-Hans": "当前天气状况" }, "targetCellWidth": 2, "targetCellHeight": 2 } ] } ``` Use BCP-47-style locale tags such as `en`, `en-US`, `pt-BR`, or `zh-Hans`. Fallback behavior: - Voltra first tries the device locale. - If there is no exact match, it falls back to the language-only match. - If there is still no match, it prefers an English locale such as `en` or `en-US`. - If no English entry exists, it uses the first configured locale. ## Widget Sizing ### Grid Cells vs Density-Independent Pixels (dp) Android uses grid cells to define widget sizes. By default, the formula is: - **minWidth/minHeight (dp) = (cellCount × 70) - 30** **Example:** - 2 cells = (2 × 70) - 30 = **110 dp** - 4 cells = (4 × 70) - 30 = **250 dp** You can override this with explicit `minWidth` and `minHeight` in dp. ### Standard Dimensions | Family | Cells | Default DP | Typical Use | | ----------- | ----- | ---------- | ----------------- | | Small | 2×1 | 110 × 40 | Quick glance info | | Medium | 2×2 | 110 × 110 | Main widget size | | Large | 4×2 | 250 × 110 | Rich content | | Extra Large | 4×4 | 250 × 250 | Complex layouts | ## Widget Picker Previews When users add a widget to their home screen, Android displays a preview in the widget picker. Voltra supports three preview methods, with automatic fallback: ### Preview Priority Chain 1. **`previewLayout`** (Android 12+) - Custom XML layout for scalable preview 2. **`previewImage`** (All versions) - Static image or auto-generated layout 3. **Default** - System placeholder layout ### Using `previewImage` Static preview image for all Android versions: ```json { "widgets": [ { "id": "weather", "displayName": "Weather Widget", "targetCellWidth": 2, "targetCellHeight": 2, "previewImage": "./assets/widgets/weather-preview.png" } ] } ``` When only `previewImage` is specified, Voltra automatically generates a layout that displays the image with proper scaling. ### Using `previewLayout` Custom XML layout for scalable previews (Android 12+): ```json { "widgets": [ { "id": "todos", "displayName": "Todo Widget", "targetCellWidth": 2, "targetCellHeight": 2, "previewLayout": "./assets/widgets/todos-preview.xml" } ] } ``` **Example `todos-preview.xml`:** ```xml ``` The preview layout is rendered at the widget's target size and displayed in the widget picker. ### Combined Preview Setup For best results across Android versions: ```json { "widgets": [ { "id": "weather", "displayName": "Weather Widget", "targetCellWidth": 2, "targetCellHeight": 2, "previewImage": "./assets/widgets/weather-preview.png", "previewLayout": "./assets/widgets/weather-preview.xml", "initialStatePath": "./widgets/weather-initial.tsx" } ] } ``` This configuration: - Uses `previewLayout` on Android 12+ (scalable, accurate preview) - Falls back to `previewImage` on Android 11 and earlier - Shows actual widget content on home screen via `initialStatePath` (when available) ## Widget Pre-rendering Use `initialStatePath` to provide pre-rendered widget state: ```json { "widgets": [ { "id": "weather", "displayName": "Weather Widget", "targetCellWidth": 2, "targetCellHeight": 2, "initialStatePath": "./widgets/weather-initial.tsx" } ] } ``` When the app is built, Voltra pre-renders the widget at the specified path and bundles it as `voltra_initial_states.json`. The widget displays this content immediately when first added to the home screen, before any dynamic updates. See [Widget Pre-rendering](/android/development/widget-pre-rendering.md) for details on creating initial state files. ## Example Configuration ```json { "expo": { "plugins": [ [ "@use-voltra/android-client", { "enableNotifications": true, "widgets": [ { "id": "voltra", "displayName": "Voltra Widget", "description": "Voltra logo widget", "minCellWidth": 2, "minCellHeight": 2, "targetCellWidth": 2, "targetCellHeight": 2, "resizeMode": "horizontal|vertical", "widgetCategory": "home_screen", "initialStatePath": "./widgets/android-voltra-widget-initial.tsx", "previewImage": "./assets/voltra-icon.jpg" }, { "id": "interactive_todos", "displayName": "Interactive Todos", "description": "Quick todo list widget", "targetCellWidth": 2, "targetCellHeight": 2, "previewLayout": "./assets/widgets/todos-preview.xml" } ] } ] ] } } ``` --- url: /android/charts.md --- # Charts (Android) Use charts in Android widgets to show trends, comparisons, progress, or composition at a glance. You can mix bars, lines, areas, points, rules, and sectors in a single chart. :::info Charts are rendered to a bitmap using the Android Canvas API and displayed as a Glance `Image`. This approach is required because Jetpack Glance has no native charting components. ::: :::warning Mark components (`BarMark`, `LineMark`, and the other mark types) must be direct children of ``. Do not wrap them in a custom component. ::: ## Basic Usage Wrap one or more mark components inside ``: ```tsx ``` ## Data Types All marks except `RuleMark` take a `data` prop. There are two data shapes depending on the mark type: ```typescript // BarMark, LineMark, AreaMark, PointMark type ChartDataPoint = { x: string | number // Categorical ("Jan") or numeric (42) y: number series?: string // Optional — groups data for multi-series charts } // SectorMark type SectorDataPoint = { value: number // Proportional angular value category: string // Sector label } ``` ## Marks ### BarMark Use bars when people need to compare values across categories. **Parameters:** - `data` (ChartDataPoint\[], required): The data points. - `color` (string, optional): Fill color. - `cornerRadius` (number, optional): Rounded bar corners. - `width` (number, optional): Fixed bar width. - `stacking` (string, optional): `"grouped"` for side-by-side bars in a multi-series chart. ```tsx ``` *** ### LineMark Use a line when the shape of change matters more than individual columns. **Parameters:** - `data` (ChartDataPoint\[], required): The data points. - `color` (string, optional): Line color. - `lineWidth` (number, optional): Stroke width. - `interpolation` (string, optional): `"linear"`, `"monotone"`, `"catmullRom"`, `"cardinal"`, `"stepStart"`, `"stepCenter"`, or `"stepEnd"`. ```tsx ``` *** ### AreaMark Use an area chart when you want the overall volume or rise/fall pattern to read quickly. **Parameters:** - `data` (ChartDataPoint\[], required): The data points. - `color` (string, optional): Fill color. - `interpolation` (string, optional): Same options as `LineMark`. ```tsx ``` *** ### PointMark Use points for sparse measurements, scatter plots, or to emphasize exact observations. **Parameters:** - `data` (ChartDataPoint\[], required): The data points. - `color` (string, optional): Point color. - `symbolSize` (number, optional): Point size. ```tsx ``` *** ### RuleMark A horizontal or vertical reference line. Unlike other marks, RuleMark has no `data` array. **Parameters:** - `yValue` (number, optional): Draw a horizontal line at this y value. - `xValue` (string | number, optional): Draw a vertical line at this x value. - `color` (string, optional): Line color. - `lineWidth` (number, optional): Stroke width. ```tsx ``` *** ### SectorMark Pie and donut charts. **Parameters:** - `data` (SectorDataPoint\[], required): Sector data with `value` and `category`. - `color` (string, optional): Fill color (overrides automatic coloring). - `innerRadius` (number, optional): `0` = pie chart, any value above `0` = donut chart. Values ≤ 1 are treated as a ratio of the max radius; values > 1 are treated as absolute dp. - `outerRadius` (number, optional): Same behavior as `innerRadius`. - `angularInset` (number, optional): Gap between sectors in degrees. ```tsx // Pie chart // Donut chart ``` ## Chart Props The `` container accepts these props in addition to the standard `style` prop: | Prop | Type | Description | | ---------------------- | -------------------------------------- | ------------------------------ | | `xAxisVisibility` | `"automatic" \| "visible" \| "hidden"` | Show or hide the x-axis | | `xAxisGridStyle` | `{ visible?: boolean }` | Show or hide x-axis grid lines | | `yAxisVisibility` | `"automatic" \| "visible" \| "hidden"` | Show or hide the y-axis | | `yAxisGridStyle` | `{ visible?: boolean }` | Show or hide y-axis grid lines | | `foregroundStyleScale` | `Record` | Map series names to colors | ## Grid Lines Hide grid lines when you want the chart to feel more compact: ```tsx ``` ## Multi-Series Charts Add a `series` field to your data points when you want multiple datasets in the same chart. Use `foregroundStyleScale` to keep those series colors consistent: ```tsx ``` Without `foregroundStyleScale`, colors are chosen automatically. For side-by-side bars, set `stacking="grouped"`: ```tsx ``` ## Combining Marks Mix mark types when one chart needs both context and emphasis, such as bars for totals plus a rule for a target: ```tsx ``` ## Sparkline / Minimal Style Hide axes for a clean, compact visualization: ```tsx ``` ## Sizing Chart dimensions are read from the `style` prop: - **Fixed size**: `style={{ width: 300, height: 200 }}` - **Fill parent**: `style={{ width: '100%', height: '100%' }}` If no width or height is specified, the chart defaults to 300×200 dp. ## Platform Notes - `legendVisibility` is not currently available on Android charts. - Point markers are circular on Android. - `innerRadius` and `outerRadius` on `SectorMark` accept either a ratio (`0` to `1`) or a larger fixed value. --- url: /android/components/interactive.md --- # Interactive Controls (Android) User interface controls that respond to user interaction on Android widgets. ### Button Standard button component. On Android, all buttons always open the application when clicked. You can provide a `deepLinkUrl` to open a specific screen. **Parameters:** - `enabled` (boolean, optional): Whether the button is enabled. - `deepLinkUrl` (string, optional): URL to open when the button is clicked. If not provided, the app will open to its main activity. Voltra also provides specialized button variants: #### FilledButton - `text` (string): Button label. - `enabled` (boolean, optional). - `deepLinkUrl` (string, optional). - `icon` (object, optional): `{ assetName: string }`. - `backgroundColor` (string, optional). - `contentColor` (string, optional). - `maxLines` (number, optional). #### OutlineButton - `text` (string): Button label. - `enabled` (boolean, optional). - `deepLinkUrl` (string, optional). - `icon` (object, optional): `{ assetName: string }`. - `contentColor` (string, optional). - `maxLines` (number, optional). #### CircleIconButton & SquareIconButton - `enabled` (boolean, optional). - `deepLinkUrl` (string, optional). - `icon` (object, optional): `{ assetName: string, base64: string }`. - `contentDescription` (string, optional). - `backgroundColor` (string, optional). - `contentColor` (string, optional). *** ### Clickable Components Most components support being clickable by setting the `pressable` prop (short name `prs` in raw elements) in their props. **Parameters:** - `pressable` (boolean): Set to `true` to make the component respond to clicks. - `deepLinkUrl` (string, optional): URL to open when clicked. *** ### Switch A toggle switch component. On Android, toggles always open the application when clicked. **Parameters:** - `id` (string): Unique identifier for interaction events. - `checked` (boolean, optional): Current state of the switch. - `deepLinkUrl` (string, optional): URL to open when clicked. - `text` (string, optional): Label displayed next to the switch. - `thumbCheckedColor` (string, optional). - `thumbUncheckedColor` (string, optional). - `trackCheckedColor` (string, optional). - `trackUncheckedColor` (string, optional). - `maxLines` (number, optional): Maximum lines for the label. *** ### CheckBox Standard checkbox component. On Android, checkboxes always open the application when clicked. **Parameters:** - `id` (string): Unique identifier for interaction events. - `checked` (boolean, optional). - `deepLinkUrl` (string, optional). - `text` (string, optional). - `checkedColor` (string, optional). - `uncheckedColor` (string, optional). - `maxLines` (number, optional). *** ### RadioButton Standard radio button component. On Android, radio buttons always open the application when clicked. **Parameters:** - `id` (string): Unique identifier for interaction events. - `checked` (boolean, optional). - `enabled` (boolean, optional). - `deepLinkUrl` (string, optional). - `text` (string, optional). - `checkedColor` (string, optional). - `uncheckedColor` (string, optional). - `maxLines` (number, optional). --- url: /android/components/layout.md --- # Layout & Containers (Android) Components that arrange other elements or provide structural grouping using Jetpack Compose Glance primitives. See [Styling](/android/development/styling.md) for details on layout and spacing properties. ### Column A vertical container that arranges its children in a column. **Parameters:** - `horizontalAlignment` (string, optional): `"start"`, `"center-horizontally"`, `"end"`. - `verticalAlignment` (string, optional): `"top"`, `"center-vertically"`, `"bottom"`. *** ### Row A horizontal container that arranges its children in a row. **Parameters:** - `horizontalAlignment` (string, optional): `"start"`, `"center-horizontally"`, `"end"`. - `verticalAlignment` (string, optional): `"top"`, `"center-vertically"`, `"bottom"`. *** ### Box A container that stacks its children on top of each other. **Parameters:** - `contentAlignment` (string, optional): Combined alignment. Supports `"top-start"`, `"top-center"`, `"top-end"`, `"center-start"`, `"center"`, `"center-end"`, `"bottom-start"`, `"bottom-center"`, `"bottom-end"`. *** ### Scaffold A top-level container that provides a standard layout structure for widgets. **Parameters:** - `backgroundColor` (string, optional): Background color for the scaffold. - `horizontalPadding` (number, optional): Horizontal padding in dp. *** ### TitleBar A component that displays a title bar with a required leading icon. **Parameters:** - `title` (string): Title text to display. - `startIcon` (object, required): `{ assetName: string }` or `{ base64: string }`. - `textColor` (string, optional). - `iconColor` (string, optional). - `fontFamily` (string, optional): `"monospace"`, `"serif"`, `"sans-serif"`, or `"cursive"`. *** ### Spacer A component that provides fixed spacing between elements. **Parameters:** - `size` (number): Size of the spacer in dp. *** ### LazyColumn A scrollable vertical list that only renders visible items. **Parameters:** - `horizontalAlignment` (string, optional): `"start"`, `"center-horizontally"`, `"end"`. *** ### LazyVerticalGrid A scrollable grid of items. **Parameters:** - `columns` (number | `"adaptive"`): Number of columns or `"adaptive"` for an adaptive grid. - `minSize` (number, optional): Minimum size (in dp) for items in adaptive grid mode. - `horizontalAlignment` (string, optional): `"start"`, `"center-horizontally"`, `"end"`. - `verticalAlignment` (string, optional): `"top"`, `"center"`, `"bottom"`. --- url: /android/components/status.md --- # Data Visualization & Status (Android) Components for displaying data and status information on Android widgets. ### LinearProgressIndicator A horizontal progress bar. **Parameters:** - `progress` (number, optional): Current progress value (0.0 to 1.0). If omitted, the indicator will be indeterminate. - `color` (string, optional): Color for the progress indicator. - `backgroundColor` (string, optional): Color for the background track. *** ### CircularProgressIndicator A circular progress indicator. **Parameters:** - `color` (string, optional): Color for the progress indicator. :::warning Indeterminate Only Due to Jetpack Compose Glance limitations, the `CircularProgressIndicator` on Android renders in **indeterminate** mode. ::: --- url: /android/components/visual.md --- # Visual Elements & Typography (Android) Static or decorative elements used to display content on Android widgets. See [Styling](/android/development/styling.md) for details on supported style properties. ### Text Displays text content. **Parameters:** - `maxLines` (number, optional): Maximum number of lines to display. - `renderAsBitmap` (boolean, optional): Renders text as a bitmap image to enable [custom fonts](/android/development/custom-fonts.md). Requires `fontFamily` in the style prop. *** ### Image Displays bitmap images from the asset catalog, preloaded runtime cache, or base64 encoded data. SVGs should be rasterized through the image preloading API and referenced by `assetName`. **Parameters:** - `source` (object, optional): Image source object. - `assetName` (string): Reference to a pre-bundled image (drawable resource) or a [preloaded image](/android/development/image-preloading.md). - `base64` (string): Base64 encoded image data. - `resizeMode` (string, optional): `"cover"`, `"contain"`, `"stretch"`, `"repeat"`, or `"center"`. - `contentDescription` (string, optional): Accessibility description for the image. - `contentScale` (string, optional): Glance-specific scaling mode: `"crop"`, `"cover"`, `"fit"`, `"contain"`, `"fill-bounds"`, or `"stretch"`. - `alpha` (number, optional): Opacity value from 0.0 to 1.0. - `colorFilter` (string, optional): Color tint filter for the image. - `tintColor` (string, optional): Legacy alias for `colorFilter`. - `fallback` (ReactNode, optional): Custom content rendered when the image is missing. **Styling the fallback:** To add a background color when an image is missing, use `backgroundColor` in the `style` prop: ```jsx ``` :::tip Image Preloading For dynamic images from remote URLs, use the [Image Preloading](/android/development/image-preloading.md) API to cache them locally for use in widgets. ::: --- url: /android/development/custom-fonts.md --- # Custom Fonts Android Glance only supports a handful of built-in font families (`monospace`, `serif`, `sans-serif`, `cursive`). Voltra works around this by rendering text as a bitmap with a custom `Typeface` loaded from `assets/fonts/`. ## Setup ### 1. Add font files to the plugin config List your font paths in the top-level `fonts` array. These can be local files or packages from `@expo-google-fonts`: ```json { "expo": { "plugins": [ [ "@use-voltra/android-client", { "fonts": [ "node_modules/@expo-google-fonts/pacifico/400Regular/Pacifico_400Regular.ttf", "./assets/fonts/MyCustomFont.ttf" ], "widgets": [] } ] ] } } ``` ### 2. Run prebuild ```bash npx expo prebuild ``` The plugin copies each font file to `android/app/src/main/assets/fonts/` automatically. ### 3. Use `renderAsBitmap` on Text ```tsx import { VoltraAndroid } from '@use-voltra/android' Hello Voltra! ``` The `fontFamily` value should match the font filename **without the extension**. ## How it works When `renderAsBitmap` is set and `fontFamily` is provided in the style: 1. The font is loaded via `Typeface.createFromAsset()` (cached with an LRU cache) 2. Text is drawn to an Android `Canvas` bitmap using `StaticLayout` 3. The bitmap is displayed as a Glance `Image` with fixed dp dimensions This means the text is rasterized — it won't respond to system font size settings. Use it only when a custom typeface is needed. ## Supported style properties When rendering as bitmap, the following text style properties are supported: | Property | Description | | -------------------- | ------------------------------------------ | | `fontSize` | Font size in sp (scaled to device density) | | `fontFamily` | Font filename without extension | | `fontWeight` | `"normal"` or `"bold"` | | `color` | Text color | | `textAlign` | `"left"`, `"center"`, `"right"` | | `textDecorationLine` | `"underline"`, `"line-through"` | | `letterSpacing` | Letter spacing value | | `lineHeight` | Line spacing | ## Built-in font families For built-in families you don't need `renderAsBitmap` — use `fontFamily` in style directly: - `monospace` - `serif` - `sans-serif` - `cursive` These are passed through to Glance's native `FontFamily` API. --- url: /android/development/developing-widgets.md --- # Developing Android Widgets Voltra allows you to build Android Home Screen widgets using JSX and Jetpack Compose Glance primitives. ## Glance Primitives On Android, you use `VoltraAndroid` components which map to Glance primitives: - **Column:** Vertical layout - **Row:** Horizontal layout - **Box:** Stacked layout - **Spacer:** Flexible spacing - **Text:** Displaying text - **Image:** Displaying images - **Scaffold:** Top-level container ### Example Widget ```tsx import { VoltraAndroid } from '@use-voltra/android' const WeatherWidget = ({ temperature, condition }) => ( {temperature}°C {condition} ) ``` ## Update API To update a widget's content, use the `updateAndroidWidget` function from `@use-voltra/android-client`: ```typescript import { updateAndroidWidget } from '@use-voltra/android-client' await updateAndroidWidget('weather_widget', ) ``` ## Layout Constraints Unlike standard React Native or iOS Stacks, Android Glance layouts are more restrictive: - **Width/Height:** Use fixed numbers (dp), `"100%"` to fill available space, or `"auto"` to wrap content. - **Modifiers:** Most styling is handled via the `style` prop, which maps to Glance `Modifier`s. See [Styling](/android/development/styling.md) for full details. - **Alignment:** Use `verticalAlignment` and `horizontalAlignment` props on `Column` and `Row`. ## Advanced Features - **[Querying Active Widgets](/android/development/querying-active-widgets.md):** Detect active widget instances and their sizes. - **[Testing and Previews](/android/development/testing-and-previews.md):** Preview layouts within your app. - **[Widget Picker Previews](/android/api/plugin-configuration.md#widget-picker-previews):** Configure how your widget appears in the Android widget picker. - **[Image Preloading](/android/development/image-preloading.md):** Cache remote images for use in widgets. - **[Widget Pre-rendering](/android/development/widget-pre-rendering.md):** Provide initial state for widgets before the app first runs. ## Widget Picker Previews When users browse the widget picker to add your widget to their home screen, they see a preview. You can customize this preview using: - **`previewImage`:** Static image (PNG/JPG/WebP) that shows in the picker on all Android versions - **`previewLayout`:** Custom XML layout that renders a scalable preview on Android 12+ See [Plugin Configuration - Widget Picker Previews](/android/api/plugin-configuration.md#widget-picker-previews) for configuration details and examples. --- url: /android/development/dynamic-colors.md --- # Dynamic colors Voltra supports Android dynamic colors through semantic tokens exposed from `@use-voltra/android`. These colors follow the current Android Material palette, so widgets can pick up wallpaper and theme changes without waiting for JavaScript to run again. ## Importing dynamic colors ```tsx import { AndroidDynamicColors, VoltraAndroid } from '@use-voltra/android' ``` `AndroidDynamicColors` includes these roles: - `primary` - `onPrimary` - `primaryContainer` - `onPrimaryContainer` - `secondary` - `onSecondary` - `secondaryContainer` - `onSecondaryContainer` - `tertiary` - `onTertiary` - `tertiaryContainer` - `onTertiaryContainer` - `error` - `errorContainer` - `onError` - `onErrorContainer` - `background` - `onBackground` - `surface` - `onSurface` - `surfaceVariant` - `onSurfaceVariant` - `outline` - `inverseOnSurface` - `inverseSurface` - `inversePrimary` - `widgetBackground` ## Example ```tsx import { AndroidDynamicColors, VoltraAndroid } from '@use-voltra/android' export function WeatherWidget() { return ( 21° ) } ``` ## Where you can use them You can use `AndroidDynamicColors.*` anywhere Android accepts a color value, including: - `style.backgroundColor` - `style.color` - `Image.colorFilter` - button `backgroundColor` and `contentColor` - `TitleBar.textColor` and `TitleBar.iconColor` - switch, checkbox, and radio button colors - progress indicator colors - chart mark colors ## Server-driven widgets Dynamic color tokens work in server-rendered Android widgets too. ```tsx import { AndroidDynamicColors, VoltraAndroid } from '@use-voltra/android' const content = ( Server-rendered widget ) ``` The same `AndroidDynamicColors.*` values work whether the widget is rendered in-app or returned from your server. ## Migration notes Voltra no longer uses the old Android dynamic palette snapshot approach. - Use `AndroidDynamicColors.*` for Android widgets that should react to system palette changes. - Keep using literal colors when you want a fixed color. - There is no `useAndroidDynamicColorPalette()` or `getAndroidDynamicColorPalette()` API anymore. --- url: /android/development/dynamic-widgets.md --- # Dynamic Widgets :::warning Experimental Feature Dynamic widgets are experimental. Please [report any issues](https://github.com/callstackincubator/voltra/issues) you find. ::: Dynamic Widgets let your widget react to the current device state on Android. Declare them in `app.json` with a stable `id` and an explicit `entry`, then default-export the widget from that file. Your widget can react to: - `env.widgetFamily` - `env.colorScheme` - `env.locale` - `env.configuration` - `AndroidDynamicColors` tokens, which resolve to the current Material You palette natively When you change `app.json`, run Expo Prebuild or Voltra Apply so the updated widget configuration is available on device. If you change only the widget JS, reopen the app in development and the widget updates automatically. ## How to use it 1. Add an Android widget declaration to `app.json` with an `id`, an `entry`, and any widget metadata you need. 2. Default-export the widget function or component from the module named by `entry`. 3. Use `initialStatePath` if you want a pre-rendered first view. 4. Re-run Expo Prebuild or Voltra Apply after updating `app.json`. 5. Keep Android and iOS widget declarations separate; the same `id` can exist on both platforms because each platform is configured separately. ```tsx import { AndroidDynamicColors, VoltraAndroid, type WidgetEnvironment } from '@use-voltra/android' export default function WeatherWidget(_props: object, env: WidgetEnvironment = {} as WidgetEnvironment) { const renderedAt = env.date.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', }) return ( Weather Widget Size: {env.widgetFamily} Scheme: {env.colorScheme ?? 'light'} Rendered: {renderedAt} ) } ``` Example plugin config: ```json { "expo": { "plugins": [ [ "@use-voltra/android-client", { "widgets": [ { "id": "weather_widget", "entry": "./widgets/android/weather-widget.tsx", "displayName": "Weather Widget", "description": "A Dynamic Widget that reacts to live device state", "targetCellWidth": 2, "targetCellHeight": 2, "initialStatePath": "./widgets/android/weather-widget.tsx" } ] } ] ] } } ``` If you need user-controlled values, add `appIntent.parameters` and update them in-app with `setWidgetConfiguration(widgetId, key, value)`. ## Notes - There is no `export` field in app.json for Dynamic Widgets. - The default-exported function or component name does not need to match the widget `id`. - Use a real device to verify release rendering. - `initialStatePath` gives the widget a pre-rendered first view. --- url: /android/development/image-preloading.md --- # Image Preloading (Android) Android widgets have limitations when it comes to displaying remote images directly. The image preloading API allows you to download images to the app's cache directory, making them available to your widgets via a local `FileProvider`. ## Overview The image preloading system on Android works by: 1. Downloading images from URLs to the internal app cache. 2. Rasterizing SVG inputs to PNG when needed. 3. Making these images available to Voltra widgets via the `assetName` property. 4. Providing APIs to reload widgets when new images are ready. ## API Reference ### `preloadImages(images: PreloadImageOptions[]): Promise` Downloads images to the Android cache for use in Widgets. Each item must be either a URL preload (`url`) or an SVG preload (`svg`). The TypeScript types express this as a union of two variants: ```typescript type PreloadImageUrlOptions = { key: string // The assetName to use when referencing this image url: string // URL to download the image from method?: 'GET' | 'POST' | 'PUT' // HTTP method (default: 'GET') headers?: Record // Optional HTTP headers width?: number height?: number } type PreloadImageSvgOptions = { key: string // The assetName to use when referencing this image svg: string // Inline SVG markup to rasterize and cache as PNG width?: number // Typically required for SVG rasterization (see native errors if omitted) height?: number } type PreloadImageOptions = PreloadImageUrlOptions | PreloadImageSvgOptions type PreloadImageFailure = { key: string error: string } type PreloadImagesResult = { succeeded: string[] // Keys of successfully downloaded images failed: PreloadImageFailure[] // Failed downloads with error messages } ``` **Example:** ```typescript import { preloadImages } from '@use-voltra/android-client' const result = await preloadImages([ { url: 'https://example.com/album-art.jpg', key: 'current-album', headers: { Authorization: 'Bearer token' }, }, { key: 'status-icon-active', svg: '', width: 24, height: 24, }, ]) if (result.succeeded.includes('status-icon-active')) { // Images are ready to be used in widgets } ``` ### `reloadWidgets(widgetIds?: string[]): Promise` Reloads Android widgets to pick up newly preloaded images. If no `widgetIds` are provided, all active widgets will be reloaded. ```typescript import { reloadWidgets } from '@use-voltra/android-client' // Reload all widgets await reloadWidgets() // Reload specific widgets await reloadWidgets(['weather_widget']) ``` ### `clearPreloadedImages(keys?: string[]): Promise` Removes preloaded images from the Android cache. If no `keys` are provided, all preloaded images will be cleared. ```typescript import { clearPreloadedImages } from '@use-voltra/android-client' // Clear specific images await clearPreloadedImages(['current-album']) // Clear all preloaded images await clearPreloadedImages() ``` ## Usage in Android Widgets Once images are preloaded, reference them using the `assetName` property in the `VoltraAndroid.Image` component: ```tsx import { VoltraAndroid } from '@use-voltra/android' function MusicWidget({ albumKey }) { return ( ) } ``` --- url: /android/development/images.md --- # Images Voltra provides three different approaches for including images in your Android widgets, each with different trade-offs and use cases: - **Build-time asset copying**: Best for static icons and assets known at build time - **Runtime preloading**: Best for dynamic images from remote URLs - **Base64 encoding**: Best for small, generated images ## Build-time asset copying Place images in the `/assets/voltra-android/` directory and they'll be automatically processed and copied to the Android drawable resources during build. ``` project-root/ ├── assets/ │ └── voltra-android/ │ ├── logo.png │ ├── weather/ │ │ ├── sunny.svg │ │ └── rainy.xml │ └── background.webp ``` Here's how build-time asset copying works: 1. Images in `/assets/voltra-android/` are automatically detected during build (run `npx expo prebuild` to apply changes). 2. Filenames are sanitized to be compatible with Android resource naming rules (lowercase, underscores only). 3. SVGs are automatically converted to Android Vector Drawables (XML). 4. Images are copied to `res/drawable/` in the native Android project. ### Naming & Sanitization Android drawable resources have strict naming conventions. Voltra automatically handles this for you: - **Sanitization:** Uppercase letters are converted to lowercase. Hyphens and other special characters are replaced with underscores. - **Flattening:** Subdirectories are flattened into the resource name to avoid conflicts. **Examples:** | Source File | Android Resource Name | | :---------------------------------------- | :-------------------- | | `assets/voltra-android/Logo.png` | `logo` | | `assets/voltra-android/icons/My-Icon.png` | `icons_my_icon` | | `assets/voltra-android/weather/sunny.svg` | `weather_sunny` | ### Supported Formats - **Bitmap:** `.png`, `.jpg`, `.jpeg`, `.gif`, `.webp` - **Vector:** `.svg` (converted to Vector Drawable), `.xml` (native Vector Drawable) ### Usage Reference these images using their sanitized name in the `assetName` property. You do not need to include the file extension. ```tsx import { VoltraAndroid } from '@use-voltra/android' // assets/voltra-android/logo.png -> "logo" // assets/voltra-android/weather/sunny.svg -> "weather_sunny" ``` ## Runtime preloading For dynamic images from remote URLs, use Voltra's image preloading API to cache images locally. The image preloading system works by: 1. Downloading images from URLs to the app's internal cache. 2. Making images available to widgets via a local content provider. 3. Providing APIs to reload widgets when new images are ready. Once images are preloaded, reference them using the key you provided: ```tsx import { VoltraAndroid } from '@use-voltra/android' function ProfileWidget({ user }) { return ( {user.name} ) } ``` For detailed API documentation, see [Image Preloading](/android/development/image-preloading.md). ## Base64 encoding You can also embed images directly as base64-encoded strings. This is useful for small, generated images or when you want to avoid file management for very simple assets. ```tsx ``` ## Comparison table | Approach | When Known | Dynamic | Setup Required | Performance | | :------------- | :------------ | :------ | :--------------- | :------------------------------- | | **Build-time** | Build time | No | File placement | Best (Native Resource) | | **Preloading** | Runtime | Yes | Preload API call | Good (Cached File) | | **Base64** | Runtime/Build | No | None | Fair (Memory intensive if large) | --- url: /android/development/managing-ongoing-notifications.md --- # Managing Android Ongoing Notifications :::warning Experimental API Android ongoing notifications are **experimental**. The API may change in future releases. ::: Voltra supports Android ongoing notifications for local, app-driven status updates such as deliveries, rides, workouts, or timers. Use this API when you want to: - start a persistent notification from your app - update its content over time - stop it when the task ends - add action buttons that open deep links in your app Voltra also supports remote updates if your app receives push notifications in the background and forwards the payload to the ongoing notification APIs. ## Server-side rendering support Voltra already provides a server-side API for converting JSX into the semantic payload used by Android ongoing notifications. Use these APIs only in server-side or backend code. Do not import them from your React Native app runtime. Use `@use-voltra/android-server`. The main renderer APIs are: - `renderAndroidOngoingNotificationPayloadToJson()` returns an object - `renderAndroidOngoingNotificationPayload()` returns a JSON string This API only renders the payload. Your server still needs to send that payload through your push provider, and your app still needs a background task that calls `upsertAndroidOngoingNotification()` or `stopAndroidOngoingNotification()` when the push arrives. ## Before you start ### 1. Enable notification manifest support Add `android.enableNotifications` to the Voltra Expo plugin config: ```json { "expo": { "plugins": [ [ "@use-voltra/android-client", { "enableNotifications": true } ] ] } } ``` This adds the Android manifest entries required by Voltra's notification features. See [Plugin Configuration](/android/api/plugin-configuration.md#enablenotifications-optional) for details. ### 2. Create a notification channel `channelId` is required when starting an ongoing notification, and the channel must already exist. If you use `expo-notifications`, you can create a channel like this: ```tsx import * as Notifications from 'expo-notifications' await Notifications.setNotificationChannelAsync('delivery_updates', { name: 'Delivery updates', importance: Notifications.AndroidImportance.DEFAULT, }) ``` ### 3. Request notification permission on Android 13+ On Android 13 and above, posting notifications requires runtime permission. ```tsx import { hasAndroidNotificationPermission, requestAndroidNotificationPermission, } from '@use-voltra/android-client' const granted = (await hasAndroidNotificationPermission()) || (await requestAndroidNotificationPermission()) if (!granted) { // Show your own UI explaining why notifications are needed. } ``` ### 4. If you want remote updates, register a background notification task The playground app uses `expo-notifications` together with `expo-task-manager` to process real push notifications and update ongoing notifications in the background. Register a background task early in app startup: ```tsx import * as Notifications from 'expo-notifications' import * as TaskManager from 'expo-task-manager' const TASK_NAME = 'voltra-ongoing-notification-task' TaskManager.defineTask(TASK_NAME, async ({ data, error }) => { if (error) { return } // Read your push payload and call Voltra APIs here. }) await Notifications.registerTaskAsync(TASK_NAME) ``` The example app does this during startup so that incoming pushes can update or stop an ongoing notification even when the app is backgrounded. ## Starting a notification Voltra provides two built-in layouts: - `AndroidOngoingNotification.Progress` - `AndroidOngoingNotification.BigText` ### Progress notification ```tsx import { AndroidOngoingNotification } from '@use-voltra/android' import { startAndroidOngoingNotification, } from '@use-voltra/android-client' const result = await startAndroidOngoingNotification( , { notificationId: 'order-123', channelId: 'delivery_updates', deepLinkUrl: 'myapp://orders/123', } ) if (result.ok) { console.log('Started:', result.notificationId) } ``` ### Big text notification ```tsx import { AndroidOngoingNotification } from '@use-voltra/android' import { startAndroidOngoingNotification, } from '@use-voltra/android-client' await startAndroidOngoingNotification( , { notificationId: 'match-42', channelId: 'sports_updates', } ) ``` ## Updating a notification Use the same `notificationId` to update an existing notification. ```tsx import { AndroidOngoingNotification } from '@use-voltra/android' import { updateAndroidOngoingNotification, } from '@use-voltra/android-client' await updateAndroidOngoingNotification( 'order-123', ) ``` `updateAndroidOngoingNotification()` returns a result object. If the notification no longer exists, it returns `reason: 'not_found'` or `reason: 'dismissed'`. ## Starting or updating with one call If your app may re-enter the same flow multiple times, `upsertAndroidOngoingNotification()` can be easier than separate start/update logic. ```tsx import { AndroidOngoingNotification } from '@use-voltra/android' import { upsertAndroidOngoingNotification, } from '@use-voltra/android-client' const result = await upsertAndroidOngoingNotification( , { notificationId: 'workout-1', channelId: 'fitness_updates', } ) if (result.ok) { console.log(result.action) // 'started' or 'updated' } ``` This API is especially useful for remote updates, where the same incoming push may need to create the notification the first time and update it later. ## Stopping a notification ```tsx import { stopAndroidOngoingNotification } from '@use-voltra/android-client' await stopAndroidOngoingNotification('order-123') ``` To dismiss every active Voltra ongoing notification at once: ```tsx import { endAllAndroidOngoingNotifications } from '@use-voltra/android-client' await endAllAndroidOngoingNotifications() ``` ## Hook API For React screens and flows, use `useAndroidOngoingNotification()`. ```tsx import { AndroidOngoingNotification } from '@use-voltra/android' import { useAndroidOngoingNotification } from '@use-voltra/android-client' function DeliveryNotification({ orderId, etaMinutes }) { const { start, update, end, isActive } = useAndroidOngoingNotification( , { notificationId: `order-${orderId}`, channelId: 'delivery_updates', deepLinkUrl: `myapp://orders/${orderId}`, autoStart: true, autoUpdate: true, } ) return null } ``` The hook returns: - `start()` - `update()` - `end()` - `isActive` Use `autoStart` to create the notification when the component mounts, and `autoUpdate` to refresh it when the JSX content changes. ## Action buttons You can add action buttons as children of `Progress` or `BigText`. ```tsx import { AndroidOngoingNotification } from '@use-voltra/android' ``` Action buttons currently: - open the provided deep link - can be used with `Progress` and `BigText` - support an optional `icon` ```tsx ``` Android may not show action icons in the standard notification UI, so treat them as optional enhancement rather than a guaranteed visual element. ## Remote updates Voltra can apply remote ongoing-notification updates if your app receives a push notification and handles it in a background task. The end-to-end flow is: 1. Your server renders Voltra JSX into an Android ongoing-notification payload. 2. Your server sends a high-priority push notification. 3. The push `data` contains a `voltraOngoingNotification` object. 4. Your background task parses that object. 5. The task calls `upsertAndroidOngoingNotification()` or `stopAndroidOngoingNotification()`. ### 1. Render the payload on your server Use `renderAndroidOngoingNotificationPayloadToJson()` when preparing a payload on your server or in app tooling: ```tsx import { AndroidOngoingNotification, renderAndroidOngoingNotificationPayloadToJson, } from '@use-voltra/android-server' const payload = renderAndroidOngoingNotificationPayloadToJson( ) ``` Then send that payload inside a push message. If your push provider expects strings for nested payload data, use `renderAndroidOngoingNotificationPayload()` instead and send the JSON string directly. ### 2. Send the payload through your push provider The playground app expects `data.voltraOngoingNotification` to contain: - `notificationId`: the stable notification identifier - `operation`: `'upsert'` or `'stop'` - `options`: start options such as `channelId`, `smallIcon`, `deepLinkUrl`, `requestPromotedOngoing`, or `fallbackBehavior` - `payload`: the Voltra semantic payload for `'upsert'` Example Expo push request: ```json { "to": "ExponentPushToken[project-token]", "priority": "high", "data": { "voltraOngoingNotification": "{\"notificationId\":\"order-123\",\"operation\":\"upsert\",\"options\":{\"channelId\":\"delivery_updates\",\"deepLinkUrl\":\"myapp://orders/123\",\"requestPromotedOngoing\":true},\"payload\":{\"v\":1,\"kind\":\"progress\",\"title\":\"Driver is approaching\",\"text\":\"2 stops away\",\"value\":80,\"max\":100}}" } } ``` The playground app accepts either an object or a JSON string for `data.voltraOngoingNotification`. Stringifying it is often the safest option when sending through push providers. To stop the notification remotely, send the same `notificationId` with `operation: "stop"` and omit `payload`. ### 3. Apply the payload in your background task ```tsx import * as Notifications from 'expo-notifications' import * as TaskManager from 'expo-task-manager' import { stopAndroidOngoingNotification, upsertAndroidOngoingNotification, } from '@use-voltra/android-client' const TASK_NAME = 'voltra-ongoing-notification-task' const parseMessage = (value: unknown) => { if (typeof value === 'string') { try { return JSON.parse(value) } catch { return null } } return value } TaskManager.defineTask(TASK_NAME, async ({ data, error }) => { if (error) { return } const message = parseMessage(data?.voltraOngoingNotification) if (!message || typeof message !== 'object') { return } const notificationId = typeof message.notificationId === 'string' ? message.notificationId : null if (!notificationId) { return } if (message.operation === 'stop') { await stopAndroidOngoingNotification(notificationId) return } if (!message.payload || !message.options?.channelId) { return } await upsertAndroidOngoingNotification(message.payload, { ...message.options, notificationId, }) }) await Notifications.registerTaskAsync(TASK_NAME) ``` ### Channel setup for remote updates Your background task should ensure that the target notification channel exists before calling `upsertAndroidOngoingNotification()`. The playground app creates the channel on startup and also ensures it exists again inside the background handler. ### Important notes - Voltra does include a server-side JSX-to-payload renderer for Android ongoing notifications. - Remote updates depend on your push provider and app-level background notification setup. - Voltra provides the ongoing-notification rendering and lifecycle APIs, but your app is responsible for receiving the push and invoking those APIs. - `upsertAndroidOngoingNotification()` is the easiest entry point for remote updates because it can create or update the notification with the same payload path. - If your push provider serializes nested objects as strings, parse `data.voltraOngoingNotification` before passing it to Voltra. ## Main tap behavior Use `deepLinkUrl` in the start or update options to control what happens when the user taps the main notification body: ```tsx await startAndroidOngoingNotification(content, { notificationId: 'order-123', channelId: 'delivery_updates', deepLinkUrl: 'myapp://orders/123', }) ``` This is separate from action button deep links. ## Status and capability helpers Use these helpers to adapt your UI to the device state: ```tsx import { canPostPromotedAndroidNotifications, getAndroidOngoingNotificationCapabilities, getAndroidOngoingNotificationStatus, openAndroidNotificationSettings, } from '@use-voltra/android-client' const status = getAndroidOngoingNotificationStatus('order-123') const capabilities = getAndroidOngoingNotificationCapabilities() const canPostPromoted = canPostPromotedAndroidNotifications() if (!capabilities.notificationsEnabled) { await openAndroidNotificationSettings() } ``` Useful values include: - `status.isActive` - `status.isDismissed` - `capabilities.notificationsEnabled` - `capabilities.supportsPromotedNotifications` - `capabilities.canPostPromotedNotifications` - `capabilities.canRequestPromotedOngoing` ## Promoted ongoing notifications If your app wants to request promoted ongoing presentation when the device supports it, pass `requestPromotedOngoing: true`: ```tsx await startAndroidOngoingNotification(content, { notificationId: 'ride-44', channelId: 'ride_updates', requestPromotedOngoing: true, }) ``` You can also set `fallbackBehavior` if promoted presentation is unavailable: ```tsx await startAndroidOngoingNotification(content, { notificationId: 'ride-44', channelId: 'ride_updates', requestPromotedOngoing: true, fallbackBehavior: 'standard', }) ``` Check device support first with `getAndroidOngoingNotificationCapabilities()` if you want to tailor the UX. ## Current limitations - Remote updates require your own push delivery and background task integration. - Your app must create the Android notification channel before starting a notification. - Notification permission still needs to be requested by your app on Android 13+. - Action buttons open deep links. They are not a JavaScript event system. --- url: /android/development/querying-active-widgets.md --- # Querying Active Widgets On Android, you can detect every active instance of your widgets currently placed on the Home Screen. This is particularly useful for Android since each widget instance can have different dimensions and a unique `widgetId`. ## getActiveWidgets API The `getActiveWidgets` function returns a promise that resolves to an array of all active widget instances for your app. ```typescript import { getActiveWidgets } from '@use-voltra/android-client' async function checkAndroidWidgets() { const activeWidgets = await getActiveWidgets() console.log(`Found ${activeWidgets.length} active widget instances`) activeWidgets.forEach(widget => { console.log(`- Widget Name: ${widget.name}`) console.log(` ID: ${widget.widgetId}`) console.log(` Size: ${widget.width}x${widget.height}dp`) }) } ``` ### WidgetInfo Object Each object in the returned array contains: | Property | Type | Description | | :------------------ | :------- | :------------------------------------------------------------------------------------------- | | `name` | `string` | The unique ID of the widget as defined in your Expo config plugin (e.g., `"weather"`). | | `widgetId` | `number` | The unique system identifier for this specific widget instance. | | `providerClassName` | `string` | The full class name of the widget provider (e.g., `".widget.VoltraWidget_weatherReceiver"`). | | `label` | `string` | The human-readable label shown in the Android widget picker. | | `width` | `number` | The current width of the widget instance in dp. | | `height` | `number` | The current height of the widget instance in dp. | --- url: /android/development/server-driven-widgets.md --- # Server-driven widgets Server-driven widgets allow your Android Home Screen widgets to periodically fetch fresh content from a remote server—without the user opening the app. This is powered by WorkManager, which handles scheduling, retries, and network constraints automatically. Before you start, make sure the widget is registered in the Voltra plugin config and plan to rebuild the native app after adding or changing server-driven widget settings. Android semantic color tokens from [`AndroidDynamicColors`](/android/development/dynamic-colors.md) work in server-rendered widgets too, so your backend can return dynamic Material roles instead of fixed hex values. ## How it works 1. You configure a `serverUpdate` URL in your Android widget's plugin config 2. WorkManager runs a periodic background task at the configured interval 3. Your server renders Voltra JSX components into a JSON payload 4. The worker parses the payload and pushes a `RemoteViews` update to the widget Your app doesn't need to be running. WorkManager handles everything in the background. ## Plugin configuration Add the `serverUpdate` option to your Android widget in `app.json` or `app.config.js`: ```json { "expo": { "plugins": [ [ "@use-voltra/android-client", { "widgets": [ { "id": "dynamic_weather", "displayName": "Dynamic Weather", "description": "Weather with live server updates", "targetCellWidth": 2, "targetCellHeight": 1, "serverUpdate": { "url": "https://api.example.com/widgets/render", "intervalMinutes": 60 } } ] } ] ] } } ``` **`serverUpdate` options:** - `url`: The Voltra SSR endpoint that returns widget JSON. Voltra appends `widgetId`, `platform`, and `theme` query parameters automatically (e.g. `?widgetId=dynamic_weather&platform=android&theme=dark`). - `intervalMinutes`: How often the widget fetches updates. Defaults to `15`. The minimum effective interval is 15 minutes (WorkManager requirement). - `refresh`: Whether to show a native refresh button in the top-right corner of the widget. When tapped, triggers an immediate server fetch. Defaults to `false`. After updating plugin configuration, run `npx expo prebuild` if you're using Continuous Native Generation, then rebuild the app so the generated native widget code picks up the new server update settings. :::note On the Android emulator, use `10.0.2.2` instead of `localhost` to reach the host machine. Real devices need the host's LAN IP address. ::: ## Building the server Voltra provides widget server handlers for the common runtime styles. Use `createAndroidWidgetUpdateHandler()` for Fetch-compatible runtimes, `createAndroidWidgetUpdateNodeHandler()` for `node:http`, and `createAndroidWidgetUpdateExpressHandler()` for Express-style handlers. All three share the same request parsing, platform validation, token validation, and response serialization. ```tsx import { createServer } from 'node:http' import React from 'react' import { createAndroidWidgetUpdateNodeHandler } from '@use-voltra/android-server' import { AndroidDynamicColors, VoltraAndroid } from '@use-voltra/android' const handler = createAndroidWidgetUpdateNodeHandler({ render: async (req) => { // req.widgetId — the widget requesting an update // req.platform — always "android" for Android widget requests // req.theme — the system color scheme ("light" or "dark") // req.token — the auth token (if credentials were set) const weather = await fetchWeatherData() const content = ( {weather.temp}° {weather.condition} ) // Return size breakpoints for different widget sizes return [ { size: { width: 200, height: 100 }, content }, { size: { width: 200, height: 200 }, content }, { size: { width: 300, height: 200 }, content }, ] }, validateToken: async (token) => { return token === 'valid-token' }, }) createServer(handler).listen(3333) ``` The handler responds to GET requests with these query parameters: | Parameter | Description | | ---------- | ------------------------------------------------------ | | `widgetId` | The widget identifier (required) | | `platform` | The requesting platform. Must be `android` (required). | | `family` | Not used on Android | | `theme` | The system color scheme (`light` or `dark`) | The `User-Agent` header is set to `VoltraWidget/ (Android/)`. ## Authentication Widgets on Android are part of the main app binary, so the WorkManager background worker can access credential storage directly. Voltra encrypts credentials at rest using **Google Tink** (AES-256-GCM with Android Keystore-backed key management) and persists them in Jetpack DataStore. ### Setting credentials Call `setWidgetServerCredentials` after the user logs in: ```typescript import { setWidgetServerCredentials } from '@use-voltra/android-client' await setWidgetServerCredentials({ token: userAccessToken, headers: { 'X-App-Version': '1.0.0', }, }) ``` The `token` is required and is sent as `Authorization: Bearer ` on every server request. Any additional `headers` are also included. If your widget endpoint does not require authentication, skip `setWidgetServerCredentials()` entirely. ### Clearing credentials Call `clearWidgetServerCredentials` when the user logs out: ```typescript import { clearWidgetServerCredentials } from '@use-voltra/android-client' await clearWidgetServerCredentials() ``` All widgets are automatically reloaded after credentials are cleared, so they revert to their default/unauthenticated state immediately. ## Refresh button Server-driven widgets can display a native refresh button that lets users trigger an immediate update on demand. Enable it in your widget config: ```json { "serverUpdate": { "url": "https://api.example.com/widgets/render", "intervalMinutes": 60, "refresh": true } } ``` When enabled, a small circular button (↻) appears in the top-right corner of the widget. Tapping it performs an inline HTTP fetch, generates new `RemoteViews`, and pushes the update directly to the widget—all without waiting for the next WorkManager cycle. :::note The refresh callback bypasses Glance's `update()` method (which doesn't reliably trigger `provideGlance()`) and instead uses `GlanceRemoteViews.compose()` to generate `RemoteViews` that are pushed directly via `AppWidgetManager.updateAppWidget()`. ::: ## Resize handling Your server should return all size variants in every response. When the user resizes a widget on the home screen, Voltra re-renders from cached data—no network request is made. The `RemoteViews(sizeMapping)` mechanism automatically picks the closest matching variant. ## Triggering manual refreshes You can force-refresh server-driven widgets outside of the regular interval: ```typescript import { reloadAndroidWidgets } from '@use-voltra/android-client' // Reload specific widgets (triggers an immediate WorkManager fetch) await reloadAndroidWidgets(['dynamic_weather']) // Reload all widgets await reloadAndroidWidgets() ``` For server-driven widgets, this enqueues an immediate one-time WorkManager request to fetch fresh content. For local-only widgets, it re-renders from cached data. ## Initial state Server-driven widgets still need content to display before the first server fetch completes. Use `initialStatePath` to provide a pre-rendered default: ```json { "id": "dynamic_weather", "displayName": "Dynamic Weather", "description": "Weather with live server updates", "targetCellWidth": 2, "targetCellHeight": 1, "initialStatePath": "./widgets/android/weather-initial.tsx", "serverUpdate": { "url": "https://api.example.com/widgets/render", "intervalMinutes": 60 } } ``` See [Widget pre-rendering](/android/development/widget-pre-rendering.md) for details on creating initial state files. :::tip Provide a meaningful initial state (e.g. "Loading..." or placeholder content) rather than leaving it empty. The user sees this until the first server fetch succeeds. ::: ## Cross-platform server A single server can handle both iOS and Android requests using `createWidgetUpdateHandler` from `@use-voltra/server`: ```tsx import { Voltra } from '@use-voltra/ios' import { AndroidDynamicColors, VoltraAndroid } from '@use-voltra/android' import { createWidgetUpdateHandler } from '@use-voltra/server' const handler = createWidgetUpdateHandler({ renderIos: async (req) => { // Return WidgetVariants (systemSmall, systemMedium, etc.) return { systemSmall: Hello } }, renderAndroid: async (req) => { // Return AndroidWidgetVariants (size breakpoints) return [{ size: { width: 200, height: 100 }, content: Hello }] }, validateToken: async (token) => { // Shared token validation for both platforms return verifyJwt(token) }, }) ``` The handler uses the required `platform` query parameter to route requests to the correct render function. If you're serving the cross-platform endpoint from Node or Express, use `createWidgetUpdateNodeHandler()` or `createWidgetUpdateExpressHandler()` from `@use-voltra/server` instead. ## Architecture overview ``` ┌─────────────────┐ setWidgetServerCredentials() ┌─────────────────────────┐ │ React Native │ ─────────────────────────────► │ EncryptedSharedPrefs │ │ (main app) │ └─────────────────────────┘ └─────────────────┘ │ │ reads token ▼ ┌─────────────────┐ GET ?widgetId=X&platform=android&theme=Y ┌──────────────────┐ │ WorkManager │ ─────────────────────────────► │ Your Server │ │ (background) │ ◄───────────────────────────── │ (Voltra SSR) │ └─────────────────┘ JSON payload └──────────────────┘ │ ▼ AppWidgetManager (RemoteViews update) │ ▼ Home Screen Widget ``` WorkManager handles scheduling, network constraints, and retries. The background worker reads credentials from encrypted storage, makes the HTTP request, parses the response, generates `RemoteViews`, and pushes the update via `AppWidgetManager`. ## Error handling and retries WorkManager automatically handles failures with exponential backoff. After 5 consecutive failed attempts, the worker gives up to avoid infinite retry loops. The next periodic run will start fresh. - **Network unavailable:** The request is deferred until connectivity is restored (via `NetworkType.CONNECTED` constraint). - **Server errors (non-2xx):** The worker retries with exponential backoff, up to 3 attempts. - **Empty response:** The worker retries with exponential backoff, up to 3 attempts. - **Parse errors:** If the JSON is stored but parsing fails, the data is still saved so Glance can attempt to use it later. This counts as a success since the data is persisted. --- url: /android/development/styling.md --- # Styling You can style Voltra components on Android using React Native-style `style` props. These properties are automatically converted to Jetpack Compose Glance modifiers. For Android system-aware colors, use [`AndroidDynamicColors`](/android/development/dynamic-colors.md) from `@use-voltra/android` instead of snapshotting palette values in JavaScript. :::warning Glance Limitations Android widgets are built using **Jetpack Compose Glance**, which has a significantly more limited styling API compared to standard Compose or SwiftUI. Many common React Native style properties are either not supported or have limited support. ::: ## Supported Properties The following React Native style properties are supported on Android: ### Layout - `width`, `height` - Fixed dimensions (number values in dp) or `"100%"` to fill available space. - `flex`, `flexGrow` - Flex weight. When > 0, the component will take up a proportional amount of space in its parent container (maps to `.defaultWeight()` in Glance). - `padding` - Uniform padding on all edges. - `paddingTop`, `paddingBottom`, `paddingLeft`, `paddingRight` - Individual edge padding. - `paddingHorizontal`, `paddingVertical` - Horizontal and vertical padding. - `visibility` - Controls component visibility (`"visible"`, `"hidden"`, or `"invisible"`). ### Visual Style - `backgroundColor` - Background color (hex strings, color names, or `AndroidDynamicColors.*` tokens). - `backgroundImage` - CSS gradient background. Supports `linear-gradient(...)`, `radial-gradient(...)`, and `conic-gradient(...)`. - `borderRadius` - Corner radius value. **Note:** Requires Android 12+ (API 31). On older versions, this property is ignored. ### Text - `fontSize` - Font size in sp. - `fontWeight` - Supports `"normal"` and `"bold"`. - `fontFamily` - Font family name. Built-in values: `"monospace"`, `"serif"`, `"sans-serif"`, `"cursive"`. For custom fonts, see [Custom Fonts](/android/development/custom-fonts.md). - `color` - Text color (literal colors or `AndroidDynamicColors.*`). - `textDecorationLine` - Supports `"underline"` and `"line-through"`. - `textAlign` - Alignment of text within the component (`"left"`, `"center"`, `"right"`). - `numberOfLines` - Limits the number of lines displayed. ### Image Specific In addition to general styles, `Image` components support: - `resizeMode` - `"cover"`, `"contain"`, `"stretch"`, `"repeat"`, or `"center"`. - `contentScale` - `"crop"`, `"cover"`, `"fit"`, `"contain"`, `"fill-bounds"`, or `"stretch"`. - `alpha` - Opacity of the image (0.0 to 1.0). - `colorFilter` - Applies a color filter to the image. ## Dynamic colors Android widgets can use semantic Material color roles that resolve through native `GlanceTheme.colors.*` values during rendering. ```tsx import { AndroidDynamicColors, VoltraAndroid } from '@use-voltra/android' const element = ( Android Widget Text ) ``` This is the preferred approach when you want widgets to follow Android's dynamic palette even when the app is not running. See [Dynamic Colors](/android/development/dynamic-colors.md) for the full role list and server-rendering behavior. ## Gradient Backgrounds Android widgets support gradient backgrounds through the camel-case `style.backgroundImage` property. ```tsx import { VoltraAndroid } from '@use-voltra/android' const element = ( Gradient Widget ) ``` Supported gradient functions are `linear-gradient(...)`, `radial-gradient(...)`, and `conic-gradient(...)`. Repeating gradients, malformed gradients, unsupported color tokens, and invalid stop positions are ignored. If `backgroundColor` is also provided, Android paints it behind transparent gradient pixels and uses it as a fallback when a gradient cannot be rendered. Gradient stops can use Android dynamic color tokens from `AndroidDynamicColors`, but those colors are resolved into the generated bitmap when the widget renders or updates. Existing gradient bitmaps do not recolor until the widget is rendered again. Use `backgroundImage`, not `background-image`. Gradient bitmaps are generated natively during widget rendering and capped before being passed to Glance, so the bitmap does not control layout size. ## Limitations The following properties are **NOT supported** on Android due to Glance limitations: - **Margins:** `margin`, `marginTop`, etc. are not part of Android style types. If you need margin-like outside spacing, use `VoltraAndroid.Spacer` between elements. - **Borders:** `borderWidth` and `borderColor` are not yet implemented. - **Shadows:** `shadowColor`, `shadowOffset`, `shadowOpacity`, and `shadowRadius` are not supported. - **Positioning:** Absolute positioning (`top`, `left`, `zIndex`) is not supported. Use stack alignments and spacers. - **Transforms:** `transform` (rotate, scale, etc.) is not supported. - **Opacity:** The general `style.opacity` property is not supported (except for the `alpha` prop on `Image`). - **Dimensions:** `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, and `aspectRatio` are not supported. - **Text Effects:** `letterSpacing`, `fontVariant`, and custom `lineHeight` are not supported. ## Example ```tsx import { VoltraAndroid } from '@use-voltra/android' const element = ( Android Widget Text ) ``` --- url: /android/development/testing-and-previews.md --- # Testing and Previews (Android) Voltra provides multiple ways to preview your Android widgets: 1. **In-App Previews** - Preview layouts within your development app using `VoltraWidgetPreview` 2. **Widget Picker Previews** - Customize what users see in the Android widget picker when adding your widget This page covers in-app previews for development. For widget picker previews, see [Plugin Configuration - Widget Picker Previews](/android/api/plugin-configuration.md#widget-picker-previews). ## VoltraWidgetPreview The `VoltraWidgetPreview` component renders Voltra Android JSX content at the exact dimensions of standard Android widget sizes. ### Usage ```tsx import { VoltraAndroid } from '@use-voltra/android' import { VoltraWidgetPreview } from '@use-voltra/android-client' export function MyWidgetPreview() { return ( My Awesome Widget This is how it looks on the home screen! ) } ``` ### Supported Families Android widgets use responsive sizing. Voltra provides several standard families based on typical grid dimensions: | Family | DP Dimensions | Typical Grid Size | | :------------- | :------------ | :---------------- | | `small` | 150 x 100 | 2x1 | | `mediumSquare` | 200 x 200 | 2x2 | | `mediumWide` | 250 x 150 | 3x2 | | `mediumTall` | 150 x 250 | 2x3 | | `large` | 300 x 200 | 4x2 | | `extraLarge` | 350 x 300 | 4x4 | ## VoltraView (Android) If you need more control or want to test custom dimensions, you can use the low-level `VoltraView` component. ```tsx import { VoltraAndroid } from '@use-voltra/android' import { VoltraView } from '@use-voltra/android-client' Custom Preview ``` ## Accuracy The Android preview components use the **actual native Glance renderers** under the hood. When you provide JSX to `VoltraWidgetPreview`, it is converted to a native `RemoteViews` object and rendered using the same logic that Android uses on the home screen. This ensures that: - Layout constraints are respected. - Styling (colors, fonts, spacing) is accurate. - Component mapping is identical to the production widget. :::info While layout and styling are accurate, some home-screen specific behaviors (like actual widget resizing by the user) are not simulated by the preview component. ::: --- url: /android/development/widget-pre-rendering.md --- # Widget Pre-rendering (Android) Widget pre-rendering allows you to provide a meaningful initial state for your Android widgets before they are updated by the app for the first time. ## Configuration Add `initialStatePath` to your widget configuration in the `@use-voltra/android-client` plugin in `app.json`: ```json { "expo": { "plugins": [ [ "@use-voltra/android-client", { "widgets": [ { "id": "weather", "displayName": "Weather Widget", "description": "Shows current weather", "targetCellWidth": 2, "targetCellHeight": 2, "initialStatePath": "./widgets/weather-android-initial.tsx" } ] } ] ] } } ``` For multiple locales, set `initialStatePath` to a map of locale tag → file path (same pattern as iOS); the first-run payload matches the device locale when possible. ## Implementation Create a file at the specified `initialStatePath` that exports a default Voltra component (or a React element). For Android, this should use `VoltraAndroid` primitives. ```tsx import { VoltraAndroid } from '@use-voltra/android' const InitialWeatherWidget = ( Loading weather... ) export default InitialWeatherWidget ``` :::info `initialStatePath` files are **not** part of your React Native app bundle. They run in Node.js during prebuild. Import `VoltraAndroid` from `@use-voltra/android`, not `@use-voltra/android-client` — the client package pulls in native modules that are unavailable in the prebuild sandbox. ::: ## Build Process During the build process (`npx expo prebuild`), Voltra executes these initial state files in a Node.js environment to generate the static layouts that will be displayed when the widget is first added to the home screen. ## Limitations - **Environment**: The code runs in Node.js during build time, not on the device. - **Imports**: Use `@use-voltra/android` for JSX and types in `initialStatePath` files. Do not import from `@use-voltra/android-client` or other React Native client APIs. - **Static Content**: The initial state should represent a "loading" or "offline" state, as it won't have access to dynamic runtime data until the app runs. --- url: /android/setup.md --- # Android Setup Once you have [installed Voltra](/getting-started/installation.md) for Android, configure the Expo plugin and use `@use-voltra/android` for widget JSX. ## 1. Configure the Expo Plugin Add `@use-voltra/android-client` to your `app.json` or `app.config.js`: ```json { "expo": { "plugins": [ [ "@use-voltra/android-client", { "enableNotifications": true, "widgets": [ { "id": "my_widget", "displayName": "My First Widget", "description": "A simple Voltra widget", "targetCellWidth": 2, "targetCellHeight": 2 } ] } ] ] } } ``` See [Plugin Configuration](/android/api/plugin-configuration.md) for all options. ## 2. Prebuild for Android Update your native Android project: ```sh [npx] npx expo prebuild --platform android ``` ```sh [yarn] yarn dlx expo prebuild --platform android ``` ```sh [pnpm] pnpm dlx expo prebuild --platform android ``` ```sh [bunx] bunx expo prebuild --platform android ``` ## 3. Run the app ```bash npx expo run:android ``` Your widget should now be available in the Android widget picker! --- url: /getting-started/installation.md --- # Installation Voltra v2 ships as platform packages. In most apps, you install one package for JSX primitives and one package for runtime APIs per platform. ## 1. Install packages ### iOS (Live Activities and widgets) ```sh [npm] npm install @use-voltra/ios @use-voltra/ios-client ``` ```sh [yarn] yarn add @use-voltra/ios @use-voltra/ios-client ``` ```sh [pnpm] pnpm add @use-voltra/ios @use-voltra/ios-client ``` ```sh [bun] bun add @use-voltra/ios @use-voltra/ios-client ``` ```sh [deno] deno add npm:@use-voltra/ios npm:@use-voltra/ios-client ``` ### Android (Home Screen widgets and ongoing notifications) ```sh [npm] npm install @use-voltra/android @use-voltra/android-client ``` ```sh [yarn] yarn add @use-voltra/android @use-voltra/android-client ``` ```sh [pnpm] pnpm add @use-voltra/android @use-voltra/android-client ``` ```sh [bun] bun add @use-voltra/android @use-voltra/android-client ``` ```sh [deno] deno add npm:@use-voltra/android npm:@use-voltra/android-client ``` Use the platform package for JSX primitives: - `@use-voltra/ios` exports `Voltra` - `@use-voltra/android` exports `VoltraAndroid`, `AndroidDynamicColors`, and `AndroidOngoingNotification` Use the client package for React Native runtime APIs: - `@use-voltra/ios-client` exports `startLiveActivity`, `updateWidget`, `VoltraView`, `VoltraWidgetPreview`, and other app-side APIs - `@use-voltra/android-client` exports `updateAndroidWidget`, `requestPinAndroidWidget`, `reloadAndroidWidgets`, `VoltraView`, `VoltraWidgetPreview`, and other app-side APIs ### Server rendering (optional) Use these in your backend or SSR service-not in the React Native app bundle: - **iOS payloads:** `@use-voltra/ios-server` - **Android payloads:** `@use-voltra/android-server` - **Cross-platform widget HTTP handlers:** `@use-voltra/server` ## 2. Import patterns The docs use these package boundaries consistently: - React Native app code: JSX from `@use-voltra/ios` or `@use-voltra/android`, lifecycle APIs from the matching `*-client` package - Node/server code: JSX plus render helpers from `@use-voltra/ios-server` or `@use-voltra/android-server` - Pre-render files such as `initialStatePath`: import from `@use-voltra/ios` or `@use-voltra/android`, not from client packages ## 3. Next Steps After installing packages, configure the Voltra Expo plugin for each platform you support: - [Configure iOS Setup](/ios/setup.md) - [Configure Android Setup](/android/setup.md) - [Use Voltra in React Native CLI projects](/getting-started/react-native-cli.md) - [Migrate from v1 to v2](/getting-started/migration-v2.md) --- url: /getting-started/migration-v2.md --- # Migration to v2 Voltra v2 introduces two major architectural changes: - The old Voltra umbrella package is gone. Voltra now ships as separate iOS, Android, and server packages. - The native layer moved from Expo Modules to Turbo Modules. We made these changes to fix a few long-standing problems in the old package layout. The old umbrella package made it too easy for React Native code to leak into server builds, especially when people only wanted server-side rendering or pre-rendering. It also forced many apps to pull in both platform surfaces even when they only shipped iOS or only shipped Android. The Turbo Module migration also changes the native integration layer, so upgrading to v2 requires updating package installs, Expo plugin configuration, and some API usage. This guide walks through the package, import, and configuration changes you need to make when upgrading to v2. ## What changed ### Package split Old setups commonly used package paths like: - `voltra` - `voltra/client` - `voltra/server` - `voltra/android` - `voltra/android/client` - `voltra/android/server` v2 uses scoped packages instead: - `@use-voltra/ios` - `@use-voltra/ios-client` - `@use-voltra/ios-server` - `@use-voltra/android` - `@use-voltra/android-client` - `@use-voltra/android-server` - `@use-voltra/server` ### Platform-specific JSX namespaces The old docs implied one shared component namespace. v2 documents the real platform split: - iOS JSX primitives come from `@use-voltra/ios` as `Voltra` - Android JSX primitives come from `@use-voltra/android` as `VoltraAndroid` - Android semantic color tokens also live in `@use-voltra/android` as `AndroidDynamicColors` - Android ongoing notification JSX lives in `@use-voltra/android` as `AndroidOngoingNotification` ### Client APIs stay in `*-client` Use `@use-voltra/ios-client` and `@use-voltra/android-client` for runtime APIs that run inside the React Native app, such as: - starting or updating Live Activities - updating widgets - pinning Android widgets - preview components such as `VoltraView` and `VoltraWidgetPreview` - event listeners and hooks ### Server APIs are platform-specific Use platform server packages for server-side rendering: - `@use-voltra/ios-server` for Live Activities and iOS widgets - `@use-voltra/android-server` for Android widgets and ongoing notification payloads Use `@use-voltra/server` only for cross-platform widget HTTP handlers. ## Installation changes Install the packages that match the layers you use. ### iOS app code ```sh [npm] npm install @use-voltra/ios @use-voltra/ios-client ``` ```sh [yarn] yarn add @use-voltra/ios @use-voltra/ios-client ``` ```sh [pnpm] pnpm add @use-voltra/ios @use-voltra/ios-client ``` ```sh [bun] bun add @use-voltra/ios @use-voltra/ios-client ``` ```sh [deno] deno add npm:@use-voltra/ios npm:@use-voltra/ios-client ``` ### Android app code ```sh [npm] npm install @use-voltra/android @use-voltra/android-client ``` ```sh [yarn] yarn add @use-voltra/android @use-voltra/android-client ``` ```sh [pnpm] pnpm add @use-voltra/android @use-voltra/android-client ``` ```sh [bun] bun add @use-voltra/android @use-voltra/android-client ``` ```sh [deno] deno add npm:@use-voltra/android npm:@use-voltra/android-client ``` ### Optional server packages ```sh [npm] npm install @use-voltra/ios-server @use-voltra/android-server @use-voltra/server ``` ```sh [yarn] yarn add @use-voltra/ios-server @use-voltra/android-server @use-voltra/server ``` ```sh [pnpm] pnpm add @use-voltra/ios-server @use-voltra/android-server @use-voltra/server ``` ```sh [bun] bun add @use-voltra/ios-server @use-voltra/android-server @use-voltra/server ``` ```sh [deno] deno add npm:@use-voltra/ios-server npm:@use-voltra/android-server npm:@use-voltra/server ``` ## Import mapping ### iOS app code ```tsx // v1 import { Voltra } from 'voltra' import { startLiveActivity } from 'voltra/client' // v2 import { Voltra } from '@use-voltra/ios' import { startLiveActivity } from '@use-voltra/ios-client' ``` ### iOS server code ```tsx // v1 import { renderLiveActivityToString, Voltra } from 'voltra/server' // v2 import { Voltra, renderLiveActivityToString } from '@use-voltra/ios-server' ``` ### Android widget code ```tsx // v1 import { VoltraAndroid } from 'voltra/android' import { updateWidget } from 'voltra/android' // v2 import { VoltraAndroid } from '@use-voltra/android' import { updateAndroidWidget } from '@use-voltra/android-client' ``` ### Android server widget code ```tsx // v1 import { createWidgetUpdateNodeHandler } from 'voltra/server' import { AndroidDynamicColors, VoltraAndroid } from 'voltra/android' // v2 import { createAndroidWidgetUpdateNodeHandler } from '@use-voltra/android-server' import { AndroidDynamicColors, VoltraAndroid } from '@use-voltra/android' ``` ## Expo plugin migration The old docs used a single `voltra` plugin and nested per-platform config. v2 uses platform-specific plugins. ### iOS ```json // v1 { "expo": { "plugins": [ [ "voltra", { "ios": { "groupIdentifier": "group.example.app", "widgets": [] } } ] ] } } // v2 { "expo": { "plugins": [ [ "@use-voltra/ios-client", { "groupIdentifier": "group.example.app", "widgets": [] } ] ] } } ``` ### Android ```json // v1 { "expo": { "plugins": [ [ "voltra", { "android": { "widgets": [] } } ] ] } } // v2 { "expo": { "plugins": [ [ "@use-voltra/android-client", { "widgets": [] } ] ] } } ``` ## API renames to watch for - Android widget updates: `updateWidget(...)` -> `updateAndroidWidget(...)` - Android widget reloads: `reloadWidgets(...)` for image-preloading remains, but widget refresh APIs use `reloadAndroidWidgets(...)` - Android widget server handlers: `createWidgetUpdateHandler(...)` style examples should become `createAndroidWidgetUpdateHandler(...)`, `createAndroidWidgetUpdateNodeHandler(...)`, or `createAndroidWidgetUpdateExpressHandler(...)` - iOS widget server handlers: use `createIOSWidgetUpdateHandler(...)`, `createIOSWidgetUpdateNodeHandler(...)`, or `createIOSWidgetUpdateExpressHandler(...)` ## Pre-render files `initialStatePath` files run in Node during prebuild, not in the React Native runtime. Use: - `@use-voltra/ios` for iOS widget JSX and types - `@use-voltra/android` for Android widget JSX and types Do not import `@use-voltra/ios-client` or `@use-voltra/android-client` from pre-render files. ## Recommended migration order 1. Replace package installs. 2. Swap the Expo plugin to the platform-specific package. 3. Move JSX imports to `@use-voltra/ios` or `@use-voltra/android`. 4. Move runtime APIs to the matching `*-client` package. 5. Move server rendering code to `@use-voltra/ios-server` or `@use-voltra/android-server`. 6. Rename Android widget APIs that became platform-specific. 7. Verify `initialStatePath` files only import from platform packages. ## After migrating Re-run prebuild for any platform whose plugin configuration changed: ```bash npx expo prebuild --platform ios npx expo prebuild --platform android ``` --- url: /getting-started/prior-art.md --- # Prior Art Voltra wouldn't be possible without the incredible work of the open-source community. This page acknowledges the libraries and projects that inspired and informed our approach to bridging JavaScript and native iOS Live Activities. ## Inspiration ### Dynamic UI The core concept of describing SwiftUI layouts through JSON configuration was pioneered by Wesley de Groot's [Dynamic UI](https://github.com/0xWDG/DynamicUI). ### Expo Live Activity The insight of combining JavaScript-driven development with iOS Live Activities came from Software Mansion's [Expo Live Activity](https://github.com/software-mansion-labs/expo-live-activity). Referencing their open-source Expo config plugin logic was helpful during early development. ## Thank You We extend our gratitude to the following organizations and individuals: - **Wesley de Groot** for creating Dynamic UI and demonstrating the power of JSON-driven SwiftUI interfaces. - **Software Mansion** for open-sourcing their Expo Live Activity library. - **Expo** for building Expo Modules that power Voltra behind the scenes. - **The broader open-source community** for all the libraries, tools, and ideas that make innovative projects like Voltra possible in the first place. Voltra is our contribution back to the broader React Native community. We hope it helps developers create more engaging and interactive Live Activities and Widgets for their iOS applications. *** _If you're working on something that builds upon Voltra, we'd love to hear about it! Feel free to reach out and let us know how you're using it._ --- url: /getting-started/react-native-cli.md --- # React Native CLI Projects :::warning Experimental Voltra support for React Native CLI projects is experimental. Feedback is welcome in [GitHub issues](https://github.com/callstackincubator/voltra/issues). ::: Voltra fully supports React Native CLI projects through the `voltra` CLI. Instead of relying on Expo config plugins, `voltra apply` updates the native project for you: it modifies the files Voltra needs, generates new Voltra-owned files, and cleans up outdated generated files from previous runs. ## Installation Install the same native Voltra packages you would use in Expo for the platforms you support, then add `voltra` as a dev dependency. ### iOS ```sh [npm] npm install @use-voltra/ios @use-voltra/ios-client ``` ```sh [yarn] yarn add @use-voltra/ios @use-voltra/ios-client ``` ```sh [pnpm] pnpm add @use-voltra/ios @use-voltra/ios-client ``` ```sh [bun] bun add @use-voltra/ios @use-voltra/ios-client ``` ```sh [deno] deno add npm:@use-voltra/ios npm:@use-voltra/ios-client ``` ### Android ```sh [npm] npm install @use-voltra/android @use-voltra/android-client ``` ```sh [yarn] yarn add @use-voltra/android @use-voltra/android-client ``` ```sh [pnpm] pnpm add @use-voltra/android @use-voltra/android-client ``` ```sh [bun] bun add @use-voltra/android @use-voltra/android-client ``` ```sh [deno] deno add npm:@use-voltra/android npm:@use-voltra/android-client ``` ### Voltra CLI ```sh [npm] npm install --save-dev voltra ``` ```sh [yarn] yarn add --save-dev voltra ``` ```sh [pnpm] pnpm add --save-dev voltra ``` ```sh [bun] bun add --save-dev voltra ``` ```sh [deno] deno add --save-dev npm:voltra ``` Then create a `voltra.config.ts` file. Most properties under `ios` and `android` are the same as the Expo config equivalents, so use the existing platform docs for the shared configuration: - [iOS Expo config options](/ios/api/plugin-configuration.md) - [Android Expo config options](/android/api/plugin-configuration.md) Minimal example: ```ts import type { VoltraConfig } from 'voltra' const config: VoltraConfig = { ios: { // Commonly needed on iOS so the app and extension can share data. groupIdentifier: 'group.com.example.app', widgets: [ { // Basic widget identity fields. id: 'portfolio', displayName: 'Portfolio', description: 'Track holdings', // Optional, but commonly used for build-time initial widget content. initialStatePath: './widgets/portfolio.ios.tsx', }, ], }, android: { widgets: [ { id: 'portfolio', displayName: 'Portfolio', description: 'Track holdings', // Required on Android. targetCellWidth: 2, targetCellHeight: 2, initialStatePath: './widgets/portfolio.android.tsx', }, ], }, } export default config ``` Once the config is in place, apply the native project changes: ```sh [npx] npx voltra apply ``` ```sh [yarn] yarn voltra apply ``` ```sh [pnpm] pnpm voltra apply ``` ```sh [bunx] bunx voltra apply ``` ## Configuration Most properties under `ios` and `android` use the same names as the Expo config equivalents: - [iOS Expo config options](/ios/api/plugin-configuration.md) - [Android Expo config options](/android/api/plugin-configuration.md) The properties below are specific to the Voltra CLI: | Property | Purpose | | ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `projectRoot` | Overrides the root directory Voltra should treat as the native project root. Useful when the config file does not live at the app root. | | `ios.userImagesPath` | Overrides the directory for user-provided iOS widget images. | | `ios.project` | Lets you override iOS project discovery when the app does not use the standard React Native layout. Supports fields such as `rootDir`, `xcodeprojPath`, `mainTargetName`, `infoPlistPath`, `entitlementsPath`, and `podfilePath`. | | `android.userImagesPath` | Overrides the directory for user-provided Android widget images. | | `android.project` | Lets you override Android project discovery when the app does not use the standard React Native layout. Supports fields such as `rootDir`, `appModuleName`, `manifestPath`, and `packageName`. | For the full config shape, see [`VoltraConfig` and related types in source](https://github.com/callstackincubator/voltra/blob/main/packages/cli/src/config/types.ts). ## Using Voltra CLI Every time you change `voltra.config.ts`, reapply the native project changes: ```sh [npx] npx voltra apply ``` ```sh [yarn] yarn voltra apply ``` ```sh [pnpm] pnpm voltra apply ``` ```sh [bunx] bunx voltra apply ``` The JSX APIs and runtime APIs are the same as in the rest of the Voltra docs. Once `voltra apply` has set up your native project, continue with the platform guides for [iOS](/ios/introduction.md) and [Android](/android/introduction.md). --- url: /index.md --- # Live Activities & Widgets in React > Voltra lets React Native developers build native Live Activities and widgets on iOS and Android using React components — no Swift or Kotlin required. It supports hot reload, push updates on iOS, and a config plugin that wires everything automatically. [Get Started](/getting-started/introduction) ## Features - **Native Primitives in JSX**: Compose native interfaces using SwiftUI (iOS) and Jetpack Compose Glance (Android) primitives directly in JSX. - **Live Activities & Widgets**: Build Dynamic Island experiences and Live Activities for iOS, plus Home Screen widgets for both iOS and Android using a unified React workflow. - **Push-to-update for Live Activities**: Stream real-time updates to Live Activities via push notifications from any JavaScript runtime. Keep activities current without app interaction. --- url: /ios/api/configuration.md --- # Configuration Voltra provides several configuration options to control Live Activity behavior, lifecycle, and appearance. These options can be used when starting, updating, or stopping Live Activities. For Expo plugin configuration options (like `groupIdentifier`, `enablePushNotifications`, `deploymentTarget`, and `widgets`), see the [Plugin Configuration](/ios/api/plugin-configuration.md) documentation. ## Dismissal Policy Voltra supports configuring how Live Activities behave after they end. You can control the dismissal timing using the `dismissalPolicy` option: ### Dismissal Policy Options - **`'immediate'`** (default): The Live Activity is dismissed immediately when it ends - **`{ after: number }`**: The Live Activity remains visible for the specified number of seconds after ending, then automatically dismisses ### Usage Examples **Immediate dismissal (default behavior):** ```typescript import { startLiveActivity } from '@use-voltra/ios-client' await startLiveActivity(variants, { dismissalPolicy: 'immediate', // or omit for default }) ``` **Delayed dismissal (keep visible for 30 seconds after ending):** ```typescript await startLiveActivity(variants, { dismissalPolicy: { after: 30 }, }) ``` **Update dismissal policy for active Live Activities:** ```typescript import { updateLiveActivity } from '@use-voltra/ios-client' await updateLiveActivity(activityId, variants, { dismissalPolicy: { after: 60 }, }) ``` **Set dismissal policy when ending a Live Activity:** ```typescript import { stopLiveActivity } from '@use-voltra/ios-client' await stopLiveActivity(activityId, { dismissalPolicy: { after: 10 }, }) ``` The dismissal policy applies to both programmatic ending (`stopLiveActivity`) and natural ending (when timers reach their end time). This gives you fine-grained control over the user experience when Live Activities conclude. ## Additional Configuration Options Voltra provides additional configuration options to control Live Activity behavior and appearance. ### Stale Date The `staleDate` option allows you to specify when a Live Activity should be considered stale and automatically dismissed by the system. ```typescript import { startLiveActivity } from '@use-voltra/ios-client' // Dismiss the Live Activity after 1 hour await startLiveActivity(variants, { staleDate: Date.now() + 60 * 60 * 1000, // 1 hour from now }) ``` **Note:** If you provide a `staleDate` in the past, it will be ignored and the Live Activity will use default behavior. ### Relevance Score The `relevanceScore` option helps iOS prioritize which Live Activities to display when space is limited. Higher scores (closer to 1.0) indicate more important activities. ```typescript import { startLiveActivity } from '@use-voltra/ios-client' // High priority Live Activity (e.g., active delivery) await startLiveActivity(variants, { relevanceScore: 0.8, }) // Low priority Live Activity (e.g., background task) await startLiveActivity(variants, { relevanceScore: 0.2, }) ``` **Valid range:** 0.0 to 1.0 (default: 0.0) ### Channel ID (broadcast push, iOS 18+) The `channelId` option subscribes the Live Activity to a broadcast channel for server-side updates. When provided, the activity receives updates via broadcast push notifications instead of individual device tokens—one server notification updates all activities on that channel. Requires `enablePushNotifications: true` and the Broadcast Capability enabled in your Apple Developer account. ```typescript import { startLiveActivity } from '@use-voltra/ios-client' await startLiveActivity(variants, { activityName: 'match-123', channelId: 'CTrNsYq/Ee8AALLzHQaVlA==', // From APNs channel management }) ``` For full broadcast setup, see [Server-side updates - Broadcast push notifications](/ios/development/server-side-updates.md#broadcast-push-notifications-ios-18). These options can be used together with dismissal policy and other configuration options: ```typescript await startLiveActivity(variants, { dismissalPolicy: { after: 30 }, staleDate: Date.now() + 2 * 60 * 60 * 1000, // 2 hours relevanceScore: 0.7, }) ``` --- url: /ios/api/plugin-configuration.md --- # Plugin configuration The Voltra Expo config plugin accepts several configuration options in your `app.json` or `app.config.js`: ```json { "expo": { "plugins": [ [ "@use-voltra/ios-client", { "groupIdentifier": "group.your.bundle.identifier", "enablePushNotifications": true, "deploymentTarget": "18.0", "targetName": "MyAppLiveActivity", "widgets": [ { "id": "weather", "displayName": "Weather Widget", "description": "Shows current weather conditions", "supportedFamilies": ["systemSmall", "systemMedium", "systemLarge"], "initialStatePath": "./widgets/weather-initial.tsx" } ] } ] ] } } ``` ## Configuration options ### `groupIdentifier` (optional) App Group identifier for sharing data between your app and the widget extension. Required if you want to: - Forward component events (like button taps) from Live Activities to your JavaScript code - Share images between your app and the extension - Use image preloading features **Format:** Must start with `group.` (e.g., `group.your.bundle.identifier`) ### `enablePushNotifications` (optional) Enable server-side updates for Live Activities via Apple Push Notification Service (APNS). When enabled, you can update Live Activities even when your app is in the background or terminated. **Type:** `boolean`\ **Default:** `false` ### `deploymentTarget` (optional) iOS deployment target version for the widget extension. If not provided, defaults to `17.0`. This allows the widget extension to have its own deployment target independent of the main app. **Type:** `string` **Default:** `"17.0"` **Example:** `"18.0"` **Note:** Code signing settings (development team, provisioning profiles) are automatically synchronized from the main app target, but the deployment target can be set independently. ### `targetName` (optional) Custom target name for the widget extension. If not provided, defaults to `{AppName}LiveActivity` where `AppName` is your app's sanitized name. This is useful when: - Migrating from other Live Activity solutions (e.g., `@bacons/apple-targets`) - Matching existing provisioning profiles or credentials - Using a specific naming convention for your organization **Type:** `string` **Default:** `"{AppName}LiveActivity"` **Example:** `"widget"`, `"MyAppLiveActivity"` ```json { "expo": { "plugins": [ [ "@use-voltra/ios-client", { "groupIdentifier": "group.your.bundle.identifier", "targetName": "widget" } ] ] } } ``` ### `widgets` (optional) Array of widget configurations for Home Screen widgets. Each widget will be available in the iOS widget gallery. **Widget Configuration Properties:** - `id`: Unique identifier for the widget (alphanumeric with underscores only) - `displayName`: Name shown in the widget gallery (plain string, or per-locale map like `{ "en": "Weather", "pl": "Pogoda" }`; locale keys are BCP‑47-style tags) - `description`: Description shown in the widget gallery (same localization rules as `displayName`) - `supportedFamilies`: Array of supported widget sizes (`systemSmall`, `systemMedium`, `systemLarge`) - `initialStatePath`: (optional) Project-relative path to a file that exports initial widget state, **or** a locale map of paths for localized build-time pre-rendering (see [Widget Pre-rendering](/ios/development/widget-pre-rendering.md)) - `serverUpdate`: (optional) Enable server-driven updates. See [Server-driven widgets](/ios/development/server-driven-widgets.md) for full details. - `url`: The Voltra SSR endpoint URL - `intervalMinutes`: Update interval in minutes (default: `15`) - `refresh`: Show a native refresh button (default: `false`, requires iOS 17+) **Example:** ```json { "widgets": [ { "id": "weather", "displayName": "Weather Widget", "description": "Current weather conditions", "supportedFamilies": ["systemSmall", "systemMedium", "systemLarge"], "initialStatePath": { "en": "./widgets/weather-initial.tsx", "pl": "./widgets/weather-initial-pl.tsx" }, "serverUpdate": { "url": "https://api.example.com/widgets/render", "intervalMinutes": 30, "refresh": true } } ] } ``` ### Localizing `displayName` and `description` Use a locale map when the widget gallery label should be translated: ```json { "widgets": [ { "id": "weather", "displayName": { "en": "Weather", "pl": "Pogoda", "zh-Hans": "天气" }, "description": { "en": "Current weather conditions", "pl": "Aktualne warunki pogodowe", "zh-Hans": "当前天气状况" } } ] } ``` Use BCP-47-style locale tags such as `en`, `en-US`, `pt-BR`, or `zh-Hans`. Fallback behavior: - Voltra first tries the device locale. - If there is no exact match, it falls back to the language-only match. - If there is still no match, it prefers an English locale such as `en` or `en-US`. - If no English entry exists, it uses the first configured locale. --- url: /ios/charts.md --- # Charts (iOS) Use charts in Live Activities and widgets to show trends, comparisons, progress, or composition at a glance. You can mix bars, lines, areas, points, rules, and sectors in a single chart. :::info Charts require iOS 16.0+. SectorMark (pie/donut) requires iOS 17.0+. ::: :::warning Mark components (`BarMark`, `LineMark`, etc.) must be **direct children** of ``. They cannot be wrapped in custom components. For example, this will not work: ```tsx // This won't work — marks are not direct children function MyMarks() { return } ``` Instead, always place marks directly inside Chart: ```tsx ``` ::: ## Basic Usage Wrap one or more mark components inside ``: ```tsx ``` ## Data Types All marks except `RuleMark` take a `data` prop. There are two data shapes depending on the mark type: ```typescript // BarMark, LineMark, AreaMark, PointMark type ChartDataPoint = { x: string | number // Categorical ("Jan") or numeric (42) y: number series?: string // Optional — groups data for multi-series charts } // SectorMark type SectorDataPoint = { value: number // Proportional angular value category: string // Sector label } ``` ## Marks ### BarMark Use bars when people need to compare values across categories. **Parameters:** - `data` (ChartDataPoint\[], required): The data points. - `color` (string, optional): Fill color. - `cornerRadius` (number, optional): Rounded bar corners. - `width` (number, optional): Fixed bar width. - `stacking` (string, optional): `"grouped"` (side by side). ```tsx ``` *** ### LineMark Use a line when the shape of change matters more than individual columns. **Parameters:** - `data` (ChartDataPoint\[], required): The data points. - `color` (string, optional): Line color. - `lineWidth` (number, optional): Stroke width in points. - `interpolation` (string, optional): Curve type — `"linear"`, `"monotone"`, `"catmullRom"`, `"cardinal"`, `"stepStart"`, `"stepCenter"`, or `"stepEnd"`. - `symbol` (string, optional): Symbol at each point — `"circle"`, `"square"`, `"triangle"`, `"diamond"`, `"pentagon"`, `"cross"`, `"plus"`, or `"asterisk"`. ```tsx ``` *** ### AreaMark Use an area chart when you want the overall volume or rise/fall pattern to read quickly. **Parameters:** - `data` (ChartDataPoint\[], required): The data points. - `color` (string, optional): Fill color. - `interpolation` (string, optional): Same options as LineMark. ```tsx ``` *** ### PointMark Use points for sparse measurements, scatter plots, or to emphasize exact observations. **Parameters:** - `data` (ChartDataPoint\[], required): The data points. - `color` (string, optional): Point color. - `symbol` (string, optional): Same options as LineMark. - `symbolSize` (number, optional): Symbol size in points. ```tsx ``` *** ### RuleMark A horizontal or vertical reference line. Unlike other marks, RuleMark has no `data` array. **Parameters:** - `yValue` (number, optional): Draw a horizontal line at this y value. - `xValue` (string | number, optional): Draw a vertical line at this x value. - `color` (string, optional): Line color. - `lineWidth` (number, optional): Stroke width in points. If both `xValue` and `yValue` are provided, Voltra renders both lines. ```tsx ``` *** ### SectorMark Pie and donut charts. Requires iOS 17+. **Parameters:** - `data` (SectorDataPoint\[], required): Sector data with `value` and `category`. - `color` (string, optional): Fill color (overrides automatic coloring). - `innerRadius` (number, optional): Ratio from 0 to 1. `0` = pie chart, any value above `0` = donut chart. - `outerRadius` (number, optional): Ratio from 0 to 1. - `angularInset` (number, optional): Gap between sectors in degrees. ```tsx // Pie chart // Donut chart ``` ## Chart Props The `` container accepts these props in addition to the standard Voltra `style` prop: | Prop | Type | Description | | ---------------------- | ---------------------------------------------------------------------------- | -------------------------- | | `xAxisVisibility` | `"automatic" \| "visible" \| "hidden"` | Show or hide the x-axis | | `xAxisGridStyle` | `{ visible?: boolean; color?: string; lineWidth?: number; dash?: number[] }` | Control x-axis grid lines | | `yAxisVisibility` | `"automatic" \| "visible" \| "hidden"` | Show or hide the y-axis | | `yAxisGridStyle` | `{ visible?: boolean; color?: string; lineWidth?: number; dash?: number[] }` | Control y-axis grid lines | | `legendVisibility` | `"automatic" \| "visible" \| "hidden"` | Show or hide the legend | | `foregroundStyleScale` | `Record` | Map series names to colors | ## Grid Lines Use `xAxisGridStyle` and `yAxisGridStyle` when the default grid is too busy or not visible enough for your design: ```tsx ``` ## Multi-Series Charts Add a `series` field to your data points when you want multiple datasets in the same chart. Use `foregroundStyleScale` to keep those series colors consistent: ```tsx ``` Without `foregroundStyleScale`, colors are chosen automatically. For side-by-side bars, set `stacking="grouped"`: ```tsx ``` ## Combining Marks Mix mark types when one chart needs both context and emphasis, such as bars for totals plus a rule for a target: ```tsx ``` ## Sparkline / Minimal Style Hide axes and legend for a clean, compact visualization: ```tsx ``` ## Widget / Live Activity Notes Charts in widgets and Live Activities are static. Focus on a clear snapshot of the data rather than interactions like panning or scrolling. The `` component supports the full Voltra style system on its container — padding, background, borders, corner radius, shadows, and sizing all work: ```tsx ``` --- url: /ios/components/interactive.md --- # Interactive Controls (iOS) User interface controls that respond to user interaction in Live Activities. *** ## Button An interactive button component that triggers in-app events via interaction intents. **Parameters:** - `buttonStyle` (string, optional): Visual style of the button: - `"automatic"` - System-determined style - `"bordered"` - Bordered style - `"borderedProminent"` - Bordered with prominent fill - `"plain"` - Plain style without border - `"borderless"` - Borderless style **Apple Documentation:** [Button](https://developer.apple.com/documentation/swiftui/button) **Availability:** iOS 17.0+ (interaction intents) ### Usage Buttons fire interaction events that you can handle in your app: ```tsx Play Music ``` Handle the event: ```typescript import { addVoltraListener } from '@use-voltra/ios-client' const subscription = addVoltraListener('interaction', (event) => { if (event.identifier === 'play-button') { // Handle play action } }) ``` ### Examples **Styled button:** ```tsx Save Changes ``` **Button with icon:** ```tsx Delete ``` **Compact button:** ```tsx ``` *** ## Link A navigable link component that opens a URL when tapped. Uses SwiftUI's native Link for semantic navigation. **Parameters:** - `destination` (string, required): URL to navigate to when tapped. Supports both absolute URLs and relative paths. **Apple Documentation:** [Link](https://developer.apple.com/documentation/swiftui/link) **Availability:** iOS 14.0+ ### URL Normalization Link automatically normalizes URLs using your app's URL scheme: - Absolute URLs: Used as-is (`"myapp://orders/123"`, `"https://example.com"`) - Relative with `/`: `"/settings"` → `"myapp://settings"` - Relative without `/`: `"help"` → `"myapp://help"` ### Examples **Link with absolute URL:** ```tsx Order #123 Tap to view details ``` **Link with relative path:** ```tsx Open Settings ``` **External link:** ```tsx Visit Support Site ``` ### When to use Link vs Button | Feature | Link | Button | | ---------------- | ------------------------------ | ------------------------------------------ | | **Use Case** | Navigation to URLs | In-app actions/events | | **Visual** | Unstyled (custom via children) | Button styling (bordered, prominent, etc.) | | **iOS Version** | 14.0+ | 17.0+ | | **Tap Behavior** | Opens URL | Fires interaction event | | **Mechanism** | SwiftUI Link | AppIntents (VoltraInteractionIntent) | **Recommendation:** Use `Link` for navigation (e.g., list items, cards that open URLs). Use `Button` for actions that your app needs to handle (e.g., play/pause, save, delete). *** ## Toggle Toggles a boolean state via an intent. Fires an interaction event when changed. **Parameters:** - `defaultValue` (boolean, optional): Initial toggle state (default: `false`) **Apple Documentation:** [Toggle](https://developer.apple.com/documentation/swiftui/toggle) **Availability:** iOS 17.0+ **Example:** ```tsx ``` Handle toggle events: ```typescript import { addVoltraListener } from '@use-voltra/ios-client' const subscription = addVoltraListener('interaction', (event) => { if (event.identifier === 'notifications-toggle') { // Handle toggle state change } }) ``` --- url: /ios/components/layout.md --- # Layout & Containers (iOS) Components that arrange other elements or provide structural grouping. ## Alignment & Positioning Voltra uses SwiftUI's native positioning model. Instead of CSS-style `position: absolute` with `top`/`left`/`right`/`bottom`, you use: 1. **Stack `alignment` props** - Position children within their container 2. **`offsetX`/`offsetY` styles** - Fine-tune individual element positions Each stack type has different alignment options based on its layout direction. *** ### VStack A vertical stack container that arranges its children in a column. **Parameters:** - `spacing` (number, optional): Spacing between children in points - `alignment` (string, optional): Horizontal alignment of children: - `"leading"` - Align to left edge - `"center"` (default) - Align to center - `"trailing"` - Align to right edge **Apple Documentation:** [VStack](https://developer.apple.com/documentation/swiftui/vstack) *** ### HStack A horizontal stack container that arranges its children in a row. **Parameters:** - `spacing` (number, optional): Spacing between children in points - `alignment` (string, optional): Vertical alignment of children: - `"top"` - Align to top edge - `"center"` (default) - Align to center - `"bottom"` - Align to bottom edge - `"firstTextBaseline"` - Align to first text baseline - `"lastTextBaseline"` - Align to last text baseline **Apple Documentation:** [HStack](https://developer.apple.com/documentation/swiftui/hstack) *** ### ZStack A depth-based stack container that overlays its children on top of each other. Use ZStack when you need to layer elements, such as placing a badge over an image. **Parameters:** - `alignment` (string, optional): Positions ALL children at the specified alignment point. Available values: - `"center"` (default) - Center of the stack - `"leading"` - Left edge (or right in RTL) - `"trailing"` - Right edge (or left in RTL) - `"top"` - Top edge - `"bottom"` - Bottom edge - `"topLeading"` - Top-left corner - `"topTrailing"` - Top-right corner - `"bottomLeading"` - Bottom-left corner - `"bottomTrailing"` - Bottom-right corner **Apple Documentation:** [ZStack](https://developer.apple.com/documentation/swiftui/zstack) #### Positioning with ZStack In SwiftUI (and Voltra), positioning works differently than CSS. The `alignment` prop on ZStack positions **all children** at the same alignment point. The ZStack's size is determined by its largest child. **Example: Badge overlay** ```tsx {/* Image defines the ZStack size */} {/* Badge is positioned at top-right, then nudged with offset */} 3 ``` :::tip Use `offsetX` and `offsetY` style properties to fine-tune individual element positions after alignment. Positive `offsetX` moves right, positive `offsetY` moves down. ::: *** ### View A flexible container component that **always uses flexbox layout**. Unlike VStack and HStack which use native SwiftUI stacks by default, View is specifically designed for React Native-style flexbox layouts. :::tip Flexbox-First Component The View component is purpose-built for flexbox layouts and always uses the flexbox layout engine. See the [Flexbox Layout](/ios/development/flexbox-layout.md) guide for comprehensive documentation. ::: **Style Properties:** View responds to flexbox style properties set via the `style` prop: - `flexDirection`: `'row'` | `'column'` (default: `'column'`) - `alignItems`: `'flex-start'` | `'center'` | `'flex-end'` | `'stretch'` - `justifyContent`: `'flex-start'` | `'center'` | `'flex-end'` | `'space-between'` | `'space-around'` | `'space-evenly'` - `gap`: Spacing between children in points **Example:** ```tsx // Horizontal layout with space between Left Center Right // Vertical layout with centered items Success ``` **When to use View:** - You need React Native-style flexbox behavior - You want dynamic `flexDirection` (switching between row/column) - You need `justifyContent` spacing modes **When to use VStack/HStack:** - Simple vertical or horizontal layouts - You want SwiftUI's native stack performance - You need SwiftUI-specific alignment (like firstTextBaseline) **Availability:** iOS 16.0+ **Learn More:** [Flexbox Layout Guide](/ios/development/flexbox-layout.md) *** ### Spacer A flexible space component that expands to fill available space in its container. **Parameters:** - `minLength` (number, optional): Minimum length **Apple Documentation:** [Spacer](https://developer.apple.com/documentation/swiftui/spacer) *** ### GroupBox A grouped content container that visually groups related content with a styled background. **Parameters:** None **Apple Documentation:** [GroupBox](https://developer.apple.com/documentation/swiftui/groupbox) *** ### GlassContainer A Liquid Glass container that wraps Apple's [`GlassEffectContainer`](https://developer.apple.com/documentation/swiftui/glasseffectcontainer) to provide a modern glassmorphism effect for grouping content. :::warning iOS 26 SDK Required This component uses Apple's `GlassEffectContainer` API which requires **Xcode with iOS 26 SDK** to build. If you're using an older Xcode version, you'll encounter build errors: ``` - value of type 'some View' has no member 'glassEffect' - cannot find 'GlassEffectContainer' in scope ``` **Workaround:** Avoid using this component until iOS 26 SDK is available in your Xcode version. At runtime, devices with iOS \< 26 will gracefully fall back to a regular container without the glass effect. ::: **Parameters:** - `spacing` (number, optional): Spacing between glass elements **Availability:** - **Build:** Requires Xcode with iOS 26 SDK (uses `GlassEffectContainer` API) - **Runtime:** iOS 26+ for glass effect, graceful fallback on earlier versions **Apple Documentation:** [GlassEffectContainer](https://developer.apple.com/documentation/swiftui/glasseffectcontainer) --- url: /ios/components/overview.md --- # Components Overview (iOS) Voltra provides SwiftUI primitives with JSX bindings, allowing developers to create rich, interactive Live Activities using React/JSX syntax. These components connect web development workflows with native iOS Live Activity rendering. ## Getting Started Import iOS primitives from `@use-voltra/ios` and use the `Voltra` namespace: ```tsx import { Voltra } from '@use-voltra/ios' const MyComponent = () => { // Use any component return ( Hello Live Activity! Tap me ) } ``` ## Component Categories Voltra organizes its components into categories: ### Layout & Containers Components that arrange other elements or provide structural grouping. These include stacks (VStack, HStack, ZStack), spacers, and container components like GroupBox and GlassContainer. [See all layout & container components →](/ios/components/layout.md) ### Visual Elements & Typography Static or decorative elements used to display content. This category includes Text, Label, Image, Symbol, and visual effects like LinearGradient, Mask, and Divider. [See all visual elements & typography components →](/ios/components/visual.md) ### Data Visualization & Status Components for displaying data and status information. This includes progress indicators (LinearProgressView, CircularProgressView), gauges, and timers for showing dynamic information in Live Activities. [See all data visualization & status components →](/ios/components/status.md) ### Interactive Controls & Navigation User interface controls that respond to user interaction and navigation. This category includes Button and Toggle components for interactive Live Activities, plus Link for semantic URL navigation. [See all interactive control & navigation components →](/ios/components/interactive.md) --- url: /ios/components/status.md --- # Data Visualization & Status (iOS) Components specifically designed to show dynamic values or states over time in Live Activities and Widgets. ### LinearProgressView A horizontal progress bar that displays determinate progress or timer-based progress. #### Limitations - When using `timerInterval` for smooth animations, custom styling properties such as `height`, `trackColor`, `cornerRadius`, and the `thumb` component are ignored. The component will use the default system appearance in this mode. *** ### CircularProgressView A circular progress indicator that displays determinate progress or timer-based progress. #### Limitations - While `timerInterval` is supported, the progress ring will not animate continuously. It only updates its visual state when the component state is refreshed. For a smooth, live-animating progress bar, use `LinearProgressView`. *** ### Gauge A gauge indicator for progress visualization (iOS 16+). *** ### Timer A flexible component for displaying live-updating time intervals. Crucial for Live Activities, it uses native SwiftUI text interpolation to ensure the time updates automatically on the lock screen and in the Dynamic Island without requiring background updates from React Native. **Modes:** - **Timer Mode:** For fixed intervals (countdowns or counting up to a target). Requires `endAtMs` or `durationMs`. - **Stopwatch Mode:** For open-ended intervals counting up from a starting point. Requires `startAtMs` and `direction="up"`, but both `endAtMs` and `durationMs` must be omitted. **Parameters:** - `startAtMs` (number, optional): Start time in milliseconds since epoch. - `endAtMs` (number, optional): End time in milliseconds since epoch. - `durationMs` (number, optional): Duration in milliseconds. Used if `endAtMs` is omitted. - `direction` (string, optional): Count direction. Can be `'up'` or `'down'`. Defaults to `'down'`. - `textStyle` (string, optional): Formatting style. - `'timer'`: Standard clock format (e.g., `05:00`). - `'relative'`: Relative format (e.g., `5m`). - `showHours` (boolean, optional): Whether to show hours (e.g., `1:30:00` vs `90:00`). Defaults to `false`. - `textTemplates` (string, optional): JSON-encoded object with `running` and `completed` templates. Use `{time}` as a placeholder. **Examples:** ```tsx // Timer Mode: Countdown 5 minutes // Stopwatch Mode: Count up indefinitely from now // Relative Timer with Template ``` --- url: /ios/components/visual.md --- # Visual Elements & Typography (iOS) Static or decorative elements used to display content. ### Text Displays text content. **Parameters:** - `numberOfLines` (number, optional): Maximum number of lines to display *** ### Label A semantic label that can display both an icon and title text. **Parameters:** - `title` (string, optional): Text content for the label - `systemImage` (string, optional): SF Symbol name for the label icon *** ### Image Displays bitmap images from the asset catalog or base64 encoded data. **Parameters:** - `source` (object, optional): Image source object (`assetName` or `base64`) - `resizeMode` (string, optional): `"cover"`, `"contain"`, `"stretch"`, `"repeat"`, or `"center"` - `fallback` (ReactNode, optional): Custom content rendered when the image is missing **Styling the fallback:** To add a background color when an image is missing, use `backgroundColor` in the `style` prop: ```jsx ``` *** ### Symbol Displays SF Symbols (system icons) with configuration options. *** ### Divider A visual divider component. *** ### LinearGradient A linear gradient background that can contain children. *** ### Mask Masks content using any Voltra element as the mask shape. --- url: /ios/development/configurable-widgets.md --- # Configurable Widgets :::warning Experimental Feature Configurable widgets are experimental. Please [report any issues](https://github.com/callstackincubator/voltra/issues) you find. ::: Configurable widgets let users edit widget parameters in the native iOS Edit Widget sheet. Use them when a Dynamic Widget needs a few user-editable knobs, such as a label, unit, theme, or source. It requires iOS 17+, because Voltra wires it through `AppIntentConfiguration`. ## How it works 1. Define a Dynamic Widget module with a default export. 2. Add `entry` and `appIntent.parameters` to the widget config in `app.json`. 3. Read the selected values in your widget. 4. Let the user edit the widget from the iOS widget sheet. Each parameter has: - `name`: key that appears in `env.configuration` - `title`: label shown in the Edit Widget sheet - `default`: code-defined starting value before the user changes anything ## How to use it ```tsx import { Voltra, type WidgetEnvironment } from '@use-voltra/ios' type GreetingConfig = { label?: string } export default function GreetingWidget( _props: object, env: WidgetEnvironment = {} as WidgetEnvironment ) { const label = env.configuration?.label ?? 'Hello' return ( {label} Edit me from the widget sheet. ) } ``` Plugin config: ```json { "expo": { "plugins": [ [ "@use-voltra/ios-client", { "widgets": [ { "id": "greeting_widget", "entry": "./widgets/ios/greeting-widget.tsx", "displayName": "Greeting Widget", "description": "A Dynamic Widget with user-editable parameters", "supportedFamilies": ["systemSmall", "systemMedium"], "initialStatePath": "./widgets/ios/greeting-widget.tsx", "appIntent": { "parameters": [ { "name": "label", "title": "Label", "default": "Hello" } ] } } ] } ] ] } } ``` ## Using it in app 1. Build and install the app on a real iPhone. 2. Add the widget to Home Screen. 3. Long-press it and tap **Edit Widget**. 4. Change parameters. 5. Read values from `env.configuration` in your JSX. If you need more than one value, add more entries to `appIntent.parameters` and read each key from `env.configuration`. ## Notes - `appIntent` only wires up for Dynamic Widgets. - Defaults come from code, not from the native sheet. - There is no `export` field in app.json for Dynamic Widgets. - Use a real device to verify the Edit Widget flow. --- url: /ios/development/developing-live-activities.md --- # Developing Live Activities Voltra provides APIs that make building and testing Live Activities easier during development. ## Supported variants Live Activities in iOS can appear in different contexts, and Voltra supports defining UI variants for each of these contexts. For detailed information about Live Activity design guidelines, see the [Apple Human Interface Guidelines](https://developer.apple.com/design/human-interface-guidelines/live-activities). ### Lock Screen The `lockScreen` variant defines how your Live Activity appears on the lock screen. It can be either a ReactNode directly, or an object with content and optional styling: ```typescript const variants = { lockScreen: ( Your content here ), } ``` To customize the system Lock Screen chrome, pass an object with `content` and `activityBackgroundTint`: ```typescript const variants = { lockScreen: { activityBackgroundTint: '#101828', content: ( Your content here ), }, } ``` `activityBackgroundTint` is applied via SwiftUI's `activityBackgroundTint(...)` modifier on iOS. Voltra currently accepts the same color formats handled by the native iOS parser: - Hex colors such as `#RGB`, `#RGBA`, `#RRGGBB`, and `#RRGGBBAA` - `rgb(...)` and `rgba(...)` - `hsl(...)` and `hsla(...)` - Named colors: `red`, `orange`, `yellow`, `green`, `mint`, `teal`, `cyan`, `blue`, `indigo`, `purple`, `pink`, `brown`, `white`, `gray`, `black`, `clear`, `transparent`, `primary`, `secondary` Use `clear` or `transparent` to make the Live Activity background transparent. If the string cannot be parsed as one of those formats, iOS ignores the tint. ### Dynamic Island The `island` variant defines how your Live Activity appears in the Dynamic Island (available on iPhone 14 Pro and later). The Dynamic Island has three display states: - **Minimal**: A compact pill-shaped view that appears when the activity is in the background - **Compact**: A slightly larger view with leading and trailing regions - **Expanded**: A full-width view with center, leading, trailing, and bottom regions ```typescript const variants = { island: { keylineTint: '#10B981', // Optional tint color for the Dynamic Island keyline minimal: , compact: { leading: Order, trailing: Confirmed, }, expanded: { center: Order Confirmed, leading: , trailing: ETA: 15 min, bottom: Your order is being prepared, }, }, } ``` `keylineTint` uses the same iOS color parser and accepted formats as `activityBackgroundTint`. ### Supplemental Activity Families (iOS 18+, watchOS 11+) The `supplementalActivityFamilies` variant defines how your Live Activity appears on Apple Watch Smart Stack and CarPlay displays. This variant is optional and works seamlessly with your existing lock screen and Dynamic Island variants. ```typescript const variants = { lockScreen: ( {/* iPhone lock screen content */} ), island: { /* Dynamic Island variants for iPhone */ }, supplementalActivityFamilies: { small: ( 12 min ETA ), }, } ``` If `supplementalActivityFamilies.small` is not provided, Voltra will automatically construct it from your Dynamic Island `compact` variant by combining the leading and trailing content in an HStack. ## Limitations ### Animations and Live Updates There are specific constraints on how content can animate or update: - **Continuous Animations**: Custom continuous animations, such as rotating icons or elements moving along a path, are not supported. - **Smooth Updates**: Per-second "live" updates are only supported by specific components designed for this purpose: - `Timer`: For countdowns and stopwatches. - `LinearProgressView`: When used with `timerInterval`. - **Styling Trade-offs**: To enable smooth, system-driven animations (like a progress bar filling up in real-time), certain components may ignore custom styling properties (e.g., custom heights or thumb components) and fallback to standard system appearances. All other components only update their visual state when a new activity state is pushed from your application. ## useLiveActivity For React development, Voltra provides the `useLiveActivity` hook for integration with the component lifecycle and automatic updates during development. :::warning Unfortunately, iOS suspends background apps after approximately 30 seconds. This means that if you navigate away from your app (for example, to check the Dynamic Island or lock screen), live reload and auto-update functionality will be paused. ::: ```typescript import { Voltra } from '@use-voltra/ios' import { useLiveActivity } from '@use-voltra/ios-client' function OrderLiveActivity({ orderId, status }) { const variants = { lockScreen: ( {status === 'confirmed' ? 'Order Confirmed' : 'Order Ready'} {status === 'confirmed' ? 'Your order is being prepared' : 'Your order is ready for pickup'} {status === 'ready' && ( I'm Here )} ), } const { start, update, end, isActive } = useLiveActivity(variants, { activityName: `order-${orderId}`, autoStart: true, // Automatically start when component mounts autoUpdate: true, // Automatically update when variants change deepLinkUrl: `myapp://order/${orderId}`, }) // Manual control if needed const handleCancelOrder = async () => { await end() } return ( Live Activity: {isActive ? 'Active' : 'Inactive'}