From 56f71a0478122e146a5ff4b05a3cbe2a61d56d4b Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Thu, 13 Aug 2026 10:39:02 +0200 Subject: [PATCH 1/2] docs(v2): trim, dedupe, and split the docs for clarity Cut internal implementation details (wire formats, architecture diagrams, internal library names), removed content duplicated across pages, rewrote formal/marketing-toned passages into plain language, split overlong pages (styling, plugin-configuration, managing-ongoing-notifications), and fixed a broken code fence, an unreachable page, and a couple of dangling links found along the way. Also added expo-widgets as prior art and fixed the --save-dev flag in the Voltra CLI install command, which isn't supported by yarn or bun. --- website/docs/v2/android/api/_meta.json | 12 + .../v2/android/api/plugin-configuration.md | 140 +--------- .../android/api/widget-sizing-and-previews.md | 115 ++++++++ website/docs/v2/android/charts.md | 45 +--- .../docs/v2/android/components/interactive.md | 12 +- .../docs/v2/android/development/_meta.json | 10 + .../v2/android/development/custom-fonts.md | 10 +- .../android/development/developing-widgets.md | 11 +- .../v2/android/development/dynamic-colors.md | 8 - website/docs/v2/android/development/images.md | 15 +- .../managing-ongoing-notifications.md | 179 +------------ .../remote-ongoing-notifications.md | 170 ++++++++++++ .../development/server-driven-widgets.md | 32 +-- .../docs/v2/android/development/styling.md | 27 +- .../development/testing-and-previews.md | 9 +- .../development/widget-pre-rendering.md | 5 - website/docs/v2/android/introduction.md | 10 +- .../docs/v2/getting-started/installation.mdx | 10 +- .../docs/v2/getting-started/introduction.md | 15 +- .../docs/v2/getting-started/migration-v2.mdx | 76 +----- website/docs/v2/getting-started/prior-art.md | 4 + .../v2/getting-started/react-native-cli.mdx | 18 +- website/docs/v2/ios/api/configuration.md | 2 - .../docs/v2/ios/api/plugin-configuration.md | 5 +- website/docs/v2/ios/components/interactive.md | 23 +- website/docs/v2/ios/components/layout.md | 14 +- website/docs/v2/ios/components/overview.md | 2 +- website/docs/v2/ios/development/_meta.json | 10 + .../ios/development/configurable-widgets.md | 12 +- .../docs/v2/ios/development/custom-fonts.md | 118 +++++++++ .../development/developing-live-activities.md | 9 +- .../development/dynamic-live-activities.md | 18 +- website/docs/v2/ios/development/events.md | 26 +- .../docs/v2/ios/development/flexbox-layout.md | 18 +- website/docs/v2/ios/development/gradients.md | 118 +++++++++ website/docs/v2/ios/development/images.md | 45 +--- .../docs/v2/ios/development/interactions.md | 4 - .../managing-live-activities-locally.md | 44 +--- .../docs/v2/ios/development/performance.md | 29 +-- .../ios/development/server-driven-widgets.md | 29 +-- website/docs/v2/ios/development/styling.md | 246 +----------------- .../ios/development/widget-pre-rendering.md | 6 +- website/docs/v2/ios/introduction.md | 29 +-- website/docs/v2/ios/setup.mdx | 3 +- 44 files changed, 644 insertions(+), 1099 deletions(-) create mode 100644 website/docs/v2/android/api/_meta.json create mode 100644 website/docs/v2/android/api/widget-sizing-and-previews.md create mode 100644 website/docs/v2/android/development/remote-ongoing-notifications.md create mode 100644 website/docs/v2/ios/development/custom-fonts.md create mode 100644 website/docs/v2/ios/development/gradients.md diff --git a/website/docs/v2/android/api/_meta.json b/website/docs/v2/android/api/_meta.json new file mode 100644 index 00000000..802b96e2 --- /dev/null +++ b/website/docs/v2/android/api/_meta.json @@ -0,0 +1,12 @@ +[ + { + "type": "file", + "name": "plugin-configuration", + "label": "Plugin Configuration" + }, + { + "type": "file", + "name": "widget-sizing-and-previews", + "label": "Widget Sizing & Previews" + } +] diff --git a/website/docs/v2/android/api/plugin-configuration.md b/website/docs/v2/android/api/plugin-configuration.md index 7a496e61..c51bd91d 100644 --- a/website/docs/v2/android/api/plugin-configuration.md +++ b/website/docs/v2/android/api/plugin-configuration.md @@ -104,147 +104,11 @@ Fallback behavior: - 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) +For widget sizing math and widget-picker preview setup (`previewImage`, `previewLayout`), see [Widget Sizing & Previews](./widget-sizing-and-previews). ## 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](../development/widget-pre-rendering) for details on creating initial state files. +Use `initialStatePath` to bundle pre-rendered widget state for the first time a widget is added to the home screen. See [Widget Pre-rendering](../development/widget-pre-rendering) for details. ## Example Configuration diff --git a/website/docs/v2/android/api/widget-sizing-and-previews.md b/website/docs/v2/android/api/widget-sizing-and-previews.md new file mode 100644 index 00000000..99d98dfa --- /dev/null +++ b/website/docs/v2/android/api/widget-sizing-and-previews.md @@ -0,0 +1,115 @@ +# Widget Sizing & Previews (Android) + +## Widget Sizing + +### Grid Cells vs Density-Independent Pixels (dp) + +Android uses grid cells to define widget sizes. By default: + +- **minWidth/minHeight (dp) = (cellCount × 70) - 30** + +For example, 2 cells comes out to 110 dp, and 4 cells to 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: + +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, provide both: + +```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 uses `previewLayout` on Android 12+, falls back to `previewImage` on Android 11 and earlier, and shows actual widget content on the home screen via `initialStatePath` once available. diff --git a/website/docs/v2/android/charts.md b/website/docs/v2/android/charts.md index 309ef73b..e33f8a2c 100644 --- a/website/docs/v2/android/charts.md +++ b/website/docs/v2/android/charts.md @@ -2,10 +2,6 @@ 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. ::: @@ -49,9 +45,9 @@ type SectorDataPoint = { ## Marks -### BarMark +Pick the mark that fits your data: bars for comparing values across categories, lines for trends over time, areas for volume, points for sparse or scattered measurements, rules for reference lines, and sectors for pie/donut breakdowns. -Use bars when people need to compare values across categories. +### BarMark **Parameters:** @@ -79,8 +75,6 @@ Use bars when people need to compare values across categories. ### LineMark -Use a line when the shape of change matters more than individual columns. - **Parameters:** - `data` (ChartDataPoint[], required): The data points. @@ -109,8 +103,6 @@ Use a line when the shape of change matters more than individual columns. ### 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. @@ -134,8 +126,6 @@ Use an area chart when you want the overall volume or rise/fall pattern to read ### PointMark -Use points for sparse measurements, scatter plots, or to emphasize exact observations. - **Parameters:** - `data` (ChartDataPoint[], required): The data points. @@ -232,20 +222,6 @@ The `` container accepts these props in addition to the sta | `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: @@ -292,23 +268,6 @@ Mix mark types when one chart needs both context and emphasis, such as bars for ``` -## Sparkline / Minimal Style - -Hide axes for a clean, compact visualization: - -```tsx - - - -``` - ## Sizing Chart dimensions are read from the `style` prop: diff --git a/website/docs/v2/android/components/interactive.md b/website/docs/v2/android/components/interactive.md index 62d8de6e..ee934d71 100644 --- a/website/docs/v2/android/components/interactive.md +++ b/website/docs/v2/android/components/interactive.md @@ -2,9 +2,13 @@ User interface controls that respond to user interaction on Android widgets. +:::info +On Android, buttons, toggles, checkboxes, and radio buttons always open the application when clicked. You can provide a `deepLinkUrl` to open a specific screen instead of the main activity. +::: + ### Button -Standard button component. On Android, all buttons always open the application when clicked. You can provide a `deepLinkUrl` to open a specific screen. +Standard button component. **Parameters:** @@ -52,7 +56,7 @@ Most components support being clickable by setting the `pressable` prop (short n ### Switch -A toggle switch component. On Android, toggles always open the application when clicked. +A toggle switch component. **Parameters:** @@ -70,7 +74,7 @@ A toggle switch component. On Android, toggles always open the application when ### CheckBox -Standard checkbox component. On Android, checkboxes always open the application when clicked. +Standard checkbox component. **Parameters:** @@ -86,7 +90,7 @@ Standard checkbox component. On Android, checkboxes always open the application ### RadioButton -Standard radio button component. On Android, radio buttons always open the application when clicked. +Standard radio button component. **Parameters:** diff --git a/website/docs/v2/android/development/_meta.json b/website/docs/v2/android/development/_meta.json index 70d2c62b..02d113b5 100644 --- a/website/docs/v2/android/development/_meta.json +++ b/website/docs/v2/android/development/_meta.json @@ -14,11 +14,21 @@ "name": "managing-ongoing-notifications", "label": "Managing Ongoing Notifications" }, + { + "type": "file", + "name": "remote-ongoing-notifications", + "label": "Remote Ongoing Notifications" + }, { "type": "file", "name": "querying-active-widgets", "label": "Querying Active Widgets" }, + { + "type": "file", + "name": "testing-and-previews", + "label": "Testing and Previews" + }, { "type": "file", "name": "styling", diff --git a/website/docs/v2/android/development/custom-fonts.md b/website/docs/v2/android/development/custom-fonts.md index b898dcd5..58639180 100644 --- a/website/docs/v2/android/development/custom-fonts.md +++ b/website/docs/v2/android/development/custom-fonts.md @@ -54,15 +54,7 @@ import { VoltraAndroid } from '@use-voltra/android' 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. +Note that `renderAsBitmap` text is rasterized — it won't respond to system font size or accessibility settings. Use it only when a custom typeface is needed. ## Supported style properties diff --git a/website/docs/v2/android/development/developing-widgets.md b/website/docs/v2/android/development/developing-widgets.md index cadc5f07..c126294e 100644 --- a/website/docs/v2/android/development/developing-widgets.md +++ b/website/docs/v2/android/development/developing-widgets.md @@ -62,15 +62,6 @@ Unlike standard React Native or iOS Stacks, Android Glance layouts are more rest - **[Querying Active Widgets](./querying-active-widgets):** Detect active widget instances and their sizes. - **[Testing and Previews](./testing-and-previews):** Preview layouts within your app. -- **[Widget Picker Previews](../api/plugin-configuration#widget-picker-previews):** Configure how your widget appears in the Android widget picker. +- **[Widget Picker Previews](../api/widget-sizing-and-previews):** Configure how your widget appears in the Android widget picker. - **[Image Preloading](./image-preloading):** Cache remote images for use in widgets. - **[Widget Pre-rendering](./widget-pre-rendering):** 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](../api/plugin-configuration#widget-picker-previews) for configuration details and examples. diff --git a/website/docs/v2/android/development/dynamic-colors.md b/website/docs/v2/android/development/dynamic-colors.md index a498ac15..244cccb9 100644 --- a/website/docs/v2/android/development/dynamic-colors.md +++ b/website/docs/v2/android/development/dynamic-colors.md @@ -114,11 +114,3 @@ const content = ( ``` 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. diff --git a/website/docs/v2/android/development/images.md b/website/docs/v2/android/development/images.md index 289d88d8..510e0cd1 100644 --- a/website/docs/v2/android/development/images.md +++ b/website/docs/v2/android/development/images.md @@ -21,12 +21,7 @@ project-root/ │ └── 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. +Run `npx expo prebuild` to apply changes — images are detected during the build, sanitized to fit Android resource naming rules, and copied into the native Android project (SVGs are converted to Vector Drawables along the way). ### Naming & Sanitization @@ -70,13 +65,7 @@ import { VoltraAndroid } from '@use-voltra/android' ## 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. +For dynamic images from remote URLs, use Voltra's image preloading API to cache images locally. See [Image Preloading](./image-preloading) for how it works. Once images are preloaded, reference them using the key you provided: diff --git a/website/docs/v2/android/development/managing-ongoing-notifications.md b/website/docs/v2/android/development/managing-ongoing-notifications.md index 4bedd2be..f451c32b 100644 --- a/website/docs/v2/android/development/managing-ongoing-notifications.md +++ b/website/docs/v2/android/development/managing-ongoing-notifications.md @@ -13,22 +13,7 @@ Use this API when you want to: - 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. +Voltra also supports remote updates if your app receives push notifications in the background and forwards the payload to the ongoing notification APIs. See [Remote Ongoing Notifications](./remote-ongoing-notifications) for the server-side rendering API and full push integration guide. ## Before you start @@ -88,31 +73,6 @@ if (!granted) { } ``` -### 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: @@ -310,141 +270,6 @@ Action buttons currently: 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: @@ -516,7 +341,7 @@ Check device support first with `getAndroidOngoingNotificationCapabilities()` if ## Current limitations -- Remote updates require your own push delivery and background task integration. +- Remote updates require your own push delivery and background task integration. See [Remote Ongoing Notifications](./remote-ongoing-notifications). - 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. diff --git a/website/docs/v2/android/development/remote-ongoing-notifications.md b/website/docs/v2/android/development/remote-ongoing-notifications.md new file mode 100644 index 00000000..6d8e686a --- /dev/null +++ b/website/docs/v2/android/development/remote-ongoing-notifications.md @@ -0,0 +1,170 @@ +# Remote Ongoing Notifications (Android) + +:::warning Experimental API +Android ongoing notifications are **experimental**. The API may change in future releases. +::: + +Voltra can apply remote updates to an ongoing notification when your app receives a push notification and handles it in a background task. This builds on the local lifecycle API — see [Managing Ongoing Notifications](./managing-ongoing-notifications) first for creating the notification channel, requesting permission, and the start/update/stop APIs. + +## Server-side rendering support + +Voltra 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. + +## Register a background notification task + +You should use `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, so incoming pushes can update or stop an ongoing notification even when the app is backgrounded: + +```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 remote update flow + +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 + +Your payload's `data.voltraOngoingNotification` should 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}}" + } +} +``` + +Voltra 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()`. Create the channel on startup, and also ensure it exists again inside the background handler. diff --git a/website/docs/v2/android/development/server-driven-widgets.md b/website/docs/v2/android/development/server-driven-widgets.md index a62bd396..4f63a3a9 100644 --- a/website/docs/v2/android/development/server-driven-widgets.md +++ b/website/docs/v2/android/development/server-driven-widgets.md @@ -126,7 +126,7 @@ 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. +Widgets on Android are part of the main app binary, so the WorkManager background worker can access credential storage directly. Voltra credentials are encrypted at rest on-device. ### Setting credentials @@ -171,11 +171,7 @@ Server-driven widgets can display a native refresh button that lets users trigge } ``` -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()`. -::: +When enabled, a small circular button (↻) appears in the top-right corner of the widget. Tapping it performs an inline HTTP fetch and pushes the update directly to the widget—all without waiting for the next WorkManager cycle. ## Resize handling @@ -251,30 +247,6 @@ The handler uses the required `platform` query parameter to route requests to th 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. diff --git a/website/docs/v2/android/development/styling.md b/website/docs/v2/android/development/styling.md index b79af171..2db9e6be 100644 --- a/website/docs/v2/android/development/styling.md +++ b/website/docs/v2/android/development/styling.md @@ -48,32 +48,7 @@ In addition to general styles, `Image` components support: ## 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](./dynamic-colors) for the full role list and server-rendering behavior. +Android widgets can use semantic Material color roles (`AndroidDynamicColors.*`) so they follow the system palette even when the app isn't running. See [Dynamic Colors](./dynamic-colors) for the full role list, examples, and server-rendering behavior. ## Gradient Backgrounds diff --git a/website/docs/v2/android/development/testing-and-previews.md b/website/docs/v2/android/development/testing-and-previews.md index 1cc142fb..7662df26 100644 --- a/website/docs/v2/android/development/testing-and-previews.md +++ b/website/docs/v2/android/development/testing-and-previews.md @@ -5,7 +5,7 @@ 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](../api/plugin-configuration#widget-picker-previews). +This page covers in-app previews for development. For widget picker previews, see [Widget Sizing & Previews](../api/widget-sizing-and-previews). ## VoltraWidgetPreview @@ -66,12 +66,7 @@ import { VoltraView } from '@use-voltra/android-client' ## 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. +The Android preview components use the **actual native Glance renderers** under the hood, so the preview matches production rendering — layout, styling, and component mapping are all identical to what shows on the home screen. :::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. diff --git a/website/docs/v2/android/development/widget-pre-rendering.md b/website/docs/v2/android/development/widget-pre-rendering.md index 58216e50..a2ebefd5 100644 --- a/website/docs/v2/android/development/widget-pre-rendering.md +++ b/website/docs/v2/android/development/widget-pre-rendering.md @@ -54,12 +54,7 @@ export default InitialWeatherWidget `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. diff --git a/website/docs/v2/android/introduction.md b/website/docs/v2/android/introduction.md index 586bc1cc..ad9af870 100644 --- a/website/docs/v2/android/introduction.md +++ b/website/docs/v2/android/introduction.md @@ -1,10 +1,10 @@ # Android Introduction :::warning Experimental Support -Android support is **experimental**. Although it should work just fine, the API may change. Stay vigilant. +Android support is **experimental**. The API may still change before it's stable. ::: -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. +Voltra lets you build Android Home Screen widgets with JSX. Under the hood it uses Jetpack Compose Glance, and Voltra's primitives map directly to Glance components. ## Widgets on Android @@ -52,12 +52,6 @@ const MyWidget = () => ( ) ``` -## 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. diff --git a/website/docs/v2/getting-started/installation.mdx b/website/docs/v2/getting-started/installation.mdx index a5f5b66d..2daa6a20 100644 --- a/website/docs/v2/getting-started/installation.mdx +++ b/website/docs/v2/getting-started/installation.mdx @@ -32,15 +32,9 @@ Use these in your backend or SSR service-not in the React Native app bundle: - **Android payloads:** `@use-voltra/android-server` - **Cross-platform widget HTTP handlers:** `@use-voltra/server` -## 2. Import patterns +Note that pre-render files such as `initialStatePath` should import JSX from `@use-voltra/ios` or `@use-voltra/android` directly, not from the client packages. -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 +## 2. Next Steps After installing packages, configure the Voltra Expo plugin for each platform you support: diff --git a/website/docs/v2/getting-started/introduction.md b/website/docs/v2/getting-started/introduction.md index 4e475e21..8ded58f6 100644 --- a/website/docs/v2/getting-started/introduction.md +++ b/website/docs/v2/getting-started/introduction.md @@ -1,20 +1,9 @@ # 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 is a library that brings new "platforms" to React Native. Creating features like iOS Live Activities, Dynamic Island layouts, or Android Home Screen Widgets normally requires 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 @@ -38,7 +27,7 @@ await startLiveActivity({ }) ``` -If you prefer using the hook API (`useLiveActivity`), you'll get live reloads for live activities, with changes appearing in milliseconds without manual restarts. +If you prefer using the hook API, check out [`useLiveActivity`](/ios/development/developing-live-activities#useliveactivity) for integrating live activities with the component lifecycle and automatic updates during development. ## Server-side updates via push notifications diff --git a/website/docs/v2/getting-started/migration-v2.mdx b/website/docs/v2/getting-started/migration-v2.mdx index b6e6a4aa..c5d94a1b 100644 --- a/website/docs/v2/getting-started/migration-v2.mdx +++ b/website/docs/v2/getting-started/migration-v2.mdx @@ -7,64 +7,26 @@ 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. +Upgrading to v2 requires updating package installs, Expo plugin configuration, and some API usage. This guide walks through those changes. -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. +## Recommended migration order -This guide walks through the package, import, and configuration changes you need to make when upgrading to v2. +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. ## 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` +- **Package split:** old `voltra`, `voltra/client`, `voltra/server`, `voltra/android`, etc. paths are replaced by scoped packages (`@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:** iOS primitives come from `@use-voltra/ios` as `Voltra`; Android primitives, color tokens, and ongoing notification JSX come from `@use-voltra/android` as `VoltraAndroid`, `AndroidDynamicColors`, and `AndroidOngoingNotification`. +- **Client APIs stay in `*-client`:** runtime APIs that run inside the React Native app (starting/updating Live Activities, updating or pinning widgets, preview components, event listeners and hooks) come from `@use-voltra/ios-client` and `@use-voltra/android-client`. +- **Server APIs are platform-specific:** use `@use-voltra/ios-server` and `@use-voltra/android-server` for server-side rendering, and `@use-voltra/server` only for cross-platform widget HTTP handlers. -### 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. +See the sections below for exact import diffs. ## Installation changes @@ -222,16 +184,6 @@ Use: 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: diff --git a/website/docs/v2/getting-started/prior-art.md b/website/docs/v2/getting-started/prior-art.md index ee1f45f5..6194493d 100644 --- a/website/docs/v2/getting-started/prior-art.md +++ b/website/docs/v2/getting-started/prior-art.md @@ -12,6 +12,10 @@ The core concept of describing SwiftUI layouts through JSON configuration was pi 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. +### Expo Widgets + +Voltra spawns a secondary JS runtime to drive native widget and Live Activity rendering. That's not just a theoretical option — Expo's own [`expo-widgets`](https://docs.expo.dev/versions/latest/sdk/widgets/) module does the same thing in production: when the system asks for a widget timeline or starts a Live Activity, your component runs in a separate JS runtime and produces a native layout tree. Seeing this pattern work at Expo-SDK scale, across both iOS and Android, was a strong signal that it's a practical foundation to build on, not just a corner case. + ## Thank You We extend our gratitude to the following organizations and individuals: diff --git a/website/docs/v2/getting-started/react-native-cli.mdx b/website/docs/v2/getting-started/react-native-cli.mdx index 9ef08be6..a735cd96 100644 --- a/website/docs/v2/getting-started/react-native-cli.mdx +++ b/website/docs/v2/getting-started/react-native-cli.mdx @@ -22,12 +22,9 @@ Install the same native Voltra packages you would use in Expo for the platforms ### Voltra CLI - + -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) -- [Android Expo config options](/android/api/plugin-configuration) +Then create a `voltra.config.ts` file. Most properties under `ios` and `android` are the same as the Expo config equivalents (see [Configuration](#configuration) below). Minimal example: @@ -99,15 +96,6 @@ For the full config shape, see [`VoltraConfig` and related types in source](http ## Using Voltra CLI -Every time you change `voltra.config.ts`, reapply the native project changes: - - +Every time you change `voltra.config.ts`, reapply the native project changes (see the command above). 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) and [Android](/android/introduction). diff --git a/website/docs/v2/ios/api/configuration.md b/website/docs/v2/ios/api/configuration.md index 86d9bcae..ffed91f0 100644 --- a/website/docs/v2/ios/api/configuration.md +++ b/website/docs/v2/ios/api/configuration.md @@ -57,8 +57,6 @@ The dismissal policy applies to both programmatic ending (`stopLiveActivity`) an ## 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. diff --git a/website/docs/v2/ios/api/plugin-configuration.md b/website/docs/v2/ios/api/plugin-configuration.md index 833a9bed..de8a648f 100644 --- a/website/docs/v2/ios/api/plugin-configuration.md +++ b/website/docs/v2/ios/api/plugin-configuration.md @@ -70,10 +70,7 @@ iOS deployment target version for the widget extension. If not provided, default 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 +Useful for matching existing provisioning profiles, credentials, or naming conventions (e.g., when migrating from `@bacons/apple-targets`). **Type:** `string` **Default:** `"{AppName}LiveActivity"` diff --git a/website/docs/v2/ios/components/interactive.md b/website/docs/v2/ios/components/interactive.md index 2d67394f..738d4159 100644 --- a/website/docs/v2/ios/components/interactive.md +++ b/website/docs/v2/ios/components/interactive.md @@ -45,7 +45,7 @@ const subscription = addVoltraListener('interaction', (event) => { ### Examples -**Styled button:** +**Basic button:** ```tsx @@ -64,14 +64,6 @@ const subscription = addVoltraListener('interaction', (event) => { ``` -**Compact button:** - -```tsx - - - -``` - --- ## Link @@ -121,17 +113,6 @@ Link automatically normalizes URLs using your app's URL scheme: ``` -**External link:** - -```tsx - - - - Visit Support Site - - -``` - ### When to use Link vs Button | Feature | Link | Button | @@ -142,8 +123,6 @@ Link automatically normalizes URLs using your app's URL scheme: | **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 diff --git a/website/docs/v2/ios/components/layout.md b/website/docs/v2/ios/components/layout.md index d27fabe8..d13f3458 100644 --- a/website/docs/v2/ios/components/layout.md +++ b/website/docs/v2/ios/components/layout.md @@ -7,7 +7,7 @@ Components that arrange other elements or provide structural grouping. 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 +2. **`offsetX`/`offsetY` styles** - Fine-tune individual element positions after alignment (positive `offsetX` moves right, positive `offsetY` moves down) Each stack type has different alignment options based on its layout direction. @@ -68,8 +68,6 @@ A depth-based stack container that overlays its children on top of each other. U #### 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 @@ -98,19 +96,11 @@ In SwiftUI (and Voltra), positioning works differently than CSS. The `alignment` ``` -:::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](../development/flexbox-layout) guide for comprehensive documentation. -::: +A flexible container that **always uses flexbox layout**, unlike VStack and HStack which use native SwiftUI stacks. See the [Flexbox Layout](../development/flexbox-layout) guide for details. **Style Properties:** diff --git a/website/docs/v2/ios/components/overview.md b/website/docs/v2/ios/components/overview.md index 00bf97d4..67eee39d 100644 --- a/website/docs/v2/ios/components/overview.md +++ b/website/docs/v2/ios/components/overview.md @@ -1,6 +1,6 @@ # 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. +Voltra provides SwiftUI primitives with JSX bindings, allowing developers to create rich, interactive Live Activities using React/JSX syntax. ## Getting Started diff --git a/website/docs/v2/ios/development/_meta.json b/website/docs/v2/ios/development/_meta.json index a42f6ed9..332ef615 100644 --- a/website/docs/v2/ios/development/_meta.json +++ b/website/docs/v2/ios/development/_meta.json @@ -59,6 +59,16 @@ "name": "styling", "label": "Styling" }, + { + "type": "file", + "name": "gradients", + "label": "Gradients" + }, + { + "type": "file", + "name": "custom-fonts", + "label": "Custom Fonts" + }, { "type": "file", "name": "flexbox-layout", diff --git a/website/docs/v2/ios/development/configurable-widgets.md b/website/docs/v2/ios/development/configurable-widgets.md index 7813a079..e3c151b6 100644 --- a/website/docs/v2/ios/development/configurable-widgets.md +++ b/website/docs/v2/ios/development/configurable-widgets.md @@ -12,8 +12,8 @@ It requires iOS 17+, because Voltra wires it through `AppIntentConfiguration`. 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. +3. Read the selected values from `env.configuration` in your widget's JSX. +4. Build and install the app on a real iPhone, add the widget to the Home Screen, then long-press it and tap **Edit Widget** to change parameters — your widget re-reads `env.configuration` with the new values. Each parameter has: @@ -83,14 +83,6 @@ Plugin config: } ``` -## 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 diff --git a/website/docs/v2/ios/development/custom-fonts.md b/website/docs/v2/ios/development/custom-fonts.md new file mode 100644 index 00000000..e0dccea7 --- /dev/null +++ b/website/docs/v2/ios/development/custom-fonts.md @@ -0,0 +1,118 @@ +# Custom Fonts + +Voltra supports custom fonts through the `fontFamily` style property. + +## Adding Custom Fonts to Your Project + +Voltra supports custom fonts in your Live Activities and Widgets through two main methods: + +### 1. Using Voltra's Font Configuration (Recommended) + +The simplest way is to specify fonts directly in the Voltra plugin configuration. This follows the same pattern as `expo-font`: + +```json +{ + "expo": { + "plugins": [ + [ + "@use-voltra/ios-client", + { + "groupIdentifier": "group.com.example.app", + "fonts": ["./assets/fonts", "./assets/custom-font.ttf"] + } + ] + ] + } +} +``` + +The `fonts` array can include: +- Individual font files: `"./assets/fonts/CustomFont.ttf"` +- Entire directories: `"./assets/fonts"` (all fonts in the directory will be included) +- Supported formats: `.ttf`, `.otf`, `.woff`, `.woff2` + +### 2. Adding Fonts Manually in Xcode + +For non-Expo projects or if you prefer manual configuration, you can add fonts directly to your Xcode project: + +1. Add your font files (`.otf` or `.ttf`) to your Xcode project +2. Ensure they're included in the Live Activity target's "Copy Bundle Resources" build phase +3. Add the font file names to your `Info.plist` under the `UIAppFonts` key for Live Activity target + +For detailed instructions, see Apple's documentation on [Applying custom fonts to text](https://developer.apple.com/documentation/swiftui/applying-custom-fonts-to-text). + +## Using Custom Fonts + +Once your fonts are added to the project, you can use them with the `fontFamily` style property: + +```tsx +import { Voltra } from '@use-voltra/ios' + +const element = ( + + Text with Custom Font + +) +``` + +:::tip Font Family Names + +The font family name you use in `fontFamily` should match the font's PostScript name, not the file name. You can find the PostScript name: +- In the Font Book app on macOS +- Using online tools like [fontdrop.info](https://fontdrop.info) +- In Xcode's font picker + +For example, the font file `Inter-Bold.ttf` has the PostScript name `Inter-Bold`. + +::: + +## Font Weight with Custom Fonts + +When using `fontFamily`, the `fontWeight` style property is ignored since you typically specify the exact font variant (e.g., `Inter-Bold`, `Inter-Regular`). If you need different weights, add multiple font files and specify the complete font name: + +```tsx +// Regular weight + + Regular Text + + +// Bold weight + + Bold Text + +``` + +## Example with Google Fonts + +If you're using Google Fonts via `@expo-google-fonts`, they work seamlessly with Voltra: + +```json +{ + "expo": { + "plugins": [ + [ + "expo-font", + { + "fonts": ["node_modules/@expo-google-fonts/inter/Inter_400Regular.ttf"] + } + ] + ] + } +} +``` + +```tsx + + Text using Google Font + +``` + +:::note System Font Fallback +If `fontFamily` is not specified or the font cannot be found, Voltra will fall back to the system font with the specified `fontWeight`. +::: diff --git a/website/docs/v2/ios/development/developing-live-activities.md b/website/docs/v2/ios/development/developing-live-activities.md index 37878e68..2abdd47b 100644 --- a/website/docs/v2/ios/development/developing-live-activities.md +++ b/website/docs/v2/ios/development/developing-live-activities.md @@ -172,11 +172,7 @@ function OrderLiveActivity({ orderId, status }) { ## VoltraView Component -For testing and development, Voltra provides a `VoltraView` component that renders Voltra JSX components directly in your React Native app. This is useful for: - -- Testing component layouts before deploying to Live Activities -- Handling user interactions in development -- Previewing how your Live Activity will look +For testing and development, Voltra provides a `VoltraView` component that renders Voltra JSX components directly in your React Native app, so you can test layouts and interactions before deploying to a real Live Activity. ```tsx import { Voltra } from '@use-voltra/ios' @@ -213,8 +209,6 @@ function MyComponent() { - `style`: React Native style for the container - `onInteraction`: Callback for user interactions with buttons/toggles -``` - **Hook Options:** - `activityName`: Name of the Live Activity @@ -228,4 +222,3 @@ function MyComponent() { - `update()`: Update the Live Activity - `end()`: Stop the Live Activity - `isActive`: Boolean indicating if the Live Activity is currently active -``` diff --git a/website/docs/v2/ios/development/dynamic-live-activities.md b/website/docs/v2/ios/development/dynamic-live-activities.md index bf13206e..88477291 100644 --- a/website/docs/v2/ios/development/dynamic-live-activities.md +++ b/website/docs/v2/ios/development/dynamic-live-activities.md @@ -2,11 +2,11 @@ :::warning Experimental feature -Dynamic Live Activities and their public APIs are experimental in V1. They are separate from both legacy Live Activities and [Dynamic Widgets](./dynamic-widgets): they have their own configuration collection, generated ActivityKit types, Metro route, runtime registry, and release bundles. +Dynamic Live Activities and their public APIs are experimental in V1. They're a separate engine from both legacy Live Activities and [Dynamic Widgets](./dynamic-widgets). ::: -A legacy Live Activity sends a fully rendered Voltra UI in every update. A Dynamic Live Activity bundles its rendering definition in the app and receives only a complete, JSON-compatible props record. This keeps update payloads small, but makes the definition ID and its props contract a compatibility boundary between the app and the push producer. +A legacy Live Activity sends a fully rendered Voltra UI in every update. A Dynamic Live Activity bundles its rendering definition in the app and receives only a complete, JSON-compatible props record. Keep payloads small — but remember the definition ID and its props are basically a contract with whoever sends the push. ## Configure a definition @@ -163,10 +163,10 @@ const subscription = addVoltraListener('dynamicLiveActivityRenderFailed', (event ## Rollout and compatibility -- An older app can only accept a push-to-start for a definition whose generated attributes type and ActivityKit configuration it contains. Supporting another Dynamic Live Activity is not enough. -- An undeclared definition has no generated type, configuration, catalog entry, or bundle; local APIs reject it and ActivityKit does not create it remotely. -- Keep a definition bundled until all activities using it have ended. End active instances before removing its declaration. -- Treat an ID as a versioned rendering-and-props contract. Use a new ID such as `order_finished_v2` for breaking prop changes; incompatible props under the old ID are producer error. -- A broadcast channel cannot tailor format per recipient. Use Dynamic Live Activities only on channels whose recipients support the same definition; otherwise retain the legacy format. -- A missing/corrupt bundle or late renderer failure leaves a remotely created activity active but empty, records a failure, and can recover on a later successful update. V1 does not cache the last successful UI. -- ActivityKit can create duplicate remote starts with the same name; V1 does not reconcile them. This differs from local replacement behavior. +- An older app version can only accept a push-to-start for a definition it actually has bundled. Having some other Dynamic Live Activity isn't enough — the specific definition ID needs to exist in that build. +- If a definition isn't declared in the app, both local APIs and remote ActivityKit pushes for it will fail. +- Don't remove a definition's declaration while activities using it might still be running — end those first. +- Treat each definition ID as a versioned contract for its rendering and props. If you need to make a breaking change to the props shape, ship it under a new ID (e.g. `order_finished_v2`) instead of changing the old one. +- Broadcast channels send the same payload to every subscriber, so only use Dynamic Live Activities on a channel if every recipient supports that same definition — otherwise stick with the legacy format for that channel. +- If the bundle is missing or corrupt, or the renderer fails, a remotely-started activity can end up active but empty. It'll recover on the next successful update, but V1 doesn't cache the last good UI in the meantime. +- ActivityKit can create duplicate remote starts under the same name, and V1 doesn't dedupe them — unlike local starts, where an existing activity with the same name gets replaced. diff --git a/website/docs/v2/ios/development/events.md b/website/docs/v2/ios/development/events.md index 205a87ab..092df4e7 100644 --- a/website/docs/v2/ios/development/events.md +++ b/website/docs/v2/ios/development/events.md @@ -107,31 +107,7 @@ For more information about using push tokens for starting Live Activity remotely ## User interactions -When users interact with buttons or toggles in your Live Activity, Voltra emits events that allow your app to respond to these interactions. This works even when your app isn't running, thanks to Apple's AppIntents framework. - -### Listening for interactions - -Subscribe to interaction events using `addVoltraListener` with the `'interaction'` event type: - -```typescript -import { addVoltraListener } from '@use-voltra/ios-client' - -const subscription = addVoltraListener('interaction', (event) => { - console.log('Component interacted:', event.identifier) - console.log('Payload:', event.payload) - - // Handle the interaction based on the identifier - if (event.identifier === 'contact-driver') { - // Open contact screen - } else if (event.identifier === 'notifications-toggle') { - // Handle toggle state change - } -}) - -subscription.remove() -``` - -For detailed information about handling interactions, including component identifiers, deep linking, and app lifecycle considerations, see the [interactions guide](./interactions.md). +When users interact with buttons or toggles in your Live Activity, Voltra emits an `'interaction'` event via `addVoltraListener` — this works even when your app isn't running. For the full example, component identifiers, deep linking, and app lifecycle considerations, see the [interactions guide](./interactions.md). ## Best practices diff --git a/website/docs/v2/ios/development/flexbox-layout.md b/website/docs/v2/ios/development/flexbox-layout.md index 423e14dc..3ee21d9c 100644 --- a/website/docs/v2/ios/development/flexbox-layout.md +++ b/website/docs/v2/ios/development/flexbox-layout.md @@ -55,23 +55,7 @@ The following React Native flexbox properties are **not supported** in Voltra: ## Differences from React Native -If you're coming from React Native, keep these differences in mind: - -### Flexbox is opt-in - -In React Native, every `View` uses flexbox by default. In Voltra, only `Voltra.View` uses flexbox. Other containers (`VStack`, `HStack`) use native SwiftUI layout. - -### Gap is single-axis only - -React Native supports `gap`, `rowGap`, and `columnGap`. Voltra only supports `gap`, which applies spacing between children along the main axis (the direction of `flexDirection`). - -### No flex wrap - -React Native supports `flexWrap: 'wrap'` to flow items onto multiple lines. Voltra does not — all items stay on a single line and will overflow or shrink. - -### No percentage dimensions - -React Native allows `width: '50%'` and similar percentage values. In Voltra, use `flexGrow` for proportional sizing instead. +If you're coming from React Native, the biggest difference is that flexbox is opt-in: in React Native every `View` uses flexbox by default, but in Voltra only `Voltra.View` does — other containers (`VStack`, `HStack`) use native SwiftUI layout instead. The rest of the differences are the unsupported/limited properties listed in the tables above (single-axis `gap`, no `flexWrap`, no percentage dimensions). ## Next Steps diff --git a/website/docs/v2/ios/development/gradients.md b/website/docs/v2/ios/development/gradients.md new file mode 100644 index 00000000..3bea6722 --- /dev/null +++ b/website/docs/v2/ios/development/gradients.md @@ -0,0 +1,118 @@ +# Gradients + +The `backgroundColor` style property accepts CSS gradient strings in addition to solid colors. Gradients are rendered natively using SwiftUI gradient modifiers and are automatically clipped by `borderRadius`. + +Invalid or unsupported gradient syntax is parsed in **strict mode** and results in **no gradient background** (instead of silent best-effort fallback). + +## Linear gradients + +```tsx +// Named direction + + +// Diagonal + + +// Angle in degrees/radians/turns + + +``` + +Supported directions: `to right`, `to left`, `to top`, `to bottom`, `to top right`, `to top left`, `to bottom right`, `to bottom left`. + +## Color stops + +Explicit percentage positions are supported: + +```tsx + +``` + +When positions are omitted, Voltra applies CSS-like stop fix-up: +- First and last unspecified stops default to `0%` and `100%`. +- Unspecified stops between explicit anchors are linearly interpolated. +- Non-monotonic explicit positions are clamped forward. + +## RGBA colors inside gradients + +```tsx + +``` + +## Radial gradients + +```tsx + + + +``` + +:::note +Radial gradients are rendered with geometry-aware radii computed from the view size (`closest-side`, `farthest-side`, `closest-corner`, `farthest-corner`). + +For `ellipse`, SwiftUI does not provide native elliptical radial gradients. Voltra approximates ellipse behavior by scaling a circular radial gradient. +::: + +## Conic gradients + +```tsx + + +``` + +## With border radius + +Gradients are clipped by `borderRadius` automatically — no extra configuration needed: + +```tsx + + Rounded gradient card + +``` + +## Solid colors still work unchanged + +Passing a plain color string to `backgroundColor` continues to work exactly as before: + +```tsx + + + +``` + +## Comparison with `` component + +| Feature | `backgroundColor` gradient | `` component | +|---|---|---| +| CSS string syntax | ✓ | — | +| Named directions (`to right`) | ✓ (physical direction) | ✓ | +| Angle units | `deg`, `rad`, `turn` | ✓ via `{x,y}` | +| `{x, y}` coordinate control | — | ✓ | +| Stop positions | `linear/radial`: `%`, `conic`: `%` + angle units | ✓ via `locations` prop | +| Multi-position stops (`red 20% 40%`) | ✓ | — | +| `radial-gradient` | ✓ (`circle` / approximated `ellipse`) | — | +| `conic-gradient` | ✓ (`from` + `at`) | — | +| Strict invalid syntax handling | ✓ (fails closed) | — | +| Dithering | — | ✓ | +| Children layered on top | ✓ (as background) | ✓ (as container) | + +Use `backgroundColor` gradient strings for convenience and web-style syntax. Use `` when you need precise `{x, y}` coordinate control or dithering. + +## Scope and exclusions + +- Supported core syntax: `linear-gradient(...)`, `radial-gradient(...)`, `conic-gradient(...)`. +- Not supported in this parser: `repeating-linear-gradient(...)`, `repeating-radial-gradient(...)`, `repeating-conic-gradient(...)`. diff --git a/website/docs/v2/ios/development/images.md b/website/docs/v2/ios/development/images.md index 39a7f82e..d1de2855 100644 --- a/website/docs/v2/ios/development/images.md +++ b/website/docs/v2/ios/development/images.md @@ -32,14 +32,7 @@ project-root/ │ └── background-pattern.png ``` -Here's how build-time asset copying works: - -1. Images in `/assets/voltra/` are automatically detected during build -2. Each image is validated to be under 4KB (ActivityKit limit) -3. Images are copied to the Live Activity extension's `Assets.xcassets` -4. Xcode generates proper `.imageset` directories and metadata - -You can then reference these images using their assetName: +Images must be under 4KB (the ActivityKit limit) — anything larger fails the build. You can then reference these images using their assetName: ```tsx @@ -48,41 +41,7 @@ You can then reference these images using their assetName: ## Runtime preloading -For dynamic images from remote URLs or inline SVG markup, use Voltra's image preloading API to cache images in App Group shared storage. SVG inputs are rasterized to PNG before storage. - -The image preloading system works by: - -1. Downloading images from URLs to App Group shared storage -2. Validating that images are under the 4KB ActivityKit limit -3. Making images available to Live Activities via the `assetName` property -4. Providing APIs to reload existing Live Activities when new images are available - -Once images are preloaded, reference them using the `assetName` property: - -```typescript -import { Voltra } from '@use-voltra/ios' - -function MusicPlayerLiveActivity({ song }) { - return { - lockScreen: ( - - - - {song.title} - - - {song.artist} - - - ) - } -} -``` - -For detailed API documentation, see [Image Preloading](image-preloading). +For dynamic images from remote URLs or inline SVG markup, use Voltra's image preloading API to cache images (also subject to the 4KB limit) in App Group shared storage, then reference them the same way via `assetName`. See [Image Preloading](image-preloading) for the full API and a usage example. ## Comparison table diff --git a/website/docs/v2/ios/development/interactions.md b/website/docs/v2/ios/development/interactions.md index 3a86c569..b0e4f1d4 100644 --- a/website/docs/v2/ios/development/interactions.md +++ b/website/docs/v2/ios/development/interactions.md @@ -14,8 +14,6 @@ ActivityKit provides a limited set of interactions with Live Activities. The onl - **Toggles**: Interactive toggle switches that allow users to change boolean states. Like buttons, toggles use AppIntents and are supported across all Live Activity contexts (iOS 17.0+). -These interactions are powered by Apple's AppIntents framework and SwiftUI's Link component, which enable Live Activities to communicate with your app even when it's not running. - ## Handling interactions When a user interacts with a button or toggle in your Live Activity, Voltra automatically emits an event containing the identifier of the component that was interacted with. This allows your app to respond appropriately to specific user actions. @@ -116,8 +114,6 @@ For styled navigation buttons, wrap a Link with custom children to achieve butto ## Limitations -While Live Activities provide powerful interaction capabilities, there are some limitations to be aware of: - - **Limited interactable elements**: Only buttons and toggles are supported as interactive components. Other UI elements like text fields, sliders, or custom controls are not available in Live Activities. - **iOS version requirements**: Interactive buttons and toggles require iOS 17.0+. On iOS 16.x, these components will render but will not be interactive. diff --git a/website/docs/v2/ios/development/managing-live-activities-locally.md b/website/docs/v2/ios/development/managing-live-activities-locally.md index 25b6366f..5c7d8380 100644 --- a/website/docs/v2/ios/development/managing-live-activities-locally.md +++ b/website/docs/v2/ios/development/managing-live-activities-locally.md @@ -2,22 +2,13 @@ Live Activities are dynamic interfaces that display real-time information on iOS devices. Voltra provides a comprehensive set of APIs for managing the complete lifecycle of Live Activities directly from your React Native app, without requiring server-side infrastructure. -## Overview - -Managing Live Activities locally involves four main phases: - -1. **Starting** a Live Activity with initial content and configuration -2. **Updating** the content and configuration as data changes -3. **Monitoring** state changes and user interactions -4. **Stopping** the Live Activity when it's no longer needed - Voltra offers both imperative APIs for direct control and React hooks for seamless integration with your components. This page documents the legacy server-rendered engine. For the separate experimental engine that bundles a definition with the app and updates it with props, see [Dynamic Live Activities](./dynamic-live-activities). ## Imperative APIs -The imperative APIs provide direct, programmatic control over Live Activities. These are the core functions you'll use to manage Live Activity lifecycles. +The imperative APIs provide direct, programmatic control over Live Activities. These are the core functions you'll use to manage Live Activity lifecycles. For the shape of the `variants` object used below, see [Supported variants](./developing-live-activities#supported-variants). ### Starting Live Activities @@ -148,24 +139,18 @@ if (isLiveActivityActive('order-123')) { #### Platform detection ```typescript -import { isGlassSupported, useIsHeadless } from '@use-voltra/ios-client' +import { isGlassSupported } from '@use-voltra/ios-client' function App() { - const isHeadless = useIsHeadless() - // Check if the device supports Liquid Glass (iOS 26+) if (isGlassSupported()) { // Use Liquid Glass features } - - // Check if app was launched in background - if (isHeadless) { - // App was launched in background (e.g., from Live Activity interaction) - // Perform background tasks without UI - } } ``` +Voltra also exposes `useIsHeadless()` for detecting when your app was launched in the background (e.g. to handle a push token refresh without mounting your UI). See [Handling background execution](./server-side-updates#handling-background-execution) for the full explanation and example. + #### Ending all Live Activities Use `endAllLiveActivities()` to immediately end all active Live Activities in your app. @@ -280,20 +265,7 @@ Requires `enablePushNotifications: true` in the Voltra plugin and the Broadcast ## Best practices -### Activity lifecycle management - -- Always provide meaningful `activityId` values for re-binding on app restart -- Clean up Live Activities when they're no longer relevant -- Use appropriate dismissal policies based on your use case - -### Performance considerations - -- Keep UI variants lightweight and avoid complex component trees -- Use appropriate relevance scores to ensure important activities are visible -- Set reasonable stale dates to prevent accumulation of outdated activities - -### User experience - -- Provide deep link URLs for navigation when Live Activities are tapped -- Use meaningful compact and minimal variants for Dynamic Island -- Consider dismissal timing carefully - users may want to see final states +- Provide an `activityName` when starting a Live Activity, so you can re-bind to it (e.g. via `isLiveActivityActive`) after an app restart. +- Set a `staleDate` so iOS can automatically dismiss activities you forget to end (e.g. after a delivery is likely already complete). +- Provide a `deepLinkUrl` so tapping the Live Activity takes users somewhere useful in your app. +- Pick a `dismissalPolicy` deliberately — `{ after: N }` is useful when you want users to see a final state (like "Delivered") before it disappears. diff --git a/website/docs/v2/ios/development/performance.md b/website/docs/v2/ios/development/performance.md index 4b9b401a..d06c4cbc 100644 --- a/website/docs/v2/ios/development/performance.md +++ b/website/docs/v2/ios/development/performance.md @@ -4,7 +4,7 @@ Voltra provides automatic optimizations to help you create efficient Live Activi ## Element deduplication -Reuse JSX elements by creating them once, storing them in variables, and reusing them across your JSX tree. Voltra automatically detects duplicate element references and stores them only once in the payload, using lightweight references (`{ $r: index }`) for subsequent occurrences. +Reuse JSX elements by creating them once, storing them in variables, and reusing them across your JSX tree, instead of recreating the same element inline in multiple places. Voltra automatically detects duplicate element references and shrinks the payload size as a result — no extra code required. ```tsx import { Voltra } from '@use-voltra/ios' @@ -32,33 +32,6 @@ const variants = { } ``` -**Payload result:** - -```json -{ - "v": 2, - "e": [ - { "t": 1, "p": { "onPress": "action" }, "c": "Click me" }, - { "t": 2, "p": { "name": "star.fill" } } - ], - "ls": { - "t": 11, - "c": [{ "t": 0, "c": "Order Status" }, { "$r": 0 }, { "$r": 1 }] - }, - "isl_min": { - "t": 12, - "c": [{ "$r": 1 }, { "$r": 0 }] - } -} -``` - -**Benefits:** - -- Eliminates redundant element serialization -- Can significantly reduce payload size for complex UIs -- Works across different Live Activity variants -- Automatic - no additional code required - ```tsx // ❌ Avoid: Creating separate element instances const variants = { diff --git a/website/docs/v2/ios/development/server-driven-widgets.md b/website/docs/v2/ios/development/server-driven-widgets.md index 0b9a6b9c..9d856fdc 100644 --- a/website/docs/v2/ios/development/server-driven-widgets.md +++ b/website/docs/v2/ios/development/server-driven-widgets.md @@ -244,35 +244,12 @@ See [Widget pre-rendering](./widget-pre-rendering) for details on creating initi 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. ::: -## Architecture overview - -``` -┌─────────────────┐ setWidgetServerCredentials() ┌──────────────────┐ -│ React Native │ ──────────────────────────────────► │ Shared Keychain │ -│ (main app) │ └──────────────────┘ -└─────────────────┘ │ - │ reads token - ▼ -┌─────────────────┐ GET ?widgetId=X&platform=ios&family=Y&theme=Z ┌──────────────────┐ -│ WidgetKit │ ──────────────────────────────────► │ Your Server │ -│ (extension) │ ◄────────────────────────────────── │ (Voltra SSR) │ -└─────────────────┘ JSON payload └──────────────────┘ - │ - ▼ - Home Screen Widget -``` - -WidgetKit manages the scheduling and calls your server at the configured interval. The widget extension reads credentials from the Shared Keychain, makes the HTTP request, and renders the response payload. - ## Error handling and retries -When a server fetch fails, the widget extension falls back to the last successfully fetched data (or the initial state if no data has been fetched yet). WidgetKit schedules a retry after 15 minutes. +When a server fetch fails, the widget extension falls back to the last successfully fetched data (or the initial state if no data has been fetched yet), and WidgetKit schedules a retry after 15 minutes. This applies to network errors/timeouts, non-2xx server errors, and empty responses. -- **Network error / timeout:** The widget falls back to cached content and retries in 15 minutes. -- **Server errors (non-2xx):** Same fallback behavior — cached content is shown and a retry is scheduled in 15 minutes. -- **Empty response:** Treated as an error; cached content is displayed. -- **Parse errors:** If the server returns a 2xx response but the JSON can't be parsed into a valid widget tree, the cached data from the previous successful fetch is preserved (not overwritten). The widget continues to show the last known good content. +Parse errors are handled slightly differently: if the server returns a 2xx response but the JSON can't be parsed into a valid widget tree, the cached data from the previous successful fetch is preserved (not overwritten), so the widget keeps showing the last known good content. :::note -Unlike Android's WorkManager which retries with exponential backoff, iOS WidgetKit uses its own timeline-based scheduling. After a failed fetch, the timeline provider falls back to local data and schedules a retry in 15 minutes. WidgetKit may also throttle updates based on battery level and widget visibility. +WidgetKit may also throttle updates based on battery level and widget visibility. ::: diff --git a/website/docs/v2/ios/development/styling.md b/website/docs/v2/ios/development/styling.md index 473a3a26..6cd6e30f 100644 --- a/website/docs/v2/ios/development/styling.md +++ b/website/docs/v2/ios/development/styling.md @@ -1,11 +1,9 @@ # Styling -You can style Voltra components using React Native-style `style` props. The `style` prop works with a limited set of React Native properties that are automatically converted to SwiftUI modifiers under the hood. +You can style Voltra components using React Native-style `style` props. Voltra supports a limited subset of React Native style properties — enough to get you productive quickly if you already know RN styling. ## React Native style prop -Voltra supports a limited subset of React Native style properties. When you pass a `style` prop to a Voltra component, these properties are automatically converted to SwiftUI modifiers under the hood. This makes it easy to get started if you're familiar with React Native styling. - ### Supported properties The following React Native style properties are supported: @@ -31,7 +29,7 @@ The following React Native style properties are supported: **Style:** -- `backgroundColor` - Background color (hex strings, color names, or CSS gradient strings — see [Gradients](#gradients)) +- `backgroundColor` - Background color (hex strings, color names, or CSS gradient strings — see [Gradients](./gradients)) - `opacity` - Opacity value between 0 and 1 - `borderRadius` - Corner radius value - `borderWidth` - Border width @@ -48,7 +46,7 @@ The following React Native style properties are supported: - `fontSize` - Font size (maps to `font` modifier) - `fontWeight` - Font weight (e.g., `'600'`, `'bold'`, `'regular'`) -- `fontFamily` - Custom font family name (see [Custom Fonts](#custom-fonts) section below) +- `fontFamily` - Custom font family name (see [Custom Fonts](./custom-fonts)) - `color` - Text color (maps to `foregroundStyle` modifier) - `letterSpacing` - Spacing between characters (maps to `kerning` modifier) - `fontVariant` - Font variant array (e.g., `['small-caps', 'tabular-nums']`). Supported values: @@ -133,240 +131,4 @@ const element = ( ) ``` -## Gradients - -The `backgroundColor` style property accepts CSS gradient strings in addition to solid colors. Gradients are rendered natively using SwiftUI gradient modifiers and are automatically clipped by `borderRadius`. - -Invalid or unsupported gradient syntax is parsed in **strict mode** and results in **no gradient background** (instead of silent best-effort fallback). - -### Linear gradients - -```tsx -// Named direction - - -// Diagonal - - -// Angle in degrees/radians/turns - - -``` - -Supported directions: `to right`, `to left`, `to top`, `to bottom`, `to top right`, `to top left`, `to bottom right`, `to bottom left`. - -### Color stops - -Explicit percentage positions are supported: - -```tsx - -``` - -When positions are omitted, Voltra applies CSS-like stop fix-up: -- First and last unspecified stops default to `0%` and `100%`. -- Unspecified stops between explicit anchors are linearly interpolated. -- Non-monotonic explicit positions are clamped forward. - -### RGBA colors inside gradients - -```tsx - -``` - -### Radial gradients - -```tsx - - - -``` - -:::note -Radial gradients are rendered with geometry-aware radii computed from the view size (`closest-side`, `farthest-side`, `closest-corner`, `farthest-corner`). - -For `ellipse`, SwiftUI does not provide native elliptical radial gradients. Voltra approximates ellipse behavior by scaling a circular radial gradient. -::: - -### Conic gradients - -```tsx - - -``` - -### With border radius - -Gradients are clipped by `borderRadius` automatically — no extra configuration needed: - -```tsx - - Rounded gradient card - -``` - -### Solid colors still work unchanged - -Passing a plain color string to `backgroundColor` continues to work exactly as before: - -```tsx - - - -``` - -### Comparison with `` component - -| Feature | `backgroundColor` gradient | `` component | -|---|---|---| -| CSS string syntax | ✓ | — | -| Named directions (`to right`) | ✓ (physical direction) | ✓ | -| Angle units | `deg`, `rad`, `turn` | ✓ via `{x,y}` | -| `{x, y}` coordinate control | — | ✓ | -| Stop positions | `linear/radial`: `%`, `conic`: `%` + angle units | ✓ via `locations` prop | -| Multi-position stops (`red 20% 40%`) | ✓ | — | -| `radial-gradient` | ✓ (`circle` / approximated `ellipse`) | — | -| `conic-gradient` | ✓ (`from` + `at`) | — | -| Strict invalid syntax handling | ✓ (fails closed) | — | -| Dithering | — | ✓ | -| Children layered on top | ✓ (as background) | ✓ (as container) | - -Use `backgroundColor` gradient strings for convenience and web-style syntax. Use `` when you need precise `{x, y}` coordinate control or dithering. - -### Scope and exclusions - -- Supported core syntax: `linear-gradient(...)`, `radial-gradient(...)`, `conic-gradient(...)`. -- Not supported in this parser: `repeating-linear-gradient(...)`, `repeating-radial-gradient(...)`, `repeating-conic-gradient(...)`. - -## Custom Fonts - -Voltra supports custom fonts through the `fontFamily` style property. - -### Adding Custom Fonts to Your Project - -Voltra supports custom fonts in your Live Activities and Widgets through two main methods: - -#### 1. Using Voltra's Font Configuration (Recommended) - -The simplest way is to specify fonts directly in the Voltra plugin configuration. This follows the same pattern as `expo-font`: - -```json -{ - "expo": { - "plugins": [ - [ - "@use-voltra/ios-client", - { - "groupIdentifier": "group.com.example.app", - "fonts": ["./assets/fonts", "./assets/custom-font.ttf"] - } - ] - ] - } -} -``` - -The `fonts` array can include: -- Individual font files: `"./assets/fonts/CustomFont.ttf"` -- Entire directories: `"./assets/fonts"` (all fonts in the directory will be included) -- Supported formats: `.ttf`, `.otf`, `.woff`, `.woff2` - -#### 2. Adding Fonts Manually in Xcode - -For non-Expo projects or if you prefer manual configuration, you can add fonts directly to your Xcode project: - -1. Add your font files (`.otf` or `.ttf`) to your Xcode project -2. Ensure they're included in the Live Activity target's "Copy Bundle Resources" build phase -3. Add the font file names to your `Info.plist` under the `UIAppFonts` key for Live Activity target - -For detailed instructions, see Apple's documentation on [Applying custom fonts to text](https://developer.apple.com/documentation/swiftui/applying-custom-fonts-to-text). - -### Using Custom Fonts - -Once your fonts are added to the project, you can use them with the `fontFamily` style property: - -```tsx -import { Voltra } from '@use-voltra/ios' - -const element = ( - - Text with Custom Font - -) -``` - -:::tip Font Family Names - -The font family name you use in `fontFamily` should match the font's PostScript name, not the file name. You can find the PostScript name: -- In the Font Book app on macOS -- Using online tools like [fontdrop.info](https://fontdrop.info) -- In Xcode's font picker - -For example, the font file `Inter-Bold.ttf` has the PostScript name `Inter-Bold`. - -::: - -### Font Weight with Custom Fonts - -When using `fontFamily`, the `fontWeight` style property is ignored since you typically specify the exact font variant (e.g., `Inter-Bold`, `Inter-Regular`). If you need different weights, add multiple font files and specify the complete font name: - -```tsx -// Regular weight - - Regular Text - - -// Bold weight - - Bold Text - -``` - -### Example with Google Fonts - -If you're using Google Fonts via `@expo-google-fonts`, they work seamlessly with Voltra: - -```json -{ - "expo": { - "plugins": [ - [ - "expo-font", - { - "fonts": ["node_modules/@expo-google-fonts/inter/Inter_400Regular.ttf"] - } - ] - ] - } -} -``` - -```tsx - - Text using Google Font - -``` - -:::note System Font Fallback -If `fontFamily` is not specified or the font cannot be found, Voltra will fall back to the system font with the specified `fontWeight`. -::: +For gradients and custom fonts, see the dedicated [Gradients](./gradients) and [Custom Fonts](./custom-fonts) pages. diff --git a/website/docs/v2/ios/development/widget-pre-rendering.md b/website/docs/v2/ios/development/widget-pre-rendering.md index 8baba6be..9b042b14 100644 --- a/website/docs/v2/ios/development/widget-pre-rendering.md +++ b/website/docs/v2/ios/development/widget-pre-rendering.md @@ -57,13 +57,9 @@ export default initialState ``` :::info -`initialStatePath` files are **not** part of your React Native app bundle. They run in Node.js during prebuild. Import `Voltra` and types from `@use-voltra/ios`, not `@use-voltra/ios-client` — the client package pulls in native modules that are unavailable in the prebuild sandbox. +`initialStatePath` files are **not** part of your React Native app bundle. Voltra transpiles them with Babel and runs them in Node.js during prebuild to generate the initial states bundled into the iOS app. Import `Voltra` and types from `@use-voltra/ios`, not `@use-voltra/ios-client` — the client package pulls in native modules that are unavailable in the prebuild sandbox. ::: -## Build Process - -During build time, Voltra transpiles your widget files with Babel and executes them in a Node.js environment to generate initial states that are bundled into the iOS app. - ## Limitations - **Node.js Environment**: Code runs in Node.js, not in React Native or iOS diff --git a/website/docs/v2/ios/introduction.md b/website/docs/v2/ios/introduction.md index e851122f..46c374bd 100644 --- a/website/docs/v2/ios/introduction.md +++ b/website/docs/v2/ios/introduction.md @@ -1,12 +1,6 @@ # 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: +This page shows what building a Live Activity with Voltra looks like: you write JSX using Voltra components, and it gets converted to SwiftUI automatically. ```tsx import { Voltra } from '@use-voltra/ios' @@ -31,25 +25,6 @@ await startLiveActivity({ 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 - - ), -}) -``` +Voltra also supports updating Live Activities from your server via push notifications — see [Server-side updates](./development/server-side-updates). You're ready to dive into the [setup guide](./setup) and get started with live activities in your app. diff --git a/website/docs/v2/ios/setup.mdx b/website/docs/v2/ios/setup.mdx index b07bb3d3..57cec642 100644 --- a/website/docs/v2/ios/setup.mdx +++ b/website/docs/v2/ios/setup.mdx @@ -49,6 +49,7 @@ Now let's create a simple "Hello World" live activity. Create a new component: ```tsx import React from 'react' +import { View, TouchableOpacity, Text } from 'react-native' import { Voltra } from '@use-voltra/ios' import { startLiveActivity } from '@use-voltra/ios-client' @@ -75,5 +76,3 @@ function HelloWorldActivity() { ) } ``` - -You've created your first live activity with Voltra. The JSX you write gets automatically converted to SwiftUI and displayed natively on iOS. From d563c77312264ab47a7a7c8a761387e9bf30e25d Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Thu, 13 Aug 2026 10:54:58 +0200 Subject: [PATCH 2/2] docs(v2): fix Expo SDK wording in prior art --- website/docs/v2/getting-started/prior-art.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/v2/getting-started/prior-art.md b/website/docs/v2/getting-started/prior-art.md index 6194493d..43d1d4f3 100644 --- a/website/docs/v2/getting-started/prior-art.md +++ b/website/docs/v2/getting-started/prior-art.md @@ -14,7 +14,7 @@ The insight of combining JavaScript-driven development with iOS Live Activities ### Expo Widgets -Voltra spawns a secondary JS runtime to drive native widget and Live Activity rendering. That's not just a theoretical option — Expo's own [`expo-widgets`](https://docs.expo.dev/versions/latest/sdk/widgets/) module does the same thing in production: when the system asks for a widget timeline or starts a Live Activity, your component runs in a separate JS runtime and produces a native layout tree. Seeing this pattern work at Expo-SDK scale, across both iOS and Android, was a strong signal that it's a practical foundation to build on, not just a corner case. +Voltra spawns a secondary JS runtime to drive native widget and Live Activity rendering. That's not just a theoretical option — Expo's own [`expo-widgets`](https://docs.expo.dev/versions/latest/sdk/widgets/) module does the same thing in production: when the system asks for a widget timeline or starts a Live Activity, your component runs in a separate JS runtime and produces a native layout tree. Seeing this pattern work at Expo SDK scale, across both iOS and Android, was a strong signal that it's a practical foundation to build on, not just a corner case. ## Thank You