Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ jobs:

- name: Run Kotlin native tests
working-directory: example/android
run: ./gradlew :voltra:testDebugUnitTest
run: ./gradlew :use-voltra_android-client:testDebugUnitTest

native-swift-test:
name: '[Swift] Test'
Expand Down
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,4 @@ npm-debug.log

## Build
/build
/packages/voltra/ios/.build/
/packages/ios-client/ios/.build/
24 changes: 14 additions & 10 deletions .oxlintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -322,32 +322,36 @@
}
},
{
"files": ["packages/expo-plugin/src/**/*.{ts,tsx,js,jsx}"],
"files": [
"packages/expo-plugin/src/**/*.{ts,tsx,js,jsx}",
"packages/ios-client/expo-plugin/src/**/*.{ts,tsx,js,jsx}",
"packages/android-client/expo-plugin/src/**/*.{ts,tsx,js,jsx}"
],
"rules": {
"no-restricted-imports": [
"error",
{
"paths": [
{ "name": "voltra", "message": "@use-voltra/expo-plugin must stay installation-time only." },
{ "name": "@use-voltra/core", "message": "@use-voltra/expo-plugin must stay installation-time only." },
{ "name": "@use-voltra/server", "message": "@use-voltra/expo-plugin must stay installation-time only." },
{ "name": "@use-voltra/ios", "message": "@use-voltra/expo-plugin must stay installation-time only." },
{ "name": "@use-voltra/android", "message": "@use-voltra/expo-plugin must stay installation-time only." },
{ "name": "voltra", "message": "Voltra config plugins must stay installation-time only." },
{ "name": "@use-voltra/core", "message": "Voltra config plugins must stay installation-time only." },
{ "name": "@use-voltra/server", "message": "Voltra config plugins must stay installation-time only." },
{ "name": "@use-voltra/ios", "message": "Voltra config plugins must stay installation-time only." },
{ "name": "@use-voltra/android", "message": "Voltra config plugins must stay installation-time only." },
{
"name": "@use-voltra/ios-client",
"message": "@use-voltra/expo-plugin must stay installation-time only."
"message": "Voltra config plugins must stay installation-time only."
},
{
"name": "@use-voltra/android-client",
"message": "@use-voltra/expo-plugin must stay installation-time only."
"message": "Voltra config plugins must stay installation-time only."
},
{
"name": "@use-voltra/ios-server",
"message": "@use-voltra/expo-plugin must stay installation-time only."
"message": "Voltra config plugins must stay installation-time only."
},
{
"name": "@use-voltra/android-server",
"message": "@use-voltra/expo-plugin must stay installation-time only."
"message": "Voltra config plugins must stay installation-time only."
},
{
"name": "@use-voltra/ios/client",
Expand Down
88 changes: 52 additions & 36 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,52 +12,55 @@ All work on Voltra happens directly on GitHub. Contributors send pull requests w

1. Fork the repo and create your branch from `main` (a guide on [how to fork a repository](https://help.github.com/articles/fork-a-repo/)).
2. Run `npm install` to install all required dependencies.
3. Build the plugin: `npm run build --workspace @use-voltra/expo-plugin`.
3. Build the plugins: `npm run build:plugin`.
4. Now you are ready to make changes.

## Architecture overview

### JS/TS code structure

The JavaScript/TypeScript code has **two separate entry points** that must be maintained as independent boundaries:
The JavaScript/TypeScript code is split by platform and runtime boundary:

- **Client entry (`packages/voltra/src/index.ts`)**: React Native code that runs in the app. Exports JSX components, hooks, and the imperative API for managing Live Activities.
- **Server entry (`packages/voltra/src/server.ts`)**: Node.js code for rendering Voltra components to string payloads. Used for server-side rendering and push notification payloads.
- **iOS JSX (`packages/ios`)**: `Voltra` components and `@use-voltra/ios/server` rendering helpers.
- **iOS client (`packages/ios-client`)**: React Native runtime APIs, previews, and the iOS Expo config plugin.
- **Android JSX (`packages/android`)**: `VoltraAndroid` components and `@use-voltra/android/server` rendering helpers.
- **Android client (`packages/android-client`)**: React Native runtime APIs and the Android Expo config plugin.
- **Server packages (`packages/ios-server`, `packages/android-server`, `packages/server`)**: Node-only rendering and HTTP widget handlers.

⚠️ **Important**: These two entry points must remain separate. Client code should not import server-only dependencies, and server code should not import React Native-specific modules.
⚠️ **Important**: Keep client and server entry points separate. App bundles must not import server-only packages.

### Expo config plugin (`packages/expo-plugin/`)
### Expo config plugins

The Expo plugin in `packages/expo-plugin/src/` handles all Xcode project setup during `expo prebuild`:
- `packages/expo-plugin/` — shared validation, locale picking, prerender utilities
- `packages/ios-client/expo-plugin/` — iOS Live Activities, widget extension, Xcode setup
- `packages/android-client/expo-plugin/` — Android widgets and manifest

The iOS plugin handles Xcode project setup during `expo prebuild`:

1. **Creates the widget extension target** with proper build settings
2. **Copies template files** from `ios-files/` (widget bundle, assets, Info.plist) into the extension target
3. **Configures CocoaPods** to include the `VoltraWidget` subspec in the extension target
4. **Sets up entitlements** for App Groups (optional, for event forwarding)
5. **Configures push notifications** (optional)

### Swift code distribution (`ios/`)
### Swift code distribution (`packages/ios-client/ios/`)

Voltra's Swift sources for the iOS React Native client live under `@use-voltra/ios-client` and ship as **CocoaPods** pods:

Voltra's Swift code lives in `ios/` and is distributed as a **CocoaPods package** with multiple subspecs:
- **`Voltra.podspec`**: React Native Turbo Module + Fabric view + shared Swift UI (`ios/app/`, `ios/ui/`, `ios/shared/`).
- **`VoltraWidget.podspec`**: Widget extension Swift (`ios/ui/`, `ios/shared/`, `ios/target/`).

```ruby
# From ios/Voltra.podspec
s.subspec 'Core' do |ss|
# React Native bridge module (auto-linked by Expo)
ss.source_files = ["app/**/*.swift", "shared/**/*.swift", "ui/**/*.swift"]
end

s.subspec 'Widget' do |ss|
# Widget extension code (used by Live Activity target)
ss.source_files = ["shared/**/*.swift", "ui/**/*.swift", "target/**/*.swift"]
end
# From packages/ios-client/Voltra.podspec (paths relative to the podspec)
s.source_files = [
"ios/app/**/*.swift",
"ios/app/**/*.m",
"ios/app/**/*.mm",
"ios/ui/**/*.swift",
"ios/shared/**/*.swift",
]
```

- **`Core` subspec**: Contains the React Native module (`app/`), shared code (`shared/`), and UI components (`ui/`). Auto-linked by Expo in the main app.
- **`Widget` subspec**: Contains shared code, UI components, and widget-specific files (`target/`). Used by the Live Activity extension target.

This separation ensures the widget extension doesn't include unnecessary React Native dependencies.

### Template files (`ios-files/`)

Files in `ios-files/` are copied by the config plugin into the generated widget extension:
Expand All @@ -68,28 +71,41 @@ Files in `ios-files/` are copied by the config plugin into the generated widget

## Props synchronization

Component props are kept in sync between TypeScript and Swift via a **custom code generator**. The single source of truth is:
Component props are kept in sync across TypeScript, Swift, and Kotlin via a **custom code generator**. The single source of truth is:

```
data/components.json
packages/voltra/data/components.json
```

This file defines all components, their parameters, types, and short names used for payload compression.
This file defines all components, their parameters, platform availability, and short names used for payload compression.

### Running the generator

```sh
npm run generate
```

This generates:
This runs the generator (`packages/voltra/generator/generate-types.ts`).

The generator filters components by platform (`swiftAvailability` for iOS, `androidAvailability` for Android) and writes outputs to the packages that own each runtime:

| Output | Path |
| --- | --- |
| **TypeScript prop types (iOS)** | `packages/ios/src/jsx/props/*.ts` |
| **TypeScript prop types (Android)** | `packages/android/src/jsx/props/*.ts` |
| **Swift parameter structs** | `packages/ios-client/ios/ui/Generated/Parameters/*.swift` |
| **Kotlin parameter structs** | `packages/android-client/android/src/main/java/voltra/models/parameters/*Parameters.kt` |
| **iOS component ID mappings (TS)** | `packages/ios/src/payload/component-ids.ts` |
| **Android component ID mappings (TS)** | `packages/android/src/payload/component-ids.ts` |
| **iOS component ID mappings (Swift)** | `packages/ios-client/ios/shared/ComponentTypeID.swift` |
| **Android component ID mappings (Kotlin)** | `packages/android-client/android/src/main/java/voltra/payload/ComponentTypeID.kt` |
| **Short name mappings (TS)** | `packages/core/src/payload/short-names.ts` |
| **Short name mappings (Swift)** | `packages/ios-client/ios/shared/ShortNames.swift` |
| **Short name mappings (Kotlin)** | `packages/android-client/android/src/main/java/voltra/generated/ShortNames.kt` |

- **TypeScript prop types**: `src/jsx/props/*.ts`
- **Swift parameter structs**: `ios/ui/Generated/Parameters/*.swift`
- **Component ID mappings**: `src/payload/component-ids.ts` and `ios/shared/ComponentTypeID.swift`
- **Short name mappings**: `src/payload/short-names.ts` and `ios/shared/ShortNames.swift`
After generation, the script formats JS (iOS and Android packages), Kotlin (`@use-voltra/android-client`), and Swift (`@use-voltra/ios-client`).

⚠️ **Important**: When adding new components or modifying props, always update `data/components.json` first, then run the generator. Do not manually edit generated files (marked with `.generated`).
⚠️ **Important**: When adding new components or modifying props, always update `packages/voltra/data/components.json` first, then run the generator. Do not manually edit generated files (directories include a `.generated` marker file). Component `.tsx` files that call `createVoltraComponent` are still written by hand in `packages/ios/src/jsx/` and `packages/android/src/jsx/`.

## Payload size budget

Expand Down Expand Up @@ -126,7 +142,7 @@ The payload schema has a version number to support forward compatibility. When t
The version is defined in two places that must stay in sync:

- **TypeScript**: `packages/voltra/src/renderer/renderer.ts` → `VOLTRA_PAYLOAD_VERSION`
- **Swift**: `packages/voltra/ios/shared/VoltraPayloadMigrator.swift` → `currentVersion`
- **Swift**: `packages/ios-client/ios/shared/VoltraPayloadMigrator.swift` → `currentVersion`

### When to increment the version

Expand Down Expand Up @@ -181,7 +197,7 @@ The `example/` directory contains an Expo app for testing changes.

```sh
# 1) Build the plugin
npm run build --workspace @use-voltra/expo-plugin
npm run build:plugin

# 2) Install example dependencies
(cd example && npm install)
Expand All @@ -193,7 +209,7 @@ npm run build --workspace @use-voltra/expo-plugin
(cd example && npx expo run:ios)
```

If iterating on the plugin, rebuild after each change in `packages/expo-plugin/src/`.
If iterating on a plugin, rebuild after each change under `packages/expo-plugin/src/`, `packages/ios-client/expo-plugin/src/`, or `packages/android-client/expo-plugin/src/`.

### Running tests

Expand Down
47 changes: 36 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Voltra turns React Native JSX into SwiftUI and Jetpack Compose Glance so you can

- **Type-Safe & Developer-Friendly**: The Voltra schema, hooks, and examples ship with TypeScript definitions, tests, and docs so AI coding agents stay productive.

- **Works With Your Setup**: Compatible with Expo Dev Client and bare React Native projects. The config plugin automatically wires native extension targets for you.
- **Works With Your Setup**: Compatible with Expo Dev Client and bare React Native projects. Platform config plugins wire native extension targets for you.

## Documentation

Expand All @@ -38,18 +38,44 @@ The documentation is available at [use-voltra.dev](https://use-voltra.dev). You
> [!NOTE]
> The library isn't supported in Expo Go. To set it up correctly, you need to use [Expo Dev Client](https://docs.expo.dev/versions/latest/sdk/dev-client/).

Install the package:
Install the client package for each platform you need:

```sh
npm install voltra
# iOS
npm install @use-voltra/ios-client

# Android
npm install @use-voltra/android-client
```

Add the config plugin to your `app.json`:
Add the Expo plugins to your `app.json`:

```json
{
"expo": {
"plugins": ["voltra"]
"plugins": [
[
"@use-voltra/ios-client",
{
"groupIdentifier": "group.your.bundle.identifier",
"enablePushNotifications": true
}
],
[
"@use-voltra/android-client",
{
"widgets": [
{
"id": "my_widget",
"displayName": "My Widget",
"description": "A Voltra widget",
"targetCellWidth": 2,
"targetCellHeight": 2
}
]
}
]
]
}
}
```
Expand All @@ -61,8 +87,7 @@ See the [documentation](https://use-voltra.dev/getting-started/installation) for
## Quick example

```tsx
import { useLiveActivity } from 'voltra/client'
import { Voltra } from 'voltra'
import { useLiveActivity, Voltra } from '@use-voltra/ios-client'

export function OrderTracker({ orderId }: { orderId: string }) {
const ui = (
Expand Down Expand Up @@ -94,7 +119,7 @@ Voltra is a cross-platform library that supports:

## Authors

`voltra` is an open source collaboration between [Saúl Sharma](https://github.com/saulsharma) and [Szymon Chmal](https://github.com/szymonchmal) at [Callstack][callstack-readme-with-love].
Voltra is an open source collaboration between [Saúl Sharma](https://github.com/saulsharma) and [Szymon Chmal](https://github.com/szymonchmal) at [Callstack][callstack-readme-with-love].

If you think it's cool, please star it 🌟. This project will always remain free to use.

Expand All @@ -103,9 +128,9 @@ If you think it's cool, please star it 🌟. This project will always remain fre
Like the project? ⚛️ [Join the Callstack team](https://callstack.com/careers/?utm_campaign=Senior_RN&utm_source=github&utm_medium=readme) who does amazing stuff for clients and drives React Native Open Source! 🔥

[callstack-readme-with-love]: https://callstack.com/?utm_source=github.com&utm_medium=referral&utm_campaign=voltra&utm_term=readme-with-love
[license-badge]: https://img.shields.io/npm/l/voltra?style=for-the-badge
[license-badge]: https://img.shields.io/npm/l/@use-voltra/ios?style=for-the-badge
[license]: https://github.com/callstackincubator/voltra/blob/main/LICENSE.txt
[npm-downloads-badge]: https://img.shields.io/npm/dm/voltra?style=for-the-badge
[npm-downloads]: https://www.npmjs.com/package/voltra
[npm-downloads-badge]: https://img.shields.io/npm/dm/@use-voltra/ios-client?style=for-the-badge
[npm-downloads]: https://www.npmjs.com/package/@use-voltra/ios-client
[prs-welcome-badge]: https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=for-the-badge
[prs-welcome]: ./CONTRIBUTING.md
2 changes: 1 addition & 1 deletion example/__tests__/ios/live-activity-snapshots.harness.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { screen } from '@react-native-harness/ui'
import { View } from 'react-native'
import { describe, expect, render, test } from 'react-native-harness'
import { VoltraLiveActivityPreview } from 'voltra/client'
import { VoltraLiveActivityPreview } from '@use-voltra/ios-client'

import {
BasicLiveActivityUI,
Expand Down
2 changes: 1 addition & 1 deletion example/__tests__/ios/widget-snapshots.harness.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { screen } from '@react-native-harness/ui'
import { View } from 'react-native'
import { afterAll, beforeAll, describe, expect, Mock, render, spyOn, test } from 'react-native-harness'
import { VoltraWidgetPreview } from 'voltra/client'
import { VoltraWidgetPreview } from '@use-voltra/ios-client'

import { IosWeatherWidget } from '../../widgets/ios/IosWeatherWidget'
import { SAMPLE_WEATHER_DATA } from '../../widgets/weather-types'
Expand Down
21 changes: 14 additions & 7 deletions example/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,11 @@
},
"plugins": [
[
"voltra",
"@use-voltra/ios-client",
{
"groupIdentifier": "group.callstackincubator.voltraexample",
"keychainGroup": "$(AppIdentifierPrefix)group.callstackincubator.voltraexample",
"enablePushNotifications": true,
"liveActivity": {
"supplementalActivityFamilies": ["small"]
},
"widgets": [
{
"id": "weather",
Expand Down Expand Up @@ -70,9 +67,19 @@
}
}
],
"android": {
"enableNotifications": true,
"widgets": [
"fonts": [
"@expo-google-fonts/merriweather/400Regular/Merriweather_400Regular.ttf",
"@expo-google-fonts/merriweather/700Bold/Merriweather_700Bold.ttf",
"@expo-google-fonts/pacifico/400Regular/Pacifico_400Regular.ttf",
"@expo-google-fonts/press-start-2p/400Regular/PressStart2P_400Regular.ttf"
]
}
],
[
"@use-voltra/android-client",
{
"enableNotifications": true,
"widgets": [
{
"id": "voltra",
"displayName": {
Expand Down
2 changes: 1 addition & 1 deletion example/components/ActiveWidgetsAndroidCard.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { useCallback, useEffect, useState } from 'react'
import { ActivityIndicator, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native'
import { getActiveWidgets, WidgetInfo } from 'voltra/android/client'
import { getActiveWidgets, WidgetInfo } from '@use-voltra/android-client'

import { Card } from './Card'

Expand Down
2 changes: 1 addition & 1 deletion example/components/ActiveWidgetsIOSCard.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { useCallback, useEffect, useState } from 'react'
import { ActivityIndicator, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native'
import { getActiveWidgets, WidgetInfo } from 'voltra/client'
import { getActiveWidgets, WidgetInfo } from '@use-voltra/ios-client'

import { Card } from './Card'

Expand Down
2 changes: 1 addition & 1 deletion example/components/live-activities/BasicLiveActivityUI.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react'
import { Voltra } from 'voltra'
import { Voltra } from '@use-voltra/ios'

export function BasicLiveActivityUI() {
return (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react'
import { Voltra } from 'voltra'
import { Voltra } from '@use-voltra/ios'

type CompassLiveActivityUIProps = {
/** Heading in degrees (0-360, where 0 = North) */
Expand Down
Loading
Loading